Skip to content

feat(ui): read-only local SPA for instances, forges, and ledger - #8

Merged
ric03uec merged 4 commits into
mainfrom
feat/ui-spa
Aug 4, 2026
Merged

feat(ui): read-only local SPA for instances, forges, and ledger#8
ric03uec merged 4 commits into
mainfrom
feat/ui-spa

Conversation

@ric03uec

@ric03uec ric03uec commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a read-only React SPA served on 127.0.0.1:7878 that reads ~/.lmstack/ and lets a user watch instances and forges without attaching to tmux. Real work still runs in tmux panes / coding harnesses.
  • bin/lmstack-ui is a Node 20+ single-file HTTP server (no npm deps at runtime) with start | stop | status | restart | open | foreground subcommands. start daemonizes and is idempotent; PID/port land in ~/.lmstack/ui.{pid,port,log}.
  • skills/install/SKILL.md gains a Phase 7 that calls lmstack-ui start at the tail of a successful install so the URL is ready to hand to the user; a missing SPA build prints the build hint but does not abort the install.

What's in the UI

  • Instances — one card per installed role with engine/gpu/models/verdict pulled from host.yml + probe.json, plus per-status forge counts.
  • Forges — flat table grouped by status (running / in-review / queued / completed / failed / stale), filterable by role and status. Click a row for detail.
  • Forge detail — task metadata, brief.md, every judge round, worktree/branch/tmux liveness, and the last 200 lines of exec.log / judge.log with ANSI escapes stripped so terminal transcripts render cleanly. Falls back to the ledger row when the task record has been cleaned up.
  • Ledger — the full ledger.jsonl as a filterable table (shape / outcome / role).
  • Light theme, left sidebar nav with icons, top-right links to Docs and GitHub.

Architecture

  • Loopback bind only (127.0.0.1). No auth, no CORS, no writes anywhere.
  • Node's built-in node:http / node:fs / node:child_process — no runtime deps.
  • Every screen has a Refresh button; the user drives updates (no SSE, no auto-poll).
  • Vite build under ui/dist/ is committed so end users don't need Node — only lmstack developers do (for npm run build).

Follow-ups (each worth its own issue)

  1. Task record ↔ ledger join — mirror outcome / wall_min / interventions into the task record on ledger write.
  2. lmstack-task list --stale so any client shares the same stale-session definition.
  3. Explicit runs/<slug>/ended for every terminal path, not only the clean one.
  4. Status-change reason on the task record.
  5. lmstack-task list --all-roles — the CLI is per-role; UI wants a flat listing.
  6. Uninstall vs. history: define whether task records survive removing a role dir.

Test plan

  • cd ui && npm install && npm run build produces ui/dist/index.html.
  • bin/lmstack-ui start daemonizes; bin/lmstack-ui status prints running http://127.0.0.1:7878/; second start is a no-op.
  • curl -sf http://127.0.0.1:7878/api/instances returns the roles under ~/.lmstack/*/{host.yml,tasks}; forges list is correct; ledger endpoint returns parsed jsonl.
  • ss -ltn | grep 7878 confirms the bind is 127.0.0.1, not 0.0.0.0.
  • Running /lmstack:install end-to-end brings the UI up without a manual step and prints the URL in the finish message.
  • With no forges, every screen renders an empty state.
  • Renaming ui/dist/ and running lmstack-ui start exits non-zero with the build hint.

🤖 Generated with Claude Code

Adds bin/lmstack-ui, a Node 20+ single-file HTTP server that binds
127.0.0.1:7878 and serves a React SPA (ui/dist/, pre-built) plus a small
JSON API over ~/.lmstack/. The UI is strictly read-only: it walks task
records, ledger, and run artifacts, intersects with `tmux list-sessions`
for liveness, and never mutates state. Real work stays in tmux panes and
coding harnesses.

Screens: Instances (installed roles + counts), Forges (grouped by
status, filterable by role/status), Forge detail (task metadata, brief,
judge rounds, ANSI-stripped log tails), Ledger (filterable table).

Lifecycle: `lmstack-ui start|stop|status|restart|open|foreground`.
`start` daemonizes and is idempotent; PID/port land in ~/.lmstack/.
The install skill now runs `lmstack-ui start` at the tail of a
successful install so the URL is ready to hand to the user; a missing
SPA build prints the build hint but does not abort the install.
- The Instances page now renders one full-width card per role with sections
  for Hardware, Inference engine, Operating system, and Forge activity,
  plus a Models table (slug / hf model / context / max seqs / VRAM est /
  port / tier / active) and blocks for the classifier's memory arithmetic
  and warnings.
- Vendor badge (stylised NVIDIA green N, AMD red A, or generic GPU tile)
  driven by probe.gpu.vendor, falling back to a role-name heuristic so
  h1-nvidia / h2-amd cards still show the right badge before probe runs.
- Explicit empty-state hint on each card when host.yml / probe.json /
  classify.json are missing on disk, pointing the user at
  `/lmstack:analyze <target>`.
- Backend `/api/instances` now merges host.yml + probe.json + classify.json
  into one shape per role, surfaces which source files are present, and
  enriches the models list with per-model params from classify.

Also updates the `analyze` skill so it writes classify.json and a minimal
host.yml stub alongside probe.json — the three files a rich card needs.
The install skill still overwrites host.yml with the final version.
The NVIDIA and AMD marks were literal SVG <text> "N" and "A" characters on
brand-hue tiles — the previous comment on that block said "deliberately not
reproducing the trademarked wordmarks", which is a fair legal caution but the
result read as a placeholder, not a badge.

Replace the letters with stylised geometric marks (an eye-shaped lens for
NVIDIA, a chevroned "A" for AMD, a card silhouette for the generic case), and
add an override path: drop the vendor's official SVG at
ui/public/vendor/<slug>.svg and VendorMark renders it instead of the built-in
mark. The <img>'s onError falls the component back to the SVG if the file is
absent, so the common "no override" case still renders and does not console-
log a 404 after the first miss.

Nothing in the trademarked assets ships with the repo; the vendor/ README
points at each vendor's brand kit as the source.
… fallback

The stylised marks were only meant to bridge the case where no logo file is
present. Ship the actual marks in ui/public/vendor/, so a fresh checkout
renders the badge someone recognises without anyone having to download an SVG
first.

Paths come from simple-icons (CC0), each wrapped in a brand-hue tile — the
SVG file carries its own colour, so nothing on the consuming end needs to
know about it. The fallback marks in Instances.tsx stay put for any vendor
we do not yet have a file for.
@ric03uec
ric03uec merged commit 80fe060 into main Aug 4, 2026
1 check passed
@ric03uec
ric03uec deleted the feat/ui-spa branch August 4, 2026 14:37
ric03uec added a commit that referenced this pull request Aug 4, 2026
The prior rule explicitly whitelisted ui/dist/ against the top-level dist/
ignore, on the argument that end users would not need Node to view the UI.
That trade did not hold: every UI edit produced a diff dominated by rebuilt
bundles, and PR #8 shipped 4k lines of dist/ churn alongside a small logic
change.

bin/lmstack-ui already prints the install-and-build command when the bundle
is missing, so first run reads as a one-time setup rather than a broken repo.
Drop the whitelist, remove the four tracked artifacts, keep a ui/dist/ entry
in the UI section for readers who look for UI paths in one place.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant