Takazudo Modular Docs

Type to search...

to open search from anywhere

/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 varPurpose
VITE_PHOTO_ADMIN_TOKENBearer token sent on every admin request (matches Worker PHOTO_ADMIN_TOKEN).
VITE_PHOTO_API_BASE_URLWorker 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)

  1. Generate a token:

    openssl rand -hex 32
  2. Set it as the Worker secret on BOTH envs (the Photo Uploader Worker stores PHOTO_ADMIN_TOKEN as a runtime secret per env — see sub-packages/photo-uploader-worker/CLAUDE.md):

    wrangler secret put PHOTO_ADMIN_TOKEN --env preview
    wrangler secret put PHOTO_ADMIN_TOKEN --env production
  3. Paste the same value into a new sub-packages/photo-manager/.env.local (see .env.example for the shape):

    VITE_PHOTO_ADMIN_TOKEN=<the value from step 1>
    VITE_PHOTO_API_BASE_URL=https://photo-uploader-preview.<account>.workers.dev

    Build a separate .env.local for production-Worker builds — e.g. swap VITE_PHOTO_API_BASE_URL to the production Worker URL before tauri:build.

  4. 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

  1. Generate a new token: openssl rand -hex 32.
  2. wrangler secret put PHOTO_ADMIN_TOKEN for --env preview and --env production.
  3. Update .env.local with the new value.
  4. 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.

References

  • Epic: #1651
  • Super-epic: #1649
  • Backend admin handlers: sub-packages/photo-uploader-worker/src/admin-*-handler.ts (auth, list, update, delete variants)
  • Worker secrets runbook: sub-packages/photo-uploader-worker/CLAUDE.md