feat: nav badges for modules; log pane follows its stream #75

Manually merged
julian merged 0 commits from feat/nav-badges into main 2026-10-28 01:06:33 +01:00
Owner

Problem

  1. Only Activity could show a count in the sidebar. The updates module needs one for pending packages, and other modules will want the same, so this is a core feature rather than a special case in the shell.
  2. A schema logText pane on the web stayed at the top during an apt run. You had to scroll by hand to see new output.

Commits

  • c920d9c feat: a module can put a count on its nav entry
  • cf98e1d fix(shell): a log pane follows its stream and stays at the bottom

Nav badges

ctx.ui.badge({ count: 29, tone: "warning", title: "29 updates available, 3 security" });
ctx.ui.badge(null); // clear; a count of 0 also clears
  • The badge is a state, not an event. The latest call wins and an unchanged badge is not re-sent, so a collector can call this on every tick. A malformed count throws.
  • Badges are sent on the new retained topic core:badges. The snapshot is { [module]: badge } and each event is { module, badge | null }. The sidebar needs no REST call and no polling.
  • A user only receives badges for modules they can see. This uses the same moduleVisible rule as GET /modules, so LiveViewer now carries the session's roles.
  • A badge set inside register() is held until the module has finished loading, then sent.
  • Tones are info (accent), warning (yellow with dark text) and critical.
  • All sidebar counts are now bold, Activity's included. Activity's red pill now uses the shared critical class with white text.
  • docs/modules.md has a new section, "Navigation badges".

Log pane

The web renderer ignored logText.follow, so the pane only changed when the source was re-fetched, and each re-fetch scrolled it back to the top.

  • LogTextBlock now subscribes to the follow topic and appends {chunk} events to the fetched text. A new fetch replaces the appended text, because it already contains it.
  • The pane stays at the bottom while you are at the bottom, and stops following when you scroll up.

Verification

  • deno task check, check:svelte (0 errors, no warnings in the changed files), deno lint and deno fmt --check are clean on the changed paths.
  • deno task test packages/server/tests/: 191 passed. This includes the new badges_test.ts, which covers visibility per role, no re-send of an unchanged badge, 0 clearing a badge, and malformed counts.
  • Checked in a browser with Playwright and OPSDECK_FAKE_DATA=1: the updates module set a dummy badge and the sidebar showed a yellow, bold 29. Activity's pill is bold.
  • Not checked in a browser: the log pane following a real apt run. The rig has no apt host. Please watch one real run.

Not in here

  • The mobile app does not show badges yet. It can subscribe to core:badges.

Downstream

OpsDeck/module_updates feat/nav-badge calls ctx.ui.badge. Merge and deploy this PR first. On a core without ctx.ui.badge, that module's page fails to load.

🤖 Generated with Claude Code

