high priority low complexity infrastructure pending backend specialist Tier 0

Acceptance Criteria

ReportSchemaCache class exposes getSchema(orgId) which returns the cached schema if valid, or fetches from Supabase and caches before returning
Cache key is a composite of orgId + schemaVersion — schemas for different versions are stored independently and do not overwrite each other
TTL is configurable (default 24 hours); a schema older than the TTL is considered stale and triggers a background refresh, but the stale version is returned immediately (stale-while-revalidate pattern) to support offline use
Cache is invalidated immediately when the fetched schema version does not match the locally stored version for the same orgId
When the device is offline and no cached schema exists, getSchema() throws an OfflineWithNoCacheException with a user-friendly message — does not silently return null
When the device is offline but a valid cached schema exists (even if stale), getSchema() returns the cached schema with a CacheResult.stale flag
Cache persists across app restarts (cold start)
Cache storage does not exceed 5 MB total across all orgs — enforce a max-entries limit with LRU eviction

Technical Requirements

frameworks
Flutter
Hive (preferred) or shared_preferences
Riverpod (for cache provider)
flutter_test
apis
Supabase PostgreSQL 15 (schema fetch query)
Supabase Auth (org context from JWT)
data models
bufdir_column_schema
performance requirements
Cache read (hit) must complete in under 10 ms — synchronous Hive box read
Cache write after network fetch must be non-blocking — write asynchronously in background after returning schema to caller
Schema JSON deserialization must complete in under 50 ms for schemas up to 100 KB
security requirements
Schema JSON is not PII but may contain org-specific field labels — store in app-private storage, not in a world-readable directory
Cache key must include orgId to prevent cross-org schema leakage in multi-org login scenarios
On user logout, clear all cached schemas for the logged-out org to prevent data residue

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Prefer Hive over shared_preferences for structured JSON storage — Hive supports typed boxes and is faster for larger objects. Define a HiveObject subclass ReportSchemaCacheEntry { orgId, schemaVersion, schemaJson, cachedAt } with a @HiveType adapter generated via build_runner. Implement the stale-while-revalidate pattern by returning the cached value immediately in a synchronous path and scheduling a Future (unawaited) for the background refresh — use a Riverpod FutureProvider or a simple Isolate-free async function. The Connectivity package (connectivity_plus) can check network status before attempting a fetch.

The bufdir_column_schema entity is structurally similar to org report schemas — align cache entry fields with that model's version and column_mappings fields for consistency. This cache is critical for Blindeforbundet's offline home-visit reporting use case.

Testing Requirements

Unit tests: (1) cache miss triggers Supabase fetch and writes result to cache; (2) cache hit within TTL returns cached value without network call; (3) stale entry (beyond TTL) returns stale value and triggers background refresh; (4) version mismatch invalidates cache and re-fetches; (5) offline + no cache throws OfflineWithNoCacheException; (6) offline + stale cache returns stale value with CacheResult.stale flag; (7) logout clears org cache. Mock the Supabase client and use an in-memory Hive box (HiveAesCipher not required for tests). Verify LRU eviction triggers when max entries exceeded.

Component
Report Schema Cache
data low
Epic Risks (3)
high impact medium prob technical

Dynamically rendered form fields built from runtime JSON schema are significantly harder to make accessible than statically declared widgets — Flutter's Semantics tree must be correct for every possible field type and every validation state. Failures here block the entire feature for Blindeforbundet's visually impaired peer mentors.

Mitigation & Contingency

Mitigation: Define WCAG 2.2 AA semantics requirements for each field type before implementation and write widget tests using Flutter's SemanticsController for every type. Include a real-device VoiceOver test session in the acceptance gate for this epic before marking it done.

Contingency: If dynamic semantics prove too difficult to get right generically, implement field-type-specific Semantics wrappers (one per supported field type) instead of a single generic renderer, accepting slightly more code duplication in exchange for reliable accessibility.

high impact medium prob technical

The report-form-orchestrator must manage a complex state machine — schema loading, draft persistence, per-field validation, submission retries, and error recovery — across multiple async operations. Incorrect state transitions could result in lost user data, double submissions, or UI freezes.

Mitigation & Contingency

Mitigation: Define all Bloc states and events explicitly as sealed classes before writing any logic. Use a state machine diagram reviewed by the team before implementation. Write exhaustive Bloc unit tests covering every state transition, including concurrent events and network interruption mid-submission.

Contingency: If Bloc complexity becomes unmanageable, extract draft persistence into a separate DraftManagerCubit and keep report-form-orchestrator focused solely on the submit workflow. The additional granularity makes each component independently testable.

medium impact low prob scope

Organisations may require field types beyond the five currently specified (text, multiline, checkbox group, radio, date). If a new type is discovered during pilot testing, the dynamic-field-renderer must be extended, potentially requiring changes across multiple layers.

Mitigation & Contingency

Mitigation: Design dynamic-field-renderer as a registry of field-type renderers with a clear extension point. Document the pattern for adding a new field type so that it can be done in one file without touching existing renderers.

Contingency: If an unhandled field type is encountered at runtime, dynamic-field-renderer renders a labelled plain-text fallback widget and logs a warning so the missing type is surfaced in monitoring, preventing a crash while making the gap visible.