Rsync exit 23 (partial transfer) now triggers an automatic retry to recover
failed files before accepting with a warning. Also adds a task counter next
to the clock in the header bar showing running job count.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split each screen into two columns: existing controls on the left,
a DocsPanel with Rich-markup documentation on the right (30% width).
Press F1 to toggle the help panel on/off from any screen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The settings screen wrote WORK_DIR="/usr/local/gniza/workdir" (root-mode
path) into gniza.conf. load_config then overrode the correct user-mode
path set by detect_mode, causing mktemp and rsync log redirects to fail
with Permission denied — crashing the bash script while rsync continued
as an orphan.
- Remove WORK_DIR from AppSettings model and settings screen
- Re-run detect_mode after config load to restore correct paths
- Removed stale WORK_DIR from user's gniza.conf
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add shquote() to escape single quotes in paths passed to remote_exec,
preventing shell injection via REMOTE_BASE containing single quotes
- Apply shquote to remote_exec calls in remotes.sh, backup.sh, transfer.sh, ssh.sh
- Add DISK_USAGE_THRESHOLD validation in config.sh
- Export SMTP_PASSWORD (was missing from export list)
- Fix WEB_PORT default mismatch: use 2323 consistently in from_conf and settings save
- Narrow exception catch in remotes.py disk info fetch to KeyError/LookupError
- Quote REMOTE_KEY in build_rsync_ssh_cmd for paths with spaces
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Disk usage threshold (default 95%) can now be controlled from
Settings. Set to 0 to disable the check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Revert from textual-serve back to Flask (textual-serve had WebSocket issues)
- Completely redesigned dashboard: modern dark theme, stat cards, clean tables
- Redesigned login page to match
- Restored API key generation in install script
- Keep API key field in TUI settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Serves the exact same TUI in the browser via textual-serve.
No more separate Flask app, API keys, or login page needed.
- gniza web start now runs textual-serve instead of Flask
- Simplified systemd service to use python3 -m tui --web
- Removed web_enabled and web_api_key from settings/models
- Simplified install script web setup (no API key generation)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Flask web dashboard with dark theme matching TUI
- Login with API key authentication
- Dashboard shows targets, remotes, schedules, last backup status
- Trigger backups from web UI per target
- View logs via /api/logs endpoint
- systemd service: gniza web install-service / remove-service / status
- CLI: gniza web start [--port=PORT] [--host=HOST]
- TUI settings: web enabled, port, host, API key fields
- Install script: optional web dashboard setup with auto-generated API key
- Uninstall script: removes systemd service
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Default: /usr/local/gniza/workdir (root) or ~/.local/state/gniza/workdir
(user). MySQL dumps and rclone temp configs now use WORK_DIR. Configurable
via gniza.conf or TUI Settings screen. Created during install.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace `is Select.BLANK` / `is not Select.BLANK` with isinstance
checks, as the sentinel identity varies across Textual versions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>