## Implementation Plan: Automated Backup Schedule & Disk Cleanup ### 1. Objective - Seed a default daily backup schedule on first install so new instances automatically get backup protection. - Integrate workspace and dangling-image cleanup into the periodic container reconcile loop so that stale resources are reclaimed automatically. - Surface a disk‑usage warning in the admin dashboard when any managed storage directory exceeds a configurable threshold (default 85%). ### 2. Files to Modify | File | Change | |------|--------| | `services/backup/store.py` | In `init_db()` (or a new `post_init` hook), create a default schedule row if `backup_schedules` is empty. | | `services/containers/service.py` | Extend the `_reconcile()` method to also run workspace GC (`gc_workspaces()` logic) and dangling image prune. | | `services/backup/store.py` | Add a `get_disk_warnings()` function that returns a list of directories exceeding a threshold percentage. | | `routers/admin/backups.py` | Modify the `/admin/backups/data` endpoint to include `disk_warnings` in the JSON response. | | `templates/admin_backups.html` | Conditionally render a warning banner if `disk_warnings` is non‑empty. | ### 3. Detailed Code Changes #### 3.1 Seed Default Backup Schedule In `services/backup/store.py`, after `init_db` or in the `BackupService.__init__`: ```python # At end of init_db() or in a new method _ensure_default_schedule() from .models import BackupSchedule from datetime import datetime, timezone if not db.query(BackupSchedule).first(): default_schedule = BackupSchedule( uid=..., name="Daily database backup", target="database", cron="0 3 * * *", keep_last=14, enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) db.add(default_schedule) db.commit() ``` *Note: UIDs are generated via `uuid4()`. Use the same pattern as `BackupScheduleModel._generate_uid` in `models.py`.* #### 3.2 Automated Workspace GC & Image Pruning in Reconcile Loop In `services/containers/service.py`, method `_reconcile()`: ```python # After the existing orphan‑container reap loop, add: import shutil, os from pathlib import Path # Workspace GC: remove directories that have no active container instance ws_dir = settings.CONTAINER_WORKSPACES_DIR # e.g. /tmp/deploy_workspaces active_instance_uids = {c.instance_uid for c in db.query(Container).all() if c.instance_uid is not None} for ws_path in Path(ws_dir).iterdir(): if ws_path.is_dir() and ws_path.name not in active_instance_uids: shutil.rmtree(ws_path, ignore_errors=True) logger.info(f"Removed stale workspace: {ws_path}") # Dangling image prune backend.image_prune() # already implemented in cli/containers.py ``` *Note: `backend` is `self._backend` (the container backend instance).* #### 3.3 Disk Threshold Warning In `services/backup/store.py`, add: ```python import shutil from pathlib import Path def get_disk_warnings(storage_paths: dict, threshold: float = 85.0) -> list[dict]: warnings = [] for name, path_str in storage_paths.items(): try: usage = shutil.disk_usage(path_str) used_pct = (usage.used / usage.total) * 100 if used_pct > threshold: warnings.append({ "name": name, "path": path_str, "used_percent": used_pct, "free_bytes": usage.free }) except OSError: continue return warnings ``` In `routers/admin/backups.py`, inside the `/admin/backups/data` handler: ```python from services.backup.store import get_disk_warnings, _storage_paths # ... after building the response dict, add: storage_paths = _storage_paths() response["disk_warnings"] = get_disk_warnings(storage_paths) ``` In `templates/admin_backups.html`, somewhere near the top: ```html {% if disk_warnings %} {% endif %} ``` *Note: The template receives `disk_warnings` via the context dictionary passed by the route.* ### 4. Configuration Threshold The disk threshold can be an environment variable `DISK_WARNING_PERCENT` (default 85). Use `os.getenv("DISK_WARNING_PERCENT", "85")` and convert to float. ### 5. Testing - **New unit tests** in `tests/unit/services/backup/` (create if missing) for `get_disk_warnings`. - **New integration test** in `tests/api/admin/backups/` to verify that the default schedule is seeded after a fresh DB init. - **New test** for the reconcile loop: verify that a stale workspace directory is removed after reconcile runs. - Existing tests must still pass. ### 6. Handling PEP 668 (System Package Restrictions) To run the project’s verification commands (`make test-unit` and `ruff check .`), use a virtual environment: ```bash python3 -m venv /tmp/venv source /tmp/venv/bin/activate pip install --upgrade pip pip install -e '.[dev]' -q make test-unit deactivate ``` For lint, inside the same venv: ```bash pip install -q ruff ruff check . ``` If the system restricts `pip install` outside a venv, the above steps will bypass the PEP 668 error. --- ## Definition of Done - [ ] A daily database backup schedule is automatically created on a fresh install (schedules table empty). Verified by checking the admin dashboard or by querying the database after `init_db`. - [ ] The container reconcile loop automatically removes workspace directories belonging to non‑existent container instances and prunes dangling Docker images. Verified by creating a stale workspace directory, triggering a reconcile, and confirming it is deleted. - [ ] When any monitored storage directory exceeds the configured threshold (default 85%), the `/admin/backups/data` endpoint includes a `disk_warnings` list, and the admin dashboard template renders a visible warning banner. Verified by temporarily setting a low threshold and checking the dashboard. - [ ] All existing unit and integration tests pass. Command: `make test-unit` (exit code 0). - [ ] Code passes `ruff check .` with no errors (exit code 0). - [ ] No regression in backup schedule creation, editing, toggling, or deletion (existing tests cover these).