Commit Graph
8 Commits
Author SHA1 Message Date
grenadeandClaude Opus 5.5 ec615cac52 ci: gate every branch, and make a "validate" dispatch ship nothing
ci / gate (push) Successful in 4m24s
deploy / validate (push) Skipped
deploy / build (push) Successful in 8m27s
deploy / deploy-web (push) Successful in 7s
deploy / deploy-api (push) Successful in 17s
deploy.yaml was the only workflow and ran only on main, so a pushed branch
started no run at all: the first time CI saw a change was the push that
deployed it. And its `mode: validate` input, offered as "check only", was read
by no job — a validate dispatch built and deployed whatever ref it ran on.

ci.yaml now runs on every branch but main, on the `rust` runner, with no deploy
job, no secrets and no artifact. The gate itself moves into a local composite
action, .gitea/actions/gate, which both ci.yaml and deploy.yaml's `build` job
use, so the gate a branch passes is the gate main runs rather than a copy that
can drift. Its commands are CLAUDE.md's "Run the gate CI runs" verbatim. One
deliberate change on the way: the drift check is `git status --porcelain`, as
CLAUDE.md and #28 state it, not `git diff --quiet`, which cannot see the new
untracked file a new DTO generates.

Both deploy jobs now require `github.ref == 'refs/heads/main'` and
`inputs.mode != 'validate'` (empty on a push, so the push path and its
"what changed" gating are unchanged). A validate dispatch on main runs the
build and gate plus a new `validate` job: the read-only checks from the end of
deploy-api and deploy-web — API health from bob, reachability from the edge,
the chain list through the vhost — against what is already deployed. Those
checks are copied rather than shared, because the deploy jobs run from the
build artifact without a checkout and restructuring them is a change only a
production deploy would test; the comment says they must change together.

Closes #28

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-03 19:12:17 +03:00
grenadeandClaude Opus 5 d12299f242 ci: gate the API deploy on what the API is running, not on the last push
deploy / build (push) Successful in 8m3s
deploy / deploy-web (push) Successful in 7s
deploy / deploy-api (push) Successful in 20s
The site is currently serving a frontend that asks for `window=3600` from an API
that only knows `six_hours`, and every step behaved as designed:

  run 51  ef367d5  rust + web   BUILD FAILED — nothing deployed
  run 52  2594190  web only     web deployed, deploy-api skipped
  run 53  49f50bd  docs only    nothing deployed

Run 52 diffed against `github.event.before`, which is run 51's commit — a commit
whose Rust half never shipped. It correctly saw only `web/src/lib/routes.ts` and
correctly skipped the API. The frontend went out alone.

The gate was asking the wrong question. "What changed since the previous push"
is only the same as "what changed since what is deployed" while every push
deploys, and a failed run breaks that quietly — the workflow's own comment warns
about "a binary on bob that does not match this commit, which nothing would
report", and this is that case arriving by a route the path list cannot see.

So ask the API. `/v1/healthz` now carries the commit it was built from, stamped
via `GIT_SHA` at build time, and the gate diffs from there. `build.rs` exists
only to make cargo notice the value changed, since `option_env!` is read at
compile time and a warm cache would otherwise keep a stale stamp.

Best effort throughout: an unreachable API, a binary too old to carry a stamp,
or a commit this clone does not have all fall back to `github.event.before`,
which is exactly today's behaviour. The gate only ever widens — a false positive
costs a slow deploy, and that trade is already written down beside it.

This commit touches `.gitea/` and `crates/`, so it deploys the API and unbreaks
the site as it lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-10 14:56:07 +03:00
grenadeandClaude Opus 5 1410d3dda7 ci: skip the Rust build and API deploy on frontend-only pushes
deploy / build (push) Successful in 7m25s
deploy / deploy-web (push) Successful in 6s
deploy / deploy-api (push) Successful in 15s
The musl release build plus the gate is most of the wall clock, and a commit
that touches only `web/` cannot change a byte of it. A `what changed` step
diffs against the previous head and sets one output; the gate, the build, the
ts-rs drift check and the whole `deploy-api` job hang off it. Not
`on.push.paths`, which would skip the entire workflow — the site still has to
build and ship.

Anything unrecognised counts as Rust. A false positive costs a slow deploy; a
false negative leaves a binary on bob that does not match the commit, and
nothing would report it. Two entries in the path list are less obvious than
they look: `asset/`, because deploy-api ships the systemd units, the firewalld
service and the rendered config from it; and `web/src/api/generated/`, because
the drift gate only runs once `cargo test` has regenerated those files, so a
hand-edit of them must not be able to arrive labelled frontend-only — that is
exactly the change the gate exists to catch.

Checked against this session's five commits: the two pure-UI ones classify as
frontend, the three touching crates or .sqlx classify as Rust, and a
hand-edited generated type classifies as Rust.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-09 17:23:31 +03:00
grenadeandClaude Opus 5 337d1ef516 ci: format the frontend with prettier and gate it
deploy / build (push) Successful in 6m44s
deploy / deploy-web (push) Successful in 8s
deploy / deploy-api (push) Successful in 15s
`cargo fmt --check` has gated the Rust half since the first commit; nothing
gated the TypeScript, so 25 of 31 files under web/src had drifted from the
.prettierrc that was already sitting in the repo. `pnpm format:check` is now the
counterpart, in the same step as `pnpm lint`.

The reformat is whitespace only — the emitted bundle hashes identically before
and after.

