Add LegacyLoadingIndicator widget using a transparent GIF animation
for all full-page loading states across legacy modules. Also fix
HealthController crash by deferring provider mutation during build.
legacy_shared was the junk drawer of the legacy app mode — 37 files
mixing device entities, command infrastructure, UI primitives, generic
formatters, and a duplicate of sf_infrastructure's dio_error_mapper.
Any module needing one piece pulled the whole bag into its graph.
Split it by responsibility, following Mandamiento 4 of Real-World Flutter:
- legacy_ui (new): 6 widget/layout primitives (PageLayout, MenuButton,
SectionButton, PulsingLocationMarker, RefreshableErrorState,
WeekDayChips) plus mapStyleProvider — shared UI state that was the
only reason two modules needed a common package.
- legacy_device_state (expanded 9 → 30): absorbed device entities,
commands infrastructure (datasource + repo + provider + guard),
device settings update flow, and the CSV exporter. Now one package
owns the device domain end-to-end.
- packages/utils: absorbed battery_utils and date_format_utils as pure
formatters that never belonged in a legacy-scoped package.
- legacy_shared: deleted entirely.
The duplicate dio_error_mapper in legacy_shared is gone; callers now use
the sf_infrastructure version (which was always the superset — it adds
ApiException and the dart:io socket handling).
DeviceEntity note: legacy_device_state keeps its own DeviceEntity (with
int timestamps and typed paymentOptions) separate from sf_shared's
DeviceEntity (String timestamps, untyped paymentOptions). The legacy
one is intentionally not exported from the barrel to avoid the
ambiguous_import collision that legacy_shared quietly hid by never
exporting it in the first place. Unifying the two is a domain-model
refactor out of scope here.
0 cross-module imports remain among legacy feature modules.
Spanish is the app default (SFLocalizations.testInit uses 'es',
localeResolutionCallback falls back to the first supported locale), so
make that explicit by pointing the code generator at es.json instead of
en.json. Regenerating picked up 12 activity-meter keys that were already
present in every locale file but had drifted out of I18n.
Add scripts/check_i18n_parity.dart: treats es.json as the template and
reports any missing or orphan keys in en/fr/de/it/pt. Exits non-zero so
it can gate CI or a pre-commit hook later.
Replace the ThemePort/ThemeCode abstraction (GetIt-registered adapter)
with a Riverpod-driven Material 3 ColorScheme, an SfColors ThemeExtension
for brand tokens, and a user-facing appearance selector for light/dark/
system modes. Persisted via SharedPreferences, reacts to system
brightness changes. Payments mode keeps the existing ThemePort API.
Highlights
- New legacy_theme package: LegacyAppTheme (light/dark), LegacyColorSchemes,
SfColors ThemeExtension, LegacyThemePreferences, LegacyThemeNotifier,
LegacyThemeSelector. Timeframe-based variants scaffolded but disabled.
- New /legacy/dashboard/control_panel/settings/appearance route + screen.
- MaterialApp.router picks the legacy theme only when isLegacyMode.
- ~90 ThemeCode.* usages migrated to colorScheme.* / context.sfColors.*.
- 25 widgets dropped the 'ThemePort theme' constructor param.
- ~145 hardcoded colors migrated (exact hex 1:1, grey.shade tiers,
destructive red -> colorScheme.error, background whites -> surface).
Content-over-color whites, transparents, and brand semantic reds/
oranges/greens intentionally preserved.
- sf_localizations updated with appearance / appearanceDescription keys
in all six locales.
These files are regenerated by flutter pub get and contain machine-
specific paths (FLUTTER_ROOT, FLUTTER_APPLICATION_PATH). They should
not be tracked — the file header itself states "do not check into
version control".
- Update .gitignore: prepend **/ to .flutter-plugins-dependencies so
it matches in sub-packages, add Generated.xcconfig and
flutter_export_environment.sh patterns.
- git rm --cached the 4 files that were already tracked (keeps them
on disk for local builds but removes from repo).
Add debugPrint entries before and after POST /universal-notifications/push
so we can trace whether a given token was accepted by the backend
during debugging of push delivery issues.
Introduce LegacyDeviceSetupErrorEvent and two mappers for the
generate-activation-key and create-device endpoints. 403 on
activation-key maps to invalidIdentificator; 404 to deviceNotFound;
401 (already activated or B2B) unifies as deviceNotAvailable. On
create-device, 403 → invalidField and 404 → invalidActivationKey.
The view state now splits validation vs API errors with a
displayErrorKey extension, replacing the previous e.toString() leak
that could surface stack traces to users.
Map PUT /auth/reset-password and PUT /auth/recovery-password failures
into LegacyRecoverPasswordErrorEvent. Reset-password now treats 404
(email not found) as success and surfaces a generic sent-if-exists
flow, closing an account enumeration vector. Recovery-password
differentiates 401 (tokenExpired), 404 (tokenNotFound), 403+Property
(invalidField) from 403 without Property (weakPassword). The view
state splits validation vs API errors with a displayErrorKey extension
for the inline error text.
Introduce LegacySignupErrorEvent to map backend failures from
POST /auth/signup: 400 → emailAlreadyExists, 403 → invalidField,
429 → tooManyAttempts, timeout → network. The view state now
separates validationErrorKey (pre-submit i18n keys) from apiErrorEvent
(typed API outcomes), and the screen listens to both to show proper
i18n messages instead of leaking raw backend text.
Replace the raw errorMessage string in the login view state with a
typed LegacyAuthErrorEvent that classifies backend failures by HTTP
status (403/404 → invalidCredentials, 423 → accountLocked, 401 +
NOT_VERIFIED/expired → dedicated events, 429 → tooManyAttempts,
timeout → network). The login screen and 2FA sheet now switch on the
enum to show specific i18n messages instead of surfacing raw backend
text. Adds auth i18n keys for the full set of mapped states.
Replace the untyped Exception(msg) thrown by mapDioError with an
ApiException that exposes statusCode and isNetworkError alongside the
message. Callers can now classify failures by HTTP status without
string-matching on the error message — this unblocks typed error
mapping in the auth feature modules.
Introduce a shared RefreshableErrorState widget that wraps the retry
hint in a RefreshIndicator with an explicit 'pull down to retry'
caption, so users can recover from load failures without navigating
away. Wire it into the location screen's error fallbacks and make the
control_panel body pull-to-refresh at any time, invalidating the device
list so the dashboard picks up fresh data.
Add a setNetwork action that sends the setWifi command for a saved
network and waits for wifiCurrent confirmation with a 15s timeout.
Saved network cards are now tappable to trigger the switch, and
dedicated success/error states surface the result via snackbar.
Convert the shared command guard to an async check that refetches
/devices when the cached state is older than 30s, so the isDisconnect
flag reflects reality before a command runs. A TopSnackBar explains the
check only if the fetch takes longer than 400ms, avoiding noise on fast
responses. Update all 44 call sites to await the guard.
Device providers (legacyDevicesProvider, selectedDeviceProvider), repository,
datasource, and GetDevicesResponseModel now live in sf_shared. Also moved
dio_error_mapper (safeCall, mapDioError, formatErrorMessage) to sf_infrastructure.
Consumers import directly from sf_shared instead of re-exporting through legacy_shared.