high priority low complexity backend pending backend specialist Tier 3

Acceptance Criteria

TerminologyCacheAdapter stores a UTC timestamp alongside the label map; the schema is `{labels: Map<String,String>, cachedAt: ISO8601 string}`
fetchLabels() checks cache age before making a network call: if age < TTL, return cached value immediately without hitting Supabase
If cache age >= TTL, the stale cached value is returned immediately (Right(staleMap)) AND a background Supabase refresh is triggered asynchronously
Background refresh on TTL expiry does not block the caller — it fires via unawaited Future or a background microtask
Background refresh failure is silently swallowed (logged at debug level only); the stale cache remains valid
Background refresh success writes the new map to cache with an updated timestamp
invalidateCache(orgId) clears the cache entry for that orgId and triggers an immediate foreground fetch, returning Either<TerminologyFailure, Map<String,String>>
TTL is configurable at repository construction time (default: const Duration(hours: 24))
Two different orgIds have independent TTL clocks — invalidating one does not affect the other
All new logic is covered by unit tests using fake timestamps (injectable clock dependency)

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
apis
Supabase PostgREST — organization_configs table (same query as task-006)
data models
CachedTerminologyEntry {labelMap: Map<String,String>, cachedAt: DateTime}
TerminologyCacheAdapter (extended with timestamp read/write)
Duration (TTL configuration)
performance requirements
Cache-hit path (age < TTL) must return in < 5ms — no async gap allowed
Background refresh must not cause frame jank — use a detached Future, never await on the widget rebuild path
TTL staleness check must use monotonic or UTC clock to avoid DST edge cases
security requirements
Forced invalidation via invalidateCache() must only be callable by authenticated coordinator or admin roles — enforce at the service layer, not the repository
Cache entries must not persist across app uninstall (SharedPreferences is acceptable; Keychain is not required for label data)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Pass a `DateTime Function() clock` parameter defaulting to `() => DateTime.now().toUtc()` to make the staleness check fully testable. For the background refresh, use `unawaited(Future(() => _refreshInBackground(orgId)))` — import `package:meta/meta.dart` for the `@visibleForTesting` annotation on the refresh method. Avoid using Timer for the background job; a plain unawaited Future is sufficient and easier to test. The stale-while-revalidate pattern is the intended UX: users always get a fast response, and the next app session will have fresh labels.

Document the TTL default (24h) in a class-level doc comment explaining the rationale (org label changes are infrequent admin operations).

Testing Requirements

Inject a fake clock (DateTime Function() nowFn) into TerminologyRepository constructor to make TTL logic deterministic in tests. Cover: (1) fresh cache (age < TTL) → returns cache, zero Supabase calls; (2) stale cache (age >= TTL) → returns stale value immediately, background fetch fires; (3) background fetch updates cache after completion; (4) background fetch failure → cache unchanged, no exception propagated; (5) invalidateCache() → clears entry, triggers foreground fetch, returns new value; (6) invalidateCache() with network failure → returns Left(failure), cache remains empty. Use flutter_test with mockito/mocktail. All tests must be deterministic — no real Durations or actual delays.

Component
Terminology Repository
data low
Epic Risks (2)
high impact medium prob integration

The labels JSONB column in organization_configs may lack a consistent schema across organizations, causing deserialization failures or silent missing keys when a new organization is onboarded with a differently structured map.

Mitigation & Contingency

Mitigation: Define and enforce a canonical JSONB schema via a Supabase check constraint and a migration script. Validate the schema in TerminologyRepository at parse time and emit structured errors for any key-type mismatches.

Contingency: If schema drift is discovered post-deployment, LabelKeyResolver's fallback logic ensures the app continues to function with English defaults while a data migration is prepared to normalize the offending organization's config.

medium impact low prob technical

Device local storage corruption or platform-specific SharedPreferences serialization bugs could render a cached terminology map unreadable, causing the app to fall back to English defaults unexpectedly for an organization with custom terminology.

Mitigation & Contingency

Mitigation: Wrap all cache reads in try/catch, validate the deserialized map against a minimum-required-keys check, and evict corrupted entries automatically before re-fetching from Supabase.

Contingency: Surface a non-blocking in-app warning to the coordinator that terminology may be in default English until the next sync completes; trigger an immediate background sync.