web/.prettierignore excludes src/api/generated. Those files are written by ts-rs
from the entities crate and CI already fails on any diff in them after `cargo
test`; formatting them here would be reverted by the next regeneration and turn
that drift gate red on every subsequent build. Verified by regenerating and
confirming the tree stays clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-08 15:01:28 +03:00
grenadeandClaude Opus 5 3db86cfc66 fix(ci): health-probe the address the API actually binds, not loopback
deploy / build (push) Successful in 6m51s
deploy / deploy-web (push) Successful in 5s
deploy / deploy-api (push) Successful in 15s
The probe curled 127.0.0.1:25864 and got connection-refused against a daemon
that was running perfectly — the journal it captured on failure showed it
serving. The API binds the host's *mesh* address by design: the fleet has one
firewalld default zone, so a wildcard bind plus the named service would publish
it on every address the host carries, and the edge proxy is at another site, so
loopback-only would not serve the site at all.

`hostname -f` resolves to that mesh address, so the probe needs no value carried
between steps. Verified against the running service: loopback refuses, the FQDN
returns 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
2026-09-04 13:48:14 +03:00
grenadeandClaude Opus 5 151a30ae1a ci: run the build on the rust runner, drop corepack, pin the site check to loopback
deploy / build (push) Successful in 7m0s
deploy / deploy-web (push) Successful in 4s
deploy / deploy-api (push) Failing after 1m26s
Three things that would each have failed the run seconds in
(architecture/gitea-runners.md §4):

- `runs-on: fedora-43` for a cargo build is `cargo: command not found` — the
  fedora images are node/shell runners with no Rust. `rust` is built on
  `runner-fedora-44`, so it carries node + pnpm as well and one job still builds
  both halves. musl stays: that runner is Fedora 44 and the targets are 43.
- `corepack enable` is not bundled by any image and pnpm is already on PATH.
- The site check curled the public name *from the edge proxy*, where it resolves
  to the site WAN address and dead-ends on the OPNsense LAN interface. Pinned to
  loopback, which still exercises the stream router, SNI, the vhost, the
  cross-site hop and the API.

Deploy jobs stay on `infra`: §4 is about tooling, not routing, and these targets
are mesh-only .internal names — same reasoning as lair/quantus and lair/mail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
2026-09-04 13:37:31 +03:00
grenadeandClaude Opus 5 5f3aac2a13 feat(chains): discover every Quantus chain from telemetry; track the ones we can reach
deploy / build (push) Failing after 5s
deploy / deploy-api (push) Has been skipped
deploy / deploy-web (push) Has been skipped
The chain list is no longer config. substrate-telemetry announces every chain it
knows — with live node counts — before and independent of any subscription, so
one feed connection that never subscribes is a complete, self-maintaining index.
A chain that launches tomorrow appears in the nav on its own.

Config now contributes only RPC endpoints. Authorship lives in the `pow_` digest
of a block *header*, and telemetry publishes hashes and heights but never
headers — so a chain with no endpoint is listed with its node count and is not
navigable, and the nav says why rather than offering an empty board. The two
sets join on genesis hash, never on name.

Two chains are tracked in full: Planck (our own node on bob) and Heisenberg
(a1-heisenberg.quantus.cat, found via Quantus-Network/quantus-apps). Both decode
identically — same digest shape, same QPoW runtime API — which is the proof that
adding a chain is one [[chains]] entry and no new code. Quantus Staging Mainnet
and Quantus Dirac Testnet are listed and disabled: no endpoint exists under the
a1-/a2- pattern and none resolves under any plausible name.

Nav is ordered by node count descending, ties broken on id so it cannot reshuffle
between refreshes.

Telemetry now also yields chain-wide best height, best finalized height, average
block time and the client-version histogram (feed codes 1, 2, 12 and 22).

Infra moves to a single site: oolon fronts blackbeard.observer, reverse-proxying
cross-site over the mesh to the API beside the node on bob. That hop is the one
the loopback health probe cannot see, so the deploy now checks it explicitly —
a firewalld service scoped to bob's own /16 would leave a live site with a dead
/v1 and nothing would fail. infra-setup gains `dns` and `cert` roles that run on
oolon with the credential already there: the Cloudflare token is never copied off
the proxy, and it refuses to repoint an apex record that already exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
2026-09-04 13:04:11 +03:00
grenadeandClaude Opus 5 110fbc3631 feat: blackbeard.observer — live Quantus mining leaderboard
Cargo workspace plus a Vite frontend, following ~/git/architecture/generic.md.

Every block header carries its author's wormhole reward preimage in a `pow_`
PreRuntime digest, so authorship for the whole network is derivable from headers
alone — no indexer, no registration, no way for a miner to be left out. That
decoding, the hashrate maths and the telemetry name attribution live in
blackbeard-core with no I/O at all, so the parts that are easy to get subtly
wrong are exercised by unit tests rather than only against a live chain.

The browser holds one WebSocket: snapshot on subscribe, deltas thereafter. The
head stream is itself a push (chain_subscribeNewHeads), so a block reaches the
page the moment the node imports it. Messages are serialised once per broadcast,
and leaderboards are recomputed only for windows a socket is actually watching.
No RxJS — useSyncExternalStore is React's own contract for this.

Verified against the live Planck testnet: 12/12 headers decoded, telemetry names
attributed (quanpool-planck, baba-gorchitsa, …), warm start restoring 84 blocks
and 5 held names across a restart.

Three findings worth recording, all in CLAUDE.md:

- substrate-telemetry sends its JSON in *binary* frames. A text-only client
  connects, subscribes, reports healthy and receives nothing at all — and a
  Python probe hides it, because json.loads accepts bytes.
- Difficulty is a little-endian U512; decoding it big-endian gives a number
  wrong by ~10^150 that still renders fine.
- Planck's real block interval is ~13-15s against a 6s target with enormous
  variance, so a measured interval needs 20 tip samples before it is publishable.

Deploy assets, the Gitea Actions workflow and script/infra-setup.sh are included;
port 25864 is registered in architecture/port-allocations.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
2026-09-04 12:33:54 +03:00