feat: add auto-update system with release checking, notification UI, and install-type-aware apply

- 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)
This commit is contained in:
2026-03-25 13:16:18 +03:00
parent d2b3fdf786
commit 382a42755d
30 changed files with 1750 additions and 44 deletions

View File

@@ -104,6 +104,7 @@
</button>
</div>
</header>
<div id="update-banner" class="update-banner" style="display:none"></div>
<div class="container">
<div class="tabs">
<div class="tab-panel" id="tab-dashboard" role="tabpanel" aria-labelledby="tab-btn-dashboard">

View File

@@ -12,6 +12,7 @@
<button class="settings-tab-btn" data-settings-tab="backup" onclick="switchSettingsTab('backup')" data-i18n="settings.tab.backup">Backup</button>
<button class="settings-tab-btn" data-settings-tab="mqtt" onclick="switchSettingsTab('mqtt')" data-i18n="settings.tab.mqtt">MQTT</button>
<button class="settings-tab-btn" data-settings-tab="appearance" onclick="switchSettingsTab('appearance')" data-i18n="settings.tab.appearance">Appearance</button>
<button class="settings-tab-btn" data-settings-tab="updates" onclick="switchSettingsTab('updates')" data-i18n="settings.tab.updates">Updates</button>
</div>
<div class="modal-body">
@@ -206,6 +207,84 @@
<!-- Rendered dynamically by renderAppearanceTab() -->
</div>
<!-- ═══ Updates tab ═══ -->
<div id="settings-panel-updates" class="settings-panel">
<!-- Current version + status -->
<div class="form-group">
<div class="label-row">
<label data-i18n="update.status_label">Update Status</label>
</div>
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
<span data-i18n="update.current_version">Current version:</span>
<strong id="update-current-version"></strong>
</div>
<div id="update-status-text" style="font-size:0.9rem;font-weight:600;margin-bottom:0.5rem;"></div>
<div id="update-last-check" style="font-size:0.85rem;color:var(--text-muted);margin-bottom:0.3rem;"></div>
<div style="font-size:0.85rem;color:var(--text-muted);margin-bottom:0.75rem;">
<span data-i18n="update.install_type_label">Install type:</span>
<span id="update-install-type"></span>
</div>
<!-- Download progress bar -->
<div style="display:none;margin-bottom:0.5rem;height:4px;background:var(--border-color);border-radius:2px;overflow:hidden;">
<div id="update-progress-bar" style="width:0%;height:100%;background:var(--primary-color);transition:width 0.3s;"></div>
</div>
<div style="display:flex;gap:0.5rem;">
<button id="update-check-btn" class="btn btn-secondary" onclick="checkForUpdates()" style="flex:1">
<span data-i18n="update.check_now">Check for Updates</span>
<span id="update-check-spinner" class="spinner-inline" style="display:none"></span>
</button>
<button id="update-apply-btn" class="btn btn-primary" onclick="applyUpdate()" style="flex:1;display:none" data-i18n="update.apply_now">Update Now</button>
</div>
</div>
<!-- Release notes preview -->
<div class="form-group" style="display:none">
<div class="label-row">
<label data-i18n="update.release_notes">Release Notes</label>
</div>
<pre id="update-release-notes" style="max-height:200px;overflow-y:auto;font-size:0.82rem;white-space:pre-wrap;word-break:break-word;padding:0.5rem;background:var(--bg-secondary);border-radius:var(--radius-sm);border:1px solid var(--border-color);"></pre>
</div>
<!-- Settings -->
<div class="form-group">
<div class="label-row">
<label data-i18n="update.auto_check_label">Auto-Check Settings</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="update.auto_check_hint">Periodically check for new releases in the background.</small>
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
<input type="checkbox" id="update-enabled">
<label for="update-enabled" style="margin:0" data-i18n="update.enable">Enable auto-check</label>
</div>
<div style="display:flex;gap:0.5rem;margin-bottom:0.5rem;">
<div style="flex:1">
<label for="update-interval" style="font-size:0.85rem" data-i18n="update.interval_label">Check interval</label>
<select id="update-interval" style="width:100%">
<option value="1">1h</option>
<option value="6">6h</option>
<option value="12">12h</option>
<option value="24">24h</option>
<option value="48">48h</option>
<option value="168">7d</option>
</select>
</div>
<div style="flex:1">
<label for="update-channel" style="font-size:0.85rem" data-i18n="update.channel_label">Channel</label>
<select id="update-channel">
<option value="false">Stable</option>
<option value="true">Pre-release</option>
</select>
</div>
</div>
<button class="btn btn-primary" onclick="saveUpdateSettings()" style="width:100%" data-i18n="update.save_settings">Save Settings</button>
</div>
</div>
<div id="settings-error" class="error-message" style="display:none;"></div>
</div>
<div class="modal-footer">