> For the complete documentation index, see [llms.txt](https://summerain-1.gitbook.io/summerain/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://summerain-1.gitbook.io/summerain/archived-design-records/frontend-architecture/02-architecture.md).

# 02 - Architecture and Infrastructure

> \[!WARNING] **Archived design record.** This page predates the completed V2 frontend and may contain obsolete versions, paths, or implementation status.

> Part of: [Frontend Architecture Design (Index)](/summerain/archived-design-records/frontend-architecture.md)

## Directory Structure

React source lives in a new repository-root `frontend/` directory, and build artifacts are emitted to `backend/web/`.

```
frontend/
├─ index.html
├─ vite.config.ts          # outDir: ../backend/web; base: '/'; dev proxy /api+/i -> :8080; HTTPS development
├─ src/styles.css          # Tailwind v4 entry: @import "tailwindcss"; @theme {...coffee palette}; :root/.dark variables
├─ tsconfig.json           # strict; target/lib are defined in the “TypeScript / ES Baseline” section of 01-overview.md
├─ components.json         # shadcn configuration (Tailwind v4 mode)
├─ package.json            # version is the release SemVer referenced by the SRI manifest
└─ src/
   ├─ main.tsx             # Mount QueryClientProvider + Router + ThemeProvider + Toaster
   ├─ App.tsx              # Route tree + AuthGuard/AdminGuard + Layout
   ├─ config/
   │  └─ constants.ts      # Centralized constants (see 07-production-standards.md; eliminates magic values)
   ├─ i18n/
   │  ├─ index.ts          # i18next initialization (default en-US)
   │  └─ locales/
   │     ├─ en-US.json     # Default English copy
   │     ├─ zh-CN.json     # Simplified Chinese copy
   │     └─ ja-JP.json     # Japanese copy
   ├─ lib/
   │  ├─ api.ts            # Core fetch wrapper
   │  ├─ csrf.ts           # Read the __Host-csrf_token cookie
   │  ├─ query-client.ts   # QueryClient configuration
   │  └─ utils.ts          # cn() / formatSize / timeAgo / formatNumber
   ├─ store/
   │  ├─ theme-store.ts    # zustand persist (light/dark)
   │  └─ auth-store.ts     # Current user (hydrate / clear)
   ├─ components/
   │  ├─ ui/               # Generated by shadcn (button/dialog/table/form/toast/...)
   │  └─ layout/           # Navbar / Footer / ThemeToggle / NotificationBell
   ├─ routes/
   │  └─ lazy.tsx          # React.lazy route-level entrypoints
   └─ features/
      ├─ auth/             # api.ts hooks.ts pages(Login, Register) components/
      ├─ captcha/          # api.ts(usePublicConfig) hooks.ts(useCaptcha) components(Captcha branches by provider)
      ├─ images/           # api.ts hooks.ts pages(List, Detail, Upload) components/
      ├─ user/             # api.ts hooks.ts pages(Profile) components/
      ├─ notifications/    # api.ts hooks.ts components(Dropdown)
      └─ admin/            # api.ts hooks.ts pages(Users, Stats, Configs) components/
```

Each `features/<domain>/` is self-contained: `api.ts` for domain endpoints, `hooks.ts` for Query/Mutation hooks, plus `pages/` and `components/`. Cross-domain shared code belongs in the top-level `components/` and `lib/` directories.

## Core Infrastructure

### `lib/api.ts`: The Only Request Gateway

* Set `baseUrl = '/api/v1'` and use `credentials: 'include'` for every request.
* Automatically add the `X-CSRF-Token` header to non-GET requests, reading its value from the `__Host-csrf_token` cookie through `lib/csrf.ts`.
* Unwrap the standard envelope `{ code, message, data }`: return `data` when `code === 0`; otherwise, `throw new ApiError(code, message)`.
* **Centralize error-code handling**, branching on `code` rather than only on HTTP status:
  * **401 / 4010 / 4011** (unauthenticated/session expired): **by default**, clear `auth-store` and navigate to `/login`. Support the per-request `{ skipAuthRedirect: true }` exemption used by the `/auth/me` probe, where 401 means anonymous and must not redirect. This prevents anonymous users from being incorrectly removed from public pages.
  * **4030** (account disabled): log out, navigate to `/login`, and show “This account has been disabled.” A disabled user receives the same code when attempting to log in, and the login form maps it to the same copy.
  * **429 / 2008 / 4029 / 2090** (rate limited): do not retry automatically, which would amplify load. Throw `ApiError` so the UI can show “Too many operations. Please try again later.” If the response includes `Retry-After`, show a corresponding countdown.
  * **4032 / 4033** (admin endpoint restricted to the web/identity misuse): this is an incorrect calling pattern. Notify the user and report it, but do not log out.
* **Localized error copy:** map each `code` to the `errors.<code>` key in the active locale. Use the backend `message` only as fallback for an unknown `code`; components never display it directly.
* Expose convenience methods `api.get`, `api.post`, `api.patch`, and `api.del`, plus `api.upload(formData)`.
* **Do not add a field-mapping layer.** Components consume backend snake\_case fields such as `created_at`, `view_count`, and `storage_used` directly, preserving one data shape.

### Authentication Flow

* On startup, the application calls `GET /auth/me`. On success, write the user to `auth-store`; on **401, remain signed out**. This request uses `skipAuthRedirect: true`, so 401 identifies an anonymous visitor without triggering global navigation, unlike a 401 caused by a session expiring during use.
* At the same time, probe unauthenticated `GET /public/config` for `captcha_provider` and its client key. This determines whether login/registration loads CAPTCHA (see [03](/summerain/archived-design-records/frontend-architecture/03-features.md#pluggable-captcha-administrator-selected-default-none)). With `provider=none`, no external script is loaded.
* **Image rendering under `/i/`:** an owner/admin viewing their own/any private image is authorized automatically by the same-origin session, so `<img src="/i/<link>">` needs no token. A third party uses the share URL `/i/<link>?token=<token>`. Private-image responses use `no-store`, and the frontend does not cache them.
* `AuthGuard` wraps routes that require authentication; `AdminGuard` additionally checks `auth.user.role === 'admin'`.
* **AdminGuard downgrade recovery:** the client `role` snapshot may become stale if an administrator is demoted. If a request in the admin area returns **4030/4032**, call `refreshUser()` to reload `/auth/me`, update the snapshot, and redirect out of `/admin/*`; if the user no longer has permission, return to `/dashboard`.
* After login succeeds, the browser stores the cookies automatically. Invalidate the relevant queries and navigate to **`/dashboard`**.
* An authenticated user visiting the public destination `/` is redirected to `/dashboard` with an in-route `<Navigate>` and no extra request.
* A 401 from any **non-probe** request, indicating an expired or terminated session, is handled centrally by `lib/api.ts`: log out and navigate to `/login`.
* **Logout:** call `POST /auth/logout` with CSRF. On success, clear cached data as described below and hard-refresh to `/`.

### Session-Switch Data Cleanup (Security)

Prevent **data residue across users**. For example, after an administrator logs out and an ordinary user signs in within the same tab, that user must not read the previous session's admin data from memory. Cached JavaScript chunks themselves are harmless because the frontend is not a security boundary; backend `RequireAdmin` with 403 prevents unauthorized data retrieval. The concern here is **in-memory query results**.

* **Logout:** call `queryClient.clear()` to remove the entire Query cache, clear `auth-store`, and use `window.location.assign('/')` for a **hard refresh**. The refresh destroys all memory, including QueryClient, React state, and `auth-store`, so the next login starts a fresh page session.
* **Successful login:** defensively call `queryClient.clear()` again, then navigate to `/dashboard`.
* **Do not persist `auth-store`** (see Client State below). localStorage retains no user identity; the first page always hydrates from `GET /auth/me`, with the server as the sole source of truth.
* **Backend fallback:** `/admin/*` uses `RequireAdmin` to check `role` and returns 403 to ordinary users. Frontend cache clearing prevents residue, while backend 403 prevents unauthorized retrieval; both layers apply.
* A hard refresh does not redownload the bundle: every user receives the same chunks, which are served from the HTTP cache, so the cost is negligible.

### TanStack Query Conventions

* Key conventions:
  * `['images', { visibility, search }]` (list)
  * `['images', id]` (detail; the response includes the private image's current token in `access_token`, with no separate token endpoint/key; see [API.md §5.3](/summerain/user-and-operations/api.md))
  * `['admin', 'users', { page }]`
  * `['admin', 'stats']` and `['admin', 'configs']`
  * `['notifications']`, `['profile']`, and `['public-config']` (CAPTCHA provider)
* Image lists use `useInfiniteQuery` with `getNextPageParam: last => last.has_more ? last.next_cursor : undefined`. **Use `has_more` as the termination condition**; `next_cursor` is only the cursor. If the values conflict, trust `has_more`.
* After a successful `useMutation`, call `queryClient.invalidateQueries` for the relevant key. For example, upload/delete/visibility changes invalidate `['images']`.
* Default `refetchOnWindowFocus: true`, so notification counts and statistics refresh automatically.

### Client State (zustand)

* `theme-store`: persist `light` or `dark` under the **localStorage key `ic_theme`**, centralized in `constants.ts`. Add or remove the `.dark` class on `<html>` when the value changes.
* **Theme-switch animation** (see [MASTER §9](/summerain/archived-design-records/frontend-architecture/master.md)): prefer `document.startViewTransition` when available, using a `::view-transition-new(root)` circular clip-path reveal. Otherwise, fall back to `.theme-mask` with a WAAPI `transform:scale` animation. With `prefers-reduced-motion`, switch without animation. Before startup, a pre-paint inline script applies the stored theme to prevent a flash.
* `auth-store`: store only a snapshot of the current user for initial hydration. **Do not persist it**; it exists only in memory, and the first page always rehydrates it with `GET /auth/me`. Do not cache other business data there.
* **Snapshot refresh:** expose `refreshUser()` to call `/auth/me` again and write the result to the store. Trigger it after successful login; after image upload, deletion, or visibility changes, which affect `storage_used` and `image_count`; and during AdminGuard downgrade recovery. This scope has no avatar/profile-edit endpoints, so there are no other refresh sources. Do not add `/auth/me` to the Query cache, which would create two sources alongside the store; call it explicitly as needed.

## Routing and Code Splitting

* Use `BrowserRouter`. Backend `NoRoute` falls back to `index.html`, so deep-link refreshes work. Set `base: '/'`; see [05 Build and Deployment](/summerain/archived-design-records/frontend-architecture/05-build-and-deploy.md).
* Route table:
  * Public: `/` (landing page), `/login`, `/register`
  * Protected by `AuthGuard`: `/dashboard`, `/images`, `/images/:id`, `/upload`, `/profile`
  * Protected by `AuthGuard` + `AdminGuard`: `/admin`, `/admin/users`, `/admin/configs`
  * Authenticated visits to `/` -> `<Navigate to="/dashboard">`; successful login -> `/dashboard`
* `/dashboard` is the console destination and renders conditionally by `auth.user.role`: a common area with personal statistics, quota, and recent images, plus system-overview cards and an “Open administration” entry point for `admin` only. The admin area is additionally lazy-loaded through `React.lazy`, so ordinary users do not download its code.
* Split code by feature/route with `React.lazy`. Use `<Suspense>` and a global `<Spinner>` fallback. The [07 SRI runtime guard](/summerain/archived-design-records/frontend-architecture/07-production-standards.md) covers dynamic-chunk integrity.

***

<- [01 Overview](/summerain/archived-design-records/frontend-architecture/01-overview.md) · [Index](/summerain/archived-design-records/frontend-architecture.md) · Next: [03 Features and Pages](/summerain/archived-design-records/frontend-architecture/03-features.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://summerain-1.gitbook.io/summerain/archived-design-records/frontend-architecture/02-architecture.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
