feat(studio): module uninstall + Doctor daemon control (v0.17.0)
UI parity for Windows operators who never touch a shell:
- Module sheet gains an Uninstall button at the bottom.
Two-step confirmation, then calls UninstallModule + closes
the sheet + refreshes the Modules page.
- Doctor page picks up two new sections:
* Daemon files — log / config / audit DB / modules /
flows / pid, each with a one-click "Open" button that
shells out to the OS handler (open / xdg-open /
explorer).
* Daemon control — Restart / Stop / Status buttons that
spawn `fai daemon …`. Captured stdout/stderr renders
inline so the operator sees what happened.
- Update banner gains an "Apply update" button that spawns
`fai update apply --channel <c>`. The previous version
showed the command as text — Windows users had no way to
execute it.
- Event log panel gains a "Verify now" button that re-runs
the chain check (via the existing doctor refresh).
New SystemActions helper resolves the `fai` binary via
$FAI_BIN → PATH → `~/.fai/bin/fai` (or the Windows
equivalent), so the buttons work whether the operator
restarted their shell after installing or not.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
d9aabfd00a
commit
73e394b39f
8 changed files with 624 additions and 49 deletions
|
|
@ -303,6 +303,29 @@ class HubService {
|
|||
return (purged: r.purged.toInt(), channel: r.channel);
|
||||
}
|
||||
|
||||
/// Filesystem paths the daemon owns. Used by Doctor to wire
|
||||
/// "Open log file" / "Open config file" buttons.
|
||||
Future<DaemonPathsSnapshot> daemonPaths() async {
|
||||
final r = await _client.daemonPaths();
|
||||
return DaemonPathsSnapshot(
|
||||
logPath: r.logPath,
|
||||
dbPath: r.dbPath,
|
||||
modulesDir: r.modulesDir,
|
||||
flowsDir: r.flowsDir,
|
||||
configPath: r.configPath,
|
||||
pidPath: r.pidPath,
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove an installed module by name. Returns the version
|
||||
/// that was removed so the caller can confirm.
|
||||
Future<({String name, String version})> uninstallModule(
|
||||
String name,
|
||||
) async {
|
||||
final r = await _client.uninstallModule(name);
|
||||
return (name: r.name, version: r.version);
|
||||
}
|
||||
|
||||
/// Active channel + per-channel daemon status snapshot.
|
||||
Future<ChannelStatusSnapshot> channelStatus() async {
|
||||
final r = await _client.channelStatus();
|
||||
|
|
@ -521,6 +544,7 @@ class HubService {
|
|||
_client.checkUpdate().catchError(
|
||||
(_) => CheckUpdateResponse(),
|
||||
),
|
||||
_client.daemonPaths().catchError((_) => DaemonPathsResponse()),
|
||||
]);
|
||||
|
||||
final caps = results[0] as List<CapabilityEntry>;
|
||||
|
|
@ -528,6 +552,7 @@ class HubService {
|
|||
final chain = results[2] as VerifyEventChainResponse;
|
||||
final services = results[3] as List<DeclaredService>;
|
||||
final update = results[4] as CheckUpdateResponse;
|
||||
final paths = results[5] as DaemonPathsResponse;
|
||||
|
||||
// Module count = distinct module_name across capabilities.
|
||||
final moduleNames = <String>{for (final c in caps) c.moduleName};
|
||||
|
|
@ -559,6 +584,14 @@ class HubService {
|
|||
update.releaseNotesUrl.isEmpty ? null : update.releaseNotesUrl,
|
||||
reason: update.reason.isEmpty ? null : update.reason,
|
||||
),
|
||||
paths: DaemonPathsSnapshot(
|
||||
logPath: paths.logPath,
|
||||
dbPath: paths.dbPath,
|
||||
modulesDir: paths.modulesDir,
|
||||
flowsDir: paths.flowsDir,
|
||||
configPath: paths.configPath,
|
||||
pidPath: paths.pidPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -592,6 +625,9 @@ class DoctorSnapshot {
|
|||
final String? eventChainTamperedAt;
|
||||
final List<ServiceEntry> services;
|
||||
final UpdateStatus update;
|
||||
/// Filesystem paths the daemon owns. Empty fields when the
|
||||
/// hub couldn't resolve them (older daemon, no $HOME).
|
||||
final DaemonPathsSnapshot paths;
|
||||
|
||||
const DoctorSnapshot({
|
||||
required this.moduleCount,
|
||||
|
|
@ -602,6 +638,7 @@ class DoctorSnapshot {
|
|||
required this.eventChainTamperedAt,
|
||||
required this.services,
|
||||
required this.update,
|
||||
required this.paths,
|
||||
});
|
||||
|
||||
bool get chainHealthy => eventChainTamperedAt == null;
|
||||
|
|
@ -868,6 +905,28 @@ class ChannelStatusSnapshot {
|
|||
});
|
||||
}
|
||||
|
||||
/// Filesystem paths the daemon owns. Studio's Doctor page
|
||||
/// surfaces these as "Open" buttons so Windows operators who
|
||||
/// never touch a shell can still inspect everything via the OS
|
||||
/// file manager / default editor.
|
||||
class DaemonPathsSnapshot {
|
||||
final String logPath;
|
||||
final String dbPath;
|
||||
final String modulesDir;
|
||||
final String flowsDir;
|
||||
final String configPath;
|
||||
final String pidPath;
|
||||
|
||||
const DaemonPathsSnapshot({
|
||||
required this.logPath,
|
||||
required this.dbPath,
|
||||
required this.modulesDir,
|
||||
required this.flowsDir,
|
||||
required this.configPath,
|
||||
required this.pidPath,
|
||||
});
|
||||
}
|
||||
|
||||
/// Detected host hardware — see
|
||||
/// docs/architecture/system-ai.md → "Hardware tiers".
|
||||
class HardwareSnapshot {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue