diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index b469440..123e486 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -6,6 +6,7 @@ import 'package:http/http.dart' as http; import 'package:simplecloudnotifier/models/api_error.dart'; import 'package:simplecloudnotifier/models/key_token_auth.dart'; import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/models/channel.dart'; @@ -67,7 +68,8 @@ class APIClient { responseHeaders = response.headers; } catch (exc, trace) { RequestLog.addRequestException(name, t0, method, uri, req.body, req.headers, exc, trace); - showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); + showPlatformToast(child: Text('Request "${name}" failed'), context: ToastProvider.context); + ApplicationLog.error('Request "${name}" failed: ' + exc.toString(), trace: trace); rethrow; } @@ -78,7 +80,9 @@ class APIClient { RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr); showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); throw Exception(apierr.message); - } catch (_) {} + } catch (exc, trace) { + ApplicationLog.warn('Failed to decode api response as error-object', additional: exc.toString() + "\nBody:\n" + responseBody, trace: trace); + } RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); @@ -99,6 +103,7 @@ class APIClient { } catch (exc, trace) { RequestLog.addRequestDecodeError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, exc, trace); showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); + ApplicationLog.error('Failed to decode response: ' + exc.toString(), additional: "\nBody:\n" + responseBody, trace: trace); rethrow; } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6699d77..d715741 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -17,29 +17,31 @@ void main() async { Hive.registerAdapter(SCNRequestAdapter()); Hive.registerAdapter(SCNLogAdapter()); + Hive.registerAdapter(SCNLogLevelAdapter()); try { await Hive.openBox('scn-requests'); - await Hive.openBox('scn-logs'); - } catch (e) { - print(e); + } catch (exc, trace) { Hive.deleteBoxFromDisk('scn-requests'); - Hive.deleteBoxFromDisk('scn-logs'); await Hive.openBox('scn-requests'); - await Hive.openBox('scn-logs'); + ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace); } + try { + await Hive.openBox('scn-logs'); + } catch (exc, trace) { + Hive.deleteBoxFromDisk('scn-logs'); + await Hive.openBox('scn-logs'); + ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace); + } + + ApplicationLog.debug('Application started'); + runApp( MultiProvider( providers: [ - ChangeNotifierProvider( - create: (context) => UserAccount(), - lazy: false, - ), - ChangeNotifierProvider( - create: (context) => AppTheme(), - lazy: false, - ), + ChangeNotifierProvider(create: (context) => UserAccount(), lazy: false), + ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false), ], child: const SCNApp(), ), diff --git a/flutter/lib/models/message.dart b/flutter/lib/models/message.dart index cee7fca..563ede4 100644 --- a/flutter/lib/models/message.dart +++ b/flutter/lib/models/message.dart @@ -35,13 +35,13 @@ class Message { senderUserID: json['sender_user_id'] as String, channelInternalName: json['channel_internal_name'] as String, channelID: json['channel_id'] as String, - senderName: json['sender_name'] as String, + senderName: json['sender_name'] as String?, senderIP: json['sender_ip'] as String, timestamp: json['timestamp'] as String, title: json['title'] as String, - content: json['content'] as String, + content: json['content'] as String?, priority: json['priority'] as int, - userMessageID: json['usr_message_id'] as String, + userMessageID: json['usr_message_id'] as String?, usedKeyID: json['used_key_id'] as String, trimmed: json['trimmed'] as bool, ); diff --git a/flutter/lib/nav_layout.dart b/flutter/lib/nav_layout.dart index 47cbc42..967d1f5 100644 --- a/flutter/lib/nav_layout.dart +++ b/flutter/lib/nav_layout.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:simplecloudnotifier/components/layout/app_bar.dart'; -import 'package:simplecloudnotifier/pages/channel_list/root.dart'; +import 'package:simplecloudnotifier/pages/channel_list/channel_list.dart'; import 'package:simplecloudnotifier/pages/send/root.dart'; import 'package:simplecloudnotifier/components/bottom_fab/fab_bottom_app_bar.dart'; import 'package:simplecloudnotifier/pages/account/root.dart'; diff --git a/flutter/lib/pages/account/login.dart b/flutter/lib/pages/account/login.dart index fc6ce0c..3ed8361 100644 --- a/flutter/lib/pages/account/login.dart +++ b/flutter/lib/pages/account/login.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/key_token_auth.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/user_account.dart'; class AccountLoginPage extends StatefulWidget { @@ -75,7 +76,7 @@ class _AccountLoginPageState extends State { if (verified) { msgr.showSnackBar( const SnackBar( - content: Text('Data ok'), + content: Text('Data ok'), //TODO use toast? ), ); prov.setToken(KeyTokenAuth(userId: uid, token: tok)); @@ -84,12 +85,12 @@ class _AccountLoginPageState extends State { } else { msgr.showSnackBar( const SnackBar( - content: Text('Failed to verify token'), + content: Text('Failed to verify token'), //TODO use toast? ), ); } - } catch (e) { - //TODO + } catch (exc, trace) { + ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace); } } } diff --git a/flutter/lib/pages/channel_list/root.dart b/flutter/lib/pages/channel_list/channel_list.dart similarity index 88% rename from flutter/lib/pages/channel_list/root.dart rename to flutter/lib/pages/channel_list/channel_list.dart index 4889718..e341a41 100644 --- a/flutter/lib/pages/channel_list/root.dart +++ b/flutter/lib/pages/channel_list/channel_list.dart @@ -3,6 +3,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/user_account.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart'; @@ -44,8 +45,9 @@ class _ChannelRootPageState extends State { final items = await APIClient.getChannelList(acc.auth!, ChannelSelector.all); _pagingController.appendLastPage(items); - } catch (error) { - _pagingController.error = error; + } catch (exc, trace) { + _pagingController.error = exc.toString(); + ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace); } } diff --git a/flutter/lib/pages/debug/debug_logs.dart b/flutter/lib/pages/debug/debug_logs.dart index e95c5ec..be8f5d2 100644 --- a/flutter/lib/pages/debug/debug_logs.dart +++ b/flutter/lib/pages/debug/debug_logs.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; class DebugLogsPage extends StatefulWidget { @override @@ -6,8 +9,97 @@ class DebugLogsPage extends StatefulWidget { } class _DebugLogsPageState extends State { + Box logBox = Hive.box('scn-logs'); + + static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); + @override Widget build(BuildContext context) { - return Container(/* Add your UI components here */); + return Container( + child: ValueListenableBuilder( + valueListenable: logBox.listenable(), + builder: (context, Box box, _) { + return ListView.builder( + itemCount: logBox.length, + itemBuilder: (context, listIndex) { + final log = logBox.getAt(logBox.length - listIndex - 1)!; + switch (log.level) { + case SCNLogLevel.debug: + return buildItem(context, log, Theme.of(context).hintColor); + case SCNLogLevel.info: + return buildItem(context, log, Colors.blueAccent); + case SCNLogLevel.warning: + return buildItem(context, log, Colors.orangeAccent); + case SCNLogLevel.error: + return buildItem(context, log, Colors.redAccent); + case SCNLogLevel.fatal: + return buildItem(context, log, Colors.black); + } + }, + ); + }, + ), + ); + } + + Widget buildItem(BuildContext context, SCNLog log, Color tagColor) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0), + child: Card.filled( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.fromLTRB(12, 1, 12, 1), + decoration: BoxDecoration( + color: tagColor, + borderRadius: BorderRadius.only(topLeft: Radius.circular(8)), + ), + child: Text( + log.level.name.toUpperCase(), + style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: 14), + ), + ), + Expanded(child: SizedBox()), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), + child: Text(_dateFormat.format(log.timestamp), style: TextStyle(fontSize: 12)), + ), + ], + ), + SizedBox(height: 4), + if (log.message.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Text(log.message, style: TextStyle(fontWeight: FontWeight.bold)), + ), + if (log.additional.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: SelectableText( + log.additional, + style: TextStyle(fontSize: 12), + minLines: 1, + maxLines: 10, + ), + ), + if (log.trace.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: SelectableText( + log.trace, + style: TextStyle(fontSize: 12), + minLines: 1, + maxLines: 10, + ), + ), + SizedBox(height: 8), + ], + ), + ), + ); } } diff --git a/flutter/lib/pages/debug/debug_persistence.dart b/flutter/lib/pages/debug/debug_persistence.dart index fb0d9be..cd3ebeb 100644 --- a/flutter/lib/pages/debug/debug_persistence.dart +++ b/flutter/lib/pages/debug/debug_persistence.dart @@ -1,4 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart'; +import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/request_log.dart'; class DebugPersistencePage extends StatefulWidget { @override @@ -6,8 +12,78 @@ class DebugPersistencePage extends StatefulWidget { } class _DebugPersistencePageState extends State { + SharedPreferences? prefs = null; + + @override + void initState() { + super.initState(); + + SharedPreferences.getInstance().then((value) => setState(() => prefs = value)); + } + @override Widget build(BuildContext context) { - return Container(/* Add your UI components here */); + return Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card.outlined( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => DebugSharedPrefPage(sharedPref: prefs!))); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 30, child: Text('')), + Expanded(child: Text('Shared Preferences', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)), + SizedBox(width: 30, child: Text('${prefs?.getKeys().length.toString()}', textAlign: TextAlign.end)), + ], + ), + ), + ), + ), + Card.outlined( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => DebugHiveBoxPage(boxName: 'scn-requests', box: Hive.box('scn-requests')))); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 30, child: Text('')), + Expanded(child: Text('Hive [scn-requests]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)), + SizedBox(width: 30, child: Text('${Hive.box('scn-requests').length.toString()}', textAlign: TextAlign.end)), + ], + ), + ), + ), + ), + Card.outlined( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => DebugHiveBoxPage(boxName: 'scn-requests', box: Hive.box('scn-logs')))); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 30, child: Text('')), + Expanded(child: Text('Hive [scn-logs]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)), + SizedBox(width: 30, child: Text('${Hive.box('scn-logs').length.toString()}', textAlign: TextAlign.end)), + ], + ), + ), + ), + ), + ], + ), + ); } } diff --git a/flutter/lib/pages/debug/debug_persistence_hive.dart b/flutter/lib/pages/debug/debug_persistence_hive.dart new file mode 100644 index 0000000..c58a709 --- /dev/null +++ b/flutter/lib/pages/debug/debug_persistence_hive.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/pages/debug/debug_persistence_hiveentry.dart'; +import 'package:simplecloudnotifier/state/interfaces.dart'; + +class DebugHiveBoxPage extends StatelessWidget { + final String boxName; + final Box box; + + DebugHiveBoxPage({required this.boxName, required this.box}); + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: 'Hive: ' + boxName, + showSearch: false, + showDebug: false, + child: ListView.separated( + itemCount: box.length, + itemBuilder: (context, listIndex) { + return GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => DebugHiveEntryPage(value: box.getAt(listIndex)!))); + }, + child: ListTile( + title: Text(box.getAt(listIndex).toString(), style: TextStyle(fontWeight: FontWeight.bold)), + ), + ); + }, + separatorBuilder: (context, index) => Divider(), + ), + ); + } +} diff --git a/flutter/lib/pages/debug/debug_persistence_hiveentry.dart b/flutter/lib/pages/debug/debug_persistence_hiveentry.dart new file mode 100644 index 0000000..52a2e52 --- /dev/null +++ b/flutter/lib/pages/debug/debug_persistence_hiveentry.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/state/interfaces.dart'; + +class DebugHiveEntryPage extends StatelessWidget { + final FieldDebuggable value; + final List<(String, String)> fields; + + DebugHiveEntryPage({required this.value}) : fields = value.debugFieldList(); + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: 'HiveEntry', + showSearch: false, + showDebug: false, + child: ListView.separated( + itemCount: fields.length, + itemBuilder: (context, listIndex) { + return ListTile( + title: Text(fields[listIndex].$1, style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(fields[listIndex].$2, style: TextStyle(fontFamily: "monospace")), + ); + }, + separatorBuilder: (context, index) => Divider(), + ), + ); + } +} diff --git a/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart b/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart new file mode 100644 index 0000000..d927dbe --- /dev/null +++ b/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; + +class DebugSharedPrefPage extends StatelessWidget { + final SharedPreferences sharedPref; + final List keys; + + DebugSharedPrefPage({required this.sharedPref}) : keys = sharedPref.getKeys().toList(); + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: 'SharedPreferences', + showSearch: false, + showDebug: false, + child: ListView.separated( + itemCount: sharedPref.getKeys().length, + itemBuilder: (context, listIndex) { + return ListTile( + title: Text(keys[listIndex], style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(sharedPref.get(keys[listIndex]).toString()), + ); + }, + separatorBuilder: (context, index) => Divider(), + ), + ); + } +} diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 46a00c6..f63ec8d 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -5,6 +5,7 @@ import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/pages/message_view/message_view.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/user_account.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart'; @@ -57,10 +58,9 @@ class _MessageListPageState extends State { } else { _pagingController.appendPage(newItems, npt); } - } catch (error) { - print("API-Error: "); //TODO remove me, proper error handling - print(error); //TODO remove me, proper error handling - _pagingController.error = error; + } catch (exc, trace) { + _pagingController.error = exc.toString(); + ApplicationLog.error('Failed to list messages: ' + exc.toString(), trace: trace); } } diff --git a/flutter/lib/pages/send/root.dart b/flutter/lib/pages/send/root.dart index d0aa084..b2b496d 100644 --- a/flutter/lib/pages/send/root.dart +++ b/flutter/lib/pages/send/root.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:simplecloudnotifier/state/user_account.dart'; @@ -151,8 +152,8 @@ class _SendRootPageState extends State { } else { // TODO ("Cannot open URL"); } - } catch (e) { - // TODO ('Cannot open URL'); + } catch (exc, trace) { + ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace); } } } diff --git a/flutter/lib/state/application_log.dart b/flutter/lib/state/application_log.dart index e96b75e..071195c 100644 --- a/flutter/lib/state/application_log.dart +++ b/flutter/lib/state/application_log.dart @@ -1,29 +1,128 @@ import 'package:hive_flutter/hive_flutter.dart'; +import 'package:simplecloudnotifier/state/interfaces.dart'; +import 'package:xid/xid.dart'; part 'application_log.g.dart'; -class ApplicationLog {} +class ApplicationLog { + static void debug(String message, {String? additional, StackTrace? trace}) { + print('[DEBUG] ${message}: ${additional ?? ''}'); -enum SCNLogLevel { debug, info, warning, error, fatal } + Hive.box('scn-logs').add(SCNLog( + id: Xid().toString(), + timestamp: DateTime.now(), + level: SCNLogLevel.debug, + message: message, + additional: additional ?? '', + trace: trace?.toString() ?? '', + )); + } + + static void info(String message, {String? additional, StackTrace? trace}) { + print('[INFO] ${message}: ${additional ?? ''}'); + + Hive.box('scn-logs').add(SCNLog( + id: Xid().toString(), + timestamp: DateTime.now(), + level: SCNLogLevel.info, + message: message, + additional: additional ?? '', + trace: trace?.toString() ?? '', + )); + } + + static void warn(String message, {String? additional, StackTrace? trace}) { + print('[WARN] ${message}: ${additional ?? ''}'); + + Hive.box('scn-logs').add(SCNLog( + id: Xid().toString(), + timestamp: DateTime.now(), + level: SCNLogLevel.warning, + message: message, + additional: additional ?? '', + trace: trace?.toString() ?? '', + )); + } + + static void error(String message, {String? additional, StackTrace? trace}) { + print('[ERROR] ${message}: ${additional ?? ''}'); + + Hive.box('scn-logs').add(SCNLog( + id: Xid().toString(), + timestamp: DateTime.now(), + level: SCNLogLevel.error, + message: message, + additional: additional ?? '', + trace: trace?.toString() ?? '', + )); + } + + static void fatal(String message, {String? additional, StackTrace? trace}) { + print('[FATAL] ${message}: ${additional ?? ''}'); + + Hive.box('scn-logs').add(SCNLog( + id: Xid().toString(), + timestamp: DateTime.now(), + level: SCNLogLevel.fatal, + message: message, + additional: additional ?? '', + trace: trace?.toString() ?? '', + )); + } +} + +@HiveType(typeId: 103) +enum SCNLogLevel { + @HiveField(0) + debug, + @HiveField(1) + info, + @HiveField(2) + warning, + @HiveField(3) + error, + @HiveField(4) + fatal +} @HiveType(typeId: 101) -class SCNLog extends HiveObject { +class SCNLog extends HiveObject implements FieldDebuggable { @HiveField(0) + final String id; + + @HiveField(10) final DateTime timestamp; - @HiveField(1) + @HiveField(11) final SCNLogLevel level; - @HiveField(2) + @HiveField(12) final String message; - @HiveField(3) + @HiveField(13) final String additional; - @HiveField(4) + @HiveField(14) final String trace; - SCNLog( - this.timestamp, - this.level, - this.message, - this.additional, - this.trace, - ); + SCNLog({ + required this.id, + required this.timestamp, + required this.level, + required this.message, + required this.additional, + required this.trace, + }); + + @override + String toString() { + return 'SCNLog[${this.id}]'; + } + + List<(String, String)> debugFieldList() { + return [ + ('id', this.id), + ('timestamp', this.timestamp.toIso8601String()), + ('level', this.level.name), + ('message', this.message), + ('additional', this.additional), + ('trace', this.trace), + ]; + } } diff --git a/flutter/lib/state/application_log.g.dart b/flutter/lib/state/application_log.g.dart index 36426b0..3de5e91 100644 --- a/flutter/lib/state/application_log.g.dart +++ b/flutter/lib/state/application_log.g.dart @@ -17,27 +17,30 @@ class SCNLogAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return SCNLog( - fields[0] as DateTime, - fields[1] as SCNLogLevel, - fields[2] as String, - fields[3] as String, - fields[4] as String, + id: fields[0] as String, + timestamp: fields[10] as DateTime, + level: fields[11] as SCNLogLevel, + message: fields[12] as String, + additional: fields[13] as String, + trace: fields[14] as String, ); } @override void write(BinaryWriter writer, SCNLog obj) { writer - ..writeByte(5) + ..writeByte(6) ..writeByte(0) + ..write(obj.id) + ..writeByte(10) ..write(obj.timestamp) - ..writeByte(1) + ..writeByte(11) ..write(obj.level) - ..writeByte(2) + ..writeByte(12) ..write(obj.message) - ..writeByte(3) + ..writeByte(13) ..write(obj.additional) - ..writeByte(4) + ..writeByte(14) ..write(obj.trace); } @@ -51,3 +54,57 @@ class SCNLogAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class SCNLogLevelAdapter extends TypeAdapter { + @override + final int typeId = 103; + + @override + SCNLogLevel read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return SCNLogLevel.debug; + case 1: + return SCNLogLevel.info; + case 2: + return SCNLogLevel.warning; + case 3: + return SCNLogLevel.error; + case 4: + return SCNLogLevel.fatal; + default: + return SCNLogLevel.debug; + } + } + + @override + void write(BinaryWriter writer, SCNLogLevel obj) { + switch (obj) { + case SCNLogLevel.debug: + writer.writeByte(0); + break; + case SCNLogLevel.info: + writer.writeByte(1); + break; + case SCNLogLevel.warning: + writer.writeByte(2); + break; + case SCNLogLevel.error: + writer.writeByte(3); + break; + case SCNLogLevel.fatal: + writer.writeByte(4); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SCNLogLevelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flutter/lib/state/interfaces.dart b/flutter/lib/state/interfaces.dart new file mode 100644 index 0000000..89282be --- /dev/null +++ b/flutter/lib/state/interfaces.dart @@ -0,0 +1,3 @@ +abstract class FieldDebuggable { + List<(String, String)> debugFieldList(); +} diff --git a/flutter/lib/state/request_log.dart b/flutter/lib/state/request_log.dart index 1014217..28d404f 100644 --- a/flutter/lib/state/request_log.dart +++ b/flutter/lib/state/request_log.dart @@ -1,11 +1,14 @@ -import 'package:hive/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:simplecloudnotifier/models/api_error.dart'; +import 'package:simplecloudnotifier/state/interfaces.dart'; +import 'package:xid/xid.dart'; part 'request_log.g.dart'; class RequestLog { static void addRequestException(String name, DateTime tStart, String method, Uri uri, String reqbody, Map reqheaders, dynamic e, StackTrace trace) { Hive.box('scn-requests').add(SCNRequest( + id: Xid().toString(), timestampStart: tStart, timestampEnd: DateTime.now(), name: name, @@ -24,6 +27,7 @@ class RequestLog { static void addRequestAPIError(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders, APIError apierr) { Hive.box('scn-requests').add(SCNRequest( + id: Xid().toString(), timestampStart: t0, timestampEnd: DateTime.now(), name: name, @@ -42,6 +46,7 @@ class RequestLog { static void addRequestErrorStatuscode(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders) { Hive.box('scn-requests').add(SCNRequest( + id: Xid().toString(), timestampStart: t0, timestampEnd: DateTime.now(), name: name, @@ -60,6 +65,7 @@ class RequestLog { static void addRequestSuccess(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders) { Hive.box('scn-requests').add(SCNRequest( + id: Xid().toString(), timestampStart: t0, timestampEnd: DateTime.now(), name: name, @@ -78,6 +84,7 @@ class RequestLog { static void addRequestDecodeError(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders, Object exc, StackTrace trace) { Hive.box('scn-requests').add(SCNRequest( + id: Xid().toString(), timestampStart: t0, timestampEnd: DateTime.now(), name: name, @@ -96,37 +103,41 @@ class RequestLog { } @HiveType(typeId: 100) -class SCNRequest extends HiveObject { +class SCNRequest extends HiveObject implements FieldDebuggable { @HiveField(0) + final String id; + + @HiveField(10) final DateTime timestampStart; - @HiveField(1) + @HiveField(11) final DateTime timestampEnd; - @HiveField(2) + @HiveField(12) final String name; - @HiveField(3) - final String type; - @HiveField(4) + @HiveField(13) + final String type; // SUCCESS | EXCEPTION | API_ERROR | ERROR_STATUSCODE | DECODE_ERROR + @HiveField(14) final String error; - @HiveField(5) + @HiveField(15) final String stackTrace; - @HiveField(6) + @HiveField(21) final String method; - @HiveField(7) + @HiveField(22) final String url; - @HiveField(8) + @HiveField(23) final Map requestHeaders; - @HiveField(12) + @HiveField(24) final String requestBody; - @HiveField(9) + @HiveField(31) final int responseStatusCode; - @HiveField(10) + @HiveField(32) final Map responseHeaders; - @HiveField(11) + @HiveField(33) final String responseBody; SCNRequest({ + required this.id, required this.timestampStart, required this.timestampEnd, required this.name, @@ -141,4 +152,28 @@ class SCNRequest extends HiveObject { required this.error, required this.stackTrace, }); + + @override + String toString() { + return 'SCNRequest[${this.id}]'; + } + + List<(String, String)> debugFieldList() { + return [ + ('id', this.id), + ('timestampStart', this.timestampStart.toIso8601String()), + ('timestampEnd', this.timestampEnd.toIso8601String()), + ('name', this.name), + ('method', this.method), + ('url', this.url), + for (var (idx, item) in this.requestHeaders.entries.indexed) ('requestHeaders[$idx]', '${item.key}=${item.value}'), + ('requestBody', this.requestBody), + ('responseStatusCode', this.responseStatusCode.toString()), + for (var (idx, item) in this.responseHeaders.entries.indexed) ('responseHeaders[$idx]', '${item.key}=${item.value}'), + ('responseBody', this.responseBody), + ('type', this.type), + ('error', this.error), + ('stackTrace', this.stackTrace), + ]; + } } diff --git a/flutter/lib/state/request_log.g.dart b/flutter/lib/state/request_log.g.dart index c2007c6..9419fa0 100644 --- a/flutter/lib/state/request_log.g.dart +++ b/flutter/lib/state/request_log.g.dart @@ -17,51 +17,54 @@ class SCNRequestAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return SCNRequest( - timestampStart: fields[0] as DateTime, - timestampEnd: fields[1] as DateTime, - name: fields[2] as String, - method: fields[6] as String, - url: fields[7] as String, - requestHeaders: (fields[8] as Map).cast(), - requestBody: fields[12] as String, - responseStatusCode: fields[9] as int, - responseHeaders: (fields[10] as Map).cast(), - responseBody: fields[11] as String, - type: fields[3] as String, - error: fields[4] as String, - stackTrace: fields[5] as String, + id: fields[0] as String, + timestampStart: fields[10] as DateTime, + timestampEnd: fields[11] as DateTime, + name: fields[12] as String, + method: fields[21] as String, + url: fields[22] as String, + requestHeaders: (fields[23] as Map).cast(), + requestBody: fields[24] as String, + responseStatusCode: fields[31] as int, + responseHeaders: (fields[32] as Map).cast(), + responseBody: fields[33] as String, + type: fields[13] as String, + error: fields[14] as String, + stackTrace: fields[15] as String, ); } @override void write(BinaryWriter writer, SCNRequest obj) { writer - ..writeByte(13) + ..writeByte(14) ..writeByte(0) - ..write(obj.timestampStart) - ..writeByte(1) - ..write(obj.timestampEnd) - ..writeByte(2) - ..write(obj.name) - ..writeByte(3) - ..write(obj.type) - ..writeByte(4) - ..write(obj.error) - ..writeByte(5) - ..write(obj.stackTrace) - ..writeByte(6) - ..write(obj.method) - ..writeByte(7) - ..write(obj.url) - ..writeByte(8) - ..write(obj.requestHeaders) - ..writeByte(12) - ..write(obj.requestBody) - ..writeByte(9) - ..write(obj.responseStatusCode) + ..write(obj.id) ..writeByte(10) - ..write(obj.responseHeaders) + ..write(obj.timestampStart) ..writeByte(11) + ..write(obj.timestampEnd) + ..writeByte(12) + ..write(obj.name) + ..writeByte(13) + ..write(obj.type) + ..writeByte(14) + ..write(obj.error) + ..writeByte(15) + ..write(obj.stackTrace) + ..writeByte(21) + ..write(obj.method) + ..writeByte(22) + ..write(obj.url) + ..writeByte(23) + ..write(obj.requestHeaders) + ..writeByte(24) + ..write(obj.requestBody) + ..writeByte(31) + ..write(obj.responseStatusCode) + ..writeByte(32) + ..write(obj.responseHeaders) + ..writeByte(33) ..write(obj.responseBody); } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 254cac0..20ac11e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -280,7 +280,7 @@ packages: source: hosted version: "2.3.1" hive: - dependency: "direct main" + dependency: transitive description: name: hive sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" @@ -884,6 +884,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xid: + dependency: "direct main" + description: + name: xid + sha256: "58b61c7c1234810afa500cde7556d7c407ce72154e0d5a8a5618690f03848dbd" + url: "https://pub.dev" + source: hosted + version: "1.2.1" yaml: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 9a98857..bf0513d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: flutter: sdk: flutter + font_awesome_flutter: '>= 4.7.0' cupertino_icons: ^1.0.2 http: ^1.2.0 @@ -20,10 +21,10 @@ dependencies: infinite_scroll_pagination: ^4.0.0 intl: ^0.19.0 path_provider: ^2.1.3 - hive: ^2.2.3 hive_flutter: ^1.1.0 package_info_plus: ^8.0.0 fl_toast: ^3.2.0 + xid: ^1.2.1 dependency_overrides: