From 7e9cbcf1e9bab4ba8b303cd318e3df1012f310ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 28 Jul 2026 12:51:39 +0200 Subject: [PATCH 1/2] fixed widget mounting errors --- .../absence_report/absence_report_view.dart | 39 +++++++++++-------- lib/widget/user_avatar.dart | 14 ++++--- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/view/pages/absence_report/absence_report_view.dart b/lib/view/pages/absence_report/absence_report_view.dart index 0ddb332..d3737c9 100644 --- a/lib/view/pages/absence_report/absence_report_view.dart +++ b/lib/view/pages/absence_report/absence_report_view.dart @@ -165,23 +165,28 @@ class _AbsenceReportViewState extends State { Widget _buildBody(BuildContext context) => FutureBuilder( future: _init, builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const Center(child: AppProgressIndicator.large()); - } - if (snapshot.hasError) { - return PlaceholderView( - icon: Icons.error_outline, - text: errorToUserMessage(snapshot.error), - button: ElevatedButton.icon( - onPressed: () => setState(() => _init = _load()), - icon: const Icon(Icons.refresh), - label: const Text('Erneut versuchen'), - ), - ); - } - return _buildForm(context); - }, - ); + if (snapshot.connectionState != ConnectionState.done) { + return const Center(child: AppProgressIndicator.large()); + } + if (snapshot.hasError) { + return PlaceholderView( + icon: Icons.error_outline, + text: errorToUserMessage(snapshot.error), + button: ElevatedButton.icon( + onPressed: () { + final reload = _load(); + setState(() { + _init = reload; + }); + }, + icon: const Icon(Icons.refresh), + label: const Text('Erneut versuchen'), + ), + ); + } + return _buildForm(context); + }, + ); Widget _buildForm(BuildContext context) { final theme = Theme.of(context); diff --git a/lib/widget/user_avatar.dart b/lib/widget/user_avatar.dart index fdaf017..4a43e09 100644 --- a/lib/widget/user_avatar.dart +++ b/lib/widget/user_avatar.dart @@ -230,11 +230,15 @@ class _UserAvatarState extends State { final pending = _pendingAvatars.putIfAbsent(url, () { final future = _fetch(url); - future.whenComplete(() { - if (identical(_pendingAvatars[url], future)) { - _pendingAvatars.remove(url); - } - }); + // Cleanup hangs off an error-neutralised copy: whenComplete on `future` + // itself returns a second future that forwards the error unawaited. + unawaited( + future.then((_) {}, onError: (_) {}).whenComplete(() { + if (identical(_pendingAvatars[url], future)) { + _pendingAvatars.remove(url); + } + }), + ); return future; }); From 778c473631a12b01a52d59fb555cfac763fe7fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 28 Jul 2026 12:52:07 +0200 Subject: [PATCH 2/2] added telemetry call on app resume with 15 minute cooldown --- .../telemetry_heartbeat.dart | 8 +++--- lib/app.dart | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart b/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart index 5fb367f..8a8b2a5 100644 --- a/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart +++ b/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart @@ -12,8 +12,9 @@ import 'telemetry_device_id.dart'; /// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) — /// upserts the stable install id, platform, app version and device info. Sent -/// once on app start and again once push registration completes that session -/// (so a fresh registration isn't under-reported until the next launch). +/// on app start, again once push registration completes that session (so a +/// fresh registration isn't under-reported until the next launch), and on +/// resume after the app spent more than 15 minutes in the background. /// Bearer-authenticated via the shared dio interceptor. Replaces the legacy /// mhsl.eu `server/userIndex/update` call. class TelemetryHeartbeat extends MarianumConnectQuery { @@ -21,7 +22,8 @@ class TelemetryHeartbeat extends MarianumConnectQuery { /// Fire-and-forget: schedules a heartbeat and swallows any error, so a failed /// send never disrupts app start. Used from the app shell's initState and - /// re-emitted once push registration completes (see `_MainState._syncPush`). + /// lifecycle handler, and re-emitted once push registration completes (see + /// `_MainState._syncPush`). static void report({required bool notificationsEnabled}) { unawaited( TelemetryHeartbeat() diff --git a/lib/app.dart b/lib/app.dart index 8ce0b4f..deb94ac 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -46,9 +46,11 @@ class _AppState extends State with WidgetsBindingObserver { int _knownTotalTabs = 1; int _lastTabIndex = 0; bool _userOnLastTab = false; + DateTime? _lastTelemetryAt; static const Duration _chatListActiveInterval = Duration(seconds: 15); static const Duration _chatListIdleInterval = Duration(seconds: 60); + static const Duration _telemetryInterval = Duration(minutes: 15); void _onTabControllerChanged() { final newIndex = Main.bottomNavigator.index; @@ -73,10 +75,29 @@ class _AppState extends State with WidgetsBindingObserver { if (talkIsActive) bloc.refresh(); } + // Wall-clock throttle rather than Debouncer.throttle: a Timer does not tick + // reliably while the app is suspended, so the window would still be open on + // the resume it is supposed to let through. + void _reportTelemetry() { + if (!mounted) return; + final now = DateTime.now(); + final last = _lastTelemetryAt; + if (last != null && now.difference(last) < _telemetryInterval) return; + _lastTelemetryAt = now; + TelemetryHeartbeat.report( + notificationsEnabled: context + .read() + .val() + .notificationSettings + .enabled, + ); + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { log('AppLifecycle: $state'); if (state == AppLifecycleState.resumed) { + _reportTelemetry(); Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () { if (!mounted) return; log('Refreshing due to LifecycleChange'); @@ -178,10 +199,7 @@ class _AppState extends State with WidgetsBindingObserver { if (mounted) setState(() {}); }); - TelemetryHeartbeat.report( - notificationsEnabled: - context.read().val().notificationSettings.enabled, - ); + _reportTelemetry(); // A refreshed FCM token invalidates the existing push subscription — the // NC device identifier stays stable, so we simply re-register (NC first,