feat(studio): hub maintenance panel with reset action (v0.45.0)
Some checks are pending
Security / Security check (push) Waiting to run
Some checks are pending
Security / Security check (push) Waiting to run
UI parity with the platform CLI: `fai reset` (v0.10.93) is now reachable from Settings. The panel shows the reset blurb plus two checkboxes mirroring the CLI flags (Keep modules / Keep audit log + saved flows), then a destructive-styled "Reset hub state" button. Click goes through a confirm dialog before SystemActions.faiReset is invoked. After the CLI returns, Studio bounces its gRPC channel against the same endpoint (the daemon restarts on the same channel + port, so no Settings change needed) and reloads every panel: channel status, system AI, MCP clients, n8n endpoints, registry token. Output (success or failure) is shown in a copyable error box so any unexpected stderr lands somewhere the operator can paste from. Sits at the bottom of the dialog under "HUB MAINTENANCE", separated from the everyday config so a destructive button doesn't crowd the day-to-day controls. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
9944a7ac8a
commit
3db7db7330
8 changed files with 358 additions and 1 deletions
|
|
@ -509,6 +509,24 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
onSave: _saveRegistryToken,
|
||||
onClear: _clearRegistryToken,
|
||||
),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_MaintenancePanel(
|
||||
onResetDone: () async {
|
||||
// Daemon just restarted under the same channel +
|
||||
// port. Bouncing the gRPC connection picks up the
|
||||
// fresh state without the operator having to
|
||||
// close+reopen Settings.
|
||||
await HubService.instance.reconnect(
|
||||
HubService.instance.currentEndpoint,
|
||||
);
|
||||
if (!mounted) return;
|
||||
await _loadChannels();
|
||||
await _loadAiStatus();
|
||||
await _loadMcpClients();
|
||||
await _loadN8nEndpoints();
|
||||
await _loadRegistryToken();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -1437,6 +1455,163 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
|||
/// metadata; nothing is auto-spawned. Trust model:
|
||||
/// referencing public projects with their canonical install
|
||||
/// command is the same legal / security shape as a package
|
||||
/// Hub maintenance panel — currently one action: reset operator
|
||||
/// state via `fai reset --yes`. Lives at the bottom of Settings
|
||||
/// so the everyday config controls aren't crowded by a destructive
|
||||
/// button. The reset itself is gated by an interactive confirm
|
||||
/// dialog before the CLI is invoked.
|
||||
class _MaintenancePanel extends StatefulWidget {
|
||||
/// Called after a successful reset so the parent Settings dialog
|
||||
/// can bounce its gRPC channel and reload its panels.
|
||||
final Future<void> Function() onResetDone;
|
||||
|
||||
const _MaintenancePanel({required this.onResetDone});
|
||||
|
||||
@override
|
||||
State<_MaintenancePanel> createState() => _MaintenancePanelState();
|
||||
}
|
||||
|
||||
class _MaintenancePanelState extends State<_MaintenancePanel> {
|
||||
bool _keepModules = false;
|
||||
bool _keepData = false;
|
||||
bool _resetting = false;
|
||||
String? _resultText;
|
||||
bool _resultOk = false;
|
||||
|
||||
Future<void> _confirmAndReset() async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(l.maintenanceResetConfirmTitle),
|
||||
content: Text(l.maintenanceResetConfirmBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(ctx).colorScheme.error,
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(l.maintenanceResetConfirmButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
setState(() {
|
||||
_resetting = true;
|
||||
_resultText = null;
|
||||
});
|
||||
final r = await SystemActions.faiReset(
|
||||
keepModules: _keepModules,
|
||||
keepData: _keepData,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_resetting = false;
|
||||
_resultOk = r.ok;
|
||||
_resultText = r.ok
|
||||
? r.stdout.trim()
|
||||
: (r.stderr.isEmpty ? r.stdout : r.stderr).trim();
|
||||
});
|
||||
if (r.ok) await widget.onResetDone();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l.maintenanceHeader,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
l.maintenanceResetBlurb,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _keepModules,
|
||||
onChanged: _resetting
|
||||
? null
|
||||
: (v) => setState(() => _keepModules = v ?? false),
|
||||
title: Text(l.maintenanceKeepModulesTitle),
|
||||
subtitle: Text(
|
||||
l.maintenanceKeepModulesSubtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _keepData,
|
||||
onChanged: _resetting
|
||||
? null
|
||||
: (v) => setState(() => _keepData = v ?? false),
|
||||
title: Text(l.maintenanceKeepDataTitle),
|
||||
subtitle: Text(
|
||||
l.maintenanceKeepDataSubtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Row(
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: _resetting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.restart_alt, size: 16),
|
||||
label: Text(
|
||||
_resetting
|
||||
? l.maintenanceResetInProgress
|
||||
: l.maintenanceResetButton,
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.errorContainer,
|
||||
foregroundColor: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
onPressed: _resetting ? null : _confirmAndReset,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_resultText != null) ...[
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
FaiErrorBox(
|
||||
text: _resultText!,
|
||||
isError: !_resultOk,
|
||||
maxHeight: 240,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Credentials panel for the operator's registry auth token.
|
||||
/// The hub reads the same token via `~/.fai/registry-token`
|
||||
/// (or the `FAI_REGISTRY_TOKEN` env var, which still wins)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue