[AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs

Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:38:49 +03:00
parent da0a5aa187
commit 510df68bcf
84 changed files with 13065 additions and 0 deletions
@@ -0,0 +1,151 @@
# 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.