diff --git a/internal/api/web/app.js b/internal/api/web/app.js index 36fec07..a5ca562 100644 --- a/internal/api/web/app.js +++ b/internal/api/web/app.js @@ -46,7 +46,7 @@ function loadAPIKey() { input.value = localStorage.getItem("om_api_key") || ""; } -function saveAPIKey() { +async function saveAPIKey() { const input = document.getElementById("api-key"); const key = input.value.trim(); @@ -55,6 +55,7 @@ function saveAPIKey() { } else { localStorage.removeItem("om_api_key"); } + await loadConsole(); } function apiHeaders() { @@ -65,10 +66,42 @@ function apiHeaders() { async function fetchJSON(path) { const res = await fetch(path, { headers: apiHeaders() }); - if (!res.ok) throw new Error(`${path} returned ${res.status}`); + if (!res.ok) { + const error = new Error(`${path} returned ${res.status}`); + error.status = res.status; + throw error; + } return res.json(); } +function isUnauthorized(err) { + return err && err.status === 401; +} + +function consoleLoadError(err) { + if (!isUnauthorized(err)) { + return `Failed to load console: ${err.message}`; + } + if (!localStorage.getItem("om_api_key")) { + return "The server refused to load the graph because no API key is saved. Paste the OM_API_SECRET value into the API key field and click Save API key."; + } + return "The server refused the saved API key. Paste the OM_API_SECRET value into the API key field and click Save API key."; +} + +function showConsoleError(err) { + const banner = document.getElementById("console-error"); + banner.textContent = consoleLoadError(err); + banner.classList.remove("hidden"); + document.getElementById("api-key").classList.toggle("needs-key", isUnauthorized(err)); +} + +function clearConsoleError() { + const banner = document.getElementById("console-error"); + banner.textContent = ""; + banner.classList.add("hidden"); + document.getElementById("api-key").classList.remove("needs-key"); +} + function severityClass(level) { return (level || "info").toLowerCase(); } @@ -174,15 +207,30 @@ function highlightNode(nodeID) { async function loadQueries() { const data = await fetchJSON("/v1/graph/queries"); const select = document.getElementById("query-select"); + const previous = select.value; + const keepSelection = select.dataset.loaded === "1"; + select.replaceChildren(); + const full = document.createElement("option"); + full.value = ""; + full.textContent = "Full graph snapshot"; + select.appendChild(full); data.queries.forEach((entry) => { const opt = document.createElement("option"); opt.value = entry.name; opt.textContent = `${entry.name} — ${entry.description}`; select.appendChild(opt); }); - if ([...select.options].some((opt) => opt.value === "internet-to-datastore")) { - select.value = "internet-to-datastore"; + const preferred = keepSelection ? previous : "internet-to-datastore"; + if ([...select.options].some((opt) => opt.value === preferred)) { + select.value = preferred; } + select.dataset.loaded = "1"; +} + +async function loadConsole() { + clearConsoleError(); + await loadQueries(); + await refresh(); } function findEdge(edges, sourceID, targetID) { @@ -309,17 +357,22 @@ async function refresh() { renderGraph(snapshot.nodes, snapshot.edges); } -document.getElementById("refresh-btn").addEventListener("click", refresh); -document.getElementById("query-select").addEventListener("change", refresh); +document.getElementById("refresh-btn").addEventListener("click", () => { + loadConsole().catch(showConsoleError); +}); +document.getElementById("query-select").addEventListener("change", () => { + refresh().catch(showConsoleError); +}); document.getElementById("rules-btn").addEventListener("click", runRules); -document.getElementById("save-api-key-btn").addEventListener("click", saveAPIKey); +document.getElementById("save-api-key-btn").addEventListener("click", () => { + saveAPIKey().catch(showConsoleError); +}); (async function init() { loadAPIKey(); try { - await loadQueries(); - await refresh(); + await loadConsole(); } catch (err) { - document.body.insertAdjacentHTML("beforeend", `

Failed to load console: ${err.message}

`); + showConsoleError(err); } })(); diff --git a/internal/api/web/index.html b/internal/api/web/index.html index 68f620d..2856359 100644 --- a/internal/api/web/index.html +++ b/internal/api/web/index.html @@ -36,6 +36,7 @@

Graph Explorer

+ diff --git a/internal/api/web/styles.css b/internal/api/web/styles.css index d6326e5..9c9d7a5 100644 --- a/internal/api/web/styles.css +++ b/internal/api/web/styles.css @@ -114,7 +114,17 @@ h2 { margin: 0 0 0.75rem; font-size: 1rem; } color: var(--muted); font-size: 0.9rem; } -.blast-radius.hidden, .detail-panel.hidden, #partial-note.hidden { display: none; } +.console-error { + margin: 0 0 0.75rem; + padding: 0.75rem 0.9rem; + border: 1px solid rgba(255, 93, 93, 0.45); + border-radius: 8px; + background: rgba(255, 93, 93, 0.12); + color: #ffc9c9; + font-size: 0.9rem; + line-height: 1.45; +} +.blast-radius.hidden, .detail-panel.hidden, #partial-note.hidden, .console-error.hidden { display: none; } #partial-note { margin: 0 0 0.75rem; } .detail-panel ol { margin: 0.4rem 0 0; padding-left: 1.2rem; } .detail-panel li { margin: 0.35rem 0; } @@ -128,6 +138,9 @@ h2 { margin: 0 0 0.75rem; font-size: 1rem; } #api-key { width: 140px; } +#api-key.needs-key { + border-color: var(--critical); +} @media (max-width: 900px) { .layout { grid-template-columns: 1fr; }