feat: sync Meticulous profiles to coffee.fismen.no #1

Merged
arne merged 13 commits from feat/profile-sync into main 2026-04-26 07:41:04 +02:00
Owner

Summary

Mirrors the master profile list from the Meticulous to coffee.fismen.no so the website can show every profile authored on the device, not just the per-shot snapshots embedded in shots already pulled.

  • Pi forwarder: each existing 10-min tick now also pulls GET /api/v1/profile/list, fetches images for any new/changed display.image URLs, and bulk-POSTs to a new /api/profiles endpoint. Image hashes are tracked in profile_image_hashes.json (atomic rename) so unchanged images aren't refetched. Failure-isolated from shot sync.
  • Server: new profiles table (UUID PK, name, author, last_changed, accent_color, image_hash, image_content_type, image_bytes, profile_json, updated_at) populated by an authenticated bulk-upsert endpoint. image_content_type is whitelisted to jpeg/png/webp at ingest. Per-row atomic, idempotent across retries.
  • Schema: v1 → v2 migration in a single transaction — adds the profiles table, adds shots.profile_id column + index, idempotent backfill from JSON_EXTRACT(profile_json, '$.id') (with JSON_VALID guard).
  • UI: /profiles index (cards by shot-count desc), /profiles/{id} detail (hero image, accent stripe, variables list with type-derived units, shot list), /profiles/{id}/image/{hash}.jpeg (immutable cache because hash is in the URL — 404 on hash mismatch). ?profile_id= filters the existing shot index with a chip. Profile name on shot detail now links back to the canonical profile.

Spec: docs/superpowers/specs/2026-04-25-profile-sync-design.md. Plan: docs/superpowers/plans/2026-04-25-profile-sync.md.

Already deployed and verified end-to-end on coffee.fismen.no (5 profiles synced, image at `f8790296-…/image/cd02de48b620242f5c1b903cebdf03f6.jpeg` returns 29,692 bytes image/jpeg, shot detail links land on the right profile page).

Test Plan

  • `go test ./...` (6 packages green)
  • `python3 -m unittest discover tests` (9/9 pass, including 4 new `TestProfileSync` cases)
  • Schema migration verified on production DB (40 existing shots backfilled with their snapshot's profile id)
  • End-to-end: forwarder restart → `profile sync: 5 profiles` in journal → all 5 cards rendered on /profiles → image endpoint serves bytes
  • Negative path: wrong image hash returns 404, bad image_content_type returns 400, older last_changed skips upsert

🤖 Generated with Claude Code

## Summary Mirrors the master profile list from the Meticulous to coffee.fismen.no so the website can show every profile authored on the device, not just the per-shot snapshots embedded in shots already pulled. - **Pi forwarder:** each existing 10-min tick now also pulls `GET /api/v1/profile/list`, fetches images for any new/changed `display.image` URLs, and bulk-POSTs to a new `/api/profiles` endpoint. Image hashes are tracked in `profile_image_hashes.json` (atomic rename) so unchanged images aren't refetched. Failure-isolated from shot sync. - **Server:** new `profiles` table (UUID PK, name, author, last_changed, accent_color, image_hash, image_content_type, image_bytes, profile_json, updated_at) populated by an authenticated bulk-upsert endpoint. `image_content_type` is whitelisted to jpeg/png/webp at ingest. Per-row atomic, idempotent across retries. - **Schema:** v1 → v2 migration in a single transaction — adds the `profiles` table, adds `shots.profile_id` column + index, idempotent backfill from `JSON_EXTRACT(profile_json, '$.id')` (with `JSON_VALID` guard). - **UI:** `/profiles` index (cards by shot-count desc), `/profiles/{id}` detail (hero image, accent stripe, variables list with type-derived units, shot list), `/profiles/{id}/image/{hash}.jpeg` (immutable cache because hash is in the URL — 404 on hash mismatch). `?profile_id=` filters the existing shot index with a chip. Profile name on shot detail now links back to the canonical profile. Spec: `docs/superpowers/specs/2026-04-25-profile-sync-design.md`. Plan: `docs/superpowers/plans/2026-04-25-profile-sync.md`. Already deployed and verified end-to-end on coffee.fismen.no (5 profiles synced, image at \`f8790296-…/image/cd02de48b620242f5c1b903cebdf03f6.jpeg\` returns 29,692 bytes image/jpeg, shot detail links land on the right profile page). ## Test Plan - [x] \`go test ./...\` (6 packages green) - [x] \`python3 -m unittest discover tests\` (9/9 pass, including 4 new \`TestProfileSync\` cases) - [x] Schema migration verified on production DB (40 existing shots backfilled with their snapshot's profile id) - [x] End-to-end: forwarder restart → \`profile sync: 5 profiles\` in journal → all 5 cards rendered on /profiles → image endpoint serves bytes - [x] Negative path: wrong image hash returns 404, bad image_content_type returns 400, older last_changed skips upsert 🤖 Generated with [Claude Code](https://claude.com/claude-code)
arne added 13 commits 2026-04-26 06:26:10 +02:00
Profiles currently exist on the website only as per-shot snapshots in
shots.profile_json. This spec adds a master profiles table populated by
the Pi forwarder (via /api/v1/profile/list and the device's image
endpoint), three new UI surfaces (/profiles index, /profiles/{id} detail,
profile filter on shot index), and a backfill of profile_id onto
existing shots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan covers schema migration, profile store layer, ingest endpoint,
populating shots.profile_id, web handlers + templates for /profiles
and /profiles/{id}, image serving with hash-in-URL caching, the
forwarder profile-sync job, and the build-and-deploy steps for both
sides. Spec adjusted in lockstep so the image URL embeds the hash —
otherwise the immutable Cache-Control header would serve stale bytes
after a profile-image swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Should have been updated alongside f8d4096 but slipped through —
fixing the baseline before starting profile-sync work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap migrate() in a single transaction so partial failures (crash
between ALTER TABLE, version bump, and backfill) leave the DB
consistent, and so a check-then-ALTER race between two coffee-web
instances can't surface as "duplicate column name". Add JSON_VALID
guard to the backfill UPDATE to defend against non-JSON in
profile_json (manual edits, future writers) — without it JSON_EXTRACT
would error and abort the whole UPDATE, blocking server startup.
Adds a test exercising missing id, malformed JSON, and preserved
existing profile_id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final-review followup: untrusted image_content_type from the forwarder
was echoed verbatim by the image endpoint, so a hostile or buggy client
could push text/html and turn /profiles/{id}/image/{hash}.jpeg into a
same-origin HTML sink. Whitelist to image/jpeg, image/png, image/webp
at ingest; reject everything else with 400. Spec text updated to match
both the new check and the (correct) per-row transactional behaviour
the implementation already had.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arne merged commit 4adf9b7bbd into main 2026-04-26 07:41:04 +02:00
Sign in to join this conversation.
No reviewers
No labels
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
arne/coffee!1
No description provided.