updated: app/data-service.js, app/styles.css, app/ui-settings.js, index.html
This commit is contained in:
+8
-2
@@ -210,7 +210,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toApiAssetUrl(assetPath) {
|
function toApiAssetUrl(assetPath) {
|
||||||
const apiBaseUrl = getApiBaseUrl();
|
const { apiBaseUrl, apiKey } = resolveConnectionSettings();
|
||||||
const normalizedAssetPath = String(assetPath || "")
|
const normalizedAssetPath = String(assetPath || "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^\/+/, "")
|
.replace(/^\/+/, "")
|
||||||
@@ -220,7 +220,12 @@
|
|||||||
return "";
|
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() {
|
function resetCaches() {
|
||||||
@@ -635,6 +640,7 @@
|
|||||||
reason: "connected",
|
reason: "connected",
|
||||||
message: "Connected.",
|
message: "Connected.",
|
||||||
health,
|
health,
|
||||||
|
auth: health?.auth || null,
|
||||||
deckCount: Array.isArray(decksPayload?.decks) ? decksPayload.decks.length : null
|
deckCount: Array.isArray(decksPayload?.decks) ? decksPayload.decks.length : null
|
||||||
};
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
|
|||||||
@@ -567,6 +567,46 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
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 {
|
.settings-cache-progress-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
onRenderWeek: null
|
onRenderWeek: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lastConnectionProbeResult = null;
|
||||||
|
|
||||||
let lastNonSettingsSection = "home";
|
let lastNonSettingsSection = "home";
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
@@ -42,6 +44,11 @@
|
|||||||
stellariumBackgroundHintEl: document.getElementById("stellarium-background-hint"),
|
stellariumBackgroundHintEl: document.getElementById("stellarium-background-hint"),
|
||||||
apiBaseUrlEl: document.getElementById("api-base-url"),
|
apiBaseUrlEl: document.getElementById("api-base-url"),
|
||||||
apiKeyEl: document.getElementById("api-key"),
|
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"),
|
settingsPageStatusEl: document.getElementById("settings-page-status"),
|
||||||
settingsPageStatusTextEl: document.getElementById("settings-page-status-text"),
|
settingsPageStatusTextEl: document.getElementById("settings-page-status-text"),
|
||||||
settingsPageStatusTimeEl: document.getElementById("settings-page-status-time"),
|
settingsPageStatusTimeEl: document.getElementById("settings-page-status-time"),
|
||||||
@@ -192,6 +199,98 @@
|
|||||||
|| String(previous?.apiKey || "").trim() !== String(next?.apiKey || "").trim();
|
|| 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) {
|
function setStatus(text) {
|
||||||
if (typeof config.onStatus === "function") {
|
if (typeof config.onStatus === "function") {
|
||||||
@@ -571,6 +670,9 @@
|
|||||||
}
|
}
|
||||||
applySettingsToInputs(loadSavedSettings());
|
applySettingsToInputs(loadSavedSettings());
|
||||||
syncSavedSettingsStatus();
|
syncSavedSettingsStatus();
|
||||||
|
void refreshConnectionSummary(getConnectionSettings(), {
|
||||||
|
probeResult: lastConnectionProbeResult
|
||||||
|
});
|
||||||
config.setActiveSection?.("settings");
|
config.setActiveSection?.("settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,8 +697,12 @@
|
|||||||
|
|
||||||
const probeResult = await window.TarotDataService?.probeConnection?.(connectionSettings);
|
const probeResult = await window.TarotDataService?.probeConnection?.(connectionSettings);
|
||||||
if (!probeResult?.ok) {
|
if (!probeResult?.ok) {
|
||||||
|
setConnectionSummary(probeResult);
|
||||||
throw new Error(probeResult?.message || "Unable to validate the API connection.");
|
throw new Error(probeResult?.message || "Unable to validate the API connection.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastConnectionProbeResult = probeResult;
|
||||||
|
setConnectionSummary(probeResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionResult = window.TarotAppConfig?.updateConnectionSettings?.(connectionSettings) || { didPersist: true };
|
const connectionResult = window.TarotAppConfig?.updateConnectionSettings?.(connectionSettings) || { didPersist: true };
|
||||||
@@ -738,6 +844,7 @@
|
|||||||
syncConnectionInputs();
|
syncConnectionInputs();
|
||||||
syncTarotDeckInputOptions();
|
syncTarotDeckInputOptions();
|
||||||
syncDeckCacheStatus(window.TarotCardImages?.getDeckPreloadStatus?.());
|
syncDeckCacheStatus(window.TarotCardImages?.getDeckPreloadStatus?.());
|
||||||
|
void refreshConnectionSummary(getConnectionSettings());
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("tarot:deck-cache-status", (event) => {
|
document.addEventListener("tarot:deck-cache-status", (event) => {
|
||||||
@@ -756,6 +863,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
syncSavedSettingsStatus();
|
syncSavedSettingsStatus();
|
||||||
|
setConnectionSummary(lastConnectionProbeResult);
|
||||||
bindInteractions();
|
bindInteractions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
@@ -179,6 +179,24 @@
|
|||||||
<input id="api-key" type="password" autocomplete="off" placeholder="Optional unless the API requires one">
|
<input id="api-key" type="password" autocomplete="off" placeholder="Optional unless the API requires one">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="api-connection-summary" class="settings-connection-summary" data-tone="neutral" aria-live="polite">
|
||||||
|
<div class="settings-connection-summary-row">
|
||||||
|
<span class="settings-connection-summary-label">Status</span>
|
||||||
|
<span id="api-connection-summary-state" class="settings-connection-summary-value">Not checked yet.</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-connection-summary-row">
|
||||||
|
<span class="settings-connection-summary-label">Client</span>
|
||||||
|
<span id="api-connection-summary-client" class="settings-connection-summary-value">No authenticated API identity.</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-connection-summary-row">
|
||||||
|
<span class="settings-connection-summary-label">Access</span>
|
||||||
|
<span id="api-connection-summary-access" class="settings-connection-summary-value">Unknown</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-connection-summary-row">
|
||||||
|
<span class="settings-connection-summary-label">Permissions</span>
|
||||||
|
<span id="api-connection-summary-permissions" class="settings-connection-summary-value">Save settings to validate this API key.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-actions settings-page-actions">
|
<div class="settings-actions settings-page-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user