## Problem 1. Only Activity could show a count in the sidebar. The updates module needs one for pending packages, and other modules will want the same, so this is a core feature rather than a special case in the shell. 2. A schema `logText` pane on the web stayed at the top during an apt run. You had to scroll by hand to see new output. ## Commits - `c920d9c` feat: a module can put a count on its nav entry - `cf98e1d` fix(shell): a log pane follows its stream and stays at the bottom ## Nav badges ```ts ctx.ui.badge({ count: 29, tone: "warning", title: "29 updates available, 3 security" }); ctx.ui.badge(null); // clear; a count of 0 also clears ``` - The badge is a state, not an event. The latest call wins and an unchanged badge is not re-sent, so a collector can call this on every tick. A malformed count throws. - Badges are sent on the new retained topic `core:badges`. The snapshot is `{ [module]: badge }` and each event is `{ module, badge | null }`. The sidebar needs no REST call and no polling. - A user only receives badges for modules they can see. This uses the same `moduleVisible` rule as `GET /modules`, so `LiveViewer` now carries the session's `roles`. - A badge set inside `register()` is held until the module has finished loading, then sent. - Tones are `info` (accent), `warning` (yellow with dark text) and `critical`. - All sidebar counts are now **bold**, Activity's included. Activity's red pill now uses the shared `critical` class with white text. - `docs/modules.md` has a new section, "Navigation badges". ## Log pane The web renderer ignored `logText.follow`, so the pane only changed when the source was re-fetched, and each re-fetch scrolled it back to the top. - `LogTextBlock` now subscribes to the follow topic and appends `{chunk}` events to the fetched text. A new fetch replaces the appended text, because it already contains it. - The pane stays at the bottom while you are at the bottom, and stops following when you scroll up. ## Verification - `deno task check`, `check:svelte` (0 errors, no warnings in the changed files), `deno lint` and `deno fmt --check` are clean on the changed paths. - `deno task test packages/server/tests/`: 191 passed. This includes the new `badges_test.ts`, which covers visibility per role, no re-send of an unchanged badge, 0 clearing a badge, and malformed counts. - Checked in a browser with Playwright and `OPSDECK_FAKE_DATA=1`: the updates module set a dummy badge and the sidebar showed a yellow, bold **29**. Activity's pill is bold. - **Not checked in a browser:** the log pane following a real apt run. The rig has no apt host. Please watch one real run. ## Not in here - The mobile app does not show badges yet. It can subscribe to `core:badges`. ## Downstream OpsDeck/module_updates `feat/nav-badge` calls `ctx.ui.badge`. **Merge and deploy this PR first.** On a core without `ctx.ui.badge`, that module's page fails to load. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
`ctx.ui.badge({ count, tone, title })` shows a pill next to the module's
sidebar entry, the way Activity already shows failed and live runs. The
first user is the updates module (pending packages), but anything with a
state worth noticing from another page needs the same thing, so it is a
core capability rather than a special case in the shell.

It is a state, not an event: the latest call wins, null or 0 clears, and
an unchanged badge is not re-sent, so a collector can call it every tick.
Badges travel on the retained topic `core:badges` so the sidebar needs no
REST baseline and no poll. Snapshot and deltas are projected per viewer
against the same `moduleVisible` rule `/modules` uses, which is why
`LiveViewer` now carries the session's roles: `isAdmin` alone cannot
answer `nav.role: "viewer"`. A badge set during `register()` is held
until the module is in `loaded` and announced then; before that no
viewer may see the module and the delta would be dropped.

Tones are info (accent), warning and critical. Every sidebar count is now
bold, Activity's included, and Activity's red pill uses the shared
`critical` class with white ink — the status ink tokens are text-safe on
the page, not on their own fill.
fix(shell): a log pane follows its stream and stays at the bottom
All checks were successful
Build and Deploy / verify (pull_request) Successful in 1m51s
Build and Deploy / build (pull_request) Has been skipped
Dependency Check / dependencies (pull_request) Successful in 1m11s
cf98e1d356
The web renderer ignored `logText.follow` entirely, so a module's live
output (the updates module's apt run) only changed when something
re-fetched the source, and every re-fetch put the pane back at the top
of a few thousand lines.

The block now subscribes to the follow topic and appends `{chunk}`
events to the fetched snapshot; a new snapshot already contains what
was streamed, so it resets the appended tail, and a resync re-fetches.
The pane scrolls to the newest line on every change while the reader is
within a line of the bottom, and stops following once they scroll up.
chore(shell): type-check badges.svelte.ts in check:frontend
All checks were successful
Build and Deploy / verify (pull_request) Successful in 1m46s
Build and Deploy / build (pull_request) Has been skipped
Dependency Check / dependencies (pull_request) Successful in 1m0s
945540fd19
Only AppShell.svelte imports it, and deno check stops at a component, so
without an explicit entry the file is never visited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
julian manually merged commit e691920247 into main 2026-10-28 01:06:33 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OpsDeck/core!75
No description provided.