diff --git a/AGENTS.md b/AGENTS.md index 03bf1d6..be32fbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,19 +9,61 @@ ## Architecture -- **Vue 3 + Vite + Tailwind CSS + Vue Router + Pinia + Supabase + AMap (高德地图)** +- **Vue 3 + Vite + Tailwind CSS + Vue Router + Pinia + Supabase + AMap (高德地图) + Naive UI** - Path alias `@/` → `src/` (configured in both `vite.config.ts` and `tsconfig.app.json`) - Supabase client singleton at `src/lib/supabase.ts` — throws at import if env vars missing -- AMap loaded lazily via `src/lib/amap.ts` with hand-rolled type declarations in `src/types/amap.d.ts` +- AMap loaded lazily via `src/lib/amap.ts` with type declarations in `src/types/amap.d.ts` - UI language is Chinese (zh-CN) +- All DB operations are direct `supabase.from(...)` calls from the browser — security depends on Supabase RLS policies -## Auth Flow (non-obvious) +## Directory Map -- **`main.ts` races on pathname**: `/auth/confirm` mounts the app immediately without calling `authStore.initialize()`. All other routes wait for auth initialization before mounting. If you add new routes that need to bypass auth init, add them to the `if` check in `main.ts`. -- **AuthConfirmView uses a separate Supabase client**: It creates a temporary `createClient()` to call `setSession()` then `signOut()`, so the main auth store never sees a logged-in state. Do NOT consolidate with the singleton — the singleton isn't initialized at confirmation time. -- **Login sets `user.value` explicitly**: `login()` extracts `data.user` from `signInWithPassword` response and assigns it to the store directly, rather than relying on `onAuthStateChange`. -- **Profile is auto-created by DB trigger** (`handle_new_user` on `auth.users`), not by the frontend. The trigger uses `SECURITY DEFINER` with `SET search_path = public`. +| Path | Purpose | +| --- | --- | +| `src/lib/` | Singletons and utilities: supabase, amap, canvas patch, cloudTypes constants, cloudBadges canvas renderer, SEO meta builder | +| `src/stores/` | Pinia stores: auth, clouds (cloud_types cache), encyclopedia (collection + unlock tracking), profile (user pages + cloud CRUD) | +| `src/composables/` | Vue composables: `useUpload` (batch upload with thumbnail generation, EXIF extraction, badge unlocking) | +| `src/components/cloud/` | Cloud-related modals and widgets: ImageDetailModal, CloudEditModal, MapPickerModal, QuickUploadModal, MiniLocationMap | +| `src/components/layout/` | AppHeader (top nav bar with auth state) | +| `src/components/profile/` | ContributionHeatmap | +| `src/views/` | Route-level page components (see Routes below) | +| `src/types/` | TypeScript types: database models (`database.ts`), AMap declarations (`amap.d.ts`), router meta (`router.d.ts`) | + +## Routes + +| Path | View | Auth Required | +| --- | --- | :---: | +| `/` | MapView | No | +| `/login`, `/register`, `/auth/confirm`, `/auth/reset-password` | Auth views | No | +| `/upload` | UploadView | Yes | +| `/encyclopedia` | EncyclopediaView | No | +| `/encyclopedia/:id` | CloudTypeView | No | +| `/gallery` | GalleryView | No | +| `/community` | CommunityView | No | +| `/profile` | ProfileView (own) | Yes | +| `/profile/settings` | ProfileSettingsView | Yes | +| `/profile/:id` | ProfileView (public) | No | +| `/admin` | AdminView | Yes (admin) | +| `/403` | ForbiddenView | No | +| `/:pathMatch(.*)*` | NotFoundView | No | + +- Route guards in `router/index.ts`: `requiresAuth` redirects to `/login`, `requiresAdmin` redirects to `/403` +- SEO meta tags applied per-route via `lib/seo.ts` in `router.afterEach` + +## Auth Flow + +- **`main.ts` initializes auth before mounting**: `authStore.initialize()` is called, which calls `getSession()` and subscribes to `onAuthStateChange`. The app only mounts after the initial session check completes. +- **Login sets `user.value` explicitly**: `login()` extracts `data.user` from `signInWithPassword` and assigns it to the store directly, rather than relying on `onAuthStateChange`. +- **Profile is auto-created by DB trigger** (`handle_new_user` on `auth.users`), not by the frontend. - Auth error messages are translated to Chinese in the store. +- `register()` checks username uniqueness before calling `signUp()` — username is passed via `options.data.username`. + +## Stores + +- **auth** — User session, profile, login/register/logout, username/password update, password reset. `initialize()` must be called before app mounts. +- **clouds** — Simple cache of `cloud_types` table. Fetched once, shared across views. +- **encyclopedia** — Cloud types + user's collection (unlock state). Tracks `unlockPercent` for progress display. Depends on `authStore`. +- **profile** — User profile pages. Fetches profile + cloud list per-user. Supports update/delete/visibility-toggle with optimistic cache patching. `deleteClouds` also removes from Supabase Storage. ## Supabase @@ -29,6 +71,56 @@ - All tables have RLS enabled. Check `plan.md` section 10 for the full schema and RLS policies. - Storage bucket `clouds` is public read, authenticated upload. - Profile `role` field (`user`/`admin`) controls admin access — checked in route guard, not in JWT metadata. +- `user_collections` tracks encyclopedia unlocks with `first_cloud_id` FK to `clouds`. + +## Gallery Pagination + +- Page-based navigation (50 items/page), not infinite scroll. +- `resolveSearchFilters()` pre-fetches user IDs or cloud type IDs before the main query. +- `buildFilteredQuery()` returns a chainable query builder; `loadPage()` forks it into a `count` query and a `data` query that run in parallel via `Promise.all`. +- Search debounced at 250ms. + +## Map Timeline + +- Realtime mode: slider selects a minute of today, shows clouds captured within 2 hours before that time. Marker opacity decays with age. +- Archive mode: browse clouds by day or month. Toggling timeline controls closed auto-returns to realtime. +- Slider resets to current time each time the controls panel is opened. + +## Upload Flow + +- `useUpload` composable handles: file selection (drag/drop/click), client-side thumbnail generation (JPEG, max 640px edge, 0.72 quality), EXIF date extraction, coordinate blurring (2 decimal places), Supabase Storage upload (original + thumbnail), DB insert with `status: 'pending'`, badge unlock detection via `user_collections` upsert. +- `UploadView` is the full-page batch uploader. `QuickUploadModal` is a single-image shortcut from the map page. + +## Build & Deploy + +- **Vercel** with `vercel.json`: SPA rewrites, security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy). No CSP or HSTS configured. +- **SEO plugin** in `vite.config.ts`: generates `robots.txt` and `sitemap.xml` at build time. Disallows `/admin`, `/auth/`, `/login`, `/register`, `/upload`, `/profile/settings`. +- **`lib/canvas.ts`**: patches `HTMLCanvasElement.getContext('2d')` to always pass `willReadFrequently: true` — needed for the badge card renderer in `lib/cloudBadges.ts`. + +## Environment Variables + +Required (app won't start without): + +- `VITE_SUPABASE_URL` +- `VITE_SUPABASE_PUBLISHABLE_KEY` + +Required for map features: + +- `VITE_AMAP_KEY` + +Optional: + +- `VITE_SITE_URL` — canonical URL for SEO meta and sitemap (falls back to Vercel env or hardcoded default) + +Future (not in `.env`): + +- `OPENAI_API_KEY`, `OPENWEATHERMAP_API_KEY` + +## Naive UI + +- Used for modals, buttons, inputs, tags, progress bars, alerts, dropdowns, empty states, skeletons, and message toasts. +- No SSR — pure client-side rendering. +- Custom CSS overrides via scoped `