Add the initial mediamark app #1

Merged
benvin merged 3 commits from benvin/initial-app into main 2026-08-29 22:00:12 +10:00
Member

Kids-safe media needs a curated cheeztv tree, and today promoting a title means somebody SSHing in to run cp -al by hand. mediamark makes it a two-keystroke web action for the akP-mediamark-user group, with the hardlink mechanics and the traversal guards written down and tested.

  • Adds internal/library: a recursive hardlink sync that promotes a fafflix/<section>/<title> directory into cheeztv/, idempotent on re-run (relinks only new/changed files), and reports per-title size, file count, marked state and link drift (needsSync). Unmark removes only the kids-side directory, so media is never destroyed.
  • Guards title names as a single clean path element (no separators, .., or absolute paths) and re-verifies containment before every filesystem call; table tests cover the traversal cases.
  • Adds internal/arr: a minimal typed sonarr/radarr v3 client with a 60s list cache, matching *arr entries to library directories by path basename, plus a poster proxy that injects the API key server-side and never leaks it into a body, header, or error.
  • Adds internal/auth: server-side Authentik group enforcement on every API call and on the page load itself, so oauth2-proxy is defence in depth rather than the only gate. An empty allow-list fails startup.
  • Adds internal/server: the library JSON API, mark/unmark mutations, the art proxy, /livez + /readyz, and the embedded SPA with deep-link fallback.
  • Adds the UI: a two-tile landing page (m/t), an autofocused fuzzy search over a title list (arrow to select, m to toggle, Enter for detail), and a detail panel with an optimistic toggle and toast. Plain HTML/CSS/JS on the arrproxy pirate skin, no CDNs and no build step.
  • Adds the Makefile, Dockerfile, four Woodpecker pipelines on mediamark-ci, and the shared pre-commit config; extends the README with the env var table, keyboard map, and dev run.

go test -race, gofmt, go vet, golangci-lint and pre-commit run --all-files are green, and the binary was smoke-tested against a scratch library (hardlinks verified by inode, 403 without the group header).

Kids-safe media needs a curated `cheeztv` tree, and today promoting a title means somebody SSHing in to run `cp -al` by hand. mediamark makes it a two-keystroke web action for the `akP-mediamark-user` group, with the hardlink mechanics and the traversal guards written down and tested. - Adds `internal/library`: a recursive hardlink sync that promotes a `fafflix/<section>/<title>` directory into `cheeztv/`, idempotent on re-run (relinks only new/changed files), and reports per-title size, file count, marked state and link drift (`needsSync`). Unmark removes only the kids-side directory, so media is never destroyed. - Guards title names as a single clean path element (no separators, `..`, or absolute paths) and re-verifies containment before every filesystem call; table tests cover the traversal cases. - Adds `internal/arr`: a minimal typed sonarr/radarr v3 client with a 60s list cache, matching *arr entries to library directories by path basename, plus a poster proxy that injects the API key server-side and never leaks it into a body, header, or error. - Adds `internal/auth`: server-side Authentik group enforcement on every API call and on the page load itself, so oauth2-proxy is defence in depth rather than the only gate. An empty allow-list fails startup. - Adds `internal/server`: the library JSON API, mark/unmark mutations, the art proxy, `/livez` + `/readyz`, and the embedded SPA with deep-link fallback. - Adds the UI: a two-tile landing page (`m`/`t`), an autofocused fuzzy search over a title list (arrow to select, `m` to toggle, Enter for detail), and a detail panel with an optimistic toggle and toast. Plain HTML/CSS/JS on the arrproxy pirate skin, no CDNs and no build step. - Adds the Makefile, Dockerfile, four Woodpecker pipelines on `mediamark-ci`, and the shared pre-commit config; extends the README with the env var table, keyboard map, and dev run. `go test -race`, `gofmt`, `go vet`, `golangci-lint` and `pre-commit run --all-files` are green, and the binary was smoke-tested against a scratch library (hardlinks verified by inode, 403 without the group header).
unkin-agent added 1 commit 2026-08-29 21:27:28 +10:00
Add the initial mediamark app
ci/woodpecker/pr/test Pipeline failed
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
c129cb99fc
Single Go binary serving the API and an embedded keyboard-first UI for
promoting fafflix titles into the cheeztv kids tree via hardlinks.

- internal/library: hardlink sync, idempotent re-runs, drift reporting,
  strict single-path-element name validation as the traversal guard
- internal/arr: minimal sonarr/radarr v3 client with a 60s list cache and
  a key-brokered poster proxy
- internal/auth: server-side Authentik group enforcement on every route
- internal/server: library JSON API, art proxy, health probes, SPA
- ui: two-tile landing page, fuzzy-filtered title list, detail panel
- Makefile, Dockerfile, .woodpecker pipelines, pre-commit config
unkin-agent added 1 commit 2026-08-29 21:30:37 +10:00
Assert the traversal redirect by property, not by status code
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
b15aa1a340
net/http's mux answers a literal ../ in the path with a redirect whose
exact code differs between Go 1.25 (301) and 1.26 (307), so pinning the
code failed CI while passing locally. Assert what the test is actually
about: the request is redirected rather than handled, and no kids-tree
entry is created.
unkin-agent added 1 commit 2026-08-29 21:42:43 +10:00
Close the review gaps: CI lint, server timeouts, security headers
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
df07085ecb
The initial scaffold left three holes the review caught. CI only checked
gofmt and go vet, so golangci-lint and the pre-commit hooks were advisory
rather than enforced. The HTTP server bounded only the header read, so a
slow or stalled peer could hold a connection indefinitely. And the browser
got no content-security policy at all, leaving the SPA's same-origin
assumption unenforced.

