SNAPKITTYWEST commited on
Commit
dcb6526
·
verified ·
1 Parent(s): f3a08d8

Add priv/frontend/js/app.js

Browse files
Files changed (1) hide show
  1. priv/frontend/js/app.js +170 -0
priv/frontend/js/app.js ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // HyperKitty Chromium -- control-center frontend.
2
+ //
3
+ // Deliberately framework-free: this is the visibility/control
4
+ // surface described in the architecture, not a product UI. It
5
+ // talks to the six API domains over plain fetch() and consumes the
6
+ // live event stream over one WebSocket -- every view here is driven
7
+ // by the same data any other API client could read, nothing is
8
+ // frontend-only state.
9
+
10
+ (() => {
11
+ "use strict";
12
+
13
+ const $ = (sel) => document.querySelector(sel);
14
+ const $$ = (sel) => Array.from(document.querySelectorAll(sel));
15
+
16
+ // ---- navigation --------------------------------------------------
17
+ $$("#nav button").forEach((btn) => {
18
+ btn.addEventListener("click", () => {
19
+ $$("#nav button").forEach((b) => b.classList.remove("active"));
20
+ $$(".view").forEach((v) => v.classList.remove("active"));
21
+ btn.classList.add("active");
22
+ $(`#view-${btn.dataset.view}`).classList.add("active");
23
+ });
24
+ });
25
+
26
+ // ---- API helpers ---------------------------------------------------
27
+ async function api(method, path, body) {
28
+ const res = await fetch(path, {
29
+ method,
30
+ headers: { "content-type": "application/json", "x-request-id": crypto.randomUUID() },
31
+ body: body ? JSON.stringify(body) : undefined,
32
+ });
33
+ const data = await res.json().catch(() => ({}));
34
+ return { ok: res.ok, status: res.status, data };
35
+ }
36
+
37
+ // ---- dashboard -------------------------------------------------------
38
+ async function refreshDashboard() {
39
+ const { data } = await api("GET", "/api/health");
40
+ $("#status-dot").className = data.status === "healthy" ? "up" : "down";
41
+ $("#dash-status").textContent = data.status || "unknown";
42
+ $("#dash-agents").textContent = data.counts?.active_agents ?? 0;
43
+ $("#dash-sessions").textContent = data.counts?.active_browser_sessions ?? 0;
44
+ $("#dash-jobs").textContent = data.counts?.active_search_jobs ?? 0;
45
+ $("#dash-uptime").textContent = Math.round((data.uptime_ms || 0) / 1000) + "s";
46
+ const rows = Object.entries(data.subsystems || {})
47
+ .map(([name, st]) => `<tr><td>${name}</td><td><span class="pill ${st === "up" ? "ok" : "err"}">${st}</span></td></tr>`)
48
+ .join("");
49
+ $("#dash-subsystems").innerHTML = rows;
50
+ }
51
+
52
+ // ---- agent console -----------------------------------------------------
53
+ let selectedAgent = null;
54
+
55
+ async function refreshAgents() {
56
+ const { data } = await api("GET", "/api/agents");
57
+ const rows = (data.agents || []).map((a) => `
58
+ <tr data-agent="${a.agent_id}" style="cursor:pointer">
59
+ <td>${a.agent_id}</td>
60
+ <td><span class="pill">${a.state}</span></td>
61
+ <td>${a.action_count}</td>
62
+ <td class="muted">${(a.capabilities || []).join(", ")}</td>
63
+ </tr>`).join("");
64
+ $("#agents-table").innerHTML = rows;
65
+ $$("#agents-table tr").forEach((tr) => {
66
+ tr.addEventListener("click", () => { selectedAgent = tr.dataset.agent; });
67
+ });
68
+ }
69
+
70
+ $("#agent-create").addEventListener("click", async () => {
71
+ await api("POST", "/api/agents", { capabilities: ["create_session", "open_tab", "navigate",
72
+ "read_page", "click", "type", "scroll", "screenshot", "extract_links", "close_session",
73
+ { search: "start" }] });
74
+ refreshAgents();
75
+ });
76
+
77
+ // ---- browser -----------------------------------------------------------
78
+ async function refreshSessions() {
79
+ const { data } = await api("GET", "/api/browser/sessions");
80
+ $("#sessions-table").innerHTML = (data.sessions || []).map((s) => `
81
+ <tr><td>${s.session_id}</td><td><span class="pill ${s.status === 'ready' ? 'ok' : ''}">${s.status}</span></td>
82
+ <td>${(s.tabs || []).length}</td><td class="muted">${s.owner_agent_id ?? "--"}</td></tr>`).join("");
83
+ }
84
+ $("#browser-create").addEventListener("click", async () => {
85
+ await api("POST", "/api/browser/sessions", { profile: "manual" });
86
+ refreshSessions();
87
+ });
88
+
89
+ // ---- search --------------------------------------------------------------
90
+ async function refreshJobs() {
91
+ const { data } = await api("GET", "/api/search");
92
+ $("#jobs-table").innerHTML = (data.jobs || []).map((j) => `
93
+ <tr><td>${j.job_id}</td><td><span class="pill">${j.stage}</span></td>
94
+ <td>${j.iteration}</td><td class="muted">${j.query}</td></tr>`).join("");
95
+ }
96
+ $("#search-start").addEventListener("click", async () => {
97
+ const query = $("#search-query-input").value.trim();
98
+ if (!query) return;
99
+ await api("POST", "/api/search", { query });
100
+ refreshJobs();
101
+ });
102
+
103
+ // ---- messages --------------------------------------------------------------
104
+ async function refreshMessages() {
105
+ const conv = $("#msg-conversation").value.trim();
106
+ if (!conv) return;
107
+ const { data } = await api("GET", `/api/messages?conversation_id=${encodeURIComponent(conv)}`);
108
+ $("#messages-table").innerHTML = (data.messages || []).map((m) => `
109
+ <tr><td class="muted">${new Date(m.timestamp).toLocaleTimeString()}</td>
110
+ <td>${m.sender}</td><td>${m.recipient}</td><td><span class="pill">${m.status}</span></td>
111
+ <td>${JSON.stringify(m.payload)}</td></tr>`).join("");
112
+ }
113
+ $("#msg-send").addEventListener("click", async () => {
114
+ const conversation_id = $("#msg-conversation").value.trim() || crypto.randomUUID();
115
+ $("#msg-conversation").value = conversation_id;
116
+ await api("POST", "/api/messages", {
117
+ sender: $("#msg-sender").value || "operator",
118
+ recipient: $("#msg-recipient").value || "system",
119
+ conversation_id,
120
+ message_type: "user_to_agent",
121
+ payload: { text: $("#msg-text").value },
122
+ });
123
+ refreshMessages();
124
+ });
125
+
126
+ // ---- audit -----------------------------------------------------------------
127
+ async function refreshAudit() {
128
+ const { data } = await api("GET", "/api/events/recent?limit=200");
129
+ $("#audit-table").innerHTML = (data.events || []).map((e) => `
130
+ <tr><td class="muted">${e.event_id}</td><td>${e.category}</td>
131
+ <td class="muted">${e.subject ? `${e.subject.type}:${e.subject.id}` : "--"}</td>
132
+ <td class="muted">${e.operation_id ?? "--"}</td>
133
+ <td class="muted">${new Date(e.emitted_at_ms).toLocaleTimeString()}</td></tr>`).join("");
134
+ }
135
+ $("#audit-refresh").addEventListener("click", refreshAudit);
136
+
137
+ // ---- live event stream -----------------------------------------------------
138
+ function connectEventStream() {
139
+ const proto = location.protocol === "https:" ? "wss" : "ws";
140
+ const ws = new WebSocket(`${proto}://${location.host}/api/events/stream`);
141
+ ws.onmessage = (msg) => {
142
+ let parsed;
143
+ try { parsed = JSON.parse(msg.data); } catch { return; }
144
+ if (parsed.type !== "event") return;
145
+ const e = parsed.event;
146
+ const row = document.createElement("div");
147
+ row.className = "row";
148
+ row.innerHTML = `<span class="ts">${new Date(e.emitted_at_ms).toLocaleTimeString()}</span> ` +
149
+ `<span class="cat">${e.category}</span> ${JSON.stringify(e.data)}`;
150
+ const log = $("#event-log");
151
+ log.prepend(row);
152
+ while (log.children.length > 500) log.removeChild(log.lastChild);
153
+ // Live-refresh whichever views this event is relevant to.
154
+ if (e.category.startsWith("agent.")) refreshAgents();
155
+ if (e.category.startsWith("browser.")) refreshSessions();
156
+ if (e.category.startsWith("search.")) refreshJobs();
157
+ refreshDashboard();
158
+ };
159
+ ws.onclose = () => setTimeout(connectEventStream, 2000);
160
+ }
161
+
162
+ // ---- boot ---------------------------------------------------------------------
163
+ refreshDashboard();
164
+ refreshAgents();
165
+ refreshSessions();
166
+ refreshJobs();
167
+ refreshAudit();
168
+ connectEventStream();
169
+ setInterval(refreshDashboard, 5000);
170
+ })();