Add tabbed settings screen and Send Test Email button

Organize settings into General, Email, SSH, and Web Dashboard tabs
using TabbedContent. Add test-email CLI command and button that
auto-saves settings then sends a test email via SMTP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-07 06:00:30 +02:00
parent a0c891b093
commit 280b80c956
2 changed files with 98 additions and 53 deletions

View File

@@ -594,6 +594,29 @@ run_cli() {
exit "$rc" exit "$rc"
;; ;;
test-email)
if [[ -z "${NOTIFY_EMAIL:-}" ]]; then
echo "Error: NOTIFY_EMAIL is not set. Configure it in Settings first."
exit 1
fi
if [[ -z "${SMTP_HOST:-}" ]]; then
echo "Error: SMTP_HOST is not set. Configure SMTP settings first."
exit 1
fi
local hostname; hostname=$(hostname -f 2>/dev/null || hostname)
local subject="[gniza] [$hostname] Test Email"
local body="This is a test email from gniza on $hostname."$'\n'
body+="Sent at: $(date '+%Y-%m-%d %H:%M:%S')"$'\n'
body+=""$'\n'
body+="If you received this message, your SMTP settings are working correctly."
if _send_via_smtp "$subject" "$body"; then
echo "Test email sent successfully to $NOTIFY_EMAIL"
else
echo "Failed to send test email. Check your SMTP settings."
exit 1
fi
;;
version) version)
echo "gniza v${GNIZA4LINUX_VERSION}" echo "gniza v${GNIZA4LINUX_VERSION}"
;; ;;

View File

@@ -1,12 +1,14 @@
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, Input, Select from textual.widgets import Header, Footer, Static, Button, Input, Select, TabbedContent, TabPane
from tui.widgets.header import GnizaHeader as Header # noqa: F811 from tui.widgets.header import GnizaHeader as Header # noqa: F811
from textual.containers import Vertical, Horizontal from textual.containers import Vertical, Horizontal
from textual import work
from tui.config import parse_conf, write_conf, CONFIG_DIR from tui.config import parse_conf, write_conf, CONFIG_DIR
from tui.models import AppSettings from tui.models import AppSettings
from tui.widgets import DocsPanel from tui.backend import run_cli
from tui.widgets import DocsPanel, OperationLog
class SettingsScreen(Screen): class SettingsScreen(Screen):
@@ -20,6 +22,8 @@ class SettingsScreen(Screen):
with Horizontal(classes="screen-with-docs"): with Horizontal(classes="screen-with-docs"):
with Vertical(id="settings-screen"): with Vertical(id="settings-screen"):
yield Static("Settings", id="screen-title") yield Static("Settings", id="screen-title")
with TabbedContent():
with TabPane("General", id="tab-general"):
yield Static("Log Level:") yield Static("Log Level:")
yield Select( yield Select(
[("Debug", "debug"), ("Info", "info"), ("Warning", "warn"), ("Error", "error")], [("Debug", "debug"), ("Info", "info"), ("Warning", "warn"), ("Error", "error")],
@@ -34,6 +38,9 @@ class SettingsScreen(Screen):
yield Input(value=settings.bwlimit, id="set-bwlimit") yield Input(value=settings.bwlimit, id="set-bwlimit")
yield Static("Disk Usage Threshold (%, 0=disable):") yield Static("Disk Usage Threshold (%, 0=disable):")
yield Input(value=settings.disk_usage_threshold, id="set-diskthreshold") yield Input(value=settings.disk_usage_threshold, id="set-diskthreshold")
yield Static("Extra rsync options:")
yield Input(value=settings.rsync_extra_opts, id="set-rsyncopts")
with TabPane("Email", id="tab-email"):
yield Static("Notification Email:") yield Static("Notification Email:")
yield Input(value=settings.notify_email, id="set-email") yield Input(value=settings.notify_email, id="set-email")
yield Static("Notify On:") yield Static("Notify On:")
@@ -58,13 +65,12 @@ class SettingsScreen(Screen):
id="set-smtpsec", id="set-smtpsec",
value=settings.smtp_security, value=settings.smtp_security,
) )
with TabPane("SSH", id="tab-ssh"):
yield Static("SSH Timeout:") yield Static("SSH Timeout:")
yield Input(value=settings.ssh_timeout, id="set-sshtimeout") yield Input(value=settings.ssh_timeout, id="set-sshtimeout")
yield Static("SSH Retries:") yield Static("SSH Retries:")
yield Input(value=settings.ssh_retries, id="set-sshretries") yield Input(value=settings.ssh_retries, id="set-sshretries")
yield Static("Extra rsync options:") with TabPane("Web Dashboard", id="tab-web"):
yield Input(value=settings.rsync_extra_opts, id="set-rsyncopts")
yield Static("Web Dashboard", classes="section-label")
yield Static("Port:") yield Static("Port:")
yield Input(value=settings.web_port, id="set-web-port") yield Input(value=settings.web_port, id="set-web-port")
yield Static("Host:") yield Static("Host:")
@@ -73,6 +79,7 @@ class SettingsScreen(Screen):
yield Input(value=settings.web_api_key, password=True, id="set-web-key") yield Input(value=settings.web_api_key, password=True, id="set-web-key")
with Horizontal(id="set-buttons"): with Horizontal(id="set-buttons"):
yield Button("Save", variant="primary", id="btn-save") yield Button("Save", variant="primary", id="btn-save")
yield Button("Send Test Email", id="btn-test-email")
yield Button("Back", id="btn-back") yield Button("Back", id="btn-back")
yield DocsPanel.for_screen("settings-screen") yield DocsPanel.for_screen("settings-screen")
yield Footer() yield Footer()
@@ -82,6 +89,9 @@ class SettingsScreen(Screen):
self.app.pop_screen() self.app.pop_screen()
elif event.button.id == "btn-save": elif event.button.id == "btn-save":
self._save() self._save()
elif event.button.id == "btn-test-email":
self._save()
self._send_test_email()
def _get_select_val(self, sel_id: str, default: str) -> str: def _get_select_val(self, sel_id: str, default: str) -> str:
sel = self.query_one(sel_id, Select) sel = self.query_one(sel_id, Select)
@@ -113,5 +123,17 @@ class SettingsScreen(Screen):
write_conf(conf_path, settings.to_conf()) write_conf(conf_path, settings.to_conf())
self.notify("Settings saved.") self.notify("Settings saved.")
@work
async def _send_test_email(self) -> None:
log_screen = OperationLog("Send Test Email", show_spinner=True)
self.app.push_screen(log_screen)
log_screen.write("Sending test email...")
rc, stdout, stderr = await run_cli("test-email")
if stdout:
log_screen.write(stdout)
if stderr:
log_screen.write(stderr)
log_screen.finish()
def action_go_back(self) -> None: def action_go_back(self) -> None:
self.app.pop_screen() self.app.pop_screen()