mirror of
https://github.com/PRVTPRO/Amnezia-Web-Panel.git
synced 2026-09-18 01:44:10 +02:00
ed08fc0f70
tests/test_server_template_integrity.py calls `node --check` through
subprocess.run() unconditionally. Without node on PATH that raises
FileNotFoundError and fails the suite rather than skipping - verified by
running it with PATH=/usr/bin:/bin. It now carries the same
`skipUnless(shutil.which('node'))` guard the other template-JS check uses.
static/sw.js precached three URLs nobody requests. Both fetch branches match
on the exact URL, and the templates ask for /static/favicon.svg,
/static/js/qrcode.min.js and /static/js/searchable-select.js with no query -
so the `?v=${VERSION}` entries never served a request, and the favicon ended
up stored twice (cache-first misses the versioned entry, fetches the plain
URL, caches that too). style.css keeps its query: the templates request it as
?v={{ static_v }} and the worker is registered with the same value, so that
one does match. Freshness is unaffected either way, since CACHE is named after
VERSION and a new version starts from an empty cache.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
3.3 KiB
JavaScript
100 lines
3.3 KiB
JavaScript
/* Amnezia Web Panel — static-assets-only service worker.
|
|
*
|
|
* Cache version comes from the script URL (?v=...), which base.html sets to
|
|
* static_v (newest mtime under static/). Asset changes → new SW URL → fresh
|
|
* install → activate evicts every other awp-static-* cache.
|
|
*
|
|
* CSS/JS are network-first so a new ?v= is never served from an old cache
|
|
* entry. Match is always the exact request URL, including the query string.
|
|
*
|
|
* Pages and /api/* are never intercepted: session auth has no CSRF and a
|
|
* shared device must not serve one user's cached HTML to another.
|
|
*/
|
|
const VERSION = new URL(self.location).searchParams.get('v') || 'dev';
|
|
const CACHE = `awp-static-${VERSION}`;
|
|
|
|
// Every entry has to match the URL the templates actually request, because
|
|
// both fetch branches below match on the exact URL. style.css is requested as
|
|
// ?v={{ static_v }} and the worker itself is registered with the same value,
|
|
// so that one keeps the query; the favicon and the two vendored scripts are
|
|
// requested without it. Freshness still comes from CACHE being named after
|
|
// VERSION - a new version starts an empty cache.
|
|
const PRECACHE = [
|
|
`/static/css/style.css?v=${VERSION}`,
|
|
`/static/favicon.svg`,
|
|
`/static/js/qrcode.min.js`,
|
|
`/static/js/searchable-select.js`,
|
|
`/static/icons/icon-192.png`,
|
|
`/static/icons/icon-512.png`,
|
|
`/static/icons/maskable-192.png`,
|
|
`/static/icons/maskable-512.png`,
|
|
`/static/icons/apple-touch-icon-180.png`,
|
|
`/static/offline.html`,
|
|
];
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(keys.filter((k) => k.startsWith('awp-static-') && k !== CACHE).map((k) => caches.delete(k)))
|
|
).then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request;
|
|
if (req.method !== 'GET') return;
|
|
|
|
const url = new URL(req.url);
|
|
|
|
// Never touch API or non-same-origin requests.
|
|
if (url.origin !== self.location.origin) return;
|
|
if (url.pathname.startsWith('/api/')) return;
|
|
|
|
if (url.pathname.startsWith('/static/')) {
|
|
const isCssOrJs = url.pathname.endsWith('.css') || url.pathname.endsWith('.js');
|
|
|
|
if (isCssOrJs) {
|
|
// Network-first, exact URL match (honor ?v=).
|
|
event.respondWith(
|
|
fetch(req).then((res) => {
|
|
if (res.ok) {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then((cache) => cache.put(req, clone));
|
|
}
|
|
return res;
|
|
}).catch(() => caches.match(req))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Icons / offline.html: cache-first, exact URL match.
|
|
event.respondWith(
|
|
caches.match(req).then((hit) => {
|
|
if (hit) return hit;
|
|
return fetch(req).then((res) => {
|
|
if (res.ok) {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then((cache) => cache.put(req, clone));
|
|
}
|
|
return res;
|
|
});
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Navigations: network first, offline.html on failure. Do not cache pages.
|
|
if (req.mode === 'navigate') {
|
|
event.respondWith(
|
|
fetch(req).catch(() => caches.match('/static/offline.html'))
|
|
);
|
|
}
|
|
// Everything else: no respondWith — browser default.
|
|
});
|