Skip to content

Commit df88054

Browse files
committed
frontend: add versioning
This enables us to notify the user when the server was updated ensuring that there are no compatibility issues.
1 parent 95def98 commit df88054

File tree

5 files changed

+189
-0
lines changed

5 files changed

+189
-0
lines changed

frontend/src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Footer } from "./components/Footer";
4040
import favicon from "favicon";
4141
import logo from "logo";
4242
import { Message, MessageType } from './types';
43+
import { startVersionChecking } from './versionChecker';
4344

4445
import "./styles/main.scss";
4546
import { docs } from "links";
@@ -86,12 +87,14 @@ export function App() {
8687

8788
useEffect(() => {
8889
refresh_access_token();
90+
startVersionChecking(10);
8991
})
9092
useEffect(() => {
9193
if (loggedIn.value === AppState.LoggedIn) {
9294
refreshInterval = setInterval(async () => {
9395
await refresh_access_token();
9496
}, 1000 * 60 * refreshMinutes);
97+
9598
} else {
9699
clearInterval(refreshInterval);
97100
}

frontend/src/locales/de.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,5 +164,10 @@ export const de ={
164164
},
165165
"wg_client": {
166166
"not_initialized": "Der WireGuard-Client ist nicht initialisiert. Bitte lade die Seite neu."
167+
},
168+
"version_checker": {
169+
"new_version_available": "Eine neue Version der Anwendung ist verfügbar. Jetzt neu laden, um die neuesten Funktionen und Fehlerbehebungen zu erhalten?",
170+
"already_latest": "Du verwendest bereits die neueste Version.",
171+
"new_version_confirm": "Eine neue Version ist verfügbar. Jetzt neu laden?"
167172
}
168173
};

frontend/src/locales/en.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,5 +168,10 @@ export const en = {
168168
},
169169
"wg_client": {
170170
"not_initialized": "The WireGuard client is not initialized. Please try again later.",
171+
},
172+
"version_checker": {
173+
"new_version_available": "A new version of the application is available. Reload now to get the latest features and bug fixes?",
174+
"already_latest": "You are already using the latest version.",
175+
"new_version_confirm": "A new version is available. Reload now?"
171176
}
172177
};

frontend/src/versionChecker.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/* esp32-remote-access
2+
* Copyright (C) 2024 Frederic Henrichs <[email protected]>
3+
*
4+
* This library is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2 of the License, or (at your option) any later version.
8+
*
9+
* This library is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
* Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with this library; if not, write to the
16+
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17+
* Boston, MA 02111-1307, USA.
18+
*/
19+
20+
import i18n from './i18n';
21+
22+
let currentVersion: string | null = null;
23+
let versionCheckInterval: NodeJS.Timeout | null = null;
24+
25+
// Function to get the current build version
26+
async function getCurrentVersionHash(): Promise<string | null> {
27+
try {
28+
// Try to get version from the build manifest or a dedicated version endpoint
29+
const response = await fetch('/version.json', {
30+
cache: 'no-cache',
31+
headers: {
32+
'Cache-Control': 'no-cache, no-store, must-revalidate',
33+
'Pragma': 'no-cache',
34+
'Expires': '0'
35+
}
36+
});
37+
38+
if (response.ok) {
39+
const data = await response.json();
40+
return data.buildHash;
41+
}
42+
43+
// Fallback: use the index.html timestamp as version
44+
const indexResponse = await fetch('/', {
45+
cache: 'no-cache',
46+
headers: {
47+
'Cache-Control': 'no-cache, no-store, must-revalidate',
48+
'Pragma': 'no-cache',
49+
'Expires': '0'
50+
}
51+
});
52+
53+
if (indexResponse.ok) {
54+
const lastModified = indexResponse.headers.get('last-modified');
55+
return lastModified || Date.now().toString();
56+
}
57+
58+
} catch (error) {
59+
console.warn('Failed to check version:', error);
60+
}
61+
62+
return null;
63+
}
64+
65+
// Function to check if a new version is available
66+
async function checkForNewVersion(): Promise<boolean> {
67+
const newVersion = await getCurrentVersionHash();
68+
69+
if (!newVersion) {
70+
return false;
71+
}
72+
73+
if (currentVersion === null) {
74+
currentVersion = newVersion;
75+
return false;
76+
}
77+
78+
return currentVersion !== newVersion;
79+
}
80+
81+
export function startVersionChecking(intervalMinutes: number = 10) {
82+
getCurrentVersionHash().then(version => {
83+
currentVersion = version;
84+
});
85+
86+
if (versionCheckInterval) {
87+
clearInterval(versionCheckInterval);
88+
}
89+
90+
versionCheckInterval = setInterval(async () => {
91+
try {
92+
const hasNewVersion = await checkForNewVersion();
93+
94+
if (hasNewVersion) {
95+
if (confirm(i18n.t('version_checker.new_version_available'))) {
96+
window.location.reload();
97+
} else {
98+
stopVersionChecking();
99+
}
100+
}
101+
} catch (error) {
102+
console.warn('Version check failed:', error);
103+
}
104+
}, intervalMinutes * 60 * 1000);
105+
}
106+
107+
export function stopVersionChecking() {
108+
if (versionCheckInterval) {
109+
clearInterval(versionCheckInterval);
110+
versionCheckInterval = null;
111+
}
112+
}
113+
114+
export async function forceCheckForUpdates(): Promise<void> {
115+
const hasNewVersion = await checkForNewVersion();
116+
117+
if (hasNewVersion) {
118+
if (confirm(i18n.t('version_checker.new_version_confirm'))) {
119+
window.location.reload();
120+
}
121+
} else {
122+
alert(i18n.t('version_checker.already_latest'));
123+
}
124+
}
125+
126+
export function forceReload(): void {
127+
if ('caches' in window) {
128+
caches.keys().then(names => {
129+
names.forEach(name => {
130+
caches.delete(name);
131+
});
132+
});
133+
}
134+
135+
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
136+
navigator.serviceWorker.controller.postMessage({
137+
type: 'CLEAR_CACHE'
138+
});
139+
}
140+
141+
const keysToKeep = ['debugMode', 'currentConnection', 'loginSalt'];
142+
const keysToRemove: string[] = [];
143+
144+
for (let i = 0; i < localStorage.length; i++) {
145+
const key = localStorage.key(i);
146+
if (key && !keysToKeep.includes(key)) {
147+
keysToRemove.push(key);
148+
}
149+
}
150+
151+
keysToRemove.forEach(key => localStorage.removeItem(key));
152+
153+
window.location.href = window.location.href + (window.location.href.includes('?') ? '&' : '?') + '_t=' + Date.now();
154+
}

frontend/vite-plugin-version.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Plugin } from 'vite';
2+
3+
const versionPlugin: Plugin = {
4+
name: 'version-generator',
5+
generateBundle() {
6+
// Generate version file with build timestamp and hash
7+
const version = {
8+
version: process.env.npm_package_version || '1.0.0',
9+
buildTime: new Date().toISOString(),
10+
buildHash: Date.now().toString(36) + Math.random().toString(36).substr(2),
11+
timestamp: Date.now()
12+
};
13+
14+
this.emitFile({
15+
type: 'asset',
16+
fileName: 'version.json',
17+
source: JSON.stringify(version, null, 2)
18+
});
19+
}
20+
};
21+
22+
export default versionPlugin;

0 commit comments

Comments
 (0)