feat: walking skeleton — Go harness, SolidJS shell, supervised hugo preview #18

Merged
dominik.polakovics merged 2 commits from afk/1 into main 2026-07-11 14:13:11 +02:00

Closes #1

What this is

The end-to-end spine everything else plugs into: a single Go binary (harness serve --site <dir>) that serves an embedded SolidJS admin shell and supervises a hugo server --buildDrafts subprocess, with the live site shown in the shell's preview iframe.

How it works

  • internal/supervisor picks a free loopback port once at startup (so the preview URL stays stable across restarts), spawns hugo server --buildDrafts --bind 127.0.0.1 --port N --baseURL http://127.0.0.1:N/, and restarts it on crash with exponential backoff (500ms → 5s, reset after a healthy run). Combined stdout/stderr goes into a 200-line ring buffer; ERROR lines set lastBuildError, a new Change detected rebuild clears it. Graceful stop via SIGTERM with a 3s SIGKILL escalation — no orphan processes.
  • internal/server exposes GET /api/status (state / previewUrl / restarts / lastExit / lastBuildError / output) and serves the SPA from go:embed with index.html fallback. The CLI refuses any non-loopback --addr (D19).
  • web/ is a SolidJS + Vite + TypeScript three-pane placeholder shell (page tree / form / preview, per D11). It polls /api/status every 1.5s using per-field signals, so the iframe src is only ever assigned when the URL actually changes — Hugo LiveReload inside the iframe survives polling. Build errors show as a red banner over the live iframe; a crashed Hugo replaces the iframe with the captured output tail and a "restarting automatically" note.
  • testdata/site is a self-contained sample Hugo site (includes a draft post to prove --buildDrafts) used by the supervisor integration test.
  • CI (.forgejo/workflows/ci.yml) builds/tests the frontend first (go:embed needs web/dist), then runs go vet, go test (with hugo 0.161.1 installed for the integration test), and go build.

Verification performed (all live, against the built binary)

  • go vet ./... clean; go test -race ./... green (incl. real-hugo integration test); npm run build + npm test (4/4) green; tsc --noEmit clean
  • SPA served at / from the embedded FS; /api/status matches the documented contract
  • Preview serves the sample site with the LiveReload script injected; draft post renders (HTTP 200)
  • Hand-editing a content file on disk reflected in the preview within ~2s with no tool involvement
  • Breaking a template while running → lastBuildError populated (state stays running); restoring → cleared
  • Starting with a broken environment (hugo missing) → crash loop surfaced via lastExit instead of a dead iframe
  • SIGKILL of the hugo child → supervisor restarts it (restarts incremented, same preview URL)
  • SIGTERM of the harness → clean shutdown, zero orphan processes
  • --addr 0.0.0.0:9999 refused (exit 2); missing/non-Hugo site dir rejected with friendly errors

