Multi select upstream providers to send spots to #95

This commit is contained in:
Ian Renton
2026-09-27 14:37:48 +01:00
parent 0cfe92db01
commit e6f0f737d0
13 changed files with 178 additions and 148 deletions
+23 -21
View File
@@ -23,6 +23,7 @@ info:
* **Breaking change:** POST `/spot` now expects `activities` (a list) and `activity_refs` in the `spot` object, rather than `sig` and `sig_refs`.
* **Breaking change:** In the `/options` response, `sigs` has been renamed to `activities`, and within each activity, `sig_type` has been renamed to `activity_type`.
* **Breaking change:** In the `/status` response, `sig_ref_data_providers` has been renamed to `activity_ref_data_providers`, and within each provider, `sig_name` has been renamed to `activity_name`.
* **Breaking change:** In the POST `/spot` `handling` object, `submit_upstream` and `upstream_provider` have been replaced by `upstream_providers`, a list of provider names, so a spot can be sent to multiple upstream providers at once. `upstream_credentials` is now a map of provider name to that provider's credentials.
#### Upgrading a client from v2 to v3 API endpoints
@@ -940,6 +941,7 @@ components:
- BIWOTA
- COTA
- PGA
- Railways
- Toilets
example: POTA
@@ -1381,30 +1383,30 @@ components:
description: >
Optional server-side instructions for how to process this spot submission.
properties:
submit_upstream:
type: boolean
upstream_providers:
type: array
items:
type: string
description: >
If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather than only adding it
to this Spothole server. Requires `upstream_provider` to be set. Check `spot_submit_providers` in the
`/options` response to see which activities and providers support this.
default: false
upstream_provider:
type: string
description: >
Name of the upstream provider to submit the spot to, e.g. "POTA" or "SOTA". Must
match one of the provider names returned in `spot_submit_providers` for the chosen activity.
example: POTA
Names of upstream providers to forward the spot to (e.g. "POTA", "SOTA"), in addition to or instead
of only adding it to this Spothole server. Each name must match one of the provider names returned in
`spot_submit_providers` in the `/options` response for the chosen activity. Omit or leave empty to
add the spot to this Spothole server only.
example: [ POTA, ParksNPeaks ]
upstream_credentials:
type: object
description: >
Provider-specific credentials required to authenticate the upstream submission.
The required keys depend on the provider. Credentials are used only for the upstream
call and are never stored by Spothole.
A map of provider name to the provider-specific credentials required to authenticate the upstream
submission to that provider. The required keys depend on the provider. Credentials are used only for
the upstream call and are never stored by Spothole.
additionalProperties:
type: string
type: object
additionalProperties:
type: string
example:
user_id: "12345"
api_key: "abc123"
ParksNPeaks:
user_id: "12345"
api_key: "abc123"
captcha_token:
type: string
description: >
@@ -2294,9 +2296,9 @@ components:
type: object
description: >
A map of activity name to a list of provider names that support upstream spot submission for that
activity. If an activity appears as a key here, the POST /spot endpoint accepts `submit_upstream: true`
for spots with that activity, and will forward the spot to one of the listed providers. Omitted if no
providers support upstream submission.
activity. If an activity appears as a key here, the POST /spot endpoint accepts these provider names in
`upstream_providers` for spots with that activity, and will forward the spot to any of the listed
providers. Omitted if no providers support upstream submission.
additionalProperties:
type: array
items:
+50 -42
View File
@@ -63,46 +63,46 @@ function updateUpstreamArea() {
$("#upstream-area").show();
// Update the provider selector
$("#upstream-provider-select").empty();
// Rebuild the provider checkboxes, keeping any that were already checked for the previous activity
const previouslyChecked = getSelectedUpstreamProviders();
const container = $("#upstream-providers").empty();
$.each(providers, function (i, name) {
$("#upstream-provider-select").append($('<option>', {value: name, text: name}));
const id = "submit-upstream-" + i;
const row = $("<div class='row g-2 align-items-center mb-1'></div>");
const check = $("<div class='form-check'></div>");
check.append($("<input class='form-check-input upstream-provider-check' type='checkbox'>")
.attr("id", id).val(name).prop("checked", previouslyChecked.includes(name)));
check.append($("<label class='form-check-label'></label>").attr("for", id).text(name));
row.append($("<div class='col-auto'></div>").append(check));
// Add a credentials button if this provider has an authentication mechanism and we need input from the user
if (PROVIDER_CREDENTIAL_SCHEMAS[name]) {
const button = $("<button type='button' class='btn btn-sm btn-outline-secondary'></button>")
.attr("aria-label", "Credentials for " + name)
.html("<i class='fa-solid fa-key'></i> Credentials")
.click(() => showCredentialsModal(name));
row.append($("<div class='col-auto'></div>").append(button));
}
container.append(row);
});
if (providers.length > 1) {
$("#upstream-provider-label").text("upstream spot sources:");
$("#upstream-provider-select-col").show();
} else {
$("#upstream-provider-label").text(providers[0]);
$("#upstream-provider-select-col").hide();
}
// Show the credentials button if this provider has an authentication mechanism and we need input from the user
updateCredentialsButton();
updateSpotholeOnlyCheck();
}
// Update the credentials button visibility based on selected provider
function updateCredentialsButton() {
const providerName = getSelectedUpstreamProvider();
if (providerName && PROVIDER_CREDENTIAL_SCHEMAS[providerName]) {
$("#upstream-credentials-btn").show();
} else {
$("#upstream-credentials-btn").hide();
}
// The "Spothole only" checkbox is checked when no upstream providers are selected
function updateSpotholeOnlyCheck() {
$("#spothole-only").prop("checked", getSelectedUpstreamProviders().length === 0);
}
// Get the currently selected upstream provider name
function getSelectedUpstreamProvider() {
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
if (providers.length === 0) return null;
if (providers.length === 1) return providers[0];
return $("#upstream-provider-select").val();
// Get the names of the upstream providers the user has chosen to send the spot to
function getSelectedUpstreamProviders() {
return $(".upstream-provider-check:checked").map(function () {
return $(this).val();
}).get();
}
// Show the credentials modal for the currently selected upstream provider
function showCredentialsModal() {
const providerName = getSelectedUpstreamProvider();
if (!providerName || !PROVIDER_CREDENTIAL_SCHEMAS[providerName]) return;
// Show the credentials modal for the given upstream provider
function showCredentialsModal(providerName) {
if (!PROVIDER_CREDENTIAL_SCHEMAS[providerName]) return;
const schema = PROVIDER_CREDENTIAL_SCHEMAS[providerName];
const stored = loadCredentials(providerName);
@@ -187,12 +187,13 @@ function addSpot() {
}
// Upstream submission
const submitUpstream = $("#submit-upstream").is(":checked");
const upstreamProviderName = getSelectedUpstreamProvider();
if (submitUpstream && upstreamProviderName) {
handling["submit_upstream"] = true;
handling["upstream_provider"] = upstreamProviderName;
handling["upstream_credentials"] = loadCredentials(upstreamProviderName);
const upstreamProviders = getSelectedUpstreamProviders();
if (upstreamProviders.length > 0) {
handling["upstream_providers"] = upstreamProviders;
handling["upstream_credentials"] = {};
$.each(upstreamProviders, function (i, name) {
handling["upstream_credentials"][name] = loadCredentials(name);
});
}
$.ajax("/api/v3/spot", {
@@ -262,14 +263,21 @@ $("#mode").change(function () {
$(this).val($(this).val().trim().toUpperCase());
});
// Update upstream area and credentials button when activity changes
// Update upstream area when activity changes
$("#activity").change(function () {
updateUpstreamArea();
});
// Update credentials button when provider selector changes
$("#upstream-provider-select").change(function () {
updateCredentialsButton();
// Keep the "Spothole only" checkbox in sync with the upstream provider checkboxes
$("#upstream-providers").on("change", ".upstream-provider-check", function () {
updateSpotholeOnlyCheck();
});
// Clicking "Spothole only" deselects all upstream providers. It can't be unchecked directly, as that would leave
// nowhere to send the spot; the user unchecks it implicitly by choosing an upstream provider instead.
$("#spothole-only").change(function () {
$(".upstream-provider-check").prop("checked", false);
updateSpotholeOnlyCheck();
});
// Startup