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,59 +22,64 @@ 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")
yield Static("Log Level:") with TabbedContent():
yield Select( with TabPane("General", id="tab-general"):
[("Debug", "debug"), ("Info", "info"), ("Warning", "warn"), ("Error", "error")], yield Static("Log Level:")
id="set-loglevel", yield Select(
value=settings.log_level.lower(), [("Debug", "debug"), ("Info", "info"), ("Warning", "warn"), ("Error", "error")],
) id="set-loglevel",
yield Static("Log Retention (days):") value=settings.log_level.lower(),
yield Input(value=settings.log_retain, id="set-logretain") )
yield Static("Default Retention Count:") yield Static("Log Retention (days):")
yield Input(value=settings.retention_count, id="set-retention") yield Input(value=settings.log_retain, id="set-logretain")
yield Static("Default Bandwidth Limit (KB/s, 0=unlimited):") yield Static("Default Retention Count:")
yield Input(value=settings.bwlimit, id="set-bwlimit") yield Input(value=settings.retention_count, id="set-retention")
yield Static("Disk Usage Threshold (%, 0=disable):") yield Static("Default Bandwidth Limit (KB/s, 0=unlimited):")
yield Input(value=settings.disk_usage_threshold, id="set-diskthreshold") yield Input(value=settings.bwlimit, id="set-bwlimit")
yield Static("Notification Email:") yield Static("Disk Usage Threshold (%, 0=disable):")
yield Input(value=settings.notify_email, id="set-email") yield Input(value=settings.disk_usage_threshold, id="set-diskthreshold")
yield Static("Notify On:") yield Static("Extra rsync options:")
yield Select( yield Input(value=settings.rsync_extra_opts, id="set-rsyncopts")
[("Always", "always"), ("Failure only", "failure"), ("Never", "never")], with TabPane("Email", id="tab-email"):
id="set-notifyon", yield Static("Notification Email:")
value=settings.notify_on, yield Input(value=settings.notify_email, id="set-email")
) yield Static("Notify On:")
yield Static("SMTP Host:") yield Select(
yield Input(value=settings.smtp_host, id="set-smtphost") [("Always", "always"), ("Failure only", "failure"), ("Never", "never")],
yield Static("SMTP Port:") id="set-notifyon",
yield Input(value=settings.smtp_port, id="set-smtpport") value=settings.notify_on,
yield Static("SMTP User:") )
yield Input(value=settings.smtp_user, id="set-smtpuser") yield Static("SMTP Host:")
yield Static("SMTP Password:") yield Input(value=settings.smtp_host, id="set-smtphost")
yield Input(value=settings.smtp_password, password=True, id="set-smtppass") yield Static("SMTP Port:")
yield Static("SMTP From:") yield Input(value=settings.smtp_port, id="set-smtpport")
yield Input(value=settings.smtp_from, id="set-smtpfrom") yield Static("SMTP User:")
yield Static("SMTP Security:") yield Input(value=settings.smtp_user, id="set-smtpuser")
yield Select( yield Static("SMTP Password:")
[("TLS", "tls"), ("SSL", "ssl"), ("None", "none")], yield Input(value=settings.smtp_password, password=True, id="set-smtppass")
id="set-smtpsec", yield Static("SMTP From:")
value=settings.smtp_security, yield Input(value=settings.smtp_from, id="set-smtpfrom")
) yield Static("SMTP Security:")
yield Static("SSH Timeout:") yield Select(
yield Input(value=settings.ssh_timeout, id="set-sshtimeout") [("TLS", "tls"), ("SSL", "ssl"), ("None", "none")],
yield Static("SSH Retries:") id="set-smtpsec",
yield Input(value=settings.ssh_retries, id="set-sshretries") value=settings.smtp_security,
yield Static("Extra rsync options:") )
yield Input(value=settings.rsync_extra_opts, id="set-rsyncopts") with TabPane("SSH", id="tab-ssh"):
yield Static("Web Dashboard", classes="section-label") yield Static("SSH Timeout:")
yield Static("Port:") yield Input(value=settings.ssh_timeout, id="set-sshtimeout")
yield Input(value=settings.web_port, id="set-web-port") yield Static("SSH Retries:")
yield Static("Host:") yield Input(value=settings.ssh_retries, id="set-sshretries")
yield Input(value=settings.web_host, id="set-web-host") with TabPane("Web Dashboard", id="tab-web"):
yield Static("API Key:") yield Static("Port:")
yield Input(value=settings.web_api_key, password=True, id="set-web-key") yield Input(value=settings.web_port, id="set-web-port")
yield Static("Host:")
yield Input(value=settings.web_host, id="set-web-host")
yield Static("API 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()