Notes for the reviewer

  • ci.yml uses runs-on: docker (forgejo-runner's default label) — adjust if this instance's runners use different labels.
  • go build requires web/dist to exist (frontend build first); make encodes the ordering, README documents it.
  • Out of scope per the issue: hugo version pinning/auto-download (#5), real editing (#2/#3/#4), click-to-select bridge (#16), harness init (#17).

🤖 Generated with Claude Code

https://claude.ai/code/session_01WFnCfoX1pGAGyWeaisv2Gr

Closes #1 ## What this is The end-to-end spine everything else plugs into: a single Go binary (`harness serve --site <dir>`) that serves an embedded SolidJS admin shell and supervises a `hugo server --buildDrafts` subprocess, with the live site shown in the shell's preview iframe. ## How it works - **`internal/supervisor`** picks a free loopback port once at startup (so the preview URL stays stable across restarts), spawns `hugo server --buildDrafts --bind 127.0.0.1 --port N --baseURL http://127.0.0.1:N/`, and restarts it on crash with exponential backoff (500ms → 5s, reset after a healthy run). Combined stdout/stderr goes into a 200-line ring buffer; `ERROR` lines set `lastBuildError`, a new `Change detected` rebuild clears it. Graceful stop via SIGTERM with a 3s SIGKILL escalation — no orphan processes. - **`internal/server`** exposes `GET /api/status` (state / previewUrl / restarts / lastExit / lastBuildError / output) and serves the SPA from `go:embed` with index.html fallback. The CLI refuses any non-loopback `--addr` (D19). - **`web/`** is a SolidJS + Vite + TypeScript three-pane placeholder shell (page tree / form / preview, per D11). It polls `/api/status` every 1.5s using per-field signals, so the iframe `src` is only ever assigned when the URL actually changes — Hugo LiveReload inside the iframe survives polling. Build errors show as a red banner over the live iframe; a crashed Hugo replaces the iframe with the captured output tail and a "restarting automatically" note. - **`testdata/site`** is a self-contained sample Hugo site (includes a draft post to prove `--buildDrafts`) used by the supervisor integration test. - **CI** (`.forgejo/workflows/ci.yml`) builds/tests the frontend first (go:embed needs `web/dist`), then runs `go vet`, `go test` (with hugo 0.161.1 installed for the integration test), and `go build`. ## Verification performed (all live, against the built binary) - `go vet ./...` clean; `go test -race ./...` green (incl. real-hugo integration test); `npm run build` + `npm test` (4/4) green; `tsc --noEmit` clean - SPA served at `/` from the embedded FS; `/api/status` matches the documented contract - Preview serves the sample site with the LiveReload script injected; draft post renders (HTTP 200) - Hand-editing a content file on disk reflected in the preview within ~2s with no tool involvement - Breaking a template while running → `lastBuildError` populated (state stays `running`); restoring → cleared - Starting with a broken environment (hugo missing) → crash loop surfaced via `lastExit` instead of a dead iframe - `SIGKILL` of the hugo child → supervisor restarts it (restarts incremented, same preview URL) - `SIGTERM` of the harness → clean shutdown, zero orphan processes - `--addr 0.0.0.0:9999` refused (exit 2); missing/non-Hugo site dir rejected with friendly errors ## Notes for the reviewer - `ci.yml` uses `runs-on: docker` (forgejo-runner's default label) — adjust if this instance's runners use different labels. - `go build` requires `web/dist` to exist (frontend build first); `make` encodes the ordering, README documents it. - Out of scope per the issue: hugo version pinning/auto-download (#5), real editing (#2/#3/#4), click-to-select bridge (#16), `harness init` (#17). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WFnCfoX1pGAGyWeaisv2Gr
feat: walking skeleton — Go harness, SolidJS shell, supervised hugo preview
Some checks failed
ci / build-and-test (pull_request) Has been cancelled
8850b75153
Single Go binary (cmd/harness) that serves an embedded SolidJS admin
shell and supervises a `hugo server --buildDrafts` subprocess:

- internal/supervisor: spawns hugo on a free loopback port, restarts it
  on crash with backoff, captures combined output into a ring buffer,
  and tracks build errors (ERROR lines set lastBuildError, "Change
  detected" clears it)
- internal/server: GET /api/status JSON + embedded SPA with index.html
  fallback; binary refuses non-loopback listen addresses (D19)
- web/: SolidJS + Vite + TypeScript three-pane shell (tree/form/preview
  placeholders per D11); polls /api/status, shows the live site in an
  iframe without disturbing Hugo LiveReload, surfaces build errors as a
  banner and crashes as an error panel with the output tail
- web/embed.go: go:embed of web/dist so `go build` yields one artifact
  (D22); frontend build must run first (make handles the ordering)
- testdata/site: self-contained sample Hugo site with a draft post,
  used by the supervisor integration test
- .forgejo/workflows/ci.yml: frontend build+test, then go vet/test/build
  with hugo installed for the integration test

Closes #1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFnCfoX1pGAGyWeaisv2Gr
ci: run on ubuntu-latest instead of unavailable docker runner
All checks were successful
ci / build-and-test (pull_request) Successful in 2m8s
1d81872bc0
This instance's forgejo-runner only offers the `ubuntu-latest` label; the
`docker` label left the job stuck "Waiting to run" with no runner to pick
it up. The image is bare, but the job already installs its own toolchains
(Node, Go, Hugo), so no prebuilt-image label is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DP3sqy5YdmmFnLvYc1caBk
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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
Cloonar/deckle!18
No description provided.