AGENTS.md
Rama main · réplica de lectura del repositorio original.
# AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Authoritative Guides
This repo already has detailed `AGENTS.md` files. Read the one closest to the code you are touching before making non-trivial changes:
- `AGENTS.md` (root) — full commands list, monorepo architecture, manifest/data flow, i18n rules.
- `DESIGN.md` (root) — **the normative design system for `apps/web` + `packages/ui`**: UIKit colour tokens, material/blur roles, radius, typography, spring motion, z-index tiers, layout, iconography, and an explicit list of files that violate the system (§13) and must not be copied. Derived from shipped code. The `afilmory-web-design` skill is its fast path.
- `apps/web/AGENTS.md` — thin pointer to `DESIGN.md` plus the SPA's red lines and folder structure.
- `be/AGENTS.md` — the NestJS-style Hono framework: modules, controllers, providers, decorators, DI via `tsyringe`, request-scoped context via `AsyncLocalStorage`. **Stale on package names**: it says `@afilmory/framework`, but the framework was extracted to npm as `@tsuki-hono/{core,common,openapi,event-emitter}`. The concepts still apply; the import paths don't.
- `be/apps/core/AGENTS.md` — backend core service architecture.
- `DEVELOPMENT.md` — self-host quick start and config field reference (`builder.config.ts`, `config.json`).
- `.cursor/rules/*.mdc` — code quality, color (Apple UIKit Tailwind classes), i18n, project description.
- `docs/superpowers/specs/*.md` — dated design specs (`YYYY-MM-DD-<topic>-design.md`) for most non-trivial features; `docs/superpowers/plans/` holds their execution plans. Check for an existing spec before redesigning something.
When those files contradict anything below, prefer them (except where flagged stale above).
## Commands
```bash
# Dev
pnpm dev # SSR wrapper (also serves SPA in dev, Next on :1924)
pnpm --filter web dev # SPA only
pnpm --filter @afilmory/ssr dev # SSR only
pnpm dev:be # Backend (be/apps/core) via nodemon
pnpm --filter @afilmory/dashboard dev # Admin dashboard
pnpm dev:mobile # Build + run the native iOS app (Local variant) in Simulator
pnpm site:dev # Astro marketing site (127.0.0.1:4325)
pnpm docs:dev # Docs site
# Build
pnpm build # Production build (via @afilmory/ssr)
pnpm build:demo # Demo data + web build
pnpm build:manifest # Run photo pipeline → photos-manifest.json + thumbnails (incremental)
pnpm build:manifest -- --force # Full rebuild of photos AND manifest
pnpm build:manifest -- --force-manifest # Regenerate manifest only
pnpm build:manifest -- --force-thumbnails # Regenerate thumbnails only
pnpm build:variant-report # Builder cluster/variant analysis report
# DB (Drizzle; schema lives in be/packages/db, SSR has its own)
pnpm --filter @afilmory/be db:generate / db:migrate / db:studio
pnpm --filter @afilmory/ssr db:generate / db:migrate
# Quality
pnpm lint # eslint --fix (append paths to scope it: `pnpm lint apps/web/src/...`)
pnpm format # prettier (apps, packages, be)
pnpm type-check # recursive `pnpm -r type-check`
pnpm --filter web type-check # type-check just the SPA
# Local infra
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# docker-compose.yml owns postgres/redis; docker-compose.dev.yml adds only rustfs (S3) on :9300/:9301.
# Never redefine `db`/`redis` in the dev file — same project name silently recreates them on different volumes.
```
### Tests
There is no repo-wide test runner; tests are per-package and use two different harnesses.
```bash
pnpm --filter @afilmory/be test # recursive across be/*
pnpm --filter @afilmory/task-queue test # vitest run --coverage
pnpm --filter core exec vitest run <file> # be/apps/core has vitest.config.ts but no `test` script
pnpm --filter @afilmory/viewer-motion test # node:test via `tsx --test src/*.test.ts`
pnpm exec tsx --test apps/web/src/modules/viewer/entry-animation-state.test.ts # web's node:test files
```
Single vitest test: `pnpm --filter core exec vitest run src/guards/auth.guard.spec.ts -t "test name"`.
For everything else, verify via `pnpm build`, `pnpm type-check`, `pnpm lint`, and (for image processing) `pnpm build:manifest`.
Package manager is **pnpm 11** (`packageManager` pin in root `package.json`). Workspace globs in `pnpm-workspace.yaml`: `apps/**/*`, `packages/*`, `packages/*/*`, `be`, `be/packages/*`, `be/apps/*`. Pinned versions live in the pnpm `catalog:` (`tailwindcss`, `typescript`, `zod`, `motion`, …) — prefer `catalog:` over a literal version when adding a shared dep.
## Architecture (Big Picture)
Afilmory is a photo gallery composed of several deployable layers plus a builder pipeline. The same SPA runs in all serving modes; the difference is **who injects `window.__MANIFEST__`**.
```
┌──────────────────────────────────────────────────────────────────────┐
│ packages/builder (CLI pipeline) │
│ sync (S3/GitHub/Eagle/local) → format/EXIF → thumbs/blurhash │
│ → photos-manifest.json (+ optional repo sync) │
└────────────────────────────────┬─────────────────────────────────────┘
│
writes apps/web/src/data/photos-manifest.json
│
┌────────────────────────────────▼─────────────────────────────────────┐
│ apps/web (Vite + React 19 SPA) │
│ PhotoLoader reads window.__MANIFEST__ (data, cameras, lenses) │
│ Standalone fallback: bundled manifest JSON │
└────────────────────────────────▲─────────────────────────────────────┘
│ injects __MANIFEST__
┌────────────────────────┼─────────────────────────┐
│ │
┌───────┴───────────────┐ ┌──────────┴──────────┐
│ apps/ssr (Next.js 15) │ │ be/apps/core (Hono) │
│ static manifest JSON │ │ DB-backed manifest │
│ + dynamic OG / SEO │ │ + dashboard / API │
└───────────────────────┘ └──────────┬──────────┘
│ REST + SSE
┌──────────┴──────────┐
│ apps/mobile (Swift) │
│ native iOS client │
└─────────────────────┘
```
Key implications when changing things:
- **`apps/web` is the only web UI codebase.** It must work in *both* the static/SSR flow and the full backend flow. Don't assume a backend is present — feature-detect on `window.__MANIFEST__` shape.
- **The SPA has two build modes**, switched by env, not by config file: `BUILD_FOR_SERVER_SERVE=1` moves the Vite `base` to `/static/web/` and drops the static-web plugins; `AFILMORY_EMBED_MANIFEST=false` stops inlining the manifest so the server can inject it. `be/apps/core` sets both when it builds the SPA it serves (see `static-web.service.ts`).
- **`packages/data`** owns `PhotoLoader` (the singleton that wraps the manifest into lookup maps). Anything photo-related goes through it. Shared types live in `packages/typing`.
- **`packages/builder`** is the only place that talks to storage providers (S3/B2/GitHub/Eagle/local). It is configured by `builder.config.ts` (infrastructure) which is separate from `site.config.ts` + `config.json` (presentation/branding).
- **`be/`** uses a NestJS-inspired Hono framework (`@tsuki-hono/core`): `@Module`, `@Controller`, `@Get/@Post/...`, `@injectable()`, request context via `AsyncLocalStorage`. Don't reach for raw Hono primitives in feature code — use the framework decorators (see `be/AGENTS.md`).
- **`apps/ssr` is a thin host**, not a backend. Its job is to serve the SPA and provide OG/SEO/manifest injection. Heavy logic belongs in `be/apps/core` or `packages/builder`.
- **Two separate design systems**: `apps/web` + `packages/ui` follow the dark, material-and-blur system specified in root `DESIGN.md`; `be/apps/dashboard` is a linear, data-first admin UI. Don't mix the two.
### Workspace Map
| Path | What it is |
| --- | --- |
| `apps/web` | Vite + React 19 SPA — the gallery UI (the only web UI codebase) |
| `apps/ssr` | Next.js 15 host: serves SPA assets, injects manifest, dynamic OG/SEO |
| `apps/mobile` | Native Swift/UIKit iOS app (see below) |
| `apps/site` | Astro marketing/landing site |
| `apps/docs` | Vite-based documentation site |
| `be/apps/core` | Hono backend: DB-backed manifest, REST API, serves SPA |
| `be/apps/dashboard` | Admin SPA for the backend |
| `be/apps/oauth-gateway` | Standalone OAuth broker (own Dockerfile) |
| `be/packages/{db,redis,task-queue,utils,env}` | Drizzle schema/migrations, Redis client, job queue, shared backend utils, env validation |
| `packages/builder` | Photo pipeline CLI (the only storage-provider consumer) |
| `packages/data` | `PhotoLoader` singleton + bundled manifest |
| `packages/typing` | Shared photo/manifest types |
| `packages/webgl-viewer` | WebGL photo viewer engine |
| `packages/viewer-motion` | Framework-agnostic viewer motion primitives (has real unit tests) |
| `packages/renderer` | `@afilmory/og-renderer` — OG image rendering |
| `packages/sdk` | `@afilmory/sdk` — public SDK + share embed script |
| `packages/{ui,hooks,utils}` | Shared UI elements, React hooks, utilities |
## Mobile App (`apps/mobile`)
**Read `apps/mobile/CLAUDE.md` before touching mobile code** — it covers the architecture in depth. The essentials:
- **iOS only, forever.** Never spend effort on Android compatibility, fallbacks, or testing.
- **It is a pure native Swift/UIKit app.** Expo / React Native was deleted in `66fae030`; there is no Metro, no JS runtime, no CocoaPods, and no npm dependencies. `package.json` only wraps XcodeGen and xcodebuild.
- **`project.yml` (XcodeGen) is the source of truth**; `Afilmory.xcodeproj` is generated output that is nonetheless committed. Source roots are filesystem-synchronized folders (Xcode 16 buildable folders), so adding/deleting `.swift` files needs no regeneration — run `pnpm --filter @afilmory/mobile native:generate` only when `project.yml` changes. Never edit the project through the Xcode UI.
- **iOS 26 deployment target**, Swift 6. iOS 18 is not supported — use iOS 26 APIs directly, no availability guards or fallbacks. Liquid Glass goes through `Afilmory/DesignSystem/AdaptiveGlass.swift`.
- **Two variants from one source tree**: `Afilmory Local` (`app.afilmory.local`, `localhost:1841`, no entitlements) and `Afilmory` (production). Capability differences are gated centrally in `AfilmoryBuildConfiguration`.
- **Run/build/test**: `pnpm --filter @afilmory/mobile ios:local` / `ios:production` / `native:test`. TestFlight goes through `.github/workflows/mobile-testflight.yml`; see `apps/mobile/RELEASE.md`.
- **Simulator automation**: use `axe` (`brew install cameroncooke/axe/axe`) for tap/swipe HID injection + `xcrun simctl io <udid> screenshot`; never control the user's mouse (cliclick/AppleScript). Run the memory guard during Simulator verification — see `apps/mobile/AGENTS.md`.
- **UIGlassEffect gotcha**: `isInteractive = true` swallows touches meant for buttons hosted in the effect view's `contentView` — keep it off for tappable glass controls; floating control clusters need `UIGlassContainerEffect` hosting per-element `UIGlassEffect` views to get real refraction and merge behavior.
## Project Conventions That Matter
- **No feature flags or backwards-compat shims.** App is unreleased — change code in place. (Stated explicitly at the bottom of root `AGENTS.md`.)
- **Pages are thin routing shells.** Real UI/logic lives under `modules/<domain>/**` in `apps/web`.
- **State isolation over prop drilling.** For deep subtrees, lift handlers into colocated Jotai/Zustand stores or contexts; don't thread props through layers.
- **Push state down**, not up. Feature-local stores/providers; switching tabs should unmount unused logic.
- **Tailwind colors must use the Apple UIKit palette** (`text-text-secondary`, `bg-fill`, `bg-material-thick`, `border-accent/20`, …). Full rules in root `DESIGN.md` §2; `.cursor/rules/color.mdc` has the token list. No raw hex or `gray-*`/`zinc-*` ramps for chrome; `color-mix()` in an inline style is only for multi-stop gradients and layered shadows.
- **i18n: flat keys with `.` separators**, no nested objects. Two namespaces under `locales/`: `app/` (SPA) with `en, zh-CN, zh-HK, zh-TW, jp, ko`, and `dashboard/` (en + zh-CN only). Edit `en.json` of the relevant namespace first; ESLint auto-strips keys missing from English in other locales. **Never let a key be both a leaf string and a parent path** (`a.b` cannot coexist with `a.b.c`); the build flattens dots into nested objects and will collide. Use `_one`/`_other` for plurals.
- **The iOS app does not use `locales/`.** It localizes natively through String Catalogs — `apps/mobile/Afilmory/Resources/Localizable.xcstrings` (keys are the English source text, auto-extracted from `String(localized:)` / `Text(_:)` at build time), `ExifValues.xcstrings` (dotted keys for EXIF values looked up dynamically), and `apps/mobile/targets/share/Localizable.xcstrings` for the share extension.
- **No bare global `location`** — an ESLint `no-restricted-globals` rule forbids it (the router instance differs between Electron and browser). Use `useLocation()` or `getReadonlyRoute()`.
- **Decorators are enabled** (`emitDecoratorMetadata`, `experimentalDecorators`) for the backend framework.
- **`motion` / `motion-dom` are pinned** via pnpm `overrides` (currently `12.38.0`) — don't bump them casually. Same for the security-driven `overrides` on `hono`, `vite`, `esbuild`, `qs`, etc.
## Configuration Layering
Two configs, two purposes — don't conflate them:
- `builder.config.ts` (+ `builder.config.default.ts` template) → **infrastructure**: storage provider, concurrency, worker/cluster mode, repo sync plugin. Consumed only by `packages/builder`.
- `site.config.ts` + `config.json` → **presentation**: name, description, author, social, accent color, map provider/style/projection, feed, beian. Consumed by SPA, SSR, and backend for consistent branding.
Environment variables flow through `env.ts` (root — S3 + `PG_CONNECTION_STRING` + `GIT_TOKEN`) and `@afilmory/env` (backend) — both validate via Zod / `@t3-oss/env-core`.