diff --git a/TUI/Widgets/multiagentselector.py b/TUI/Widgets/multiagentselector.py index b3603f3..4ffc91d 100644 --- a/TUI/Widgets/multiagentselector.py +++ b/TUI/Widgets/multiagentselector.py @@ -58,7 +58,7 @@ class MultiAgentSelector(Widget): def compose(self): yield Header(show_clock=True, icon="⚙") - title_text = Static("🖥️ Agent Selector", id="selector_title") + title_text = Static("🖥️ Agent Selector", id="selector_title") title_text.styles.margin = (0, 0, 0, 1) yield title_text @@ -175,7 +175,7 @@ class MultiAgentSelector(Widget): match_list.add_option((name, name)) unmatched_label = self.query_one("#unmatched_label", Static) if unmatched: - unmatched_label.update(f"⚠️ No matches for: {', '.join(unmatched)}") + unmatched_label.update(f"No matches for: {', '.join(unmatched)}") else: unmatched_label.update("") diff --git a/TUI/Widgets/settingswidget.py b/TUI/Widgets/settingswidget.py index 1c97ee8..db0a696 100644 --- a/TUI/Widgets/settingswidget.py +++ b/TUI/Widgets/settingswidget.py @@ -23,8 +23,10 @@ import webbrowser from textual.containers import Horizontal, Vertical, VerticalScroll from textual.message import Message from textual.widget import Widget -from textual.widgets import Button, Rule, Static +from textual.widgets import Button, Input, Rule, Static, Switch +from utils.configmanager import get_user_value, save_user_config +from utils.setup import get_base_directory from utils.versionchecker import ( RELEASES_PAGE_URL, UpdateCheckResult, @@ -81,6 +83,36 @@ class SettingsWidget(Widget): #light_themes_col { margin-left: 1; } + + /* Telemetry section */ + #telemetry_row { + height: auto; + margin: 1 0; + } + + #telemetry_label { + width: auto; + margin-right: 1; + } + + #telemetry_url_container { + height: auto; + margin: 1 0; + } + + #telemetry_url_label { + width: auto; + margin-right: 1; + } + + #telemetry_url_input { + width: 1fr; + } + + .settings_description { + color: $text-muted; + margin-bottom: 1; + } """ class ThemeSelected(Message): @@ -146,6 +178,40 @@ class SettingsWidget(Widget): yield Rule() + # Telemetry Section + with Vertical(id="telemetry_section") as telemetry: + telemetry.styles.height = "auto" + yield Static( + "📊 Telemetry Settings", + id="telemetry_title", + classes="settings_section_title", + ) + + telemetry_enabled = get_user_value("TELEMETRY", bool, False) + + with Horizontal(id="telemetry_row"): + yield Static("Enable Telemetry: ", id="telemetry_label") + yield Switch(value=telemetry_enabled, id="telemetry_switch") + + # URL input container - shown only when telemetry is enabled + if telemetry_enabled: + telemetry_url = get_user_value("TELEM_URL", str, "") + with Horizontal(id="telemetry_url_container"): + yield Static("Telemetry URL: ", id="telemetry_url_label") + yield Input( + value=telemetry_url, + placeholder="https://your-telemetry-endpoint.com", + id="telemetry_url_input", + ) + + yield Static( + "Help improve Loxide by sending anonymous usage data", + id="telemetry_description", + classes="settings_description", + ) + + yield Rule() + # Theme Section - Three columns: Dark 1, Dark 2, Light with Vertical(id="themes_section") as themes: themes.styles.height = "auto" @@ -231,6 +297,85 @@ class SettingsWidget(Widget): self.post_message(self.ThemeSelected(theme_name)) event.stop() + def on_switch_changed(self, event: Switch.Changed) -> None: + """Handle telemetry switch toggle.""" + if event.switch.id == "telemetry_switch": + enabled = event.value + self._persist_telemetry_setting(enabled) + + if enabled: + self._mount_telemetry_url_input() + else: + self._unmount_telemetry_url_input() + + def on_input_changed(self, event: Input.Changed) -> None: + """Handle telemetry URL input changes.""" + if event.input.id == "telemetry_url_input": + self._persist_telemetry_url(event.value) + + def _persist_telemetry_setting(self, enabled: bool) -> None: + """Store the telemetry opt-in/out setting in the user's config.""" + base_dir = get_base_directory() + config_dir = base_dir / "config" + + try: + save_user_config(config_dir, {"TELEMETRY": enabled}) + logger.debug("Updated user config with TELEMETRY=%s", enabled) + status = "enabled" if enabled else "disabled" + self.app.notify(f"📊 Telemetry {status}", timeout=2) + except Exception as exc: + logger.error("Failed to save TELEMETRY setting: %s", exc) + self.app.notify(f"⚠️ Failed to save setting: {exc}", severity="warning") + + def _persist_telemetry_url(self, url: str) -> None: + """Store the telemetry URL in the user's config.""" + base_dir = get_base_directory() + config_dir = base_dir / "config" + + try: + save_user_config(config_dir, {"TELEM_URL": url}) + logger.debug("Updated user config with TELEM_URL=%s", url) + except Exception as exc: + logger.error("Failed to save TELEM_URL setting: %s", exc) + + def _mount_telemetry_url_input(self) -> None: + """Mount the telemetry URL input container.""" + try: + # Check if already mounted + self.query_one("#telemetry_url_container") + return + except Exception: + pass + + telemetry_url = get_user_value("TELEM_URL", str, "") + + # Create container with children + container = Horizontal( + Static("Telemetry URL: ", id="telemetry_url_label"), + Input( + value=telemetry_url, + placeholder="https://your-telemetry-endpoint.com", + id="telemetry_url_input", + ), + id="telemetry_url_container", + ) + + # Mount after the telemetry_row within telemetry_section + try: + telemetry_section = self.query_one("#telemetry_section") + telemetry_row = self.query_one("#telemetry_row") + telemetry_section.mount(container, after=telemetry_row) + except Exception as exc: + logger.error("Failed to mount telemetry URL input: %s", exc) + + def _unmount_telemetry_url_input(self) -> None: + """Unmount the telemetry URL input container.""" + try: + container = self.query_one("#telemetry_url_container") + container.remove() + except Exception: + pass + def _check_for_updates(self) -> None: """Check for updates and update UI.""" if self._checking: @@ -497,6 +642,36 @@ class ThemeSelector(Widget): #light_themes_col { margin-left: 1; } + + /* Telemetry section */ + #telemetry_row { + height: auto; + margin: 1 0; + } + + #telemetry_label { + width: auto; + margin-right: 1; + } + + #telemetry_url_container { + height: auto; + margin: 1 0; + } + + #telemetry_url_label { + width: auto; + margin-right: 1; + } + + #telemetry_url_input { + width: 1fr; + } + + .settings_description { + color: $text-muted; + margin-bottom: 1; + } """ class ThemeSelected(Message): diff --git a/docs/01_Software_Requirements_Specification.md b/docs/01_Software_Requirements_Specification.md index 53af21e..baedbc1 100644 --- a/docs/01_Software_Requirements_Specification.md +++ b/docs/01_Software_Requirements_Specification.md @@ -357,7 +357,7 @@ Loxide is designed for three primary user classes with varying levels of experti #### 2.4.3 Network Requirements -- Outbound HTTPS (port 443) to Airlock server +- Outbound to Airlock server - Outbound HTTPS to Gitea instance (development only) - No inbound connections required - Proxy support via standard environment variables diff --git a/utils/versionchecker.py b/utils/versionchecker.py index e72ce85..b3cba5f 100644 --- a/utils/versionchecker.py +++ b/utils/versionchecker.py @@ -34,7 +34,7 @@ import requests logger = logging.getLogger(__name__) # Current application version - UPDATE THIS ON EACH RELEASE -__version__ = "1.0.0" +__version__ = "1.1.0" # Gitea release API configuration GITEA_API_BASE = "https://git.racooncity.org/api/v1"