@legioncodeinc/hive 0.1.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 +661 -0
- package/README.md +262 -0
- package/dist/cli-commands.d.ts +41 -0
- package/dist/cli-commands.js +76 -0
- package/dist/cli-commands.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +46 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon/dashboard/app.js +118 -0
- package/dist/daemon/dashboard/host.d.ts +90 -0
- package/dist/daemon/dashboard/host.js +172 -0
- package/dist/daemon/dashboard/host.js.map +1 -0
- package/dist/daemon/dashboard/web-assets.d.ts +88 -0
- package/dist/daemon/dashboard/web-assets.js +195 -0
- package/dist/daemon/dashboard/web-assets.js.map +1 -0
- package/dist/daemon/fleet-status.d.ts +15 -0
- package/dist/daemon/fleet-status.js +56 -0
- package/dist/daemon/fleet-status.js.map +1 -0
- package/dist/daemon/gate.d.ts +55 -0
- package/dist/daemon/gate.js +128 -0
- package/dist/daemon/gate.js.map +1 -0
- package/dist/daemon/proxy.d.ts +38 -0
- package/dist/daemon/proxy.js +126 -0
- package/dist/daemon/proxy.js.map +1 -0
- package/dist/daemon/registry.d.ts +31 -0
- package/dist/daemon/registry.js +120 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/server.d.ts +41 -0
- package/dist/daemon/server.js +126 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/setup-auth.d.ts +37 -0
- package/dist/daemon/setup-auth.js +51 -0
- package/dist/daemon/setup-auth.js.map +1 -0
- package/dist/daemon/telemetry-proxy.d.ts +47 -0
- package/dist/daemon/telemetry-proxy.js +63 -0
- package/dist/daemon/telemetry-proxy.js.map +1 -0
- package/dist/dashboard/contracts.d.ts +235 -0
- package/dist/dashboard/contracts.js +43 -0
- package/dist/dashboard/contracts.js.map +1 -0
- package/dist/dashboard/web/app.d.ts +44 -0
- package/dist/dashboard/web/app.js +179 -0
- package/dist/dashboard/web/app.js.map +1 -0
- package/dist/dashboard/web/boot-route.d.ts +23 -0
- package/dist/dashboard/web/boot-route.js +29 -0
- package/dist/dashboard/web/boot-route.js.map +1 -0
- package/dist/dashboard/web/build-graph-button.d.ts +46 -0
- package/dist/dashboard/web/build-graph-button.js +81 -0
- package/dist/dashboard/web/build-graph-button.js.map +1 -0
- package/dist/dashboard/web/buzzing-screen.d.ts +30 -0
- package/dist/dashboard/web/buzzing-screen.js +135 -0
- package/dist/dashboard/web/buzzing-screen.js.map +1 -0
- package/dist/dashboard/web/folder-picker.d.ts +54 -0
- package/dist/dashboard/web/folder-picker.js +125 -0
- package/dist/dashboard/web/folder-picker.js.map +1 -0
- package/dist/dashboard/web/graph-layout.d.ts +101 -0
- package/dist/dashboard/web/graph-layout.js +0 -0
- package/dist/dashboard/web/graph-layout.js.map +1 -0
- package/dist/dashboard/web/harness-strip.d.ts +38 -0
- package/dist/dashboard/web/harness-strip.js +65 -0
- package/dist/dashboard/web/harness-strip.js.map +1 -0
- package/dist/dashboard/web/health-rail.d.ts +26 -0
- package/dist/dashboard/web/health-rail.js +61 -0
- package/dist/dashboard/web/health-rail.js.map +1 -0
- package/dist/dashboard/web/main.d.ts +22 -0
- package/dist/dashboard/web/main.js +51 -0
- package/dist/dashboard/web/main.js.map +1 -0
- package/dist/dashboard/web/needs-project.d.ts +51 -0
- package/dist/dashboard/web/needs-project.js +69 -0
- package/dist/dashboard/web/needs-project.js.map +1 -0
- package/dist/dashboard/web/page-frame.d.ts +95 -0
- package/dist/dashboard/web/page-frame.js +98 -0
- package/dist/dashboard/web/page-frame.js.map +1 -0
- package/dist/dashboard/web/pages/coming-soon.d.ts +23 -0
- package/dist/dashboard/web/pages/coming-soon.js +19 -0
- package/dist/dashboard/web/pages/coming-soon.js.map +1 -0
- package/dist/dashboard/web/pages/dashboard.d.ts +32 -0
- package/dist/dashboard/web/pages/dashboard.js +205 -0
- package/dist/dashboard/web/pages/dashboard.js.map +1 -0
- package/dist/dashboard/web/pages/graph.d.ts +72 -0
- package/dist/dashboard/web/pages/graph.js +362 -0
- package/dist/dashboard/web/pages/graph.js.map +1 -0
- package/dist/dashboard/web/pages/harnesses.d.ts +65 -0
- package/dist/dashboard/web/pages/harnesses.js +210 -0
- package/dist/dashboard/web/pages/harnesses.js.map +1 -0
- package/dist/dashboard/web/pages/health.d.ts +22 -0
- package/dist/dashboard/web/pages/health.js +91 -0
- package/dist/dashboard/web/pages/health.js.map +1 -0
- package/dist/dashboard/web/pages/lifecycle-panel.d.ts +60 -0
- package/dist/dashboard/web/pages/lifecycle-panel.js +187 -0
- package/dist/dashboard/web/pages/lifecycle-panel.js.map +1 -0
- package/dist/dashboard/web/pages/logs.d.ts +55 -0
- package/dist/dashboard/web/pages/logs.js +311 -0
- package/dist/dashboard/web/pages/logs.js.map +1 -0
- package/dist/dashboard/web/pages/memories.d.ts +35 -0
- package/dist/dashboard/web/pages/memories.js +439 -0
- package/dist/dashboard/web/pages/memories.js.map +1 -0
- package/dist/dashboard/web/pages/projects.d.ts +38 -0
- package/dist/dashboard/web/pages/projects.js +325 -0
- package/dist/dashboard/web/pages/projects.js.map +1 -0
- package/dist/dashboard/web/pages/roi-chart.d.ts +52 -0
- package/dist/dashboard/web/pages/roi-chart.js +133 -0
- package/dist/dashboard/web/pages/roi-chart.js.map +1 -0
- package/dist/dashboard/web/pages/roi.d.ts +93 -0
- package/dist/dashboard/web/pages/roi.js +375 -0
- package/dist/dashboard/web/pages/roi.js.map +1 -0
- package/dist/dashboard/web/pages/settings.d.ts +103 -0
- package/dist/dashboard/web/pages/settings.js +420 -0
- package/dist/dashboard/web/pages/settings.js.map +1 -0
- package/dist/dashboard/web/pages/sync.d.ts +59 -0
- package/dist/dashboard/web/pages/sync.js +280 -0
- package/dist/dashboard/web/pages/sync.js.map +1 -0
- package/dist/dashboard/web/panels.d.ts +178 -0
- package/dist/dashboard/web/panels.js +471 -0
- package/dist/dashboard/web/panels.js.map +1 -0
- package/dist/dashboard/web/primitives.d.ts +80 -0
- package/dist/dashboard/web/primitives.js +208 -0
- package/dist/dashboard/web/primitives.js.map +1 -0
- package/dist/dashboard/web/readiness-splash.d.ts +39 -0
- package/dist/dashboard/web/readiness-splash.js +201 -0
- package/dist/dashboard/web/readiness-splash.js.map +1 -0
- package/dist/dashboard/web/registry.d.ts +96 -0
- package/dist/dashboard/web/registry.js +99 -0
- package/dist/dashboard/web/registry.js.map +1 -0
- package/dist/dashboard/web/router.d.ts +48 -0
- package/dist/dashboard/web/router.js +76 -0
- package/dist/dashboard/web/router.js.map +1 -0
- package/dist/dashboard/web/scope-context.d.ts +155 -0
- package/dist/dashboard/web/scope-context.js +266 -0
- package/dist/dashboard/web/scope-context.js.map +1 -0
- package/dist/dashboard/web/service-icons.d.ts +33 -0
- package/dist/dashboard/web/service-icons.js +80 -0
- package/dist/dashboard/web/service-icons.js.map +1 -0
- package/dist/dashboard/web/setup-gate.d.ts +116 -0
- package/dist/dashboard/web/setup-gate.js +272 -0
- package/dist/dashboard/web/setup-gate.js.map +1 -0
- package/dist/dashboard/web/sidebar.d.ts +63 -0
- package/dist/dashboard/web/sidebar.js +91 -0
- package/dist/dashboard/web/sidebar.js.map +1 -0
- package/dist/dashboard/web/use-fleet-telemetry.d.ts +111 -0
- package/dist/dashboard/web/use-fleet-telemetry.js +278 -0
- package/dist/dashboard/web/use-fleet-telemetry.js.map +1 -0
- package/dist/dashboard/web/wire.d.ts +1937 -0
- package/dist/dashboard/web/wire.js +1837 -0
- package/dist/dashboard/web/wire.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/install/registry.d.ts +35 -0
- package/dist/install/registry.js +110 -0
- package/dist/install/registry.js.map +1 -0
- package/dist/lock.d.ts +10 -0
- package/dist/lock.js +87 -0
- package/dist/lock.js.map +1 -0
- package/dist/service/commands.d.ts +15 -0
- package/dist/service/commands.js +49 -0
- package/dist/service/commands.js.map +1 -0
- package/dist/service/index.d.ts +38 -0
- package/dist/service/index.js +174 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/platform.d.ts +37 -0
- package/dist/service/platform.js +67 -0
- package/dist/service/platform.js.map +1 -0
- package/dist/service/templates.d.ts +10 -0
- package/dist/service/templates.js +120 -0
- package/dist/service/templates.js.map +1 -0
- package/dist/shared/constants.d.ts +15 -0
- package/dist/shared/constants.js +19 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/daemon-routing.d.ts +11 -0
- package/dist/shared/daemon-routing.js +38 -0
- package/dist/shared/daemon-routing.js.map +1 -0
- package/dist/shared/fleet-readiness.d.ts +18 -0
- package/dist/shared/fleet-readiness.js +9 -0
- package/dist/shared/fleet-readiness.js.map +1 -0
- package/dist/shared/fleet-telemetry.d.ts +68 -0
- package/dist/shared/fleet-telemetry.js +61 -0
- package/dist/shared/fleet-telemetry.js.map +1 -0
- package/dist/shared/lifecycle-flags.d.ts +34 -0
- package/dist/shared/lifecycle-flags.js +39 -0
- package/dist/shared/lifecycle-flags.js.map +1 -0
- package/dist/shared/memory-types.d.ts +55 -0
- package/dist/shared/memory-types.js +75 -0
- package/dist/shared/memory-types.js.map +1 -0
- package/dist/shared/service-status.d.ts +82 -0
- package/dist/shared/service-status.js +97 -0
- package/dist/shared/service-status.js.map +1 -0
- package/dist/telemetry/emit.d.ts +197 -0
- package/dist/telemetry/emit.js +196 -0
- package/dist/telemetry/emit.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { acquireSingleInstanceLock, releaseSingleInstanceLock } from "../lock.js";
|
|
4
|
+
import { DOCTOR_STATUS_URL, HIVE_HOST, HIVE_PORT, HIVE_VERSION } from "../shared/constants.js";
|
|
5
|
+
import { mountDashboardAssets, mountDashboardShellFallback, renderShell } from "./dashboard/host.js";
|
|
6
|
+
import { fetchFleetStatus } from "./fleet-status.js";
|
|
7
|
+
import { createPortalGate } from "./gate.js";
|
|
8
|
+
import { createApiProxy } from "./proxy.js";
|
|
9
|
+
import { resolveRegisteredServiceNames } from "./registry.js";
|
|
10
|
+
import { createTelemetryStreamHandler } from "./telemetry-proxy.js";
|
|
11
|
+
function closeServer(server) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
server.close((error) => {
|
|
14
|
+
if (error instanceof Error) {
|
|
15
|
+
reject(error);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function createHive(options = {}) {
|
|
23
|
+
const host = options.host ?? HIVE_HOST;
|
|
24
|
+
const port = options.port ?? HIVE_PORT;
|
|
25
|
+
const now = options.now ?? Date.now;
|
|
26
|
+
const startedAt = now();
|
|
27
|
+
const app = new Hono();
|
|
28
|
+
const fleetStatusFetch = options.fleetStatusFetch ?? fetch;
|
|
29
|
+
const doctorStatusUrl = options.doctorStatusUrl ?? DOCTOR_STATUS_URL;
|
|
30
|
+
// PRD-003a: the server-side portal landing gate, registered FIRST so it runs ahead of every
|
|
31
|
+
// other route. It bypasses hive's own infra/asset/proxy paths and the two exempt screens
|
|
32
|
+
// (`/buzzing`, `/login`) internally; for every other path it evaluates health-then-auth and
|
|
33
|
+
// either redirects or falls through via `next()` to the routes below.
|
|
34
|
+
app.use("*", createPortalGate({
|
|
35
|
+
fleetStatusFetch,
|
|
36
|
+
doctorStatusUrl,
|
|
37
|
+
setupAuthFetch: options.setupAuthFetch,
|
|
38
|
+
registryPath: options.registryPath
|
|
39
|
+
}));
|
|
40
|
+
// The bundled SPA's static assets — specific, fixed paths registered before the shell catch-all
|
|
41
|
+
// (below) so they win.
|
|
42
|
+
mountDashboardAssets(app);
|
|
43
|
+
// PRD-005b: `/health` is content-negotiated (the gate makes the same distinction, `gate.ts`
|
|
44
|
+
// `isInfraPath`). A machine probe (doctor's own liveness check, monitoring) never asks for
|
|
45
|
+
// HTML and gets the existing cheap liveness JSON unchanged; a browser navigating to the new
|
|
46
|
+
// operator-facing `/health` page asks for HTML and gets the SAME SPA shell every other gated
|
|
47
|
+
// route renders (the bundled client resolves the `/health` registry page itself).
|
|
48
|
+
app.get("/health", (c) => {
|
|
49
|
+
const accept = c.req.header("accept") ?? "";
|
|
50
|
+
if (accept.includes("text/html")) {
|
|
51
|
+
c.header("cache-control", "no-cache");
|
|
52
|
+
return c.html(renderShell());
|
|
53
|
+
}
|
|
54
|
+
return c.json({
|
|
55
|
+
status: "ok",
|
|
56
|
+
uptimeMs: Math.max(0, now() - startedAt),
|
|
57
|
+
version: HIVE_VERSION
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
// hive-owned data route: aggregates doctor's supervisor status. Registered BEFORE the
|
|
61
|
+
// catch-all proxy so it wins (Hono runs matching handlers in registration order).
|
|
62
|
+
app.get("/api/fleet-status", async (c) => c.json(await fetchFleetStatus(fleetStatusFetch, doctorStatusUrl)));
|
|
63
|
+
// PRD-004a/PRD-005a: the full list of doctor-REGISTERED service names, even one hive's
|
|
64
|
+
// BFF proxy never routes to (e.g. doctor/hive itself). Lets `/buzzing` and the health
|
|
65
|
+
// rail render one tile/pill per registered service before any telemetry has arrived at all
|
|
66
|
+
// (bz-AC-1/bz-AC-2, hr-AC-1), independent of whichever daemons `resolveDaemonBases` proxies for.
|
|
67
|
+
app.get("/api/registered-services", (c) => c.json({ names: resolveRegisteredServiceNames({ registryPath: options.registryPath }) }));
|
|
68
|
+
// PRD-004/PRD-005: the same-origin relay of doctor's fleet-telemetry SSE stream
|
|
69
|
+
// (`telemetry-proxy.ts`). Registered BEFORE the generic `/api/*` proxy so THIS specific route
|
|
70
|
+
// wins; the browser only ever opens `/api/telemetry/stream`, never doctor's `:3852` directly.
|
|
71
|
+
app.get("/api/telemetry/stream", createTelemetryStreamHandler({
|
|
72
|
+
doctorEventsUrl: options.doctorEventsUrl,
|
|
73
|
+
fetchImpl: options.telemetryStreamFetch
|
|
74
|
+
}));
|
|
75
|
+
// Server-side federation (BFF): every other `/api/*` and `/setup/*` request is proxied over
|
|
76
|
+
// loopback to the workload daemon that owns it (honeycomb or nectar), resolved from
|
|
77
|
+
// doctor's registry. The browser only ever talks to hive's own origin.
|
|
78
|
+
const apiProxy = createApiProxy({ registryPath: options.registryPath, fetchImpl: options.proxyFetch });
|
|
79
|
+
app.all("/api/*", apiProxy);
|
|
80
|
+
app.all("/setup/*", apiProxy);
|
|
81
|
+
// PRD-003a: the SPA shell catch-all — MUST be registered LAST so every specific route above
|
|
82
|
+
// (assets, /health, /api/fleet-status, the BFF proxy) wins. Serves every gated page path the
|
|
83
|
+
// gate let through, plus `/buzzing` and `/login` themselves, plus any unknown deep link.
|
|
84
|
+
mountDashboardShellFallback(app);
|
|
85
|
+
return {
|
|
86
|
+
app,
|
|
87
|
+
host,
|
|
88
|
+
port,
|
|
89
|
+
startedAt
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function startHive(options = {}) {
|
|
93
|
+
const serveFn = options.serveFn ?? serve;
|
|
94
|
+
const hive = createHive(options);
|
|
95
|
+
const lockPaths = acquireSingleInstanceLock(options.lockPaths);
|
|
96
|
+
let server;
|
|
97
|
+
try {
|
|
98
|
+
server = serveFn({
|
|
99
|
+
fetch: hive.app.fetch,
|
|
100
|
+
hostname: hive.host,
|
|
101
|
+
port: hive.port
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
releaseSingleInstanceLock(lockPaths);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
let stopped = false;
|
|
109
|
+
const stop = async () => {
|
|
110
|
+
if (stopped)
|
|
111
|
+
return;
|
|
112
|
+
stopped = true;
|
|
113
|
+
try {
|
|
114
|
+
await closeServer(server);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
releaseSingleInstanceLock(lockPaths);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
...hive,
|
|
122
|
+
lockPaths,
|
|
123
|
+
stop
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/daemon/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate's AUTH input — PRD-003a/003b: "logged in" is credential presence, read through the
|
|
3
|
+
* proxied honeycomb `GET /setup/state` `authenticated` bit (no new portal session, ADR-0004).
|
|
4
|
+
*
|
|
5
|
+
* This mirrors `fleet-status.ts`'s shape deliberately: a small, injectable, fail-soft fetch that
|
|
6
|
+
* resolves the SAME honeycomb base the real `/setup/*` BFF proxy (`proxy.ts`) resolves requests to
|
|
7
|
+
* (via `resolveDaemonBases`), re-checks it is loopback (defense in depth, same posture as the
|
|
8
|
+
* proxy and `fleet-status.ts`), and degrades to `false` (not authenticated) on ANY failure —
|
|
9
|
+
* a non-loopback base, a network error, a non-OK response, invalid JSON, or a schema mismatch.
|
|
10
|
+
* l-AC-6 requires exactly this: a `/setup/state` fetch failure or timeout must read as "not logged
|
|
11
|
+
* in" (redirect to `/login`), never fail-soft into the dashboard.
|
|
12
|
+
*/
|
|
13
|
+
/** Minimal init surface for the auth fetch (mirrors `fleet-status.ts`'s `FleetFetchInit`). */
|
|
14
|
+
export type SetupAuthFetchInit = {
|
|
15
|
+
readonly redirect?: "error" | "follow" | "manual";
|
|
16
|
+
/** Ties the upstream `/setup/state` fetch to the caller's lifecycle (e.g. the incoming request). */
|
|
17
|
+
readonly signal?: AbortSignal;
|
|
18
|
+
};
|
|
19
|
+
export type SetupAuthFetchImpl = (input: string, init?: SetupAuthFetchInit) => Promise<Response>;
|
|
20
|
+
export interface FetchSetupAuthenticatedOptions {
|
|
21
|
+
/** Override the doctor registry file path the honeycomb base is resolved from. */
|
|
22
|
+
readonly registryPath?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Abort signal forwarded to the underlying fetch, so a disconnected client (the gate passes
|
|
25
|
+
* `c.req.raw.signal`) never leaves a hung `/setup/state` request pinned upstream. An abort
|
|
26
|
+
* surfaces as a rejected fetch and therefore resolves `false`, the same fail-closed posture
|
|
27
|
+
* as every other failure mode here.
|
|
28
|
+
*/
|
|
29
|
+
readonly signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve "is the operator logged in" for the gate's auth step (g-AC-4, l-AC-4). Reads the SAME
|
|
33
|
+
* honeycomb base the BFF proxy would (`resolveDaemonBases`), fetches `/setup/state` over loopback,
|
|
34
|
+
* and returns its `authenticated` bit. Fails closed to `false` on any error so a transient fault
|
|
35
|
+
* never fail-softs an unauthenticated visitor into the dashboard (l-AC-6).
|
|
36
|
+
*/
|
|
37
|
+
export declare function fetchSetupAuthenticated(fetchImpl?: SetupAuthFetchImpl, options?: FetchSetupAuthenticatedOptions): Promise<boolean>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate's AUTH input — PRD-003a/003b: "logged in" is credential presence, read through the
|
|
3
|
+
* proxied honeycomb `GET /setup/state` `authenticated` bit (no new portal session, ADR-0004).
|
|
4
|
+
*
|
|
5
|
+
* This mirrors `fleet-status.ts`'s shape deliberately: a small, injectable, fail-soft fetch that
|
|
6
|
+
* resolves the SAME honeycomb base the real `/setup/*` BFF proxy (`proxy.ts`) resolves requests to
|
|
7
|
+
* (via `resolveDaemonBases`), re-checks it is loopback (defense in depth, same posture as the
|
|
8
|
+
* proxy and `fleet-status.ts`), and degrades to `false` (not authenticated) on ANY failure —
|
|
9
|
+
* a non-loopback base, a network error, a non-OK response, invalid JSON, or a schema mismatch.
|
|
10
|
+
* l-AC-6 requires exactly this: a `/setup/state` fetch failure or timeout must read as "not logged
|
|
11
|
+
* in" (redirect to `/login`), never fail-soft into the dashboard.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { isLoopbackBaseUrl } from "../shared/daemon-routing.js";
|
|
15
|
+
import { resolveDaemonBases } from "./registry.js";
|
|
16
|
+
const SetupStateAuthSchema = z.object({
|
|
17
|
+
authenticated: z.boolean().catch(false)
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Resolve "is the operator logged in" for the gate's auth step (g-AC-4, l-AC-4). Reads the SAME
|
|
21
|
+
* honeycomb base the BFF proxy would (`resolveDaemonBases`), fetches `/setup/state` over loopback,
|
|
22
|
+
* and returns its `authenticated` bit. Fails closed to `false` on any error so a transient fault
|
|
23
|
+
* never fail-softs an unauthenticated visitor into the dashboard (l-AC-6).
|
|
24
|
+
*/
|
|
25
|
+
export async function fetchSetupAuthenticated(fetchImpl = fetch, options = {}) {
|
|
26
|
+
const base = resolveDaemonBases({ registryPath: options.registryPath }).honeycomb;
|
|
27
|
+
// Defense in depth, mirroring proxy.ts / fleet-status.ts: resolveDaemonBases only ever returns
|
|
28
|
+
// loopback origins, but a future change to base resolution must never turn this into an
|
|
29
|
+
// off-loopback fetch that could leak the request (or its redirect) off hive's trust boundary.
|
|
30
|
+
if (!isLoopbackBaseUrl(base))
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetchImpl(`${base}/setup/state`, { redirect: "error", signal: options.signal });
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
return false;
|
|
36
|
+
let parsedJson;
|
|
37
|
+
try {
|
|
38
|
+
parsedJson = await response.json();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const parsed = SetupStateAuthSchema.safeParse(parsedJson);
|
|
44
|
+
return parsed.success ? parsed.data.authenticated : false;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Network error, refused connection, timeout, or a blocked redirect: fail CLOSED (not logged in).
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=setup-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-auth.js","sourceRoot":"","sources":["../../src/daemon/setup-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;CACxC,CAAC,CAAC;AAsBH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,YAAgC,KAAK,EACrC,UAA0C,EAAE;IAE5C,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC;IAClF,+FAA+F;IAC/F,wFAAwF;IACxF,8FAA8F;IAC9F,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,IAAI,cAAc,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACvG,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;QAE/B,IAAI,UAAmB,CAAC;QACxB,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAC1D,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,kGAAkG;QAClG,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor -> hive fleet-telemetry SSE RELAY — hive PRD-004/PRD-005 (sd-AC-6,
|
|
3
|
+
* hr-AC-6, hm-AC-8, lg-AC-1), realizing ADR-0002's server-side BFF proxy for the SSE stream.
|
|
4
|
+
*
|
|
5
|
+
* Mounted at `GET /api/telemetry/stream` (`server.ts`). hive's SERVER connects to doctor's
|
|
6
|
+
* real `GET http://127.0.0.1:3852/events` and re-streams the raw `fleet-telemetry` SSE bytes to
|
|
7
|
+
* the browser same-origin. The browser NEVER opens a connection to doctor's `:3852` directly —
|
|
8
|
+
* this is the one and only seam that does, mirroring `proxy.ts`'s BFF posture for the REST surface.
|
|
9
|
+
*
|
|
10
|
+
* Memory-bounded by construction: this is a straight body-to-body PIPE (`new Response(upstream.body, ...)`),
|
|
11
|
+
* never buffered or accumulated here — the parent index's "never buffer more than the current event"
|
|
12
|
+
* constraint holds trivially because nothing is held in memory between upstream and downstream at all.
|
|
13
|
+
*
|
|
14
|
+
* Fail-soft: doctor unreachable, a non-2xx response, or a non-loopback configured URL all
|
|
15
|
+
* degrade to a 502 with no body, never a thrown error — the browser-side hook (`use-fleet-telemetry.ts`)
|
|
16
|
+
* treats a failed/errored EventSource as "SSE unavailable" and falls back to polling
|
|
17
|
+
* `GET /api/fleet-status`, exactly as it does for a mid-stream drop.
|
|
18
|
+
*/
|
|
19
|
+
import type { Context } from "hono";
|
|
20
|
+
/**
|
|
21
|
+
* The injectable fetch surface reaching doctor's status page (the global `fetch` in prod, a
|
|
22
|
+
* mock in tests). `redirect` is part of the surface so the relay can pin `redirect: "error"`
|
|
23
|
+
* (matching `proxy.ts`/`fleet-status.ts`): native fetch follows 30x by default, and
|
|
24
|
+
* `isLoopbackBaseUrl()` only validates the FIRST hop, so an unpinned redirect could take the
|
|
25
|
+
* relay off-loopback and defeat the SSRF guard.
|
|
26
|
+
*/
|
|
27
|
+
export type TelemetryFetch = (input: string, init?: {
|
|
28
|
+
readonly signal?: AbortSignal;
|
|
29
|
+
readonly redirect?: "error" | "follow" | "manual";
|
|
30
|
+
}) => Promise<Response>;
|
|
31
|
+
/** Options for {@link createTelemetryStreamHandler}. */
|
|
32
|
+
export interface CreateTelemetryStreamOptions {
|
|
33
|
+
/** Override doctor's events URL (defaults to the fixed loopback constant). */
|
|
34
|
+
readonly doctorEventsUrl?: string;
|
|
35
|
+
/** The fetch implementation used to reach doctor (defaults to the global `fetch`). */
|
|
36
|
+
readonly fetchImpl?: TelemetryFetch;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the `GET /api/telemetry/stream` handler. Register it on hive's Hono app BEFORE the
|
|
40
|
+
* generic `/api/*` BFF proxy (`proxy.ts`) so this specific route wins for this one path (Hono
|
|
41
|
+
* composes matching handlers in registration order, same discipline as `/api/fleet-status`).
|
|
42
|
+
*
|
|
43
|
+
* The upstream fetch is tied to the INCOMING request's abort signal, so when the browser closes
|
|
44
|
+
* its EventSource (navigation, unmount, tab close) hive's own connection to doctor closes
|
|
45
|
+
* too — no orphaned upstream sockets accumulate across reconnects.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createTelemetryStreamHandler(options?: CreateTelemetryStreamOptions): (c: Context) => Promise<Response>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor -> hive fleet-telemetry SSE RELAY — hive PRD-004/PRD-005 (sd-AC-6,
|
|
3
|
+
* hr-AC-6, hm-AC-8, lg-AC-1), realizing ADR-0002's server-side BFF proxy for the SSE stream.
|
|
4
|
+
*
|
|
5
|
+
* Mounted at `GET /api/telemetry/stream` (`server.ts`). hive's SERVER connects to doctor's
|
|
6
|
+
* real `GET http://127.0.0.1:3852/events` and re-streams the raw `fleet-telemetry` SSE bytes to
|
|
7
|
+
* the browser same-origin. The browser NEVER opens a connection to doctor's `:3852` directly —
|
|
8
|
+
* this is the one and only seam that does, mirroring `proxy.ts`'s BFF posture for the REST surface.
|
|
9
|
+
*
|
|
10
|
+
* Memory-bounded by construction: this is a straight body-to-body PIPE (`new Response(upstream.body, ...)`),
|
|
11
|
+
* never buffered or accumulated here — the parent index's "never buffer more than the current event"
|
|
12
|
+
* constraint holds trivially because nothing is held in memory between upstream and downstream at all.
|
|
13
|
+
*
|
|
14
|
+
* Fail-soft: doctor unreachable, a non-2xx response, or a non-loopback configured URL all
|
|
15
|
+
* degrade to a 502 with no body, never a thrown error — the browser-side hook (`use-fleet-telemetry.ts`)
|
|
16
|
+
* treats a failed/errored EventSource as "SSE unavailable" and falls back to polling
|
|
17
|
+
* `GET /api/fleet-status`, exactly as it does for a mid-stream drop.
|
|
18
|
+
*/
|
|
19
|
+
import { DOCTOR_EVENTS_URL } from "../shared/constants.js";
|
|
20
|
+
import { isLoopbackBaseUrl } from "../shared/daemon-routing.js";
|
|
21
|
+
/** The fail-soft response for an unreachable/misconfigured upstream: an empty 502, no retry-storm body to parse. */
|
|
22
|
+
function unreachableStreamResponse() {
|
|
23
|
+
return new Response(null, { status: 502 });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the `GET /api/telemetry/stream` handler. Register it on hive's Hono app BEFORE the
|
|
27
|
+
* generic `/api/*` BFF proxy (`proxy.ts`) so this specific route wins for this one path (Hono
|
|
28
|
+
* composes matching handlers in registration order, same discipline as `/api/fleet-status`).
|
|
29
|
+
*
|
|
30
|
+
* The upstream fetch is tied to the INCOMING request's abort signal, so when the browser closes
|
|
31
|
+
* its EventSource (navigation, unmount, tab close) hive's own connection to doctor closes
|
|
32
|
+
* too — no orphaned upstream sockets accumulate across reconnects.
|
|
33
|
+
*/
|
|
34
|
+
export function createTelemetryStreamHandler(options = {}) {
|
|
35
|
+
const doctorEventsUrl = options.doctorEventsUrl ?? DOCTOR_EVENTS_URL;
|
|
36
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
37
|
+
return async (c) => {
|
|
38
|
+
// Defense in depth (mirrors `proxy.ts`/`fleet-status.ts`): the URL is a fixed constant, never
|
|
39
|
+
// derived from external input, but this re-check ensures a future refactor can never turn
|
|
40
|
+
// this relay into an off-loopback fetch primitive.
|
|
41
|
+
if (!isLoopbackBaseUrl(doctorEventsUrl))
|
|
42
|
+
return unreachableStreamResponse();
|
|
43
|
+
try {
|
|
44
|
+
const upstream = await fetchImpl(doctorEventsUrl, { signal: c.req.raw.signal, redirect: "error" });
|
|
45
|
+
if (!upstream.ok || upstream.body === null)
|
|
46
|
+
return unreachableStreamResponse();
|
|
47
|
+
return new Response(upstream.body, {
|
|
48
|
+
status: 200,
|
|
49
|
+
headers: {
|
|
50
|
+
"content-type": "text/event-stream",
|
|
51
|
+
"cache-control": "no-store",
|
|
52
|
+
connection: "keep-alive",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Network error, refused connection, or an aborted fetch: fail soft, never throw. The
|
|
58
|
+
// browser-side EventSource treats this as a connection failure and retries/falls back.
|
|
59
|
+
return unreachableStreamResponse();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=telemetry-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-proxy.js","sourceRoot":"","sources":["../../src/daemon/telemetry-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAsBhE,oHAAoH;AACpH,SAAS,yBAAyB;IACjC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,4BAA4B,CAAC,UAAwC,EAAE;IACtF,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,iBAAiB,CAAC;IACrE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAK,KAAwB,CAAC;IAEjE,OAAO,KAAK,EAAE,CAAU,EAAqB,EAAE;QAC9C,8FAA8F;QAC9F,0FAA0F;QAC1F,mDAAmD;QACnD,IAAI,CAAC,iBAAiB,CAAC,eAAe,CAAC;YAAE,OAAO,yBAAyB,EAAE,CAAC;QAE5E,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;YACnG,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI;gBAAE,OAAO,yBAAyB,EAAE,CAAC;YAE/E,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;gBAClC,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACR,cAAc,EAAE,mBAAmB;oBACnC,eAAe,EAAE,UAAU;oBAC3B,UAAU,EAAE,YAAY;iBACxB;aACD,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,sFAAsF;YACtF,uFAAuF;YACvF,OAAO,yBAAyB,EAAE,CAAC;QACpC,CAAC;IACF,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PARTIAL copy of honeycomb's `src/dashboard/contracts.ts` — PRD-001b ("copy partially").
|
|
3
|
+
*
|
|
4
|
+
* Only the web-consumed ROI view-model types cross into hive: the subset that
|
|
5
|
+
* `web/wire.ts` (`EMPTY_ROI_TREND`, `EMPTY_ROI_VIEW`, `RoiTrendView`, `RoiView`) and the ROI
|
|
6
|
+
* pages (`web/pages/roi.tsx`, `web/pages/roi-chart.tsx`) import. The rest of honeycomb's
|
|
7
|
+
* contracts serve its own daemon-side view-models and STAY in honeycomb. hive owns a small
|
|
8
|
+
* copy of just the types its `wire` validates against.
|
|
9
|
+
*
|
|
10
|
+
* Copied verbatim from honeycomb `src/dashboard/contracts.ts` (the `PRD-060e` ROI section),
|
|
11
|
+
* with one cross-repo JSDoc `{@link}` path softened to a plain reference so the module is
|
|
12
|
+
* self-contained in hive.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The per-section status discriminant (e-AC-2). Every section of the {@link RoiView}
|
|
16
|
+
* carries ONE of these so the page renders the right treatment per section rather than a
|
|
17
|
+
* single page-wide state:
|
|
18
|
+
* - `ok` — a confident, complete figure (measured-or-fully-read).
|
|
19
|
+
* - `partial` — populated but degraded (some inputs read, some missing; e.g. "Claude Code only").
|
|
20
|
+
* - `absent` — no data yet (the page shows a dash glyph, NOT `$0.00`).
|
|
21
|
+
* - `unreachable` — an input could not be read (billing flap → dash + scoped retry).
|
|
22
|
+
* - `unauthenticated` — no credentials for the input (a Settings-CTA state, redacted).
|
|
23
|
+
* A measured zero is `ok` with a zero figure — DISTINCT from `absent`.
|
|
24
|
+
*/
|
|
25
|
+
export type RoiSectionStatus = "ok" | "partial" | "absent" | "unreachable" | "unauthenticated";
|
|
26
|
+
/**
|
|
27
|
+
* The cost basis a net/cost line rests on (e-AC-15, mirrors PRD-060f `cost_basis`):
|
|
28
|
+
* - `measured` — a billed/metered fact (org/workspace infra read from billing).
|
|
29
|
+
* - `allocated` — an estimated split of a shared cost down to a team/user; carries the
|
|
30
|
+
* SAME `est.`-class subordination as a modeled line, never a measured fact.
|
|
31
|
+
* - `none` — no cost line applies (e.g. a savings-only rollup).
|
|
32
|
+
* The page renders an `allocated` net distinctly and flags a MIXED-basis rollup.
|
|
33
|
+
*/
|
|
34
|
+
export type RoiCostBasisTag = "measured" | "allocated" | "none";
|
|
35
|
+
/**
|
|
36
|
+
* The modeled assumption carried on the wire as a DATA FIELD (e-AC-8, the daemon-side
|
|
37
|
+
* mirror of 060b's memory-injection assumption). The page's ⓘ popover + page-foot footnote
|
|
38
|
+
* read `assumptionText` VERBATIM from this single source — the page never hardcodes the copy.
|
|
39
|
+
* `signedOff` is `false` until the operator signs off (the page may mark the estimate provisional).
|
|
40
|
+
*/
|
|
41
|
+
export interface RoiAssumption {
|
|
42
|
+
/** The model-kind machine id (so the page can branch if more models are added). */
|
|
43
|
+
readonly kind: string;
|
|
44
|
+
/** The human-readable assumption string the disclosure surfaces verbatim (the ONE source). */
|
|
45
|
+
readonly assumptionText: string;
|
|
46
|
+
/** Whether the assumption is an operator-signed-off decision (`false` until sign-off). */
|
|
47
|
+
readonly signedOff: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The MEASURED + MODELED savings section (e-AC-3). Measured cache savings is the defensible
|
|
51
|
+
* headline (integer cents, `measured` tone); modeled memory-injection savings is the
|
|
52
|
+
* subordinate `est.` line carrying its {@link RoiAssumption}. `blendedCentsPerMtok` is the
|
|
53
|
+
* effective $/Mtok rate — `null` UNTIL token capture is live (the page shows a placeholder,
|
|
54
|
+
* never a fabricated `$0.00`, e-AC-11).
|
|
55
|
+
*/
|
|
56
|
+
export interface RoiSavingsSection {
|
|
57
|
+
/** The per-section status (e-AC-2): `absent` until any capture lands. */
|
|
58
|
+
readonly status: RoiSectionStatus;
|
|
59
|
+
/** MEASURED cache savings in INTEGER cents (the defensible, billed-fact headline). */
|
|
60
|
+
readonly measuredCents: number;
|
|
61
|
+
/** MODELED memory-injection savings in INTEGER cents (the subordinate `est.` line). */
|
|
62
|
+
readonly modeledCents: number;
|
|
63
|
+
/** The modeled estimate's assumption, carried as data (e-AC-8) — the single disclosure source. */
|
|
64
|
+
readonly assumption: RoiAssumption;
|
|
65
|
+
/** The effective blended rate in cents-per-Mtok, or `null` until capture is live (e-AC-11). */
|
|
66
|
+
readonly blendedCentsPerMtok: number | null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The INFRA COST section (e-AC-6, from 060c's billing read-model). `cents` is the measured
|
|
70
|
+
* DeepLake infra cost in INTEGER cents; the `status` carries 060c's billing discriminant so
|
|
71
|
+
* the page distinguishes a billed `$0` (`ok`) from "couldn't read billing" (`unreachable`) /
|
|
72
|
+
* "no credentials" (`unauthenticated`). `costBasis` is `measured` (org/workspace infra is
|
|
73
|
+
* always measured here; a per-team/user allocated share lives on a rollup, not this line).
|
|
74
|
+
*/
|
|
75
|
+
export interface RoiInfraSection {
|
|
76
|
+
/** The per-section status (e-AC-2/6): how the page tells billed-$0 from couldn't-read. */
|
|
77
|
+
readonly status: RoiSectionStatus;
|
|
78
|
+
/** The measured infra cost in INTEGER cents (`0` when absent — read the STATUS, not the number). */
|
|
79
|
+
readonly cents: number;
|
|
80
|
+
/** The cost basis on this line (e-AC-15): `measured` for org/workspace infra. */
|
|
81
|
+
readonly costBasis: RoiCostBasisTag;
|
|
82
|
+
}
|
|
83
|
+
/** One pollination contributor split line (the readable per-contributor breakdown the page shows). */
|
|
84
|
+
export interface RoiPollinationLine {
|
|
85
|
+
/** The contributor label (e.g. `haiku-skillify`, `deeplake-query`, `deeplake-embedding`). */
|
|
86
|
+
readonly label: string;
|
|
87
|
+
/** This contributor's cost in INTEGER cents. */
|
|
88
|
+
readonly cents: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The POLLINATION COST section (e-AC-6, from 060d's composer). `cents` is the itemized
|
|
92
|
+
* pollination total in INTEGER cents (Haiku skillify + DeepLake GPU sessions); `status` is
|
|
93
|
+
* 060d's worst-of-contributors discriminant mapped onto {@link RoiSectionStatus} so a
|
|
94
|
+
* confident total appears only when both halves are confident. `lines` is the readable split.
|
|
95
|
+
*/
|
|
96
|
+
export interface RoiPollinationSection {
|
|
97
|
+
/** The per-section status (e-AC-2): the worst of the two contributing halves (060d). */
|
|
98
|
+
readonly status: RoiSectionStatus;
|
|
99
|
+
/** The itemized pollination total in INTEGER cents. */
|
|
100
|
+
readonly cents: number;
|
|
101
|
+
/** The per-contributor split (Haiku + per session_type) the page renders. */
|
|
102
|
+
readonly lines: readonly RoiPollinationLine[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The NET-ROI section (e-AC-6). The net (`saved − (infra + pollination)`) is computed ONLY
|
|
106
|
+
* when its inputs are present (status `ok`); a missing/unreachable input leaves the section
|
|
107
|
+
* `unreachable`/`absent` with `computed: false` and `netCents: 0` — the net is NEVER
|
|
108
|
+
* fabricated from incomplete inputs (the page shows a dash glyph + scoped retry). Because the
|
|
109
|
+
* net folds a MODELED savings term it inherits `est.` (`modeled: true`); `costBasis` reflects
|
|
110
|
+
* whether the cost half is measured or carries an allocated share.
|
|
111
|
+
*/
|
|
112
|
+
export interface RoiNetSection {
|
|
113
|
+
/** The per-section status (e-AC-2/6): `ok` only when every input was present. */
|
|
114
|
+
readonly status: RoiSectionStatus;
|
|
115
|
+
/** True iff the net was actually computed from complete inputs; `false` ⇒ render a dash, not the number. */
|
|
116
|
+
readonly computed: boolean;
|
|
117
|
+
/** The net in INTEGER cents (`0` when `computed: false` — read the STATUS / `computed`, not this). */
|
|
118
|
+
readonly netCents: number;
|
|
119
|
+
/** True — the net folds a modeled term, so it ALWAYS carries `est.` (e-AC-3 net-hero inheritance). */
|
|
120
|
+
readonly modeled: boolean;
|
|
121
|
+
/** The cost basis on the net (e-AC-15): `allocated` when a per-team/user infra share fed it. */
|
|
122
|
+
readonly costBasis: RoiCostBasisTag;
|
|
123
|
+
}
|
|
124
|
+
/** One ROLLUP dimension the page's dimension switch offers (e-AC-13). */
|
|
125
|
+
export type RoiRollupDimension = "org" | "team" | "agent" | "project";
|
|
126
|
+
/** One row in a rollup: a dimension key + its summed measured/net cents + the basis flag. */
|
|
127
|
+
export interface RoiRollupRow {
|
|
128
|
+
/** The dimension key (the org id / team id / agent id / project id). */
|
|
129
|
+
readonly key: string;
|
|
130
|
+
/** A human label for the key (falls back to the key when no friendlier name resolves). */
|
|
131
|
+
readonly label: string;
|
|
132
|
+
/** Σ measured cache savings for this key, in INTEGER cents. */
|
|
133
|
+
readonly measuredSavingsCents: number;
|
|
134
|
+
/** Σ net (saved − cost) for this key, in INTEGER cents (allocated cost carries `est.`). */
|
|
135
|
+
readonly netCents: number;
|
|
136
|
+
/** Σ infra cost attributed to this key, in INTEGER cents. */
|
|
137
|
+
readonly infraCostCents: number;
|
|
138
|
+
/** The cost basis for THIS row (e-AC-15): `allocated` when its infra share is an estimate. */
|
|
139
|
+
readonly costBasis: RoiCostBasisTag;
|
|
140
|
+
/** Number of sessions (ledger rows) folded into this row. */
|
|
141
|
+
readonly sessions: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* One rollup VIEW (e-AC-13): all rows for a single dimension, plus a MIXED-BASIS flag. The
|
|
145
|
+
* daemon computes these as read-time `GROUP BY`s over `roi_metrics` (the component does NO
|
|
146
|
+
* grouping). `mixedBasis` is `true` when the rows span more than one `cost_basis`
|
|
147
|
+
* (`COUNT(DISTINCT cost_basis) > 1`) — the page flags it rather than silently blending.
|
|
148
|
+
*/
|
|
149
|
+
export interface RoiRollup {
|
|
150
|
+
/** The dimension this rollup groups by (e-AC-13). */
|
|
151
|
+
readonly dimension: RoiRollupDimension;
|
|
152
|
+
/** The grouped rows (one per distinct dimension key). */
|
|
153
|
+
readonly rows: readonly RoiRollupRow[];
|
|
154
|
+
/** True when the rows mix `measured` + `allocated` bases (e-AC-15 mixed-basis flag). */
|
|
155
|
+
readonly mixedBasis: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* THE COMPOSITE ROI VIEW-MODEL (e-AC-2). The `/roi` page is a PURE FUNCTION of this — every
|
|
159
|
+
* section carries its own {@link RoiSectionStatus}, all money is INTEGER cents, modeled
|
|
160
|
+
* savings carries its assumption as data, the daemon computes the {@link rollups}, and the
|
|
161
|
+
* per-user availability flag is `false` until verified backend claims land (060f gate, today
|
|
162
|
+
* always false). The `scopedAcrossDevices` flag tells the page the figure aggregates across
|
|
163
|
+
* devices (a `shared` read) vs only this machine (an `isolated` read), so the page can caption
|
|
164
|
+
* the scope honestly (e-AC-12).
|
|
165
|
+
*/
|
|
166
|
+
export interface RoiView {
|
|
167
|
+
/** The measured + modeled savings section (e-AC-3). */
|
|
168
|
+
readonly savings: RoiSavingsSection;
|
|
169
|
+
/** The infra cost section (e-AC-6, from 060c). */
|
|
170
|
+
readonly infra: RoiInfraSection;
|
|
171
|
+
/** The pollination cost section (e-AC-6, from 060d). */
|
|
172
|
+
readonly pollination: RoiPollinationSection;
|
|
173
|
+
/** The net-ROI section (e-AC-6) — computed ONLY from complete inputs, never fabricated. */
|
|
174
|
+
readonly net: RoiNetSection;
|
|
175
|
+
/** The org / team / agent / project rollups (e-AC-13), computed by the daemon as `GROUP BY`s. */
|
|
176
|
+
readonly rollups: readonly RoiRollup[];
|
|
177
|
+
/**
|
|
178
|
+
* The PER-USER availability flag (e-AC-14): `false` until verified backend user-claims are
|
|
179
|
+
* live (060f gate). It is `false` today — the page shows the "per-user requires verified
|
|
180
|
+
* login" empty state and NEVER a `$0` or a self-asserted name when this is false.
|
|
181
|
+
*/
|
|
182
|
+
readonly perUserAvailable: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* True when the read aggregated ACROSS DEVICES (a `shared` read_policy returned workspace-wide
|
|
185
|
+
* rows); false when it returned only this machine's rows (an `isolated` read). The page captions
|
|
186
|
+
* the scope honestly from this (e-AC-12). NOT a tenancy decision — the daemon already scoped the read.
|
|
187
|
+
*/
|
|
188
|
+
readonly scopedAcrossDevices: boolean;
|
|
189
|
+
/** The rate-table "as of" stamp (060b) so a stale rate is auditable on the page. */
|
|
190
|
+
readonly ratesAsOf: string;
|
|
191
|
+
}
|
|
192
|
+
/** One point in a trend series (e-AC-10): a period label + an INTEGER-cents value + its measured/modeled tag. */
|
|
193
|
+
export interface RoiTrendPoint {
|
|
194
|
+
/** The period label (e.g. an ISO date / `YYYY-MM` bucket). */
|
|
195
|
+
readonly period: string;
|
|
196
|
+
/** The value at this period in INTEGER cents. */
|
|
197
|
+
readonly cents: number;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* One trend SERIES (e-AC-10): a named line whose `modeled` tag drives the dashed (modeled) vs
|
|
201
|
+
* solid (measured) stroke. Money is INTEGER cents at every point. The page renders these as an
|
|
202
|
+
* inline-SVG chart (no charting dependency); this contract is purely the data the chart consumes.
|
|
203
|
+
*/
|
|
204
|
+
export interface RoiTrendSeries {
|
|
205
|
+
/** The series label (e.g. `measured-savings`, `modeled-savings`, `infra-cost`, `net`). */
|
|
206
|
+
readonly label: string;
|
|
207
|
+
/** True ⇒ the chart draws a DASHED stroke (modeled / `est.`); false ⇒ SOLID (measured). */
|
|
208
|
+
readonly modeled: boolean;
|
|
209
|
+
/** The series points, oldest-first, in INTEGER cents. */
|
|
210
|
+
readonly points: readonly RoiTrendPoint[];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* THE TREND VIEW-MODEL (e-AC-10) backing `GET /api/diagnostics/roi/trend`. `series` are the
|
|
214
|
+
* measured-vs-modeled lines (dashed/solid). `status` carries the same {@link RoiSectionStatus}
|
|
215
|
+
* vocabulary so an absent trend (no history before capture started) renders honestly rather
|
|
216
|
+
* than a fabricated flat line; `startedAt` marks when savings tracking began (or `''` when none).
|
|
217
|
+
*/
|
|
218
|
+
export interface RoiTrendView {
|
|
219
|
+
/** The overall trend status (e-AC-2 vocabulary): `absent` until a history exists. */
|
|
220
|
+
readonly status: RoiSectionStatus;
|
|
221
|
+
/** The measured/modeled series the inline-SVG chart draws (e-AC-10). */
|
|
222
|
+
readonly series: readonly RoiTrendSeries[];
|
|
223
|
+
/** When savings tracking began (ISO), or `''` when there is no history yet ("savings tracked from <date>"). */
|
|
224
|
+
readonly startedAt: string;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The honest-empty {@link RoiView} the page renders before the first load resolves, on any
|
|
228
|
+
* failure, or on a genuine first-run/empty workspace (e-AC-5). Every section is `absent`
|
|
229
|
+
* (the page shows a DASH glyph, NOT `$0.00`), the net is NOT computed, `blendedCentsPerMtok`
|
|
230
|
+
* is `null`, `perUserAvailable` is `false`, and the rollups are empty. The daemon degrades to
|
|
231
|
+
* THIS rather than throwing, and the wire degrades to it on a malformed/absent body.
|
|
232
|
+
*/
|
|
233
|
+
export declare const EMPTY_ROI_VIEW: RoiView;
|
|
234
|
+
/** The honest-empty {@link RoiTrendView} the page renders before first load / on failure / before capture (e-AC-10). */
|
|
235
|
+
export declare const EMPTY_ROI_TREND: RoiTrendView;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PARTIAL copy of honeycomb's `src/dashboard/contracts.ts` — PRD-001b ("copy partially").
|
|
3
|
+
*
|
|
4
|
+
* Only the web-consumed ROI view-model types cross into hive: the subset that
|
|
5
|
+
* `web/wire.ts` (`EMPTY_ROI_TREND`, `EMPTY_ROI_VIEW`, `RoiTrendView`, `RoiView`) and the ROI
|
|
6
|
+
* pages (`web/pages/roi.tsx`, `web/pages/roi-chart.tsx`) import. The rest of honeycomb's
|
|
7
|
+
* contracts serve its own daemon-side view-models and STAY in honeycomb. hive owns a small
|
|
8
|
+
* copy of just the types its `wire` validates against.
|
|
9
|
+
*
|
|
10
|
+
* Copied verbatim from honeycomb `src/dashboard/contracts.ts` (the `PRD-060e` ROI section),
|
|
11
|
+
* with one cross-repo JSDoc `{@link}` path softened to a plain reference so the module is
|
|
12
|
+
* self-contained in hive.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The honest-empty {@link RoiView} the page renders before the first load resolves, on any
|
|
16
|
+
* failure, or on a genuine first-run/empty workspace (e-AC-5). Every section is `absent`
|
|
17
|
+
* (the page shows a DASH glyph, NOT `$0.00`), the net is NOT computed, `blendedCentsPerMtok`
|
|
18
|
+
* is `null`, `perUserAvailable` is `false`, and the rollups are empty. The daemon degrades to
|
|
19
|
+
* THIS rather than throwing, and the wire degrades to it on a malformed/absent body.
|
|
20
|
+
*/
|
|
21
|
+
export const EMPTY_ROI_VIEW = Object.freeze({
|
|
22
|
+
savings: {
|
|
23
|
+
status: "absent",
|
|
24
|
+
measuredCents: 0,
|
|
25
|
+
modeledCents: 0,
|
|
26
|
+
assumption: { kind: "", assumptionText: "", signedOff: false },
|
|
27
|
+
blendedCentsPerMtok: null,
|
|
28
|
+
},
|
|
29
|
+
infra: { status: "absent", cents: 0, costBasis: "none" },
|
|
30
|
+
pollination: { status: "absent", cents: 0, lines: [] },
|
|
31
|
+
net: { status: "absent", computed: false, netCents: 0, modeled: true, costBasis: "none" },
|
|
32
|
+
rollups: [],
|
|
33
|
+
perUserAvailable: false,
|
|
34
|
+
scopedAcrossDevices: false,
|
|
35
|
+
ratesAsOf: "",
|
|
36
|
+
});
|
|
37
|
+
/** The honest-empty {@link RoiTrendView} the page renders before first load / on failure / before capture (e-AC-10). */
|
|
38
|
+
export const EMPTY_ROI_TREND = Object.freeze({
|
|
39
|
+
status: "absent",
|
|
40
|
+
series: [],
|
|
41
|
+
startedAt: "",
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../../src/dashboard/contracts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAgPH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAY,MAAM,CAAC,MAAM,CAAC;IACpD,OAAO,EAAE;QACR,MAAM,EAAE,QAAiB;QACzB,aAAa,EAAE,CAAC;QAChB,YAAY,EAAE,CAAC;QACf,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;QAC9D,mBAAmB,EAAE,IAAI;KACzB;IACD,KAAK,EAAE,EAAE,MAAM,EAAE,QAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,MAAe,EAAE;IAC1E,WAAW,EAAE,EAAE,MAAM,EAAE,QAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;IAC/D,GAAG,EAAE,EAAE,MAAM,EAAE,QAAiB,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAe,EAAE;IAC3G,OAAO,EAAE,EAAE;IACX,gBAAgB,EAAE,KAAK;IACvB,mBAAmB,EAAE,KAAK;IAC1B,SAAS,EAAE,EAAE;CACb,CAAC,CAAC;AAEH,wHAAwH;AACxH,MAAM,CAAC,MAAM,eAAe,GAAiB,MAAM,CAAC,MAAM,CAAC;IAC1D,MAAM,EAAE,QAAiB;IACzB,MAAM,EAAE,EAAE;IACV,SAAS,EAAE,EAAE;CACb,CAAC,CAAC"}
|