281 lines
23 KiB
Markdown
281 lines
23 KiB
Markdown
# UI Phase 9 — Customer account & care circle
|
||
|
||
> **Mission:** the customer account area is functionally complete but emotionally wrong for a home-care
|
||
> product: a flat settings form with no sign-out, care recipients as clinical rows behind an invisible tap
|
||
> target, a whole-list free-text care-record edit mode that loses drafts, and an address "map" that is a
|
||
> blank coordinate grid feeding the EVV proximity check. Reframe the area around **the people being cared
|
||
> for** — an account hub, a care circle with faces, per-item structured record editing, a real Neshan map —
|
||
> and fix the real defects along the way (silent profile-load error, error→false-empty, cramped dialogs).
|
||
>
|
||
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
|
||
> **Unlocks:** the account area feels like caring for people, not filling forms
|
||
>
|
||
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
|
||
> and invoke the frontend-designer skill — both are mandatory.**
|
||
|
||
## 1. Context — where this sits
|
||
|
||
Balinyaar's customer is a **family member arranging care for someone they love** — a parent, a spouse,
|
||
sometimes themselves. The account area (`client/src/app/[locale]/(private-routes)/(customer)/` — `profile/`,
|
||
`patients/`, `patients/[id]/record/`, `addresses/`) is where that relationship lives, and today it reads
|
||
like an admin table. Every problem below is verified in code:
|
||
|
||
- **Profile is a flat form, and a failed load silently blanks it.** `profile/page.tsx:16–18` handles only
|
||
`isLoading` from `useCustomerProfile()`; on error the form renders with `initial={profile ?? null}` — a
|
||
save from that blank state **overwrites server truth with empty fields**. No identity header, no phone,
|
||
no sign-out; the only status affordance is a color-only completeness line (lines 75–77).
|
||
- **Query errors collapse into false-empty.** `patients/page.tsx:83` — `const isEmpty = !isLoading &&
|
||
patients.length === 0;` never reads `isError`, so a failed query tells a family «هنوز بیماری ثبت نشده».
|
||
Identical bug in `addresses/page.tsx:91`.
|
||
- **The care record is undiscoverable and error-prone to edit.** `PatientCard.tsx:66–85` renders the
|
||
tap-to-open area as an unstyled `component="button"` (`background: 'none', border: 'none'`) — nothing
|
||
signals the richest screen in the area exists. Inside, `record/page.tsx` edits each tab as a whole-list
|
||
mode with per-tab `useState` drafts (lines 197/275/346) **destroyed on tab switch**; dose/frequency/
|
||
time-of-day are plain free text (`routine_time`, line 321); سوابق is a flat card list with bare
|
||
«قبلی/بعدی» paging (lines 439–445).
|
||
- **The map is not a map.** `components/geography/AddressMapPicker.tsx:28–35` documents itself as "NOT a
|
||
real map (no Neshan/Google tiles), only a bounded canvas" and surfaces raw lat/lng captions — yet the pin
|
||
it produces feeds nurse arrival and the EVV proximity check.
|
||
- **Forms are crammed into `maxWidth="sm"` dialogs** on a phone-first shell with no `fullScreen` and no
|
||
dirty-state guard (`patients/page.tsx:160`, `addresses/page.tsx:170`); backdrop-click discards work.
|
||
- **No avatar/photo concept exists in the customer identity system** — `CustomerProfile`
|
||
(`services/profiles/types.ts:44–50`) and `Patient` are text-only; `avatarUrl` is nurse-only.
|
||
|
||
**What already exists (do not rebuild):**
|
||
|
||
- [Phase 0](ui-phase-0-design-language.md) theme/brand, [Phase 1](ui-phase-1-primitives-and-states.md)
|
||
primitives (ErrorState/EmptyState kit, PageHeader, card kit, skeleton twins), [Phase 2](ui-phase-2-shells-and-navigation.md)
|
||
customer shell + the **sign-out affordance** — this phase gives sign-out its *home*, not its first existence.
|
||
- The `services/{domain}` layer: `profiles`, `patients` (+ `age.ts`), `addresses`, `patientRecords` are
|
||
wired and (except `patientRecords`, REQ-027) **real**. This phase is presentation + defect fixes.
|
||
- `PatientHeader` (shared by E1 card + E2 record), `PatientForm`, `PatientCard`, `CascadingRegionSelect`,
|
||
`AddressForm`, `AddressCard`, `AddressMapPicker` — all tested; restyle, don't fork.
|
||
- The care record's **non-leaking access gate**: `useRecordAccess` before any clinical fetch,
|
||
`usePatient(patientId, { enabled: canView })` (`record/page.tsx:50–52`), access-denied card at 59–74,
|
||
ownership banner at 108–116. Preserve verbatim.
|
||
- Soft-archive semantics + copy, the dashed-border empty states («اولین آدرس را اضافه کنید تا پرستار بداند
|
||
کجا بیاید»), content-shaped skeletons, inline per-field validation with error-clearing.
|
||
|
||
## 2. Required reading (do this first)
|
||
|
||
- [audit/customer-account.md](audit/customer-account.md) — the 16 problems + 10 opportunities + the
|
||
keep-list this phase executes; every file/line above comes from it.
|
||
- Code, in this order: `profile/page.tsx`, `patients/page.tsx`, `patients/[id]/record/page.tsx`,
|
||
`addresses/page.tsx` (all under `client/src/app/[locale]/(private-routes)/(customer)/`), then
|
||
`client/src/components/{PatientCard,PatientHeader,PatientForm,RelationSelect,GenderToggle}/` and
|
||
`client/src/components/geography/{AddressMapPicker,AddressForm,AddressCard,CascadingRegionSelect}.tsx`,
|
||
then `client/src/services/{profiles,patients,addresses,patientRecords}/types.ts` and
|
||
`client/src/services/patients/age.ts`.
|
||
- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
|
||
the design contract (invoke the skill, don't just read it).
|
||
- Product: [../../../product/business/01-actors-and-onboarding.md](../../../product/business/01-actors-and-onboarding.md)
|
||
(who the "patient" actually is — relations include «خودم») and the Persian glossary in
|
||
[../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md) — both feed
|
||
the naming decision in 3.2; [../../../product/business/06-evv-and-service-delivery.md](../../../product/business/06-evv-and-service-delivery.md)
|
||
for why the address pin matters (advisory EVV proximity).
|
||
- The REQ tracker tail: [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
|
||
— REQ-001…038 taken at audit time; earlier UI phases may have appended more. Take the next free number.
|
||
- `client/CLAUDE.md` — Golden rules + the `(customer)` part of Project Structure.
|
||
|
||
## 3. Scope — build this
|
||
|
||
### 3.1 Profile → account hub (`profile/page.tsx`)
|
||
|
||
Rebuild the Profile tab as the customer's account center:
|
||
|
||
1. **Identity header** — warm auto-colored initials (same util as 3.2) + full name + masked phone from
|
||
`/me` (`useMe().phone` via `maskIranMobile` from `@/components/PhoneNumberField`, in a `dir="ltr"` span).
|
||
The first place the customer sees *themselves* in the app.
|
||
2. **Grouped tappable rows** below the header — اطلاعات شخصی / مخاطب اضطراری / نشانیها / زبان / اعلانها /
|
||
پشتیبانی / **خروج**. نشانیها → `/addresses`, اعلانها → `/notifications`, پشتیبانی → `/support/tickets`;
|
||
اطلاعات شخصی and زبان open focused edit surfaces (prefer bottom sheets so `/profile` stays the single
|
||
route; sub-routes require a `client/CLAUDE.md` Project Structure update). The زبان row owns the
|
||
server-stored `preferredLanguage` and hands the actual locale switch to phase 2's switcher — don't build
|
||
a second locale mechanism. The خروج row is the sign-out's home: confirm dialog → `useLogout()` (the
|
||
single logout path — never hand-roll cookie clearing).
|
||
3. **Emergency contact as a status card**, not two bare fields: complete → success check + contact name +
|
||
a `tel:` link (tel-only, per the f14 emergency rule); incomplete → a warm nudge explaining *why* nurses
|
||
need it («پرستار باید بداند در شرایط اضطراری با چه کسی تماس بگیرد») + edit CTA.
|
||
4. **Fix the silent load error**: on `useCustomerProfile()` error render the phase-1 ErrorState with retry —
|
||
**never** the editable form. The blank-form-overwrites-server-truth path must be impossible.
|
||
|
||
### 3.2 Care-circle reframe (`patients/page.tsx`, `PatientCard`, `PatientHeader`)
|
||
|
||
1. **Naming**: consult the product docs + glossary (§2) before renaming «بیماران». Candidates: «عزیزان شما»
|
||
/ «حلقه مراقبت». Mind the «خودم» relation — the term must not be absurd for self-care («حلقه مراقبت» is
|
||
the safer default). **Record the decision in your report** and apply it consistently (page title, nav
|
||
label, empty states, the A5 home nudge). **Copy-level rename only**: the `/patients` route,
|
||
`services/patients`, and the `patients.*` i18n key names stay unchanged.
|
||
2. **Avatar slot on `PatientHeader`** — the patients API has no photo field, so ship warm auto-colored
|
||
initials: a deterministic util (name hash → one of ~6 new `--bal-avatar-*` token pairs added to
|
||
`tokens.css` **both scheme blocks**; note the token addition as a foundation extension in your report —
|
||
phase 0 owns `theme/`). `PatientHeader` is shared by the E1 card and E2 record, so one change gives both
|
||
a face; update its co-located test. Real photo upload is (DEFERRED → optional REQ, see §4).
|
||
3. **Visible record affordance on `PatientCard`** — replace the invisible button with a
|
||
`CardActionArea`-style hover/press surface or an explicit «مشاهده پرونده» chevron row. Add a
|
||
**last-visit meta line** («آخرین ویزیت: ۱۲ تیر») via an optional `lastVisitLabel` prop — sourced
|
||
best-effort from the cached customer bookings list; the server-truth field is a REQ (§4). Omit when
|
||
unknown; never fabricate.
|
||
4. **Fix error→false-empty**: `isError` branch with phase-1 ErrorState + retry on **both** `patients/page.tsx`
|
||
and `addresses/page.tsx`. A failed query must never render «هنوز بیماری ثبت نشده» + add CTA.
|
||
5. **Keep** soft-archive semantics and its copy verbatim (do-not-regress).
|
||
|
||
### 3.3 Care record editing (`patients/[id]/record/page.tsx`)
|
||
|
||
1. **Per-item bottom sheets replace whole-list edit mode.** Each medication/routine/task row gets edit (and
|
||
each tab an add CTA) opening a bottom sheet (`Drawer anchor="bottom"`; standard dialog on desktop).
|
||
Medication sheet: name + **structured dose** (amount + unit select قرص/کپسول/قطره/سیسی/واحد) +
|
||
**frequency presets** («روزی ۱ بار» … «هر ۸ ساعت» / «در صورت نیاز» + free-text fallback) + **time-of-day
|
||
chips** (stable codes `morning|noon|evening|night` → صبح/ظهر/عصر/شب). Routine items get the same chip row
|
||
instead of the free-text `routine_time`; tasks stay label + done. A dirty sheet gets a discard-confirm;
|
||
killing the whole-list mode removes the silent tab-switch draft loss by construction.
|
||
2. **REQ posture**: the family-owned record is REQ-027 mock territory (no backend). Keep the UI seam-tolerant
|
||
behind `services/patientRecords` — extend the client model + `mockApi` for the structured fields, and
|
||
**append the structured shape as an addendum to REQ-027** in the tracker (dose amount/unit, frequency
|
||
preset codes, time-of-day codes) so the eventual table matches what the UI collects.
|
||
3. **Visit-note history becomes a timeline**: group سوابق by Shamsi month with a subtle rail; each
|
||
`VisitNoteCard` gains a done/undone task summary line; when `VisitNote.bookingId` is non-null
|
||
(`services/patientRecords/types.ts:79–81`) link «مشاهده رزرو» → `/bookings/[id]`, showing the service
|
||
name only when derivable from the cached booking — never block on it. Keep paging (grouped within pages
|
||
is fine). A read-mode "daily schedule" view (meds grouped صبح/ظهر/شب) is (DEFERRED — not in this chain).
|
||
4. **Do not touch the access gate** — `useRecordAccess` before any clinical fetch, the access-denied card,
|
||
and the ownership banner survive the redesign verbatim.
|
||
|
||
### 3.4 Addresses + the real map (`addresses/page.tsx`, `components/geography/`)
|
||
|
||
1. **Real Neshan web tiles behind the existing `AddressMapPicker` boundary** — the component's props/output
|
||
(`{ latitude, longitude }`) don't change, so `AddressForm` is untouched. Client-side embed (official
|
||
Neshan web SDK or Leaflet + Neshan tiles), **dynamically imported with `ssr: false`**. Key from
|
||
`NEXT_PUBLIC_NESHAN_KEY` — the **web key is separate config** from the server's `NeshanGeocoder` adapter
|
||
(refinement phase 8; lives under `server/src/Infrastructure/…/Seams/Real/` — do not touch it). Document
|
||
the variable in `client/.env.sample` (the repo uses `.env.sample`, **not** `.env.example`). **When the
|
||
key is unset, fall back to the current grid stand-in** so dev/CI/jsdom tests keep working — keep the
|
||
fallback code, don't delete it.
|
||
2. **Map features**: address search box (Neshan geocode), locate-me (GPS) button, draggable pin, and a
|
||
reverse-geocoded pin preview («پین روی: خیابان ولیعصر…») replacing the raw lat/lng captions — coordinates
|
||
never render in the UI again. The pin still only *refines* coordinates; the bookable geography remains
|
||
the `CascadingRegionSelect` choice. Tokenize the pin's hard-coded `rgba` drop-shadow (line 116) too.
|
||
3. **Pin-quality cue on `AddressCard`**: `Address.latitude` is nullable (`services/addresses/types.ts:38`) —
|
||
show «پین ثبت شده» / «پین ندارد» (the latter with a warm fix-it nudge, since a missing pin degrades the
|
||
nurse's arrival + EVV). A static map thumbnail per card is (DEFERRED — revisit once tiles are proven).
|
||
4. **Full-screen mobile form dialogs** for the patient form (`patients/page.tsx:160`) and the address form
|
||
(`addresses/page.tsx:170`): `fullScreen` below the `sm` breakpoint with an app-bar header (title / close
|
||
/ save) and a dirty-state discard-confirm on close/backdrop. If phase 1 shipped a form-dialog primitive,
|
||
use it; otherwise add shared `components/FormDialogShell/` (co-located test) and note the foundation
|
||
extension in your report.
|
||
5. **Fix the fa copy bug** in `messages/fa.json:228`: «…جزئیاتی که پرستار برای یافتن در نیاز دارد.» → «…برای
|
||
یافتن درِ منزل نیاز دارد.» (or «برای یافتن نشانی نیاز دارد.»). Phase 12 owns the global copy sweep — fix
|
||
this one here since you touch the form, and flag it in your report so phase 12 doesn't double-edit.
|
||
|
||
### 3.5 Form quality (`PatientForm`, loading consistency)
|
||
|
||
1. **Structured name fields**: replace the single full-name field with نام / نام خانوادگی (the wire already
|
||
takes `firstName`/`lastName`/`displayName`; today `splitName` at `PatientForm.tsx:32–38` guesses, with
|
||
`lastName` falling back to `firstName`). `displayName` = the joined value. If the Patient *read* DTO
|
||
lacks `firstName`/`lastName` for edit-prefill, fold that into the REQ from 3.2.
|
||
2. **Birth-year presentation honesty**: `age.ts:10` fabricates a Jan-1 `birthDate` from the collected age —
|
||
that stays (wire mapping), but the UI must never render the fabricated full date anywhere; display
|
||
age-only (`age_years`), and consider collecting سال تولد instead of سن if it reads warmer. Verify no
|
||
surface prints raw `birthDate`.
|
||
3. **Loading consistency**: profile's full-page `AppLoading` (`profile/page.tsx:18`) → a form-shaped
|
||
skeleton, matching the skeleton language of patients/addresses/record.
|
||
4. **Small a11y fixes from the audit** (check an earlier phase didn't already land them): `GenderToggle`
|
||
gets `width: '100%'` so its `flex: 1` children actually split (today it renders content-width, misaligned
|
||
against `fullWidth` fields); `RelationSelect` selected state gains a check icon + fill (border-color-only
|
||
today — fails WCAG 1.4.1). Both are shared → update their co-located tests; **never** loosen
|
||
GenderToggle's never-defaulted, non-deselectable constraint.
|
||
|
||
## 4. Mocks & seams in this phase
|
||
|
||
**No new mocks or seams.** The area's domains stay as they are: `profiles`/`patients`/`addresses` real,
|
||
`patientRecords` mock-primary behind its existing seam (REQ-027). The Neshan web embed is client config
|
||
(`NEXT_PUBLIC_NESHAN_KEY`), not a backend seam — with a grid-stand-in fallback when unset.
|
||
|
||
Backend gaps become REQ entries appended to [the tracker](../../shared-working-context/frontend/requests/for-backend.md)
|
||
— check its tail and number onward (REQ-001…038 taken at audit time). Expected filings:
|
||
|
||
1. **REQ-(next): patient care metadata** — `lastVisitAt` (optionally `visitCount`) on the patient read
|
||
model, plus `firstName`/`lastName` on the read DTO if absent (3.2/3.5).
|
||
2. **REQ-027 addendum** (not a new number): the structured care-record field shape — medication
|
||
`{ doseAmount, doseUnit, frequencyCode|frequencyText, timeOfDay[] }`, routine `timeOfDay` codes (3.3).
|
||
3. **REQ-(next), optional & product-gated: patient photo upload** — the UI ships initials-only either way.
|
||
|
||
## 5. Critical rules you must not get wrong
|
||
|
||
- **The non-leaking access-denied gate stays.** `useRecordAccess` gates **before** any clinical fetch
|
||
(`usePatient` stays `enabled: canView`); a 403/denied renders the access-denied card with zero clinical
|
||
data. Preserve the family-ownership banner. **Clinical text is never logged, never in localStorage, never
|
||
in a query string** (`patientRecords` rule).
|
||
- **`AddressMapPicker`'s RTL engineering survives the tile swap**: the canvas stays `dir="ltr"`, marker
|
||
positioning stays inline-`style` (the stylis RTL plugin flips `left` and `translate` — see the comment at
|
||
lines 57–59). Real map containers get the same `dir="ltr"` island treatment.
|
||
- **`CascadingRegionSelect` architecture stays** — parent-gated enabling, per-level progress adornments, the
|
||
explicit "whole city" MenuItem as a real choice. The map pin refines, never replaces, the region choice.
|
||
- **`GenderToggle` stays never-defaulted and non-deselectable** (same-gender matching). **Soft-archive copy
|
||
+ optimistic-with-explanatory-error stays**; the archive confirm's dismiss stays the safe/neutral button.
|
||
- **Sign-out goes through `useLogout()`** — the single logout path (server revoke + cookie clear + LOG_OUT +
|
||
cache drop). No cookie handling in page code.
|
||
- Design-contract non-negotiables that bite here: i18n keys in **both** catalogs; tokens not hexes (new
|
||
avatar colors = `--bal-*` pairs in both scheme blocks); RTL logical props (phone numbers get deliberate
|
||
`dir="ltr"` islands); dark mode on every new surface; MUI v9 API only; shared components keep co-located
|
||
tests; fetch/cookies only via `@/lib/api` + `@/lib/cookies`.
|
||
|
||
## 6. Definition of Done
|
||
|
||
On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):
|
||
|
||
- [ ] `npm run check` green; `npm run test:ci` green with updated tests for every touched shared component
|
||
(`PatientHeader`/`PatientCard`/`PatientForm`/`GenderToggle`/`RelationSelect`/`AddressMapPicker`/
|
||
`AddressCard`/any new `FormDialogShell`).
|
||
- [ ] `en.json`/`fa.json` in sync; the «بیماران» rename applied consistently in both; the `fa.json`
|
||
line_hint bug fixed.
|
||
- [ ] Visual verification on the four axes (`/fa` + `/en` × light + dark), mobile **and** desktop — verify
|
||
mobile at `/fa` first.
|
||
- [ ] With the API stopped: profile shows ErrorState + retry (no editable blank form); patients and
|
||
addresses show ErrorState + retry (no false-empty). With the API back, retry recovers in place.
|
||
- [ ] `/profile` shows identity header (initials + name + masked LTR phone), grouped rows, emergency-contact
|
||
status card, and a working خروج row (confirm → logged out → `/login`).
|
||
- [ ] Care circle: every person has a colored-initials avatar (stable across reloads), a visible
|
||
«مشاهده پرونده» affordance, and archive/edit still work.
|
||
- [ ] Care record: add/edit medication via bottom sheet with structured dose/frequency/time-of-day; tab
|
||
switches lose nothing; سوابق is a month-grouped timeline with booking links when `bookingId` exists.
|
||
- [ ] Addresses: with `NEXT_PUBLIC_NESHAN_KEY` set — real tiles, search, locate-me, draggable pin,
|
||
reverse-geocoded preview, no raw lat/lng anywhere; with the key unset — the grid fallback still works
|
||
and tests pass. `AddressCard` shows the pin-quality cue. `.env.sample` documents the variable.
|
||
- [ ] Patient + address forms are full-screen dialogs on mobile with app-bar header and dirty-state confirm.
|
||
REQ entries filed per §4; REQ-027 addendum recorded.
|
||
|
||
## 7. How to test (what a human can verify after this phase)
|
||
|
||
1. Log in as a seeded customer (`0912000000x`) on a mobile viewport at `/fa`. Open the Profile tab → see
|
||
your initials, name, and masked phone (digits LTR); rows for اطلاعات شخصی/مخاطب اضطراری/نشانیها/زبان/
|
||
اعلانها/پشتیبانی/خروج. Tap خروج → confirm → you land on `/login`, session revoked.
|
||
2. Clear the emergency contact → the warm "why nurses need this" nudge; fill it → check + `tel:` link.
|
||
3. Stop the API, reload `/profile`, `/patients`, `/addresses` → each shows an error card with retry — no
|
||
blank form, no «هنوز بیماری ثبت نشده». Start the API, tap retry → data returns without a full reload.
|
||
4. Open the care circle → each person has a colored-initials avatar and a visible «مشاهده پرونده» chevron;
|
||
tap it → the record opens. The renamed title appears here and in the tab bar.
|
||
5. In the record, tap "add medication" → bottom sheet with dose amount + unit, frequency preset chips, and
|
||
صبح/ظهر/عصر/شب chips; save → the row renders the structured summary. Start editing, switch tabs, come
|
||
back → nothing lost. Close a dirty sheet → discard confirm.
|
||
6. Open سوابق → notes grouped by Shamsi month on a rail; a booking-linked note links to `/bookings/[id]`.
|
||
7. Add an address on mobile → the form opens full-screen with app-bar header; with a Neshan key set, search
|
||
«ولیعصر», drag the pin, tap locate-me → the preview line shows the reverse-geocoded street, never raw
|
||
coordinates. Back on the list, that address shows «پین ثبت شده»; an old pin-less address shows «پین ندارد».
|
||
Tap close with unsaved changes → discard confirm; cancel keeps your draft.
|
||
8. Repeat 1, 4, 5, 7 on `/en` (LTR) and in dark mode — avatars, map island, timeline, and status cards all
|
||
render correctly on all four axes.
|
||
|
||
## 8. Hand off & document (close the phase)
|
||
|
||
- Update `client/CLAUDE.md` "Project Structure" for anything added/renamed (new `components/` entries such
|
||
as `FormDialogShell`, the avatar util, any profile sub-routes) and the `(customer)` route notes for the
|
||
profile hub + the copy-level care-circle rename.
|
||
- Append the REQ entries + the REQ-027 addendum to
|
||
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
|
||
- Write the frontend report at `dev/shared-working-context/reports/ui-phase-9-report.md`: the naming
|
||
decision and why, the avatar-token additions (foundation extension), the Neshan embed choice + fallback
|
||
behavior, the fa copy fix (flag it for phase 12's sweep), REQs filed, deferrals.
|
||
- Save a memory note per operating-rules §8: account-hub structure, the naming decision, the Neshan web-key
|
||
config (`NEXT_PUBLIC_NESHAN_KEY`, fallback-to-grid), and the REQ-027 structured-fields addendum.
|