asyncio's SubprocessTransport sends SIGKILL to child processes during
event loop cleanup, killing the gniza bash wrapper when the TUI exits.
Switch to subprocess.Popen which has no such cleanup behavior, allowing
backup jobs to continue running after the TUI is closed.
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>
- Save finished jobs to registry (24h TTL) so they survive TUI restart
- Fix PermissionError in PID check incorrectly marking alive processes as dead
- Handle CancelledError explicitly to preserve running status on TUI exit
- Tail log files for reconnected running jobs instead of showing stale output
- Detect actual return code from log content; show "?" for unknown status
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The subprocess stdout was a pipe to the TUI. When the TUI exited, the
pipe broke (SIGPIPE) and killed the backup process. Now the subprocess
writes to a log file in WORK_DIR, and the TUI tails it for live
display. When the TUI exits, the subprocess keeps running because it
writes to a file, not a pipe. On restart, the log file is loaded to
show output for reconnected or finished background jobs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously _load_registry silently dropped dead PIDs, so jobs that
completed in the background were invisible on restart. Now dead PIDs
are loaded as 'success' status so the user sees they completed. The
registry is cleaned up after loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously action_quit() killed all running jobs before exiting,
making it impossible to reconnect to them on restart. Now shows a
confirmation dialog when jobs are running and lets them continue
in the background.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Jobs are now saved to gniza-jobs.json in WORK_DIR when they start and
finish. On TUI restart, the registry is loaded and PIDs are checked —
still-running jobs appear in the Running Tasks screen and can be
killed. Reconnected jobs are polled every second to detect completion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Jobs were tied to screen @work tasks, so switch_screen cancelled the
worker and lost the process reference. Now start_job uses
asyncio.create_task so jobs survive screen changes and can be killed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SIGTERM was being ignored by child processes. Use SIGKILL via the
actual process group ID, with proc.kill() as fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Start CLI subprocesses in their own session so SIGTERM via
os.killpg reaches child processes (rsync, etc.) not just the shell.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backup and restore operations now run as background jobs instead of
blocking modal screens. Users can navigate away and check progress
from a dedicated Running Tasks screen. OperationLog supports attaching
to running jobs with live output polling.
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>
- Add TARGET_INCLUDE field for rsync include patterns (comma-separated)
- Pass TARGET_INCLUDE and TARGET_EXCLUDE to rsync in transfer_folder
- Include mode uses --include='*/' + patterns + --exclude='*' + --prune-empty-dirs
- Abort backup if remote disk usage >= 95%
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Disk info is now shown directly in the table. Speed test and detailed
disk info buttons/commands are no longer needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Disk Info: runs df -h and df -i on remote via SSH (or locally)
- Speed Test: uploads/downloads 10MB test file via rsync, measures Mbps
- Both available as CLI commands: gniza remotes disk-info/speed-test --name=NAME
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Last Run: timestamp of most recent backup log file
- Next Run: calculated from schedule type, time, and day settings
- Handles hourly, daily, weekly, monthly schedule types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browser prompts for username/password before showing the TUI.
Credentials from gniza.conf: WEB_USER (default: admin) + WEB_API_KEY.
- Monkey-patches textual-serve's aiohttp app with auth middleware
- Uses secrets.compare_digest for timing-safe comparison
- Install script generates credentials and prints them
- Skips auth if no WEB_API_KEY configured
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The landing page wasn't interactive because public_url resolved to
localhost, making WebSocket connections fail from remote browsers.
- Added multiple IP detection methods (socket, hostname -I, gethostbyname)
- Support --port= and --host= flag formats
- Print actual serving URL on startup
- Switch web start back to textual-serve (TUI in browser)
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>
Replace flat file list with a Tree widget that shows directory
structure. Strips remote path prefix to show relative paths only.
Folders shown in bold with trailing /, sorted dirs-first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Select a snapshot and click Browse Files to see all files in that
snapshot on the remote. Also adds 'gniza snapshots browse' CLI command.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows a switch to include/skip MySQL restore when the selected target
has MySQL enabled. Hidden for targets without MySQL. Passes --skip-mysql
to CLI when toggled off.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Other OperationLog dialogs (crontab show, remote test, retention) no
longer display the spinner since they complete quickly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>