Pages that render TrackRow/AlbumCard/ArtistRow/PlayerBar now indirectly
mount LikeButton, which calls useQueryClient(). Route tests need the
same mocks the component-level tests have.
- Added createLikedIdsQuery mock to 7 route test files
- Added useQueryClient mock to all affected route tests
- Added readable store import where needed
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Scaffold a SvelteKit SPA at `web/`, embed its build output into the Go binary, and update the Dockerfile + CI so a single container image serves the SPA at `/` alongside the existing `/api/*` and `/rest/*` surfaces.
**Architecture:** SvelteKit with `@sveltejs/adapter-static` (SPA fallback to `index.html`) builds to `web/build/`. A thin `web` Go package `//go:embed`s that directory and returns an `http.Handler` that serves static assets and falls back to `index.html` for unknown paths. The handler is wired as the server's `NotFound` route so explicit routes (`/api`, `/rest`, `/healthz`) are unaffected; the `NotFound` wrapper also returns a JSON 404 for unmatched `/api/*` and `/rest/*` paths so the SPA fallback never produces HTML bodies for API consumers. A hand-crafted placeholder `web/build/index.html` is committed (via negated gitignore) so `go build` works in clean checkouts before anyone runs `npm run build`.
**Reference spec:**`docs/superpowers/specs/2026-04-20-web-ui-scaffold-design.md`. This plan implements only the Stack / Packaging / Dev-workflow / Build sections. Feature UI (login, library views, player) is out of scope and lands in follow-up plans.
| `web/embed.go` | Create | `//go:embed` build tree + `Handler()` with SPA fallback (must live next to `web/build/` — `go:embed` cannot reach parent directories) |
| `web/embed_test.go` | Create | Unit tests for the handler |
| `internal/server/server.go` | Modify | Wire `web.Handler()` into `NotFound`; JSON-404 for `/api/*` and `/rest/*` |
| `internal/server/server_test.go` | Modify | Assert `/` returns HTML; `/api/anything` does not return HTML |
This placeholder lets `//go:embed build` compile before anyone runs `npm run build`. Real `npm run build` overwrites it locally; only this hand-written version is committed.
```html
<!doctype html>
<htmllang="en">
<head>
<metacharset="utf-8"/>
<title>Minstrel</title>
</head>
<body>
<p>
Minstrel SPA has not been built. Run
<code>cd web && npm install && npm run build</code> or
build the container image, which runs the web build during
Expected: exit 0; `web/build/` now contains `index.html`, `_app/`, `favicon.png`. (Local `index.html` is overwritten but not committed — `.gitignore` keeps only the committed placeholder under version control.)
Undo the local overwrite so the committed placeholder remains:
# Lint + short unit tests. Integration tests needing Postgres/ffmpeg run
# locally via docker-compose; they should guard with testing.Short().
on:
push:
branches:[dev]
pull_request:
branches:[main]
jobs:
test:
runs-on:go-ci
steps:
- name:Checkout
uses:actions/checkout@v4
- name:Setup Node
uses:actions/setup-node@v4
with:
node-version:'22'
cache:'npm'
cache-dependency-path:web/package-lock.json
- name:Install web deps
working-directory:web
run:npm ci
- name:Web build (populates web/build/ for go:embed)
working-directory:web
run:npm run build
- name:Web test (vitest)
working-directory:web
run:npm test
- name:Detect Go project
id:gomod
shell:bash
run:|
if [ -f go.mod ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::No go.mod yet — Go steps will be skipped until the skeleton lands"
fi
- name:Toolchain versions
if:steps.gomod.outputs.present == 'true'
run:|
go version
golangci-lint --version
- name:go vet
if:steps.gomod.outputs.present == 'true'
run:go vet ./...
- name:golangci-lint
if:steps.gomod.outputs.present == 'true'
run:golangci-lint run ./...
- name:go test (short, race)
if:steps.gomod.outputs.present == 'true'
run:go test -short -race ./...
```
- [ ]**Step 2: Commit**
```bash
git add .forgejo/workflows/test.yml
git commit -m "ci: install Node + build web before Go steps
Go's //go:embed needs web/build/ to exist at compile time. CI now
runs 'npm ci && npm run build && npm test' ahead of the Go steps
so the embed directive sees real, freshly-built assets."
```
---
## Task 7: README dev-workflow section
**Files:**
- Modify: `README.md`
- [ ]**Step 1: Read current README**
Run: `cat README.md`
Note the existing structure (headings, sections).
- [ ]**Step 2: Append or replace a "Development" section**
Use the Edit tool to insert the following section. Place it after any existing "Getting started" or "Quickstart" section; if none exists, append to the end of the file.
```markdown
## Development
Minstrel has two concurrent dev processes:
1.**Backend:**`docker compose up` — starts Postgres and Minstrel on `:4533`.
2.**Frontend:**`cd web && npm install && npm run dev` — Vite dev server on `:5173` with HMR.
The Vite dev server proxies `/api/*` and `/rest/*` to the backend on `:4533`,
so session cookies work correctly when the SPA is loaded from the Vite origin.
### Production build
`docker build -t minstrel .` runs the SvelteKit build inside a `node` stage,
copies the output into the `golang` stage, and `//go:embed`s it into the
final binary. The resulting container serves the SPA from `/` alongside the
API surfaces. No separate static-file server is required.
```
- [ ]**Step 3: Commit**
```bash
git add README.md
git commit -m "docs: document two-process dev workflow with Vite proxy"
```
---
## Task 8: Final verification
**Files:** none (verification only).
- [ ]**Step 1: Full Go suite**
Run: `go test -short -race ./...`
Expected: PASS.
- [ ]**Step 2: Go linter**
Run: `golangci-lint run ./...`
Expected: no issues.
- [ ]**Step 3: Web tests + build**
Run: `cd web && npm test && npm run build`
Expected: PASS. Verify `web/build/index.html` was regenerated; then:
Run: `git checkout -- web/build/index.html`
Expected: committed placeholder restored (the fresh build output stays out of version control).
-`GET /api/nonexistent` → `401 Unauthorized` (RequireUser kicks in before the NotFound wrapper for mounted `/api/*` routes) OR `404 Not Found` with `Content-Type: application/json` (not `text/html`). Either is acceptable — the key requirement is **no `text/html` body**.
Tear down:
```bash
docker compose down
```
- [ ]**Step 6: Finish the branch**
Follow `superpowers:finishing-a-development-branch`: present the four-option menu and proceed per the user's choice. The expected path is Option 2 — push `dev`, open a PR to `main`, wait for Forgejo CI, merge once green.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.