[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,152 @@
# Module: `src/components/Header.tsx`
> **Source**: `src/components/Header.tsx` (133 lines)
> **Topo batch**: B4 (depends on B3: `auth/AuthContext`, `components/FlightContext`, `components/HelpModal`, `types/index`)
## Purpose
The persistent top navigation bar of the authenticated SPA. Combines
five concerns into one component: brand mark, currently-selected-flight
dropdown, top-level navigation links (permission-gated), session info
(user email + Logout), language toggle (EN ↔ UA), and a `?` button that
opens `HelpModal`. Also renders a duplicate bottom-nav on the mobile
breakpoint. Replaces the legacy WPF top ribbon + window chrome
(`_docs/legacy/wpf-era.md` §4 / §"What survived").
## Public interface
```ts
export default function Header(): JSX.Element
```
No props — Header reads everything it needs from `AuthContext`,
`FlightContext`, and `react-i18next`. Mounted exactly once in `App.tsx`
above the protected route tree.
## Internal logic
- **Local state**:
- `showDropdown: boolean` — flight selector open/closed.
- `filter: string` — text filter for the flight selector list.
- `showHelp: boolean` — controls the `HelpModal` `open` prop.
- `dropdownRef: RefObject<HTMLDivElement>` — used to detect outside
clicks.
- **Outside-click effect**: registers a `mousedown` listener on `document`
while mounted; if the event target is not inside `dropdownRef.current`,
closes the dropdown. Listener is removed on unmount. (Always-on, even
while the dropdown is closed — cheap, but not the most surgical
pattern; flag to consider gating on `showDropdown` in Step 8.)
- **Filtered flights**: `flights.filter(f => f.name.toLowerCase().includes(filter.toLowerCase()))`
— case-insensitive substring match on the flight name. Empty filter
shows everything.
- **Logout**: `await logout(); navigate('/login')`. Always navigates,
even if `logout()` throws (it never does — `AuthContext.logout` swallows
network errors by design).
- **Language toggle**: `i18n.changeLanguage(i18n.language === 'en' ? 'ua' : 'en')`.
Same two-language assumption as `HelpModal` (treats every non-`'ua'`
value as English-equivalent). The toggle button label is the
*target* language ("UA" while EN is active, "EN" while UA is active).
- **Permission-gated nav** (`navItems`):
| `to` | `label` key | `perm` |
|---|---|---|
| `/flights` | `nav.flights` | `FL` |
| `/annotations` | `nav.annotations` | `ANN` |
| `/dataset` | `nav.dataset` | `DATASET` |
| `/admin` | `nav.admin` | `ADM` |
`Settings` (`/settings`) is always rendered — no permission gate.
Filter applied via `navItems.filter(n => hasPermission(n.perm))`.
- **Layout**: a single `<header>` containing brand → flight dropdown →
primary nav (`hidden sm:flex`) → spacer (`flex-1`) → user email
(`hidden sm:block`) → language toggle → `?``⚙` (settings link) →
logout button. A second `<nav>` element below is `sm:hidden` and
positions itself fixed at the bottom of the viewport — the mobile
nav. Both navs share the same filter logic.
## Dependencies
- **Internal**:
- `../auth/AuthContext``useAuth` (user, logout, hasPermission).
- `./FlightContext``useFlight` (flights, selectedFlight, selectFlight).
- `./HelpModal` — opens on `?` click.
- `../types``Flight` type for the dropdown row.
- **External**: `react-router-dom` (`NavLink`, `useNavigate`),
`react-i18next` (`useTranslation`),
`react` (`useState`, `useRef`, `useEffect`).
## Consumers (intra-repo)
- `src/App.tsx` — mounted inside `ProtectedRoute → FlightProvider`.
No other intra-repo consumer; `Header` is a top-level chrome component.
## Data models
The `navItems` array is a module-private literal of
`{ to: string; label: string; perm: string }`. The filtered `flights`
list is `Flight[]` (from `types/index.ts`).
## Configuration
- **i18n keys consumed**: `nav.flights`, `nav.annotations`, `nav.dataset`,
`nav.admin`, `nav.logout`. (Verified to exist in `src/i18n/en.json`.)
The filter placeholder ("Filter…") and the empty-state ("— Select
Flight —", "No flights") are hardcoded strings — flag for Step 4.
- **Tailwind tokens**: `bg-az-header`, `border-az-border`, `bg-az-panel`,
`text-az-text`, `text-az-muted`, `text-az-orange`, `text-az-red`,
`bg-az-bg`. Defined in `src/index.css`.
- **Breakpoint**: `sm:` from Tailwind defaults to 640px — matches the
responsive-breakpoint spec in `_docs/ui_design/README.md`.
- **Permission strings**: `FL`, `ANN`, `DATASET`, `ADM`. Backend-defined
by the `admin/` service; treated as opaque strings here.
## External integrations
None directly. Triggers `logout()` which calls `POST /api/admin/auth/logout`
inside `AuthContext`, and `selectFlight()` which calls `PUT /api/annotations/settings/user`
inside `FlightContext`.
## Security
- `hasPermission(perm)` is **client-side advisory** — a user with the
`/admin` route URL can navigate there without going through the nav
link. The backend `admin/` service is the authority. Document in
`security_approach.md` (Step 6).
- The flight-selector dropdown displays every flight returned by
`GET /api/flights?pageSize=1000` — there is no permission check here.
If `flights/` ever returns flights the user should not see, this
component would leak them. Trust the backend filter.
- `user?.email` is rendered raw in the header — React JSX-escapes it,
so XSS via a malicious email is not possible at this layer, but
validate that the `admin/` service enforces email format.
## Tests
None.
## Notes / open questions
- **Outside-click listener is always attached** — slightly wasteful when
the dropdown is closed. Gating on `showDropdown` would also remove a
closure capture. Defer to Step 8.
- **Dropdown is not keyboard-accessible**: no `role="combobox"`, no
`aria-expanded`, no Esc-to-close, focus does not move into the filter
input on open beyond `autoFocus` (which works only the first time
the input mounts). Flag against `_docs/ui_design/README.md` keyboard
shortcuts for Step 4.
- **Mobile bottom nav duplicates `navItems`** twice in source — DRY win
by extracting a `<NavSet items={...}/>` subcomponent, but a deferral
candidate.
- **Hardcoded English strings** ("AZAION", "— Select Flight —",
"Filter...", "No flights"). Brand mark is intentional; the others
are content and should move to `nav.*` keys. Step 4 candidate.
- **`/settings` link** is unguarded by `hasPermission`. Per
`_docs/ui_design/README.md` settings page is available to every
authenticated user — confirmed intent.
- **`new Date(f.createdDate).toLocaleDateString()`** uses the browser's
default locale, not `i18n.language`. Mild inconsistency; Step 8
cosmetic.
- **`flights` cache truncation**: the dropdown shows at most 1000
flights because of the hardcoded `pageSize=1000` in `FlightContext`.
Documented as a flag there; Header inherits the limitation.