@nanobpm/nano-workforce 0.85.2 → 0.86.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/CHANGELOG.md +14 -0
- package/app/agentic/vocab/README.md +37 -0
- package/app/agentic/vocab/crew-vocab.test.ts +86 -0
- package/app/agentic/vocab/crew-vocab.ts +147 -0
- package/app/agentic/vocab/demand-report.test.ts +97 -0
- package/app/agentic/vocab/demand-report.ts +155 -0
- package/app/agentic/vocab/enrol.test.ts +49 -0
- package/app/agentic/vocab/enrol.ts +74 -0
- package/app/agentic/vocab/publish.test.ts +20 -0
- package/app/agentic/vocab/publish.ts +38 -0
- package/openapi.yaml +350 -0
- package/operations/enrolAgenticWorker.test.ts +89 -0
- package/operations/enrolAgenticWorker.ts +91 -0
- package/operations/getAgenticRegistry.test.ts +53 -0
- package/operations/getAgenticRegistry.ts +25 -0
- package/operations/getAgenticVocab.test.ts +48 -0
- package/operations/getAgenticVocab.ts +20 -0
- package/package.json +1 -1
- package/pages/board/board.css +119 -0
- package/pages/board/embed.html +30 -0
- package/pages/board/mount.js +131 -0
- package/pages/board/standalone.html +37 -0
- package/pages/board.page.json +51 -0
- package/pages/cockpit/embed.html +1 -0
- package/pages/cockpit/mount.js +14 -5
- package/pages/cockpit/standalone.html +15 -5
- package/pages/cockpit.page.json +2 -1
- package/pages/epic-detail.page.json +2 -1
- package/pages/epic.page.json +2 -1
- package/pages/feature.page.json +2 -1
- package/pages/home.page.json +4 -0
- package/pages/lineage.page.json +2 -1
- package/pages/overview.page.json +2 -1
- package/pages/tasks.page.json +4 -0
- package/test/cockpit-embed-endpoints.test.ts +73 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Tests for GET /app/api/agentic/vocab → operation `getAgenticVocab` (epic #152 / N1 #145).
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { assert, assertEquals } from "#test-assert";
|
|
4
|
+
import type { AppApi } from "@nanobpm/urban";
|
|
5
|
+
import { noopLog } from "../test/log.ts";
|
|
6
|
+
import handler from "./getAgenticVocab.ts";
|
|
7
|
+
|
|
8
|
+
const app = { log: noopLog() } as unknown as AppApi;
|
|
9
|
+
|
|
10
|
+
function input(headers: Record<string, string> = {}) {
|
|
11
|
+
return {
|
|
12
|
+
req: { method: "GET", path: "/app/api/agentic/vocab", query: new URLSearchParams(), headers: new Headers(headers), text: async () => "" } as any,
|
|
13
|
+
params: {},
|
|
14
|
+
query: {},
|
|
15
|
+
body: undefined,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("returns 200 with the published crew vocab view", async () => {
|
|
20
|
+
const res = (await handler(input(), app)) as any;
|
|
21
|
+
assertEquals(res.status, 200);
|
|
22
|
+
assert(typeof res.body.version === "number");
|
|
23
|
+
assert("planning" in res.body.networks, "networks tree is published");
|
|
24
|
+
const spar = res.body.requirements.find((r: any) => r.token === "planning.spar");
|
|
25
|
+
assert(spar !== undefined);
|
|
26
|
+
assertEquals(spar.seatsDistinctFamily, true);
|
|
27
|
+
assertEquals(spar.seats, ["red", "blue"]);
|
|
28
|
+
assert(spar.requires.includes("cognition=planning"));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("shared-secret guard rejects a missing secret when configured", async () => {
|
|
32
|
+
// The delegate captures the secret at import time, so re-import a cache-busted copy with the env
|
|
33
|
+
// var set to exercise the guarded 401 path and the authorized 200 path.
|
|
34
|
+
const prev = process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
35
|
+
process.env["NANO_PR_WEBHOOK_SECRET"] = "s3cr3t";
|
|
36
|
+
try {
|
|
37
|
+
const mod = await import(`./getAgenticVocab.ts?guard=${Date.now()}`);
|
|
38
|
+
const guarded = mod.default as typeof handler;
|
|
39
|
+
const bad = (await guarded(input(), app)) as any;
|
|
40
|
+
assertEquals(bad.status, 401);
|
|
41
|
+
const ok = (await guarded(input({ "x-hook-secret": "s3cr3t" }), app)) as any;
|
|
42
|
+
assertEquals(ok.status, 200);
|
|
43
|
+
assert(typeof ok.body.version === "number");
|
|
44
|
+
} finally {
|
|
45
|
+
if (prev === undefined) delete process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
46
|
+
else process.env["NANO_PR_WEBHOOK_SECRET"] = prev;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// GET /app/api/agentic/vocab → operationId `getAgenticVocab` (enrolment epic #152 / N1 #145, ADR 0059
|
|
2
|
+
// revised). Publishes the crew vocabulary artifact — the ONE capability→token map a worker resolves
|
|
3
|
+
// its SERVE set against — as `{ networks, requirements, version }`. Read-only and advisory; it never
|
|
4
|
+
// gates control flow.
|
|
5
|
+
//
|
|
6
|
+
// The optional shared-secret guard stays HERE (the runtime does not enforce OpenAPI `security`): when
|
|
7
|
+
// NANO_PR_WEBHOOK_SECRET is set, callers must present it via the x-hook-secret header. Unset → open.
|
|
8
|
+
import { vocabView } from "../app/agentic/vocab/publish.ts";
|
|
9
|
+
import { envVar } from "../app/version.ts";
|
|
10
|
+
import { defineOperation } from "../nano-generated/operations.ts";
|
|
11
|
+
|
|
12
|
+
const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
|
|
13
|
+
|
|
14
|
+
export default defineOperation("getAgenticVocab", ({ req }, app) => {
|
|
15
|
+
if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
|
|
16
|
+
app.log.warn("getAgenticVocab rejected: missing/invalid shared secret");
|
|
17
|
+
return { status: 401, body: { error: "unauthorized" } };
|
|
18
|
+
}
|
|
19
|
+
return { status: 200, body: vocabView() };
|
|
20
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.board {
|
|
6
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
7
|
+
color: #d7e0ea;
|
|
8
|
+
background: #0b0f14;
|
|
9
|
+
min-height: 100%;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
padding: 16px;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: 16px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.board-head {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 6px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.board .card {
|
|
24
|
+
background: #121821;
|
|
25
|
+
border: 1px solid #223042;
|
|
26
|
+
border-radius: 10px;
|
|
27
|
+
padding: 14px 16px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.board h2 {
|
|
31
|
+
margin: 0 0 8px;
|
|
32
|
+
font-size: 15px;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.board .networks {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
39
|
+
gap: 14px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.board table.grid {
|
|
43
|
+
width: 100%;
|
|
44
|
+
border-collapse: collapse;
|
|
45
|
+
font-size: 13px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.board table.grid th,
|
|
49
|
+
.board table.grid td {
|
|
50
|
+
text-align: left;
|
|
51
|
+
padding: 6px 8px;
|
|
52
|
+
border-bottom: 1px solid #1c2735;
|
|
53
|
+
vertical-align: top;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.board table.grid th {
|
|
57
|
+
color: #8aa0b8;
|
|
58
|
+
font-weight: 500;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.board td.num,
|
|
62
|
+
.board th.num {
|
|
63
|
+
text-align: right;
|
|
64
|
+
font-variant-numeric: tabular-nums;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.board code {
|
|
68
|
+
background: #0d141d;
|
|
69
|
+
border: 1px solid #223042;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
padding: 1px 5px;
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.board .muted {
|
|
76
|
+
color: #6b7f95;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.board .red {
|
|
80
|
+
color: #ff6b6b;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.board .row-missing {
|
|
84
|
+
background: rgba(255, 107, 107, 0.06);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.pill {
|
|
88
|
+
display: inline-block;
|
|
89
|
+
border-radius: 999px;
|
|
90
|
+
padding: 1px 9px;
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
text-transform: uppercase;
|
|
94
|
+
letter-spacing: 0.03em;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.pill-green {
|
|
98
|
+
background: rgba(63, 185, 80, 0.18);
|
|
99
|
+
color: #56d364;
|
|
100
|
+
border: 1px solid rgba(63, 185, 80, 0.4);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.pill-amber {
|
|
104
|
+
background: rgba(210, 153, 34, 0.18);
|
|
105
|
+
color: #e3b341;
|
|
106
|
+
border: 1px solid rgba(210, 153, 34, 0.4);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.pill-red {
|
|
110
|
+
background: rgba(248, 81, 73, 0.18);
|
|
111
|
+
color: #ff7b72;
|
|
112
|
+
border: 1px solid rgba(248, 81, 73, 0.4);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.pill-unknown {
|
|
116
|
+
background: rgba(139, 148, 158, 0.18);
|
|
117
|
+
color: #8b949e;
|
|
118
|
+
border: 1px solid rgba(139, 148, 158, 0.4);
|
|
119
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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>Demand × supply board (App View embed)</title>
|
|
7
|
+
<link rel="stylesheet" href="./board.css" />
|
|
8
|
+
<style>
|
|
9
|
+
html, body { margin: 0; height: 100%; background: #0b0f14; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<!--
|
|
14
|
+
Console App-View embed (ADR 0057). The console loads this document into its App-View surface and
|
|
15
|
+
hands it a host element; we mount the SAME demand×supply board via the SAME ./mount.js as the
|
|
16
|
+
standalone shell — only the host and the injected endpoint config differ, so the page renders
|
|
17
|
+
identically. When the console injects endpoint config via `window.__NANO_APP_VIEW__`, it wins.
|
|
18
|
+
-->
|
|
19
|
+
<main id="board-root"></main>
|
|
20
|
+
<script type="module">
|
|
21
|
+
import { mountBoard } from "./mount.js";
|
|
22
|
+
|
|
23
|
+
const cfg = window.__NANO_APP_VIEW__ ?? {};
|
|
24
|
+
mountBoard(cfg.host ?? document.getElementById("board-root"), {
|
|
25
|
+
reportUrl: cfg.reportUrl,
|
|
26
|
+
hookSecret: cfg.hookSecret,
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// pages/board/mount.js — the demand×supply board (enrolment epic #152 / N2 #153, ADR 0056 §8–10,
|
|
2
|
+
// ADR 0057 App View).
|
|
3
|
+
//
|
|
4
|
+
// A self-contained, dependency-free renderer: it polls the app's `GET /app/api/agentic/registry`
|
|
5
|
+
// report and paints the demand×supply matrix by network, the missing-agent-type reds, and the
|
|
6
|
+
// diversity-SLO lights. The SAME module renders embedded in the console (App View) and standalone on
|
|
7
|
+
// a phone — only the host element and the injected endpoint config differ. Read-only and advisory;
|
|
8
|
+
// it never gates a BPMN flow.
|
|
9
|
+
//
|
|
10
|
+
// The app has no browser build step, so the report projection is consumed straight off the wire
|
|
11
|
+
// (the typed core that produces it lives app-side in `app/agentic/vocab/demand-report.ts`); this
|
|
12
|
+
// module only renders it.
|
|
13
|
+
|
|
14
|
+
const DEFAULT_REPORT_URL = "/app/api/agentic/registry";
|
|
15
|
+
const POLL_MS = 5000;
|
|
16
|
+
|
|
17
|
+
/** Escape untrusted report strings before they touch innerHTML. */
|
|
18
|
+
function esc(value) {
|
|
19
|
+
return String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A status pill (green / amber / red) with a label. */
|
|
23
|
+
function pill(status, label) {
|
|
24
|
+
const s = status === "green" || status === "amber" || status === "red" ? status : "unknown";
|
|
25
|
+
return `<span class="pill pill-${s}">${esc(label ?? s)}</span>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderDiversity(diversity) {
|
|
29
|
+
if (!diversity) return "";
|
|
30
|
+
const rows = (diversity.roles ?? [])
|
|
31
|
+
.map((role) => {
|
|
32
|
+
const seats = (role.assignments ?? [])
|
|
33
|
+
.map((a) => `<code>${esc(a.seat)}</code>=${esc(a.family)}${a.instance ? ` <span class="muted">(${esc(a.instance)})</span>` : ""}`)
|
|
34
|
+
.join(" · ");
|
|
35
|
+
const strict = role.seatsDistinctFamily ? '<span class="muted">strict</span>' : '<span class="muted">warn</span>';
|
|
36
|
+
const collide = (role.collidingFamilies ?? []).length ? ` — <span class="red">collision: ${esc(role.collidingFamilies.join(", "))}</span>` : "";
|
|
37
|
+
return `<tr>
|
|
38
|
+
<td>${pill(role.status)}</td>
|
|
39
|
+
<td><code>${esc(role.token)}</code> ${strict}</td>
|
|
40
|
+
<td>${seats || '<span class="muted">no seated workers</span>'}${collide}</td>
|
|
41
|
+
</tr>`;
|
|
42
|
+
})
|
|
43
|
+
.join("");
|
|
44
|
+
return `<section class="card">
|
|
45
|
+
<h2>Diversity SLO ${pill(diversity.status)}</h2>
|
|
46
|
+
<p class="muted">Seat families per role — the red/blue seats want distinct families (ADR 0056 §10). A same-family collision on a strict role is a violation (red).</p>
|
|
47
|
+
${rows ? `<table class="grid"><thead><tr><th>SLO</th><th>Role</th><th>Seats</th></tr></thead><tbody>${rows}</tbody></table>` : '<p class="muted">No gradeable roles are seated yet.</p>'}
|
|
48
|
+
</section>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderNetwork(network) {
|
|
52
|
+
const rows = (network.tokens ?? [])
|
|
53
|
+
.map((token) => {
|
|
54
|
+
const status = token.satisfied ? "green" : "red";
|
|
55
|
+
const instances = (token.instances ?? []).length ? esc(token.instances.join(", ")) : '<span class="muted">—</span>';
|
|
56
|
+
return `<tr class="${token.satisfied ? "" : "row-missing"}">
|
|
57
|
+
<td>${pill(status, token.satisfied ? "supplied" : "missing")}</td>
|
|
58
|
+
<td><code>${esc(token.token)}</code></td>
|
|
59
|
+
<td class="num">${esc(token.supply)}</td>
|
|
60
|
+
<td>${instances}</td>
|
|
61
|
+
</tr>`;
|
|
62
|
+
})
|
|
63
|
+
.join("");
|
|
64
|
+
const missing = (network.missing ?? []).length
|
|
65
|
+
? `<p class="red">Missing agent types: ${network.missing.map((m) => `<code>${esc(m)}</code>`).join(", ")}</p>`
|
|
66
|
+
: '<p class="muted">All demanded tokens supplied.</p>';
|
|
67
|
+
return `<section class="card">
|
|
68
|
+
<h2><code>${esc(network.network)}</code></h2>
|
|
69
|
+
<table class="grid"><thead><tr><th>Supply</th><th>Token</th><th class="num">#</th><th>Instances</th></tr></thead><tbody>${rows}</tbody></table>
|
|
70
|
+
${missing}
|
|
71
|
+
</section>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function render(root, report) {
|
|
75
|
+
const networks = report.networks ?? [];
|
|
76
|
+
const overall = pill(report.status, `overall: ${report.status}`);
|
|
77
|
+
const demandNote = report.demandUnavailable
|
|
78
|
+
? '<p class="red">Demand unavailable — the deployed models could not be read from the engine, so this reflects live supply only.</p>'
|
|
79
|
+
: "";
|
|
80
|
+
const missingAll = (report.missing ?? []).length
|
|
81
|
+
? `<p class="red">Missing across all networks: ${report.missing.map((m) => `<code>${esc(m)}</code>`).join(", ")}</p>`
|
|
82
|
+
: '<p class="muted">No missing agent types.</p>';
|
|
83
|
+
const body = networks.length
|
|
84
|
+
? networks.map(renderNetwork).join("")
|
|
85
|
+
: '<section class="card"><p class="muted">No demand to show.</p></section>';
|
|
86
|
+
root.innerHTML = `<div class="board">
|
|
87
|
+
<header class="board-head">
|
|
88
|
+
<div>${overall} <span class="muted">vocab v${esc(report.version)} · ${esc(report.generatedAt)}</span></div>
|
|
89
|
+
${missingAll}
|
|
90
|
+
${demandNote}
|
|
91
|
+
</header>
|
|
92
|
+
${renderDiversity(report.diversity)}
|
|
93
|
+
<div class="networks">${body}</div>
|
|
94
|
+
</div>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderError(root, message) {
|
|
98
|
+
root.innerHTML = `<div class="board"><section class="card"><p class="red">Failed to load the demand×supply report: ${esc(message)}</p></section></div>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Mount the board into `host`, polling the registry report. `config.reportUrl` overrides the default
|
|
103
|
+
* endpoint; `config.hookSecret` is sent as `x-hook-secret` for a secured deployment.
|
|
104
|
+
*/
|
|
105
|
+
export function mountBoard(host, config = {}) {
|
|
106
|
+
const isElement = host != null && host.nodeType === 1 && typeof host.innerHTML === "string";
|
|
107
|
+
const root = isElement ? host : document.getElementById("board-root");
|
|
108
|
+
if (!root) return () => {};
|
|
109
|
+
const reportUrl = config.reportUrl ?? DEFAULT_REPORT_URL;
|
|
110
|
+
const headers = config.hookSecret ? { "x-hook-secret": config.hookSecret } : {};
|
|
111
|
+
let stopped = false;
|
|
112
|
+
let timer = null;
|
|
113
|
+
|
|
114
|
+
async function tick() {
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(reportUrl, { headers });
|
|
117
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
118
|
+
const report = await res.json();
|
|
119
|
+
if (!stopped) render(root, report);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (!stopped) renderError(root, err && err.message ? err.message : String(err));
|
|
122
|
+
}
|
|
123
|
+
if (!stopped) timer = setTimeout(tick, POLL_MS);
|
|
124
|
+
}
|
|
125
|
+
tick();
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
stopped = true;
|
|
129
|
+
if (timer) clearTimeout(timer);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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, viewport-fit=cover" />
|
|
6
|
+
<title>Demand × supply board</title>
|
|
7
|
+
<link rel="stylesheet" href="./board.css" />
|
|
8
|
+
<style>
|
|
9
|
+
html, body { margin: 0; height: 100%; background: #0b0f14; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<!--
|
|
14
|
+
Standalone shell (phone / direct link). Loads the SAME ./mount.js the console App-View embed
|
|
15
|
+
uses, so the standalone and embedded views render identically. Endpoints default to the current
|
|
16
|
+
origin; override the report endpoint via ?report=. For a secured deployment, pass the report
|
|
17
|
+
guard secret via the URL fragment #secret= (sent as x-hook-secret) — NOT the query string, so it
|
|
18
|
+
never leaks via server access logs, browser history, or the Referer header. The fragment is
|
|
19
|
+
stripped from the address bar immediately after it is read.
|
|
20
|
+
-->
|
|
21
|
+
<main id="board-root"></main>
|
|
22
|
+
<script type="module">
|
|
23
|
+
import { mountBoard } from "./mount.js";
|
|
24
|
+
|
|
25
|
+
const params = new URLSearchParams(location.search);
|
|
26
|
+
const secrets = new URLSearchParams(location.hash.slice(1));
|
|
27
|
+
const hookSecret = secrets.get("secret") ?? undefined;
|
|
28
|
+
if (location.hash) {
|
|
29
|
+
history.replaceState(null, "", location.pathname + location.search);
|
|
30
|
+
}
|
|
31
|
+
mountBoard(document.getElementById("board-root"), {
|
|
32
|
+
reportUrl: params.get("report") ?? undefined,
|
|
33
|
+
hookSecret,
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"title": "Board",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"type": "nav",
|
|
7
|
+
"id": "nav",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": "bar",
|
|
10
|
+
"title": "Nano Workforce",
|
|
11
|
+
"items": [
|
|
12
|
+
{ "label": "Overview", "page": "overview" },
|
|
13
|
+
{ "label": "Lineage", "page": "lineage" },
|
|
14
|
+
{ "label": "Convergence", "page": "home" },
|
|
15
|
+
{ "label": "Epics", "page": "epic" },
|
|
16
|
+
{ "label": "Feature", "page": "feature" },
|
|
17
|
+
{ "label": "Tasks", "page": "tasks" },
|
|
18
|
+
{ "label": "Cockpit", "page": "cockpit" },
|
|
19
|
+
{ "label": "Board", "page": "board" }
|
|
20
|
+
],
|
|
21
|
+
"sticky": true
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "text",
|
|
26
|
+
"id": "title",
|
|
27
|
+
"props": {
|
|
28
|
+
"text": "Demand \u00d7 supply board",
|
|
29
|
+
"variant": "heading"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "text",
|
|
34
|
+
"id": "intro",
|
|
35
|
+
"props": {
|
|
36
|
+
"text": "The enrolment epic's demand\u00d7supply matrix: what the deployed models DEMAND (their taskDefinition leaves) diffed against live SUPPLY (the connected workers, resolved through the crew vocabulary), grouped by network. Missing agent types \u2014 a demanded leaf with no supplier \u2014 light RED, and the diversity SLO grades the red/blue seats (green when the seats are distinct families, red on a same-family collision). Read-only and advisory; it never gates a BPMN flow. The same view renders embedded here (App View) and standalone on a phone.",
|
|
37
|
+
"variant": "sub"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "appView",
|
|
42
|
+
"id": "board",
|
|
43
|
+
"props": {
|
|
44
|
+
"title": "Demand \u00d7 supply",
|
|
45
|
+
"embed": "./board/embed.html",
|
|
46
|
+
"standalone": "./board/standalone.html",
|
|
47
|
+
"fill": true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
package/pages/cockpit/embed.html
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
const cfg = window.__NANO_APP_VIEW__ ?? {};
|
|
33
33
|
mountCockpit(cfg.host ?? document.getElementById("cockpit-root"), {
|
|
34
34
|
reportUrl: cfg.reportUrl,
|
|
35
|
+
transcriptsUrl: cfg.transcriptsUrl,
|
|
35
36
|
relayUrl: cfg.relayUrl,
|
|
36
37
|
hookSecret: cfg.hookSecret,
|
|
37
38
|
relayToken: cfg.relayToken,
|
package/pages/cockpit/mount.js
CHANGED
|
@@ -318,7 +318,8 @@ function relaySocketFactory(url) {
|
|
|
318
318
|
*
|
|
319
319
|
* @param {Element} host — where the cockpit renders (standalone: document.body; embedded: the App-View host).
|
|
320
320
|
* @param {object} [opts]
|
|
321
|
-
* @param {string} [opts.reportUrl] — the supply JSON endpoint the app serves
|
|
321
|
+
* @param {string} [opts.reportUrl] — the supply JSON endpoint the app serves (default
|
|
322
|
+
* `"app/api/agentic/supply"`, base-relative).
|
|
322
323
|
* @param {string} [opts.relayUrl] — the agentic channel WebSocket URL (with auth token + capability query).
|
|
323
324
|
* @param {string} [opts.hookSecret] — shared secret sent as `x-hook-secret` on the report fetch when the
|
|
324
325
|
* app's supply endpoint is guarded by NANO_PR_WEBHOOK_SECRET (omit for open deployments).
|
|
@@ -329,8 +330,8 @@ function relaySocketFactory(url) {
|
|
|
329
330
|
* least this many ms old (default 15000).
|
|
330
331
|
* @param {number} [opts.pastFetchTimeoutMs] — upper bound (ms) on a single past-sessions transcripts
|
|
331
332
|
* fetch; the fetch is aborted past this so a hung endpoint can't wedge the past panel (default 15000).
|
|
332
|
-
* @param {string} [opts.transcriptsUrl] — the captured-session list endpoint
|
|
333
|
-
*
|
|
333
|
+
* @param {string} [opts.transcriptsUrl] — the captured-session list endpoint backing the always-on
|
|
334
|
+
* "past sessions" history + replay (default `"app/api/agentic/transcripts"`, base-relative).
|
|
334
335
|
* @returns a handle with `.dispose()`.
|
|
335
336
|
*/
|
|
336
337
|
export function mountCockpit(host, opts = {}) {
|
|
@@ -340,8 +341,16 @@ export function mountCockpit(host, opts = {}) {
|
|
|
340
341
|
);
|
|
341
342
|
}
|
|
342
343
|
const doc = document;
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
// Base-relative defaults (no leading slash) so the browser resolves them against the document's
|
|
345
|
+
// baseURI. Standalone (served at the app root) this is the origin root; through the Studio console
|
|
346
|
+
// App-View the document base is the app-view path (`/console/app-view/<AppName>/`) while the iframe
|
|
347
|
+
// ORIGIN is the console (:8080) — an absolute (leading-slash) path would resolve against the console
|
|
348
|
+
// origin root (`:8080/app/api/agentic/supply` → 404) instead of the app-view base that proxies the
|
|
349
|
+
// API, leaving the cockpit empty (#279). A base-relative default lands on the right endpoint both
|
|
350
|
+
// ways, so the cockpit populates identically standalone and embedded even though the console never
|
|
351
|
+
// injects window.__NANO_APP_VIEW__.
|
|
352
|
+
const reportUrl = opts.reportUrl ?? "app/api/agentic/supply";
|
|
353
|
+
const transcriptsUrl = opts.transcriptsUrl ?? "app/api/agentic/transcripts";
|
|
345
354
|
const hookSecret = opts.hookSecret;
|
|
346
355
|
const relayUrl = opts.relayUrl ?? defaultRelayUrl(opts.relayToken, opts.relayCapability);
|
|
347
356
|
const refreshMs = opts.refreshMs ?? DEFAULT_REFRESH_MS;
|
|
@@ -31,15 +31,25 @@
|
|
|
31
31
|
import { mountCockpit } from "./mount.js";
|
|
32
32
|
|
|
33
33
|
// Standalone: mount into the page body. Endpoints default to the current origin; override via
|
|
34
|
-
// ?report= and ?relay= for a remote app. For a secured deployment, pass the
|
|
35
|
-
// via
|
|
34
|
+
// ?report= and ?relay= for a remote app. For a secured deployment, pass the sensitive
|
|
35
|
+
// credentials via the URL fragment — the report guard secret via #secret= (sent as
|
|
36
|
+
// x-hook-secret) and the relay credentials via #token= and #capability= — NOT the query string,
|
|
37
|
+
// so they never leak via server access logs, browser history, or the Referer header. The
|
|
38
|
+
// fragment is stripped from the address bar immediately after it is read.
|
|
36
39
|
const params = new URLSearchParams(location.search);
|
|
40
|
+
const secrets = new URLSearchParams(location.hash.slice(1));
|
|
41
|
+
const hookSecret = secrets.get("secret") ?? undefined;
|
|
42
|
+
const relayToken = secrets.get("token") ?? undefined;
|
|
43
|
+
const relayCapability = secrets.get("capability") ?? undefined;
|
|
44
|
+
if (location.hash) {
|
|
45
|
+
history.replaceState(null, "", location.pathname + location.search);
|
|
46
|
+
}
|
|
37
47
|
mountCockpit(document.getElementById("cockpit-root"), {
|
|
38
48
|
reportUrl: params.get("report") ?? undefined,
|
|
39
49
|
relayUrl: params.get("relay") ?? undefined,
|
|
40
|
-
hookSecret
|
|
41
|
-
relayToken
|
|
42
|
-
relayCapability
|
|
50
|
+
hookSecret,
|
|
51
|
+
relayToken,
|
|
52
|
+
relayCapability,
|
|
43
53
|
});
|
|
44
54
|
</script>
|
|
45
55
|
</body>
|
package/pages/cockpit.page.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
{ "label": "Epics", "page": "epic" },
|
|
16
16
|
{ "label": "Feature", "page": "feature" },
|
|
17
17
|
{ "label": "Tasks", "page": "tasks" },
|
|
18
|
-
{ "label": "Cockpit", "page": "cockpit" }
|
|
18
|
+
{ "label": "Cockpit", "page": "cockpit" },
|
|
19
|
+
{ "label": "Board", "page": "board" }
|
|
19
20
|
],
|
|
20
21
|
"sticky": true
|
|
21
22
|
}
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
{ "label": "Epics", "page": "epic" },
|
|
17
17
|
{ "label": "Feature", "page": "feature" },
|
|
18
18
|
{ "label": "Tasks", "page": "tasks" },
|
|
19
|
-
{ "label": "Cockpit", "page": "cockpit" }
|
|
19
|
+
{ "label": "Cockpit", "page": "cockpit" },
|
|
20
|
+
{ "label": "Board", "page": "board" }
|
|
20
21
|
]
|
|
21
22
|
}
|
|
22
23
|
},
|
package/pages/epic.page.json
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
{ "label": "Epics", "page": "epic" },
|
|
17
17
|
{ "label": "Feature", "page": "feature" },
|
|
18
18
|
{ "label": "Tasks", "page": "tasks" },
|
|
19
|
-
{ "label": "Cockpit", "page": "cockpit" }
|
|
19
|
+
{ "label": "Cockpit", "page": "cockpit" },
|
|
20
|
+
{ "label": "Board", "page": "board" }
|
|
20
21
|
]
|
|
21
22
|
}
|
|
22
23
|
},
|
package/pages/feature.page.json
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
{ "label": "Epics", "page": "epic" },
|
|
17
17
|
{ "label": "Feature", "page": "feature" },
|
|
18
18
|
{ "label": "Tasks", "page": "tasks" },
|
|
19
|
-
{ "label": "Cockpit", "page": "cockpit" }
|
|
19
|
+
{ "label": "Cockpit", "page": "cockpit" },
|
|
20
|
+
{ "label": "Board", "page": "board" }
|
|
20
21
|
]
|
|
21
22
|
}
|
|
22
23
|
},
|
package/pages/home.page.json
CHANGED
package/pages/lineage.page.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
{ "label": "Epics", "page": "epic" },
|
|
16
16
|
{ "label": "Feature", "page": "feature" },
|
|
17
17
|
{ "label": "Tasks", "page": "tasks" },
|
|
18
|
-
{ "label": "Cockpit", "page": "cockpit" }
|
|
18
|
+
{ "label": "Cockpit", "page": "cockpit" },
|
|
19
|
+
{ "label": "Board", "page": "board" }
|
|
19
20
|
],
|
|
20
21
|
"sticky": true
|
|
21
22
|
}
|
package/pages/overview.page.json
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
{ "label": "Epics", "page": "epic" },
|
|
17
17
|
{ "label": "Feature", "page": "feature" },
|
|
18
18
|
{ "label": "Tasks", "page": "tasks" },
|
|
19
|
-
{ "label": "Cockpit", "page": "cockpit" }
|
|
19
|
+
{ "label": "Cockpit", "page": "cockpit" },
|
|
20
|
+
{ "label": "Board", "page": "board" }
|
|
20
21
|
]
|
|
21
22
|
}
|
|
22
23
|
},
|