229 lines
8.0 KiB
Dart
229 lines
8.0 KiB
Dart
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(),
|
||
],
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|