Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

06 — Screen Specifications (Feature Flags Module)

Production-grade specs: chrome, states, components, micro-interactions, a11y, responsive, dark/light, keyboard, motion. Tokens: Sp = 00-shared/02 §3, Ty = §2, El = §5, R = §4, Mo = §6. All components reference 00-shared/03.


1. Flags List Screen /settings/feature-flags

Chrome: standard AppBar (surface, El-1) with headlineMedium title "Feature flags"; subtitle count "N toggles · M enabled". Filter row under AppBar (sticky): module FilterChips (sourced from the loaded data's distinct module values; when none, show "All modules" chip only) + "Enabled only" toggle (FilterChip). Body: grouped list.

AppBar: Feature flags                  [menu]
FilterChip: All | biometric | channels | academics | …
FilterChip: [x] Enabled only
[SectionHeader] BIOMETRIC                    (count)
[AppFlagRow] biometric.enabled        ON  [switch]
[AppFlagRow] biometric.fingerprint    OFF [switch]
[SectionHeader] CHANNELS
[AppFlagRow] channels.sms              ON  [switch]
[AppFlagRow] channels.whatsapp        OFF [switch]
[FAB extended] New flag

Data contract (list source):

  • Default: GET /feature-flags → array sorted by key asc (feature-flags.service.ts:10-12) — not paginated (plain array; no meta).
  • Module chip: GET /feature-flags?module=<m>findByModule (feature-flags.controller.ts:25-27, feature-flag.repository.ts:24-26).
  • Enabled tab: GET /feature-flags/enabled{enabled:true} filter (feature-flag.repository.ts:28-30).

Toggle semantics (the switch):

  • Optimistic apply (00-shared/06 §3.5): on flip → row shows pending state (switch on, row "saving…" label, AppProgress inline) → PUT /feature-flags {key, enabled:newValue, label:<current label>}.
  • Server truth: findOneAndUpdate({tenantId,key,isDeleted:false}, {$set:{enabled,label}}, {upsert:true,new:true}) (feature-flag.repository.ts:38-42). On 200 → reconcile row with returned doc. On error → rollback switch + AppSnackbar(error); 400 field error shown under row.
  • Disabled switch state: a switch is disabled when (a) another request in flight on the same row, (b) offline (writes blocked — no offline queue for flags, 00-shared/07 §10), (c) no feature-flags.update permission (target contract, OQ-5), (d) delete pending.
  • The switch reflects server doc only after reconcile — the row never claims a state the server didn't confirm.

Propagation timing (what the admin must be told):

  • Server has no cache and no event today — the toggle is effective on the next read of the flag collection; downstream isEnabled() calls (feature-flags.service.ts:22-25) see it immediately.
  • Blueprint target: flags cached in Redis sl:global:featureflag:{name} with poll + cache, 30 s TTL (CACHE_ARCHITECTURE.md:33,48) — once implemented, a toggle can take up to 30 s to be visible to all services. UI copy: after a successful toggle show a subtle AppBanner(info): "Applies to apps within ~30 seconds" only when the cache lands; today it is immediate — keep the banner (planned)-flagged in code (feature toggle for copy, (proposed)).
  • Client side: FeatureFlagsCubit TTL (default 30 s, (proposed), mirrors CACHE_ARCHITECTURE.md:48) — the app re-reads the enabled set at that cadence; a RefreshIndicator forces an immediate re-read.

Cache-rebuild awareness:

  • The cache-rebuild BullMQ queue exists (queue.constants.ts:13) and is wired to a scheduler (scheduler.service.ts:27,67,158,214) but nothing enqueues it from flag writes — no invalidation path exists in code. UI implication: never show "cache cleared" or "propagated"; the list/detail screens always read the collection directly (server source of truth). When the flag cache lands, the invalidation trigger is (planned) (CACHE_ARCHITECTURE.md §4 — permission/dashboard use event-based invalidation; flag writes should follow the same pattern, OQ-7).

States:

  • loading → AppSkeleton(list) (≤ 200 ms budget, 00-shared/10 §1).
  • empty → AppEmptyState: "No feature flags yet" — fresh tenants have no seeded flags (no seed code in repo); body: "Flags appear here once created — ask your platform team or create one." CTA FilledButton.tonal "New flag" (if update).
  • error → AppErrorState(code) with Retry; 401 → silent refresh flow.
  • offline → AppOfflineBanner + last-good cache (client cache key sl:cache:featureflags:all (proposed)); switches disabled with tooltip.

A11y: filter chips Semantics(selected:); each row = one Semantics(button, toggled:, label: "flag <key>"); toggle announced via live region; group headers Semantics(headers:).

Motion: m-entrance stagger 40 ms rows; switch m-fast; filter change re-fade m-slow (reduced-motion = instant fades).

Responsive: phone single pane; tablet/desktop master-detail (list left ~360 px, detail right); hover row tint m-fast; Tab order chips → rows → FAB.

2. Flag Detail Screen /settings/feature-flags/:key

[Back]  headlineMedium "Flag detail"
AppCard:
  key (mono, copy icon)          AppFlagKeyChip
  module badge (AppBadge)        created/updated (bodySmall, tabular figures)
  AppSwitch "Enabled"  [ON]      ← primary toggle (same optimistic semantics as list)
AppInfoRow label: <label or "—">
AppInfoRow description: <description or "—">
AppSectionHeader "Affects"
AppAffectedMapCard (proposed — client registry, may be empty)
[outlined] Edit        [text, destructive] Delete

Source: GET /feature-flags/:key → 404 RESOURCE_NOT_FOUND ("Feature flag "" not found." — feature-flags.service.ts:29) when missing (soft-deleted flags are invisible to the scoped read too, base.repository.ts:20-30).

States: skeleton → doc; 404 → AppEmptyState "Flag not found — it may have been deleted" + back; offline → cached doc + banner (writes blocked).

Toggle: same PUT /feature-flags optimistic flow; on success reconcile + snackbar "Feature

Delete flow (destructive): AppDialog "Delete this flag?" body: "It will stop appearing everywhere. Re-creating the same key may fail (the key slot is retained) — prefer disabling instead." → DELETE /feature-flags/:key (feature-flags.controller.ts:54-58). Server: soft delete by _id (feature-flags.service.ts:47-52); double 404 protected. On 200 → pop to list + snackbar. On 500 E11000-style duplicate → OQ-4 copy "Couldn't delete — try again". No optimistic removal (destructive, server-confirm).

A11y: key as code text; switch semantics; dialog focus trap; delete = error colors.

Motion: hero list→detail m-slow (optional); delete dialog m-base; row removal fade.

3. Flag Editor Screen /settings/feature-flags/:key/edit

Chrome: full-screen page (5 fields — 00-shared/05 §5), AppBar with save action.

AppBar: "Edit flag" | "Create flag"           [Save]
AppFlagEditorForm:
  key          AppTextField (mono)   — read-only in edit mode; editable in create mode
  enabled      AppSwitch "Enabled"   — required boolean
  label        AppTextField (optional)
  description  AppTextField (multiline, optional)
  module       AppTextField (optional, suggests existing modules)
[AppBanner warning — edit mode]: "Note: description & module are not saved by the server on
update — only enabled & label persist (feature-flag.repository.ts:40)." (OQ-3)

Semantics (server-exact): UpdateFeatureFlagDto = key (string, required), enabled (boolean, required), label?, description?, module? (update-feature-flag.dto.ts:4-26). Save = PUT /feature-flags (upsert, feature-flags.controller.ts:42-46). The repository drops description and module on write (feature-flag.repository.ts:40) — the editor must (a) show the warning banner above, (b) never render those two fields as "saved" after a successful response. The module field is written only at first creation of the key (upsert insert path writes the whole DTO? — no: the insert path writes only what the repository passes; since the repository only passes enabled/label, even the insert writes only those three (tenantId,key,enabled,label); description/module are effectively unwritable through the API today — OQ-3). Client must therefore treat description/module as read-only display (they can only be populated by seed/migration (planned) IMPLEMENTATION_PLAN.md:291-307).

Create mode: key empty + placeholder "e.g. channels.whatsapp"; server accepts any string (no pattern validation, update-feature-flag.dto.ts:5-7); enabling a brand-new key creates the flag. After save in create mode → navigate to detail of returned doc (server returns the new doc, {new:true}, feature-flag.repository.ts:41).

States: idle / saving (button spinner, double-submit blocked) / field errors (400 VALIDATION_ERROR per-field details, http-exception.filter.ts:103-107) / success (pop + snackbar) / offline (writes blocked banner).

Keyboard: key → enabled(switch via Space) → label → description → module; Enter submits; Esc discards (confirm discard dialog if dirty).

A11y: labels linked; error focus jump to first invalid; warning banner announced once.

4. Bulk Update Sheet

[Title] "Update 3 flags"            [drag handle]
mode SegmentedButton: [Enable all] [Disable all]
AppTextField label (optional, applied to all — note server persists only enabled+label)
[Apply] FilledButton (loading while applying)
result: per-key rows ✓ / ✕ (AppFlagKeyChip + reason)
"Retry failed (1)" text button — resends only failed subset (client-tracked)

Semantics (server-exact): body = JSON array of DTOs (feature-flags.controller.ts:50); service loops await repo.upsert(...) sequentially — no transaction, no rollback (feature-flags.service.ts:40-44). Class-validator rejects the whole body 400 if any item is invalid → sheet shows which row is invalid (map details[].message to key). A 5xx mid-loop → error envelope; client shows applied-so-far vs remaining. No auto-retry of the full batch (upsert is idempotent per key, so a manual retry is safe but noisy — keep explicit).

Propagation note: same as single toggle — server truth immediate today; 30 s window when the Redis cache lands (CACHE_ARCHITECTURE.md:48).

States: idle (count) → applying (progress bar, m-base) → done (per-row results, success secondary, failure error) → error (partial or 400). a11y: live region announces "3 of 5 applied".

5. Affected-Features Map (detail companion) — (proposed)

AppAffectedMapCard: lists client screens registered against this key in the gating registry (FeatureFlagsCubit.registry), each row: icon, screen name, "hidden when off". Empty state: "Nothing in the app maps to this key yet". No server call — pure client composition from module + registry; (proposed) because no client exists and the server has no "affected features" endpoint.


Cross-screen micro-interaction & motion notes:

  • Mo.m-base route transitions; cards El-e1; sheet e-5 r-xl.
  • Snackbar durations: success < 3 s, error ≥ 4 s (00-shared/03 A).
  • Switches everywhere: Mo.m-fast; error rollback = AnimatedSwitcher fade + error ring, no shake on toggle (shake reserved for forms).
  • Safe areas: bottom-nav height respected; sheets respect insets.