Commit graph hyperhive/frontend/packages/dashboard/src/core.js
Author SHA1 Message Date
iris
8dd6d05d6c frontend: unify pill/chip/badge shape into shared CSS
Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal" then "make it common css
instead of component, thats fine. but make them look unified (not as
much per usage css)"): shared CSS, not a JS component.

New @hive/shared/pill.css defines two classes, `.hive-pill` (primary
state chips) and `.hive-pill-sm` (secondary meta chips) — border/
border-radius/padding/font-size/letter-spacing (colour stays per-site,
the meaningful semantic part). Every render call site across dashboard
(swarm.js/core.js/builds.js) and agent (index.html/app.js) now carries
one of the two shared classes directly, alongside its own existing
semantic-colour modifier class.

Second cut of this PR, per argus's approve + mara's follow-up review
comment on the first cut: the first version instead enumerated every
legacy classname (`.badge`, `.status-badge`, `.header-pill`, etc.)
straight into pill.css's own selector groups so no call sites needed
touching. Mara's correction: that just relocates the duplication
rather than removing it, and the shared CSS shouldn't have to keep
naming every consumer. This version does the real rename instead.

Most visible consequence, unchanged from the first cut: dashboard's
`.badge` family moves off its own shape (2px square corners, uppercase,
tighter padding) onto the shared rounded-pill shape + agent's "sm" tier
sizing. `npm run build` clean across all three packages; verified the
compiled bundles carry the new classnames at every call site (dashboard
JS, agent index.html + app.js), not just the source tree.

Fixes #3053
2026-08-12 19:25:45 +02:00
iris
2ff4cddf9d frontend: hive-warn — replace severity+pulse with info/warning/error levels
mara, PR review: "i dont like that they still have different styling.
if anything, there should be distinction between info,warning,error
(semantics). all warnings should be styled identically."

Replaces the severity ('amber'|'red') + standalone pulse boolean with
a single level ('info'|'warning'|'error') attribute -- a fixed
three-tier ladder instead of an open combination. Colours reuse
theme.css's own already-documented semantics rather than inventing new
ones: --cyan is already "info accents", --amber already "warnings",
--red already "errors, fail state". Pulse is now baked into `error`
specifically rather than a separate opt-in knob, since the one call
site that wanted attention-grabbing (an active incident) is also the
one that's semantically error -- tying the two together removes a
combination that shouldn't exist independently of the tier.

Reclassified the three call sites explicitly (no implicit default
relied on): credentials.html's GitHub PAT advisory and core.js's K3PT
ST4T3 caveat are both `level="warning"` (same as before, and now
identical to each other by construction, not by coincidence);
swarm.js's port-collision banner is `level="error"` (an active
incident needing operator action now, not a standing caveat).

npm run build clean both packages, grepped for leftover
severity/pulse references (only prose mentions describing what this
replaces).
2026-08-03 18:34:31 +02:00
iris
dfd92a7da9 frontend: shared <hive-warn> component for inline warning banners
Consolidates three independently-written instances of the same thing:
.cred-warning (credentials.html, static markup), .tombstone-warn
(core.js, JS-built), .port-conflict (swarm.js, JS-built) -- the first
two differed only by an undeliberate 10% vs 8% tint, the strongest
argument this was drift rather than three genuinely different needs.

New autonomous custom element, frontend/packages/shared/src/hive-warn/,
matching the established one-component-one-dir shadow-DOM pattern
(hive-btn, hive-toast, hive-dialog). Purely presentational -- no
lifecycle beyond attaching its shadow root once, no
attributeChangedCallback needed since severity/pulse are read directly
by :host([...]) CSS selectors rather than mirrored onto an inner
element. Content passes through via a single default <slot>, so every
call site keeps writing its existing <strong>/<code> markup unchanged.

API: <hive-warn> (amber, default) / <hive-warn severity="red"> for an
active incident vs a standing caveat, plus an opt-in pulse boolean
modifier (only the port-collision banner wants it -- a banner that's
always present and always pulsing just trains you to stop seeing it).
Tint is a single canonical 8% for both severities now, replacing the
10%/8% split.

