Files
Client/lib/view/pages/settings/diagnostics_page.dart
T

229 lines
8.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../api/marianumconnect/auth/device_token_name.dart';
import '../../../api/marianumconnect/auth/token_storage.dart';
import '../../../api/marianumconnect/marianumconnect_endpoint.dart';
import '../../../api/marianumconnect/queries/telemetry_heartbeat/telemetry_device_id.dart';
import '../../../background/widget_background_task.dart';
import '../../../extensions/date_time.dart';
import '../../../model/endpoint_data.dart';
import '../../../push/push_registration_store.dart';
import '../../../push/push_registration_type.dart';
import '../../../session/session.dart';
import '../../../session/session_manager.dart';
import '../../../state/app/modules/account/bloc/account_bloc.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../utils/clipboard_helper.dart';
import '../../../widget/app_progress_indicator.dart';
import '../../../widget/async_action_button.dart';
import '../../../widget/centered_leading.dart';
import '../../../widget_data/widget_sync.dart';
import 'widgets/push_status_sheet.dart';
typedef _Row = ({String label, String value});
typedef _Section = ({String title, List<_Row> rows});
/// Read-only snapshot of build, endpoint, session, push and widget state for
/// bug reports. Secrets (passwords, bearer token) are never shown.
class DiagnosticsPage extends StatefulWidget {
const DiagnosticsPage({super.key});
@override
State<DiagnosticsPage> createState() => _DiagnosticsPageState();
}
class _DiagnosticsPageState extends State<DiagnosticsPage> {
late Future<List<_Section>> _sections = _collect();
Future<List<_Section>> _collect() async {
final settings = context.read<SettingsCubit>().val();
final accountStatus = context.read<AccountBloc>().state.status;
final session = SessionManager().current;
final nextcloud = session?.nextcloud;
const tokens = MarianumConnectTokenStorage();
final push = PushRegistrationStore();
Future<String> safe(Future<Object?> Function() read) async {
try {
final value = await read().timeout(const Duration(seconds: 5));
return switch (value) {
null => '–',
bool() => value ? 'ja' : 'nein',
DateTime() => '${value.formatDateTime()} (${value.formatRelative()})',
_ => '$value',
};
} on Object catch (e) {
return 'Fehler: $e';
}
}
Future<List<_Row>> rows(Map<String, Future<Object?> Function()> reads) =>
Future.wait(
reads.entries.map(
(e) async => (label: e.key, value: await safe(e.value)),
),
);
final info = await PackageInfo.fromPlatform();
return [
(
title: 'App',
rows: await rows({
'Version': () async => '${info.version} (${info.buildNumber})',
'Paket': () async => info.packageName,
'Build-Modus': () async => kReleaseMode
? 'Release'
: kProfileMode
? 'Profile'
: 'Debug',
'Plattform': () async =>
'${Platform.operatingSystem} ${Platform.operatingSystemVersion}',
'Gerät': DeviceTokenName.resolve,
'Dart': () async => Platform.version.split(' ').first,
}),
),
(
title: 'Server',
rows: await rows({
'Marianum Connect': () async => MarianumConnectEndpoint.current(),
'Nextcloud': () async =>
'https://${EndpointData().nextcloud().full()}',
'Notfall-Nachricht': () async =>
settings.devToolsSettings.resolveEmergencyNoticeUrl(),
}),
),
(
title: 'Sitzung',
rows: await rows({
'Status': () async => accountStatus.name,
'Benutzer': () async => switch (session) {
CredentialSession(:final username) => username,
null => null,
},
'Demo-Modus': () async => session?.isDemo ?? false,
'Login Flow v2': () async => nextcloud?.usesLoginFlow ?? false,
'App-Passwort (Dateien)': () async =>
nextcloud?.hasAppPassword ?? false,
'App-Passwort (Talk)': () async =>
nextcloud?.hasAppPasswordTalk ?? false,
'Session-Epoche': () async => SessionManager().sessionEpoch,
'MC-Token-ID': tokens.readTokenId,
'MC-Token läuft ab': tokens.readExpiresAt,
}),
),
(
title: 'Push',
rows: await rows({
'Benachrichtigungen': () async =>
settings.notificationSettings.enabled,
'FCM-Token': FirebaseMessaging.instance.getToken,
for (final type in PushRegistrationType.values) ...{
'Registriert (${type.name})': () => push.isRegistered(type),
'Letzte Registrierung (${type.name})': () =>
push.lastRegistrationAt(type),
'Letzter Fehler (${type.name})': () =>
push.lastRegistrationError(type),
},
}),
),
(
title: 'Sonstiges',
rows: await rows({
'Telemetrie-Geräte-ID': TelemetryDeviceId.resolve,
'Widget zuletzt abgerufen': WidgetSync.getFetchedAt,
}),
),
];
}
String _asText(List<_Section> sections) => [
for (final s in sections) ...[
'## ${s.title}',
for (final r in s.rows) '${r.label}: ${r.value}',
'',
],
].join('\n');
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Diagnose'),
actions: [
IconButton(
tooltip: 'Neu laden',
icon: const Icon(Icons.refresh),
onPressed: () => setState(() {
_sections = _collect();
}),
),
FutureBuilder(
future: _sections,
builder: (context, snapshot) => IconButton(
tooltip: 'Alles kopieren',
icon: const Icon(Icons.copy_all_outlined),
onPressed: snapshot.hasData
? () => copyToClipboard(context, _asText(snapshot.data!))
: null,
),
),
],
),
body: FutureBuilder(
future: _sections,
builder: (context, snapshot) {
final sections = snapshot.data;
if (sections == null) {
return const Center(child: AppProgressIndicator.large());
}
return ListView(
children: [
for (final section in sections) ...[
ListTile(
dense: true,
title: Text(
section.title,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
for (final row in section.rows)
ListTile(
dense: true,
title: Text(row.label),
subtitle: SelectableText(row.value),
onLongPress: () => copyToClipboard(context, row.value),
),
if (section.title == 'Push')
ListTile(
leading: const CenteredLeading(
Icon(Icons.notifications_active_outlined),
),
title: const Text('Push-Status prüfen'),
trailing: const Icon(Icons.arrow_right),
onTap: () => showPushStatusSheet(context),
),
if (section.title == 'Sonstiges')
AsyncListTile(
leading: const CenteredLeading(Icon(Icons.widgets_outlined)),
title: const Text('Widget jetzt aktualisieren'),
closeOnSuccess: false,
onPressed: WidgetBackgroundTask.requestImmediateRefresh,
),
const Divider(),
],
],
);
},
),
);
}