# 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.
