@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.
Files changed (190) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +262 -0
  3. package/dist/cli-commands.d.ts +41 -0
  4. package/dist/cli-commands.js +76 -0
  5. package/dist/cli-commands.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +46 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/daemon/dashboard/app.js +118 -0
  10. package/dist/daemon/dashboard/host.d.ts +90 -0
  11. package/dist/daemon/dashboard/host.js +172 -0
  12. package/dist/daemon/dashboard/host.js.map +1 -0
  13. package/dist/daemon/dashboard/web-assets.d.ts +88 -0
  14. package/dist/daemon/dashboard/web-assets.js +195 -0
  15. package/dist/daemon/dashboard/web-assets.js.map +1 -0
  16. package/dist/daemon/fleet-status.d.ts +15 -0
  17. package/dist/daemon/fleet-status.js +56 -0
  18. package/dist/daemon/fleet-status.js.map +1 -0
  19. package/dist/daemon/gate.d.ts +55 -0
  20. package/dist/daemon/gate.js +128 -0
  21. package/dist/daemon/gate.js.map +1 -0
  22. package/dist/daemon/proxy.d.ts +38 -0
  23. package/dist/daemon/proxy.js +126 -0
  24. package/dist/daemon/proxy.js.map +1 -0
  25. package/dist/daemon/registry.d.ts +31 -0
  26. package/dist/daemon/registry.js +120 -0
  27. package/dist/daemon/registry.js.map +1 -0
  28. package/dist/daemon/server.d.ts +41 -0
  29. package/dist/daemon/server.js +126 -0
  30. package/dist/daemon/server.js.map +1 -0
  31. package/dist/daemon/setup-auth.d.ts +37 -0
  32. package/dist/daemon/setup-auth.js +51 -0
  33. package/dist/daemon/setup-auth.js.map +1 -0
  34. package/dist/daemon/telemetry-proxy.d.ts +47 -0
  35. package/dist/daemon/telemetry-proxy.js +63 -0
  36. package/dist/daemon/telemetry-proxy.js.map +1 -0
  37. package/dist/dashboard/contracts.d.ts +235 -0
  38. package/dist/dashboard/contracts.js +43 -0
  39. package/dist/dashboard/contracts.js.map +1 -0
  40. package/dist/dashboard/web/app.d.ts +44 -0
  41. package/dist/dashboard/web/app.js +179 -0
  42. package/dist/dashboard/web/app.js.map +1 -0
  43. package/dist/dashboard/web/boot-route.d.ts +23 -0
  44. package/dist/dashboard/web/boot-route.js +29 -0
  45. package/dist/dashboard/web/boot-route.js.map +1 -0
  46. package/dist/dashboard/web/build-graph-button.d.ts +46 -0
  47. package/dist/dashboard/web/build-graph-button.js +81 -0
  48. package/dist/dashboard/web/build-graph-button.js.map +1 -0
  49. package/dist/dashboard/web/buzzing-screen.d.ts +30 -0
  50. package/dist/dashboard/web/buzzing-screen.js +135 -0
  51. package/dist/dashboard/web/buzzing-screen.js.map +1 -0
  52. package/dist/dashboard/web/folder-picker.d.ts +54 -0
  53. package/dist/dashboard/web/folder-picker.js +125 -0
  54. package/dist/dashboard/web/folder-picker.js.map +1 -0
  55. package/dist/dashboard/web/graph-layout.d.ts +101 -0
  56. package/dist/dashboard/web/graph-layout.js +0 -0
  57. package/dist/dashboard/web/graph-layout.js.map +1 -0
  58. package/dist/dashboard/web/harness-strip.d.ts +38 -0
  59. package/dist/dashboard/web/harness-strip.js +65 -0
  60. package/dist/dashboard/web/harness-strip.js.map +1 -0
  61. package/dist/dashboard/web/health-rail.d.ts +26 -0
  62. package/dist/dashboard/web/health-rail.js +61 -0
  63. package/dist/dashboard/web/health-rail.js.map +1 -0
  64. package/dist/dashboard/web/main.d.ts +22 -0
  65. package/dist/dashboard/web/main.js +51 -0
  66. package/dist/dashboard/web/main.js.map +1 -0
  67. package/dist/dashboard/web/needs-project.d.ts +51 -0
  68. package/dist/dashboard/web/needs-project.js +69 -0
  69. package/dist/dashboard/web/needs-project.js.map +1 -0
  70. package/dist/dashboard/web/page-frame.d.ts +95 -0
  71. package/dist/dashboard/web/page-frame.js +98 -0
  72. package/dist/dashboard/web/page-frame.js.map +1 -0
  73. package/dist/dashboard/web/pages/coming-soon.d.ts +23 -0
  74. package/dist/dashboard/web/pages/coming-soon.js +19 -0
  75. package/dist/dashboard/web/pages/coming-soon.js.map +1 -0
  76. package/dist/dashboard/web/pages/dashboard.d.ts +32 -0
  77. package/dist/dashboard/web/pages/dashboard.js +205 -0
  78. package/dist/dashboard/web/pages/dashboard.js.map +1 -0
  79. package/dist/dashboard/web/pages/graph.d.ts +72 -0
  80. package/dist/dashboard/web/pages/graph.js +362 -0
  81. package/dist/dashboard/web/pages/graph.js.map +1 -0
  82. package/dist/dashboard/web/pages/harnesses.d.ts +65 -0
  83. package/dist/dashboard/web/pages/harnesses.js +210 -0
  84. package/dist/dashboard/web/pages/harnesses.js.map +1 -0
  85. package/dist/dashboard/web/pages/health.d.ts +22 -0
  86. package/dist/dashboard/web/pages/health.js +91 -0
  87. package/dist/dashboard/web/pages/health.js.map +1 -0
  88. package/dist/dashboard/web/pages/lifecycle-panel.d.ts +60 -0
  89. package/dist/dashboard/web/pages/lifecycle-panel.js +187 -0
  90. package/dist/dashboard/web/pages/lifecycle-panel.js.map +1 -0
  91. package/dist/dashboard/web/pages/logs.d.ts +55 -0
  92. package/dist/dashboard/web/pages/logs.js +311 -0
  93. package/dist/dashboard/web/pages/logs.js.map +1 -0
  94. package/dist/dashboard/web/pages/memories.d.ts +35 -0
  95. package/dist/dashboard/web/pages/memories.js +439 -0
  96. package/dist/dashboard/web/pages/memories.js.map +1 -0
  97. package/dist/dashboard/web/pages/projects.d.ts +38 -0
  98. package/dist/dashboard/web/pages/projects.js +325 -0
  99. package/dist/dashboard/web/pages/projects.js.map +1 -0
  100. package/dist/dashboard/web/pages/roi-chart.d.ts +52 -0
  101. package/dist/dashboard/web/pages/roi-chart.js +133 -0
  102. package/dist/dashboard/web/pages/roi-chart.js.map +1 -0
  103. package/dist/dashboard/web/pages/roi.d.ts +93 -0
  104. package/dist/dashboard/web/pages/roi.js +375 -0
  105. package/dist/dashboard/web/pages/roi.js.map +1 -0
  106. package/dist/dashboard/web/pages/settings.d.ts +103 -0
  107. package/dist/dashboard/web/pages/settings.js +420 -0
  108. package/dist/dashboard/web/pages/settings.js.map +1 -0
  109. package/dist/dashboard/web/pages/sync.d.ts +59 -0
  110. package/dist/dashboard/web/pages/sync.js +280 -0
  111. package/dist/dashboard/web/pages/sync.js.map +1 -0
  112. package/dist/dashboard/web/panels.d.ts +178 -0
  113. package/dist/dashboard/web/panels.js +471 -0
  114. package/dist/dashboard/web/panels.js.map +1 -0
  115. package/dist/dashboard/web/primitives.d.ts +80 -0
  116. package/dist/dashboard/web/primitives.js +208 -0
  117. package/dist/dashboard/web/primitives.js.map +1 -0
  118. package/dist/dashboard/web/readiness-splash.d.ts +39 -0
  119. package/dist/dashboard/web/readiness-splash.js +201 -0
  120. package/dist/dashboard/web/readiness-splash.js.map +1 -0
  121. package/dist/dashboard/web/registry.d.ts +96 -0
  122. package/dist/dashboard/web/registry.js +99 -0
  123. package/dist/dashboard/web/registry.js.map +1 -0
  124. package/dist/dashboard/web/router.d.ts +48 -0
  125. package/dist/dashboard/web/router.js +76 -0
  126. package/dist/dashboard/web/router.js.map +1 -0
  127. package/dist/dashboard/web/scope-context.d.ts +155 -0
  128. package/dist/dashboard/web/scope-context.js +266 -0
  129. package/dist/dashboard/web/scope-context.js.map +1 -0
  130. package/dist/dashboard/web/service-icons.d.ts +33 -0
  131. package/dist/dashboard/web/service-icons.js +80 -0
  132. package/dist/dashboard/web/service-icons.js.map +1 -0
  133. package/dist/dashboard/web/setup-gate.d.ts +116 -0
  134. package/dist/dashboard/web/setup-gate.js +272 -0
  135. package/dist/dashboard/web/setup-gate.js.map +1 -0
  136. package/dist/dashboard/web/sidebar.d.ts +63 -0
  137. package/dist/dashboard/web/sidebar.js +91 -0
  138. package/dist/dashboard/web/sidebar.js.map +1 -0
  139. package/dist/dashboard/web/use-fleet-telemetry.d.ts +111 -0
  140. package/dist/dashboard/web/use-fleet-telemetry.js +278 -0
  141. package/dist/dashboard/web/use-fleet-telemetry.js.map +1 -0
  142. package/dist/dashboard/web/wire.d.ts +1937 -0
  143. package/dist/dashboard/web/wire.js +1837 -0
  144. package/dist/dashboard/web/wire.js.map +1 -0
  145. package/dist/errors.d.ts +5 -0
  146. package/dist/errors.js +11 -0
  147. package/dist/errors.js.map +1 -0
  148. package/dist/install/registry.d.ts +35 -0
  149. package/dist/install/registry.js +110 -0
  150. package/dist/install/registry.js.map +1 -0
  151. package/dist/lock.d.ts +10 -0
  152. package/dist/lock.js +87 -0
  153. package/dist/lock.js.map +1 -0
  154. package/dist/service/commands.d.ts +15 -0
  155. package/dist/service/commands.js +49 -0
  156. package/dist/service/commands.js.map +1 -0
  157. package/dist/service/index.d.ts +38 -0
  158. package/dist/service/index.js +174 -0
  159. package/dist/service/index.js.map +1 -0
  160. package/dist/service/platform.d.ts +37 -0
  161. package/dist/service/platform.js +67 -0
  162. package/dist/service/platform.js.map +1 -0
  163. package/dist/service/templates.d.ts +10 -0
  164. package/dist/service/templates.js +120 -0
  165. package/dist/service/templates.js.map +1 -0
  166. package/dist/shared/constants.d.ts +15 -0
  167. package/dist/shared/constants.js +19 -0
  168. package/dist/shared/constants.js.map +1 -0
  169. package/dist/shared/daemon-routing.d.ts +11 -0
  170. package/dist/shared/daemon-routing.js +38 -0
  171. package/dist/shared/daemon-routing.js.map +1 -0
  172. package/dist/shared/fleet-readiness.d.ts +18 -0
  173. package/dist/shared/fleet-readiness.js +9 -0
  174. package/dist/shared/fleet-readiness.js.map +1 -0
  175. package/dist/shared/fleet-telemetry.d.ts +68 -0
  176. package/dist/shared/fleet-telemetry.js +61 -0
  177. package/dist/shared/fleet-telemetry.js.map +1 -0
  178. package/dist/shared/lifecycle-flags.d.ts +34 -0
  179. package/dist/shared/lifecycle-flags.js +39 -0
  180. package/dist/shared/lifecycle-flags.js.map +1 -0
  181. package/dist/shared/memory-types.d.ts +55 -0
  182. package/dist/shared/memory-types.js +75 -0
  183. package/dist/shared/memory-types.js.map +1 -0
  184. package/dist/shared/service-status.d.ts +82 -0
  185. package/dist/shared/service-status.js +97 -0
  186. package/dist/shared/service-status.js.map +1 -0
  187. package/dist/telemetry/emit.d.ts +197 -0
  188. package/dist/telemetry/emit.js +196 -0
  189. package/dist/telemetry/emit.js.map +1 -0
  190. package/package.json +50 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The viewable dashboard HOST — PRD-001b Wave 2.