Registered once in dashboard's common.js (same side-effect-import
pattern as <hive-side-panel>) so every dashboard page picks it up
without a per-file import, since all three call sites (core.js,
swarm.js, credentials.js) already transitively import it.

Verified: npm run build clean for both dashboard and agent packages,
grepped source for leftover cred-warning/tombstone-warn/port-conflict
references (none), confirmed hive-warn/HiveWarn/hive-warn-pulse present
in every affected dist bundle, confirmed the still-live
questions-pulse keyframe (.questions) untouched.
2026-08-03 18:34:31 +02:00
atlas
6a8a729f58 fix(#3020): K3PT ST4T3 says what it shows, and stops reading the job graph
`build_tombstone_views` folded `transient_snapshot`'s keys into its `live`
set, so an agent with in-flight transient work was treated as not-a-tombstone.
Since #3010 that set is derived from the running job graph, which made a
page about on-disk state a function of the scheduler.

Dropping the filter exposes what was always true underneath: nothing records
a destroy. Every definition-side artifact — state subvolume, proposed +
applied repos, `deployed/0`, meta registration, topology entry — is written by
`Provision` before the container exists and survives `lifecycle::destroy`. So
a mid-spawn agent is byte-identical on disk to a tombstone.

Per mara on #3020: remove the filter, warn on the page, keep the issue open
for the swarm-controller / snapshot-storage rework where the problem shape
changes anyway.

- dashboard/tombstones.rs: drop the param + the chain; document the real
  semantics
- core.js: amber caveat banner above the rows; row badge `destroyed` ->
  `offline`, which is what an absent container actually proves
- dashboard.css: `.tombstone-warn`, modelled on `.port-conflict` but amber and
  without the pulse — a permanent banner that pulses trains you to ignore it
- docs/web-ui/dashboard.md: the pane was described as "destroyed-but-state-kept
  agents", now the exact wrong claim
2026-08-03 18:07:04 +02:00
iris
f60e8a8470 dashboard: pass kinds= on the 4 unfiltered /api/dashboard/stream subscribers
Part 1 of the dashboard-event-stream-split epic: the server-side
kinds= allow-list already exists and flow.js already uses it
(hive-c0re/src/dashboard/state_snapshot.rs). tabs.js, builds.js,
core.js, and logs.js were the remaining 4 subscribers still taking
every wire kind unfiltered — pure subscription discipline, no new
endpoint needed, per the investigation on the tracking issue.

Each kinds= list is read directly off that page's own existing
MUTATION_HANDLERS/SSE_HANDLERS dispatch table (tabs.js also needs
sent, checked separately for the operator inbox) — a kind not in a
page's table was already a silent no-op today, so this only removes
wire/parse/dispatch-lookup cost for kinds a page never acted on, zero
behavior change.

Note for reviewers: the SharedWorker (stream-worker.js) multiplexes
by exact URL string, so pages that used to share one unfiltered
upstream connection when open simultaneously (e.g. dashboard.html +
builds.html in two tabs) will now each hold their own filtered
connection instead, since their kinds= differ. Each connection is
still a single cheap SSE stream carrying only what that page acts on
— net win over the shared-but-bloated connection this replaces.
2026-08-02 22:21:24 +02:00
iris
435ef193e1 frontend: add <hive-tab-strip>, convert logs/credentials/core/builds tabbars
Every sub-page tabbar (logs.html, credentials.html, core.html,
builds.html) hand-wrote the same <nav class="hive-tabbar"><a
class="hive-tab">...</a></nav> boilerplate and then called
createTabStrip() on it after the fact. Add <hive-tab-strip>, a
markup-owning custom element (same reuse-boundary pattern as
<hive-menu>/<hive-side-panel>) that renders that markup from a
declarative tabs list, then wires the existing createTabStrip()
behaviour over what it just rendered — no behaviour duplication.

Convert all four sites to use it: each page's JS now calls
`.configure({ tabs, defaultId, onShow })` on the tabbar element instead
of `createTabStrip(el, opts)`, and configure() returns the identical
{ show, active } shape so nothing downstream changes. builds.js's
rebuild-queue count pill (builds-tab-count-rebuild) is expressed as a
tab's `badgeId` and renders nested in the same spot.

The dashboard's own tabbar and the two no-pane stats time-range
pickers are a different markup/behaviour shape and are intentionally
left alone.
2026-08-02 13:40:24 +02:00
iris
7110a25cf6 frontend: extract themed dialogs + async-form handler to shared, wire agent UI
The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.

- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
  updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
  alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
  (no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
  themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
  bindAsyncForms(), and replaced all 8 window.confirm() sites with
  themedConfirm (async, wrapped in a fire-and-forget IIFE where the
  call site needs a synchronous boolean return, e.g. the slash-command
  dispatcher).

Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
2026-07-27 18:55:17 +02:00
iris
3239526f98 fix: apply rustfmt and address argus nit (cpu_quota optional chaining)
- rustfmt expanded two inline if-else expressions in lifecycle_ops.rs
  (lines exceeded rustfmt's line width limit)
- core.js: use optional chaining (cv?.cpu_quota || '—') so a null/empty
  cpu_quota/memory_max on ContainerView renders '—' correctly
  (argus review 🟡, PR #2706)
2026-07-26 16:18:35 +02:00
iris
6c9bf2012f dashboard: show and edit per-agent resource limits in core LOAD tab
Adds CPU/memory cap columns and an inline edit form to the container-load
table in /core.html, backed by a new POST /api/resource-limits/{name}
dashboard endpoint.

## Backend (hive-c0re)

lifecycle_ops.rs — new post_resource_limits handler:
- Parses ResourceLimitsForm { cpu_quota, memory_max } (both optional; empty
  string = clear override, fall back to hive-wide default).
- Validates each non-empty value via resource_limits::validate_cpu_quota /
  validate_memory_max — returns 422 UNPROCESSABLE_ENTITY with a human-
  readable message on invalid input so the dashboard can surface it inline.
- Calls meta::commit_resource_limits (staged git write under META_LOCK, same
  as hivectl set-limits).
- Re-applies the drop-in immediately via lifecycle::write_dropins so the new
  ceilings take effect on the next container start without waiting for a
  rebuild.
- Triggers rescan_containers_and_emit so ContainerView.cpu_quota/memory_max
  update via SSE without waiting for the next periodic sweep.

dashboard/mod.rs — registers the route:
  POST /api/resource-limits/{name}

## Frontend (core.js + system-sections.css)

core.js:
- containersState derived from /api/state snapshot alongside tombstonesState
  — supplies configured cpu_quota/memory_max to the LOAD table.
- lastLoadRows stash lets SSE-triggered re-renders call renderContainerLoad
  without waiting for the next 5s poll.
- renderContainerLoad: adds cpu cap / mem cap columns (muted; tooltip
  'configured ceiling — takes effect on next start') sourced from
  ContainerView, plus a per-row S3T toggle button that expands an inline
  edit form with cpu_quota / memory_max text inputs and a S4V3 button.
  The edit form shows a restart hint, surfaces validation errors inline, and
  collapses on success.
- container_state_changed SSE handler: updates containersState in place and
  re-renders the LOAD table so the cap columns flip immediately after a save.

system-sections.css:
- CSS for the new cap columns (.cload-cap-th, .cload-cap) and inline edit
  form (.cload-edit-row, .cload-edit-form, .cload-edit-label, etc.).
- Remove dead .rqe-step rule (step sub-step label retired from the wire in
  'job_queue: retire the now-off-wire step sub-step label').
2026-07-26 16:00:45 +02:00
iris
1acc271108 fix(#2443): address asyncBtn review notes
- asyncBtn now returns fn().finally(...) so callers can await/chain it
- Move re-fetch calls inside try/catch in core.js and permissions.js so
  network errors from fetchAndRenderStalePerms / fetchAndRender* are
  caught instead of escaping as unhandled rejections
- clearStaleAgent returns the asyncBtn promise so the function is
  properly awaitable when a button is present
- Update asyncBtn doc comment to reflect the return-value contract
2026-07-20 19:59:45 +02:00
iris
4b45c5cd3d feat(#2443): asyncBtn — shared reusable component, replace ad-hoc disable/spinner patterns
add `asyncBtn(btn, fn)` to `@hive/shared/forms.js` as the single
reusable component for async button actions:
  1. double-click guard: returns immediately if btn is already disabled
  2. saves btn.innerHTML, replaces with spinner while in-flight
  3. restores btn on resolve or reject via finally

wire it into all ad-hoc disable/spinner/restore patterns:
  - common.js: bindAsyncForms uses asyncBtn internally
  - core.js: 'clear perms' button
  - permissions.js: clearStaleAgent
  - schedules.js: saveSchedule submit, editSchedule submit
  - app.js: buildAnswerForm, buildInboxMarkAllRow

fireScheduleNow in schedules.js is left with its existing childNode
save/restore because it shows a custom result flash on the button
content after a successful fire-now (the auto-restore of asyncBtn
would overwrite it); the surrounding themedConfirm dialog already
acts as a natural double-click barrier before the fetch.

saveAll in permissions.js is also left as-is: it uses a custom
'queued ✓' success label + a 900ms delay before re-fetch; the
btn.dataset.busy flag is its own double-submit guard.
2026-07-20 19:51:03 +02:00
iris
ef14641b94 feat: add infra container start/stop/restart tab to C0R3 page
New POST /api/infra-container/{name}/{action} dashboard route (start/
stop/restart on hive-ci/hive-forge/hive-gateway/hive-matrix), reusing
the existing priv_client::control_infra_container helper the
infra_admin agent path already uses, plus an audit_log entry per
attempt. Adds infra_containers to the /api/state StateSnapshot (name +
live running status via systemctl is-active). New 1NFR4 sub-tab on the
C0R3 dashboard page: one row per infra container with a running/
stopped badge and start/stop/restart buttons, polled every 5s while
the sub-tab is open.
2026-07-12 03:12:44 +02:00
iris
beb28d5c37 feat(builds): add /builds.html — build lifecycle hub (closes #1999)
New standalone page /builds.html consolidating rebuild queue, meta
inputs, and build log history into one place, with a new 'Builds' home
tile linking to it. Addresses mara's request: new sub-page with a new
home tile, all three items moved there.

Changes:
- builds.html: new page with three sub-tabs: R3BU1LD QU3U3, M3T4 1NPUTS,
  BUILD L0GS. Same minimal-chrome header + createTabStrip pattern as core
  and logs pages.
- builds.js: new bundle combining rebuild queue renderer (from core.js),
  meta inputs renderer (from core.js), rebuild-live-log renderer (from
  core.js), and build log history renderer (from logs.js). Deep-links to
  /builds.html?id=N#buildlogs. Count pill id: builds-tab-count-rebuild.
  BUILD L0GS tab lazy-loads on first activation.
- builds.css: @imports system-sections.css (rebuild queue + meta inputs +
  live-log styles) and logs.css (build-logs-* component styles).
- build.mjs: register builds.js, builds.css, builds.html.
- core.html: remove R3BU1LD QU3U3 + M3T4 1NPUTS tabs (now on builds.html).
  Default tab changes to K3PT ST4T3.
- core.js: remove renderMetaInputs, renderRebuildQueue + helpers,
  renderRebuildLiveLog + live-log state, elapsed-time tickers,
  updateRebuildCount, and the rebuild_queue/meta_inputs SSE handlers.
  Remove openBuildLogStream + util imports no longer needed.
- core.css: remove .rebuild-live-log-* rules (moved to system-sections.css
  so builds.css can share them via @import).
- system-sections.css: add .rebuild-live-log-* styles (moved from core.css);
  update comment to mention builds.html.
- logs.html: remove BUILD tab + pane (moved to builds.html).
- logs.js: remove fetchBuild(), fmtTs, fmtDuration, openBuildLogStream
  import, and rebuild_queue_changed SSE debounce. Default tab: 'agent'.
  SSE stream retained for audit_entry_added live-appends.
- index.html: add Builds tile (🔨, rebuild queue · meta inputs · build
  logs); update Core tile desc to 'kept state · container load'; update
  Logs tile desc to remove 'build'.
2026-06-28 23:56:11 +02:00
iris
4df286345a refactor(permissions): move ghost-perm detection server-side
Add GET /api/permissions/stale endpoint that returns agent names with
explicit capability/tool-group JSON entries but no live container AND
no kept-state tombstone. Ghost detection is now entirely server-side —
one authoritative call, no client-side roster cache, no staleness window.

The previous client-side approach in core.js made three parallel API
calls (GET /api/capabilities, GET /api/tool-groups, GET /api/state) and
filtered the result against a module-level `liveContainerNames` Set
populated only on cold load and form submits. Any container lifecycle
event (spawn, destroy) while core.html was open left `liveContainerNames`
stale, risking a false-positive ghost entry for a live container.

Changes:
- permissions.rs: add `get_stale_permissions` handler + `StalePermsResponse`
  struct. Computes live roster (containers_snapshot), tombstone set
  (Coordinator::kept_state_names), explicit perm names (capabilities::read
  + tool_groups::read), then returns the difference sorted.
- dashboard.rs: register GET /api/permissions/stale.
- core.js: replace the three-call client-side logic in
  `fetchAndRenderStalePerms` with a single fetch to /api/permissions/stale.
  Remove `liveContainerNames` state + its syncFromSnapshot population.
2026-06-28 23:29:51 +02:00
iris
6ab0757cc6 feat(core): add stale permission entries sub-section to K3PT ST4T3
Adds a "stale permission entries" sub-section within the K3PT ST4T3 pane
on /core.html showing agents with explicit capability/tool-group JSON
entries but no live container — typically renamed or manually-deleted agents
whose entries persisted (e.g. the old "root" manager name after rename to
"ruth").

Each ghost agent gets a "✕ clear perms" button that calls
DELETE /api/permissions/{agent} (added in the prior commit). Lazy-loaded
on first K3PT ST4T3 tab activation; auto-refreshes on capabilities_changed
and tool_groups_changed SSE events.

core.js: track liveContainerNames from /api/state.containers; add
renderStalePerms + fetchAndRenderStalePerms; hook tab onShow + SSE handlers.

system-sections.css: new .tombstones-stale-* selectors for the ghost list
rows and error message.

core.html: add #tombstones-stale-perms div inside the K3PT ST4T3 pane;
expand comment to describe both sub-sections.
2026-06-28 23:29:51 +02:00
iris
e797b75ca9 dashboard: stream the running rebuild's build log inline in the queue
The C0R3 rebuild queue only linked out to the logs page (logs →). Add an
inline live-log panel under the queue that streams the currently-running
rebuild's build output, so the operator watches progress without leaving the
page.

Extract the build-log SSE streaming logic (append stdout/stderr, sticky-bottom
scroll, stderr separator, reconnect-replay reset, done/error handling) into a
shared `openBuildLogStream(id, pre, {onDone, onError})` in common.js, and use it
from BOTH the L0GS page BUILD tab (logs.js, previously inline) and the new C0R3
panel (core.js) — one implementation, no duplication.

The panel is one persistent instance keyed to the running entry's build_log_id
(the queue runs one build at a time), in its own container (#rebuild-live-log)
outside rebuild-queue-section so the queue's per-row re-render — rows rebuild as
the build step advances — never tears down the open stream; it reconnects only
when the running build_log_id changes and won't reopen a stream that already
sent done. Collapsible, live/ok/fail badge, raw download. Hidden when nothing
is building; each row keeps its logs → link for full history.

Frontend-only — no backend change (endpoint + build_log_id already existed).
Closes #1860.
2026-06-22 01:55:38 +02:00
iris
93fa264bbf dashboard: stream the running rebuild's build log inline in the queue
The C0R3 rebuild queue only linked out to the logs page (logs →). Add an
inline live-log panel under the queue that streams the currently-running
rebuild's build output, so the operator watches progress without leaving the
page.

One panel keyed to the running entry's build_log_id (the queue runs one build
at a time), reusing the build-log SSE the logs page already uses
(GET /api/build-logs/id/{id}/stream; frames stdout_append/stderr_append/done).
It lives in its own container (#rebuild-live-log) outside rebuild-queue-section
so the queue's per-row re-render — rows rebuild as the build step advances —
never tears down the open stream; it reconnects only when the running
build_log_id changes and won't reopen a stream that already sent done. Sticky-
bottom scroll, collapsible, live/ok/fail badge, raw download. Hidden when
nothing is building; each row keeps its logs → link for full history.

Frontend-only — no backend change (endpoint + build_log_id already existed).
Closes #1860.
2026-06-22 01:48:32 +02:00
iris
905b38b6c7 dashboard: point frontend at the /api/* backend routes
Phase 2 of the dashboard route consolidation. The backend now double-registers
every bare top-level route (approve/deny/kill/restart/start/rebuild/destroy/
update-all/answer-question/cancel-question/purge-tombstone/matrix-account-login/
cancel-reminder/retry-reminder/request-spawn/op-send/meta-update/dashboard-stream/
dashboard-history) at an additional /api/<same> path. Switch every dashboard-pkg
fetch / EventSource / form action to the /api/ form so "backend = /api/*" holds
on the frontend side too.

Bare paths still answer, so this is independently deployable; once it ships the
backend can drop the bare registrations (phase 3). webhook/knowledge stays a
distinct webhook prefix (forge-driven, no SPA caller). app.js's /rebuild,/start
are the agent harness UI (different server) and are untouched.

Part of #1846 (phase 2).
2026-06-22 00:45:26 +02:00
iris
fe5e0bd841 dashboard(core): add disk column to the LOAD container-resources table
Surfaces the per-container on-disk footprint the c0re sampler now
reports as `disk_bytes` on /api/container-resources (state dir +
container writable rootfs, shared nix store excluded; sampled
out-of-band every few minutes). Renders bytes→human via the existing
cloadFmtBytes helper, with an em-dash until the first sample lands
(disk_bytes is null then). Pairs with the hive-c0re sampler half.
2026-06-17 19:12:39 +02:00
iris
878f95205b fix(dashboard): wire data-async form submit handler on the C0R3 page
Clicking "update & rebuild" (and cancel / respawn / purge) on /core.html
navigated to the bare `ok` response page instead of submitting async. The
C0R3 page (split out of the dashboard) carries `data-async` forms but its
bundle never had the global submit interceptor — that handler lived inline
in tabs.js, so only the dashboard bundle had it. The forms POSTed natively
and the browser followed the response.

Fix: lift the `data-async` submit handler out of tabs.js into a shared
`bindAsyncForms(onSuccess)` in common.js (which already owns the `form`
helper that builds these forms), and call it from both pages:
- tabs.js: `bindAsyncForms(() => refreshState())` — behaviour-preserving
  (same handler, now imported).
- core.js: add a `refreshState()` (re-fetch /api/state + re-render) used
  for the cold load and as the post-submit refresh, and call
  `bindAsyncForms(() => refreshState())` at boot.

Forms with `data-no-refresh` (e.g. meta-update, which gets its update via
the meta SSE events) skip the refresh, same as before.
2026-06-10 22:17:40 +02:00
iris
be8d97ded7 feat(dashboard): add standalone C0R3 page (system tab → its own page)
Carves the dashboard's SYST3M tab content out into a standalone page at
/core.html, reached from the H0M3 hub, with a createTabStrip sub-tab
strip (default = Rebuild Queue): rebuild queue, meta inputs, kept state,
container load. Same minimal-chrome standalone-page pattern as
/logs.html (← home back-link + sub-tab nav).

This is the additive first step: the new page is its own esbuild bundle
that cold-loads /api/state and subscribes to /dashboard/stream for the
same live rebuild_queue_changed / meta_inputs_changed / meta_update_running
/ tombstones_changed events the dashboard uses. The four section
renderers are ported from tabs.js; the dashboard's SYST3M tab is left in
place and untouched, so this PR cannot regress the existing dashboard.
Removing the now-duplicate SYST3M tab + de-duplicating the renderers
(into a shared module) + splitting the shared section CSS out of
dashboard.css is the deliberate follow-up.

core.css imports dashboard.css wholesale (transitional) so the ported
sections render identically; dashboard.css does not import common.css so
nothing double-loads. build.mjs gains core.js / core.css / core.html
entries; no Rust change (hive-c0re serves dist/ via ServeDir, and the
/dashboard/stream + /dashboard/history routes are registered ahead of
the fallback).

Page name "C0R3" is a placeholder pending the operator's pick — trivially
renamed (the /core.html URL + the C0R3 label).
2026-06-10 19:51:06 +02:00