Add in-app update checker and one-click updater
Settings now shows when a newer version is available, with the real version number read from the remote's package.json (via the git binary already required for this deployment model, so it works against any remote, not just GitHub), and an "Uppdatera nu" button that stops the server, pulls, reinstalls dependencies and restarts automatically - warning explicitly if a race is currently active. The existing app-version poller already reloads the page once a restart is detected, so no new client-side polling was needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
62
src/app.js
62
src/app.js
@@ -1476,6 +1476,18 @@ const TRANSLATIONS = {
|
||||
"settings.logo_upload": "Ladda logo",
|
||||
"settings.logo_clear": "Rensa logo",
|
||||
"settings.logo_note": "Logotypen visas i huvudmenyn, overlay och publik träningsprofil. PDF-export försöker bädda in den via backend.",
|
||||
"settings.update_title": "Uppdateringar",
|
||||
"settings.update_current_version": "Installerad version",
|
||||
"settings.update_checking": "Kontrollerar om det finns en ny version...",
|
||||
"settings.update_up_to_date": "Du har den senaste versionen.",
|
||||
"settings.update_available": "Version {version} finns tillgänglig.",
|
||||
"settings.update_error": "Kunde inte kontrollera version: {msg}",
|
||||
"settings.update_check_now": "Kontrollera nu",
|
||||
"settings.update_run_now": "Uppdatera nu",
|
||||
"settings.update_confirm": "Uppdatera RC RaceCore till version {version}?\n\nServern stoppas, ny kod hämtas och startas om automatiskt. Det tar vanligtvis under en minut och sidan laddas om av sig själv när servern är tillbaka.",
|
||||
"settings.update_confirm_active_session": "OBS: Ett aktivt race/session pågår just nu. En uppdatering stoppar servern under uppdateringen och avbryter tidtagningen. Uppdatera bara mellan heat.",
|
||||
"settings.update_started": "Uppdatering startad. Servern startar om - sidan laddas om automatiskt när den är tillbaka.",
|
||||
"settings.update_failed": "Uppdatering misslyckades: {msg}",
|
||||
"settings.storage": "Lagring",
|
||||
"settings.server_backups": "SQLite-backuper på servern",
|
||||
"settings.server_backups_hint": "En backup skapas automatiskt före varje sessionstart. De 30 senaste sparas i data/backups.",
|
||||
@@ -2954,6 +2966,18 @@ const TRANSLATIONS = {
|
||||
"settings.logo_upload": "Upload logo",
|
||||
"settings.logo_clear": "Clear logo",
|
||||
"settings.logo_note": "The logo is shown in the main menu, overlays, and public training profiles. PDF export attempts to embed it via the backend.",
|
||||
"settings.update_title": "Updates",
|
||||
"settings.update_current_version": "Installed version",
|
||||
"settings.update_checking": "Checking for a new version...",
|
||||
"settings.update_up_to_date": "You have the latest version.",
|
||||
"settings.update_available": "Version {version} is available.",
|
||||
"settings.update_error": "Could not check for updates: {msg}",
|
||||
"settings.update_check_now": "Check now",
|
||||
"settings.update_run_now": "Update now",
|
||||
"settings.update_confirm": "Update RC RaceCore to version {version}?\n\nThe server will stop, pull the new code and restart automatically. This usually takes under a minute, and the page reloads on its own once the server is back.",
|
||||
"settings.update_confirm_active_session": "Note: a race/session is currently active. Updating stops the server during the update and interrupts timing. Only update between heats.",
|
||||
"settings.update_started": "Update started. The server is restarting - the page will reload automatically once it's back.",
|
||||
"settings.update_failed": "Update failed: {msg}",
|
||||
"settings.storage": "Storage",
|
||||
"settings.server_backups": "Server SQLite backups",
|
||||
"settings.server_backups_hint": "A backup is created automatically before every session start. The latest 30 are retained in data/backups.",
|
||||
@@ -3566,6 +3590,11 @@ let settingsStorageNoticeIsError = false;
|
||||
let settingsDraftActive = false;
|
||||
let serverBackups = [];
|
||||
let serverBackupsLoaded = false;
|
||||
let updateStatus = null;
|
||||
let updateStatusLoaded = false;
|
||||
let updateCheckPending = false;
|
||||
let updateNotice = "";
|
||||
let updateNoticeIsError = false;
|
||||
const backend = {
|
||||
available: false,
|
||||
lastSyncAt: null,
|
||||
@@ -3639,8 +3668,9 @@ async function init() {
|
||||
updateSyncStatus();
|
||||
});
|
||||
startAppVersionPolling();
|
||||
setInterval(() => refreshUpdateStatus({ render: currentView === "settings" }), 20 * 60 * 1000);
|
||||
|
||||
Promise.allSettled([hydrateFromBackend(), loadAmmcConfigFromBackend(), refreshServerBackups({ render: false })]).finally(() => {
|
||||
Promise.allSettled([hydrateFromBackend(), loadAmmcConfigFromBackend(), refreshServerBackups({ render: false }), refreshUpdateStatus({ render: false })]).finally(() => {
|
||||
applyTheme();
|
||||
renderNav();
|
||||
renderViewFromBackground();
|
||||
@@ -4507,6 +4537,28 @@ async function refreshServerBackups({ render = true } = {}) {
|
||||
return serverBackups;
|
||||
}
|
||||
|
||||
async function refreshUpdateStatus({ render = true, force = false } = {}) {
|
||||
updateCheckPending = true;
|
||||
try {
|
||||
const response = await fetch(`${getBackendUrl()}/api/update-status${force ? "?force=1" : ""}`, { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
updateStatus = await response.json();
|
||||
} catch (error) {
|
||||
updateStatus = { checked: false, available: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
updateStatusLoaded = true;
|
||||
updateCheckPending = false;
|
||||
if (render && currentView === "settings") renderView();
|
||||
return updateStatus;
|
||||
}
|
||||
|
||||
async function runUpdateNow() {
|
||||
const response = await fetch(`${getBackendUrl()}/api/update/run`, { method: "POST" });
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function createServerBackup(reason = "manual", session = null) {
|
||||
await syncStateToBackend();
|
||||
if (!backend.available) throw new Error(backend.lastError || "Backend offline");
|
||||
@@ -4708,6 +4760,14 @@ function renderView() {
|
||||
createManualServerBackup: () => createServerBackup("manual"),
|
||||
createSafetyServerBackup: () => createServerBackup("pre_change"),
|
||||
refreshServerBackups,
|
||||
updateStatus,
|
||||
updateStatusLoaded,
|
||||
updateCheckPending,
|
||||
updateNotice,
|
||||
updateNoticeIsError,
|
||||
refreshUpdateStatus,
|
||||
runUpdateNow,
|
||||
setUpdateNotice: (message, isError = false) => { updateNotice = message; updateNoticeIsError = isError; },
|
||||
});
|
||||
break;
|
||||
case "guide":
|
||||
|
||||
Reference in New Issue
Block a user