Skip to content

Commit 78ce511

Browse files
feat(System): enable to enable/disable diagnostics in UI
refactor(System): move settings logic to service style(System): minimal love for Settings and Info page
1 parent 7b3899d commit 78ce511

6 files changed

Lines changed: 223 additions & 111 deletions

File tree

src/aignostics/gui/_frame.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ def frame( # noqa: C901, PLR0915
4444
@ui.refreshable
4545
def health_icon() -> None:
4646
if launchpad_healthy:
47-
ui.icon("check_circle", color="positive")
47+
ui.icon("settings", color="positive")
4848
elif launchpad_healthy is not None:
49-
ui.icon("error", color="negative")
49+
ui.icon("settings", color="negative")
5050

5151
@ui.refreshable
5252
def health_link() -> None:
@@ -215,7 +215,7 @@ def toggle_dark_mode() -> None:
215215
with ui.item_section().props("avatar"):
216216
health_icon()
217217
with ui.item_section():
218-
ui.label("Check Launchpad Status").tailwind.font_weight(
218+
ui.label("Info and Settings").tailwind.font_weight(
219219
"bold" if context.client.page.path == "/system" else "normal"
220220
)
221221

src/aignostics/system/_cli.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
import sys
55
from enum import StrEnum
66
from importlib.util import find_spec
7-
from pathlib import Path
87
from typing import Annotated
98

109
import typer
1110
import yaml
1211

1312
from ..constants import API_VERSIONS # noqa: TID252
14-
from ..utils import __project_name__, console, get_logger # noqa: TID252
13+
from ..utils import console, get_logger # noqa: TID252
1514
from ._service import Service
1615

1716
logger = get_logger(__name__)
@@ -160,7 +159,7 @@ def whoami() -> None:
160159
@config_app.command()
161160
def get(key: Annotated[str, typer.Argument(help="Configuration key to get value for")]) -> None:
162161
"""Set a configuration key to a value."""
163-
console.print(Service().dotenv_get(key.upper()))
162+
console.print(Service.dotenv_get(key.upper()))
164163

165164

166165
@config_app.command()
@@ -170,7 +169,11 @@ def set( # noqa: A001
170169
) -> None:
171170
"""Set a configuration key to a value."""
172171
key = key.upper()
173-
Service().dotenv_set(key, value)
172+
try:
173+
Service.dotenv_set(key, value)
174+
except ValueError as e:
175+
console.print(f"Invalid configuration: {e!s}", style="error")
176+
sys.exit(2)
174177
console.print(f"Configuration '{key}' set to '{value}'.", style="success")
175178

176179

@@ -180,24 +183,30 @@ def unset(
180183
) -> None:
181184
"""Set a configuration key to a value."""
182185
key = key.upper()
183-
Service().dotenv_unset(key)
186+
Service.dotenv_unset(key)
184187
console.print(f"Configuration '{key}' unset.", style="success")
185188

186189

187190
@config_app.command()
188191
def remote_diagnostics_enable() -> None:
189192
"""Enable remote diagnostics via Sentry and Logfire. Data stored in EU data centers."""
190-
Service().dotenv_set(f"{__project_name__.upper()}_SENTRY_ENABLED", "1")
191-
Service().dotenv_set(f"{__project_name__.upper()}_LOGFIRE_ENABLED", "1")
192-
console.print("Remote diagnostics enabled.", style="success")
193+
try:
194+
Service.remote_diagnostics_enable()
195+
console.print("Remote diagnostics enabled.", style="success")
196+
except ValueError as e:
197+
console.print(f"Invalid diagnostics configuration: {e!s}", style="error")
198+
sys.exit(2)
193199

194200

195201
@config_app.command()
196202
def remote_diagnostics_disable() -> None:
197203
"""Disable remote diagnostics."""
198-
Service().dotenv_unset(f"{__project_name__.upper()}_SENTRY_ENABLED")
199-
Service().dotenv_unset(f"{__project_name__.upper()}_LOGFIRE_ENABLED")
200-
console.print("Remote diagnostics disabled.", style="success")
204+
try:
205+
Service.remote_diagnostics_disable()
206+
console.print("Remote diagnostics disabled.", style="success")
207+
except ValueError as e:
208+
console.print(f"Invalid diagnostics configuration: {e!s}", style="error")
209+
sys.exit(2)
201210

202211

203212
@config_app.command()
@@ -209,42 +218,22 @@ def http_proxy_enable(
209218
no_ssl_verify: Annotated[bool, typer.Option(help="Disable SSL verification")] = False,
210219
) -> None:
211220
"""Enable HTTP proxy."""
212-
url = f"{scheme}://{host}:{port}"
213-
Service().dotenv_set("HTTP_PROXY", url)
214-
Service().dotenv_set("HTTPS_PROXY", url)
215-
if ssl_cert_file is not None and no_ssl_verify:
216-
message = "Cannot set both 'ssl_cert_file' and 'ssl_disable_verify'. Please choose one."
217-
console.print(message, style="warning")
221+
try:
222+
Service().http_proxy_enable(
223+
host=host, port=port, scheme=scheme, ssl_cert_file=ssl_cert_file, no_ssl_verify=no_ssl_verify
224+
)
225+
console.print("HTTP proxy enabled.", style="success")
226+
except ValueError as e:
227+
console.print(f"Invalid HTTP proxy configuration: {e!s}", style="error")
218228
sys.exit(2)
219-
if no_ssl_verify:
220-
Service().dotenv_set("SSL_NO_VERIFY", "1")
221-
Service().dotenv_set("SSL_CERT_FILE", "")
222-
Service().dotenv_set("REQUESTS_CA_BUNDLE", "")
223-
Service().dotenv_set("CURL_CA_BUNDLE", "")
224-
else:
225-
Service().dotenv_unset("SSL_NO_VERIFY")
226-
Service().dotenv_unset("SSL_CERT_FILE")
227-
Service().dotenv_unset("REQUESTS_CA_BUNDLE")
228-
Service().dotenv_unset("CURL_CA_BUNDLE")
229-
if ssl_cert_file:
230-
file = Path(ssl_cert_file).resolve()
231-
if not file.is_file():
232-
message = f"SSL certificate file '{ssl_cert_file}' does not exist."
233-
console.print(message, style="error")
234-
sys.exit(2)
235-
Service().dotenv_set("SSL_CERT_FILE", str(ssl_cert_file))
236-
Service().dotenv_set("REQUESTS_CA_BUNDLE", str(ssl_cert_file))
237-
Service().dotenv_set("CURL_CA_BUNDLE", str(ssl_cert_file))
238-
console.print("HTTP proxy enabled.", style="success")
239229

240230

241231
@config_app.command()
242232
def http_proxy_disable() -> None:
243233
"""Disable HTTP proxy."""
244-
Service().dotenv_unset("HTTP_PROXY")
245-
Service().dotenv_unset("HTTPS_PROXY")
246-
Service().dotenv_unset("SSL_CERT_FILE")
247-
Service().dotenv_unset("SSL_NO_VERIFY")
248-
Service().dotenv_unset("REQUESTS_CA_BUNDLE")
249-
Service().dotenv_unset("CURL_CA_BUNDLE")
250-
console.print("HTTP proxy disabled.", style="success")
234+
try:
235+
Service.http_proxy_disable()
236+
console.print("HTTP proxy disabled.", style="success")
237+
except ValueError as e:
238+
console.print(f"Invalid HTTP proxy configuration: {e!s}", style="error")
239+
sys.exit(2)

src/aignostics/system/_gui.py

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Homepage (index) of GUI."""
22

3+
from pathlib import Path
4+
35
from aignostics.gui import frame
46
from aignostics.utils import BaseService, locate_subclasses
57

@@ -10,9 +12,10 @@
1012
class PageBuilder(BasePageBuilder):
1113
@staticmethod
1214
def register_pages() -> None:
13-
from nicegui import run, ui # noqa: PLC0415
15+
from nicegui import app, run, ui # noqa: PLC0415
1416

1517
locate_subclasses(BaseService) # Ensure settings are loaded
18+
app.add_static_files("/system_assets", Path(__file__).parent / "assets")
1619

1720
ui.add_head_html("""
1821
<style>
@@ -27,35 +30,67 @@ def register_pages() -> None:
2730

2831
@ui.page("/system")
2932
async def page_system() -> None:
30-
"""System page."""
31-
with frame("Launchpad Status", left_sidebar=False):
33+
"""System info and settings page."""
34+
with frame("Info and Settings", left_sidebar=False):
3235
pass
33-
ui.label("Health").classes("text-h6")
34-
properties = {
35-
"content": {"json": Service().health().model_dump()},
36-
"mode": "tree",
37-
"readOnly": True,
38-
"mainMenuBar": False,
39-
"navigationBar": False,
40-
"statusBar": False,
41-
}
42-
ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
43-
44-
ui.label("Info").classes("text-h6")
45-
spinner = ui.spinner("dots", size="lg", color="red")
46-
properties = {
47-
"content": {"json": "Loading ..."},
48-
"mode": "tree",
49-
"readOnly": True,
50-
"mainMenuBar": False,
51-
"navigationBar": False,
52-
"statusBar": False,
53-
}
54-
editor = ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
55-
editor.set_visibility(False)
56-
info = await run.cpu_bound(Service().info, True, True)
57-
properties["content"] = {"json": info}
58-
editor.update()
59-
editor.run_editor_method(":expand", "path => true")
60-
spinner.delete()
61-
editor.set_visibility(True)
36+
37+
with ui.row().classes("w-full justify-between items-start"):
38+
with ui.column().classes("w-1/5 mt-20"):
39+
ui.space()
40+
ui.html(
41+
'<dotlottie-player src="/system_assets/system.lottie" '
42+
'background="transparent" speed="1" style="width: 300px; height: 300px" '
43+
'direction="1" playMode="normal" loop autoplay></dotlottie-player>'
44+
)
45+
ui.space()
46+
ui.space()
47+
with ui.column().classes("w-3/5"):
48+
with ui.tabs().classes("w-full") as tabs:
49+
tab_health = ui.tab("health")
50+
tab_info = ui.tab("Info")
51+
tab_settings = ui.tab("settings")
52+
with ui.tab_panels(tabs, value=tab_health).classes("w-full"):
53+
with ui.tab_panel(tab_health):
54+
properties = {
55+
"content": {"json": Service().health().model_dump()},
56+
"mode": "tree",
57+
"readOnly": True,
58+
"mainMenuBar": False,
59+
"navigationBar": False,
60+
"statusBar": False,
61+
}
62+
ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
63+
with ui.tab_panel(tab_info):
64+
spinner = ui.spinner("dots", size="lg", color="red")
65+
properties = {
66+
"content": {"json": "Loading ..."},
67+
"mode": "tree",
68+
"readOnly": True,
69+
"mainMenuBar": False,
70+
"navigationBar": False,
71+
"statusBar": False,
72+
}
73+
editor = ui.json_editor(properties).style("width: 100%").mark("JSON_EDITOR_INFO")
74+
editor.set_visibility(False)
75+
info = await run.cpu_bound(Service().info, True, True)
76+
properties["content"] = {"json": info}
77+
editor.update()
78+
editor.run_editor_method(":expand", "path => true")
79+
spinner.delete()
80+
editor.set_visibility(True)
81+
with (
82+
ui.tab_panel(tab_settings),
83+
ui.card().classes("w-full"),
84+
ui.row().classes("items-center justify-between"),
85+
):
86+
ui.switch(
87+
value=Service.remote_diagnostics_enabled(),
88+
on_change=lambda e: (
89+
Service.remote_diagnostics_enable()
90+
if e.value
91+
else Service.remote_diagnostics_disable(),
92+
ui.notify("Restart the app to apply changes.", color="warning"), # type: ignore[func-returns-value]
93+
None,
94+
)[0],
95+
)
96+
ui.label("Remote Diagnostics")

src/aignostics/system/_service.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,3 +446,88 @@ def dotenv_unset(key: str) -> int:
446446
dotenv_unset_key(dotenv_path=str(dotenv_path.resolve()), key_to_unset=key, quote_mode="never")
447447
os.environ.pop(key, None)
448448
return removed_count
449+
450+
@staticmethod
451+
def remote_diagnostics_enabled() -> bool:
452+
"""Check if remote diagnostics are enabled.
453+
454+
Returns:
455+
bool: True if remote diagnostics are enabled, False otherwise.
456+
"""
457+
return (
458+
Service.dotenv_get(f"{__project_name__.upper()}_SENTRY_ENABLED") == "1"
459+
and Service.dotenv_get(f"{__project_name__.upper()}_LOGFIRE_ENABLED") == "1"
460+
)
461+
462+
@staticmethod
463+
def remote_diagnostics_enable() -> None:
464+
"""Enable remote diagnostics via Sentry and Logfire. Data stored in EU data centers.
465+
466+
Raises:
467+
ValueError: If the environment variable cannot be set.
468+
"""
469+
Service.dotenv_set(f"{__project_name__.upper()}_SENTRY_ENABLED", "1")
470+
Service.dotenv_set(f"{__project_name__.upper()}_LOGFIRE_ENABLED", "1")
471+
472+
@staticmethod
473+
def remote_diagnostics_disable() -> None:
474+
"""Disable remote diagnostics."""
475+
Service.dotenv_unset(f"{__project_name__.upper()}_SENTRY_ENABLED")
476+
Service.dotenv_unset(f"{__project_name__.upper()}_LOGFIRE_ENABLED")
477+
478+
@staticmethod
479+
def http_proxy_enable(
480+
host: str,
481+
port: int,
482+
scheme: str,
483+
ssl_cert_file: str | None = None,
484+
no_ssl_verify: bool = False,
485+
) -> None:
486+
"""Enable HTTP proxy.
487+
488+
Args:
489+
host (str): The host of the proxy server.
490+
port (int): The port of the proxy server.
491+
scheme (str): The scheme of the proxy server (e.g., "http", "https").
492+
ssl_cert_file (str | None): Path to the SSL certificate file, if any.
493+
no_ssl_verify (bool): Whether to disable SSL verification
494+
495+
Raises:
496+
ValueError: If both 'ssl_cert_file' and 'ssl_disable_verify' are set.
497+
"""
498+
url = f"{scheme}://{host}:{port}"
499+
Service.dotenv_set("HTTP_PROXY", url)
500+
Service.dotenv_set("HTTPS_PROXY", url)
501+
if ssl_cert_file is not None and no_ssl_verify:
502+
message = "Cannot set both 'ssl_cert_file' and 'ssl_disable_verify'. Please choose one."
503+
logger.warning(message)
504+
raise ValueError(message)
505+
if no_ssl_verify:
506+
Service.dotenv_set("SSL_NO_VERIFY", "1")
507+
Service.dotenv_set("SSL_CERT_FILE", "")
508+
Service.dotenv_set("REQUESTS_CA_BUNDLE", "")
509+
Service.dotenv_set("CURL_CA_BUNDLE", "")
510+
else:
511+
Service.dotenv_unset("SSL_NO_VERIFY")
512+
Service.dotenv_unset("SSL_CERT_FILE")
513+
Service.dotenv_unset("REQUESTS_CA_BUNDLE")
514+
Service.dotenv_unset("CURL_CA_BUNDLE")
515+
if ssl_cert_file:
516+
file = Path(ssl_cert_file).resolve()
517+
if not file.is_file():
518+
message = f"SSL certificate file '{ssl_cert_file}' does not exist."
519+
logger.warning(message)
520+
raise ValueError(message)
521+
Service.dotenv_set("SSL_CERT_FILE", str(ssl_cert_file))
522+
Service.dotenv_set("REQUESTS_CA_BUNDLE", str(ssl_cert_file))
523+
Service.dotenv_set("CURL_CA_BUNDLE", str(ssl_cert_file))
524+
525+
@staticmethod
526+
def http_proxy_disable() -> None:
527+
"""Disable HTTP proxy."""
528+
Service.dotenv_unset("HTTP_PROXY")
529+
Service.dotenv_unset("HTTPS_PROXY")
530+
Service.dotenv_unset("SSL_CERT_FILE")
531+
Service.dotenv_unset("SSL_NO_VERIFY")
532+
Service.dotenv_unset("REQUESTS_CA_BUNDLE")
533+
Service.dotenv_unset("CURL_CA_BUNDLE")
1.71 KB
Binary file not shown.

0 commit comments

Comments
 (0)