Skip to content

feat(view): serve docs, plans and specs as a read-only page on 127.0.0.1 - #56

Merged
protonspy merged 2 commits into
mainfrom
feat/view
Oct 5, 2026
Merged

protonspy merged 2 commits into
mainfrom
feat/view

Conversation

@protonspy

Copy link
Copy Markdown
Owner

What changed

New command scc view [--port N] [--root DIR] [--json]: serves the workspace's record as a local, read-only web page — wiki, ADRs, codewiki with the cited source lines highlighted, glossary, stack, notes, and every plan and spec with its task progress. Runs until Ctrl+C (exit 0). Default port 7777; --port 0 picks a free one.

  • internal/view — stdlib net/http; three JSON routes (/api/tree, /api/page, /api/source), each reading the disk per request through internal/artifact, plus the embedded SPA.
  • internal/cli/view.go — the command handler; wired into the dispatcher, usage and README.
  • web/ — React 19 + react-markdown/remark-gfm, built by Vite 8 into internal/view/dist/ (committed, go:embed). go build still needs no node; go.mod stays stdlib-only.
  • .github/workflows/ci.yml — new web job: npm ci, vitest, tsc, rebuild, fail if dist/ differs from what is committed.
  • Records: adr:0003-local-read-only-viewer (reopens the "no embedded web dashboard" line of ADR 0002, with a pointer added there), docs/stack.md frontend entries, a codewiki section in docs/codewiki/packages.md.

Spec

specs/view/ — requirements R1 (serving), R2 (content), R3 (safety); 8/8 tasks done.

Security boundary

Binds 127.0.0.1 only · Host must name loopback (DNS rebinding → 403) · GET/HEAD only (405) · CSP default-src 'self' with no inline scripts, nosniff, frame-ancestors 'none' · raw HTML in Markdown renders as text (no rehype-raw) · external links limited to http(s)/mailto · paths must be clean, inside the workspace after symlinks, and carry no dot-named segment on the resolved real path · files over 1 MiB refused and left out of the tree.

How it was verified

  • scc validate --checks: build, format, lint, test — 1029 Go tests, 86.3% coverage, 0 findings.
  • web/: 14 vitest cases (link resolution, slug parity with mdscan.Slug, route parsing), tsc clean. (The commit message says 16; it is 14.)
  • Driven in Chrome against this repository: home, sidebar, wiki links, ADR citations, spec task progress, codewiki citation → highlighted source.
  • Symlink tests skip on Windows without the privilege and run on the Linux/macOS CI runners; the 8.3 short-name test ran on Windows.

Reviews

  • scc-code-review: clean. 4 minors. Fixed: the dot rule now applies to pages too; a missing asset is a real 404 instead of falling back to index.html. Not fixed: if a page has two headings with the same text, they get the same anchor id. This matches mdscan.Slug, which the server's table of contents is built from, so I kept the two in step. Not fixed: requirements.md had unstaged changes. Nothing to change in the code; everything is now staged and committed.
  • scc-security-review: changes requested, now fixed. One medium: the dot-segment rule could be bypassed through Windows 8.3 short names (GIT~1/config was read on the reviewer's Windows host) and through a committed symlink to a dot path. Fixed: the rule now runs on the EvalSymlinks path, relative to the resolved root. Tests were added for both. Its hypotheses are also fixed: the 1 MiB cap now covers pages and the tree. One low, accepted and recorded in ADR 0003: any local account can reach the unauthenticated loopback port. Re-review was not run after the fixes (one fix round, per delivery.md).

`scc view [--port N] [--root DIR] [--json]` serves the workspace's record
to a person: wiki, ADRs, codewiki next to the source lines it cites,
glossary, stack, notes, and every plan and spec with its task progress.
It runs until interrupted, and an interrupt exits 0.

internal/view is stdlib net/http with three JSON routes (/api/tree,
/api/page, /api/source), each reading the disk per request through
internal/artifact, so the page cannot disagree with `scc map`. The
frontend is React, built by Vite from web/ into internal/view/dist/,
committed and embedded with go:embed, so `go build` still needs no node.

Boundaries: binds 127.0.0.1 only; a Host header that does not name
loopback is 403 (DNS rebinding); GET/HEAD only; CSP keeps scripts and
connections on the viewer's origin; raw HTML in Markdown renders as text.
Paths must be clean, inside the workspace after symlinks, and carry no
dot-named segment on the real path, so neither a committed symlink to
.env nor a Windows short name like GIT~1 reaches past it. Files over
1 MiB are refused and left out of the tree.

Reopens the "no embedded web dashboard" line of ADR 0002 in ADR 0003;
stack.md lists the frontend's packages; CI gains a `web` job that tests,
typechecks and rebuilds dist/, failing on a diff.

Spec: specs/view/
Verified: scc validate --checks (1029 Go tests, 86.3% coverage, lint and
format clean); 16 vitest cases and tsc for web/; driven in Chrome against
this repository.
@protonspy
protonspy merged commit bb394fe into main Oct 5, 2026
5 checks passed
@protonspy
protonspy deleted the feat/view branch October 5, 2026 17:42
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