3
+ *
4
+ * Adapted from honeycomb `src/daemon/runtime/dashboard/host.ts`. hive is a standalone Hono
5
+ * process that serves the migrated React SPA at the ROOT (`/`), not under `/dashboard`. It serves
6
+ * the INDEX SHELL of the real React app bundled production-clean by esbuild
7
+ * (`src/dashboard/web/main.tsx` → `dist/daemon/dashboard/app.js`). The shell is `<div id="root">`
8
+ * plus a `<link>` to the design-system CSS and a `<script>` to the bundled app; the app then
9
+ * hydrates ITSELF from the daemon endpoints its `wire` targets.
10
+ *
11
+ * NO `unpkg`/CDN React, NO in-browser `@babel/standalone`, NO `type="text/babel"`: the shell
12
+ * references ONLY same-origin loopback assets the host serves, and carries NO token/secret.
13
+ *
14
+ * ── The routes this host registers (hive serves at the root) ───────────────
15
+ * GET /app.js → the esbuild bundle (React + ReactDOM + the app)
16
+ * GET /styles.css → the concatenated design-system CSS
17
+ * GET /honeycomb-memory-cluster.svg → the brand mark
18
+ * GET /fonts/:name → an allow-listed brand font
19
+ * GET * → the index shell ({@link renderShell}), a CATCH-ALL
20
+ *
21
+ * PRD-003a split this into two entry points, {@link mountDashboardAssets} and
22
+ * {@link mountDashboardShellFallback}, because the shell is now served for EVERY path-based SPA
23
+ * route (`/`, `/projects`, ..., `/buzzing`, `/login`, and any client-only deep link), not just `/`.
24
+ * The shell route must therefore be a catch-all (`*`) registered LAST in `server.ts` — AFTER the
25
+ * asset routes below, `/health`, `/api/fleet-status`, and the `/api/*` `/setup/*` BFF proxy — so
26
+ * those more specific routes win (Hono composes matching handlers in registration order; the first
27
+ * one that returns a response without calling `next()` wins). {@link mountDashboardHost} keeps
28
+ * calling both in the safe order for standalone callers (e.g. a bare test `Hono()` with nothing
29
+ * else registered) where that ordering concern does not arise.
30
+ */
31
+ import type { Hono } from "hono";
32
+ import { type WebAssets } from "./web-assets.js";
33
+ /** The default landing path (the dashboard) — served by the catch-all, like every other route. */
34
+ export declare const DASHBOARD_HOST_PATH: "/";
35
+ /** The same-origin path the host serves the bundled app JS at. */
36
+ export declare const DASHBOARD_APP_PATH: "/app.js";
37
+ /** The same-origin path the host serves the concatenated design-system CSS at. */
38
+ export declare const DASHBOARD_CSS_PATH: "/styles.css";
39
+ /** The same-origin path the host serves the brand mark at. */
40
+ export declare const DASHBOARD_LOGO_PATH: "/honeycomb-memory-cluster.svg";
41
+ /**
42
+ * The same-origin path prefix the host serves the brand fonts under. The served DS CSS's
43
+ * `@font-face` URLs are rewritten to this prefix (see `web-assets.ts` `rewriteFontUrls`) so the
44
+ * browser fetches `/fonts/<file>` instead of the unserved on-disk `../logos/fonts/<file>`. The
45
+ * `:name` is matched against a FIXED allow-list in `web-assets.ts` `font()` — anything not in the
46
+ * six known filenames 404s (no attacker-controlled path component).
47
+ */
48
+ export declare const DASHBOARD_FONT_PATH: "/fonts/:name";
49
+ /** Options for {@link mountDashboardHost}. */
50
+ export interface MountDashboardHostOptions {
51
+ /**
52
+ * The web-asset reader (the CSS/logo/bundle/fonts source). Defaults to {@link createWebAssets}
53
+ * (resolve the repo `assets/` + the bundle beside this module). A test injects a fixture reader
54
+ * so the host suite never depends on the real tree or a built bundle.
55
+ */
56
+ readonly assets?: WebAssets;
57
+ }
58
+ /**
59
+ * Build the index SHELL HTML. It is a COMPLETE page: doctype, head with the DS CSS `<link>` + the
60
+ * inline layout CSS, a `<div id="root">` (the app mounts here) carrying the asset base, and the
61
+ * bundled-app `<script type="module">`. NO inline data, NO token/secret, NO CDN/Babel reference —
62
+ * the bundle is same-origin loopback. The app self-hydrates.
63
+ */
64
+ export declare function renderShell(): string;
65
+ /**
66
+ * Attach the FOUR static-asset routes (app JS, CSS, logo, fonts) onto hive's Hono app. These
67
+ * are specific, fixed paths — register them BEFORE {@link mountDashboardShellFallback}'s catch-all
68
+ * so they win. The asset routes serve no secret/token (a not-yet-built bundle 404s rather than
69
+ * 500s), and — per PRD-003a — they are also EXEMPT from the portal landing gate (`gate.ts`): the
70
+ * bundle must load even when the shell just redirected the browser to `/buzzing` or `/login`,
71
+ * since those exempt screens are served by the same SPA bundle.
72
+ */
73
+ export declare function mountDashboardAssets(app: Hono, options?: MountDashboardHostOptions): void;
74
+ /**
75
+ * Attach the SPA shell CATCH-ALL onto hive's Hono app (PRD-003a g-AC-1 / g-AC-2). Register this
76
+ * LAST — after the asset routes above, `/health`, `/api/fleet-status`, and the `/api/*` `/setup/*`
77
+ * BFF proxy — so those specific routes win; `*` then serves the identical shell for every gated
78
+ * page path the portal gate (`gate.ts`) let through, PLUS the two gate-exempt screens (`/buzzing`,
79
+ * `/login`) and any unknown/client-only deep link. The bundled app self-hydrates by reading
80
+ * `location.pathname`, so one shell byte-for-byte serves every screen (D-1: no server templating).
81
+ */
82
+ export declare function mountDashboardShellFallback(app: Hono): void;
83
+ /**
84
+ * Attach the viewable dashboard host onto hive's Hono app: the four static-asset routes plus
85
+ * the shell catch-all, in the safe order. Call this ONLY when nothing else needs to be registered
86
+ * in between (e.g. a standalone test `Hono()`); `server.ts` calls {@link mountDashboardAssets} and
87
+ * {@link mountDashboardShellFallback} separately so `/health`, `/api/fleet-status`, and the BFF
88
+ * proxy can be registered between them (see the module doc for why the ordering matters).
89
+ */
90
+ export declare function mountDashboardHost(app: Hono, options?: MountDashboardHostOptions): void;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The viewable dashboard HOST — PRD-001b Wave 2.
3
+ *
4
+ * Adapted from honeycomb `src/daemon/runtime/dashboard/host.ts`. hive is a standalone Hono
5
+ * process that serves the migrated React SPA at the ROOT (`/`), not under `/dashboard`. It serves
6
+ * the INDEX SHELL of the real React app bundled production-clean by esbuild
7
+ * (`src/dashboard/web/main.tsx` → `dist/daemon/dashboard/app.js`). The shell is `<div id="root">`
8
+ * plus a `<link>` to the design-system CSS and a `<script>` to the bundled app; the app then
9
+ * hydrates ITSELF from the daemon endpoints its `wire` targets.
10
+ *
11
+ * NO `unpkg`/CDN React, NO in-browser `@babel/standalone`, NO `type="text/babel"`: the shell
12
+ * references ONLY same-origin loopback assets the host serves, and carries NO token/secret.
13
+ *
14
+ * ── The routes this host registers (hive serves at the root) ───────────────
15
+ * GET /app.js → the esbuild bundle (React + ReactDOM + the app)
16
+ * GET /styles.css → the concatenated design-system CSS
17
+ * GET /honeycomb-memory-cluster.svg → the brand mark
18
+ * GET /fonts/:name → an allow-listed brand font
19
+ * GET * → the index shell ({@link renderShell}), a CATCH-ALL
20
+ *
21
+ * PRD-003a split this into two entry points, {@link mountDashboardAssets} and
22
+ * {@link mountDashboardShellFallback}, because the shell is now served for EVERY path-based SPA
23
+ * route (`/`, `/projects`, ..., `/buzzing`, `/login`, and any client-only deep link), not just `/`.
24
+ * The shell route must therefore be a catch-all (`*`) registered LAST in `server.ts` — AFTER the
25
+ * asset routes below, `/health`, `/api/fleet-status`, and the `/api/*` `/setup/*` BFF proxy — so
26
+ * those more specific routes win (Hono composes matching handlers in registration order; the first
27
+ * one that returns a response without calling `next()` wins). {@link mountDashboardHost} keeps
28
+ * calling both in the safe order for standalone callers (e.g. a bare test `Hono()` with nothing
29
+ * else registered) where that ordering concern does not arise.
30
+ */
31
+ import { createWebAssets } from "./web-assets.js";
32
+ /** The default landing path (the dashboard) — served by the catch-all, like every other route. */
33
+ export const DASHBOARD_HOST_PATH = "/";
34
+ /** The same-origin path the host serves the bundled app JS at. */
35
+ export const DASHBOARD_APP_PATH = "/app.js";
36
+ /** The same-origin path the host serves the concatenated design-system CSS at. */
37
+ export const DASHBOARD_CSS_PATH = "/styles.css";
38
+ /** The same-origin path the host serves the brand mark at. */
39
+ export const DASHBOARD_LOGO_PATH = "/honeycomb-memory-cluster.svg";
40
+ /**
41
+ * The same-origin path prefix the host serves the brand fonts under. The served DS CSS's
42
+ * `@font-face` URLs are rewritten to this prefix (see `web-assets.ts` `rewriteFontUrls`) so the
43
+ * browser fetches `/fonts/<file>` instead of the unserved on-disk `../logos/fonts/<file>`. The
44
+ * `:name` is matched against a FIXED allow-list in `web-assets.ts` `font()` — anything not in the
45
+ * six known filenames 404s (no attacker-controlled path component).
46
+ */
47
+ export const DASHBOARD_FONT_PATH = "/fonts/:name";
48
+ /**
49
+ * The asset base the app resolves host-served assets under. hive serves the mark at the ROOT
50
+ * (`/honeycomb-memory-cluster.svg`), so the base is empty — the app's `${assetBase}/…svg`
51
+ * resolves to `/…svg`. main.tsx sanitizes this DOM-read value; an empty string is the safe default.
52
+ */
53
+ const ASSET_BASE = "";
54
+ /**
55
+ * The static layout CSS the UI kit declares inline (`.wrap`, `.grid2`, `.kpirow`, `.mem-enter`,
56
+ * `.col`). Ported verbatim so the served page lays out exactly like honeycomb's. The DS TOKENS +
57
+ * component styles come from the linked `/styles.css`; this is only the page's grid/animation rules.
58
+ */
59
+ const LAYOUT_CSS = [
60
+ "body { margin: 0; background: var(--bg-canvas); min-height: 100vh; }",
61
+ ".wrap { max-width: 1180px; margin: 0 auto; padding: 28px 28px 48px; }",
62
+ ".grid2 { display: grid; grid-template-columns: 1.15fr 1fr; gap: 16px; }",
63
+ "@media (max-width: 900px) { .grid2 { grid-template-columns: 1fr; } }",
64
+ ".kpirow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }",
65
+ "@media (max-width: 720px) { .kpirow { grid-template-columns: repeat(2, 1fr); } }",
66
+ ".mem-enter { opacity: 1; }",
67
+ "@media (prefers-reduced-motion: no-preference) {",
68
+ " .mem-enter { animation: memIn var(--dur-base) var(--ease-out) both; }",
69
+ " @keyframes memIn { from { transform: translateY(10px); } to { transform: none; } }",
70
+ "}",
71
+ ".col { display: flex; flex-direction: column; gap: 16px; }",
72
+ ].join("\n");
73
+ /**
74
+ * Build the index SHELL HTML. It is a COMPLETE page: doctype, head with the DS CSS `<link>` + the
75
+ * inline layout CSS, a `<div id="root">` (the app mounts here) carrying the asset base, and the
76
+ * bundled-app `<script type="module">`. NO inline data, NO token/secret, NO CDN/Babel reference —
77
+ * the bundle is same-origin loopback. The app self-hydrates.
78
+ */
79
+ export function renderShell() {
80
+ return [
81
+ "<!doctype html>",
82
+ '<html lang="en">',
83
+ "<head>",
84
+ '<meta charset="utf-8">',
85
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
86
+ "<title>Hive — Dashboard</title>",
87
+ `<link rel="stylesheet" href="${DASHBOARD_CSS_PATH}">`,
88
+ `<link rel="icon" href="${DASHBOARD_LOGO_PATH}">`,
89
+ `<style>${LAYOUT_CSS}</style>`,
90
+ "</head>",
91
+ "<body>",
92
+ `<div id="root" data-asset-base="${ASSET_BASE}"></div>`,
93
+ `<script type="module" src="${DASHBOARD_APP_PATH}"></script>`,
94
+ "</body>",
95
+ "</html>",
96
+ ].join("\n");
97
+ }
98
+ /**
99
+ * Attach the FOUR static-asset routes (app JS, CSS, logo, fonts) onto hive's Hono app. These
100
+ * are specific, fixed paths — register them BEFORE {@link mountDashboardShellFallback}'s catch-all
101
+ * so they win. The asset routes serve no secret/token (a not-yet-built bundle 404s rather than
102
+ * 500s), and — per PRD-003a — they are also EXEMPT from the portal landing gate (`gate.ts`): the
103
+ * bundle must load even when the shell just redirected the browser to `/buzzing` or `/login`,
104
+ * since those exempt screens are served by the same SPA bundle.
105
+ */
106
+ export function mountDashboardAssets(app, options = {}) {
107
+ const assets = options.assets ?? createWebAssets();
108
+ // GET /app.js — the esbuild bundle (React + ReactDOM + the dashboard app).
109
+ app.get(DASHBOARD_APP_PATH, (c) => {
110
+ const asset = assets.appJs();
111
+ if (asset === null)
112
+ return c.text("dashboard bundle not built", 404);
113
+ return c.body(asset.body, 200, { "content-type": asset.contentType, "cache-control": "no-cache" });
114
+ });
115
+ // GET /styles.css — the concatenated design-system CSS.
116
+ app.get(DASHBOARD_CSS_PATH, (c) => {
117
+ const asset = assets.css();
118
+ if (asset === null)
119
+ return c.text("dashboard styles unavailable", 404);
120
+ return c.body(asset.body, 200, { "content-type": asset.contentType, "cache-control": "no-cache" });
121
+ });
122
+ // GET /honeycomb-memory-cluster.svg — the brand mark. Served `no-cache` too: it shares the
123
+ // un-hashed URL contract with the shell, and revalidating a ~1 KB SVG over loopback is free.
124
+ app.get(DASHBOARD_LOGO_PATH, (c) => {
125
+ const asset = assets.logo();
126
+ if (asset === null)
127
+ return c.text("dashboard logo unavailable", 404);
128
+ return c.body(asset.body, 200, { "content-type": asset.contentType, "cache-control": "no-cache" });
129
+ });
130
+ // GET /fonts/<file> — the brand fonts. The DS CSS's `@font-face` URLs are rewritten to this
131
+ // route. `:name` is allow-listed in `font()` (only the six known filenames resolve; anything
132
+ // else — incl. traversal — 404s). Fonts carry no secret; a long-lived immutable cache-control.
133
+ app.get(DASHBOARD_FONT_PATH, (c) => {
134
+ const asset = assets.font(c.req.param("name"));
135
+ if (asset === null)
136
+ return c.text("dashboard font not found", 404);
137
+ return c.body(asset.body, 200, {
138
+ "content-type": asset.contentType,
139
+ "cache-control": "public, max-age=31536000, immutable",
140
+ });
141
+ });
142
+ }
143
+ /**
144
+ * Attach the SPA shell CATCH-ALL onto hive's Hono app (PRD-003a g-AC-1 / g-AC-2). Register this
145
+ * LAST — after the asset routes above, `/health`, `/api/fleet-status`, and the `/api/*` `/setup/*`
146
+ * BFF proxy — so those specific routes win; `*` then serves the identical shell for every gated
147
+ * page path the portal gate (`gate.ts`) let through, PLUS the two gate-exempt screens (`/buzzing`,
148
+ * `/login`) and any unknown/client-only deep link. The bundled app self-hydrates by reading
149
+ * `location.pathname`, so one shell byte-for-byte serves every screen (D-1: no server templating).
150
+ */
151
+ export function mountDashboardShellFallback(app) {
152
+ // `no-cache` (revalidate every load): the shell + app.js + css filenames are NOT content-hashed,
153
+ // so an upgrade rebuilds them in place at the SAME URL. Without a revalidation directive the
154
+ // browser heuristically caches the bundle and keeps running a STALE app.js across a restart.
155
+ // `no-cache` forces a re-pull on each load (cheap over loopback), so a rebuilt dashboard runs.
156
+ app.get("*", (c) => {
157
+ c.header("cache-control", "no-cache");
158
+ return c.html(renderShell());
159
+ });
160
+ }
161
+ /**
162
+ * Attach the viewable dashboard host onto hive's Hono app: the four static-asset routes plus
163
+ * the shell catch-all, in the safe order. Call this ONLY when nothing else needs to be registered
164
+ * in between (e.g. a standalone test `Hono()`); `server.ts` calls {@link mountDashboardAssets} and
165
+ * {@link mountDashboardShellFallback} separately so `/health`, `/api/fleet-status`, and the BFF
166
+ * proxy can be registered between them (see the module doc for why the ordering matters).
167
+ */
168
+ export function mountDashboardHost(app, options = {}) {
169
+ mountDashboardAssets(app, options);
170
+ mountDashboardShellFallback(app);
171
+ }
172
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../../../src/daemon/dashboard/host.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The daemon-side ASSET RESOLVER for the bundled dashboard web app — PRD-001b Wave 2.
3
+ *
4
+ * Adapted from honeycomb `src/daemon/runtime/dashboard/web-assets.ts`. hive serves the
5
+ * dashboard at the ROOT (`/`), not under `/dashboard`, so the served asset routes and the
6
+ * `@font-face` URL prefix are rooted at `/` instead of `/dashboard`. The bundle esbuild emits
7
+ * is `app.js` beside this module (`dist/daemon/dashboard/app.js`), not honeycomb's
8
+ * `dashboard-app.js`.
9
+ *
10
+ * The host (`host.ts`) serves, on loopback:
11
+ *
12
+ * GET / → the index SHELL (`<div id="root">` + <link> + <script>)
13
+ * GET /app.js → the esbuild bundle (React + ReactDOM + the app)
14
+ * GET /styles.css → the concatenated DS CSS (tokens + base, no @import chain)
15
+ * GET /honeycomb-memory-cluster.svg → the brand mark
16
+ * GET /fonts/:name → an allow-listed brand font
17
+ *
18
+ * This module owns LOCATING those source assets on disk and reading them. It lives under
19
+ * `src/daemon/` (it does Node `fs` IO) and is the ONLY place the host touches the filesystem.
20
+ *
21
+ * ── Why concatenate the CSS here (not ship the @import chain) ─────────────────
22
+ * `assets/styles.css` is `@import url('tokens/…')` lines — those resolve RELATIVE to the
23
+ * served URL. Rather than serve five files + rewrite the relative URLs, we read the token
24
+ * files in declared order and concatenate them into ONE payload. One request, no relative
25
+ * `@import` resolution, deterministic across install layouts.
26
+ *
27
+ * ── Resolution + injectability ───────────────────────────────────────────────
28
+ * The assets dir defaults to {@link resolveAssetsDir} (walk up from this module to the repo
29
+ * root that contains `assets/styles.css`). A test injects a fixture dir so it never depends
30
+ * on the real tree. Reads fail SOFT (a missing asset yields an empty string / a 404 at the
31
+ * route), never a throw that would 500 the page.
32
+ */
33
+ /**
34
+ * The bundled web-app filename the build emits beside this module. esbuild
35
+ * (`esbuild.config.mjs`) bundles `src/dashboard/web/main.tsx` → `dist/daemon/dashboard/app.js`.
36
+ */
37
+ export declare const DASHBOARD_APP_BUNDLE: "app.js";
38
+ /**
39
+ * Resolve the repo `assets/` directory by walking up from this module until a dir with
40
+ * `assets/styles.css` is found. Works from `src/` (tsc/vitest run from source) and from the
41
+ * built `dist/daemon/dashboard/` layout. Returns `null` when no `assets/` is found (a stripped install).
42
+ */
43
+ export declare function resolveAssetsDir(startUrl?: string): string | null;
44
+ /**
45
+ * Resolve the directory the bundled `app.js` lives in. esbuild emits it beside this module's
46
+ * compiled output (`dist/daemon/dashboard/`), so the bundle sits next to it. From source
47
+ * (vitest) the bundle may not exist yet — the read fails soft. Returns `null` when the dir
48
+ * cannot be resolved.
49
+ */
50
+ export declare function resolveBundleDir(startUrl?: string): string | null;
51
+ /** Options for {@link createWebAssets}. */
52
+ export interface WebAssetsOptions {
53
+ /** The `assets/` source dir. Defaults to {@link resolveAssetsDir}. A test injects a fixture. */
54
+ readonly assetsDir?: string | null;
55
+ /** The dir the bundled app JS lives in. Defaults to {@link resolveBundleDir}. */
56
+ readonly bundleDir?: string | null;
57
+ /** Override the bundled app JS content directly (a test serves a stub bundle). */
58
+ readonly appJs?: string;
59
+ }
60
+ /** A served text asset: its UTF-8 body + content type, or `null` when missing (→ 404). */
61
+ export interface ServedAsset {
62
+ readonly body: string;
63
+ readonly contentType: string;
64
+ }
65
+ /** A served BINARY asset (the fonts): raw bytes + content type, or `null` when missing (→ 404). */
66
+ export interface ServedBinaryAsset {
67
+ readonly body: ArrayBuffer;
68
+ readonly contentType: string;
69
+ }
70
+ /** The host-facing asset reader: the concatenated CSS, the logo, the app bundle, and the fonts. */
71
+ export interface WebAssets {
72
+ /** The concatenated DS CSS (tokens + base), font URLs rewritten, or `null` when the assets dir is missing. */
73
+ css(): ServedAsset | null;
74
+ /** The brand logo SVG, or `null` when missing. */
75
+ logo(): ServedAsset | null;
76
+ /** The bundled web-app JS, or `null` when the bundle has not been built. */
77
+ appJs(): ServedAsset | null;
78
+ /** A brand font's bytes + content type for an allow-listed `name`, or `null` (→ 404) otherwise. */
79
+ font(name: string): ServedBinaryAsset | null;
80
+ }
81
+ /**
82
+ * Build the host-side asset reader. Resolves the assets + bundle dirs once and reads on
83
+ * demand. Every read fails soft (missing → `null` → the route 404s), so a stripped install or
84
+ * a not-yet-built bundle never 500s the page. The CSS is concatenated from the token files in
85
+ * declared order so the served page needs ONE stylesheet request and no relative `@import`
86
+ * resolution.
87
+ */
88
+ export declare function createWebAssets(options?: WebAssetsOptions): WebAssets;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The daemon-side ASSET RESOLVER for the bundled dashboard web app — PRD-001b Wave 2.
3
+ *
4
+ * Adapted from honeycomb `src/daemon/runtime/dashboard/web-assets.ts`. hive serves the
5
+ * dashboard at the ROOT (`/`), not under `/dashboard`, so the served asset routes and the
6
+ * `@font-face` URL prefix are rooted at `/` instead of `/dashboard`. The bundle esbuild emits
7
+ * is `app.js` beside this module (`dist/daemon/dashboard/app.js`), not honeycomb's
8
+ * `dashboard-app.js`.
9
+ *
10
+ * The host (`host.ts`) serves, on loopback:
11
+ *
12
+ * GET / → the index SHELL (`<div id="root">` + <link> + <script>)
13
+ * GET /app.js → the esbuild bundle (React + ReactDOM + the app)
14
+ * GET /styles.css → the concatenated DS CSS (tokens + base, no @import chain)
15
+ * GET /honeycomb-memory-cluster.svg → the brand mark
16
+ * GET /fonts/:name → an allow-listed brand font
17
+ *
18
+ * This module owns LOCATING those source assets on disk and reading them. It lives under
19
+ * `src/daemon/` (it does Node `fs` IO) and is the ONLY place the host touches the filesystem.
20
+ *
21
+ * ── Why concatenate the CSS here (not ship the @import chain) ─────────────────
22
+ * `assets/styles.css` is `@import url('tokens/…')` lines — those resolve RELATIVE to the
23
+ * served URL. Rather than serve five files + rewrite the relative URLs, we read the token
24
+ * files in declared order and concatenate them into ONE payload. One request, no relative
25
+ * `@import` resolution, deterministic across install layouts.
26
+ *
27
+ * ── Resolution + injectability ───────────────────────────────────────────────
28
+ * The assets dir defaults to {@link resolveAssetsDir} (walk up from this module to the repo
29
+ * root that contains `assets/styles.css`). A test injects a fixture dir so it never depends
30
+ * on the real tree. Reads fail SOFT (a missing asset yields an empty string / a 404 at the
31
+ * route), never a throw that would 500 the page.
32
+ */
33
+ import { existsSync, readFileSync } from "node:fs";
34
+ import { dirname, join } from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+ /** The token files, in the SAME order `assets/styles.css` @imports them (order matters). */
37
+ const CSS_FILES = ["tokens/fonts.css", "tokens/colors.css", "tokens/typography.css", "tokens/spacing.css", "tokens/base.css"];
38
+ /** The design-system logo the header renders. */
39
+ const LOGO_FILE = "logos/honeycomb-memory-cluster.svg";
40
+ /**
41
+ * The on-disk dir (under `assets/`) the brand fonts live in, and the FIXED allow-list of the
42
+ * six font filenames the host will serve. The host route serves ONLY a name in this set — there
43
+ * is NO attacker-controlled path component (mirrors {@link LOGO_FILE}'s hard-coded-filename
44
+ * safety). Anything not in this set yields `null` → a 404 at the route.
45
+ */
46
+ const FONTS_DIR = "logos/fonts";
47
+ const FONT_FILES = [
48
+ "Inter-VariableFont_opsz_wght.ttf",
49
+ "Inter-Italic-VariableFont_opsz_wght.ttf",
50
+ "JetBrainsMono-Regular.woff2",
51
+ "JetBrainsMono-Medium.woff2",
52
+ "JetBrainsMono-SemiBold.woff2",
53
+ "JetBrainsMono-Bold.woff2",
54
+ ];
55
+ const FONT_ALLOW = new Set(FONT_FILES);
56
+ /** The same-origin path hive serves the fonts under (origin-rooted; see {@link rewriteFontUrls}). */
57
+ const FONT_ROUTE_PREFIX = "/fonts/";
58
+ /** Map a font filename to its `content-type` by extension (`.woff2` → `font/woff2`, `.ttf` → `font/ttf`). */
59
+ function fontContentType(name) {
60
+ return name.endsWith(".woff2") ? "font/woff2" : "font/ttf";
61
+ }
62
+ /**
63
+ * Rewrite the DS `@font-face` `url(...)` prefix in the SERVED CSS so the fonts resolve to the
64
+ * host's `/fonts/<file>` route instead of the on-disk `../logos/fonts/<file>` (which the host
65
+ * does not serve at that path → 404). The replacement is ORIGIN-ROOTED (leading `/`) so it
66
+ * resolves regardless of the stylesheet's own URL. ONLY the `logos/fonts/` prefix is touched — the
67
+ * `../` is consumed with it, and no other `url(...)` (the mark SVG etc.) is mangled. Byte-identical
68
+ * otherwise.
69
+ */
70
+ function rewriteFontUrls(css) {
71
+ return css.replaceAll("../logos/fonts/", FONT_ROUTE_PREFIX).replaceAll("logos/fonts/", FONT_ROUTE_PREFIX);
72
+ }
73
+ /**
74
+ * The bundled web-app filename the build emits beside this module. esbuild
75
+ * (`esbuild.config.mjs`) bundles `src/dashboard/web/main.tsx` → `dist/daemon/dashboard/app.js`.
76
+ */
77
+ export const DASHBOARD_APP_BUNDLE = "app.js";
78
+ /**
79
+ * Resolve the repo `assets/` directory by walking up from this module until a dir with
80
+ * `assets/styles.css` is found. Works from `src/` (tsc/vitest run from source) and from the
81
+ * built `dist/daemon/dashboard/` layout. Returns `null` when no `assets/` is found (a stripped install).
82
+ */
83
+ export function resolveAssetsDir(startUrl = import.meta.url) {
84
+ let dir;
85
+ try {
86
+ dir = dirname(fileURLToPath(startUrl));
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ // Walk up to the filesystem root looking for `<dir>/assets/styles.css`.
92
+ for (let i = 0; i < 12; i++) {
93
+ const candidate = join(dir, "assets");
94
+ if (existsSync(join(candidate, "styles.css")))
95
+ return candidate;
96
+ const parent = dirname(dir);
97
+ if (parent === dir)
98
+ break;
99
+ dir = parent;
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Resolve the directory the bundled `app.js` lives in. esbuild emits it beside this module's
105
+ * compiled output (`dist/daemon/dashboard/`), so the bundle sits next to it. From source
106
+ * (vitest) the bundle may not exist yet — the read fails soft. Returns `null` when the dir
107
+ * cannot be resolved.
108
+ */
109
+ export function resolveBundleDir(startUrl = import.meta.url) {
110
+ try {
111
+ return dirname(fileURLToPath(startUrl));
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /** Read a UTF-8 file, returning `null` (never throwing) on any IO error. */
118
+ function readSoft(path) {
119
+ try {
120
+ return existsSync(path) ? readFileSync(path, "utf-8") : null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * Read a file as a raw `ArrayBuffer` (fonts are binary; a UTF-8 round-trip would corrupt them).
128
+ * Returns an exact-length `ArrayBuffer` slice so the served body is a non-shared buffer Hono
129
+ * accepts as a binary `Data`.
130
+ */
131
+ function readBinarySoft(path) {
132
+ try {
133
+ if (!existsSync(path))
134
+ return null;
135
+ const buf = readFileSync(path);
136
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ }
142
+ /**
143
+ * Build the host-side asset reader. Resolves the assets + bundle dirs once and reads on
144
+ * demand. Every read fails soft (missing → `null` → the route 404s), so a stripped install or
145
+ * a not-yet-built bundle never 500s the page. The CSS is concatenated from the token files in
146
+ * declared order so the served page needs ONE stylesheet request and no relative `@import`
147
+ * resolution.
148
+ */
149
+ export function createWebAssets(options = {}) {
150
+ const assetsDir = options.assetsDir === undefined ? resolveAssetsDir() : options.assetsDir;
151
+ const bundleDir = options.bundleDir === undefined ? resolveBundleDir() : options.bundleDir;
152
+ const appJsOverride = options.appJs;
153
+ return {
154
+ css() {
155
+ if (assetsDir === null)
156
+ return null;
157
+ const parts = [];
158
+ for (const rel of CSS_FILES) {
159
+ const text = readSoft(join(assetsDir, rel));
160
+ if (text !== null)
161
+ parts.push(`/* ${rel} */\n${text}`);
162
+ }
163
+ if (parts.length === 0)
164
+ return null;
165
+ // Rewrite the `@font-face` URL prefix so the fonts resolve to the served `/fonts/` route
166
+ // (the host does not serve the on-disk `../logos/fonts/` path → 404). See rewriteFontUrls.
167
+ return { body: rewriteFontUrls(parts.join("\n\n")), contentType: "text/css; charset=utf-8" };
168
+ },
169
+ logo() {
170
+ if (assetsDir === null)
171
+ return null;
172
+ const svg = readSoft(join(assetsDir, LOGO_FILE));
173
+ return svg === null ? null : { body: svg, contentType: "image/svg+xml" };
174
+ },
175
+ font(name) {
176
+ if (assetsDir === null)
177
+ return null;
178
+ // Allow-list ONLY: a name not in the fixed six is rejected (→ 404). No attacker-controlled
179
+ // path component — `name` is never joined unless it is a known leaf filename.
180
+ if (!FONT_ALLOW.has(name))
181
+ return null;
182
+ const bytes = readBinarySoft(join(assetsDir, FONTS_DIR, name));
183
+ return bytes === null ? null : { body: bytes, contentType: fontContentType(name) };
184
+ },
185
+ appJs() {
186
+ if (appJsOverride !== undefined)
187
+ return { body: appJsOverride, contentType: "text/javascript; charset=utf-8" };
188
+ if (bundleDir === null)
189
+ return null;
190
+ const js = readSoft(join(bundleDir, DASHBOARD_APP_BUNDLE));
191
+ return js === null ? null : { body: js, contentType: "text/javascript; charset=utf-8" };
192
+ },
193
+ };
194
+ }
195
+ //# sourceMappingURL=web-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-assets.js","sourceRoot":"","sources":["../../../src/daemon/dashboard/web-assets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { isFleetReady, V1_REQUIRED_PEERS, type FleetDaemonStatus, type FleetHealth, type FleetStatusResponse } from "../shared/fleet-readiness.js";
2
+ export type { FleetDaemonStatus, FleetHealth, FleetStatusResponse };
3
+ export { isFleetReady, V1_REQUIRED_PEERS };
4
+ /**
5
+ * Minimal init surface for the status fetch. `redirect` is pinned (fs-AC-9 defense in depth on top
6
+ * of the loopback URL pin): native fetch defaults to `redirect: "follow"`, so a rogue or compromised
7
+ * loopback service answering on :3852 could 3xx-redirect hive's fetch to a non-loopback origin,
8
+ * silently defeating `isLoopbackBaseUrl()` (which only validates the initial URL). Pinning
9
+ * `redirect: "error"` makes fetch reject on any redirect, so the off-loopback request never fires.
10
+ */
11
+ export type FleetFetchInit = {
12
+ readonly redirect?: "error" | "follow" | "manual";
13
+ };
14
+ export type FetchImpl = (input: string, init?: FleetFetchInit) => Promise<Response>;
15
+ export declare function fetchFleetStatus(fetchImpl?: FetchImpl, url?: string): Promise<FleetStatusResponse>;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { DOCTOR_STATUS_URL } from "../shared/constants.js";
3
+ import { isLoopbackBaseUrl } from "../shared/daemon-routing.js";
4
+ import { isFleetReady, V1_REQUIRED_PEERS } from "../shared/fleet-readiness.js";
5
+ export { isFleetReady, V1_REQUIRED_PEERS };
6
+ const UNREACHABLE_RESPONSE = {
7
+ supervisor: "unreachable",
8
+ daemons: []
9
+ };
10
+ const FleetHealthSchema = z.enum(["ok", "degraded", "unreachable", "unknown"]);
11
+ const FleetDaemonSchema = z.object({
12
+ name: z.string().min(1),
13
+ health: FleetHealthSchema,
14
+ escalation: z.unknown().nullable().optional()
15
+ });
16
+ const DoctorStatusSchema = z.object({
17
+ health: FleetHealthSchema,
18
+ daemons: z.array(FleetDaemonSchema).optional().default([]),
19
+ asOf: z.string().min(1)
20
+ });
21
+ export async function fetchFleetStatus(fetchImpl = fetch, url = DOCTOR_STATUS_URL) {
22
+ if (!isLoopbackBaseUrl(url)) {
23
+ return UNREACHABLE_RESPONSE;
24
+ }
25
+ try {
26
+ const response = await fetchImpl(url, { redirect: "error" });
27
+ if (!response.ok) {
28
+ return UNREACHABLE_RESPONSE;
29
+ }
30
+ let parsedJson;
31
+ try {
32
+ parsedJson = await response.json();
33
+ }
34
+ catch {
35
+ return UNREACHABLE_RESPONSE;
36
+ }
37
+ const parsed = DoctorStatusSchema.safeParse(parsedJson);
38
+ if (!parsed.success) {
39
+ return UNREACHABLE_RESPONSE;
40
+ }
41
+ return {
42
+ supervisor: "reachable",
43
+ health: parsed.data.health,
44
+ daemons: parsed.data.daemons.map((daemon) => ({
45
+ name: daemon.name,
46
+ health: daemon.health,
47
+ escalation: daemon.escalation ?? null
48
+ })),
49
+ asOf: parsed.data.asOf
50
+ };
51
+ }
52
+ catch {
53
+ return UNREACHABLE_RESPONSE;
54
+ }
55
+ }
56
+ //# sourceMappingURL=fleet-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-status.js","sourceRoot":"","sources":["../../src/daemon/fleet-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EACL,YAAY,EACZ,iBAAiB,EAIlB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAE3C,MAAM,oBAAoB,GAAG;IAC3B,UAAU,EAAE,aAAa;IACzB,OAAO,EAAE,EAAE;CAC2B,CAAC;AAEzC,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;AAE/E,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,MAAM,EAAE,iBAAiB;IACzB,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC;AAEH,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,iBAAiB;IACzB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACxB,CAAC,CAAC;AAYH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,YAAuB,KAAK,EAC5B,MAAc,iBAAiB;IAE/B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,oBAAoB,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,OAAO,oBAAoB,CAAC;QAC9B,CAAC;QAED,IAAI,UAAmB,CAAC;QACxB,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,oBAAoB,CAAC;QAC9B,CAAC;QAED,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,oBAAoB,CAAC;QAC9B,CAAC;QAED,OAAO;YACL,UAAU,EAAE,WAAW;YACvB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM;YAC1B,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBAC5C,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,IAAI;aACtC,CAAC,CAAC;YACH,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI;SACvB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,oBAAoB,CAAC;IAC9B,CAAC;AACH,CAAC"}