Skip to content
Merged
4 changes: 4 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ jobs:
local-path: ./src/renderer/dist
remote-path: /home/www/opencor.ws/app
sync: full
# Note: we upload version.json last so that OpenCOR's Web app only reports a new version once all the other
# files have been uploaded.
ftp-mirror-options: --exclude-glob=assets/version.json
ftp-post-sync-commands: put -O /home/www/opencor.ws/app/assets ./src/renderer/dist/assets/version.json
- name: Archive OpenCOR's Web app
run: bun archive:web
- name: Release OpenCOR's Web app
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/cddev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ jobs:
local-path: ./src/renderer/dist
remote-path: /home/www/opencor.ws/appdev
sync: full
# Note: we upload version.json last so that OpenCOR's Web app only reports a new version once all the other
# files have been uploaded.
ftp-mirror-options: --exclude-glob=assets/version.json
ftp-post-sync-commands: put -O /home/www/opencor.ws/appdev/assets ./src/renderer/dist/assets/version.json
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"url": "git+https://github.com/opencor/webapp.git"
},
"type": "module",
"version": "1.20260924.0",
"version": "1.20260924.1",
"engines": {
"bun": ">=1.2.0"
},
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
},
"./style.css": "./dist/opencor.css"
},
"version": "1.20260924.0",
"version": "1.20260924.1",
"libopencorVersion": "1.20260803.0",
"scripts": {
"build": "vite build && bun scripts/generate.version.ts",
Expand Down
22 changes: 19 additions & 3 deletions src/renderer/scripts/generate.version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,32 @@ const version = (JSON.parse(fs.readFileSync(packageJsonPath)) as { version: stri

// Make sure that the dist/assets folder exists.

const distAssetsPath = path.join(__dirname, '../dist/assets');
const distPath = path.join(__dirname, '../dist');
const distAssetsPath = path.join(distPath, 'assets');

if (!fs.existsSync(distAssetsPath)) {
fs.mkdirSync(distAssetsPath, { recursive: true });
}

// List the files that the Web app needs to refresh when force reloading it.
// Note: we exclude index.html (the Web app refreshes the page itself using its actual URL), version.json (it is always
// fetched with cache busting), stats.html (it is not part of the Web app), and libOpenCOR's files (their path
// contains libOpenCOR's version, so they never get stale).

const files = (fs.readdirSync(distPath, { recursive: true }) as string[])
.map((file) => file.split(path.sep).join('/'))
.filter(
(file) =>
fs.statSync(path.join(distPath, file)).isFile() &&
!['index.html', 'assets/version.json', 'stats.html'].includes(file) &&
!file.startsWith('libopencor/')
)
.sort();

// Write the version file.

fs.writeFileSync(path.join(distAssetsPath, 'version.json'), JSON.stringify({ version }, null, 2));
fs.writeFileSync(path.join(distAssetsPath, 'version.json'), JSON.stringify({ version, files }, null, 2));

// Log the generated version.

console.log(`Generated version.json with version ${version}.`);
console.log(`Generated version.json with version ${version} and ${files.length} files.`);
61 changes: 60 additions & 1 deletion src/renderer/src/common/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const latestVersion = vue.ref<string>('');

interface IVersionInfo {
version: string;
files?: string[];
}

const checkForUpdates = async (): Promise<boolean> => {
Expand Down Expand Up @@ -95,6 +96,64 @@ const startCheck = (): void => {
}
};

// Force reload the Web app.
// Note: there is no standard way to force reload a page (location.reload(true) is non-standard and ignored by modern
// browsers). Also, our assets don't have a hash in their filenames (see vite.config.ts), so they may be stale in
// the HTTP cache too. So, we first fetch the page and all the files listed in the latest version.json file with
// cache: 'reload', which bypasses the HTTP cache and updates it, and then we reload the page. We do this within a
// given amount of time after which we reload the page anyway, so that the user is never left waiting forever.

const FORCE_RELOAD_TIMEOUT = 30 * 1000; // 30 seconds.

let forceReloading = false;

const forceReload = async (): Promise<void> => {
if (forceReloading) {
return;
}

forceReloading = true;

// Create an AbortController to abort the fetch requests if they take too long.

const abortController = new AbortController();
const timeoutId = window.setTimeout(() => {
abortController.abort();
}, FORCE_RELOAD_TIMEOUT);

// Refresh the given URL in the HTTP cache.
// Note: we need to read the body since the HTTP cache is only updated once the body has been fully received.

const refresh = async (url: string): Promise<void> => {
const response = await fetch(url, { cache: 'reload', signal: abortController.signal });

await response.blob();
};

try {
// Retrieve the list of files used by the latest version of OpenCOR's Web app.

const response = await fetch(`./assets/version.json?t=${Date.now()}`, {
cache: 'no-store',
signal: abortController.signal
});
const versionInfo: IVersionInfo = response.ok ? await response.json() : { version: '' };

// Refresh the page and all the files.

await Promise.allSettled([
refresh(window.location.href),
...(versionInfo.files ?? []).map((file) => refresh(new URL(file, document.baseURI).href))
]);
} catch (_error: unknown) {
// Ignore any error (including a timeout) and reload the page anyway.
} finally {
window.clearTimeout(timeoutId);
}

window.location.reload();
};

// Export the version checking functions and state.

export { currentVersion, latestVersion, startCheck, updateAvailable };
export { currentVersion, forceReload, latestVersion, startCheck, updateAvailable };
27 changes: 19 additions & 8 deletions src/renderer/src/components/dialogs/UpdateAvailableDialog.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
<template>
<BaseDialog header=" " class="w-169">
<BaseDialog header=" " class="w-169"
:closeButtonProps="{ severity: 'secondary', text: true, rounded: true, disabled: reloading }"
:closeOnEscape="!reloading"
>
<div class="space-y-7">
<div class="text-center">
<div class="text-3xl font-bold">OpenCOR <span class="highlight highlight-version">{{ latestVersion }}</span> is available!</div>
</div>
<div class="space-y-2">
<div class="text-center">
<span class="text-lg">Feel free to <strong>force reload</strong> OpenCOR whenever you are ready.</span><br />
<em>(You can force reload by pressing
<span v-if="common.isMacOs()" class="highlight">⇧⌘R</span>
<span v-else><span class="highlight">Ctrl</span>+<span class="highlight">Shift</span>+<span class="highlight">R</span></span>.)</em>
<span class="text-lg">Feel free to reload OpenCOR whenever you are ready.</span><br />
<em >(Please note that any unsaved work will be lost.)</em>
</div>
</div>
</div>
<template #footer>
<Button label="OK" autofocus @click="$emit('close')" />
<Button :label="reloading?'Reloading...':'Reload'" autofocus :loading="reloading" @click="onReloadNow" />
<Button label="Cancel" severity="secondary" :disabled="reloading" @click="$emit('close')" />
</template>
</BaseDialog>
</template>

<script setup lang="ts">
import * as common from '../../common/common';
import { latestVersion } from '../../common/version';
import * as vue from 'vue';

import { forceReload, latestVersion } from '../../common/version';

defineEmits<{
close: [];
}>();

const reloading = vue.ref(false);

const onReloadNow = (): void => {
reloading.value = true;

void forceReload();
};
</script>

<style scoped>
Expand Down
Loading