feat: PWA support — manifest, service worker, icons, public auth exemption
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1549,10 +1549,14 @@
|
||||
// ── Theme toggle ──────────────────────────────────────────────
|
||||
const themeBtn = document.getElementById('theme-btn');
|
||||
|
||||
const _metaThemeColor = document.getElementById('meta-theme-color');
|
||||
const _themeColors = { dark: '#1a1228', light: '#f2eef9' };
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
themeBtn.textContent = theme === 'dark' ? '☀' : '☾';
|
||||
themeBtn.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
if (_metaThemeColor) _metaThemeColor.content = _themeColors[theme] || _themeColors.dark;
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1729,3 +1733,8 @@
|
||||
const stored = get_stored_session();
|
||||
if (stored) resumeSession(stored, true).catch(clear_stored_session);
|
||||
}
|
||||
|
||||
// ── Service worker registration ───────────────────────────────
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||
}
|
||||
|
||||
BIN
cortex/static/icon-192.png
Normal file
BIN
cortex/static/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
cortex/static/icon-512.png
Normal file
BIN
cortex/static/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
4
cortex/static/icon.svg
Normal file
4
cortex/static/icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="96" fill="#1a1228"/>
|
||||
<text x="256" y="390" font-size="340" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif">✨</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 251 B |
@@ -5,6 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cortex — Inara</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#1a1228" id="meta-theme-color">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Cortex">
|
||||
<link rel="apple-touch-icon" href="/static/icon-192.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||
|
||||
30
cortex/static/manifest.json
Normal file
30
cortex/static/manifest.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "Cortex · Inara",
|
||||
"short_name": "Cortex",
|
||||
"description": "Personal AI assistant",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#1a1228",
|
||||
"theme_color": "#1a1228",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
75
cortex/static/sw.js
Normal file
75
cortex/static/sw.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const CACHE = 'cortex-v1';
|
||||
|
||||
const PRECACHE = [
|
||||
'/static/style.css',
|
||||
'/static/app.js',
|
||||
'/static/marked.min.js',
|
||||
'/static/icon-192.png',
|
||||
'/static/icon-512.png',
|
||||
'/static/icon.svg',
|
||||
'/static/manifest.json',
|
||||
];
|
||||
|
||||
self.addEventListener('install', evt => {
|
||||
evt.waitUntil(
|
||||
caches.open(CACHE)
|
||||
.then(c => c.addAll(PRECACHE))
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', evt => {
|
||||
evt.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys.filter(k => k !== CACHE).map(k => caches.delete(k))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', evt => {
|
||||
const url = new URL(evt.request.url);
|
||||
|
||||
// Only handle same-origin GETs
|
||||
if (evt.request.method !== 'GET' || url.origin !== self.location.origin) return;
|
||||
|
||||
// Never intercept streaming or API calls
|
||||
if (
|
||||
url.pathname.startsWith('/chat') ||
|
||||
url.pathname.startsWith('/orchestrate') ||
|
||||
url.pathname.startsWith('/api/') ||
|
||||
url.pathname.startsWith('/distill') ||
|
||||
url.pathname.startsWith('/webhook') ||
|
||||
url.pathname.startsWith('/auth/')
|
||||
) return;
|
||||
|
||||
// Static assets — cache first, refresh in background (stale-while-revalidate)
|
||||
if (url.pathname.startsWith('/static/')) {
|
||||
evt.respondWith(
|
||||
caches.open(CACHE).then(cache =>
|
||||
cache.match(evt.request).then(cached => {
|
||||
const network = fetch(evt.request).then(resp => {
|
||||
if (resp.ok) cache.put(evt.request, resp.clone());
|
||||
return resp;
|
||||
});
|
||||
return cached || network;
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML pages — network first, cached shell fallback
|
||||
evt.respondWith(
|
||||
fetch(evt.request)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
const clone = resp.clone();
|
||||
caches.open(CACHE).then(c => c.put(evt.request, clone));
|
||||
}
|
||||
return resp;
|
||||
})
|
||||
.catch(() => caches.match(evt.request))
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user