/** * GleamView web console — vanilla ES module SPA. * Keys live in localStorage only; XRPC needs X-Client-Key, admin needs Bearer. */ const STORAGE = { clientKey: "gleamview.clientKey", adminKey: "gleamview.adminKey", did: "gleamview.did", }; const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; const state = { config: null, tab: "feed", }; function loadSettings() { return { clientKey: localStorage.getItem(STORAGE.clientKey) || "", adminKey: localStorage.getItem(STORAGE.adminKey) || "", did: localStorage.getItem(STORAGE.did) || "", }; } function saveSettings(partial) { const cur = loadSettings(); const next = { ...cur, ...partial }; if (next.clientKey) localStorage.setItem(STORAGE.clientKey, next.clientKey); else localStorage.removeItem(STORAGE.clientKey); if (next.adminKey) localStorage.setItem(STORAGE.adminKey, next.adminKey); else localStorage.removeItem(STORAGE.adminKey); if (next.did) localStorage.setItem(STORAGE.did, next.did); else localStorage.removeItem(STORAGE.did); return next; } function toast(msg, kind = "") { const el = $("#toast"); el.hidden = false; el.textContent = msg; el.className = "toast" + (kind ? ` ${kind}` : ""); clearTimeout(toast._t); toast._t = setTimeout(() => { el.hidden = true; }, 3800); } async function api(path, { method = "GET", headers = {}, body, json } = {}) { const opts = { method, headers: { ...headers } }; if (json !== undefined) { opts.headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(json); } else if (body !== undefined) { opts.body = body; } const res = await fetch(path, opts); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; } if (!res.ok) { const msg = (data && (data.message || data.error)) || text || `${res.status} ${res.statusText}`; const err = new Error(msg); err.status = res.status; err.data = data; throw err; } return data; } function withClient(headers = {}) { const { clientKey } = loadSettings(); if (clientKey) headers["X-Client-Key"] = clientKey; return headers; } function withAdmin(headers = {}) { const { adminKey } = loadSettings(); if (adminKey) headers["Authorization"] = `Bearer ${adminKey}`; return headers; } function setTab(name) { state.tab = name; $$(".tab").forEach((btn) => { const on = btn.dataset.tab === name; btn.classList.toggle("active", on); btn.setAttribute("aria-selected", on ? "true" : "false"); }); $$(".panel").forEach((panel) => { const on = panel.id === `panel-${name}`; panel.hidden = !on; panel.classList.toggle("active", on); }); if (name === "feed") loadFeed().catch(() => {}); if (name === "lexicons") loadLexicons().catch(() => {}); if (name === "admin") loadAdmin().catch(() => {}); if (name === "oauth") { const h = $("#oauth-handle").value || loadSettings().did; if (h && !h.startsWith("did:")) checkOAuth(h).catch(() => {}); } if (name === "settings") fillSettingsForm(); if (name === "compose") { const s = loadSettings(); if (s.did && !$("#compose-did").value) $("#compose-did").value = s.did; } } function fillSettingsForm() { const s = loadSettings(); $("#set-client-key").value = s.clientKey; $("#set-admin-key").value = s.adminKey; $("#set-did").value = s.did; } function esc(s) { return String(s ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function shortDid(did) { if (!did) return "unknown"; if (did.length <= 22) return did; return did.slice(0, 12) + "…" + did.slice(-6); } function fmtTime(iso) { if (!iso) return ""; try { return new Date(iso).toLocaleString(); } catch { return iso; } } // —— Status / config —— async function refreshStatus() { const pill = $("#status-pill"); try { const [health, config] = await Promise.all([ api("/health"), api("/config"), ]); state.config = config; const mode = config.allowLocalWrites ? "local writes" : "PDS proxy"; pill.textContent = `${health.service || "ok"} · ${mode}`; pill.className = "pill ok"; $("#write-mode").textContent = mode; $("#write-mode").className = "pill sm " + (config.allowLocalWrites ? "warn" : "ok"); $("#config-json").textContent = JSON.stringify(config, null, 2); } catch (e) { pill.textContent = "offline"; pill.className = "pill err"; $("#config-json").textContent = String(e.message || e); } } // —— Feed —— async function loadFeed() { const empty = $("#feed-empty"); const list = $("#feed-list"); empty.hidden = false; empty.textContent = "Loading…"; list.hidden = true; try { const data = await api("/xrpc/com.example.getPosts?limit=50", { headers: withClient(), }); const records = data.records || data || []; if (!records.length) { empty.hidden = false; empty.textContent = "No posts indexed yet. Compose one!"; list.hidden = true; return; } empty.hidden = true; list.hidden = false; list.innerHTML = records .map((r) => { const uri = r.uri || ""; const did = uri.startsWith("at://") ? uri.slice(5).split("/")[0] : r.did || ""; const text = r.text ?? r.record?.text ?? JSON.stringify(r); const created = r.createdAt || r.indexed_at || r.indexedAt || ""; return `
${esc(shortDid(did))} ${esc(fmtTime(created))}
${esc(text)}
${esc(uri)}
`; }) .join(""); } catch (e) { empty.hidden = false; list.hidden = true; empty.textContent = `Failed to load feed: ${e.message}`; } } // —— Compose —— async function submitCompose(ev) { ev.preventDefault(); const text = $("#compose-text").value.trim(); const did = ($("#compose-did").value || loadSettings().did).trim(); const status = $("#compose-status"); const result = $("#compose-result"); if (!text) return; if (!did) { toast("Author DID / handle is required", "err"); return; } status.textContent = "Publishing…"; result.hidden = true; try { const body = { text, createdAt: new Date().toISOString(), }; const data = await api("/xrpc/com.example.createPost", { method: "POST", headers: withClient({ "X-Gleamview-Did": did }), json: body, }); status.textContent = "Published"; result.hidden = false; result.textContent = JSON.stringify(data, null, 2); $("#compose-text").value = ""; toast("Post created", "ok"); saveSettings({ did }); } catch (e) { status.textContent = ""; result.hidden = false; result.textContent = e.message; toast(e.message, "err"); } } // —— OAuth —— async function startOAuth(ev) { ev.preventDefault(); const handle = $("#oauth-handle").value.trim().replace(/^@/, ""); if (!handle) return; // Full navigation so the authorization server redirect works cleanly window.location.href = `/oauth/login?handle=${encodeURIComponent(handle)}`; } async function checkOAuth(handle) { const h = (handle || $("#oauth-handle").value || "").trim().replace(/^@/, ""); const box = $("#oauth-session"); if (!h) { toast("Enter a handle", "err"); return; } try { const data = await api( `/oauth/session?identity=${encodeURIComponent(h)}`, ); box.hidden = false; if (data.authenticated) { box.innerHTML = `
Signed in
handle: ${esc(data.handle)}
did: ${esc(data.did)}
pds: ${esc(data.pds)}
expires: ${esc(fmtTime(data.expiresAt * 1000))}
`; saveSettings({ did: data.did }); $("#compose-did").value = data.did; toast("Session active", "ok"); } else { box.innerHTML = `
Not signed in
No session for ${esc(h)}
`; } } catch (e) { box.hidden = false; box.innerHTML = `
Error
${esc(e.message)}
`; } } async function logoutOAuth() { const handle = $("#oauth-handle").value.trim().replace(/^@/, ""); if (!handle) { toast("Enter handle to sign out", "err"); return; } try { await api(`/oauth/session?handle=${encodeURIComponent(handle)}`, { method: "DELETE", }); $("#oauth-session").hidden = true; toast("Signed out", "ok"); } catch (e) { toast(e.message, "err"); } } // —— Lexicons —— async function loadLexicons() { const list = $("#lexicon-list"); if (!loadSettings().adminKey) { list.innerHTML = '
Add an admin key in Settings to list lexicons.
'; return; } list.innerHTML = '
Loading…
'; try { const data = await api("/admin/lexicons", { headers: withAdmin() }); const items = Array.isArray(data) ? data : data.lexicons || data; if (!items?.length) { list.innerHTML = '
No lexicons registered.
'; return; } list.innerHTML = items .map((lex) => { const id = lex.id || lex.nsid || "?"; const type = lex.type || lex.lexiconType || lex.kind || ""; const rev = lex.revision ?? lex.rev ?? ""; const target = lex.targetCollection || lex.target_collection || ""; return `
${esc(id)}
type: ${esc(type)}${rev !== "" ? ` · rev ${esc(rev)}` : ""}${ target ? ` · → ${esc(target)}` : "" }
`; }) .join(""); } catch (e) { list.innerHTML = `
${esc(e.message)}
`; } } // —— Admin —— async function loadAdmin() { if (!loadSettings().adminKey) { $("#client-list").innerHTML = '
Add an admin key in Settings.
'; $("#record-list").innerHTML = ""; return; } try { const stats = await api("/admin/stats", { headers: withAdmin() }); $("#stat-records").textContent = stats.records ?? "—"; $("#stat-lexicons").textContent = stats.lexicons ?? "—"; } catch (e) { $("#stat-records").textContent = "—"; $("#stat-lexicons").textContent = "—"; toast(e.message, "err"); } try { const clients = await api("/admin/api-clients", { headers: withAdmin() }); const items = Array.isArray(clients) ? clients : clients.clients || []; $("#client-list").innerHTML = items.length ? items .map( (c) => `
${esc(c.name || "client")}
${esc(c.clientKey || c.client_key || c.id || "")}
`, ) .join("") : '
No API clients yet.
'; } catch (e) { $("#client-list").innerHTML = `
${esc(e.message)}
`; } try { const recs = await api("/admin/records?collection=com.example.post&limit=20", { headers: withAdmin() }); const items = Array.isArray(recs) ? recs : recs.records || []; $("#record-list").innerHTML = items.length ? items .map((r) => { const uri = r.uri || ""; const col = r.collection || ""; const snippet = typeof r.record === "string" ? r.record.slice(0, 120) : JSON.stringify(r.record || r).slice(0, 120); return `
${esc(col || "record")}
${esc(uri)}
${esc(snippet)}
`; }) .join("") : '
No records.
'; } catch (e) { $("#record-list").innerHTML = `
${esc(e.message)}
`; } } async function createClient(ev) { ev.preventDefault(); const name = $("#client-name").value.trim() || "web"; if (!loadSettings().adminKey) { toast("Admin key required", "err"); setTab("settings"); return; } try { const data = await api("/admin/api-clients", { method: "POST", headers: withAdmin(), json: { name }, }); const key = data.clientKey || data.client_key; const secret = data.clientSecret || data.client_secret; const box = $("#client-created"); box.hidden = false; box.innerHTML = ` Client created — copy now
key: ${esc(key)}
secret: ${esc(secret || "(not shown)")}
`; $("#btn-use-client")?.addEventListener("click", () => { saveSettings({ clientKey: key }); fillSettingsForm(); toast("Client key saved to Settings", "ok"); }); if (key) { // offer auto-save } $("#client-name").value = ""; await loadAdmin(); } catch (e) { toast(e.message, "err"); } } // —— Settings —— function bindSettings() { $("#form-settings").addEventListener("submit", (ev) => { ev.preventDefault(); saveSettings({ clientKey: $("#set-client-key").value.trim(), adminKey: $("#set-admin-key").value.trim(), did: $("#set-did").value.trim(), }); $("#settings-status").textContent = "Saved"; toast("Settings saved", "ok"); setTimeout(() => { $("#settings-status").textContent = ""; }, 1500); }); $("#set-show-client")?.addEventListener("change", (ev) => { const input = $("#set-client-key"); if (input) input.type = ev.target.checked ? "text" : "password"; }); $("#set-show-admin")?.addEventListener("change", (ev) => { const input = $("#set-admin-key"); if (input) input.type = ev.target.checked ? "text" : "password"; }); $("#btn-clear-settings").addEventListener("click", () => { localStorage.removeItem(STORAGE.clientKey); localStorage.removeItem(STORAGE.adminKey); localStorage.removeItem(STORAGE.did); fillSettingsForm(); toast("Cleared local settings", "ok"); }); } // —— Boot —— function bindTabs() { $$(".tab").forEach((btn) => { btn.addEventListener("click", () => setTab(btn.dataset.tab)); }); } function bindActions() { $("#btn-refresh").addEventListener("click", () => { refreshStatus(); if (state.tab === "feed") loadFeed(); if (state.tab === "admin") loadAdmin(); if (state.tab === "lexicons") loadLexicons(); }); $("#btn-load-feed").addEventListener("click", () => loadFeed()); $("#form-compose").addEventListener("submit", submitCompose); $("#form-oauth").addEventListener("submit", startOAuth); $("#btn-oauth-check").addEventListener("click", () => checkOAuth()); $("#btn-oauth-logout").addEventListener("click", () => logoutOAuth()); $("#btn-load-lexicons").addEventListener("click", () => loadLexicons()); $("#btn-load-admin").addEventListener("click", () => loadAdmin()); $("#form-client").addEventListener("submit", createClient); } function maybeOAuthReturn() { // If user landed after oauth callback page... callback is HTML. If they // navigated home with ?oauth=1 we could show message. Optional: hash. const params = new URLSearchParams(location.search); if (params.get("signed_in")) { toast("Signed in — check the Sign in tab", "ok"); setTab("oauth"); } } async function main() { bindTabs(); bindActions(); bindSettings(); fillSettingsForm(); const s = loadSettings(); if (s.did) { $("#compose-did").value = s.did; if (!s.did.startsWith("did:")) $("#oauth-handle").value = s.did; } maybeOAuthReturn(); await refreshStatus(); loadFeed().catch(() => {}); } main();