Files
PeachDeployments/lib/Peach/see/logs/ViewLogs.dart
alvocool 38881d1fe1 Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:15:30 +03:00

148 lines
4.0 KiB
Dart

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<LogLine> 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<String> 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();
}