Commit graph

1,874 commits

Author SHA1 Message Date
atlas
f633abbdc4 refactor(hive-forge): share latest-per-reviewer logic between pr-status and pr-merge
pr-status and pr-merge both computed 'latest non-comment review verdict per
reviewer' independently (identical oldest-first, COMMENT/PENDING-skipping,
supersede-by-later loop). Extract it to a shared verbs::latest_reviews helper
so the verdict semantics live in one place and can't drift between the
health view and the pre-merge changes-requested gate. Pure dedup, no
behaviour change.
2026-06-15 11:44:19 +02:00
damocles
f6e1651dff feat(#1671): reject matrix send when room has unread messages 2026-06-15 11:38:25 +02:00
damocles
d89666baa9 docs(ci): document jobTimeout, fix forge.ci option path 2026-06-15 11:37:50 +02:00
atlas
bfeb23ce06 fix(forge): pre-create Actions artifact storage dir so uploads persist
Forgejo's local Actions-artifact storage defaults to
/var/lib/forgejo/data/actions_artifacts but Forgejo does not pre-create it.
The artifact endpoint ingests the chunked upload, then the merge-chunks step
fails with 'lstat .../actions_artifacts/tmpNNN: no such file or directory',
so every upload-artifact step dies after a successful build. Pre-create the
dir (forgejo-owned) via tmpfiles. actions.ENABLED already registers the
endpoints; this gives them somewhere to write.
2026-06-15 11:37:33 +02:00
atlas
9d8367bb24 feat(hive-forge): add pr-merge verb (#1670)
Adds `hive-forge pr-merge <n> [--method merge|rebase] [--keep-branch]
[--force]` wrapping POST /repos/{owner}/{repo}/pulls/{n}/merge, so agents on
the peer-review-and-merge workflow have a CLI path instead of the raw API.

- Methods: merge (default) | rebase. Squash is intentionally not offered.
- Deletes the head branch after merge unless --keep-branch.
- Safe by default: refuses unless the PR is mergeable, CI is not red, and no
  review's current verdict requests changes (latest-per-reviewer wins, so a
  later approval clears an earlier request-changes). --force overrides and
  also sets Forgejo's force_merge.
- New client helper post_no_content for the 200-empty-body merge response.
2026-06-15 11:36:17 +02:00
iris
06c5d68071 refactor(dashboard): move SYST3M section CSS into shared system-sections.css
The C0R3 page (/core.html) pulled the four SYST3M panel styles (rebuild
queue, meta inputs, container load) by @importing the whole ~39kb
dashboard.css, dragging the entire dashboard stylesheet into the C0R3
CSS bundle.

Extract those section-internal rules into a new system-sections.css and
@import it from both core.css and dashboard.css. core.html now loads only
the small shared file (its CSS bundle drops from ~40kb to 5.3kb) while
the dashboard keeps the rules — a couple are still referenced from
dashboard JS (the schedules view uses .rqe-source*). The container-load
table's .hive-stats-table already lives in common.css, linked directly
by core.html, so it is unaffected.

Behaviour-preserving: identical rules, same custom properties; the moved
selectors are uniquely named to these panels and not redefined
elsewhere, so the cascade is unchanged. Verified the built bundles:
core.css carries the section rules and no dashboard chrome; dashboard.css
still carries them via the import.
2026-06-15 09:54:55 +02:00
damocles
ca84cf427b fix(#1666): wake on pending matrix invites via post-sync sweep 2026-06-14 23:37:45 +02:00
atlas
c7ffe9336b docs(hive-forge): add # Errors to put_no_content (argus review) 2026-06-14 22:09:31 +02:00
atlas
3790faf318 feat(hive-forge): add repo-create + repo-add-collaborator verbs 2026-06-14 22:09:31 +02:00
damocles
41c4682f7a refactor(#1450): lift shared harness-path resolution into hive-sh4re 2026-06-14 21:34:58 +02:00
damocles
789ecd86f6 fix(#1661): show default-perms agents with effective values in perms tab 2026-06-14 21:20:03 +02:00
iris
24cbafc64a feat(dashboard): live-append the LOGS AUDIT tab via audit_entry_added
Completes the audit log end-to-end. The backend now emits an
audit_entry_added event on /dashboard/stream per privileged action (the
new row flattened at top level). logs.js already subscribes to that stream
for the BUILD tab; add a branch that prepends the new row to the AUDIT
table's cached list (de-duped by id against the cold fetch), bumps the
'latest N of M' total, and re-renders while the AUDIT tab is in view — so
a restart shows up without a manual refresh. Docs updated to match.
2026-06-14 20:23:54 +02:00
atlas
f21a812f5f audit-log: #[must_use] on record() per review
argus: record() is pub and returns Option<AuditEntry>; the hive rule
requires #[must_use] on public fns returning a non-trivial value. Call
sites already consume the return (handle_restart_infra via if-let, tests
via let _).
2026-06-13 17:17:00 +02:00
atlas
629f08a113 feat(audit): live-append event for the dashboard audit view
Follow-up to the audit-log backend + surface. Emits a DashboardEvent on
each agent-initiated privileged action so the audit view live-appends off
/dashboard/stream instead of polling.

- new DashboardEvent::AuditEntryAdded { seq, <flattened AuditEntry> } —
  serde tag `audit_entry_added`; the AuditEntry fields flatten to the top
  level so the wire shape matches one /api/audit-log `entries` row exactly.
- Coordinator::emit_audit_entry helper (stamps seq like the others).
- audit_log::record now returns the canonical inserted AuditEntry (id + ts
  assigned) so the streamed event is the same row that was stored — no
  drift. Best-effort unchanged (None on a sqlite blip).
- handle_restart_infra records + emits for every attempt (ok/err/denied),
  threading the coordinator through.

Tests: kind_tag round-trip now covers the new variant; added a flatten
test pinning the top-level wire shape (kind/seq/id/…/detail, no nesting).

Pairs with iris's audit view (the /dashboard/stream listener half).
2026-06-13 17:17:00 +02:00
damocles
9bd51a7440 feat(#1653): optional name param for bash run tool 2026-06-13 17:16:36 +02:00
iris
0b6a52e81f docs(web-ui): document the LOGS AUDIT sub-tab + /api/audit-log
The LOGS page gained a fourth sub-tab (AUDIT) surfacing the
agent-initiated privileged-action audit trail. Document the sub-tab
(lazy fetch, filterable table, outcome/denied badge colouring, the
latest-N-of-total clamp, 30s relative-time ticker) alongside BUILD /
AGENT / SYSTEM, add #audit to the hash-routing list, and add the
GET /api/audit-log endpoint to the API reference.
2026-06-13 17:16:06 +02:00
damocles
e08122a206 fix(#1643): filter destroyed-agent targets from dashboard schedule view 2026-06-13 15:58:44 +02:00
iris
8b991b2cc5 feat(dashboard): AUD1T — privileged-actions audit trail as a LOGS sub-tab
Adds an AUDIT sub-tab to /logs.html (alongside BUILD / AGENT / SYSTEM),
consuming GET /api/audit-log ({ entries, total }). A read-only filterable
table: when / agent / action / target / outcome / detail, newest-first,
with a 'latest 500 of N' header from total and a client-side substring
filter. Outcome badges colour ok green / err red, with an err whose detail
starts 'denied:' rendered amber + labelled 'denied' (capability refusal
reads distinct from an execution failure). Lazy-fetched on tab show (like
SYSTEM); a 30s ticker keeps the relative timestamps honest.

The audit_log store + endpoint landed in the prior audit-log backend work;
this is the operator-visible surface for it. Resolves #1647.
2026-06-13 15:31:50 +02:00
atlas
79fa4f97b7 audit-log: add # Errors doc sections per review
argus review: the four public Result-returning methods (open, list_recent,
count_total, vacuum) need # Errors sections per the hive Rust doc rules;
open had no doc comment. Added all four.
2026-06-13 14:00:02 +02:00
atlas
962259a8d6 audit-log: expose total count in /api/audit-log response
Per iris's dashboard-side ask: return { entries, total } instead of a
bare array so the UI can show 'latest 500 of N' rather than silently
capping at the clamp. Adds AuditLog::count_total().
2026-06-13 13:49:54 +02:00
atlas
a452a92fb1 feat(audit): persistent audit log of agent-initiated privileged actions
Adds a durable, operator-visible audit trail of privileged operations
hive-c0re performs on behalf of an agent — the ones that cross the
agent/operator trust boundary. First entry: infra-container restarts via
the infra_admin-gated `restart` tool, which until now were recorded only
as a hive-priv journal trace.

Backend:
- new `audit_log` module: sqlite-backed store (audit_log.sqlite, same dir
  as build_logs) with schema (ts/agent/action/target/outcome/detail),
  best-effort `record`, `list_recent` (clamped 500), 90-day `vacuum`, and
  a process-singleton handle mirroring build_logs.
- Coordinator opens + installs the handle; main spawns the hourly vacuum.
- agent_server::handle_restart_infra records every attempt (ok, error, and
  capability-denied) via the global handle — best-effort, never fails the
  underlying action.
- dashboard: `GET /api/audit-log` returns recent entries as JSON.

Scope is deliberately agent-initiated privileged actions only (not every
PrivRequest — token writes + nspawn edits are constant lifecycle noise).
Extensible: future agent-initiated priv ops record via the same handle.

Unit tests cover record/list ordering, the 500 clamp, and retention vacuum.

The dashboard *surface* (an AUDIT view consuming /api/audit-log) is a
frontend follow-up coordinated with iris.
2026-06-13 13:45:02 +02:00
damocles
e9dec143e5 forge_notify: replace issue-tag comments with doc references (tracker-tag lint) 2026-06-13 12:20:21 +02:00
damocles
e6edfea0a8 fix(#1637): don't label later activity (bodiless reviews) as new PR 2026-06-13 12:20:21 +02:00
damocles
f05031ebe3 feat(#1636): infra_admin capability — restart hive-ci/gateway/forge via restart tool 2026-06-13 12:19:56 +02:00
damocles
f0be0172d5 prompt: trim /knowledge bullet to path + README pointer per review 2026-06-13 12:18:28 +02:00
damocles
d4333fc22f prompt: clarify /knowledge entry point and fix the contribution flow 2026-06-13 12:18:28 +02:00
damocles
3af3c3a613 turn_stats: create the session_id index after the column exists 2026-06-13 12:18:04 +02:00
damocles
d090df3c36 matrix: skip room invite when the user is already invited or joined 2026-06-13 12:17:16 +02:00
damocles
3795ffc1c6 harness: gate the bash mcp server behind the execution tool group 2026-06-13 11:18:16 +02:00
atlas
6c3a83ffa1 fix(dashboard): correct container-load cgroup path (always-empty LOAD tab)
The dashboard LOAD tab always showed "no running agent containers":
container_stats::gather() looked for each agent's cgroup at
machine.slice/machine-h\x2d<name>.scope, but that path never exists.

nixos-container runs `systemd-nspawn --keep-unit` with
`Slice = "machine.slice"` (nixpkgs virtualisation/nixos-containers.nix),
so --keep-unit means no separate machined `machine-<name>.scope` is
created — the container's cgroup IS the launching service unit,
`container@<machine>.service`, under machine.slice. systemd-machined
still logs "New machine <name>" (registration), which is what made the
scope-path assumption look plausible, but the cgroup stays on the unit.

Fix scope_dir to machine.slice/container@<machine>.service and drop the
\x2d escaping (the service unit name is used verbatim in the cgroup dir;
the journal shows the literal `container@h-<agent>.service`). Adds a unit
test pinning the path.
2026-06-10 22:33:49 +02:00
iris
878f95205b fix(dashboard): wire data-async form submit handler on the C0R3 page
Clicking "update & rebuild" (and cancel / respawn / purge) on /core.html
navigated to the bare `ok` response page instead of submitting async. The
C0R3 page (split out of the dashboard) carries `data-async` forms but its
bundle never had the global submit interceptor — that handler lived inline
in tabs.js, so only the dashboard bundle had it. The forms POSTed natively
and the browser followed the response.

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

Forms with `data-no-refresh` (e.g. meta-update, which gets its update via
the meta SSE events) skip the refresh, same as before.
2026-06-10 22:17:40 +02:00
iris
48347a01c9 docs(web-ui): document the first-turn ctx stats chip
The per-agent stats page gained a "first-turn ctx" summary chip (the
input tokens of the most recent fresh session's first turn — a
system-prompt/CLAUDE.md sprawl proxy), but agent.md's chip-row
description didn't mention it. Add it next to the reminder chips, noting
the stats.rs derive and the inert-until-capture behaviour (omitted from
JSON until the sessions / turn_stats.session_id capture has rows).
2026-06-10 21:49:28 +02:00
iris
0d013aefbb docs(web-ui): sync dashboard docs after the SYST3M → C0R3 page move
The C0R3 page PR moved the SYST3M tab off the dashboard onto the
standalone /core.html page, leaving the web-ui docs describing a tab that
no longer exists.

- dashboard.md: retitle the "SYST3M tab" section to "C0R3 page
  (/core.html)" and reframe its intro — it's now a standalone page off the
  H0M3 hub with a createTabStrip sub-tab nav (default R3BU1LD QU3U3), its
  own core.js bundle on /dashboard/stream; the dashboard keeps only the
  rebuild-queue state for the SW4RM "building…" badges.
- correct the chrome tab-strip list to the actual in-page tabs
  (SW4RM / Y3R C4LL / P3RM1SS10NS / SCH3DUL3S) and note SYST3M / ST4TS /
  S3TT1NGS / FL0W / L0GS / M4TR1X are all standalone pages off H0M3.
- fix the container-load poll reference (core.js, while the C0NT41N3R
  L04D sub-tab is active) and the stray "SYST3M ›" panel references.
- drive-by: the schedules endpoint doc said "SYST3M scheduled-prompts
  panel" — it's the SCH3DUL3S panel; corrected.
- web-ui.md index: same tab-list correction + C0R3 page pointer.
2026-06-10 21:49:12 +02:00
atlas
859cb256f3 docs(network): document container-side route + resolver wiring for isolation
The "What the Rust side does" section predated the isolated-container
egress fix and omitted two now-implemented mechanisms:

- HOST_ADDRESS=<bridge-ip> in the nspawn conf — load-bearing for the
  container's default route (nixos-container only installs `default via`
  when HOST_ADDRESS is non-empty; bridge mode skips host-side route setup).
- the resolver wiring: hive-priv's bridge-DNS marker + the
  hyperhive-isolated-dns oneshot rewriting resolv.conf to the bridge
  dnsmasq (nixos-container re-copies the host's resolv.conf each start, so
  the fix is runtime, ordered before the harness).

Doc-only; brings network.md in line with the merged behavior.
2026-06-10 21:49:01 +02:00
atlas
495146a05c fix(network): order isolated-DNS oneshot before the harness
Follow-up to the isolated-container egress fix. The hyperhive-isolated-dns
oneshot (which rewrites resolv.conf to the bridge resolver) was ordered
before network-online.target + tea-login, but NOT relative to the harness
(hive-ag3nt) or the matrix daemon. hive-ag3nt only declares
`after network.target`, so on first boot its first-turn api.anthropic.com
lookup could race ahead of the resolv.conf rewrite and error (self-heals
next turn, but flaps the first one).

Add hive-ag3nt.service + hive-matrix-daemon.service to the oneshot's
`before` so DNS is fixed before any network consumer starts. The matrix
entry is a harmless no-op when matrix is disabled (unit absent).

Caught by damocles in review of the parent PR.
2026-06-10 21:01:13 +02:00
atlas
d993ad2c47 fix(network): wire default route + bridge DNS for isolated containers
When isolateContainers=true, claude (and all egress) broke in every
container: agents came up with an IP but no way off the bridge subnet.

Two container-side gaps, both confirmed against nixpkgs
nixos-containers.nix:

1. No default route. hive-priv wrote HOST_ADDRESS= empty in the nspawn
   conf. nixos-container's container-side setup only installs
   `ip route add default via $HOST_ADDRESS` when HOST_ADDRESS is
   non-empty, so the container had an address but no gateway -> nothing
   off-subnet (incl. api.anthropic.com) was reachable. Fix: write
   HOST_ADDRESS=<bridge-ip>. In bridge mode the host-side address/route
   setup is skipped, so this only affects the container's default route.

2. No usable resolver. nixos-container copies the host's /etc/resolv.conf
   into the container at every start; the host resolver (e.g. 127.0.0.53)
   is unreachable from a private netns and isn't authoritative for the
   hive's own zones. Fix: hive-priv drops a marker carrying the gateway
   IP only when isolated, and a new harness-base oneshot
   (hyperhive-isolated-dns) rewrites resolv.conf to point at the bridge
   dnsmasq. Inert in shared-netns mode (no marker), so the shared
   container toplevel does the right thing in both modes.

The gateway IP is the address part of HIVE_NETWORK_SUBNET (the bridge IP
verbatim, honouring a non-.1 operator override), via a new validated
bridge_gateway_ip() helper with unit tests.

Unblocks defaulting isolation on.
2026-06-10 20:59:12 +02:00
iris
8db8bd610f feat(stats): surface first-turn ctx tokens on the per-agent stats page
Builds the read/surface half of the per-session first-turn-tokens metric
(the capture — sessions table + turn_stats.session_id — landed separately).
A fresh claude session's first turn pays the full static prefix (system
prompt + tools + CLAUDE.md + first wake) as uncached input, so its
input_tokens is a clean proxy for prompt / CLAUDE.md sprawl — watching it
over time surfaces creep.

- stats.rs: add `Snapshot.first_turn_ctx: Option<u64>` populated by
  `read_first_turn_ctx` — the agreed per-session derive (first turn,
  `ORDER BY started_at LIMIT 1`, of the most recent session that started
  in the window). Inert-until-capture: `.ok()` maps both "no fresh
  session yet" and "older db without the sessions table" to None, the
  same decoupling as read_bash_breakdown; the field is skipped from the
  JSON when None. Pre-capture rows have a NULL session_id and are excluded.
- agent stats.js: add a "first-turn ctx" summary chip, guarded on a
  numeric value so it stays hidden until capture has data.

clippy + cargo fmt clean; agent bundle builds.
2026-06-10 20:37:35 +02:00
damocles
62c41baf63 docs: drop residual both-binaries framings + stale binary_subcommand doc 2026-06-10 20:04:03 +02:00
damocles
cc03af3390 docs: drop stale two-loop/two-binary framings (single hive serve loop) 2026-06-10 20:01:52 +02:00
damocles
17ddba0341 feat(#1466): add sessions table + turn_stats.session_id capture for per-session stats 2026-06-10 19:56:15 +02:00
damocles
0c1c06ad76 docs: drop stale hive-m1nd references (single hive binary) 2026-06-10 19:52:19 +02:00
iris
b38c834182 fix(dashboard): keep the logs ← home link + sub-tabs on one row
The /logs.html header puts a flex:1 sub-tab strip (`.logs-tabbar`) next
to the ← home back-link inside the flex `.page-header`. The strip is
itself a flex container, so its default `min-width: auto` keeps it at its
intrinsic content width and — without an explicit nowrap on the parent —
it gets pushed onto its own row under the back-link.

Mirror the working dashboard chrome (`.dashboard-chrome` / `.tabbar`,
which sets `flex-wrap: nowrap`): make `.page-header` explicitly nowrap and
give `.logs-tabbar` `min-width: 0` so it shrinks to share the row instead
of wrapping. Affects every standalone page that uses `.page-header`
(flow / logs / stats / settings); the nowrap is correct for all of them.
2026-06-10 19:51:34 +02:00
iris
1510071e42 feat(dashboard): remove the SYST3M tab now that C0R3 owns it
Folds the SYST3M-tab removal into the same change that adds /core.html
(per operator review — it should be one PR, not an additive page + a
follow-up). Drops the tab + pane from dashboard.html and ~600 lines of
now-dead renderers/wiring from tabs.js: renderMetaInputs, renderRebuildQueue
+ renderQueueEntry + the rebuild-queue row cache / glyph maps / fingerprint,
renderTombstones, the container-load poll (cload* + start/stopContainerLoadPolling),
the two rqe-* elapsed tickers, the meta/tombstone state + sync + apply
handlers, their SSE dispatch entries, and the SYST3M tab-count.

Deliberately KEPT in tabs.js: rebuildQueueState + syncRebuildQueueFromSnapshot
+ applyRebuildQueueChanged + inFlightOpsByAgent — these don't render the
SYST3M panel, they drive the "building…" / "meta-updating…" badges on the
SW4RM agent cards, so the dashboard still subscribes to rebuild_queue_changed
to refresh those badges (applyRebuildQueueChanged now only re-renders
containers, no panel render).

core.css still imports dashboard.css for the shared section styles; splitting
those out + de-duping the renderers into a shared module remains a follow-up.
2026-06-10 19:51:06 +02:00
iris
be8d97ded7 feat(dashboard): add standalone C0R3 page (system tab → its own page)
Carves the dashboard's SYST3M tab content out into a standalone page at
/core.html, reached from the H0M3 hub, with a createTabStrip sub-tab
strip (default = Rebuild Queue): rebuild queue, meta inputs, kept state,
container load. Same minimal-chrome standalone-page pattern as
/logs.html (← home back-link + sub-tab nav).

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

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

Page name "C0R3" is a placeholder pending the operator's pick — trivially
renamed (the /core.html URL + the C0R3 label).
2026-06-10 19:51:06 +02:00
damocles
8792db9424 feat(#551): gate server-side e2ee behind opt-in matrix.allowEncryption (default off) 2026-06-10 19:12:36 +02:00
damocles
a6dd6d5dd8 fix(#551): update Cargo.lock for matrix-sdk e2e-encryption feature 2026-06-10 19:12:36 +02:00
damocles
78317b9cac feat(#551): switch read_room to room.messages() for e2ee decryption, add UTD sentinel, update docs 2026-06-10 19:12:36 +02:00
damocles
f51402a867 feat(#551): enable matrix e2ee -- add e2e-encryption feature, wire EncryptionSettings, flip tuwunel allow_encryption 2026-06-10 19:12:36 +02:00
atlas
9e499b25ea feat(#1114): filter hyperhive flake source via named hyperhiveFlakeSource
Replace the unfiltered hyperhiveFlake = "${self}" (handed to hive-c0re
as the flake path it builds agent containers from) with a named,
explicitly-filtered source derivation that drops files no nix or cargo
derivation reads: docs/, scripts/, and root-level *.md. Editing those no
longer changes the hyperhiveFlake store path, so it no longer forces a
rebuild of every agent container.

This is the per-package source-filter pattern (operator request on
#1114): each real source is a named filtered derivation used as src,
not an inline filter at the use site. Companion to the existing rust
cleanSrc (crane cargo-source filter); supersedes the inline nixSrc
approach in the older #1114 branch.

Safe: hive-c0re consumes hyperhiveFlake as a runtime path string and
builds packages/nixosConfigurations from it (nix build <path>#…), which
never forces the checks that reference ./docs — and no package, module,
or container config reads docs/scripts/root-md.
2026-06-10 17:24:43 +02:00
iris
6b4b0f11c0 docs(web-ui): document turn start/end times + duration on the agent terminal
Follow-up to the turn-time feature: the terminal now stamps a wall-clock
time on the turn-start / turn-end rows and an elapsed duration on
turn-end, sourced from a per-event `ts` (unix seconds) that the backend
serializes as a flattened sibling of the event tag on both the live SSE
frame and each history row.

- terminal-rendering.md: add the `.turn-time` row to the taxonomy table
  + a note on the ts source and the graceful degradation when a frame
  omits ts.
- web-ui/agent.md: note the ts sibling on the emitted events, and the
  time/duration suffixes on the TurnStart / TurnEnd renderings.
2026-06-10 15:38:04 +02:00