From 09b7dd184e3c236209a1e40d675b5e241543be9b Mon Sep 17 00:00:00 2001 From: shuki Date: Sat, 7 Mar 2026 04:31:35 +0200 Subject: [PATCH] Replace all Target/Remote terminology with Source/Destination and add search to file browsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename all user-facing strings: Target→Source, Remote→Destination across TUI, docs, web dashboard, bash scripts, config examples, and README - Add go-to-path search input to both FolderPicker and RemoteFolderPicker Co-Authored-By: Claude Opus 4.6 --- README.md | 10 +-- etc/gniza.conf.example | 4 +- etc/remote.conf.example | 4 +- etc/target.conf.example | 4 +- lib/backup.sh | 4 +- lib/notify.sh | 2 +- tui/docs.py | 110 ++++++++++++++-------------- tui/gniza.tcss | 19 ++++- tui/screens/backup.py | 10 +-- tui/screens/remote_edit.py | 8 +- tui/screens/remotes.py | 12 +-- tui/screens/restore.py | 10 +-- tui/screens/retention.py | 8 +- tui/screens/snapshots.py | 6 +- tui/screens/target_edit.py | 2 +- tui/screens/targets.py | 8 +- tui/screens/wizard.py | 12 +-- tui/widgets/folder_picker.py | 22 ++++++ tui/widgets/remote_folder_picker.py | 30 +++++++- web/templates/dashboard.html | 16 ++-- 20 files changed, 183 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index 9a0e1b5..cc94b06 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gniza - Linux Backup Manager -A generic Linux backup tool with a Python Textual TUI, web GUI, and CLI interface. Define named backup targets (sets of directories), configure remote destinations (SSH, local, S3, Google Drive), and run incremental backups with rsync `--link-dest` deduplication. +A generic Linux backup tool with a Python Textual TUI, web GUI, and CLI interface. Define named backup sources (sets of directories), configure backup destinations (SSH, local, S3, Google Drive), and run incremental backups with rsync `--link-dest` deduplication. ``` ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ @@ -24,12 +24,12 @@ A generic Linux backup tool with a Python Textual TUI, web GUI, and CLI interfac ## Features -- **Target-based backups** - Define named profiles with sets of directories to back up -- **Include/exclude filters** - Rsync include or exclude patterns per target (comma-separated) +- **Source-based backups** - Define named profiles with sets of directories to back up +- **Include/exclude filters** - Rsync include or exclude patterns per source (comma-separated) - **MySQL backup** - Dump all or selected databases alongside directory backups -- **Multiple remote types** - SSH, local (USB/NFS), S3, Google Drive +- **Multiple destination types** - SSH, local (USB/NFS), S3, Google Drive - **Incremental snapshots** - rsync `--link-dest` for space-efficient deduplication -- **Disk space safety** - Abort backup if remote disk usage exceeds configurable threshold (default 95%) +- **Disk space safety** - Abort backup if destination disk usage exceeds configurable threshold (default 95%) - **Textual TUI** - Beautiful terminal UI powered by [Textual](https://textual.textualize.io/) - **Web dashboard** - Access the full TUI from any browser with HTTP Basic Auth - **CLI interface** - Scriptable commands for automation and cron diff --git a/etc/gniza.conf.example b/etc/gniza.conf.example index 3bee00c..10b5ce0 100644 --- a/etc/gniza.conf.example +++ b/etc/gniza.conf.example @@ -7,7 +7,7 @@ BACKUP_MODE="full" # Default bandwidth limit in KB/s (0 = unlimited) BWLIMIT=0 -# Default retention: number of snapshots to keep per target +# Default retention: number of snapshots to keep per source RETENTION_COUNT=30 # Logging @@ -33,7 +33,7 @@ SSH_RETRIES=3 # Extra rsync options (careful: validated for safe characters) RSYNC_EXTRA_OPTS="" -# Disk usage threshold (%). Backups abort if remote disk usage >= this value. +# Disk usage threshold (%). Backups abort if destination disk usage >= this value. # Set to 0 to disable the check. DISK_USAGE_THRESHOLD=95 diff --git a/etc/remote.conf.example b/etc/remote.conf.example index b3f022f..5915656 100644 --- a/etc/remote.conf.example +++ b/etc/remote.conf.example @@ -1,7 +1,7 @@ -# gniza — Remote Configuration +# gniza — Destination Configuration # Copy to remotes.d/.conf -# Remote type: ssh, local, s3, gdrive +# Destination type: ssh, local, s3, gdrive REMOTE_TYPE="ssh" # SSH settings diff --git a/etc/target.conf.example b/etc/target.conf.example index 80a0f71..111fbb6 100644 --- a/etc/target.conf.example +++ b/etc/target.conf.example @@ -1,4 +1,4 @@ -# gniza — Target Configuration +# gniza — Source Configuration # Copy to targets.d/.conf TARGET_NAME="example" @@ -10,7 +10,7 @@ TARGET_RETENTION="" TARGET_PRE_HOOK="" TARGET_POST_HOOK="" TARGET_ENABLED="yes" -# Remote Source (pull files from a remote before backing up) +# Source Server (pull files from a source server before backing up) #TARGET_SOURCE_TYPE="local" # local | ssh | s3 | gdrive # SSH source #TARGET_SOURCE_HOST="" diff --git a/lib/backup.sh b/lib/backup.sh index 72a75b0..bba8e43 100644 --- a/lib/backup.sh +++ b/lib/backup.sh @@ -356,13 +356,13 @@ backup_all_targets() { echo "============================================" echo "Timestamp: $(timestamp)" echo "Duration: $(human_duration $duration)" - echo "Remotes: $(echo "$remotes" | tr '\n' ' ')" + echo "Destinations: $(echo "$remotes" | tr '\n' ' ')" echo "Total: $total" echo "Succeeded: ${C_GREEN}${succeeded}${C_RESET}" if (( failed > 0 )); then echo "Failed: ${C_RED}${failed}${C_RESET}" echo "" - echo "Failed targets:" + echo "Failed sources:" echo "$failed_targets" else echo "Failed: 0" diff --git a/lib/notify.sh b/lib/notify.sh index 3700eec..3325324 100644 --- a/lib/notify.sh +++ b/lib/notify.sh @@ -165,7 +165,7 @@ send_backup_report() { if [[ -n "$failed_targets" ]]; then body+=""$'\n' - body+="Failed targets:"$'\n' + body+="Failed sources:"$'\n' body+="$failed_targets"$'\n' fi diff --git a/tui/docs.py b/tui/docs.py index 2e06dae..7f2348f 100644 --- a/tui/docs.py +++ b/tui/docs.py @@ -2,105 +2,105 @@ SCREEN_DOCS = { "backup-screen": ( "[bold]Backup Screen[/bold]\n" "\n" - "Run backups of configured targets to remote destinations.\n" + "Run backups of configured sources to backup destinations.\n" "\n" "[bold]Fields:[/bold]\n" - " [bold]Target[/bold] - The backup target to run. Each target defines which folders and databases to back up.\n" + " [bold]Source[/bold] - The backup source to run. Each source defines which folders and databases to back up.\n" "\n" - " [bold]Remote[/bold] - Where to send the backup. Choose a specific remote or 'Default (all)' to back up to every configured remote.\n" + " [bold]Destination[/bold] - Where to send the backup. Choose a specific destination or 'Default (all)' to back up to every configured destination.\n" "\n" "[bold]Buttons:[/bold]\n" - " [bold]Run Backup[/bold] - Back up the selected target to the selected remote.\n" - " [bold]Backup All[/bold] - Back up all enabled targets to all their configured remotes.\n" + " [bold]Run Backup[/bold] - Back up the selected source to the selected destination.\n" + " [bold]Backup All[/bold] - Back up all enabled sources to all their configured destinations.\n" "\n" "[bold]Tips:[/bold]\n" " - Backups run in the background. You can monitor progress on the Running Tasks screen.\n" - " - Each backup creates a timestamped snapshot on the remote, so you can restore any point in time.\n" + " - Each backup creates a timestamped snapshot on the destination, so you can restore any point in time.\n" " - Use retention cleanup to remove old snapshots." ), "restore-screen": ( "[bold]Restore Screen[/bold]\n" "\n" - "Restore files and databases from a remote snapshot.\n" + "Restore files and databases from a destination snapshot.\n" "\n" "[bold]Fields:[/bold]\n" - " [bold]Target[/bold] - The backup target to restore.\n" - " [bold]Remote[/bold] - The remote that holds the snapshot.\n" - " [bold]Snapshot[/bold] - The specific point-in-time snapshot to restore from. Loaded after selecting target and remote.\n" + " [bold]Source[/bold] - The backup source to restore.\n" + " [bold]Destination[/bold] - The destination that holds the snapshot.\n" + " [bold]Snapshot[/bold] - The specific point-in-time snapshot to restore from. Loaded after selecting source and destination.\n" " [bold]Restore location[/bold] - Choose 'In-place' to overwrite original files, or 'Custom directory' to restore to a different path.\n" - " [bold]Restore MySQL[/bold] - Toggle whether to restore MySQL databases (only shown if the target has MySQL backup enabled).\n" + " [bold]Restore MySQL[/bold] - Toggle whether to restore MySQL databases (only shown if the source has MySQL backup enabled).\n" "\n" "[bold]Warning:[/bold]\n" " In-place restore will overwrite existing files. Consider restoring to a custom directory first to verify the contents." ), "targets-screen": ( - "[bold]Targets Screen[/bold]\n" + "[bold]Sources Screen[/bold]\n" "\n" - "List and manage backup targets. A target defines what to back up: folders, file patterns, and optionally MySQL databases.\n" + "List and manage backup sources. A source defines what to back up: folders, file patterns, and optionally MySQL databases.\n" "\n" "[bold]Table columns:[/bold]\n" - " [bold]Name[/bold] - Unique target identifier.\n" + " [bold]Name[/bold] - Unique source identifier.\n" " [bold]Folders[/bold] - Comma-separated paths to back up.\n" - " [bold]Enabled[/bold] - Whether the target is active.\n" + " [bold]Enabled[/bold] - Whether the source is active.\n" "\n" "[bold]Buttons:[/bold]\n" - " [bold]Add[/bold] - Create a new target.\n" - " [bold]Edit[/bold] - Modify the selected target.\n" - " [bold]Delete[/bold] - Remove the selected target.\n" + " [bold]Add[/bold] - Create a new source.\n" + " [bold]Edit[/bold] - Modify the selected source.\n" + " [bold]Delete[/bold] - Remove the selected source.\n" "\n" "[bold]Tips:[/bold]\n" - " - You need at least one target and one remote before you can run backups.\n" - " - Disable a target to skip it during 'Backup All' without deleting it." + " - You need at least one source and one destination before you can run backups.\n" + " - Disable a source to skip it during 'Backup All' without deleting it." ), "target-edit": ( - "[bold]Target Editor[/bold]\n" + "[bold]Source Editor[/bold]\n" "\n" - "Add or edit a backup target configuration.\n" + "Add or edit a backup source configuration.\n" "\n" "[bold]Fields:[/bold]\n" " [bold]Name[/bold] - Unique identifier (letters, digits, dash, underscore; max 32 chars).\n" " [bold]Folders[/bold] - Comma-separated paths to back up. Use Browse to pick folders interactively.\n" " [bold]Include/Exclude[/bold] - Glob patterns to filter files (e.g. *.conf, *.log).\n" - " [bold]Remote override[/bold] - Force this target to a specific remote instead of the default.\n" + " [bold]Destination override[/bold] - Force this source to a specific destination instead of the default.\n" " [bold]Retention override[/bold] - Custom snapshot count.\n" " [bold]Pre/Post hooks[/bold] - Shell commands to run before/after the backup.\n" "\n" "[bold]Source section:[/bold]\n" - " Set Source Type to pull files from a remote server instead of backing up local folders.\n" + " Set Source Type to pull files from a source server instead of backing up local folders.\n" " [bold]Local[/bold] - Default. Back up folders on this machine.\n" - " [bold]SSH[/bold] - Pull files from a remote server via SSH/rsync before backing up.\n" + " [bold]SSH[/bold] - Pull files from a source server via SSH/rsync before backing up.\n" " [bold]S3[/bold] - Pull files from an S3-compatible bucket.\n" " [bold]Google Drive[/bold] - Pull files from Google Drive via service account.\n" - " When using a remote source, specify remote paths in the Folders field instead of local paths.\n" + " When using a source server, specify remote paths in the Folders field instead of local paths.\n" "\n" "[bold]MySQL section:[/bold]\n" " Enable MySQL to dump databases alongside files. Choose 'All databases' or select specific ones. Leave user/password empty for socket auth." ), "remotes-screen": ( - "[bold]Remotes Screen[/bold]\n" + "[bold]Destinations Screen[/bold]\n" "\n" - "List and manage remote backup destinations.\n" + "List and manage backup destinations.\n" "\n" "[bold]Table columns:[/bold]\n" - " [bold]Name[/bold] - Unique remote identifier.\n" + " [bold]Name[/bold] - Unique destination identifier.\n" " [bold]Type[/bold] - Connection type (SSH, Local, S3, Google Drive).\n" " [bold]Host/Path[/bold] - Connection details.\n" " [bold]Disk[/bold] - Available space (loaded in background).\n" "\n" "[bold]Buttons:[/bold]\n" - " [bold]Add[/bold] - Create a new remote.\n" - " [bold]Edit[/bold] - Modify the selected remote.\n" - " [bold]Test[/bold] - Verify connectivity to the remote.\n" - " [bold]Delete[/bold] - Remove the selected remote.\n" + " [bold]Add[/bold] - Create a new destination.\n" + " [bold]Edit[/bold] - Modify the selected destination.\n" + " [bold]Test[/bold] - Verify connectivity to the destination.\n" + " [bold]Delete[/bold] - Remove the selected destination.\n" "\n" "[bold]Tips:[/bold]\n" - " - Always test a new remote before running backups.\n" - " - The Disk column shows used/total space and may take a moment to load for SSH remotes." + " - Always test a new destination before running backups.\n" + " - The Disk column shows used/total space and may take a moment to load for SSH destinations." ), "remote-edit": ( - "[bold]Remote Editor[/bold]\n" + "[bold]Destination Editor[/bold]\n" "\n" - "Add or edit a remote backup destination.\n" + "Add or edit a backup destination.\n" "\n" "[bold]Types:[/bold]\n" " [bold]SSH[/bold] - Remote server via SSH/rsync.\n" @@ -112,30 +112,30 @@ SCREEN_DOCS = { " Host, port, user, and either SSH key or password authentication.\n" "\n" "[bold]Common fields:[/bold]\n" - " [bold]Base path[/bold] - Root directory for backups on the remote.\n" + " [bold]Base path[/bold] - Root directory for backups on the destination.\n" " [bold]Bandwidth limit[/bold] - Throttle transfer speed in KB/s (0 = unlimited).\n" " [bold]Retention count[/bold] - Max snapshots to keep.\n" "\n" "[bold]Tips:[/bold]\n" - " - For SSH, ensure the remote user has write access to the base path.\n" + " - For SSH, ensure the destination user has write access to the base path.\n" " - Use key-based auth for unattended backups." ), "snapshots-screen": ( "[bold]Snapshots Browser[/bold]\n" "\n" - "Browse snapshots stored on remote destinations.\n" + "Browse snapshots stored on backup destinations.\n" "\n" "[bold]Fields:[/bold]\n" - " [bold]Target[/bold] - The backup target.\n" - " [bold]Remote[/bold] - The remote holding snapshots.\n" + " [bold]Source[/bold] - The backup source.\n" + " [bold]Destination[/bold] - The destination holding snapshots.\n" "\n" "[bold]Buttons:[/bold]\n" - " [bold]Load Snapshots[/bold] - Fetch the list of available snapshots from the remote.\n" + " [bold]Load Snapshots[/bold] - Fetch the list of available snapshots from the destination.\n" " [bold]Browse Files[/bold] - View the file tree inside the selected snapshot.\n" "\n" "[bold]Tips:[/bold]\n" - " - Each snapshot is a timestamped directory on the remote containing the backed-up files.\n" - " - Loading may take a moment for SSH remotes.\n" + " - Each snapshot is a timestamped directory on the destination containing the backed-up files.\n" + " - Loading may take a moment for SSH destinations.\n" " - Use the Restore screen to actually restore files from a snapshot." ), "retention-screen": ( @@ -144,16 +144,16 @@ SCREEN_DOCS = { "Remove old snapshots based on retention count.\n" "\n" "[bold]Fields:[/bold]\n" - " [bold]Target[/bold] - Run cleanup for a specific target.\n" - " [bold]Default retention count[/bold] - Number of snapshots to keep per target/remote pair.\n" + " [bold]Source[/bold] - Run cleanup for a specific source.\n" + " [bold]Default retention count[/bold] - Number of snapshots to keep per source/destination pair.\n" "\n" "[bold]Buttons:[/bold]\n" - " [bold]Run Cleanup[/bold] - Clean old snapshots for the selected target.\n" - " [bold]Cleanup All[/bold] - Clean old snapshots for all targets.\n" + " [bold]Run Cleanup[/bold] - Clean old snapshots for the selected source.\n" + " [bold]Cleanup All[/bold] - Clean old snapshots for all sources.\n" " [bold]Save[/bold] - Save the default retention count.\n" "\n" "[bold]How it works:[/bold]\n" - " Retention keeps the N most recent snapshots and deletes the rest. Per-target or per-remote overrides take priority over the default count.\n" + " Retention keeps the N most recent snapshots and deletes the rest. Per-source or per-destination overrides take priority over the default count.\n" "\n" "[bold]Warning:[/bold]\n" " Deleted snapshots cannot be recovered." @@ -169,7 +169,7 @@ SCREEN_DOCS = { " [bold]Type[/bold] - Frequency (hourly/daily/weekly/monthly/custom).\n" " [bold]Time[/bold] - When the backup runs.\n" " [bold]Last/Next Run[/bold] - Timing information.\n" - " [bold]Targets/Remotes[/bold] - Scope of the schedule.\n" + " [bold]Sources/Destinations[/bold] - Scope of the schedule.\n" "\n" "[bold]Buttons:[/bold]\n" " [bold]Toggle Active[/bold] - Enable/disable a schedule.\n" @@ -193,8 +193,8 @@ SCREEN_DOCS = { "\n" "[bold]Fields:[/bold]\n" " [bold]Time[/bold] - The time of day to run (HH:MM).\n" - " [bold]Targets[/bold] - Which targets to back up. Leave empty to back up all targets.\n" - " [bold]Remotes[/bold] - Which remotes to use. Leave empty to use all remotes.\n" + " [bold]Sources[/bold] - Which sources to back up. Leave empty to back up all sources.\n" + " [bold]Destinations[/bold] - Which destinations to use. Leave empty to use all destinations.\n" "\n" "[bold]Tips:[/bold]\n" " - Daily schedules let you pick specific days of the week.\n" @@ -232,7 +232,7 @@ SCREEN_DOCS = { " [bold]Log Retention[/bold] - Days to keep log files.\n" " [bold]Retention Count[/bold] - Default number of snapshots to keep.\n" " [bold]Bandwidth Limit[/bold] - Default transfer speed limit in KB/s.\n" - " [bold]Disk Threshold[/bold] - Warn when remote disk usage exceeds this percentage.\n" + " [bold]Disk Threshold[/bold] - Warn when destination disk usage exceeds this percentage.\n" "\n" "[bold]Email Notifications:[/bold]\n" " Configure SMTP to receive email alerts on backup success or failure.\n" @@ -245,7 +245,7 @@ SCREEN_DOCS = { " Port, host, and API key for the web interface.\n" "\n" "[bold]Tips:[/bold]\n" - " - Per-target and per-remote settings override these defaults." + " - Per-source and per-destination settings override these defaults." ), "running-tasks-screen": ( "[bold]Running Tasks[/bold]\n" diff --git a/tui/gniza.tcss b/tui/gniza.tcss index cde3f38..8668c57 100644 --- a/tui/gniza.tcss +++ b/tui/gniza.tcss @@ -225,7 +225,7 @@ HelpModal { width: 90%; max-width: 70; height: 80%; - max-height: 30; + max-height: 35; padding: 1; background: $panel; border: thick $accent; @@ -237,7 +237,22 @@ HelpModal { margin: 0 0 1 0; } -#fp-tree { +#fp-search-row { + height: auto; + margin: 0 0 1 0; +} + +#fp-search-row Input { + width: 1fr; +} + +#fp-search-row Button { + width: auto; + min-width: 6; + margin: 0 0 0 1; +} + +#fp-tree, #fp-remote-tree { height: 1fr; } diff --git a/tui/screens/backup.py b/tui/screens/backup.py index b0861a4..b3f24b3 100644 --- a/tui/screens/backup.py +++ b/tui/screens/backup.py @@ -20,7 +20,7 @@ class BackupScreen(Screen): with Vertical(id="backup-screen"): yield Static("Backup", id="screen-title") if not targets: - yield Static("No targets configured. Add a target first.") + yield Static("No sources configured. Add a source first.") else: yield Static("Source:") yield Select( @@ -71,21 +71,21 @@ class BackupScreen(Screen): elif event.button.id == "btn-backup": target_sel = self.query_one("#backup-target", Select) if not isinstance(target_sel.value, str): - self.notify("Please select a target", severity="error") + self.notify("Please select a source", severity="error") return target = str(target_sel.value) remote_sel = self.query_one("#backup-remote", Select) remote = str(remote_sel.value) if isinstance(remote_sel.value, str) else "" - msg = f"Run backup for target '{target}'?" + msg = f"Run backup for source '{target}'?" if remote: - msg += f"\nRemote: {remote}" + msg += f"\nDestination: {remote}" self.app.push_screen( ConfirmDialog(msg, "Confirm Backup"), callback=lambda ok: self._do_backup(target, remote) if ok else None, ) elif event.button.id == "btn-backup-all": self.app.push_screen( - ConfirmDialog("Backup ALL targets now?", "Confirm Backup"), + ConfirmDialog("Backup ALL sources now?", "Confirm Backup"), callback=lambda ok: self._do_backup_all() if ok else None, ) diff --git a/tui/screens/remote_edit.py b/tui/screens/remote_edit.py index 5710c35..9c6c191 100644 --- a/tui/screens/remote_edit.py +++ b/tui/screens/remote_edit.py @@ -167,7 +167,7 @@ class RemoteEditScreen(Screen): callback=self._base_path_selected, ) else: - self.notify("Browse not available for this remote type", severity="warning") + self.notify("Browse not available for this destination type", severity="warning") def _base_path_selected(self, path: str | None) -> None: if path: @@ -183,7 +183,7 @@ class RemoteEditScreen(Screen): self.notify("Invalid name.", severity="error") return if (CONFIG_DIR / "remotes.d" / f"{name}.conf").exists(): - self.notify(f"Remote '{name}' already exists.", severity="error") + self.notify(f"Destination '{name}' already exists.", severity="error") return else: name = self._edit_name @@ -213,12 +213,12 @@ class RemoteEditScreen(Screen): ) if rtype == "ssh" and not remote.host: - self.notify("Host is required for SSH remotes", severity="error") + self.notify("Host is required for SSH destinations", severity="error") return conf = CONFIG_DIR / "remotes.d" / f"{name}.conf" write_conf(conf, remote.to_conf()) - self.notify(f"Remote '{name}' saved.") + self.notify(f"Destination '{name}' saved.") self.dismiss(name) def action_go_back(self) -> None: diff --git a/tui/screens/remotes.py b/tui/screens/remotes.py index 973adac..b208b06 100644 --- a/tui/screens/remotes.py +++ b/tui/screens/remotes.py @@ -69,22 +69,22 @@ class RemotesScreen(Screen): from tui.screens.remote_edit import RemoteEditScreen self.app.push_screen(RemoteEditScreen(name), callback=lambda _: self._refresh_table()) else: - self.notify("Select a remote first", severity="warning") + self.notify("Select a destination first", severity="warning") elif event.button.id == "btn-test": name = self._selected_remote() if name: self._test_remote(name) else: - self.notify("Select a remote first", severity="warning") + self.notify("Select a destination first", severity="warning") elif event.button.id == "btn-delete": name = self._selected_remote() if name: self.app.push_screen( - ConfirmDialog(f"Delete remote '{name}'? This cannot be undone.", "Delete Remote"), + ConfirmDialog(f"Delete destination '{name}'? This cannot be undone.", "Delete Destination"), callback=lambda ok: self._delete_remote(name) if ok else None, ) else: - self.notify("Select a remote first", severity="warning") + self.notify("Select a destination first", severity="warning") @work async def _fetch_disk_info(self) -> None: @@ -101,7 +101,7 @@ class RemotesScreen(Screen): @work async def _test_remote(self, name: str) -> None: - log_screen = OperationLog(f"Testing Remote: {name}", show_spinner=False) + log_screen = OperationLog(f"Testing Destination: {name}", show_spinner=False) self.app.push_screen(log_screen) rc, stdout, stderr = await run_cli("remotes", "test", f"--name={name}") if stdout: @@ -118,7 +118,7 @@ class RemotesScreen(Screen): conf = CONFIG_DIR / "remotes.d" / f"{name}.conf" if conf.is_file(): conf.unlink() - self.notify(f"Remote '{name}' deleted.") + self.notify(f"Destination '{name}' deleted.") self._refresh_table() def action_go_back(self) -> None: diff --git a/tui/screens/restore.py b/tui/screens/restore.py index 6af33b1..08fe4fc 100644 --- a/tui/screens/restore.py +++ b/tui/screens/restore.py @@ -23,14 +23,14 @@ class RestoreScreen(Screen): with Vertical(id="restore-screen"): yield Static("Restore", id="screen-title") if not targets or not remotes: - yield Static("Both targets and remotes must be configured for restore.") + yield Static("Both sources and destinations must be configured for restore.") else: yield Static("Source:") yield Select([(t, t) for t in targets], id="restore-target", prompt="Select source") yield Static("Destination:") yield Select([(r, r) for r in remotes], id="restore-remote", prompt="Select destination") yield Static("Snapshot:") - yield Select([], id="restore-snapshot", prompt="Select target and remote first") + yield Select([], id="restore-snapshot", prompt="Select source and destination first") yield Static("Restore location:") with RadioSet(id="restore-location"): yield RadioButton("In-place (original)", value=True) @@ -115,10 +115,10 @@ class RestoreScreen(Screen): remote_sel = self.query_one("#restore-remote", Select) snap_sel = self.query_one("#restore-snapshot", Select) if not isinstance(target_sel.value, str): - self.notify("Select a target", severity="error") + self.notify("Select a source", severity="error") return if not isinstance(remote_sel.value, str): - self.notify("Select a remote", severity="error") + self.notify("Select a destination", severity="error") return if not isinstance(snap_sel.value, str): self.notify("Select a snapshot", severity="error") @@ -134,7 +134,7 @@ class RestoreScreen(Screen): except Exception: restore_mysql = True skip_mysql = not restore_mysql - msg = f"Restore snapshot?\n\nTarget: {target}\nRemote: {remote}\nSnapshot: {snapshot}" + msg = f"Restore snapshot?\n\nSource: {target}\nDestination: {remote}\nSnapshot: {snapshot}" if dest: msg += f"\nDestination: {dest}" else: diff --git a/tui/screens/retention.py b/tui/screens/retention.py index 5fb5e66..16434c9 100644 --- a/tui/screens/retention.py +++ b/tui/screens/retention.py @@ -23,7 +23,7 @@ class RetentionScreen(Screen): with Vertical(id="retention-screen"): yield Static("Retention Cleanup", id="screen-title") if not targets: - yield Static("No targets configured.") + yield Static("No sources configured.") else: yield Static("Source:") yield Select( @@ -49,7 +49,7 @@ class RetentionScreen(Screen): elif event.button.id == "btn-cleanup": target_sel = self.query_one("#ret-target", Select) if not isinstance(target_sel.value, str): - self.notify("Select a target first", severity="error") + self.notify("Select a source first", severity="error") return target = str(target_sel.value) self.app.push_screen( @@ -58,7 +58,7 @@ class RetentionScreen(Screen): ) elif event.button.id == "btn-cleanup-all": self.app.push_screen( - ConfirmDialog("Run retention cleanup for ALL targets?", "Confirm"), + ConfirmDialog("Run retention cleanup for ALL sources?", "Confirm"), callback=lambda ok: self._do_cleanup_all() if ok else None, ) elif event.button.id == "btn-save-count": @@ -82,7 +82,7 @@ class RetentionScreen(Screen): @work async def _do_cleanup_all(self) -> None: - log_screen = OperationLog("Retention: All Targets", show_spinner=False) + log_screen = OperationLog("Retention: All Sources", show_spinner=False) self.app.push_screen(log_screen) rc = await stream_cli(log_screen.write, "retention", "--all") if rc == 0: diff --git a/tui/screens/snapshots.py b/tui/screens/snapshots.py index 5850760..8aa1322 100644 --- a/tui/screens/snapshots.py +++ b/tui/screens/snapshots.py @@ -34,7 +34,7 @@ class SnapshotsScreen(Screen): with Vertical(id="snapshots-screen"): yield Static("Snapshots Browser", id="screen-title") if not targets or not remotes: - yield Static("Targets and remotes must be configured to browse snapshots.") + yield Static("Sources and destinations must be configured to browse snapshots.") else: yield Static("Source:") yield Select([(t, t) for t in targets], id="snap-target", prompt="Select source") @@ -77,7 +77,7 @@ class SnapshotsScreen(Screen): target_sel = self.query_one("#snap-target", Select) remote_sel = self.query_one("#snap-remote", Select) if not isinstance(target_sel.value, str) or not isinstance(remote_sel.value, str): - self.notify("Select target and remote first", severity="error") + self.notify("Select source and destination first", severity="error") return target = str(target_sel.value) remote = str(remote_sel.value) @@ -98,7 +98,7 @@ class SnapshotsScreen(Screen): target_sel = self.query_one("#snap-target", Select) remote_sel = self.query_one("#snap-remote", Select) if not isinstance(target_sel.value, str) or not isinstance(remote_sel.value, str): - self.notify("Select target and remote first", severity="error") + self.notify("Select source and destination first", severity="error") return snapshot = self._selected_snapshot() if not snapshot: diff --git a/tui/screens/target_edit.py b/tui/screens/target_edit.py index 37907f3..b95832f 100644 --- a/tui/screens/target_edit.py +++ b/tui/screens/target_edit.py @@ -81,7 +81,7 @@ class TargetEditScreen(Screen): yield Input(value=target.include, placeholder="*.conf,docs/", id="te-include") yield Static("Exclude patterns:") yield Input(value=target.exclude, placeholder="*.tmp,*.log", id="te-exclude") - yield Static("Remote override:") + yield Static("Destination override:") yield Input(value=target.remote, placeholder="Leave empty for default", id="te-remote") yield Static("Retention override:") yield Input(value=target.retention, placeholder="Leave empty for default", id="te-retention") diff --git a/tui/screens/targets.py b/tui/screens/targets.py index c540d1f..8ed97ff 100644 --- a/tui/screens/targets.py +++ b/tui/screens/targets.py @@ -61,22 +61,22 @@ class TargetsScreen(Screen): from tui.screens.target_edit import TargetEditScreen self.app.push_screen(TargetEditScreen(name), callback=lambda _: self._refresh_table()) else: - self.notify("Select a target first", severity="warning") + self.notify("Select a source first", severity="warning") elif event.button.id == "btn-delete": name = self._selected_target() if name: self.app.push_screen( - ConfirmDialog(f"Delete target '{name}'? This cannot be undone.", "Delete Target"), + ConfirmDialog(f"Delete source '{name}'? This cannot be undone.", "Delete Source"), callback=lambda ok: self._delete_target(name) if ok else None, ) else: - self.notify("Select a target first", severity="warning") + self.notify("Select a source first", severity="warning") def _delete_target(self, name: str) -> None: conf = CONFIG_DIR / "targets.d" / f"{name}.conf" if conf.is_file(): conf.unlink() - self.notify(f"Target '{name}' deleted.") + self.notify(f"Source '{name}' deleted.") self._refresh_table() def action_go_back(self) -> None: diff --git a/tui/screens/wizard.py b/tui/screens/wizard.py index e2085d2..6563762 100644 --- a/tui/screens/wizard.py +++ b/tui/screens/wizard.py @@ -16,20 +16,20 @@ class WizardScreen(Screen): yield Static( "[bold]Welcome to gniza Backup Manager![/bold]\n\n" "This wizard will help you set up your first backup:\n\n" - " 1. Configure a backup destination (remote)\n" - " 2. Define what to back up (target)\n" + " 1. Configure a backup destination\n" + " 2. Define what to back up (source)\n" " 3. Optionally run your first backup\n", id="wizard-welcome", markup=True, ) if not has_remotes(): - yield Button("Step 1: Add Remote", variant="primary", id="wiz-remote") + yield Button("Step 1: Add Destination", variant="primary", id="wiz-remote") else: - yield Static("[green]Remote configured.[/green]", markup=True) + yield Static("[green]Destination configured.[/green]", markup=True) if not has_targets(): - yield Button("Step 2: Add Target", variant="primary", id="wiz-target") + yield Button("Step 2: Add Source", variant="primary", id="wiz-target") else: - yield Static("[green]Target configured.[/green]", markup=True) + yield Static("[green]Source configured.[/green]", markup=True) yield Button("Continue to Main Menu", id="wiz-continue") yield Button("Skip Setup", id="wiz-skip") yield Footer() diff --git a/tui/widgets/folder_picker.py b/tui/widgets/folder_picker.py index d898497..3214123 100644 --- a/tui/widgets/folder_picker.py +++ b/tui/widgets/folder_picker.py @@ -23,6 +23,9 @@ class FolderPicker(ModalScreen[str | None]): def compose(self) -> ComposeResult: with Vertical(id="folder-picker"): yield Static(self._title, id="fp-title") + with Horizontal(id="fp-search-row"): + yield Input(placeholder="Go to path (e.g. /var/www)", id="fp-search") + yield Button("Go", id="fp-go", variant="primary") yield _DirOnly(self._start, id="fp-tree") with Horizontal(id="fp-new-row"): yield Input(placeholder="New folder name", id="fp-new-name") @@ -42,11 +45,30 @@ class FolderPicker(ModalScreen[str | None]): if event.button.id == "fp-select": path = self._get_selected_path() self.dismiss(str(path) if path else None) + elif event.button.id == "fp-go": + self._go_to_path() elif event.button.id == "fp-create": self._create_folder() else: self.dismiss(None) + def on_input_submitted(self, event: Input.Submitted) -> None: + if event.input.id == "fp-search": + self._go_to_path() + + def _go_to_path(self) -> None: + raw = self.query_one("#fp-search", Input).value.strip() + if not raw: + return + target = Path(raw).expanduser() + if not target.is_dir(): + self.notify(f"Not a directory: {target}", severity="error") + return + # Replace the tree with a new root + tree = self.query_one("#fp-tree", _DirOnly) + tree.path = target + tree.reload() + def _create_folder(self) -> None: name = self.query_one("#fp-new-name", Input).value.strip() if not name: diff --git a/tui/widgets/remote_folder_picker.py b/tui/widgets/remote_folder_picker.py index ff50923..b6cd9e5 100644 --- a/tui/widgets/remote_folder_picker.py +++ b/tui/widgets/remote_folder_picker.py @@ -2,7 +2,7 @@ import subprocess from textual.app import ComposeResult from textual.screen import ModalScreen -from textual.widgets import Tree, Static, Button +from textual.widgets import Tree, Static, Button, Input from textual.containers import Horizontal, Vertical @@ -33,6 +33,9 @@ class RemoteFolderPicker(ModalScreen[str | None]): def compose(self) -> ComposeResult: with Vertical(id="folder-picker"): yield Static(f"{self._title} ({self._user}@{self._host})", id="fp-title") + with Horizontal(id="fp-search-row"): + yield Input(placeholder="Go to path (e.g. /var/www)", id="fp-search") + yield Button("Go", id="fp-go", variant="primary") yield Tree("/", id="fp-remote-tree") with Horizontal(id="fp-buttons"): yield Button("Select", variant="primary", id="fp-select") @@ -107,8 +110,33 @@ class RemoteFolderPicker(ModalScreen[str | None]): self.dismiss(str(node.data)) else: self.dismiss(None) + elif event.button.id == "fp-go": + self._go_to_path() else: self.dismiss(None) + def on_input_submitted(self, event: Input.Submitted) -> None: + if event.input.id == "fp-search": + self._go_to_path() + + def _go_to_path(self) -> None: + raw = self.query_one("#fp-search", Input).value.strip() + if not raw: + return + path = raw if raw.startswith("/") else "/" + raw + dirs = self._list_dirs(path) + tree = self.query_one("#fp-remote-tree", Tree) + tree.clear() + tree.root.data = path + tree.root.set_label(path) + if not dirs: + self.notify(f"No subdirectories in {path}", severity="warning") + return + for d in dirs: + name = d.rstrip("/").rsplit("/", 1)[-1] + child = tree.root.add(name, data=d, allow_expand=True) + child.add_leaf("...", data=None) + tree.root.expand() + def action_cancel(self) -> None: self.dismiss(None) diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html index a0006cb..3e1edf7 100644 --- a/web/templates/dashboard.html +++ b/web/templates/dashboard.html @@ -402,14 +402,14 @@ td.name {
-
Targets
+
Sources
{{ targets|length }}
{{ targets|selectattr('enabled', 'equalto', 'yes')|list|length }} enabled
-
Remotes
+
Destinations
{{ remotes|length }}
-
storage destinations
+
backup destinations
Schedules
@@ -435,13 +435,13 @@ td.name {
- Targets + Sources
{% if targets %} - + {% for t in targets %} @@ -464,7 +464,7 @@ td.name {
NameRemoteStatus
NameDestinationStatus
{% else %} -
No targets configured
+
No sources configured
{% endif %}
@@ -474,7 +474,7 @@ td.name {
- Remotes + Destinations
@@ -492,7 +492,7 @@ td.name { {% else %} -
No remotes configured
+
No destinations configured
{% endif %}