diff --git a/public/js/admin.js b/public/js/admin.js
new file mode 100644
index 0000000..d050f38
--- /dev/null
+++ b/public/js/admin.js
@@ -0,0 +1,187 @@
+/* Token kept in memory + sessionStorage so a refresh keeps you logged in
+ but closing the tab clears it. */
+let token = sessionStorage.getItem("spectre_token") || null;
+let me = sessionStorage.getItem("spectre_user") || null;
+let ghostCache = [];
+
+const $ = (id) => document.getElementById(id);
+const loginView = $("login-view");
+const dashView = $("dash-view");
+
+/* ---------- API helper ---------- */
+async function api(method, path, body, isForm = false) {
+ const headers = {};
+ if (token) headers.Authorization = "Bearer " + token;
+ let payload;
+ if (isForm) {
+ payload = body; // FormData
+ } else if (body) {
+ headers["Content-Type"] = "application/json";
+ payload = JSON.stringify(body);
+ }
+ const res = await fetch("/api" + path, { method, headers, body: payload });
+ if (res.status === 401) { doLogout(); throw new Error("Session expired"); }
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) throw new Error(data.error || "Request failed");
+ return data;
+}
+
+/* ---------- Auth ---------- */
+$("login-btn").addEventListener("click", async () => {
+ $("login-error").textContent = "";
+ try {
+ const data = await api("POST", "/login", {
+ username: $("username").value.trim(),
+ password: $("password").value,
+ });
+ token = data.token; me = data.username;
+ sessionStorage.setItem("spectre_token", token);
+ sessionStorage.setItem("spectre_user", me);
+ enterDash();
+ } catch (e) {
+ $("login-error").textContent = e.message;
+ }
+});
+$("password").addEventListener("keydown", (e) => { if (e.key === "Enter") $("login-btn").click(); });
+
+$("logout-btn").addEventListener("click", doLogout);
+function doLogout() {
+ token = null; me = null;
+ sessionStorage.removeItem("spectre_token");
+ sessionStorage.removeItem("spectre_user");
+ dashView.classList.remove("active");
+ loginView.classList.add("active");
+}
+
+function enterDash() {
+ loginView.classList.remove("active");
+ dashView.classList.add("active");
+ $("whoami").textContent = "@" + me;
+ loadGhosts();
+ loadSets();
+}
+
+/* ---------- Tabs ---------- */
+document.querySelectorAll(".tab").forEach((t) =>
+ t.addEventListener("click", () => {
+ document.querySelectorAll(".tab").forEach((x) => x.classList.remove("active"));
+ document.querySelectorAll(".tab-panel").forEach((x) => x.classList.remove("active"));
+ t.classList.add("active");
+ $("tab-" + t.dataset.tab).classList.add("active");
+ })
+);
+
+/* ---------- Ghosts ---------- */
+async function loadGhosts() {
+ ghostCache = await api("GET", "/ghosts");
+ const grid = $("ghost-grid");
+ grid.innerHTML = "";
+ for (const g of ghostCache) {
+ const card = document.createElement("div");
+ card.className = "ghost-card" + (g.enabled ? "" : " disabled");
+ card.innerHTML = `
+
+