high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

TerminologySyncServiceImpl implements TerminologySyncService and is injectable via Riverpod
start() subscribes to WidgetsBindingObserver (AppLifecycleState.resumed) and connectivity_plus stream; stop() cancels both subscriptions
When AppLifecycleState.resumed fires, _triggerSync(SyncTrigger.appForeground) is called
When connectivity changes from none/mobile to wifi/ethernet (or any restoration), _triggerSync(SyncTrigger.connectivityRestored) is called
A boolean _syncInProgress guard prevents concurrent sync executions; a second trigger while a sync is running is silently dropped
On _checkVersion returning refreshRequired, the service calls OrganizationLabelsNotifier's reload method (or equivalent invalidation) — not the repository directly
On TerminologySyncException with isNetworkError == true, the service schedules a retry after delay = min(initialRetryDelay * retryMultiplier^attempt + jitter, maxRetryDelay)
Jitter is a random duration between 0 and 20% of the computed delay to prevent thundering-herd across users
After maxRetries failed attempts, the service emits SyncEvent.syncFailed and stops retrying until the next external trigger
stop() cancels all pending retry timers and resets the attempt counter
On non-network TerminologySyncException (e.g., auth error), no retry is attempted; error is emitted via events stream
All SyncEvents are emitted to the events StreamController before and after each sync attempt
Integration test: simulate foreground trigger → version mismatch → reload called on notifier

Technical Requirements

frameworks
Flutter
Riverpod
connectivity_plus
dart:math (Random for jitter)
dart:async
apis
WidgetsBindingObserver.didChangeAppLifecycleState
Connectivity().onConnectivityChanged
OrganizationLabelsNotifier.reload()
_checkVersion(orgId)
data models
SyncTrigger
SyncDecision
SyncEvent
TerminologySyncConfig
TerminologySyncException
performance requirements
Trigger handling must not block the UI thread — all async operations must be unawaited or run in microtask/timer
Jitter computation must use dart:math Random (not SecureRandom) — not security-critical, performance matters here
security requirements
stop() must be called on logout to prevent the service from accessing Supabase with expired credentials
Never expose sync error details (including network error messages) in user-visible UI — emit to events stream only

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Mix in WidgetsBindingObserver and call WidgetsBinding.instance.addObserver(this) in start(), removeObserver in stop(). Use a StreamController.broadcast() for the events stream. Implement backoff with a recursive _scheduleRetry(int attempt) method that creates a new Timer and stores the reference for cancellation. Use Random().nextDouble() * 0.2 * baseDelay as jitter.

Guard against logout races: store a _stopped bool; check it at the start of every async step in the retry chain and abort if true. Keep the concrete class internal to the feature layer; expose only via TerminologySyncService abstract type and Riverpod provider. Consider using package:clock for testable time if the project already uses it — fakeAsync works without it but clock makes intent clearer.

Testing Requirements

Unit tests with flutter_test and fakeAsync. Mock connectivity stream and app lifecycle observer. Test scenarios: (1) foreground trigger → version upToDate → no reload; (2) foreground trigger → refreshRequired → reload called once; (3) concurrent triggers → only one sync runs; (4) network failure → retry scheduled at correct backoff delay (verify with fakeAsync timers); (5) maxRetries exhausted → syncFailed event emitted, no further timers; (6) stop() during retry → timer cancelled, no reload called. Use mockito to verify OrganizationLabelsNotifier.reload() call count.

Verify jitter: run 100 iterations, assert all delays fall within [baseDelay, baseDelay * 1.2].

Component
Terminology Sync Service
service medium
Epic Risks (3)
high impact medium prob technical

When a user switches organization context (e.g., a coordinator with multi-org access), a race condition between the outgoing organization's map disposal and the incoming organization's fetch could briefly expose the wrong organization's terminology to the widget tree.

Mitigation & Contingency

Mitigation: Implement an explicit loading state in OrganizationLabelsNotifier that widgets check before rendering any resolved labels. The provider graph should cancel the previous organization's fetch via Riverpod's ref.onDispose before initiating the next.

Contingency: If the race manifests in production, fall back to English defaults during the transition window and emit a Sentry error event for investigation; the UX impact is a brief English flash rather than wrong-org terminology.

high impact low prob security

Supabase Row Level Security policies on organization_configs may inadvertently restrict the authenticated user from reading their own organization's labels JSONB column, causing silent empty maps that appear as English fallbacks.

Mitigation & Contingency

Mitigation: Write and test explicit RLS policies that grant SELECT on the labels column to any authenticated user whose organization_id matches. Add an integration test that verifies label fetch succeeds for each role (peer mentor, coordinator, admin).

Contingency: If RLS blocks are discovered in production, temporarily escalate label fetch to a service-role edge function while the RLS policy is corrected, ensuring no labels are exposed cross-organization.

medium impact medium prob scope

A peer mentor who installs the app for the first time with no internet connection will have no cached terminology map and will see only English defaults, which may be confusing for organizations like NHF that use Norwegian-specific role names exclusively.

Mitigation & Contingency

Mitigation: Bundle a default fallback terminology map for each known organization as a compile-time asset (Dart asset file) so that even fresh installs without connectivity render correct organizational terminology immediately.

Contingency: If bundled assets are out of date, display a one-time informational banner noting that terminology will update on next connectivity restore, with no functional blocking of the app.