- Abstract ReleaseProvider interface (Gitea impl, swappable for GitHub/GitLab) - Background UpdateService with periodic checks, debounce, dismissed version persistence - Install type detection (installer/portable/docker/dev) with platform-aware asset matching - Download with progress events, silent NSIS reinstall, portable ZIP/tarball swap scripts - Version badge pulse animation, dismissible banner with icon buttons, Settings > Updates tab - Single source of truth: pyproject.toml version via importlib.metadata, CI stamps tag with sed - API: GET/POST status, check, dismiss, apply, GET/PUT settings - i18n: en, ru, zh (27+ keys each)
5.1 KiB
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:
@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.0a1v0.3.0-beta.2→0.3.0b2v0.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()independencies.py UpdateServicecreated inmain.pylifespan,start()/stop()alongside other engines- Router registered in
api/__init__.py - WebSocket event:
update_availablefired viaprocessor_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 /dismissand hides the bar for that version - Stored in
localStorageso 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