/sub-packages/photo-manager/CLAUDE.md
CLAUDE.md at /sub-packages/photo-manager/CLAUDE.md
Path: sub-packages/photo-manager/CLAUDE.md
photo-manager — agent notes
Tauri v2 + Vite + React + TypeScript desktop admin for the Photo Uploader dataset (epic #1651, super-epic #1649). Local-only — never deployed, never exposed publicly. Auth is a baked-in admin bearer token, not a UI login.
Quick start
# Vite frontend only (browser at http://localhost:14152):
pnpm --filter photo-manager dev
# Vite + Tauri desktop window:
cd sub-packages/photo-manager/src-tauri
cargo tauri dev
# From repo root:
pnpm photo-manager:dev # Vite only
pnpm photo-manager:tauri:dev # Vite + desktop
pnpm photo-manager:tauri:build # macOS .app bundle
Port
Dev server: 14152 (adjacent to image-stash-viewer
). Strict-port — if:14152 is taken, Vite fails fast instead of rolling forward.
Architecture
HTTP-only Tauri shell. The Rust side does not define any commands; the
webview talks directly to the photo-uploader-worker admin endpoints
(/admin/photos, PATCH /admin/photos/{slug}, DELETE /admin/photos/{slug})
over fetch. See sub-packages/photo-uploader-worker/src/admin-*-handler.ts
(admin-auth-handler.ts, admin-list-handler.ts, admin-update-handler.ts,
admin-delete-handler.ts) for the server contract.
sub-packages/photo-manager/
index.html # Vite entry
package.json # scripts: dev / build / typecheck / test / tauri:*
tsconfig.json
vite.config.ts # host:localhost port:14152 strictPort
vitest.config.ts # node env, tests in tests/**
postcss.config.js
tailwind.config.js # re-uses the design-system tokens
.env.example # documents VITE_PHOTO_ADMIN_TOKEN + VITE_PHOTO_API_BASE_URL
src/
index.tsx # React bootstrap
app.tsx # Top-level shell + state
styles.css # design-system import + Tailwind
components/
photo-grid.tsx # Grid + toolbar (text/date/product filters,
# "only unassigned" toggle, sort)
edit-dialog.tsx # description / hashtags / products / takenAt
delete-dialog.tsx # Two-step confirm + R2 cleanup
toast.tsx # Single-slot notification
utils/
api-client.ts # listPhotos / updatePhoto / deletePhoto
api-config.ts # Reads VITE_* envs at build time
photo-types.ts # PhotoRecord + PhotoUpdatePartial
photo-filters.ts # Pure filter / sort
products-data.ts # Vite relative-path import of allProducts
format.ts # Date / text helpers
src-tauri/
Cargo.toml # No extra crates beyond default Tauri
build.rs # tauri_build::build()
tauri.conf.json # window title "Photo Manager", devUrl :14152
capabilities/default.json # core:default only — no IPC commands needed
icons/ # Placeholder PNGs
src/
main.rs # entrypoint
lib.rs # tauri::Builder::default().run(...)
tests/
api-client.test.ts # bearer header + null-vs-undefined PATCH body
photo-filters.test.ts # unassigned toggle + filter / sort gates
products-data.test.ts # Vite relative-path bridge smoke test
Auth model — baked admin bearer token
This app intentionally has no UI login. Both env vars are baked into the JS bundle at build time and shipped with the .app:
| Env var | Purpose |
|---|---|
VITE_PHOTO_ADMIN_TOKEN | Bearer token sent on every admin request (matches Worker PHOTO_ADMIN_TOKEN). |
VITE_PHOTO_API_BASE_URL | Worker base URL (e.g. https://photo-uploader-preview.workers.dev). |
Both are read in src/utils/api-config.ts. If either is missing at build
time, the app renders a “Photo Manager not configured” panel instead of
trying to fetch.
Setup runbook (per build machine)
-
Generate a token:
openssl rand -hex 32 -
Set it as the Worker secret on BOTH envs (the Photo Uploader Worker stores
PHOTO_ADMIN_TOKENas a runtime secret per env — seesub-packages/photo-uploader-worker/CLAUDE.md):wrangler secret put PHOTO_ADMIN_TOKEN --env preview wrangler secret put PHOTO_ADMIN_TOKEN --env production -
Paste the same value into a new
sub-packages/photo-manager/.env.local(see.env.examplefor the shape):VITE_PHOTO_ADMIN_TOKEN=<the value from step 1> VITE_PHOTO_API_BASE_URL=https://photo-uploader-preview.<account>.workers.devBuild a separate
.env.localfor production-Worker builds — e.g. swapVITE_PHOTO_API_BASE_URLto the production Worker URL beforetauri:build. -
Rebuild the Tauri app:
pnpm --filter photo-manager run tauri:build
The resulting .app bundle has the token embedded in its JavaScript. Treat
the bundle itself as a secret — don’t share it.
Rotation procedure
- Generate a new token:
openssl rand -hex 32. wrangler secret put PHOTO_ADMIN_TOKENfor--env previewand--env production.- Update
.env.localwith the new value. pnpm --filter photo-manager run tauri:build, reinstall on operator machines.
There is no dual-token acceptance path on the Worker — old tokens are
rejected the moment the secret rotates, so plan rotation during a quiet
window and have the rebuilt app ready before flipping production. Preview
can lead so the rebuilt app smoke-tests against --env preview first.
API client contract — three-state PATCH semantics
The Worker’s updatePhotoFields discriminates “leave the column untouched”
from “clear the column to NULL” by inspecting whether the JSON key exists at
all on the body, distinct from whether its value is null. The TS client in
src/utils/api-client.ts preserves this:
partial.field === undefined→ key OMITTED from wire body (column untouched)partial.field === null→ key PRESENT with literal JSON null (clear)partial.field === <value>→ key PRESENT with that value (write)
JSON.stringify already drops undefined-valued keys and emits null
literally, so the client just has to avoid pre-merging the partial with the
previous record (which would inject unintended fields).
This is covered by tests/api-client.test.ts.
Testing
pnpm --filter photo-manager test # vitest, runs in node env
pnpm --filter photo-manager typecheck # tsc -b --noEmit
Vitest only — no Playwright, no port-binding tests, no full Tauri build
during CI. UI verification of the running app is a manager-dispatched
concern; this child agent does not run pnpm dev / cargo tauri dev.
Commit scope
Use [photo-manager] for changes inside this sub-package. Cross-cutting
config (root package.json scripts, root CLAUDE.md updates) goes under
[misc].
Known gap — Tailwind class audit (follow-up)
The project-wide rule (root CLAUDE.md “Code Style”) prohibits numeric
Tailwind classes such as p-2, h-24, z-50, bg-neutral-300,
text-red-700, etc. — semantic tokens (p-vgap-*, gap-vgap-*) are
required.
This sub-package consumes @takazudo-modular/design-system, which uses
the “Approach B” theme (tokens.css → theme.css, no Tailwind defaults).
Numeric / palette utilities are therefore not backed by tokens and may
silently no-op at css-build time.
The current components (photo-grid.tsx, edit-dialog.tsx,
delete-dialog.tsx, toast.tsx, app.tsx) still carry numeric and
palette classes (h-24, w-24, z-50, z-[60], max-w-3xl,
max-w-2xl, max-h-40, min-w-[8rem], min-w-[12rem],
bg-neutral-*, text-neutral-*, border-red-300, etc.), plus an
inline gridTemplateColumns style in photo-grid.tsx.
Sweeping these out should be a single follow-up PR scoped to UI correctness (visual verification via the desktop window or a browser session is needed because typecheck/test won’t catch missing utilities). Tracked as a separate task to keep deep-review fix PRs small.