import 'dart:convert'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; import '../../about/internal/application/ServerProfile.dart'; import '../../about/internal/application/ServiceConfig.dart'; import '../../comms/Comms.dart' show LogLine; import '../parent/ParentViewModel.dart'; import 'ConnectLogs.dart'; class ViewLogs extends ParentViewModel { ConnectLogs connection; ViewLogs(super.context, this.connection); /// Log lines as they arrive from the server. The screen subscribes to this /// directly — a stream of remote output has no request/response shape to /// hand back through the Connect interface one call at a time. Stream get logs => getDataManager().logs; /// Pulls the journal for [service], optionally bounded by a date range. void fetchLogs( ServerProfile server, ServiceConfig service, { DateTime? since, DateTime? until, }) async { if (!await canReach( server, () => fetchLogs(server, service, since: since, until: until), )) { return; } getDataManager().stopLogs(); try { await getDataManager().fetchLogs( server, service, since: since, until: until, ); connection.onLogsFetched(); } catch (error) { connection.onLogsFetched(); handleError( error, () => fetchLogs(server, service, since: since, until: until), () {}, "Retry", ); } } /// Reads the unit's systemd state. Failures are reported as `unknown` /// rather than raised — the status pill is ambient, not an operation the /// user asked for. void loadStatus(ServerProfile server, ServiceConfig service) async { try { final status = await getDataManager().serviceStatus(server, service); connection.onStatusLoaded(status); } catch (_) { connection.onStatusLoaded('unknown'); } } /// start | stop | restart on the service's unit. void control( ServerProfile server, ServiceConfig service, String action, ) async { if (!await canReach(server, () => control(server, service, action))) return; getDataManager().stopLogs(); try { final result = await getDataManager().serviceControl( server, service, action, ); connection.onControlFinished(result.message); loadStatus(server, service); } catch (error) { handleError( error, () => control(server, service, action), () {}, "Retry", ); } } /// Starts or stops the live tail. void toggleFollow( ServerProfile server, ServiceConfig service, bool currentlyFollowing, ) async { if (currentlyFollowing) { getDataManager().stopLogs(); connection.onFollowChanged(false); return; } if (!await canReach( server, () => toggleFollow(server, service, currentlyFollowing), )) { return; } connection.onFollowChanged(true); try { await getDataManager().followLogs(server, service); } catch (error) { connection.onFollowChanged(false); handleError( error, () => toggleFollow(server, service, false), () {}, "Retry", ); } } /// Writes [lines] to a file the user picks. void saveLogs(String serviceName, List lines) async { try { final body = lines.join('\n'); final stamp = DateTime.now() .toIso8601String() .replaceAll(':', '-') .split('.') .first; final safeName = serviceName.replaceAll(RegExp(r'[^A-Za-z0-9_-]'), '_'); final path = await FilePicker.saveFile( dialogTitle: 'Save logs', fileName: '$safeName-logs-$stamp.txt', bytes: Uint8List.fromList(utf8.encode(body)), ); connection.onLogsSaved(path); } catch (error) { handleError(error, () => saveLogs(serviceName, lines), () {}, "Retry"); } } /// Stops any tail still running when the screen goes away. void stopLogs() => getDataManager().stopLogs(); }