@agent-arc-status/dashboard 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/bin/arc-dashboard.d.ts +3 -0
- package/dist/bin/arc-dashboard.d.ts.map +1 -0
- package/dist/bin/arc-dashboard.js +13 -0
- package/dist/bin/arc-dashboard.js.map +1 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +138 -0
- package/dist/server.js.map +1 -0
- package/dist/sse.d.ts +10 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +20 -0
- package/dist/sse.js.map +1 -0
- package/dist/store.d.ts +15 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +47 -0
- package/dist/store.js.map +1 -0
- package/package.json +56 -0
- package/public/app.js +94 -0
- package/public/index.html +20 -0
- package/public/style.css +147 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joe Fisher
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @agent-arc-status/dashboard — `arc-dashboard`
|
|
2
|
+
|
|
3
|
+
A **zero-dependency** live web view of in-flight [Agent Arc Status
|
|
4
|
+
Protocol](https://github.com/joethefisher/agent-arc-status) arcs. POST events to `/ingest`; watch
|
|
5
|
+
them render as cards with progress bars, status, milestone counts, blockers, and a stall glow.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @agent-arc-status/dashboard # http://127.0.0.1:8686
|
|
9
|
+
# then POST arc.status events to http://127.0.0.1:8686/ingest
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Pairs directly with `@agent-arc-status/emitter`'s `httpTransport` (point it at `/ingest`).
|
|
13
|
+
|
|
14
|
+
## Design
|
|
15
|
+
|
|
16
|
+
A deliberately thin server: it validates untrusted webhook events, folds them to arc **state**, and
|
|
17
|
+
streams that state as JSON over Server-Sent Events. The browser renders cards from JSON using
|
|
18
|
+
`textContent`/`createElement` — **never `innerHTML`** — so a hostile `title` or `body` can't execute.
|
|
19
|
+
That trust boundary (spec §9.4) is the whole point.
|
|
20
|
+
|
|
21
|
+
- `POST /ingest` — validate (400 on bad input), 64KB body cap (413), optional HMAC-SHA256 (401), then broadcast.
|
|
22
|
+
- `GET /events` — SSE: an initial `snapshot` of every arc's `reduceArc` state, then live `event` frames.
|
|
23
|
+
- `GET /` — a single static page (`default-src 'self'` CSP, no inline scripts).
|
|
24
|
+
|
|
25
|
+
Binds `127.0.0.1` by default. The store is bounded (evicts oldest arcs). Programmatic use:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { startDashboard } from "@agent-arc-status/dashboard";
|
|
29
|
+
const dash = await startDashboard({ port: 8686, hmacSecret: process.env.ARC_STATUS_SECRET });
|
|
30
|
+
console.log(dash.url);
|
|
31
|
+
// ... await dash.close();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Environment: `PORT`, `HOST`, `ARC_STATUS_SECRET`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arc-dashboard.d.ts","sourceRoot":"","sources":["../../src/bin/arc-dashboard.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startDashboard } from "../server.js";
|
|
3
|
+
const port = process.env["PORT"] !== undefined ? Number(process.env["PORT"]) : 8686;
|
|
4
|
+
const host = process.env["HOST"] ?? "127.0.0.1";
|
|
5
|
+
const hmacSecret = process.env["ARC_STATUS_SECRET"];
|
|
6
|
+
const dashboard = await startDashboard(hmacSecret !== undefined ? { port, host, hmacSecret } : { port, host });
|
|
7
|
+
process.stderr.write(`arc-dashboard on ${dashboard.url} (POST arc.status events to ${dashboard.url}/ingest)\n`);
|
|
8
|
+
const stop = () => {
|
|
9
|
+
void dashboard.close().then(() => process.exit(0));
|
|
10
|
+
};
|
|
11
|
+
process.once("SIGINT", stop);
|
|
12
|
+
process.once("SIGTERM", stop);
|
|
13
|
+
//# sourceMappingURL=arc-dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arc-dashboard.js","sourceRoot":"","sources":["../../src/bin/arc-dashboard.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACpF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,WAAW,CAAC;AAChD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AAEpD,MAAM,SAAS,GAAG,MAAM,cAAc,CACpC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CACvE,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,SAAS,CAAC,GAAG,gCAAgC,SAAS,CAAC,GAAG,YAAY,CAAC,CAAC;AAEjH,MAAM,IAAI,GAAG,GAAS,EAAE;IACtB,KAAK,SAAS,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC,CAAC;AACF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC7B,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thin, zero-dependency dashboard server: validate untrusted webhook events,
|
|
3
|
+
* fold them to arc state, and stream that state (as DATA, never HTML) to
|
|
4
|
+
* browsers over SSE. The browser renders cards from JSON with textContent, so a
|
|
5
|
+
* hostile `title`/`body` can never execute (spec §9.4). Binds 127.0.0.1 by
|
|
6
|
+
* default; caps request bodies; optional HMAC-SHA256 on ingest.
|
|
7
|
+
*/
|
|
8
|
+
import { type Server } from "node:http";
|
|
9
|
+
import { type CadenceConfig } from "@agent-arc-status/reference";
|
|
10
|
+
export interface DashboardOptions {
|
|
11
|
+
host?: string;
|
|
12
|
+
port?: number;
|
|
13
|
+
/** If set, require a valid HMAC-SHA256 `X-Webhook-Signature` on /ingest. */
|
|
14
|
+
hmacSecret?: string;
|
|
15
|
+
maxBodyBytes?: number;
|
|
16
|
+
cadence?: CadenceConfig;
|
|
17
|
+
}
|
|
18
|
+
export declare function createServer(options?: DashboardOptions): Server;
|
|
19
|
+
export interface RunningDashboard {
|
|
20
|
+
server: Server;
|
|
21
|
+
url: string;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/** Create and start a dashboard, resolving with its URL and a close(). */
|
|
25
|
+
export declare function startDashboard(options?: DashboardOptions): Promise<RunningDashboard>;
|
|
26
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAoC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAK/F,OAAO,EAAS,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAaxE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AA+CD,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA0DnE;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAexF"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thin, zero-dependency dashboard server: validate untrusted webhook events,
|
|
3
|
+
* fold them to arc state, and stream that state (as DATA, never HTML) to
|
|
4
|
+
* browsers over SSE. The browser renders cards from JSON with textContent, so a
|
|
5
|
+
* hostile `title`/`body` can never execute (spec §9.4). Binds 127.0.0.1 by
|
|
6
|
+
* default; caps request bodies; optional HMAC-SHA256 on ingest.
|
|
7
|
+
*/
|
|
8
|
+
import { createServer as httpCreateServer } from "node:http";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { parse } from "@agent-arc-status/reference";
|
|
14
|
+
import { ArcStore } from "./store.js";
|
|
15
|
+
import { SseHub } from "./sse.js";
|
|
16
|
+
const PUBLIC_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "public");
|
|
17
|
+
const STATIC = {
|
|
18
|
+
"/": { file: "index.html", type: "text/html; charset=utf-8", csp: true },
|
|
19
|
+
"/index.html": { file: "index.html", type: "text/html; charset=utf-8", csp: true },
|
|
20
|
+
"/app.js": { file: "app.js", type: "text/javascript; charset=utf-8" },
|
|
21
|
+
"/style.css": { file: "style.css", type: "text/css; charset=utf-8" },
|
|
22
|
+
};
|
|
23
|
+
function readBody(req, maxBytes) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let size = 0;
|
|
27
|
+
let tooBig = false;
|
|
28
|
+
req.on("data", (chunk) => {
|
|
29
|
+
size += chunk.length;
|
|
30
|
+
if (size > maxBytes)
|
|
31
|
+
tooBig = true;
|
|
32
|
+
else
|
|
33
|
+
chunks.push(chunk);
|
|
34
|
+
});
|
|
35
|
+
req.on("end", () => resolve({ body: Buffer.concat(chunks).toString("utf8"), tooBig }));
|
|
36
|
+
req.on("error", () => resolve({ body: "", tooBig: false }));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function signatureValid(secret, body, header) {
|
|
40
|
+
if (header === undefined)
|
|
41
|
+
return false;
|
|
42
|
+
const expected = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
43
|
+
const a = Buffer.from(expected);
|
|
44
|
+
const b = Buffer.from(header);
|
|
45
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
46
|
+
}
|
|
47
|
+
async function serveStatic(url, res) {
|
|
48
|
+
const entry = STATIC[url];
|
|
49
|
+
if (entry === undefined) {
|
|
50
|
+
res.writeHead(404);
|
|
51
|
+
res.end();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const content = await readFile(join(PUBLIC_DIR, entry.file));
|
|
56
|
+
const headers = { "content-type": entry.type };
|
|
57
|
+
if (entry.csp)
|
|
58
|
+
headers["content-security-policy"] = "default-src 'self'";
|
|
59
|
+
res.writeHead(200, headers);
|
|
60
|
+
res.end(content);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
res.writeHead(500);
|
|
64
|
+
res.end();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function createServer(options = {}) {
|
|
68
|
+
const maxBody = options.maxBodyBytes ?? 65536;
|
|
69
|
+
const store = new ArcStore();
|
|
70
|
+
const hub = new SseHub();
|
|
71
|
+
return httpCreateServer((req, res) => {
|
|
72
|
+
void (async () => {
|
|
73
|
+
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
74
|
+
const method = req.method ?? "GET";
|
|
75
|
+
if (method === "POST" && url === "/ingest") {
|
|
76
|
+
const { body, tooBig } = await readBody(req, maxBody);
|
|
77
|
+
if (tooBig) {
|
|
78
|
+
res.writeHead(413);
|
|
79
|
+
res.end();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (options.hmacSecret !== undefined) {
|
|
83
|
+
const sig = req.headers["x-webhook-signature"];
|
|
84
|
+
if (!signatureValid(options.hmacSecret, body, Array.isArray(sig) ? sig[0] : sig)) {
|
|
85
|
+
res.writeHead(401);
|
|
86
|
+
res.end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const result = parse(body);
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
93
|
+
res.end(JSON.stringify({ issues: result.issues }));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const state = store.append(result.event);
|
|
97
|
+
hub.broadcast("event", state);
|
|
98
|
+
res.writeHead(202);
|
|
99
|
+
res.end();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (method === "GET" && url === "/events") {
|
|
103
|
+
res.writeHead(200, {
|
|
104
|
+
"content-type": "text/event-stream",
|
|
105
|
+
"cache-control": "no-cache",
|
|
106
|
+
connection: "keep-alive",
|
|
107
|
+
});
|
|
108
|
+
hub.add(res);
|
|
109
|
+
hub.send(res, "snapshot", store.snapshots());
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (method === "GET") {
|
|
113
|
+
await serveStatic(url, res);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.writeHead(405);
|
|
117
|
+
res.end();
|
|
118
|
+
})();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Create and start a dashboard, resolving with its URL and a close(). */
|
|
122
|
+
export function startDashboard(options = {}) {
|
|
123
|
+
const host = options.host ?? "127.0.0.1";
|
|
124
|
+
const port = options.port ?? 8686;
|
|
125
|
+
const server = createServer(options);
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
server.listen(port, host, () => {
|
|
128
|
+
const address = server.address();
|
|
129
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : port;
|
|
130
|
+
resolve({
|
|
131
|
+
server,
|
|
132
|
+
url: `http://${host}:${boundPort}`,
|
|
133
|
+
close: () => new Promise((done) => server.close(() => done())),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
/** A minimal Server-Sent-Events hub: named events broadcast to all clients. */
|
|
3
|
+
export declare class SseHub {
|
|
4
|
+
#private;
|
|
5
|
+
add(res: ServerResponse): void;
|
|
6
|
+
send(res: ServerResponse, event: string, data: unknown): void;
|
|
7
|
+
broadcast(event: string, data: unknown): void;
|
|
8
|
+
get size(): number;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,+EAA+E;AAC/E,qBAAa,MAAM;;IAGjB,GAAG,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI;IAK9B,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IAI7D,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IAK7C,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A minimal Server-Sent-Events hub: named events broadcast to all clients. */
|
|
2
|
+
export class SseHub {
|
|
3
|
+
#clients = new Set();
|
|
4
|
+
add(res) {
|
|
5
|
+
this.#clients.add(res);
|
|
6
|
+
res.on("close", () => this.#clients.delete(res));
|
|
7
|
+
}
|
|
8
|
+
send(res, event, data) {
|
|
9
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
10
|
+
}
|
|
11
|
+
broadcast(event, data) {
|
|
12
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
13
|
+
for (const res of this.#clients)
|
|
14
|
+
res.write(payload);
|
|
15
|
+
}
|
|
16
|
+
get size() {
|
|
17
|
+
return this.#clients.size;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,MAAM,OAAO,MAAM;IACR,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE9C,GAAG,CAAC,GAAmB;QACrB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,CAAC,GAAmB,EAAE,KAAa,EAAE,IAAa;QACpD,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClE,CAAC;IAED,SAAS,CAAC,KAAa,EAAE,IAAa;QACpC,MAAM,OAAO,GAAG,UAAU,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;QACrE,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ;YAAE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;CACF"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ArcState, type ArcStatusEvent } from "@agent-arc-status/reference";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory arc store, bounded so a long-lived dashboard cannot grow without
|
|
4
|
+
* limit. Keeps the full event timeline per arc (cheap to fold) and evicts the
|
|
5
|
+
* oldest arcs once the cap is reached.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ArcStore {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(maxArcs?: number);
|
|
10
|
+
/** Append an event and return the arc's current reduced state. */
|
|
11
|
+
append(event: ArcStatusEvent): ArcState;
|
|
12
|
+
/** Current reduced state for every tracked arc, in first-seen order. */
|
|
13
|
+
snapshots(): ArcState[];
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE5F;;;;GAIG;AACH,qBAAa,QAAQ;;gBAKP,OAAO,SAAM;IAIzB,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,QAAQ;IAYvC,wEAAwE;IACxE,SAAS,IAAI,QAAQ,EAAE;CAkBxB"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { reduceArc } from "@agent-arc-status/reference";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory arc store, bounded so a long-lived dashboard cannot grow without
|
|
4
|
+
* limit. Keeps the full event timeline per arc (cheap to fold) and evicts the
|
|
5
|
+
* oldest arcs once the cap is reached.
|
|
6
|
+
*/
|
|
7
|
+
export class ArcStore {
|
|
8
|
+
#events = new Map();
|
|
9
|
+
#order = [];
|
|
10
|
+
#maxArcs;
|
|
11
|
+
constructor(maxArcs = 200) {
|
|
12
|
+
this.#maxArcs = maxArcs;
|
|
13
|
+
}
|
|
14
|
+
/** Append an event and return the arc's current reduced state. */
|
|
15
|
+
append(event) {
|
|
16
|
+
let bucket = this.#events.get(event.arc_id);
|
|
17
|
+
if (bucket === undefined) {
|
|
18
|
+
bucket = [];
|
|
19
|
+
this.#events.set(event.arc_id, bucket);
|
|
20
|
+
this.#order.push(event.arc_id);
|
|
21
|
+
this.#evict();
|
|
22
|
+
}
|
|
23
|
+
bucket.push(event);
|
|
24
|
+
return reduceArc(bucket);
|
|
25
|
+
}
|
|
26
|
+
/** Current reduced state for every tracked arc, in first-seen order. */
|
|
27
|
+
snapshots() {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const arcId of this.#order) {
|
|
30
|
+
const bucket = this.#events.get(arcId);
|
|
31
|
+
if (bucket !== undefined) {
|
|
32
|
+
const state = reduceArc(bucket);
|
|
33
|
+
if (state !== null)
|
|
34
|
+
out.push(state);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
#evict() {
|
|
40
|
+
while (this.#order.length > this.#maxArcs) {
|
|
41
|
+
const evicted = this.#order.shift();
|
|
42
|
+
if (evicted !== undefined)
|
|
43
|
+
this.#events.delete(evicted);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsC,MAAM,6BAA6B,CAAC;AAE5F;;;;GAIG;AACH,MAAM,OAAO,QAAQ;IACV,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IACvD,MAAM,GAAa,EAAE,CAAC;IACb,QAAQ,CAAS;IAE1B,YAAY,OAAO,GAAG,GAAG;QACvB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,kEAAkE;IAClE,MAAM,CAAC,KAAqB;QAC1B,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,GAAG,EAAE,CAAC;YACZ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACvC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnB,OAAO,SAAS,CAAC,MAAM,CAAa,CAAC;IACvC,CAAC;IAED,wEAAwE;IACxE,SAAS;QACP,MAAM,GAAG,GAAe,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACvC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;gBAChC,IAAI,KAAK,KAAK,IAAI;oBAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,OAAO,KAAK,SAAS;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-arc-status/dashboard",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "arc-dashboard: a zero-dependency live web view of in-flight Agent Arc Status Protocol arcs.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Joe Fisher",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/joethefisher/agent-arc-status.git",
|
|
10
|
+
"directory": "packages/dashboard"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/joethefisher/agent-arc-status#readme",
|
|
13
|
+
"bugs": "https://github.com/joethefisher/agent-arc-status/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"agent",
|
|
16
|
+
"agents",
|
|
17
|
+
"ai",
|
|
18
|
+
"llm",
|
|
19
|
+
"observability",
|
|
20
|
+
"progress",
|
|
21
|
+
"arc-status",
|
|
22
|
+
"dashboard"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"arc-dashboard": "./dist/bin/arc-dashboard.js"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/server.js",
|
|
29
|
+
"types": "./dist/server.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/server.d.ts",
|
|
33
|
+
"import": "./dist/server.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"public",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"provenance": true
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@agent-arc-status/reference": "^0.3.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/public/app.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Client for the Agent Arc Status dashboard. Renders arc cards from JSON state
|
|
2
|
+
// streamed over SSE. All event text is inserted with textContent / createElement
|
|
3
|
+
// (never innerHTML), so a hostile title or body can never execute — the trust
|
|
4
|
+
// boundary from spec §9.4 is enforced here in the DOM layer.
|
|
5
|
+
|
|
6
|
+
const PHASE_SYMBOL = { started: "▶", milestone: "✓", heartbeat: "·", done: "■", blocked: "⛔" };
|
|
7
|
+
const STALL_MS = 20 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
const arcsEl = document.getElementById("arcs");
|
|
10
|
+
const emptyEl = document.getElementById("empty");
|
|
11
|
+
const connEl = document.getElementById("conn");
|
|
12
|
+
|
|
13
|
+
const cards = new Map();
|
|
14
|
+
const receipt = new Map();
|
|
15
|
+
|
|
16
|
+
function el(tag, className, text) {
|
|
17
|
+
const node = document.createElement(tag);
|
|
18
|
+
if (className) node.className = className;
|
|
19
|
+
if (text !== undefined) node.textContent = text;
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function upsert(state) {
|
|
24
|
+
receipt.set(state.arc_id, Date.now());
|
|
25
|
+
|
|
26
|
+
let card = cards.get(state.arc_id);
|
|
27
|
+
if (!card) {
|
|
28
|
+
card = el("section", "card");
|
|
29
|
+
cards.set(state.arc_id, card);
|
|
30
|
+
arcsEl.appendChild(card);
|
|
31
|
+
}
|
|
32
|
+
card.dataset.status = state.status;
|
|
33
|
+
card.textContent = "";
|
|
34
|
+
|
|
35
|
+
const head = el("div", "head");
|
|
36
|
+
head.appendChild(el("span", "sym", PHASE_SYMBOL[state.phase] || "•"));
|
|
37
|
+
head.appendChild(el("span", "title", state.title));
|
|
38
|
+
head.appendChild(el("span", "badge " + state.status, state.status));
|
|
39
|
+
card.appendChild(head);
|
|
40
|
+
|
|
41
|
+
if (typeof state.step === "number" && typeof state.total === "number") {
|
|
42
|
+
const bar = el("div", "bar");
|
|
43
|
+
const fill = el("div", "fill");
|
|
44
|
+
fill.style.width = Math.round((state.step / state.total) * 100) + "%";
|
|
45
|
+
bar.appendChild(fill);
|
|
46
|
+
card.appendChild(bar);
|
|
47
|
+
let meta = "step " + state.step + "/" + state.total;
|
|
48
|
+
if (typeof state.eta_minutes === "number") meta += " · ETA " + state.eta_minutes + "m";
|
|
49
|
+
card.appendChild(el("div", "meta", meta));
|
|
50
|
+
} else if (typeof state.eta_minutes === "number") {
|
|
51
|
+
card.appendChild(el("div", "meta", "ETA " + state.eta_minutes + "m"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
card.appendChild(
|
|
55
|
+
el("div", "meta dim", state.eventCount + " events · " + state.milestones.length + " milestones"),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (state.blocked) {
|
|
59
|
+
const blocker = el("div", "blocker");
|
|
60
|
+
blocker.appendChild(el("strong", null, "blocked: "));
|
|
61
|
+
blocker.appendChild(document.createTextNode(state.blocked.title));
|
|
62
|
+
card.appendChild(blocker);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
emptyEl.style.display = cards.size ? "none" : "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function connect() {
|
|
69
|
+
const source = new EventSource("/events");
|
|
70
|
+
source.addEventListener("open", () => {
|
|
71
|
+
connEl.textContent = "live";
|
|
72
|
+
connEl.className = "conn live";
|
|
73
|
+
});
|
|
74
|
+
source.addEventListener("error", () => {
|
|
75
|
+
connEl.textContent = "reconnecting…";
|
|
76
|
+
connEl.className = "conn";
|
|
77
|
+
});
|
|
78
|
+
source.addEventListener("snapshot", (e) => {
|
|
79
|
+
JSON.parse(e.data).forEach(upsert);
|
|
80
|
+
});
|
|
81
|
+
source.addEventListener("event", (e) => {
|
|
82
|
+
upsert(JSON.parse(e.data));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
connect();
|
|
87
|
+
|
|
88
|
+
setInterval(() => {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
for (const [id, card] of cards) {
|
|
91
|
+
const stalled = card.dataset.status === "active" && now - (receipt.get(id) || now) > STALL_MS;
|
|
92
|
+
card.classList.toggle("stalled", stalled);
|
|
93
|
+
}
|
|
94
|
+
}, 15000);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Agent Arc Status</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<h1>Agent Arc Status</h1>
|
|
12
|
+
<span id="conn" class="conn">connecting…</span>
|
|
13
|
+
</header>
|
|
14
|
+
<main id="arcs" aria-live="polite"></main>
|
|
15
|
+
<p id="empty" class="empty">
|
|
16
|
+
No arcs yet. POST <code>arc.status</code> events to <code>/ingest</code> to watch them here.
|
|
17
|
+
</p>
|
|
18
|
+
<script src="/app.js"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
package/public/style.css
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0e1116;
|
|
3
|
+
--panel: #171b22;
|
|
4
|
+
--border: #262c36;
|
|
5
|
+
--text: #d6dde6;
|
|
6
|
+
--dim: #8b99a8;
|
|
7
|
+
--cyan: #34c3d6;
|
|
8
|
+
--green: #3fb950;
|
|
9
|
+
--red: #f85149;
|
|
10
|
+
--amber: #d29922;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
20
|
+
background: var(--bg);
|
|
21
|
+
color: var(--text);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
padding: 16px 24px;
|
|
29
|
+
border-bottom: 1px solid var(--border);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
h1 {
|
|
33
|
+
font-size: 16px;
|
|
34
|
+
margin: 0;
|
|
35
|
+
letter-spacing: 0.02em;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.conn {
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
color: var(--dim);
|
|
41
|
+
}
|
|
42
|
+
.conn.live {
|
|
43
|
+
color: var(--green);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
main {
|
|
47
|
+
display: grid;
|
|
48
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
49
|
+
gap: 14px;
|
|
50
|
+
padding: 24px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.empty {
|
|
54
|
+
padding: 0 24px;
|
|
55
|
+
color: var(--dim);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.card {
|
|
59
|
+
background: var(--panel);
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
border-left: 3px solid var(--dim);
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
padding: 14px 16px;
|
|
64
|
+
}
|
|
65
|
+
.card[data-status="active"] {
|
|
66
|
+
border-left-color: var(--cyan);
|
|
67
|
+
}
|
|
68
|
+
.card[data-status="done"] {
|
|
69
|
+
border-left-color: var(--green);
|
|
70
|
+
}
|
|
71
|
+
.card[data-status="blocked"] {
|
|
72
|
+
border-left-color: var(--red);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.head {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: baseline;
|
|
78
|
+
gap: 8px;
|
|
79
|
+
}
|
|
80
|
+
.sym {
|
|
81
|
+
font-size: 16px;
|
|
82
|
+
}
|
|
83
|
+
.title {
|
|
84
|
+
flex: 1;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
overflow-wrap: anywhere;
|
|
87
|
+
}
|
|
88
|
+
.badge {
|
|
89
|
+
font-size: 11px;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
padding: 1px 7px;
|
|
92
|
+
border-radius: 999px;
|
|
93
|
+
border: 1px solid var(--border);
|
|
94
|
+
color: var(--dim);
|
|
95
|
+
}
|
|
96
|
+
.badge.active {
|
|
97
|
+
color: var(--cyan);
|
|
98
|
+
}
|
|
99
|
+
.badge.done {
|
|
100
|
+
color: var(--green);
|
|
101
|
+
}
|
|
102
|
+
.badge.blocked {
|
|
103
|
+
color: var(--red);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.bar {
|
|
107
|
+
height: 6px;
|
|
108
|
+
margin: 10px 0 6px;
|
|
109
|
+
background: #0b0e13;
|
|
110
|
+
border-radius: 999px;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
.fill {
|
|
114
|
+
height: 100%;
|
|
115
|
+
background: var(--cyan);
|
|
116
|
+
transition: width 0.4s ease;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.meta {
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
color: var(--text);
|
|
122
|
+
}
|
|
123
|
+
.meta.dim {
|
|
124
|
+
color: var(--dim);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.blocker {
|
|
128
|
+
margin-top: 8px;
|
|
129
|
+
padding: 8px 10px;
|
|
130
|
+
background: rgba(248, 81, 73, 0.1);
|
|
131
|
+
border-radius: 6px;
|
|
132
|
+
color: var(--red);
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.card.stalled {
|
|
137
|
+
animation: glow 1.6s ease-in-out infinite;
|
|
138
|
+
}
|
|
139
|
+
@keyframes glow {
|
|
140
|
+
0%,
|
|
141
|
+
100% {
|
|
142
|
+
box-shadow: 0 0 0 0 rgba(210, 153, 34, 0);
|
|
143
|
+
}
|
|
144
|
+
50% {
|
|
145
|
+
box-shadow: 0 0 0 3px rgba(210, 153, 34, 0.35);
|
|
146
|
+
}
|
|
147
|
+
}
|