import { ansiToHtml } from "./ansi.js"; // ---------- state ---------- let token = localStorage.getItem("helmd_token") || ""; let sessions = []; let current = null; // valt sessionsnamn let currentSeq = -1; let shares = []; let currentShare = null; let es = null; // EventSource let pollTimer = null; const $ = (id) => document.getElementById(id); // ---------- api ---------- async function api(path, opts = {}) { opts.headers = Object.assign({ Authorization: "Bearer " + token }, opts.headers); const r = await fetch(path, opts); if (r.status === 401) { showLogin("Ogiltig token."); throw new Error("401"); } if (!r.ok) { let msg = r.statusText; try { msg = (await r.json()).error || msg; } catch {} throw new Error(msg); } const ct = r.headers.get("content-type") || ""; return ct.includes("json") ? r.json() : r.text(); } const post = (p, body) => api(p, { method: "POST", body: JSON.stringify(body) }); // ---------- login ---------- function showLogin(err = "") { stopLive(); $("login").classList.remove("hidden"); $("app").classList.add("hidden"); $("loginError").textContent = err; } async function tryConnect() { try { const h = await api("/api/health"); $("login").classList.add("hidden"); $("app").classList.remove("hidden"); $("verInfo").textContent = "helmd " + h.version; startLive(); await Promise.all([refreshSessions(), refreshShares(), loadConfig()]); if (!current && sessions.length) selectSession(sessions[0].name); } catch (e) { if (e.message !== "401") showLogin("Kunde inte nå servern: " + e.message); } } $("tokenSave").onclick = () => { token = $("tokenInput").value.trim(); localStorage.setItem("helmd_token", token); tryConnect(); }; $("tokenInput").addEventListener("keydown", (e) => { if (e.key === "Enter") $("tokenSave").click(); }); $("logoutBtn").onclick = () => { localStorage.removeItem("helmd_token"); token = ""; showLogin(); }; // ---------- tabs ---------- document.querySelectorAll(".tab").forEach((b) => { b.onclick = () => { document.querySelectorAll(".tab").forEach((x) => x.classList.toggle("active", x === b)); ["sessions", "shares", "settings"].forEach((t) => $("tab-" + t).classList.toggle("hidden", b.dataset.tab !== t)); if (b.dataset.tab === "shares") { $("shareBadge").classList.add("hidden"); $("shareBadge").textContent = ""; } }; }); // ---------- SSE + poll ---------- function startLive() { stopLive(); es = new EventSource("/api/events?token=" + encodeURIComponent(token)); es.onopen = () => $("connDot").classList.add("ok"); es.onerror = () => $("connDot").classList.remove("ok"); es.addEventListener("screen", (e) => { const ev = JSON.parse(e.data); if (ev.session === current) refreshScreen(); refreshSessions(); }); es.addEventListener("question", (e) => { const ev = JSON.parse(e.data); if (ev.session === current) renderQuestion(ev.data); }); es.addEventListener("session", () => refreshSessions()); es.addEventListener("share", (e) => { refreshShares(); const b = $("shareBadge"); if ($("tab-shares").classList.contains("hidden")) { b.textContent = String(Number(b.textContent || 0) + 1); b.classList.remove("hidden"); } }); // fallback-poll om SSE tappas pollTimer = setInterval(() => { if (es && es.readyState === 2) { startLive(); return; } if (current) refreshScreen(); }, 5000); } function stopLive() { if (es) { es.close(); es = null; } if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } $("connDot").classList.remove("ok"); } // ---------- sessioner ---------- async function refreshSessions() { sessions = (await api("/api/sessions")) || []; sessions.sort((a, b) => a.name.localeCompare(b.name)); const box = $("sessItems"); box.innerHTML = ""; for (const s of sessions) { const d = document.createElement("div"); d.className = "sess-item" + (s.name === current ? " active" : ""); d.innerHTML = ` ${s.name}${s.question ? '❓' : ""}`; d.onclick = () => selectSession(s.name); box.appendChild(d); } if (current) { const cur = sessions.find((s) => s.name === current); if (!cur) { current = null; $("sessPane").classList.add("hidden"); $("noSess").classList.remove("hidden"); } else if (cur.seq !== currentSeq) refreshScreen(); } } async function selectSession(name) { current = name; currentSeq = -1; $("noSess").classList.add("hidden"); $("sessPane").classList.remove("hidden"); $("sessTitle").textContent = name; const s = sessions.find((x) => x.name === name); renderQuestion(s && s.question); await refreshScreen(); refreshSessions(); } async function refreshScreen() { if (!current) return; try { const s = sessions.find((x) => x.name === current); if (s) currentSeq = s.seq; const txt = await api(`/api/sessions/${current}/screen?ansi=1`); const el = $("screen"); const stick = el.scrollTop + el.clientHeight >= el.scrollHeight - 30; el.innerHTML = ansiToHtml(txt); if (stick) el.scrollTop = el.scrollHeight; const st = await api(`/api/sessions/${current}`); renderQuestion(st.question); } catch {} } function renderQuestion(q) { const card = $("questionCard"); if (!q) { card.classList.add("hidden"); card.innerHTML = ""; return; } card.classList.remove("hidden"); card.innerHTML = `
${escapeHtml(txt)}`;
}
}
// ---------- inställningar ----------
async function loadConfig() {
const c = await api("/api/config");
$("cfgCmd").value = c.agent_cmd || "";
$("cfgArgs").value = (c.agent_args || []).join(" ");
$("cfgWorkdir").value = c.workdir || "";
$("cfgPoll").value = c.poll_ms || 500;
$("cfgNtfyOn").checked = !!c.ntfy?.enabled;
$("cfgNtfyQ").checked = !!c.ntfy?.notify_on_question;
$("cfgNtfyServer").value = c.ntfy?.server || "";
$("cfgNtfyTopic").value = c.ntfy?.topic || "";
window._ntfyAllowed = c.ntfy?.allowed_topics || ["agent-helm", "claude"];
}
$("cfgForm").onsubmit = async (e) => {
e.preventDefault();
try {
await api("/api/config", {
method: "PUT",
body: JSON.stringify({
agent_cmd: $("cfgCmd").value.trim(),
agent_args: $("cfgArgs").value.trim() ? $("cfgArgs").value.trim().split(/\s+/) : [],
workdir: $("cfgWorkdir").value.trim(),
poll_ms: Number($("cfgPoll").value),
ntfy: {
enabled: $("cfgNtfyOn").checked,
notify_on_question: $("cfgNtfyQ").checked,
server: $("cfgNtfyServer").value.trim(),
topic: $("cfgNtfyTopic").value.trim(),
allowed_topics: window._ntfyAllowed,
},
}),
});
$("cfgStatus").textContent = "Sparat ✓";
setTimeout(() => ($("cfgStatus").textContent = ""), 2500);
} catch (err) { $("cfgStatus").textContent = "Fel: " + err.message; }
};
// ---------- hjälp ----------
function escapeHtml(s) {
return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
// Miniminimal markdown: rubriker, fet/kursiv, kod, listor, länkar, hr.
function mdToHtml(md) {
const blocks = md.split(/\n{2,}/);
return blocks.map((b) => {
if (/^```/.test(b)) return `${escapeHtml(b.replace(/^```\w*\n?|```$/g, ""))}`;
if (/^#{1,6} /.test(b)) {
const n = b.match(/^#+/)[0].length;
return `${inline(b)}
`; }).join("\n"); function inline(s) { return escapeHtml(s) .replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1")
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1')
.replace(/\n/g, "