// ui/pilotWindow.js - Fenêtre principale de l'extension Pilot Control import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk'; import Adw from 'gi://Adw'; import GLib from 'gi://GLib'; import {MetricEditDialog} from './metricEditDialog.js'; import {CommandEditDialog} from './commandEditDialog.js'; /** * Fenêtre principale avec les sections Services, Telemetry, Commands */ export const PilotWindow = GObject.registerClass( class PilotWindow extends Adw.Window { _init(extension, yamlConfig, serviceManager) { super._init({ title: 'Pilot Control Panel', default_width: 800, default_height: 600, }); this._extension = extension; this._yamlConfig = yamlConfig; this._serviceManager = serviceManager; this._buildUI(); this._loadData(); } /** * Construit l'interface utilisateur */ _buildUI() { // Header bar const headerBar = new Adw.HeaderBar(); // Bouton refresh const refreshButton = new Gtk.Button({ icon_name: 'view-refresh-symbolic', tooltip_text: 'Reload configuration', }); refreshButton.connect('clicked', () => { this._loadData(); }); headerBar.pack_end(refreshButton); // Bouton save const saveButton = new Gtk.Button({ icon_name: 'document-save-symbolic', tooltip_text: 'Save configuration', }); saveButton.connect('clicked', () => { this._saveConfig(); }); headerBar.pack_end(saveButton); // Toolbar view (GNOME 45+) const toolbarView = new Adw.ToolbarView(); toolbarView.add_top_bar(headerBar); // Main content box const mainBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin_top: 12, margin_bottom: 12, margin_start: 12, margin_end: 12, spacing: 12, }); // Scrolled window const scrolledWindow = new Gtk.ScrolledWindow({ vexpand: true, hscrollbar_policy: Gtk.PolicyType.NEVER, }); scrolledWindow.set_child(mainBox); toolbarView.set_content(scrolledWindow); this.set_content(toolbarView); // Section: Service Control mainBox.append(this._buildServiceSection()); // Section: Telemetry Metrics mainBox.append(this._buildTelemetrySection()); // Section: Commands mainBox.append(this._buildCommandsSection()); } /** * Construit la section Service Control */ _buildServiceSection() { const group = new Adw.PreferencesGroup({ title: 'Service Control', description: 'Manage the Pilot systemd service', }); // Service status row this._serviceStatusRow = new Adw.ActionRow({ title: 'Service Status', subtitle: 'Unknown', }); const serviceSwitch = new Gtk.Switch({ valign: Gtk.Align.CENTER, }); serviceSwitch.connect('notify::active', (sw) => { if (sw.active) { this._serviceManager.startService(); } else { this._serviceManager.stopService(); } GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { this._updateServiceStatus(); return GLib.SOURCE_REMOVE; }); }); this._serviceSwitch = serviceSwitch; this._serviceStatusRow.add_suffix(serviceSwitch); this._serviceStatusRow.activatable_widget = serviceSwitch; group.add(this._serviceStatusRow); // Service auto-start row this._serviceEnableRow = new Adw.ActionRow({ title: 'Auto-start Service', subtitle: 'Enable service at system startup', }); const enableSwitch = new Gtk.Switch({ valign: Gtk.Align.CENTER, }); enableSwitch.connect('notify::active', (sw) => { if (sw.active) { this._serviceManager.enableService(); } else { this._serviceManager.disableService(); } }); this._serviceEnableSwitch = enableSwitch; this._serviceEnableRow.add_suffix(enableSwitch); this._serviceEnableRow.activatable_widget = enableSwitch; group.add(this._serviceEnableRow); // Restart button row const restartRow = new Adw.ActionRow({ title: 'Restart Service', subtitle: 'Apply configuration changes', }); const restartButton = new Gtk.Button({ label: 'Restart', valign: Gtk.Align.CENTER, }); restartButton.connect('clicked', () => { this._serviceManager.restartService(); GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { this._updateServiceStatus(); return GLib.SOURCE_REMOVE; }); }); restartRow.add_suffix(restartButton); group.add(restartRow); return group; } /** * Construit la section Telemetry */ _buildTelemetrySection() { const group = new Adw.PreferencesGroup({ title: 'Telemetry Metrics', description: 'Configure system monitoring metrics', }); // Global telemetry switch this._telemetryGlobalRow = new Adw.ActionRow({ title: 'Enable Telemetry', subtitle: 'Master switch for all metrics', }); const telemetrySwitch = new Gtk.Switch({ valign: Gtk.Align.CENTER, }); telemetrySwitch.connect('notify::active', (sw) => { this._yamlConfig.setTelemetryEnabled(sw.active); this._markDirty(); }); this._telemetrySwitch = telemetrySwitch; this._telemetryGlobalRow.add_suffix(telemetrySwitch); this._telemetryGlobalRow.activatable_widget = telemetrySwitch; group.add(this._telemetryGlobalRow); // Container pour les métriques individuelles this._telemetryMetricsBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 0, }); group.add(this._telemetryMetricsBox); return group; } /** * Construit la section Commands */ _buildCommandsSection() { const group = new Adw.PreferencesGroup({ title: 'Commands', description: 'Configure allowed system commands', }); // Global commands switch this._commandsGlobalRow = new Adw.ActionRow({ title: 'Enable Commands', subtitle: 'Master switch for all commands', }); const commandsSwitch = new Gtk.Switch({ valign: Gtk.Align.CENTER, }); commandsSwitch.connect('notify::active', (sw) => { this._yamlConfig.setCommandsEnabled(sw.active); this._markDirty(); }); this._commandsSwitch = commandsSwitch; this._commandsGlobalRow.add_suffix(commandsSwitch); this._commandsGlobalRow.activatable_widget = commandsSwitch; group.add(this._commandsGlobalRow); // Allowlist editor row this._commandsAllowlistRow = new Adw.ActionRow({ title: 'Allowed Commands', subtitle: 'Click to edit the allowlist', }); const editButton = new Gtk.Button({ icon_name: 'document-edit-symbolic', valign: Gtk.Align.CENTER, }); editButton.connect('clicked', () => { this._editCommandsAllowlist(); }); this._commandsAllowlistRow.add_suffix(editButton); this._commandsAllowlistRow.set_activatable(true); this._commandsAllowlistRow.connect('activated', () => { this._editCommandsAllowlist(); }); group.add(this._commandsAllowlistRow); return group; } /** * Charge les données depuis la config YAML */ _loadData() { const config = this._yamlConfig.load(); if (!config) { this._showError('Failed to load configuration'); return; } // Update service status this._updateServiceStatus(); // Update telemetry section const telemetryEnabled = config.features?.telemetry?.enabled || false; this._telemetrySwitch.active = telemetryEnabled; // Clear existing metrics let child = this._telemetryMetricsBox.get_first_child(); while (child) { const next = child.get_next_sibling(); this._telemetryMetricsBox.remove(child); child = next; } // Add metrics const metrics = this._yamlConfig.getTelemetryMetrics(); for (const [name, metricConfig] of Object.entries(metrics)) { this._addMetricRow(name, metricConfig); } // Update commands section const commandsEnabled = config.features?.commands?.enabled || false; this._commandsSwitch.active = commandsEnabled; const allowlist = this._yamlConfig.getCommandsAllowlist(); this._commandsAllowlistRow.subtitle = `${allowlist.length} commands allowed`; this._dirtyConfig = false; } /** * Ajoute une ligne pour une métrique */ _addMetricRow(name, metricConfig) { const row = new Adw.ActionRow({ title: metricConfig.name || name, subtitle: `Interval: ${metricConfig.interval_s || 'N/A'}s`, }); // Switch pour enable/disable const metricSwitch = new Gtk.Switch({ active: metricConfig.enabled || false, valign: Gtk.Align.CENTER, }); metricSwitch.connect('notify::active', (sw) => { this._yamlConfig.updateTelemetryMetric(name, {enabled: sw.active}); this._markDirty(); }); // Bouton edit const editButton = new Gtk.Button({ icon_name: 'document-edit-symbolic', valign: Gtk.Align.CENTER, }); editButton.connect('clicked', () => { this._editMetric(name, metricConfig); }); row.add_suffix(metricSwitch); row.add_suffix(editButton); this._telemetryMetricsBox.append(row); } /** * Édite une métrique */ _editMetric(name, currentConfig) { const dialog = new MetricEditDialog(this, name, currentConfig); dialog.connect('response', (dlg, responseId) => { if (responseId === Gtk.ResponseType.OK) { const updates = dialog.getUpdates(); this._yamlConfig.updateTelemetryMetric(name, updates); this._markDirty(); this._loadData(); } dialog.destroy(); }); dialog.present(); } /** * Édite la allowlist des commandes */ _editCommandsAllowlist() { const currentAllowlist = this._yamlConfig.getCommandsAllowlist(); const dialog = new CommandEditDialog(this, currentAllowlist); dialog.connect('response', (dlg, responseId) => { if (responseId === Gtk.ResponseType.OK) { const newAllowlist = dialog.getAllowlist(); this._yamlConfig.updateCommandsAllowlist(newAllowlist); this._markDirty(); this._loadData(); } dialog.destroy(); }); dialog.present(); } /** * Met à jour le status du service */ _updateServiceStatus() { const isActive = this._serviceManager.isServiceActive(); const isEnabled = this._serviceManager.isServiceEnabled(); this._serviceSwitch.active = isActive; this._serviceEnableSwitch.active = isEnabled; const statusText = isActive ? '🟢 Running' : '🔴 Stopped'; this._serviceStatusRow.subtitle = statusText; } /** * Marque la config comme modifiée */ _markDirty() { this._dirtyConfig = true; } /** * Sauvegarde la configuration */ _saveConfig() { if (!this._dirtyConfig) { this._showInfo('No changes to save'); return; } const success = this._yamlConfig.save(); if (success) { this._dirtyConfig = false; this._showInfo('Configuration saved successfully'); // Recharger le service this._serviceManager.reloadService(); GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { this._updateServiceStatus(); return GLib.SOURCE_REMOVE; }); } else { this._showError('Failed to save configuration'); } } /** * Affiche un message d'information */ _showInfo(message) { const toast = new Adw.Toast({ title: message, timeout: 2, }); // Note: Toast overlay nécessite Adw.ToastOverlay // Pour l'instant, on utilise console.log console.log(`Info: ${message}`); } /** * Affiche un message d'erreur */ _showError(message) { const dialog = new Adw.MessageDialog({ transient_for: this, heading: 'Error', body: message, }); dialog.add_response('ok', 'OK'); dialog.set_default_response('ok'); dialog.set_close_response('ok'); dialog.present(); } });