# Auto-Update Plan — Phase 1: Check & Notify > Created: 2026-03-25. Status: **planned, not started.** ## Backend Architecture ### Release Provider Abstraction ``` core/update/ release_provider.py — ABC: get_releases(), get_releases_page_url() gitea_provider.py — Gitea REST API implementation version_check.py — normalize_version(), is_newer() using packaging.version update_service.py — Background asyncio task + state machine ``` **`ReleaseProvider` interface** — two methods: - `get_releases(limit) → list[ReleaseInfo]` — fetch releases (newest first) - `get_releases_page_url() → str` — link for "view on web" **`GiteaReleaseProvider`** calls `GET {base_url}/api/v1/repos/{repo}/releases`. Swapping to GitHub later means implementing the same interface against `api.github.com`. **Data models:** ```python @dataclass(frozen=True) class AssetInfo: name: str # "LedGrab-v0.3.0-win-x64.zip" size: int # bytes download_url: str @dataclass(frozen=True) class ReleaseInfo: tag: str # "v0.3.0" version: str # "0.3.0" name: str # "LedGrab v0.3.0" body: str # release notes markdown prerelease: bool published_at: str # ISO 8601 assets: tuple[AssetInfo, ...] ``` ### Version Comparison `version_check.py` — normalize Gitea tags to PEP 440: - `v0.3.0-alpha.1` → `0.3.0a1` - `v0.3.0-beta.2` → `0.3.0b2` - `v0.3.0-rc.3` → `0.3.0rc3` Uses `packaging.version.Version` for comparison. ### Update Service Follows the **AutoBackupEngine pattern**: - Settings in `Database.get_setting("auto_update")` - asyncio.Task for periodic checks - 30s startup delay (avoid slowing boot) - 60s debounce on manual checks **State machine (Phase 1):** `IDLE → CHECKING → UPDATE_AVAILABLE` No download/apply in Phase 1 — just detection and notification. **Settings:** `enabled` (bool), `check_interval_hours` (float), `channel` ("stable" | "pre-release") **Persisted state:** `dismissed_version`, `last_check` (survives restarts) ### API Endpoints | Method | Path | Purpose | |--------|------|---------| | `GET` | `/api/v1/system/update/status` | Current state + available version | | `POST` | `/api/v1/system/update/check` | Trigger immediate check | | `POST` | `/api/v1/system/update/dismiss` | Dismiss notification for current version | | `GET` | `/api/v1/system/update/settings` | Get settings | | `PUT` | `/api/v1/system/update/settings` | Update settings | ### Wiring - New `get_update_service()` in `dependencies.py` - `UpdateService` created in `main.py` lifespan, `start()`/`stop()` alongside other engines - Router registered in `api/__init__.py` - WebSocket event: `update_available` fired via `processor_manager.fire_event()` ## Frontend ### Version badge highlight The existing `#server-version` pill in the header gets a CSS class `has-update` when a newer version exists — changes the background to `var(--warning-color)` with a subtle pulse, making it clickable to open the update panel in settings. ### Notification popup On `server:update_available` WebSocket event (and on page load if status says `has_update` and not dismissed): - A **persistent dismissible banner** slides in below the header (not the ephemeral 3s toast) - Shows: "Version {x.y.z} is available" + [View Release Notes] + [Dismiss] - Dismiss calls `POST /dismiss` and hides the bar for that version - Stored in `localStorage` so it doesn't re-show after page refresh for dismissed versions ### Settings tab: "Updates" New 5th tab in the settings modal: - Current version display - "Check for updates" button + spinner - Channel selector (stable / pre-release) via IconSelect - Auto-check toggle + interval selector - When update available: release name, notes preview, link to releases page ### i18n keys New `update.*` keys in `en.json`, `ru.json`, `zh.json` for all labels. ## Files to Create | File | Purpose | |------|---------| | `core/update/__init__.py` | Package init | | `core/update/release_provider.py` | Abstract provider interface + data models | | `core/update/gitea_provider.py` | Gitea API implementation | | `core/update/version_check.py` | Semver normalization + comparison | | `core/update/update_service.py` | Background service + state machine | | `api/routes/update.py` | REST endpoints | | `api/schemas/update.py` | Pydantic request/response models | ## Files to Modify | File | Change | |------|--------| | `api/__init__.py` | Register update router | | `api/dependencies.py` | Add `get_update_service()` | | `main.py` | Create & start/stop UpdateService in lifespan | | `templates/modals/settings.html` | Add Updates tab | | `static/js/features/settings.ts` | Update check/settings UI logic | | `static/js/core/api.ts` | Version badge highlight on health check | | `static/css/layout.css` | `.has-update` styles for version badge | | `static/locales/en.json` | i18n keys | | `static/locales/ru.json` | i18n keys | | `static/locales/zh.json` | i18n keys | ## Future Phases (not in scope) - **Phase 2**: Download & stage artifacts - **Phase 3**: Apply update & restart (external updater script, NSIS silent mode) - **Phase 4**: Checksums, "What's new" dialog, update history