diff --git a/app/data-service.js b/app/data-service.js index 6459b42..f2b58cc 100644 --- a/app/data-service.js +++ b/app/data-service.js @@ -210,7 +210,7 @@ } function toApiAssetUrl(assetPath) { - const apiBaseUrl = getApiBaseUrl(); + const { apiBaseUrl, apiKey } = resolveConnectionSettings(); const normalizedAssetPath = String(assetPath || "") .trim() .replace(/^\/+/, "") @@ -220,7 +220,12 @@ return ""; } - return new URL(`/api/v1/assets/${encodePathSegments(normalizedAssetPath)}`, `${apiBaseUrl}/`).toString(); + const url = new URL(`/api/v1/assets/${encodePathSegments(normalizedAssetPath)}`, `${apiBaseUrl}/`); + if (apiKey) { + url.searchParams.set("apiKey", apiKey); + } + + return url.toString(); } function resetCaches() { @@ -635,6 +640,7 @@ reason: "connected", message: "Connected.", health, + auth: health?.auth || null, deckCount: Array.isArray(decksPayload?.decks) ? decksPayload.decks.length : null }; } catch (_error) { diff --git a/app/styles.css b/app/styles.css index 8420310..311147f 100644 --- a/app/styles.css +++ b/app/styles.css @@ -567,6 +567,46 @@ font-size: 12px; line-height: 1.4; } + .settings-connection-summary { + margin-top: 16px; + padding: 14px 16px; + border-radius: 18px; + border: 1px solid rgba(63, 63, 70, 0.9); + background: rgba(9, 9, 11, 0.68); + display: grid; + gap: 10px; + } + .settings-connection-summary[data-tone="success"] { + border-color: rgba(34, 197, 94, 0.45); + background: rgba(20, 83, 45, 0.16); + } + .settings-connection-summary[data-tone="warning"] { + border-color: rgba(245, 158, 11, 0.45); + background: rgba(120, 53, 15, 0.16); + } + .settings-connection-summary[data-tone="error"] { + border-color: rgba(239, 68, 68, 0.45); + background: rgba(127, 29, 29, 0.18); + } + .settings-connection-summary-row { + display: grid; + grid-template-columns: 84px minmax(0, 1fr); + gap: 12px; + align-items: start; + } + .settings-connection-summary-label { + color: #94a3b8; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + } + .settings-connection-summary-value { + color: #f8fafc; + font-size: 13px; + line-height: 1.5; + word-break: break-word; + } .settings-cache-progress-wrap { display: grid; grid-template-columns: minmax(0, 1fr) auto; diff --git a/app/ui-settings.js b/app/ui-settings.js index e3dc99d..5655b0e 100644 --- a/app/ui-settings.js +++ b/app/ui-settings.js @@ -24,6 +24,8 @@ onRenderWeek: null }; + let lastConnectionProbeResult = null; + let lastNonSettingsSection = "home"; function getElements() { @@ -42,6 +44,11 @@ stellariumBackgroundHintEl: document.getElementById("stellarium-background-hint"), apiBaseUrlEl: document.getElementById("api-base-url"), apiKeyEl: document.getElementById("api-key"), + apiConnectionSummaryEl: document.getElementById("api-connection-summary"), + apiConnectionSummaryStateEl: document.getElementById("api-connection-summary-state"), + apiConnectionSummaryClientEl: document.getElementById("api-connection-summary-client"), + apiConnectionSummaryAccessEl: document.getElementById("api-connection-summary-access"), + apiConnectionSummaryPermissionsEl: document.getElementById("api-connection-summary-permissions"), settingsPageStatusEl: document.getElementById("settings-page-status"), settingsPageStatusTextEl: document.getElementById("settings-page-status-text"), settingsPageStatusTimeEl: document.getElementById("settings-page-status-time"), @@ -192,6 +199,98 @@ || String(previous?.apiKey || "").trim() !== String(next?.apiKey || "").trim(); } + function normalizeConnectionValues(values) { + return Array.isArray(values) + ? values.map((entry) => String(entry || "").trim()).filter(Boolean) + : []; + } + + function hasAdminCapability(auth) { + const roles = normalizeConnectionValues(auth?.roles); + const scopes = normalizeConnectionValues(auth?.scopes); + return roles.includes("admin") || scopes.includes("api:admin"); + } + + function formatConnectionValues(values, fallback = "none") { + const normalized = normalizeConnectionValues(values); + return normalized.length ? normalized.join(", ") : fallback; + } + + function setConnectionSummary(result = null) { + const { + apiConnectionSummaryEl, + apiConnectionSummaryStateEl, + apiConnectionSummaryClientEl, + apiConnectionSummaryAccessEl, + apiConnectionSummaryPermissionsEl + } = getElements(); + + if (!apiConnectionSummaryEl) { + return; + } + + if (!result) { + apiConnectionSummaryEl.dataset.tone = "neutral"; + if (apiConnectionSummaryStateEl) { + apiConnectionSummaryStateEl.textContent = "Not checked yet."; + } + if (apiConnectionSummaryClientEl) { + apiConnectionSummaryClientEl.textContent = "No authenticated API identity."; + } + if (apiConnectionSummaryAccessEl) { + apiConnectionSummaryAccessEl.textContent = "Unknown"; + } + if (apiConnectionSummaryPermissionsEl) { + apiConnectionSummaryPermissionsEl.textContent = "Save settings to validate this API key."; + } + return; + } + + const tone = result.ok ? "success" : (result.reason === "auth-required" ? "warning" : "error"); + const auth = result.auth || result.health?.auth || {}; + const roles = normalizeConnectionValues(auth.roles); + const scopes = normalizeConnectionValues(auth.scopes); + const authenticated = auth.authenticated === true; + const accessValue = authenticated + ? `${String(auth.accessLevel || "premium").trim() || "premium"}${hasAdminCapability(auth) ? " - admin capable" : ""}` + : (result.health?.apiKeyRequired ? "API key required" : "public"); + const permissionsValue = authenticated + ? `roles: ${formatConnectionValues(roles)} | scopes: ${formatConnectionValues(scopes)}` + : (result.message || "Unable to validate the API connection."); + + apiConnectionSummaryEl.dataset.tone = tone; + if (apiConnectionSummaryStateEl) { + apiConnectionSummaryStateEl.textContent = result.ok + ? `Connected${Number.isInteger(result.deckCount) ? ` • ${result.deckCount} deck${result.deckCount === 1 ? "" : "s"}` : ""}` + : String(result.message || "Unable to validate the API connection."); + } + if (apiConnectionSummaryClientEl) { + apiConnectionSummaryClientEl.textContent = authenticated + ? [String(auth.clientId || "").trim(), String(auth.accountId || "").trim()].filter(Boolean).join(" / ") || "Authenticated client" + : (result.health?.apiKeyRequired ? "No valid API identity returned." : "Public access"); + } + if (apiConnectionSummaryAccessEl) { + apiConnectionSummaryAccessEl.textContent = accessValue; + } + if (apiConnectionSummaryPermissionsEl) { + apiConnectionSummaryPermissionsEl.textContent = permissionsValue; + } + } + + async function refreshConnectionSummary(connectionSettings = getConnectionSettings(), { probeResult = null } = {}) { + const apiBaseUrl = String(connectionSettings?.apiBaseUrl || "").trim(); + if (!apiBaseUrl) { + lastConnectionProbeResult = null; + setConnectionSummary(null); + return null; + } + + const result = probeResult || await window.TarotDataService?.probeConnection?.(connectionSettings) || null; + lastConnectionProbeResult = result; + setConnectionSummary(result); + return result; + } + function setStatus(text) { if (typeof config.onStatus === "function") { @@ -571,6 +670,9 @@ } applySettingsToInputs(loadSavedSettings()); syncSavedSettingsStatus(); + void refreshConnectionSummary(getConnectionSettings(), { + probeResult: lastConnectionProbeResult + }); config.setActiveSection?.("settings"); } @@ -595,8 +697,12 @@ const probeResult = await window.TarotDataService?.probeConnection?.(connectionSettings); if (!probeResult?.ok) { + setConnectionSummary(probeResult); throw new Error(probeResult?.message || "Unable to validate the API connection."); } + + lastConnectionProbeResult = probeResult; + setConnectionSummary(probeResult); } const connectionResult = window.TarotAppConfig?.updateConnectionSettings?.(connectionSettings) || { didPersist: true }; @@ -738,6 +844,7 @@ syncConnectionInputs(); syncTarotDeckInputOptions(); syncDeckCacheStatus(window.TarotCardImages?.getDeckPreloadStatus?.()); + void refreshConnectionSummary(getConnectionSettings()); }); document.addEventListener("tarot:deck-cache-status", (event) => { @@ -756,6 +863,7 @@ }; syncSavedSettingsStatus(); + setConnectionSummary(lastConnectionProbeResult); bindInteractions(); } diff --git a/index.html b/index.html index ca536bf..bd61431 100644 --- a/index.html +++ b/index.html @@ -179,6 +179,24 @@ +