/**
* 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();