# Module: `src/features/login/LoginPage.tsx`

> **Source**: `src/features/login/LoginPage.tsx` (95 lines)
> **Topo batch**: B4 (depends on B3: `auth/AuthContext`)

## Purpose

The single public route of the SPA. Collects email + password, calls
`AuthContext.login(...)`, and on success runs a four-step "unlock"
animation (download key → decrypting → starting services → ready)
before navigating to `/flights`. Replaces the WPF `LoginWindow.xaml`
including its multi-step progress UI (`_docs/legacy/wpf-era.md` §3 / §4).

## Public interface

```ts
export default function LoginPage(): JSX.Element
```

No props. Reads `useAuth().login` and `react-router-dom`'s `useNavigate`.

## Internal logic

- **State machine** — `step: UnlockStep` cycles through:
  `idle → authenticating → downloadingKey → decrypting → startingServices → ready`,
  with `idle` also reachable on error from `authenticating`.

  ```ts
  type UnlockStep = 'idle' | 'authenticating' | 'downloadingKey'
                  | 'decrypting' | 'startingServices' | 'ready'
  ```

- **Local state**:
  - `email: string`, `password: string` — controlled inputs.
  - `error: string` — empty unless the previous attempt failed.
  - `step: UnlockStep` — drives both the form/spinner branch and the
    spinner caption.
- **`handleSubmit(e)`**:
  1. `preventDefault`, clear `error`, set `step = 'authenticating'`.
  2. `await login(email, password)` — this throws on bad credentials
     (`AuthContext.login` rethrows the `api.post` error).
  3. On success: call `runUnlockSequence()`.
  4. On failure: reset `step` to `'idle'`, set `error = t('login.error')`.
- **`runUnlockSequence()`** — sequentially sets `step` to each of
  `downloadingKey`, `decrypting`, `startingServices`, `ready`, with a
  600ms `setTimeout` delay between each. After `ready`, navigates to
  `/flights`. The unlock steps are **purely presentational** — there is
  no real downloadKey/decrypt work happening; the SPA's bearer token is
  already set inside `await login(...)`. The animation reproduces the
  WPF "vault unlocking" UX for continuity, not for any cryptographic
  operation. Total minimum wait after a successful auth: 4 × 600 ms =
  2.4 s. Document explicitly to avoid future "what does this decrypt?"
  confusion.
- **`STEP_KEYS`**: a `Record<UnlockStep, string>` mapping each step to a
  translation key. `'idle'` maps to the empty string (the spinner
  caption is not rendered when idle).
- **Render**: when `step === 'idle'`, the form is shown (email +
  password + error + submit). Otherwise the form is hidden and a
  centered spinner with the localized step caption is shown. Note the
  spinner is rendered even during `authenticating` (real network), so
  the user sees a single uninterrupted spinner across the entire
  auth → animation flow.

## Dependencies

- **Internal**: `../../auth/AuthContext` — `useAuth().login`.
- **External**: `react` (`useState`, `FormEvent` type),
  `react-router-dom` (`useNavigate`),
  `react-i18next` (`useTranslation`).

## Consumers (intra-repo)

- `src/App.tsx` — mounted at the public `/login` route, *outside*
  `AuthProvider`. (Verify in B8 — currently `App.tsx` puts
  `AuthProvider` inside the protected branch only, so `LoginPage`
  reaches `useAuth()` only because `App.tsx` actually wraps everything
  in `AuthProvider` at a higher level. Confirmed via the §7a graph
  edge `App → AuthProvider`.)

## Data models

`UnlockStep` and `STEP_KEYS` are module-private. No DTOs or shared
types.

## Configuration

- **i18n keys**: `login.title`, `login.email`, `login.password`,
  `login.submit`, `login.error`, `login.authenticating`,
  `login.downloadingKey`, `login.decrypting`, `login.startingServices`,
  `login.ready`. (All present in `src/i18n/en.json` and `ua.json` —
  confirmed.)
- **Tailwind tokens**: `bg-az-bg`, `bg-az-panel`, `border-az-border`,
  `text-az-orange`, `text-az-text`, `text-az-muted`, `text-az-red`.
  Defined in `src/index.css`.
- **Hardcoded animation timing**: `600 ms` per step. No env override.

## External integrations

None directly. Indirect: `AuthContext.login` calls
`POST /api/admin/auth/login` (routed by `nginx.conf` to the `admin/`
service).

## Security

- Both inputs use the right `type` attribute (`email`, `password`),
  giving the browser the chance to mask the password and offer
  password-manager autofill.
- The HTML form does NOT have `autoComplete="off"` — autofill is
  intentionally allowed.
- The component does NOT log credentials anywhere. The `error` state
  carries only the localized "Invalid credentials" string, never the
  raw backend error.
- After successful auth, the bearer is already in memory (`AuthContext`
  set it). The 2.4s "unlock" animation does NOT extend the auth window
  — if the bearer expires server-side during the animation the next
  request retries via `client.ts`'s 401 → refresh path.
- The `setError(t('login.error'))` shows a single generic error for
  every failure mode (wrong password, account locked, server down).
  Acceptable for security (no user-enumeration leak), but logs an
  observability flag — backend should keep specific reasons in its
  audit log.

## Tests

None.

## Notes / open questions

- **The unlock animation is theatrical** — it suggests cryptographic
  work that is NOT happening. If a future phase actually adds
  client-side key derivation, the animation should reflect real
  progress (`'decrypting'` would block on actual work). Keep the names
  but document the placeholder status in `solution.md` (Step 5).
- **No "loading" while `authenticating`** beyond hiding the form —
  Submit button shows no spinner of its own; the form simply hides
  the moment `step` leaves `'idle'`. Mild UX gap (a quick auth
  failure shows the form blink). Defer to Step 8.
- **Caps Lock indicator missing** — the WPF version had one
  (`_docs/legacy/wpf-era.md`). Not currently a goal; flag if the UX
  spec calls for it.
- **No "Forgot password" link** — out of scope; the `admin/` service
  may or may not support that flow. Verify during Step 6 problem
  extraction.
- **Form does not disable Submit while authenticating** — but the
  form is unmounted as soon as `step !== 'idle'`, so a double-click
  cannot fire a second `login(...)` call. Acceptable.
- **`runUnlockSequence` resolution race**: if the user navigates
  away mid-animation (e.g. closes the tab), the pending `setTimeout`s
  still fire but harmlessly. React's StrictMode double-invokes the
  effect-mount path in dev — but `runUnlockSequence` is invoked from
  `handleSubmit`, not an effect, so no duplication.
