Update checker

This commit is contained in:
binwiederhier
2026-01-17 20:36:15 -05:00
parent 603273ab9d
commit cc9f9c0d24
8 changed files with 265 additions and 3 deletions

View File

@@ -0,0 +1,93 @@
/**
* VersionChecker polls the /v1/version endpoint to detect server restarts
* or configuration changes, prompting users to refresh the page.
*/
const CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes
class VersionChecker {
constructor() {
this.initialConfigHash = null;
this.listener = null;
this.intervalId = null;
}
/**
* Starts the version checker worker. It stores the initial config hash
* from the config.js and polls the server every 5 minutes.
*/
startWorker() {
// Store initial config hash from the config loaded at page load
this.initialConfigHash = window.config?.config_hash || null;
if (!this.initialConfigHash) {
console.log("[VersionChecker] No initial config_hash found, version checking disabled");
return;
}
console.log("[VersionChecker] Starting version checker with initial hash:", this.initialConfigHash);
// Start polling
this.intervalId = setInterval(() => this.checkVersion(), CHECK_INTERVAL);
}
/**
* Stops the version checker worker.
*/
stopWorker() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
console.log("[VersionChecker] Stopped version checker");
}
/**
* Registers a listener that will be called when a version change is detected.
* @param {function} listener - Callback function that receives no arguments
*/
registerListener(listener) {
this.listener = listener;
}
/**
* Resets the listener.
*/
resetListener() {
this.listener = null;
}
/**
* Fetches the current version from the server and compares it with the initial config hash.
*/
async checkVersion() {
if (!this.initialConfigHash) {
return;
}
try {
const response = await fetch(`${window.config?.base_url || ""}/v1/version`);
if (!response.ok) {
console.log("[VersionChecker] Failed to fetch version:", response.status);
return;
}
const data = await response.json();
const currentHash = data.config_hash;
console.log("[VersionChecker] Checked version, initial:", this.initialConfigHash, "current:", currentHash);
if (currentHash && currentHash !== this.initialConfigHash) {
console.log("[VersionChecker] Config hash changed, notifying listener");
if (this.listener) {
this.listener();
}
}
} catch (error) {
console.log("[VersionChecker] Error checking version:", error);
}
}
}
const versionChecker = new VersionChecker();
export default versionChecker;

View File

@@ -1,6 +1,18 @@
import * as React from "react";
import { createContext, Suspense, useContext, useEffect, useState, useMemo } from "react";
import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery, ThemeProvider, createTheme } from "@mui/material";
import { createContext, Suspense, useContext, useEffect, useState, useMemo, useCallback } from "react";
import {
Box,
Toolbar,
CssBaseline,
Backdrop,
CircularProgress,
useMediaQuery,
ThemeProvider,
createTheme,
Snackbar,
Button,
Alert,
} from "@mui/material";
import { useLiveQuery } from "dexie-react-hooks";
import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
@@ -14,7 +26,13 @@ import userManager from "../app/UserManager";
import { expandUrl, getKebabCaseLangStr } from "../app/utils";
import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes";
import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks";
import {
useAccountListener,
useBackgroundProcesses,
useConnectionListeners,
useWebPushTopics,
useVersionChangeListener,
} from "./hooks";
import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging";
import Login from "./Login";
@@ -100,10 +118,12 @@ const updateTitle = (newNotificationsCount) => {
};
const Layout = () => {
const { t } = useTranslation();
const params = useParams();
const { account, setAccount } = useContext(AccountContext);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
const [versionChanged, setVersionChanged] = useState(false);
const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all());
const webPushTopics = useWebPushTopics();
@@ -115,9 +135,18 @@ const Layout = () => {
(config.base_url === s.baseUrl && params.topic === s.topic)
);
const handleVersionChange = useCallback(() => {
setVersionChanged(true);
}, []);
const handleRefresh = useCallback(() => {
window.location.reload();
}, []);
useConnectionListeners(account, subscriptions, users, webPushTopics);
useAccountListener(setAccount);
useBackgroundProcesses();
useVersionChangeListener(handleVersionChange);
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
return (
@@ -140,6 +169,23 @@ const Layout = () => {
/>
</Main>
<Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} />
<Snackbar
open={versionChanged}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
sx={{ bottom: { xs: 70, md: 24 } }}
>
<Alert
severity="info"
variant="filled"
action={
<Button color="inherit" size="small" onClick={handleRefresh}>
{t("common_refresh")}
</Button>
}
>
{t("version_update_available")}
</Alert>
</Snackbar>
</Box>
);
};

View File

@@ -9,6 +9,7 @@ import poller from "../app/Poller";
import pruner from "../app/Pruner";
import session from "../app/Session";
import accountApi from "../app/AccountApi";
import versionChecker from "../app/VersionChecker";
import { UnauthorizedError } from "../app/errors";
import notifier from "../app/Notifier";
import prefs from "../app/Prefs";
@@ -292,12 +293,14 @@ const startWorkers = () => {
poller.startWorker();
pruner.startWorker();
accountApi.startWorker();
versionChecker.startWorker();
};
const stopWorkers = () => {
poller.stopWorker();
pruner.stopWorker();
accountApi.stopWorker();
versionChecker.stopWorker();
};
export const useBackgroundProcesses = () => {
@@ -323,3 +326,15 @@ export const useAccountListener = (setAccount) => {
};
}, []);
};
/**
* Hook to detect version/config changes and call the provided callback when a change is detected.
*/
export const useVersionChangeListener = (onVersionChange) => {
useEffect(() => {
versionChecker.registerListener(onVersionChange);
return () => {
versionChecker.resetListener();
};
}, [onVersionChange]);
};