build agents only from internal/meta; delete the hive's local meta flake
The hive no longer renders, locks or commits a meta flake. A rebuild's MetaSync node picks the internal/meta commit and records it with the hyperhive and config pins it holds; Prebuild, Swap and Create build from it, and a head that does not pin the agent's current config fails the build naming the agent. hive-priv's meta_rev is required. Removed: meta.rs sync/lock/render, the manual-rebuild relock, the boot sweep's hyperhive bump (MetaLock), the update-meta-inputs form and its panel, MetaWindow, the hive's hyperhive_rev setting. Kept: agent_set::refresh holds what sync_agents also did (topology.json, the gateway's agent-socket map and nginx include), called from spawn, destroy, rebuild and the migration. The boot reconcile and the stale-start upgrade compare the agent's marker with the hyperhive pin of internal/meta; the dashboard's deployed chip reads the config marker a successful build writes.
This commit is contained in:
parent
cea4827918
commit
4aa11b3a94
64 changed files with 1042 additions and 3793 deletions
|
|
@ -52,7 +52,7 @@ Per-hive (host + in-container):
|
||||||
|
|
||||||
- **`hive-c0re/`** — host daemon (runs as the unprivileged `hive-core`
|
- **`hive-c0re/`** — host daemon (runs as the unprivileged `hive-core`
|
||||||
user), **daemon-only** — the operator CLI is `hivectl`. Owns the sqlite
|
user), **daemon-only** — the operator CLI is `hivectl`. Owns the sqlite
|
||||||
broker, reminder queue, meta flake, lifecycle (`nixos-container`
|
broker, reminder queue, lifecycle (`nixos-container`
|
||||||
shellouts), gateway wiring, per-container stats and the axum operator
|
shellouts), gateway wiring, per-container stats and the axum operator
|
||||||
dashboard. Agent forge/matrix accounts are `swarm-controller`'s. Largest
|
dashboard. Agent forge/matrix accounts are `swarm-controller`'s. Largest
|
||||||
crate.
|
crate.
|
||||||
|
|
@ -95,8 +95,8 @@ Per-hive (host + in-container):
|
||||||
utility crate.
|
utility crate.
|
||||||
- **`hive-metric/`** — one-shot CLI pushing a labeled metric over OTLP.
|
- **`hive-metric/`** — one-shot CLI pushing a labeled metric over OTLP.
|
||||||
- **`hive-meta-flake/`** — the meta flake's `flake.nix` text, a pure
|
- **`hive-meta-flake/`** — the meta flake's `flake.nix` text, a pure
|
||||||
render, and the reads of its pins. hive-c0re and swarm-controller both call
|
render, and the reads of its pins. swarm-controller renders with it;
|
||||||
it.
|
hive-c0re reads the pins of the `internal/meta` it builds from.
|
||||||
|
|
||||||
Wire types + socket plumbing:
|
Wire types + socket plumbing:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ create` (or the swarm UI, via `POST /api/agents`) writes that identity
|
||||||
once, by calling `ensure_agent_identity`.
|
once, by calling `ensure_agent_identity`.
|
||||||
|
|
||||||
This page is about a different, **hive-local** file: the hive-c0re-owned
|
This page is about a different, **hive-local** file: the hive-c0re-owned
|
||||||
**meta repo**, alongside `flake.nix`, at
|
`meta/` directory's `topology.json`, at
|
||||||
`/var/lib/hyperhive/meta/topology.json`:
|
`/var/lib/hyperhive/meta/topology.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -102,10 +102,8 @@ state dirs work as for every other agent.
|
||||||
`/var/lib/hyperhive/agents` bind-mounted RW at `/agents` (so it can
|
`/var/lib/hyperhive/agents` bind-mounted RW at `/agents` (so it can
|
||||||
manage any agent's state dir — config isn't authored there, since a
|
manage any agent's state dir — config isn't authored there, since a
|
||||||
real config change is a PR from a clone), plus RO mounts for
|
real config change is a PR from a clone), plus RO mounts for
|
||||||
`/applied` (diff against what's deployed) and `/meta` (system-wide
|
`/applied` (diff against what's deployed) and `/meta` (the hive's
|
||||||
deploy log). Any agent holding it gets the same mounts.
|
topology and limits files). Any agent holding it gets the same mounts.
|
||||||
The operator dashboard's `POST /api/meta-update` is the only path
|
|
||||||
that writes `flake.lock`.
|
|
||||||
- **Prompt** — identical for every container but one block (below):
|
- **Prompt** — identical for every container but one block (below):
|
||||||
`prompts/system.md`
|
`prompts/system.md`
|
||||||
carries `<!-- role:agent -->` / `<!-- role:manager -->` marker
|
carries `<!-- role:agent -->` / `<!-- role:manager -->` marker
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,11 @@ until you create ruth again from the swarm dashboard.
|
||||||
|
|
||||||
## Meta flake
|
## Meta flake
|
||||||
|
|
||||||
The hive-c0re-owned repo at `/var/lib/hyperhive/meta/`
|
A hive has no meta flake of its own. It builds every agent from the swarm's
|
||||||
declares one flake input per agent (`agent-<n>.url =
|
`internal/meta`, which swarm-controller commits ([below](#the-swarms-meta-flake)).
|
||||||
"git+<forge>/agent-configs/<n>.git?rev=<sha>"`) and one
|
It declares one flake input per agent (`agent-<n>.url =
|
||||||
|
"git+<forge>/agent-configs/<n>.git?rev=<sha>"`, the head of `main` on the
|
||||||
|
agent's forge config repo, the one config PRs merge into) and one
|
||||||
`nixosConfigurations.<n>` output per agent. Each output wraps
|
`nixosConfigurations.<n>` output per agent. Each output wraps
|
||||||
`inputs.agent-<n>.nixosModules.default` with the identity +
|
`inputs.agent-<n>.nixosModules.default` with the identity +
|
||||||
`HIVE_PORT` / `HIVE_LABEL` injection module. Values that differ per host
|
`HIVE_PORT` / `HIVE_LABEL` injection module. Values that differ per host
|
||||||
|
|
@ -123,53 +125,32 @@ declares one flake input per agent (`agent-<n>.url =
|
||||||
address) aren't in it:
|
address) aren't in it:
|
||||||
`hive-priv` writes them to the container's `/etc/hyperhive-hive-facts` at
|
`hive-priv` writes them to the container's `/etc/hyperhive-hive-facts` at
|
||||||
start (see `docs/process/conventions.md`).
|
start (see `docs/process/conventions.md`).
|
||||||
Containers run against `--flake /var/lib/hyperhive/meta#<n>`, or against
|
A config that sits only in the hive's `applied/<n>` mirror, not on forge
|
||||||
`internal/meta` when it pins the agent ([below](#the-swarms-meta-flake)).
|
`main`, never deploys.
|
||||||
|
|
||||||
The declared input url is the head of `main` on the agent's forge config repo
|
What `hive-c0re` still keeps under `/var/lib/hyperhive/meta/` is plain
|
||||||
(`agent-configs/<n>`, the one config PRs merge into), which `sync_agents`
|
state, bound read-only into the root agent's container as `/meta`:
|
||||||
reads at render time. The flake text depends only on those shas and the
|
`topology.json`, the swarm resource limits, and the permission seed files of a
|
||||||
agent set, so any machine that can reach the forge resolves it. `sync_agents` fetches
|
hive that predates the swarm's permissions. `agent_set::refresh(agents:
|
||||||
each repo's `main` with the hive's read token (`forge::reader_token()`),
|
&[AgentSpec])` keeps `topology.json`, the gateway's agent-socket map and its
|
||||||
pins its head, and takes the canonical `follows` lines from the
|
nginx include in line with the live agents. `provision_container`, `destroy`,
|
||||||
`flake.lock` at that commit. If the forge is unreachable or a repo has no
|
`rebuild` and the startup migration call it. It refuses an empty agent list,
|
||||||
`main`, `sync_agents` fails for that agent with its name and writes
|
which means `nixos-container list` failed, and it fails when `HIVE_FORGE_URL`
|
||||||
nothing; there is no fallback to a local path. A config that sits only in
|
is unset, since every agent would come up with no forge.
|
||||||
the hive's `applied/<n>` mirror, not on forge `main`, never deploys.
|
|
||||||
The rebuild paths re-lock the input with `nix flake update agent-<n>` against that
|
|
||||||
same pinned url, so `flake.nix` and `flake.lock` name no local path.
|
|
||||||
|
|
||||||
Lock updates are single-phase and commit when the lock changed:
|
The dashboard's `deployed:<sha12>` chip per container row shows the config
|
||||||
`meta::lock_update_for_rebuild(name)` relocks one agent's input for a
|
commit the agent's last successful build pinned.
|
||||||
relocking rebuild (the manual `↻ R3BU1LD` button, and the rebuild a
|
|
||||||
merged config commit queues), and `meta::lock_update_hyperhive()` is the
|
|
||||||
autoupdate flake-rev bump (one shot before per-agent rebuilds).
|
|
||||||
|
|
||||||
`meta::sync_agents(hive: &HiveEnv, agents: &[AgentSpec])` — `hive`
|
|
||||||
carries `hyperhive_flake`, `dashboard_port`, and the rest of the
|
|
||||||
per-hive config — is the idempotent reconciler called by `provision_container`,
|
|
||||||
`destroy`, `rebuild`, and the startup migration. Renders `flake.nix`
|
|
||||||
from the agent list; if it differs from disk, or `flake.nix` or `flake.lock`
|
|
||||||
has an uncommitted change (a sync stopped before its commit), runs
|
|
||||||
`nix flake lock` + commits as `regenerate meta flake` (or
|
|
||||||
`seed meta from N agent(s)` on the first call). A clean tree whose
|
|
||||||
`flake.nix` already matches returns without running nix.
|
|
||||||
|
|
||||||
The root agent has `/meta` RO-bound inside its container:
|
|
||||||
`git -C /meta log --oneline` is the swarm-wide deploy log,
|
|
||||||
`cat /meta/flake.lock | jq '.nodes["agent-<n>"].locked'`
|
|
||||||
resolves which sha the flake pins each agent at right now.
|
|
||||||
Dashboard surfaces the same info as a `deployed:<sha12>` chip
|
|
||||||
per container row.
|
|
||||||
|
|
||||||
## The swarm's meta flake
|
## The swarm's meta flake
|
||||||
|
|
||||||
swarm-controller also commits a meta flake of its own, `internal/meta`, with
|
swarm-controller commits `internal/meta`: one `flake.nix` and `flake.lock` for
|
||||||
the same renderer: one `flake.nix` and `flake.lock` for every agent any hive
|
every agent any hive places (the pins:
|
||||||
places (the pins: [`docs/swarm/builder.md`](../swarm/builder.md#meta-flake-lock)).
|
[`docs/swarm/builder.md`](../swarm/builder.md#meta-flake-lock)). A hive builds
|
||||||
A hive builds an agent from it when the repo has a commit that pins the
|
every agent from it. While the repo has no commit that pins an agent's current
|
||||||
agent's current config, and from its own meta flake above otherwise, so a hive
|
config, that agent's build fails and names the agent. The controller queues
|
||||||
older or newer than the controller keeps building.
|
its update of the repo before the deploy request for a merged config commit
|
||||||
|
and for a new agent, so a failure here is rare. It sends the deploy even when
|
||||||
|
that update failed, and the build then fails as described.
|
||||||
|
|
||||||
- **Choosing the commit.** The rebuild's `MetaSync` node reads the repo's
|
- **Choosing the commit.** The rebuild's `MetaSync` node reads the repo's
|
||||||
default-branch head with the hive's read token (`forge::reader_token()`)
|
default-branch head with the hive's read token (`forge::reader_token()`)
|
||||||
|
|
@ -178,8 +159,9 @@ older or newer than the controller keeps building.
|
||||||
`main`, an archetype agent from the archetype it has now. An agent switched
|
`main`, an archetype agent from the archetype it has now. An agent switched
|
||||||
between custom and archetype, or to another archetype, fails that test.
|
between custom and archetype, or to another archetype, fails that test.
|
||||||
A hive without the `core` token file that nix's git reads, a repo with no
|
A hive without the `core` token file that nix's git reads, a repo with no
|
||||||
commit, a head that lacks the agent and a head behind the config all select
|
commit, a head that lacks the agent and a head behind the config each fail
|
||||||
the hive's own flake. `MetaSync` records the commit in `hive-c0re`'s own state,
|
the build with an error that names the agent. `MetaSync` records the commit,
|
||||||
|
with the hyperhive and config commits the head pins, in `hive-c0re`'s own state,
|
||||||
`/var/lib/hyperhive/meta-rev/<n>`, which the agent can't write (the container mounts its runtime
|
`/var/lib/hyperhive/meta-rev/<n>`, which the agent can't write (the container mounts its runtime
|
||||||
directory read-write); `Prebuild` and `Swap` read that
|
directory read-write); `Prebuild` and `Swap` read that
|
||||||
record, so both build one flake. A first deploy resolves the commit in its `Create`
|
record, so both build one flake. A first deploy resolves the commit in its `Create`
|
||||||
|
|
@ -190,10 +172,8 @@ older or newer than the controller keeps building.
|
||||||
`CreateContainer`). `hive-priv` accepts only a 40-digit lowercase hex
|
`CreateContainer`). `hive-priv` accepts only a 40-digit lowercase hex
|
||||||
commit and builds the flake from its own forge URL
|
commit and builds the flake from its own forge URL
|
||||||
(`HIVE_PRIV_FORGE_URL`); the URL never comes from a request. On the wire the
|
(`HIVE_PRIV_FORGE_URL`); the URL never comes from a request. On the wire the
|
||||||
`hive_priv_sock::PrivRequest` variants carry an optional field:
|
`hive_priv_sock::PrivRequest` variants carry a required field:
|
||||||
`{"op":"update_container","name":"atlas","stream":true,"meta_rev":"<40 hex>"}`.
|
`{"op":"update_container","name":"atlas","stream":true,"meta_rev":"<40 hex>"}`.
|
||||||
Without it, as from a hive-c0re that predates the field, hive-priv builds the
|
|
||||||
hive's own flake.
|
|
||||||
- **Fetching.** nix fetches the forge's inputs with `git`. Both `hive-c0re`
|
- **Fetching.** nix fetches the forge's inputs with `git`. Both `hive-c0re`
|
||||||
and `hive-priv` carry a `$HOME/.gitconfig` that answers for the forge host
|
and `hive-priv` carry a `$HOME/.gitconfig` that answers for the forge host
|
||||||
through `git-credential-hive-forge` (the `core` token file) and rewrites the
|
through `git-credential-hive-forge` (the `core` token file) and rewrites the
|
||||||
|
|
@ -201,6 +181,11 @@ older or newer than the controller keeps building.
|
||||||
`http`.
|
`http`.
|
||||||
- **Hyperhive.** The commit pins one hyperhive rev for the swarm, so an agent
|
- **Hyperhive.** The commit pins one hyperhive rev for the swarm, so an agent
|
||||||
runs the hyperhive of the controller's deploy, not its hive's.
|
runs the hyperhive of the controller's deploy, not its hive's.
|
||||||
|
- **Markers.** After a successful build `hive-c0re` writes the hyperhive
|
||||||
|
commit the head pins to `applied/.<n>.hyperhive-rev`, and the config
|
||||||
|
commit to `applied/.<n>.config-rev` (none for an archetype agent). The boot
|
||||||
|
reconcile ([below](#autoupdate-on-startup)) and the dashboard's update chip
|
||||||
|
read them.
|
||||||
|
|
||||||
## Two repos per agent
|
## Two repos per agent
|
||||||
|
|
||||||
|
|
@ -223,11 +208,9 @@ older or newer than the controller keeps building.
|
||||||
├── agent.nix # working tree of main
|
├── agent.nix # working tree of main
|
||||||
└── <other committed files> # also tracked
|
└── <other committed files> # also tracked
|
||||||
|
|
||||||
/var/lib/hyperhive/meta/ swarm-wide flake — core
|
/var/lib/hyperhive/meta/ plain hive state — core
|
||||||
├── .git/ # one commit per lock
|
├── topology.json # the live agent set
|
||||||
│ # change
|
└── swarm-resource-limits.json # the swarm's per-agent caps
|
||||||
├── flake.nix # generated from agent set
|
|
||||||
└── flake.lock # pins each agent's sha
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Why two physical repos: the submitting agent's `/agents/<n>/config/` is
|
Why two physical repos: the submitting agent's `/agents/<n>/config/` is
|
||||||
|
|
@ -236,8 +219,8 @@ proposed tree. The applied repo is never bind-mounted (except
|
||||||
the read-only `.git` exposure described below) so a destructive
|
the read-only `.git` exposure described below) so a destructive
|
||||||
move inside the container can't reach it.
|
move inside the container can't reach it.
|
||||||
|
|
||||||
The container's `--flake` ref is `/var/lib/hyperhive/meta#<name>` (or `internal/meta`)
|
The container's `--flake` ref is `internal/meta` (see "Meta flake" above).
|
||||||
(see "Meta flake" above). The agent's own `applied/<n>/flake.nix`
|
The agent's own `applied/<n>/flake.nix`
|
||||||
is a fixed boilerplate that exports `nixosModules.default =
|
is a fixed boilerplate that exports `nixosModules.default =
|
||||||
import ./agent.nix`; the meta flake imports that module and
|
import ./agent.nix`; the meta flake imports that module and
|
||||||
wraps it with identity + `HIVE_PORT` / `HIVE_LABEL`.
|
wraps it with identity + `HIVE_PORT` / `HIVE_LABEL`.
|
||||||
|
|
@ -246,7 +229,7 @@ wraps it with identity + `HIVE_PORT` / `HIVE_LABEL`.
|
||||||
|
|
||||||
hive-c0re plants `deployed/0` on the seed commit at first spawn. A
|
hive-c0re plants `deployed/0` on the seed commit at first spawn. A
|
||||||
merged config commit that deploys plants no tag: the merged PR on the
|
merged config commit that deploys plants no tag: the merged PR on the
|
||||||
forge and meta's lock commits record it. A config PR nobody merges
|
forge and `internal/meta`'s commits record it. A config PR nobody merges
|
||||||
carries no extra state on the forge side: the PR stays open, and
|
carries no extra state on the forge side: the PR stays open, and
|
||||||
the submitter pushes again (or closes it) to retry.
|
the submitter pushes again (or closes it) to retry.
|
||||||
|
|
||||||
|
|
@ -335,9 +318,6 @@ git -C /agents/<n>/config log applied/main --oneline
|
||||||
git -C /agents/<n>/config show applied/refs/tags/deployed/0 # the seed commit
|
git -C /agents/<n>/config show applied/refs/tags/deployed/0 # the seed commit
|
||||||
git -C /agents/<n>/config show applied/refs/tags/denied/<id> # body = operator note
|
git -C /agents/<n>/config show applied/refs/tags/denied/<id> # body = operator note
|
||||||
git -C /agents/<n>/config rebase applied/main # base in-flight work on what's deployed
|
git -C /agents/<n>/config rebase applied/main # base in-flight work on what's deployed
|
||||||
|
|
||||||
git -C /meta log --oneline # swarm-wide deploy history
|
|
||||||
cat /meta/flake.lock | jq '.nodes | with_entries(select(.key | startswith("agent-")))'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The RO binds block push at the kernel level — git plumbing inside the
|
The RO binds block push at the kernel level — git plumbing inside the
|
||||||
|
|
@ -346,16 +326,17 @@ container can't corrupt either authoritative repo.
|
||||||
## Startup migrations (older hosts)
|
## Startup migrations (older hosts)
|
||||||
|
|
||||||
hive-c0re runs a couple of idempotent migrations on every startup so a
|
hive-c0re runs a couple of idempotent migrations on every startup so a
|
||||||
host set up before the tag-driven-deploy + meta-flake scheme (both
|
host set up before the tag-driven-deploy scheme (described above)
|
||||||
described above) converges to it automatically. Each phase is a no-op
|
converges to it automatically. Each phase is a no-op
|
||||||
once already applied:
|
once already applied:
|
||||||
|
|
||||||
- **Tags**: hive-c0re tags agents from before the tag-driven scheme
|
- **Tags**: hive-c0re tags agents from before the tag-driven scheme
|
||||||
`deployed/0` on `main` once. Non-destructive — it doesn't touch live
|
`deployed/0` on `main` once. Non-destructive — it doesn't touch live
|
||||||
containers, state dirs, or claude creds.
|
containers, state dirs, or claude creds.
|
||||||
- **Meta flake**: rewrites each `applied/<n>/flake.nix` to the
|
- **Agent files**: rewrites each `applied/<n>/flake.nix` to the
|
||||||
module-only boilerplate, wires the `applied` remote in each proposed
|
module-only boilerplate, wires the `applied` remote in each proposed
|
||||||
repo, and bootstraps the meta repo from the current agent list. Set
|
repo, and refreshes the files that follow the agent set
|
||||||
|
(`agent_set::refresh`) from the current agent list. Set
|
||||||
`HIVE_SKIP_META_MIGRATION=1` on the service to defer this phase.
|
`HIVE_SKIP_META_MIGRATION=1` on the service to defer this phase.
|
||||||
|
|
||||||
No state loss in either migration: claude creds, `/state/` notes, the
|
No state loss in either migration: claude creds, `/state/` notes, the
|
||||||
|
|
@ -450,19 +431,25 @@ swarm-level alert could carry the same signal) — only reach for a new
|
||||||
|
|
||||||
`hive-c0re serve` runs `auto_update::run` in a background task right
|
`hive-c0re serve` runs `auto_update::run` in a background task right
|
||||||
after opening the coordinator. It enumerates managed containers and
|
after opening the coordinator. It enumerates managed containers and
|
||||||
rebuilds any whose recorded hyperhive rev differs from the current
|
rebuilds any whose recorded hyperhive rev differs from the one
|
||||||
one — sub-agents and the root agent go through the same
|
`internal/meta` pins now. That's how a hive that missed the swarm's
|
||||||
`job_queue::templates::rebuild` DAG.
|
fan-out catches up. Sub-agents and the root agent go through the same
|
||||||
|
graceful rebuild subgraph.
|
||||||
|
|
||||||
"Rev" = canonical filesystem path of
|
"Rev" = the hyperhive commit pinned by the head of `internal/meta`, as
|
||||||
`services.hyperhive.c0re.hyperhiveFlake`. Marker
|
the hive reads it with its forge token. Marker
|
||||||
file: `/var/lib/hyperhive/applied/.<name>.hyperhive-rev`. If the
|
file: `/var/lib/hyperhive/applied/.<name>.hyperhive-rev`, written after
|
||||||
flake input has no canonical path (for example a `github:` URL),
|
a successful build. If the hive can't read the pin (no forge token, the
|
||||||
autoupdate is a no-op — rebuild manually.
|
forge down, `internal/meta` not published yet), the scan rebuilds
|
||||||
|
nothing; a rebuild would have no flake to build from either. A hive deploy
|
||||||
|
alone doesn't rebuild agents.
|
||||||
|
|
||||||
The dashboard surfaces pending updates per agent: a clickable
|
A stopped agent gets its rebuild at its next start: the start path upgrades
|
||||||
"needs update ↻" badge appears whenever the marker differs from
|
the start of a stale, stopped agent to a rebuild followed by the start.
|
||||||
current rev. The badge POSTs `/api/rebuild/<name>`, which inserts the
|
|
||||||
|
The dashboard surfaces pending config changes per agent: a clickable
|
||||||
|
"needs update ↻" badge appears whenever the agent's applied repo has a
|
||||||
|
commit that its last build didn't pin. The badge POSTs `/api/rebuild/<name>`, which inserts the
|
||||||
same `job_queue::templates::rebuild` DAG so manual triggers and the
|
same `job_queue::templates::rebuild` DAG so manual triggers and the
|
||||||
startup scan can't drift. When at least one container is stale, a
|
startup scan can't drift. When at least one container is stale, a
|
||||||
top-level `↻ UPD4TE 4LL` button appears that loops over every
|
top-level `↻ UPD4TE 4LL` button appears that loops over every
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,7 @@ this hive's archetype agents runs, as last read from the swarm's
|
||||||
with an entry has no `applied/<n>` repo and no `agent-<n>` meta input; see
|
with an entry has no `applied/<n>` repo and no `agent-<n>` meta input; see
|
||||||
[archetypes](../swarm/archetypes.md).
|
[archetypes](../swarm/archetypes.md).
|
||||||
|
|
||||||
hive-c0re renders containers onto `meta#<n>` at creation.
|
hive-c0re creates containers from `internal/meta` (`nixosConfigurations.<n>`).
|
||||||
|
|
||||||
## Destroy vs purge
|
## Destroy vs purge
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,7 @@ forge container:
|
||||||
|
|
||||||
- fetching a merged config commit for a swarm deploy, and
|
- fetching a merged config commit for a swarm deploy, and
|
||||||
`hivectl forge reconcile-config`
|
`hivectl forge reconcile-config`
|
||||||
- reseeding a purged `applied` repo, and the meta-update cascade's
|
- reseeding a purged `applied` repo
|
||||||
fast-forward of `applied/main`
|
|
||||||
- seeding `proposed` from `agent-configs/<agent>` on first deploy
|
- seeding `proposed` from `agent-configs/<agent>` on first deploy
|
||||||
- cloning and pulling `/knowledge`
|
- cloning and pulling `/knowledge`
|
||||||
|
|
||||||
|
|
@ -84,7 +83,7 @@ The builds themselves read as `core`. nix fetches the config inputs and
|
||||||
`internal/meta` with `git`, and `git` gets its credential from
|
`internal/meta` with `git`, and `git` gets its credential from
|
||||||
`git-credential-hive-forge`, which reads the `core` token file. A hive without
|
`git-credential-hive-forge`, which reads the `core` token file. A hive without
|
||||||
that file fetches nothing private through nix, whatever its own token reads, so
|
that file fetches nothing private through nix, whatever its own token reads, so
|
||||||
it builds from its own meta flake rather than `internal/meta`.
|
its builds fail: they have no other flake than `internal/meta`.
|
||||||
|
|
||||||
Roll a change to any of this out store host first (its granter writes the
|
Roll a change to any of this out store host first (its granter writes the
|
||||||
controller's grant on `swarm/hives/+/forge/token`), then the controller host,
|
controller's grant on `swarm/hives/+/forge/token`), then the controller host,
|
||||||
|
|
|
||||||
|
|
@ -294,15 +294,15 @@ internal convention that routes an agent name to
|
||||||
|
|
||||||
```
|
```
|
||||||
# ❌ silently builds the wrong thing (or errors if attr doesn't exist)
|
# ❌ silently builds the wrong thing (or errors if attr doesn't exist)
|
||||||
nix build /var/lib/hyperhive/meta#argus.config.system.build.toplevel
|
nix build "git+<forge>/internal/meta.git?rev=<rev>#argus.config.system.build.toplevel"
|
||||||
|
|
||||||
# ✅ explicit path nix build actually resolves
|
# ✅ explicit path nix build actually resolves
|
||||||
nix build /var/lib/hyperhive/meta#nixosConfigurations.argus.config.system.build.toplevel
|
nix build "git+<forge>/internal/meta.git?rev=<rev>#nixosConfigurations.argus.config.system.build.toplevel"
|
||||||
```
|
```
|
||||||
|
|
||||||
`lifecycle::prebuild_toplevel` hit this once by constructing the attr
|
`lifecycle::prebuild_toplevel` hit this once by constructing the attr
|
||||||
path as `{flake_ref}.config…` — which produced `meta#argus.config…`
|
path as `{flake_ref}.config…` — which produced `#argus.config…`
|
||||||
instead of `meta#nixosConfigurations.argus.config…`. The fix:
|
instead of `#nixosConfigurations.argus.config…`. The fix:
|
||||||
`split_once('#')` to separate flake path from name, then template
|
`split_once('#')` to separate flake path from name, then template
|
||||||
`{path}#nixosConfigurations.{name}.config.system.build.toplevel`.
|
`{path}#nixosConfigurations.{name}.config.system.build.toplevel`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ section covers the concrete node inventory hive-c0re builds on top of that
|
||||||
engine, plus the internals (module layout, scheduler mechanics,
|
engine, plus the internals (module layout, scheduler mechanics,
|
||||||
resource/lease semantics).
|
resource/lease semantics).
|
||||||
|
|
||||||
hive-c0re submits every container/meta operation (rebuild, meta-update, first-spawn, power
|
hive-c0re submits every container operation (rebuild, first-spawn, power
|
||||||
changes) to the global job-DAG queue (`hive-c0re/src/job_queue/`)
|
changes) to the global job-DAG queue (`hive-c0re/src/job_queue/`)
|
||||||
as a **DAG of primitive nodes**. One scheduler task drives all DAGs;
|
as a **DAG of primitive nodes**. One scheduler task drives all DAGs;
|
||||||
concurrency comes from the resource classes below, not from multiple workers.
|
concurrency comes from the resource classes below, not from multiple workers.
|
||||||
Special cases like graceful stop, deferred starts, and the meta-update
|
Special cases like graceful stop, deferred starts, and the boot sweep
|
||||||
cascade need no bespoke code paths — each takes the shape of a DAG built
|
need no bespoke code paths — each takes the shape of a DAG built
|
||||||
from the same primitive nodes.
|
from the same primitive nodes.
|
||||||
|
|
||||||
### Two levels: DAG and node
|
### Two levels: DAG and node
|
||||||
|
|
@ -46,8 +46,7 @@ Nix-heavy — hold one of the `buildSlots` permits for the node's duration:
|
||||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `Prebuild` | `lifecycle::prebuild_toplevel` — build the toplevel out-of-band while the container keeps serving (its meta preamble is the upstream `MetaSync` node). Skipped when the container is already down; `Swap` builds inline instead |
|
| `Prebuild` | `lifecycle::prebuild_toplevel` — build the toplevel out-of-band while the container keeps serving (its meta preamble is the upstream `MetaSync` node). Skipped when the container is already down; `Swap` builds inline instead |
|
||||||
| `Swap` | drop-in rewrite + `nixos-container update` profile-swap (requires the container stopped); the post-swap bookkeeping tail lives in the sibling `RebuildBookkeeping` node |
|
| `Swap` | drop-in rewrite + `nixos-container update` profile-swap (requires the container stopped); the post-swap bookkeeping tail lives in the sibling `RebuildBookkeeping` node |
|
||||||
| `Create` | first-spawn `nixos-container create` proper; assumes the upstream `Provision` node already registered the agent in meta |
|
| `Create` | first-spawn `nixos-container create` proper; assumes the upstream `Provision` node already registered the agent; builds from the `internal/meta` commit it resolves and marks the agent's pins |
|
||||||
| `MetaLock` | meta flake lock bump (`lock_update` / boot-sweep `lock_update_hyperhive`, commit fused — see below); fans out child `Rebuild` DAGs on completion |
|
|
||||||
|
|
||||||
Cheap — no build slot:
|
Cheap — no build slot:
|
||||||
|
|
||||||
|
|
@ -55,13 +54,13 @@ Cheap — no build slot:
|
||||||
|
|
||||||
| Node | Behavior |
|
| Node | Behavior |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `MetaSync` | the rebuild's meta preamble — rebuild-dir prep, idempotent meta `sync_agents`, optional per-agent relock. Holds the `MetaWindow` resource (below); deliberately its own node so the window never covers `Prebuild`'s multi-minute build |
|
| `MetaSync` | the rebuild's preamble — rebuild-dir prep, the agent-set refresh (`agent_set::refresh`), and the `internal/meta` commit `Prebuild` and `Swap` build from, resolved once and recorded; fails when the repo doesn't pin the agent's current config. Declares no resources |
|
||||||
| `Provision` | first-spawn pre-create provisioning — proposed/applied repos, state subvolume, meta registration (`sync_agents`); runs ahead of `Create` so the `nixos-container create --flake meta#<name>` ref resolves. Store/meta-only, no container yet |
|
| `Provision` | first-spawn pre-create provisioning — proposed/applied repos, state subvolume, agent-set refresh (`agent_set::refresh`); runs ahead of `Create`. Store-only, no container yet |
|
||||||
| `Reconcile` | idempotent power converge: read `wanted` (below) + observed state; start if `Up` & down (cold-start fallback included), stop if `Offline` & up, else noop |
|
| `Reconcile` | idempotent power converge: read `wanted` (below) + observed state; start if `Up` & down (cold-start fallback included), stop if `Offline` & up, else noop |
|
||||||
| `Start` | mechanical container start — runtime dir + drop-ins, `start_with_fallback`, MCP listener registration, the manager kick. Fanned out by a `Reconcile` that observed `wanted = Up` and the container down |
|
| `Start` | mechanical container start — runtime dir + drop-ins, `start_with_fallback`, MCP listener registration, the manager kick. Fanned out by a `Reconcile` that observed `wanted = Up` and the container down |
|
||||||
| `Stop` | mechanical container stop — `nixos-container` kill, MCP listener unregister, the `Killed` manager notify. Fanned out by a `Reconcile` that observed `wanted = Offline` and up |
|
| `Stop` | mechanical container stop — `nixos-container` kill, MCP listener unregister, the `Killed` manager notify. Fanned out by a `Reconcile` that observed `wanted = Offline` and up |
|
||||||
| `StopForUpdate` | mechanical `nixos-container stop` for the profile swap; never touches `wanted`; noop if already stopped |
|
| `StopForUpdate` | mechanical `nixos-container stop` for the profile swap; never touches `wanted`; noop if already stopped |
|
||||||
| `RebuildBookkeeping` | the swap's Ok-only bookkeeping tail — rev marker, forge/matrix sync, manager kick, rescan, meta-inputs snapshot; `AfterOk(Swap)` so it runs only on a successful swap (the DAG's `EmitRebuilt` tail node emits the `Rebuilt` manager event, not here). Split out of `Swap` for dashboard visibility + retry granularity, declares no resources of its own — a coordinated child of the `AgentWindow` brace |
|
| `RebuildBookkeeping` | the swap's Ok-only bookkeeping tail — rev markers, forge/matrix sync, manager kick, rescan; `AfterOk(Swap)` so it runs only on a successful swap (the DAG's `EmitRebuilt` tail node emits the `Rebuilt` manager event, not here). Split out of `Swap` for dashboard visibility + retry granularity, declares no resources of its own — a coordinated child of the `AgentWindow` brace |
|
||||||
| `AgentWindow` | pure resource holder — the brace for one agent's rebuild. Declares the build slot + agent lease atomically and holds both for its whole subtree, so `Prebuild` and the `Signal`→`Drain` quiesce window run concurrently instead of one nested under the other. Performs no work; see _Braces_ |
|
| `AgentWindow` | pure resource holder — the brace for one agent's rebuild. Declares the build slot + agent lease atomically and holds both for its whole subtree, so `Prebuild` and the `Signal`→`Drain` quiesce window run concurrently instead of one nested under the other. Performs no work; see _Braces_ |
|
||||||
| `Signal` | set the graceful fence + kick, so the harness runs one stop-checkpoint turn |
|
| `Signal` | set the graceful fence + kick, so the harness runs one stop-checkpoint turn |
|
||||||
| `Drain` | await the harness clearing the fence, bounded by the 3-min graceful-stop timeout; resolves ok either way |
|
| `Drain` | await the harness clearing the fence, bounded by the 3-min graceful-stop timeout; resolves ok either way |
|
||||||
|
|
@ -80,29 +79,8 @@ Cheap — no build slot:
|
||||||
|
|
||||||
<!-- vale write-good.Passive = YES -->
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Deliberately, **no `GitCommit` node** exists: `meta.rs` fuses each mutation
|
Nothing in the queue commits to a meta repo: the hive has none to commit to,
|
||||||
with its commit under its internal `META_LOCK` mutex, so a standalone commit
|
and `agent_set::refresh` serializes itself with its own mutex.
|
||||||
node would open a dirty-working-tree window between nodes.
|
|
||||||
|
|
||||||
Two further layers protect the meta repo across _windows_ that span multiple
|
|
||||||
`META_LOCK` acquisitions:
|
|
||||||
|
|
||||||
- **The deploy window** (`Resource::MetaWindow`): a global, capacity-1 queue
|
|
||||||
resource declared by every node kind that mutates the meta repo — `MetaSync`,
|
|
||||||
`MetaLock` and `Provision`'s agent registration. Two meta
|
|
||||||
mutations can therefore never interleave, so no commit lands inside another
|
|
||||||
node's window. It's a queue resource rather than a runtime mutex
|
|
||||||
because a subtree root holds a resource across its whole subtree, which
|
|
||||||
a `MutexGuard` (bounded by one executor fn) can't. For the same reason the window must stay
|
|
||||||
_off_ long store-only work: the rebuild's meta preamble is its own
|
|
||||||
`MetaSync` node, a sibling of (never a parent of) `Prebuild`, so the
|
|
||||||
toplevel build runs outside the window and `buildSlots > 1` still gives
|
|
||||||
concurrent rebuilds across agents.
|
|
||||||
- **Path-limited commits**: the targeted meta committers (perm files,
|
|
||||||
topology, lock bumps) commit `-- <their paths>` with path-scoped
|
|
||||||
dirty checks, so even a non-queue caller (boot migration, destroy's
|
|
||||||
`sync_agents`) can never sweep someone else's staged content into its
|
|
||||||
commit.
|
|
||||||
|
|
||||||
### Every operation as a DAG
|
### Every operation as a DAG
|
||||||
|
|
||||||
|
|
@ -138,8 +116,7 @@ restart(a..): online a: [Signal→Drain→ if graceful] StopForUpdate(a) → Re
|
||||||
offline a: Reconcile(a) (nothing to stop; Reconcile converges to existing wanted)
|
offline a: Reconcile(a) (nothing to stop; Reconcile converges to existing wanted)
|
||||||
start(a..): a: SetWanted(a,Up) → Reconcile(a) (down+stale ⇒ SetWanted(a,Up) → «rebuild subgraph»)
|
start(a..): a: SetWanted(a,Up) → Reconcile(a) (down+stale ⇒ SetWanted(a,Up) → «rebuild subgraph»)
|
||||||
spawn(a): [wanted=Up at deploy] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a)
|
spawn(a): [wanted=Up at deploy] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a)
|
||||||
meta-update(inp): MetaLock(inp) →(in-DAG) «rebuild subgraph» per affected agent
|
boot: «graceful rebuild subgraph» per stale agent (rev marker ≠ the `internal/meta` pin);
|
||||||
boot: (if any rev marker stale) MetaLock(hyperhive) →(in-DAG) «rebuild subgraph» per stale agent;
|
|
||||||
plus Reconcile(a) for every drifted agent (all ONE DAG)
|
plus Reconcile(a) for every drifted agent (all ONE DAG)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -157,14 +134,9 @@ Notable collapses:
|
||||||
- **Graceful stop needs no watcher thread**: `Signal`/`Drain` are cheap, so a
|
- **Graceful stop needs no watcher thread**: `Signal`/`Drain` are cheap, so a
|
||||||
whole-hive graceful stop fires every agent's signal immediately and all
|
whole-hive graceful stop fires every agent's signal immediately and all
|
||||||
drains overlap; each DAG's tail `Reconcile` does the actual stop.
|
drains overlap; each DAG's tail `Reconcile` does the actual stop.
|
||||||
- **The meta-update cascade grows in the same DAG on completion**:
|
- **The boot sweep is one DAG**: it declares one graceful `rebuild` subgraph
|
||||||
`MetaLock`'s executor computes the affected agent set after the bump lands
|
per stale agent, each an independent root, beside a `Reconcile` per drifted
|
||||||
and grows one `rebuild` subgraph per agent into its _own_ DAG via
|
agent. Not child DAGs — one DAG, no `parent_id`.
|
||||||
`append_subgraph` (rooted on the `MetaLock`, `relock = false` so the cascade
|
|
||||||
doesn't revert the bump). Not child DAGs — one DAG, no `parent_id`. A failed
|
|
||||||
bump appends nothing (no cancel-children dance). Same shape as the startup
|
|
||||||
sweep; the meta-update DAG carries the `Rebuilding` transient so each cascade
|
|
||||||
agent keeps crash-watch suppression during its `Swap`.
|
|
||||||
|
|
||||||
### Desired-state (spec vs status)
|
### Desired-state (spec vs status)
|
||||||
|
|
||||||
|
|
@ -174,7 +146,7 @@ Per-agent power _intent_ — `wanted: Up | Offline` — is durable as the
|
||||||
two. Setting `wanted` is never a queued node: the power layer
|
two. Setting `wanted` is never a queued node: the power layer
|
||||||
(`job_queue/power.rs`) writes the row synchronously, then inserts the DAG
|
(`job_queue/power.rs`) writes the row synchronously, then inserts the DAG
|
||||||
whose `Reconcile` reads the fresh value — rapid toggles are last-writer-wins.
|
whose `Reconcile` reads the fresh value — rapid toggles are last-writer-wins.
|
||||||
Power toggles never commit to the meta repo. Every operator power surface —
|
Every operator power surface —
|
||||||
dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` —
|
dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` —
|
||||||
rides the queue through that power layer, so intent, lease serialization,
|
rides the queue through that power layer, so intent, lease serialization,
|
||||||
and crash-watch suppression can't drift per surface; the only direct starts
|
and crash-watch suppression can't drift per surface; the only direct starts
|
||||||
|
|
@ -188,9 +160,9 @@ The admin-socket responses carry the submitted DAG ids; `hivectl` polls
|
||||||
`HostRequest::QueueNodes` (~1s) and prints a progress line per DAG — roll-up
|
`HostRequest::QueueNodes` (~1s) and prints a progress line per DAG — roll-up
|
||||||
glyph, template, agent, node chain — so
|
glyph, template, agent, node chain — so
|
||||||
CLI verbs block until their jobs finish (`--no-wait` opts out; failures exit
|
CLI verbs block until their jobs finish (`--no-wait` opts out; failures exit
|
||||||
non-zero). Nodes appended in-DAG (a `MetaLock` growing per-agent rebuild
|
non-zero). Nodes appended in-DAG (a `Reconcile` fanning its `Start`/`Stop`)
|
||||||
subgraphs, a `Reconcile` fanning its `Start`/`Stop`) join the same DAG, so
|
join the same DAG, so they surface under that DAG's id in the same loop — no
|
||||||
they surface under that DAG's id in the same loop — no separate child DAGs.
|
separate child DAGs.
|
||||||
|
|
||||||
### Scheduler semantics
|
### Scheduler semantics
|
||||||
|
|
||||||
|
|
@ -213,8 +185,8 @@ resources are free. Resources:
|
||||||
(`SetWanted` is a store write, not a container op, but takes the lease anyway
|
(`SetWanted` is a store write, not a container op, but takes the lease anyway
|
||||||
so a power-op DAG's intent write + reconcile is atomic — two racing ops can't
|
so a power-op DAG's intent write + reconcile is atomic — two racing ops can't
|
||||||
clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`,
|
clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`,
|
||||||
`Prebuild`, `Provision`, `MetaLock` — they
|
`Prebuild`, `Provision` — they
|
||||||
touch the store / meta, not the running container, which is exactly why a
|
touch the store, not the running container, which is exactly why a
|
||||||
stop can land while another DAG's prebuild is still building. Also exempt, for a different
|
stop can land while another DAG's prebuild is still building. Also exempt, for a different
|
||||||
reason, are the rebuild subtree's own members (`StopForUpdate`, `Swap`,
|
reason, are the rebuild subtree's own members (`StopForUpdate`, `Swap`,
|
||||||
`Signal`, `Drain`, `RebuildBookkeeping`): they genuinely do touch the
|
`Signal`, `Drain`, `RebuildBookkeeping`): they genuinely do touch the
|
||||||
|
|
@ -317,56 +289,39 @@ render.
|
||||||
|
|
||||||
On startup, `auto_update::run` classifies every agent by rev freshness (the
|
On startup, `auto_update::run` classifies every agent by rev freshness (the
|
||||||
per-agent `.{name}.hyperhive-rev` marker under `/var/lib/hyperhive/applied/`
|
per-agent `.{name}.hyperhive-rev` marker under `/var/lib/hyperhive/applied/`
|
||||||
vs the current flake path) and persisted `wanted` intent, then:
|
vs the hyperhive commit the head of `internal/meta` pins) and persisted
|
||||||
|
`wanted` intent, then:
|
||||||
|
|
||||||
1. **Config path** — when _any_ marker is stale, submit one `Boot`
|
1. **Config path** — submit one `Boot` DAG holding a graceful `Rebuild`
|
||||||
DAG: a `MetaLock` (hyperhive input bump, non-fatal) that grows an in-DAG
|
subgraph for each stale agent whose `wanted = Up` (name-sorted). This is how
|
||||||
`Rebuild` subgraph for each stale agent whose `wanted = Up` (name-sorted). Stale but wanted-offline agents get no boot-time nix work — their
|
a hive that missed the swarm's fan-out catches up. Stale but wanted-offline
|
||||||
rebuild happens on their next start (the start submit path upgrades a
|
agents get no boot-time nix work — their rebuild happens on their next start
|
||||||
stale start to rebuild+start), which is also why the lock bump runs even
|
(the start submit path upgrades a stale start to rebuild+start). Each
|
||||||
when every stale agent is offline: those later start-upgrades must build
|
rebuild's tail `Reconcile` brings the agent (back) up, covering both the
|
||||||
against the bumped lock. Each child rebuild's tail `Reconcile` brings the
|
running-stale and stopped-but-wanted-up cases. A hive that can't read the
|
||||||
agent (back) up, covering both the running-stale and stopped-but-wanted-up
|
pin (no forge token, the forge down, no `internal/meta` yet) rebuilds
|
||||||
cases.
|
nothing.
|
||||||
|
|
||||||
2. **Power path** — every agent whose observed state drifted from `wanted`
|
2. **Power path** — every agent whose observed state drifted from `wanted`
|
||||||
gets a plain `Reconcile` DAG (`kind = reconcile`, source `auto_update`).
|
gets a plain `Reconcile` DAG (`kind = reconcile`, source `auto_update`).
|
||||||
|
|
||||||
Booting with no config change performs **no meta commit** — only reconciles.
|
Booting with no pin change performs no rebuild — only reconciles.
|
||||||
The sweep reason records the rebuild / deferred / up-to-date counts so the
|
The boot log line records the rebuild / deferred / up-to-date counts so the
|
||||||
operator sees at a glance how much work the boot triggered. `auto_update::run`
|
operator sees at a glance how much work the boot triggered. `auto_update::run`
|
||||||
seeds agents without an `agent_power` row from observed state during
|
seeds agents without an `agent_power` row from observed state during
|
||||||
classification (the one-time migration; thereafter the DB is authoritative).
|
classification (the one-time migration; thereafter the DB is authoritative).
|
||||||
|
|
||||||
## Meta flake
|
## Meta flake
|
||||||
|
|
||||||
`meta.rs` owns the single coordinator-managed flake at `/var/lib/hyperhive/meta/`.
|
Agents build from the swarm's `internal/meta` flake, which swarm-controller
|
||||||
This flake consumes every agent's applied config repo as a flake input and exports
|
commits and every hive reads (see
|
||||||
one `nixosConfiguration` per agent. Container lifecycle ops drive the lock file so
|
[Config changes](../agent-lifecycle/config-changes.md#the-swarms-meta-flake)).
|
||||||
meta's git log is the system-wide deploy audit trail.
|
The hive keeps no flake of its own. `meta_rev.rs` picks the `internal/meta`
|
||||||
|
commit an agent builds from and records it; `agent_set.rs` keeps the
|
||||||
Key operations:
|
host-side files that follow the agent set (`topology.json`, the gateway's
|
||||||
|
agent-socket map and nginx include) in line with the live agents, and
|
||||||
- **`sync_agents`** (idempotent) — render `flake.nix` for the current agent set,
|
`meta.rs` keeps the one remaining commit to the legacy `meta/` git repo, the
|
||||||
init the repo on first call, relock if the rendered contents changed, commit.
|
removal of the permission seed files.
|
||||||
Called by spawn / destroy / startup migration.
|
|
||||||
- **`lock_update_hyperhive`** — one-shot for the boot-reconcile path (the
|
|
||||||
sweep DAG's `MetaLock` node): bumps the `hyperhive` input lock and commits;
|
|
||||||
the scheduler fans out the agent rebuilds on completion.
|
|
||||||
|
|
||||||
Every public `meta.rs` operation takes the module's internal `META_LOCK`
|
|
||||||
mutex, so concurrent job-queue nodes never
|
|
||||||
race on the repo's `.git/index.lock`.
|
|
||||||
|
|
||||||
Each git or nix child under `META_LOCK` has a deadline, so a hung forge fetch
|
|
||||||
fails the operation instead of holding the lock: 10 minutes for
|
|
||||||
`nix flake update`/`lock` and forge fetch/push, 2 minutes for local git, including
|
|
||||||
the `rev-parse` and `show` reads `pin_agents` makes. The error names the command
|
|
||||||
and the deadline. Each child leads its own process group and the timeout
|
|
||||||
`SIGKILL`s the whole group, so a `git` or credential helper spawned by a hung
|
|
||||||
`nix` dies too. A caller waiting on `META_LOCK` for over a minute logs a
|
|
||||||
`meta_lock: still waiting` warning every minute, naming its own operation and
|
|
||||||
the holder's. Waiters keep their place in the lock's FIFO queue across warnings.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -389,10 +344,8 @@ one go, rather than the double-bounce a live `update` would trigger.
|
||||||
|
|
||||||
Sequence for a rebuild DAG (each step is its own queue node):
|
Sequence for a rebuild DAG (each step is its own queue node):
|
||||||
|
|
||||||
1. `MetaSync` — rebuild-dir prep, meta `sync_agents`, and (unless this is a
|
1. `MetaSync` — rebuild-dir prep, the agent-set refresh, and the choice of the
|
||||||
meta-update cascade child) the per-agent relock. Short, and the only step
|
`internal/meta` commit to build from. Short, and it declares no resources.
|
||||||
that mutates the meta repo, so it's the only one holding the global deploy
|
|
||||||
window.
|
|
||||||
2. `Prebuild` — build the new `system.build.toplevel` **before** stopping.
|
2. `Prebuild` — build the new `system.build.toplevel` **before** stopping.
|
||||||
The container keeps serving the previous generation while eval + fetch +
|
The container keeps serving the previous generation while eval + fetch +
|
||||||
build happen out-of-band. `nixos-container update` then finds the result
|
build happen out-of-band. `nixos-container update` then finds the result
|
||||||
|
|
@ -401,7 +354,7 @@ Sequence for a rebuild DAG (each step is its own queue node):
|
||||||
container is already down — a stopped agent has no uptime to preserve,
|
container is already down — a stopped agent has no uptime to preserve,
|
||||||
so `Swap` builds inline instead of paying the double eval.
|
so `Swap` builds inline instead of paying the double eval.
|
||||||
3. `StopForUpdate` — bring the container down (noop when already stopped).
|
3. `StopForUpdate` — bring the container down (noop when already stopped).
|
||||||
4. `Swap` — `nixos-container update --flake meta#<name>` profile-swap
|
4. `Swap` — `nixos-container update` profile-swap from `internal/meta`
|
||||||
(near-instant after the prebuild).
|
(near-instant after the prebuild).
|
||||||
5. `Reconcile` — boot into the new generation when `wanted = Up`; the
|
5. `Reconcile` — boot into the new generation when `wanted = Up`; the
|
||||||
in-container activation script transitions old → new. Holds no build
|
in-container activation script transitions old → new. Holds no build
|
||||||
|
|
@ -424,15 +377,15 @@ fallback.
|
||||||
|
|
||||||
For a first-time `create`, `nixos-container create` is atomic: if the build fails,
|
For a first-time `create`, `nixos-container create` is atomic: if the build fails,
|
||||||
it leaves no container record to clean up. A separate prebuild would just duplicate
|
it leaves no container record to clean up. A separate prebuild would just duplicate
|
||||||
the eval, so it's skipped. Sequence: `create --flake meta#<name>` → write nspawn
|
the eval, so it's skipped. Sequence: `create` from `internal/meta` → write nspawn
|
||||||
flags → `systemctl daemon-reload` → `start`.
|
flags → `systemctl daemon-reload` → `start`.
|
||||||
|
|
||||||
### Prebuild attr path
|
### Prebuild attr path
|
||||||
|
|
||||||
`nix build` doesn't autoresolve `meta#<name>` against `nixosConfigurations` the
|
`nix build` doesn't autoresolve `<flake-root>#<name>` against `nixosConfigurations` the
|
||||||
way `nixos-container` does internally. nix requires the explicit attr path
|
way `nixos-container` does internally. nix requires the explicit attr path
|
||||||
`<flake-root>#nixosConfigurations.<name>.config.system.build.toplevel`;
|
`<flake-root>#nixosConfigurations.<name>.config.system.build.toplevel`;
|
||||||
using the bare `meta#<name>` ref would make nix look in `packages`, `legacyPackages`,
|
using the bare `<flake-root>#<name>` ref would make nix look in `packages`, `legacyPackages`,
|
||||||
or the flake root directly — none of which exist in the rendered meta flake.
|
or the flake root directly — none of which exist in the rendered meta flake.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,11 @@ rebuild, and keeps the last value read in
|
||||||
without the queue. When the read fails, a rebuild keeps the value
|
without the queue. When the read fails, a rebuild keeps the value
|
||||||
stored there. For an archetype agent the hive:
|
stored there. For an archetype agent the hive:
|
||||||
|
|
||||||
- seeds no `proposed` or `applied` repo, and gives the meta flake no
|
- seeds no `proposed` or `applied` repo, and gives `internal/meta` no
|
||||||
`agent-<name>` input. `mkAgent` imports
|
`agent-<name>` input. `mkAgent` imports
|
||||||
`hyperhive.hyperhiveArchetypes.<name>.module` in place of the agent's own
|
`hyperhive.hyperhiveArchetypes.<name>.module` in place of the agent's own
|
||||||
flake, and sets `HIVE_ARCHETYPE` in the harness environment;
|
flake, and sets `HIVE_ARCHETYPE` in the harness environment;
|
||||||
- relocks no agent input on a rebuild or a meta-input bump, and ignores the
|
- ignores the `rev` of a swarm deploy request;
|
||||||
`rev` of a swarm deploy request;
|
|
||||||
- mounts an empty `/agents/<name>/config/`.
|
- mounts an empty `/agents/<name>/config/`.
|
||||||
|
|
||||||
The agent's system prompt says it has no config repo and names its
|
The agent's system prompt says it has no config repo and names its
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ lock doesn't wait for a build in progress and has ten minutes. The builder
|
||||||
commits nothing.
|
commits nothing.
|
||||||
|
|
||||||
swarm-controller sends the request. It renders `internal/meta`'s `flake.nix`
|
swarm-controller sends the request. It renders `internal/meta`'s `flake.nix`
|
||||||
with the same renderer hive-c0re uses (`hive-meta-flake`), all over `https`.
|
with the `hive-meta-flake` renderer, all over `https`.
|
||||||
Each custom agent that a hive places pins to its `agent-configs/<agent>`
|
Each custom agent that a hive places pins to its `agent-configs/<agent>`
|
||||||
`main`; the controller omits and logs one whose repo has no commit on `main`, so it
|
`main`; the controller omits and logs one whose repo has no commit on `main`, so it
|
||||||
doesn't block the rest. Hyperhive pins to one rev, a ratchet on the controller's own
|
doesn't block the rest. Hyperhive pins to one rev, a ratchet on the controller's own
|
||||||
|
|
@ -147,8 +147,9 @@ repo. It re-renders when it starts, and again every ten minutes while the latest
|
||||||
update hasn't succeeded (a tick adds nothing while one is pending or running,
|
update hasn't succeeded (a tick adds nothing while one is pending or running,
|
||||||
or after a success), before it deploys a merged config PR, and
|
or after a success), before it deploys a merged config PR, and
|
||||||
before the first deploy of a new agent. Those deploys run whether or not the
|
before the first deploy of a new agent. Those deploys run whether or not the
|
||||||
update succeeded: a hive that builds from `internal/meta` does so only while it
|
update succeeded: a hive builds only from `internal/meta`, so while the repo
|
||||||
pins the agent's current config (see
|
doesn't pin the agent's current config that build fails and names the agent
|
||||||
|
(see
|
||||||
[Config changes](../agent-lifecycle/config-changes.md#the-swarms-meta-flake)). A render
|
[Config changes](../agent-lifecycle/config-changes.md#the-swarms-meta-flake)). A render
|
||||||
equal to the committed `flake.nix` sends no lock.
|
equal to the committed `flake.nix` sends no lock.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ The agent's `flake.nix` forwards every flake input to `agent.nix` as
|
||||||
the `flakeInputs` module arg, so you pull in external MCP-server flakes
|
the `flakeInputs` module arg, so you pull in external MCP-server flakes
|
||||||
by adding them to `inputs.*` and reference them as
|
by adding them to `inputs.*` and reference them as
|
||||||
`flakeInputs.<name>.packages.${pkgs.system}.default` — the resolved
|
`flakeInputs.<name>.packages.${pkgs.system}.default` — the resolved
|
||||||
sha lands in the agent's own `flake.lock` and rolls up to meta's.
|
sha lands in the agent's own `flake.lock` and rolls up to `internal/meta`'s.
|
||||||
|
|
||||||
`allowedTools` defaults to `["*"]`, which expands to
|
`allowedTools` defaults to `["*"]`, which expands to
|
||||||
`mcp__<key>__*` (every tool from that server autoapproved). Restrict
|
`mcp__<key>__*` (every tool from that server autoapproved). Restrict
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ surfaces — Stats, the API docs and the swarm UI — and each of
|
||||||
those pages links back to the dashboard.
|
those pages links back to the dashboard.
|
||||||
|
|
||||||
The dashboard is where you'll spend most of your time. It's a single
|
The dashboard is where you'll spend most of your time. It's a single
|
||||||
page with five tabs:
|
page with four tabs:
|
||||||
|
|
||||||
- **4G3NTS** — every agent, live. This is the default tab and the one
|
- **4G3NTS** — every agent, live. This is the default tab and the one
|
||||||
you'll check most.
|
you'll check most.
|
||||||
- **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build
|
- **R3BU1LD QU3U3** and **BUILD L0GS** — the build
|
||||||
lifecycle: the rebuild queue with a live log of whatever is building,
|
lifecycle: the rebuild queue with a live log of whatever is building,
|
||||||
the meta flake inputs you can bump, and the build log history.
|
and the build log history.
|
||||||
- **K3PT ST4T3** — kept state from agents with no container
|
- **K3PT ST4T3** — kept state from agents with no container
|
||||||
(tombstones), with a purge button per entry.
|
(tombstones), with a purge button per entry.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ dashboard tab strip.
|
||||||
`HIVE_AUTHELIA_PUBLIC_URL` (set from `services.hyperhive.swarm.authelia.url`).
|
`HIVE_AUTHELIA_PUBLIC_URL` (set from `services.hyperhive.swarm.authelia.url`).
|
||||||
If the name doesn't load the avatar shows a question mark, and without the URL the
|
If the name doesn't load the avatar shows a question mark, and without the URL the
|
||||||
popover has no links.
|
popover has no links.
|
||||||
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`,
|
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`,
|
||||||
`◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS
|
`◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS
|
||||||
lives on its own page, reached from the pages menu. Count pills on
|
lives on its own page, reached from the pages menu. Count pills on
|
||||||
4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and
|
4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and
|
||||||
|
|
@ -73,7 +73,7 @@ agent is stale.
|
||||||
|
|
||||||
Passive / rare-interaction state. `kept.js` renders it from the
|
Passive / rare-interaction state. `kept.js` renders it from the
|
||||||
`/api/state` snapshot and keeps it live from the `tombstones_changed`
|
`/api/state` snapshot and keeps it live from the `tombstones_changed`
|
||||||
stream event. (Rebuild queue and meta inputs are dashboard tabs too —
|
stream event. (The rebuild queue and build logs are dashboard tabs too —
|
||||||
see below.)
|
see below.)
|
||||||
|
|
||||||
Per-container CPU, memory and disk load isn't shown in the hive UI. The
|
Per-container CPU, memory and disk load isn't shown in the hive UI. The
|
||||||
|
|
@ -96,16 +96,16 @@ container is all hive-c0re can actually prove. Fixing it properly
|
||||||
needs a recorded destroy — **deferred** to the swarm-controller /
|
needs a recorded destroy — **deferred** to the swarm-controller /
|
||||||
snapshot-storage rework where the problem changes shape.
|
snapshot-storage rework where the problem changes shape.
|
||||||
|
|
||||||
## Build tabs (`#rebuild`, `#meta`, `#buildlogs`)
|
## Build tabs (`#rebuild`, `#buildlogs`)
|
||||||
|
|
||||||
The build lifecycle — rebuild queue, live build log, meta inputs, and
|
The build lifecycle — rebuild queue, live build log, and
|
||||||
build log history — is three tabs of the dashboard: **R3BU1LD QU3U3**
|
build log history — is two tabs of the dashboard: **R3BU1LD QU3U3**
|
||||||
(`#rebuild`), **M3T4 1NPUTS** (`#meta`) and **BUILD L0GS**
|
(`#rebuild`) and **BUILD L0GS**
|
||||||
(`#buildlogs`). They live in `builds.js`, which `tabs.js` imports: the
|
(`#buildlogs`). They live in `builds.js`, which `tabs.js` imports: the
|
||||||
dashboard's one tab strip, `/api/state` snapshot and
|
dashboard's one tab strip, `/api/state` snapshot and
|
||||||
`/api/dashboard/stream` subscription serve them, with
|
`/api/dashboard/stream` subscription serve them, with
|
||||||
`rebuild_queue_changed`, `meta_inputs_changed` and `meta_update_running`
|
`rebuild_queue_changed` on the stream alongside the dashboard's other
|
||||||
on the stream alongside the dashboard's other kinds. The count pill on
|
kinds. The count pill on
|
||||||
R3BU1LD QU3U3 is the number of rebuilds in flight.
|
R3BU1LD QU3U3 is the number of rebuilds in flight.
|
||||||
The 4G3NTS tab does **not** read the graph endpoint, by design — the swarm
|
The 4G3NTS tab does **not** read the graph endpoint, by design — the swarm
|
||||||
view stays independent of job-queue internals. Its per-agent pending
|
view stays independent of job-queue internals. Its per-agent pending
|
||||||
|
|
@ -115,7 +115,7 @@ handful of pre-tallied counts, not the graph. The banner's "view queue →"
|
||||||
link goes to the `#rebuild` tab.
|
link goes to the `#rebuild` tab.
|
||||||
|
|
||||||
**R3BU1LD QU3U3** — pending, in-flight, and recently settled container
|
**R3BU1LD QU3U3** — pending, in-flight, and recently settled container
|
||||||
operations: rebuilds, meta-update cascades, and first-spawns. One
|
operations: rebuilds and first-spawns. One
|
||||||
operation runs at a time; the worker drains FIFO. **Is a mounted
|
operation runs at a time; the worker drains FIFO. **Is a mounted
|
||||||
`JobqGraph`** (the shared generic graph-viewer Preact component,
|
`JobqGraph`** (the shared generic graph-viewer Preact component,
|
||||||
`@hive/shared/jobq-graph.js` — the same one swarm-ui uses) —
|
`@hive/shared/jobq-graph.js` — the same one swarm-ui uses) —
|
||||||
|
|
@ -184,20 +184,6 @@ own container outside `#rebuild-queue-section` so the mounted
|
||||||
`JobqGraph`'s own re-renders never disturb the open poll; it hides
|
`JobqGraph`'s own re-renders never disturb the open poll; it hides
|
||||||
when nothing is building.
|
when nothing is building.
|
||||||
|
|
||||||
**M3T4 1NPUTS** — inputs in `meta/flake.lock` the operator can
|
|
||||||
selectively `nix flake update`, rendered as an indented tree:
|
|
||||||
every fetched input at every depth (`hyperhive`,
|
|
||||||
`hyperhive/nixpkgs`, `agent-<n>`, `agent-<n>/mcp-<x>`, …), each
|
|
||||||
shown once at its shallowest path. `read_meta_inputs` walks the
|
|
||||||
lock graph with a `visited` set, skipping `follows` aliases and rev-less
|
|
||||||
nodes. A `select all / select none`
|
|
||||||
control sits above the tree. Checking inputs + submitting bumps
|
|
||||||
the lock in `/meta/` and rebuilds the selected agents in
|
|
||||||
sequence; each outcome reaches the root agent as a `rebuilt`
|
|
||||||
system event. `POST /meta-update`. While a lock-bump ripple runs,
|
|
||||||
the panel shows a pulsing "⏳ meta-update running" banner and disables
|
|
||||||
the update button (snapshot field `meta_update_running`,
|
|
||||||
live event `meta_update_running`).
|
|
||||||
|
|
||||||
**BUILD L0GS** — all-agents build log history. Lazy-loaded on first tab
|
**BUILD L0GS** — all-agents build log history. Lazy-loaded on first tab
|
||||||
activation; autorefreshes when
|
activation; autorefreshes when
|
||||||
|
|
@ -378,7 +364,7 @@ life). Two consequences for anything rendering it:
|
||||||
to rebuild-shaped work — `running_transients()` in hive-c0re is
|
to rebuild-shaped work — `running_transients()` in hive-c0re is
|
||||||
a status-only test (any `Running` node whose payload names a
|
a status-only test (any `Running` node whose payload names a
|
||||||
non-empty agent lights a pill), so work the operator never
|
non-empty agent lights a pill), so work the operator never
|
||||||
triggered (a meta-update cascade, a crash-recover rebuild, a
|
triggered (a crash-recover rebuild, a boot-sweep rebuild, a
|
||||||
lease-exempt `prebuild`) lights the same mechanism.
|
lease-exempt `prebuild`) lights the same mechanism.
|
||||||
|
|
||||||
Ops with no queue node behind them (destroy, migration) supply
|
Ops with no queue node behind them (destroy, migration) supply
|
||||||
|
|
@ -575,8 +561,7 @@ below — some endpoints aren't in it yet.
|
||||||
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
|
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
|
||||||
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
|
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
|
||||||
failed (clears the failure state so the scheduler retries).
|
failed (clears the failure state so the scheduler retries).
|
||||||
- `POST /api/meta-update` — `nix flake update` the selected
|
|
||||||
`meta/flake.lock` inputs, then rebuild the affected agents.
|
|
||||||
- `GET /api/dashboard/stream` — unified live event channel:
|
- `GET /api/dashboard/stream` — unified live event channel:
|
||||||
broker `sent` / `delivered`, plus the mutation events listed
|
broker `sent` / `delivered`, plus the mutation events listed
|
||||||
below. Each frame carries `seq`.
|
below. Each frame carries `seq`.
|
||||||
|
|
@ -620,18 +605,12 @@ payload):
|
||||||
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
|
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
|
||||||
full snapshot of destroyed-but-state-kept agents. Emitted on
|
full snapshot of destroyed-but-state-kept agents. Emitted on
|
||||||
destroy, purge, and revive. The K3PT ST4T3 tab subscribes.
|
destroy, purge, and revive. The K3PT ST4T3 tab subscribes.
|
||||||
- `meta_inputs_changed` (seq, inputs: `Vec<MetaInput>`) — full
|
|
||||||
snapshot of `meta/flake.lock` inputs. The M3T4 1NPUTS tab
|
|
||||||
subscribes; same shape as `GET /api/meta-inputs`.
|
|
||||||
- `meta_update_running` (running: bool) — emitted when a
|
|
||||||
`nix flake update` ripple starts or completes. The M3T4 1NPUTS
|
|
||||||
tab uses this to show/hide the "⏳ meta-update running" banner.
|
|
||||||
|
|
||||||
<!-- vale write-good.Passive = NO -->
|
<!-- vale write-good.Passive = NO -->
|
||||||
`/api/state` is **only fetched on cold-load and on the few
|
`/api/state` is **only fetched on cold-load and on the few
|
||||||
forms that mutate non-event-derived state** (PURG3 +
|
forms that mutate non-event-derived state** (PURG3, since
|
||||||
meta-update, since tombstones + meta_inputs aren't event-
|
tombstones aren't event-shaped yet). Every other section — transients,
|
||||||
shaped yet). Every other section — transients,
|
|
||||||
containers —
|
containers —
|
||||||
derives from `/api/dashboard/stream` after the initial snapshot,
|
derives from `/api/dashboard/stream` after the initial snapshot,
|
||||||
maintaining its own client-side store and applying events on
|
maintaining its own client-side store and applying events on
|
||||||
|
|
|
||||||
|
|
@ -597,7 +597,7 @@ export function renderContainers(s) {
|
||||||
|
|
||||||
// Containers come from the derived map (event-driven) rather than
|
// Containers come from the derived map (event-driven) rather than
|
||||||
// `s.containers`; `s` still supplies hostname (for the web-ui
|
// `s.containers`; `s` still supplies hostname (for the web-ui
|
||||||
// link) and tombstones/meta_inputs (not event-derived yet). The
|
// link) and tombstones (not event-derived yet). The
|
||||||
// tree builder handles the ordering — we don't pre-sort here.
|
// tree builder handles the ordering — we don't pre-sort here.
|
||||||
const containers = Array.from(containersState.values());
|
const containers = Array.from(containersState.values());
|
||||||
const portConflicts = derivePortConflicts(containers);
|
const portConflicts = derivePortConflicts(containers);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
// The build lifecycle tabs of the dashboard — rebuild queue, live build
|
// The build lifecycle tabs of the dashboard — rebuild queue, live build
|
||||||
// log, meta inputs, and build log history. Not an entry point: tabs.js
|
// log, and build log history. Not an entry point: tabs.js
|
||||||
// imports it, owns the tab strip and the dashboard event stream, and calls
|
// imports it, owns the tab strip and the dashboard event stream, and calls
|
||||||
// the exports below. Three tabs, the panes' ids matching the tab ids:
|
// the exports below. Two tabs, the panes' ids matching the tab ids:
|
||||||
// - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log
|
// - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log
|
||||||
// - meta (M3T4 1NPUTS): nix flake input selector + meta-update
|
|
||||||
// - buildlogs (BUILD L0GS): all-agents build log history, lazy-loaded
|
// - buildlogs (BUILD L0GS): all-agents build log history, lazy-loaded
|
||||||
// the first time the tab is shown
|
// the first time the tab is shown
|
||||||
|
|
||||||
|
|
@ -11,12 +10,11 @@ import { $, fmtAgeSecs, openBuildLogStream } from "./common.js";
|
||||||
import { el } from "@hive/shared/dom.js";
|
import { el } from "@hive/shared/dom.js";
|
||||||
import { themedConfirm } from "@hive/shared/modal.js";
|
import { themedConfirm } from "@hive/shared/modal.js";
|
||||||
import { h, render } from "preact";
|
import { h, render } from "preact";
|
||||||
import { fmtAgo, fmtDuration, truncate } from "./util.js";
|
import { fmtDuration } from "./util.js";
|
||||||
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
||||||
|
|
||||||
// ─── derived state ───────────────────────────────────────────────────────────
|
// ─── derived state ───────────────────────────────────────────────────────────
|
||||||
let metaInputsState = [];
|
|
||||||
let metaUpdateRunning = false;
|
|
||||||
// Kept in sync from JobqGraph's `onUpdate` callback (see mountRebuildQueue
|
// Kept in sync from JobqGraph's `onUpdate` callback (see mountRebuildQueue
|
||||||
// below) — the component owns fetching GET /api/jobq/graph and renders
|
// below) — the component owns fetching GET /api/jobq/graph and renders
|
||||||
// the tree itself; this flat array exists only for the two things it
|
// the tree itself; this flat array exists only for the two things it
|
||||||
|
|
@ -24,133 +22,6 @@ let metaUpdateRunning = false;
|
||||||
let jobqNodes = [];
|
let jobqNodes = [];
|
||||||
let jobqGraphToken = 0;
|
let jobqGraphToken = 0;
|
||||||
|
|
||||||
function syncFromSnapshot(s) {
|
|
||||||
metaInputsState = (s.meta_inputs || []).slice();
|
|
||||||
metaUpdateRunning = !!s.meta_update_running;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── meta inputs ─────────────────────────────────────────────────────────────
|
|
||||||
function renderMetaInputs(s) {
|
|
||||||
const root = $("meta-inputs-section");
|
|
||||||
if (!root) return;
|
|
||||||
// Snapshot ticked checkboxes before wiping so a concurrent
|
|
||||||
// MetaInputsChanged doesn't silently clear a pending selection.
|
|
||||||
const checkedInputs = new Set(
|
|
||||||
Array.from(
|
|
||||||
root.querySelectorAll('input[type="checkbox"][data-meta-input]:checked'),
|
|
||||||
).map((cb) => cb.dataset.metaInput),
|
|
||||||
);
|
|
||||||
root.replaceChildren();
|
|
||||||
const inputs = s.meta_inputs || [];
|
|
||||||
if (!inputs.length) {
|
|
||||||
root.append(el("p", { class: "empty" }, "meta repo not seeded yet"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (metaUpdateRunning) {
|
|
||||||
root.append(
|
|
||||||
el(
|
|
||||||
"p",
|
|
||||||
{ class: "meta-update-running" },
|
|
||||||
"⏳ meta-update running — flake lock bump + affected agents rebuilding. " +
|
|
||||||
"watch the agent cards for per-rebuild progress.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const f = el("form", {
|
|
||||||
method: "POST",
|
|
||||||
action: "/api/meta-update",
|
|
||||||
class: "meta-inputs-form",
|
|
||||||
"data-async": "",
|
|
||||||
"data-no-refresh": "",
|
|
||||||
"data-confirm":
|
|
||||||
"update selected meta flake inputs + rebuild affected agents?",
|
|
||||||
});
|
|
||||||
const bulk = el("div", { class: "meta-inputs-bulk" });
|
|
||||||
const selAll = el(
|
|
||||||
"button",
|
|
||||||
{ type: "button", class: "meta-bulk-btn" },
|
|
||||||
"select all",
|
|
||||||
);
|
|
||||||
const selNone = el(
|
|
||||||
"button",
|
|
||||||
{ type: "button", class: "meta-bulk-btn" },
|
|
||||||
"select none",
|
|
||||||
);
|
|
||||||
bulk.append("bulk: ", selAll, " ", selNone);
|
|
||||||
f.append(bulk);
|
|
||||||
const ul = el("ul", { class: "meta-inputs" });
|
|
||||||
for (const inp of inputs) {
|
|
||||||
const depth = (inp.name.match(/\//g) || []).length;
|
|
||||||
const leaf = inp.name.slice(inp.name.lastIndexOf("/") + 1);
|
|
||||||
const li = el("li");
|
|
||||||
if (depth > 0) li.style.marginLeft = depth * 1.3 + "em";
|
|
||||||
const id = "meta-input-" + inp.name.replace(/[^a-z0-9-]/gi, "_");
|
|
||||||
const cb = el("input", {
|
|
||||||
type: "checkbox",
|
|
||||||
name: "meta_input_" + inp.name,
|
|
||||||
id,
|
|
||||||
value: inp.name,
|
|
||||||
"data-meta-input": inp.name,
|
|
||||||
});
|
|
||||||
if (checkedInputs.has(inp.name)) cb.checked = true;
|
|
||||||
const label = el("label", { for: id, title: inp.name });
|
|
||||||
label.append(cb);
|
|
||||||
if (depth > 0) label.append(el("span", { class: "meta-input-twig" }, "└ "));
|
|
||||||
label.append(
|
|
||||||
el("span", { class: "meta-input-name" }, leaf),
|
|
||||||
" ",
|
|
||||||
el("code", { class: "meta-input-rev" }, inp.rev.slice(0, 12)),
|
|
||||||
" ",
|
|
||||||
el("span", { class: "meta-input-ts" }, fmtAgo(inp.last_modified)),
|
|
||||||
);
|
|
||||||
if (inp.url) {
|
|
||||||
label.append(
|
|
||||||
" ",
|
|
||||||
el(
|
|
||||||
"span",
|
|
||||||
{ class: "meta-input-url", title: inp.url },
|
|
||||||
"· " + truncate(inp.url, 48),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
li.append(label);
|
|
||||||
ul.append(li);
|
|
||||||
}
|
|
||||||
f.append(ul);
|
|
||||||
const hidden = el("input", { type: "hidden", name: "inputs", value: "" });
|
|
||||||
f.append(hidden);
|
|
||||||
const btn = el(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "submit",
|
|
||||||
class: "btn btn-meta-update",
|
|
||||||
disabled: "",
|
|
||||||
},
|
|
||||||
metaUpdateRunning ? "⏳ UPD4T1NG…" : "◆ UPD4TE & R3BU1LD",
|
|
||||||
);
|
|
||||||
f.append(btn);
|
|
||||||
function refreshDisabled() {
|
|
||||||
const any = f.querySelectorAll("input[data-meta-input]:checked").length > 0;
|
|
||||||
if (any && !metaUpdateRunning) btn.removeAttribute("disabled");
|
|
||||||
else btn.setAttribute("disabled", "");
|
|
||||||
}
|
|
||||||
f.addEventListener("change", refreshDisabled);
|
|
||||||
function setAllChecked(val) {
|
|
||||||
for (const b of f.querySelectorAll("input[data-meta-input]"))
|
|
||||||
b.checked = val;
|
|
||||||
refreshDisabled();
|
|
||||||
}
|
|
||||||
selAll.addEventListener("click", () => setAllChecked(true));
|
|
||||||
selNone.addEventListener("click", () => setAllChecked(false));
|
|
||||||
f.addEventListener("submit", () => {
|
|
||||||
const selected = Array.from(
|
|
||||||
f.querySelectorAll("input[data-meta-input]:checked"),
|
|
||||||
).map((b) => b.dataset.metaInput);
|
|
||||||
hidden.value = selected.join(",");
|
|
||||||
});
|
|
||||||
root.append(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
||||||
// R3BU1LD QU3U3 is `JobqGraph` (Preact, rendered via plain `render(h(...))`
|
// R3BU1LD QU3U3 is `JobqGraph` (Preact, rendered via plain `render(h(...))`
|
||||||
// — no JSX pragma needed for either call, no mount wrapper: `render` is
|
// — no JSX pragma needed for either call, no mount wrapper: `render` is
|
||||||
|
|
@ -594,20 +465,13 @@ async function fetchBuild() {
|
||||||
|
|
||||||
/** Mount the rebuild queue and wire the build-log refresh button. Called
|
/** Mount the rebuild queue and wire the build-log refresh button. Called
|
||||||
* once, after the tab strip exists. The queue self-fetches on mount
|
* once, after the tab strip exists. The queue self-fetches on mount
|
||||||
* (JobqGraph's own effect), unlike the meta inputs, which wait for the
|
* (JobqGraph's own effect). */
|
||||||
* snapshot. */
|
|
||||||
export function initBuilds() {
|
export function initBuilds() {
|
||||||
const refresh = $("build-refresh");
|
const refresh = $("build-refresh");
|
||||||
if (refresh) refresh.addEventListener("click", fetchBuild);
|
if (refresh) refresh.addEventListener("click", fetchBuild);
|
||||||
mountRebuildQueue();
|
mountRebuildQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The meta inputs, from an `/api/state` snapshot. */
|
|
||||||
export function syncBuildsFromSnapshot(s) {
|
|
||||||
syncFromSnapshot(s);
|
|
||||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A tab was shown: BUILD L0GS loads its list the first time. */
|
/** A tab was shown: BUILD L0GS loads its list the first time. */
|
||||||
export function onBuildsTabShown(tab) {
|
export function onBuildsTabShown(tab) {
|
||||||
if (tab === "buildlogs" && !buildLogsLoaded) {
|
if (tab === "buildlogs" && !buildLogsLoaded) {
|
||||||
|
|
@ -629,13 +493,3 @@ export function onRebuildQueueChanged() {
|
||||||
buildRefreshTimer = setTimeout(fetchBuild, 2000);
|
buildRefreshTimer = setTimeout(fetchBuild, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onMetaInputsChanged(ev) {
|
|
||||||
metaInputsState = (ev.inputs || []).slice();
|
|
||||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onMetaUpdateRunning(ev) {
|
|
||||||
metaUpdateRunning = !!ev.running;
|
|
||||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
it defines the CSS custom properties, typography, and shared
|
it defines the CSS custom properties, typography, and shared
|
||||||
component rules that this file depends on. */
|
component rules that this file depends on. */
|
||||||
|
|
||||||
/* SYST3M panel rules (rebuild queue / meta inputs / kept state)
|
/* SYST3M panel rules (rebuild queue / kept state)
|
||||||
live in system-sections.css. */
|
live in system-sections.css. */
|
||||||
@import "./system-sections.css";
|
@import "./system-sections.css";
|
||||||
@import "@hive/shared/jobq-rollup.css";
|
@import "@hive/shared/jobq-rollup.css";
|
||||||
|
|
@ -674,7 +674,7 @@ body.dashboard-shell.has-selection {
|
||||||
shared `.hive-stats-table` moved to common.css. */
|
shared `.hive-stats-table` moved to common.css. */
|
||||||
|
|
||||||
/* ─── build tabs (builds.js) ────────────────────────────────────────
|
/* ─── build tabs (builds.js) ────────────────────────────────────────
|
||||||
R3BU1LD QU3U3 and M3T4 1NPUTS are styled by system-sections.css and
|
R3BU1LD QU3U3 is styled by system-sections.css and
|
||||||
jobq-graph.css; the BUILD L0GS list rules are below. */
|
jobq-graph.css; the BUILD L0GS list rules are below. */
|
||||||
|
|
||||||
.logs-toolbar {
|
.logs-toolbar {
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,9 @@
|
||||||
<span class="tab-label">◆ 4G3NTS ◆</span>
|
<span class="tab-label">◆ 4G3NTS ◆</span>
|
||||||
<span class="tab-count" id="tab-count-agents" hidden></span>
|
<span class="tab-count" id="tab-count-agents" hidden></span>
|
||||||
</a>
|
</a>
|
||||||
<!-- The build lifecycle is three tabs here (builds.js): the rebuild
|
<!-- The build lifecycle is two tabs here (builds.js): the rebuild
|
||||||
queue with its live log, the meta flake inputs, and the build log
|
queue with its live log, and the build log history. Kept state
|
||||||
history. Kept state (kept.js) is the fourth. -->
|
(kept.js) is the third. -->
|
||||||
<a
|
<a
|
||||||
class="tab"
|
class="tab"
|
||||||
id="tab-rebuild"
|
id="tab-rebuild"
|
||||||
|
|
@ -47,16 +47,7 @@
|
||||||
<span class="tab-label">◆ R3BU1LD QU3U3 ◆</span>
|
<span class="tab-label">◆ R3BU1LD QU3U3 ◆</span>
|
||||||
<span class="tab-count" id="tab-count-rebuild" hidden></span>
|
<span class="tab-count" id="tab-count-rebuild" hidden></span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
class="tab"
|
|
||||||
id="tab-meta"
|
|
||||||
href="#meta"
|
|
||||||
role="tab"
|
|
||||||
aria-controls="tab-pane-meta"
|
|
||||||
data-tab="meta"
|
|
||||||
>
|
|
||||||
<span class="tab-label">◆ M3T4 1NPUTS ◆</span>
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
class="tab"
|
class="tab"
|
||||||
id="tab-buildlogs"
|
id="tab-buildlogs"
|
||||||
|
|
@ -126,8 +117,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
|
<!-- R3BU1LD QU3U3: pending + running rebuilds and first-spawns. Rendered from GET /api/jobq/graph by the
|
||||||
first-spawns. Rendered from GET /api/jobq/graph by the
|
|
||||||
`JobqGraph` Preact component (@hive/shared/jobq-graph.js);
|
`JobqGraph` Preact component (@hive/shared/jobq-graph.js);
|
||||||
`rebuild_queue_changed` over /api/dashboard/stream is the
|
`rebuild_queue_changed` over /api/dashboard/stream is the
|
||||||
refresh trigger and carries no payload of its own. -->
|
refresh trigger and carries no payload of its own. -->
|
||||||
|
|
@ -140,9 +130,9 @@
|
||||||
aria-labelledby="tab-rebuild"
|
aria-labelledby="tab-rebuild"
|
||||||
>
|
>
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
pending + running rebuilds, meta-updates, and first-spawns. one runs
|
pending + running rebuilds and first-spawns. one runs at a time.
|
||||||
at a time; meta-update cascades nest under their parent. dedup:
|
dedup: re-enqueueing a still-queued op collapses into the existing
|
||||||
re-enqueueing a still-queued op collapses into the existing entry.
|
entry.
|
||||||
</p>
|
</p>
|
||||||
<div id="rebuild-queue-section">
|
<div id="rebuild-queue-section">
|
||||||
<p class="meta">loading…</p>
|
<p class="meta">loading…</p>
|
||||||
|
|
@ -154,26 +144,6 @@
|
||||||
<div id="rebuild-live-log" class="rebuild-live-log" hidden></div>
|
<div id="rebuild-live-log" class="rebuild-live-log" hidden></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- M3T4 1NPUTS: select inputs to nix flake update in /meta/. -->
|
|
||||||
<section
|
|
||||||
class="tab-pane"
|
|
||||||
id="tab-pane-meta"
|
|
||||||
data-tab-pane="meta"
|
|
||||||
hidden
|
|
||||||
role="tabpanel"
|
|
||||||
aria-labelledby="tab-meta"
|
|
||||||
>
|
|
||||||
<p class="meta">
|
|
||||||
select inputs to <code>nix flake update</code> in
|
|
||||||
<code>/meta/</code>. selected agents rebuild in sequence after the
|
|
||||||
lock bump; the submitting agent learns each outcome via the usual
|
|
||||||
<code>rebuilt</code> system event.
|
|
||||||
</p>
|
|
||||||
<div id="meta-inputs-section">
|
|
||||||
<p class="meta">loading…</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- BUILD L0GS: all-agents build log history. Click a row to expand
|
<!-- BUILD L0GS: all-agents build log history. Click a row to expand
|
||||||
stdout + stderr. Live builds stream in real time. Lazy-loaded the
|
stdout + stderr. Live builds stream in real time. Lazy-loaded the
|
||||||
first time the tab is shown; auto-refreshes when
|
first time the tab is shown; auto-refreshes when
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,7 @@
|
||||||
/* ─── SYST3M/BU1LDS section panels — used by the dashboard ───
|
/* ─── SYST3M/BU1LDS section panels — used by the dashboard ───
|
||||||
Rebuild queue, meta inputs and kept-state panels.
|
Rebuild queue and kept-state panels.
|
||||||
Imported by dashboard.css. */
|
Imported by dashboard.css. */
|
||||||
|
|
||||||
/* ─── meta inputs panel ────────────────────────────────────────────── */
|
|
||||||
.meta-inputs {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 0.8em;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.2em;
|
|
||||||
}
|
|
||||||
.meta-inputs li {
|
|
||||||
padding: 0.25em 0.6em;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: color-mix(in srgb, var(--bg-elev) 60%, transparent);
|
|
||||||
}
|
|
||||||
.meta-inputs label {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.5em;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
.meta-input-name {
|
|
||||||
color: var(--amber);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.meta-input-rev {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.meta-input-ts {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
.meta-input-url {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.85em;
|
|
||||||
margin-left: auto;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.meta-inputs-bulk {
|
|
||||||
margin: 0 0 0.5em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.meta-bulk-btn {
|
|
||||||
font: inherit;
|
|
||||||
font-size: 1em;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--purple-dim);
|
|
||||||
color: var(--cyan);
|
|
||||||
padding: 0.1em 0.6em;
|
|
||||||
margin-right: 0.2em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.meta-bulk-btn:hover {
|
|
||||||
border-color: var(--cyan);
|
|
||||||
text-shadow: 0 0 6px currentColor;
|
|
||||||
}
|
|
||||||
.meta-input-twig {
|
|
||||||
color: var(--purple-dim);
|
|
||||||
margin-right: 0.1em;
|
|
||||||
}
|
|
||||||
.btn-meta-update {
|
|
||||||
background: color-mix(in srgb, var(--purple) 12%, transparent);
|
|
||||||
border: 1px solid var(--purple);
|
|
||||||
color: var(--purple);
|
|
||||||
text-shadow: 0 0 4px currentColor;
|
|
||||||
padding: 0.3em 1em;
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.85em;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
box-shadow 0.15s ease,
|
|
||||||
background 0.15s ease;
|
|
||||||
}
|
|
||||||
.btn-meta-update:hover:not([disabled]) {
|
|
||||||
background: color-mix(in srgb, var(--purple) 22%, transparent);
|
|
||||||
box-shadow: 0 0 10px -2px currentColor;
|
|
||||||
}
|
|
||||||
.btn-meta-update[disabled] {
|
|
||||||
opacity: 0.35;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.meta-update-running {
|
|
||||||
margin: 0 0 0.7em;
|
|
||||||
padding: 0.4em 0.7em;
|
|
||||||
border: 1px solid var(--purple);
|
|
||||||
background: color-mix(in srgb, var(--purple) 12%, transparent);
|
|
||||||
color: var(--purple);
|
|
||||||
font-size: 0.85em;
|
|
||||||
animation: badge-pulse 1.6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── rebuild queue panel ──────────────────────────────────────────────── */
|
/* ─── rebuild queue panel ──────────────────────────────────────────────── */
|
||||||
.rebuild-queue {
|
.rebuild-queue {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,8 @@ import {
|
||||||
import {
|
import {
|
||||||
initBuilds,
|
initBuilds,
|
||||||
onBuildsTabShown,
|
onBuildsTabShown,
|
||||||
onMetaInputsChanged,
|
|
||||||
onMetaUpdateRunning,
|
|
||||||
onRebuildQueueChanged,
|
onRebuildQueueChanged,
|
||||||
rebuildCount,
|
rebuildCount,
|
||||||
syncBuildsFromSnapshot,
|
|
||||||
} from "./builds.js";
|
} from "./builds.js";
|
||||||
import {
|
import {
|
||||||
applyTombstonesChanged,
|
applyTombstonesChanged,
|
||||||
|
|
@ -160,7 +157,7 @@ import { renderUserMenu } from "./user-menu.js";
|
||||||
// initJobqRollup mounts once and is a no-op on later calls; the
|
// initJobqRollup mounts once and is a no-op on later calls; the
|
||||||
// mount's own effect handles the actual fetch.
|
// mount's own effect handles the actual fetch.
|
||||||
initJobqRollup();
|
initJobqRollup();
|
||||||
syncBuildsFromSnapshot(s);
|
|
||||||
syncKeptFromSnapshot(s);
|
syncKeptFromSnapshot(s);
|
||||||
renderContainers(s);
|
renderContainers(s);
|
||||||
renderKept();
|
renderKept();
|
||||||
|
|
@ -216,8 +213,7 @@ import { renderUserMenu } from "./user-menu.js";
|
||||||
applyRebuildQueueChanged();
|
applyRebuildQueueChanged();
|
||||||
onRebuildQueueChanged();
|
onRebuildQueueChanged();
|
||||||
},
|
},
|
||||||
meta_inputs_changed: onMetaInputsChanged,
|
|
||||||
meta_update_running: onMetaUpdateRunning,
|
|
||||||
tombstones_changed: applyTombstonesChanged,
|
tombstones_changed: applyTombstonesChanged,
|
||||||
};
|
};
|
||||||
(function bindDashboardStream() {
|
(function bindDashboardStream() {
|
||||||
|
|
@ -229,12 +225,12 @@ import { renderUserMenu } from "./user-menu.js";
|
||||||
// fallback when SharedWorker isn't supported.
|
// fallback when SharedWorker isn't supported.
|
||||||
//
|
//
|
||||||
// kinds= matches MUTATION_HANDLERS verbatim, so the stream carries
|
// kinds= matches MUTATION_HANDLERS verbatim, so the stream carries
|
||||||
// only the 8 wire kinds this page acts on.
|
// only the 6 wire kinds this page acts on.
|
||||||
const es = openStream(
|
const es = openStream(
|
||||||
"/api/dashboard/stream?kinds=" +
|
"/api/dashboard/stream?kinds=" +
|
||||||
"transient_set,transient_cleared," +
|
"transient_set,transient_cleared," +
|
||||||
"container_state_changed,container_removed,rebuild_queue_changed," +
|
"container_state_changed,container_removed,rebuild_queue_changed," +
|
||||||
"meta_inputs_changed,meta_update_running,tombstones_changed",
|
"tombstones_changed",
|
||||||
);
|
);
|
||||||
es.onmessage = (e) => {
|
es.onmessage = (e) => {
|
||||||
let ev;
|
let ev;
|
||||||
|
|
@ -266,7 +262,7 @@ import { renderUserMenu } from "./user-menu.js";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ─── tab routing ───────────────────────────────────────────────────────
|
// ─── tab routing ───────────────────────────────────────────────────────
|
||||||
// Hash-based: `#agents` / `#rebuild` / `#meta` / `#buildlogs` / `#kept`
|
// Hash-based: `#agents` / `#rebuild` / `#buildlogs` / `#kept`
|
||||||
// activate the matching pane on the dashboard. Empty
|
// activate the matching pane on the dashboard. Empty
|
||||||
// hash defaults to 4G3NTS. ST4TS is NOT a tab — it is a separate page
|
// hash defaults to 4G3NTS. ST4TS is NOT a tab — it is a separate page
|
||||||
// (`/stats.html`) reached from the pages menu (links-menu.js). Tab routing
|
// (`/stats.html`) reached from the pages menu (links-menu.js). Tab routing
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ export function paintAtomic(liveRoot, build) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Epoch seconds from an API timestamp. The hive-c0re dashboard API
|
// Epoch seconds from an API timestamp. The hive-c0re dashboard API
|
||||||
// ships timestamps as RFC 3339 strings; a few feeds (rebuild queue,
|
// ships timestamps as RFC 3339 strings; a few feeds (rebuild queue and
|
||||||
// meta inputs, turn stats) still carry unix-second numbers, so
|
// turn stats) still carry unix-second numbers, so
|
||||||
// numbers pass through unchanged.
|
// numbers pass through unchanged.
|
||||||
export function epochSec(ts) {
|
export function epochSec(ts) {
|
||||||
return typeof ts === "number" ? ts : Math.floor(Date.parse(ts) / 1000);
|
return typeof ts === "number" ? ts : Math.floor(Date.parse(ts) / 1000);
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function asyncBtn(btn, fn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page-level submit interceptor for `data-async` forms — the pattern every
|
// Page-level submit interceptor for `data-async` forms — the pattern every
|
||||||
// dashboard/agent action button uses (meta-update, spawn, cancel, purge,
|
// dashboard/agent action button uses (spawn, cancel, purge,
|
||||||
// rebuild, …). Without this a `data-async` form POSTs natively and the
|
// rebuild, …). Without this a `data-async` form POSTs natively and the
|
||||||
// browser navigates to the bare `ok` response page. Each page that renders
|
// browser navigates to the bare `ok` response page. Each page that renders
|
||||||
// such forms must call this once at boot. `onSuccess` runs after a
|
// such forms must call this once at boot. `onSuccess` runs after a
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue