feat(ui): read-only local SPA for instances, forges, and ledger - #8
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
127.0.0.1:7878that 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-uiis a Node 20+ single-file HTTP server (no npm deps at runtime) withstart | stop | status | restart | open | foregroundsubcommands.startdaemonizes and is idempotent; PID/port land in~/.lmstack/ui.{pid,port,log}.skills/install/SKILL.mdgains a Phase 7 that callslmstack-ui startat 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
host.yml+probe.json, plus per-status forge counts.exec.log/judge.logwith ANSI escapes stripped so terminal transcripts render cleanly. Falls back to the ledger row when the task record has been cleaned up.ledger.jsonlas a filterable table (shape / outcome / role).Architecture
127.0.0.1). No auth, no CORS, no writes anywhere.node:http/node:fs/node:child_process— no runtime deps.ui/dist/is committed so end users don't need Node — onlylmstackdevelopers do (fornpm run build).Follow-ups (each worth its own issue)
outcome/wall_min/interventionsinto the task record on ledger write.lmstack-task list --staleso any client shares the same stale-session definition.runs/<slug>/endedfor every terminal path, not only the clean one.lmstack-task list --all-roles— the CLI is per-role; UI wants a flat listing.Test plan
cd ui && npm install && npm run buildproducesui/dist/index.html.bin/lmstack-ui startdaemonizes;bin/lmstack-ui statusprintsrunning http://127.0.0.1:7878/; secondstartis a no-op.curl -sf http://127.0.0.1:7878/api/instancesreturns the roles under~/.lmstack/*/{host.yml,tasks}; forges list is correct; ledger endpoint returns parsed jsonl.ss -ltn | grep 7878confirms the bind is127.0.0.1, not0.0.0.0./lmstack:installend-to-end brings the UI up without a manual step and prints the URL in the finish message.ui/dist/and runninglmstack-ui startexits non-zero with the build hint.🤖 Generated with Claude Code