Add golangci-lint and pre-commit hook steps to the existing pre-commit
workflow, mirroring the estate's images so the required context name stays
ci/woodpecker/pr/pre-commit.
Bound the server with ReadTimeout, WriteTimeout, and IdleTimeout, keeping
the write budget generous enough for the poster proxy's streamed responses.
Stamp Content-Security-Policy, X-Content-Type-Options, and Referrer-Policy
onto every response from a single middleware wrapping the root handler.
Assert the headers across the API, UI, assets, probes, and rejections.
Guard the CSP's no-unsafe-inline assumption with a ui test that fails if a
shipped asset grows an inline script, style block, or event handler.
Extend the make pre-commit target to match the widened CI checks.
Author
Member

Review fixes pushed as df07085 (additive commit on benvin/initial-app).

1. CI lint gap — widened the existing .woodpecker/pre-commit.yaml (context name ci/woodpecker/pr/pre-commit unchanged; no new workflow) with two steps alongside the gofmt/vet one:

  • lint: golangci-lint run ./... on golangci/golangci-lint:latest
  • hooks: uvx pre-commit run --all-files on git.unkin.net/unkin/almalinux9-gobuilder:20260606

Both images mirror estate precedent (node-lookup, pdbmux, encapic, clickhouse-tools, agent-tools all use exactly these). Every step keeps its backend_options.kubernetes requests/limits and serviceAccountName: mediamark-ci. make pre-commit was extended to match so the local target still mirrors CI.

2. HTTP server timeoutscmd/mediamark/main.go now sets ReadTimeout: 30s, WriteTimeout: 60s, IdleTimeout: 120s on top of the existing ReadHeaderTimeout. The 60s write budget is sized for the art proxy streaming posters.

3. Security headers — a secureHeaders middleware wraps the root handler, so the API, the SPA shell, static assets, the health probes and the 403 rejections all carry:

  • Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: no-referrer

Verified the SPA works under it unchanged — index.html has no inline <script> body, no <style> block, no style= or on*= attributes, and app.css references only bg-space-pirate.svg (same-origin). data: is in img-src solely for the inline SVG favicon, which is the only data: URI shipped. No policy loosening was needed.

Tests: TestSecurityHeadersOnEveryResponse asserts all three headers across /, /movies, /app.js, /api/library/movies, /livez, /readyz and an unauthorized 403. New ui/embed_test.go guards the no-unsafe-inline assumption so a future asset that grows an inline script/style fails the build rather than silently breaking in the browser.

Resolved by convention, not changed:

  • Dockerfile base images — arrproxy, the estate reference for this pattern, uses the same bases. This is an estate-wide item, tracked separately rather than diverged here.
  • docker.yaml push credentials — anonymous in-cluster push to docker-internal is the documented estate design for artifactapi's local registry, not a missing secret.

Local validation all clean: go test -race ./..., gofmt -l, go vet ./..., golangci-lint run ./... (0 issues), uvx pre-commit run --all-files (8/8 hooks pass).

Review fixes pushed as df07085 (additive commit on benvin/initial-app). **1. CI lint gap** — widened the existing `.woodpecker/pre-commit.yaml` (context name `ci/woodpecker/pr/pre-commit` unchanged; no new workflow) with two steps alongside the gofmt/vet one: - `lint`: `golangci-lint run ./...` on `golangci/golangci-lint:latest` - `hooks`: `uvx pre-commit run --all-files` on `git.unkin.net/unkin/almalinux9-gobuilder:20260606` Both images mirror estate precedent (node-lookup, pdbmux, encapic, clickhouse-tools, agent-tools all use exactly these). Every step keeps its `backend_options.kubernetes` requests/limits and `serviceAccountName: mediamark-ci`. `make pre-commit` was extended to match so the local target still mirrors CI. **2. HTTP server timeouts** — `cmd/mediamark/main.go` now sets `ReadTimeout: 30s`, `WriteTimeout: 60s`, `IdleTimeout: 120s` on top of the existing `ReadHeaderTimeout`. The 60s write budget is sized for the art proxy streaming posters. **3. Security headers** — a `secureHeaders` middleware wraps the root handler, so the API, the SPA shell, static assets, the health probes and the 403 rejections all carry: - `Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'` - `X-Content-Type-Options: nosniff` - `Referrer-Policy: no-referrer` Verified the SPA works under it unchanged — `index.html` has no inline `<script>` body, no `<style>` block, no `style=` or `on*=` attributes, and `app.css` references only `bg-space-pirate.svg` (same-origin). `data:` is in `img-src` solely for the inline SVG favicon, which is the only `data:` URI shipped. No policy loosening was needed. Tests: `TestSecurityHeadersOnEveryResponse` asserts all three headers across `/`, `/movies`, `/app.js`, `/api/library/movies`, `/livez`, `/readyz` and an unauthorized 403. New `ui/embed_test.go` guards the no-unsafe-inline assumption so a future asset that grows an inline script/style fails the build rather than silently breaking in the browser. **Resolved by convention, not changed:** - *Dockerfile base images* — arrproxy, the estate reference for this pattern, uses the same bases. This is an estate-wide item, tracked separately rather than diverged here. - *docker.yaml push credentials* — anonymous in-cluster push to `docker-internal` is the documented estate design for artifactapi's local registry, not a missing secret. Local validation all clean: `go test -race ./...`, `gofmt -l`, `go vet ./...`, `golangci-lint run ./...` (0 issues), `uvx pre-commit run --all-files` (8/8 hooks pass).
benvin merged commit 33a558fcf6 into main 2026-08-29 22:00:12 +10:00
benvin deleted branch benvin/initial-app 2026-08-29 22:00:13 +10:00
Sign in to join this conversation.