feat: open mobile dashboard offline (#226)
CI / lint (pull_request) Successful in 19s
CI / build-frontend (pull_request) Successful in 6s

This commit is contained in:
timmy committed 2026-08-07 20:56:13 +00:00
1 parent 1c2dbd2168
commit 6bf5512b47
6 files changed
+252 -5

No files matched your search

+19
View File
@@ -55,6 +55,8 @@ textarea { resize: vertical; min-height: 120px; }
.small { font-size: 12px; color: #94a3b8; }
.status { display:inline-flex; gap:6px; align-items:center; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 8px #22c55e; }
.offline-status { position:relative; z-index:19; padding:10px 16px; border-bottom:1px solid #f59e0b; background:#35210b; color:#fde68a; font-size:13px; text-align:center; }
.offline-status[hidden] { display:none; }
.widget { border: 1px solid #1b2d45; border-radius: 12px; padding: 10px; background: linear-gradient(180deg,#0f1d33,#0b1526); }
.widget h3 { margin: 4px 0 8px; font-size: 13px; color: #7aa1c9; }
.event { padding: 8px 0; border-bottom: 1px solid #1b2d45; }
@@ -273,6 +275,9 @@ textarea { resize: vertical; min-height: 120px; }
<span class="small muted" id="clock"></span>
</div>
</header>
<div class="offline-status" id="offline-status" role="status" aria-live="polite" hidden>
Offline · live Gitea data is unavailable. Saved drafts remain available on this device.
</div>
<main>
<section class="panel my-work" id="my-work">
@@ -2989,6 +2994,20 @@ textarea { resize: vertical; min-height: 120px; }
});
function load() { return contextPoller.refresh(); }
const offlineStatus = qs('#offline-status');
function showOfflineStatus() {
offlineStatus.hidden = false;
setStatus('Offline');
}
function reconnectLiveData() {
offlineStatus.hidden = true;
setStatus('Reconnecting…');
contextPoller.refresh();
}
if (!navigator.onLine) showOfflineStatus();
window.addEventListener('offline', showOfflineStatus);
window.addEventListener('online', reconnectLiveData);
qs('#refresh').addEventListener('click', load);
qs('#start-work-session').addEventListener('click', () => {
if (!filterMyWork(lastMyWork, selectedWorkFilter).length) {
+31 -3
View File
@@ -1,9 +1,24 @@
const CACHE = 'stackchain-shell-v1';
const CACHE = 'stackchain-dashboard-shell-v2';
const BASE = new URL('./', self.location.href).pathname;
const SHELL = [
BASE,
BASE + 'manifest.webmanifest',
BASE + 'static/icons/stackchain-192.png',
BASE + 'static/icons/stackchain-512.png',
BASE + 'static/markdown.js',
BASE + 'static/commands.js',
BASE + 'static/search-preview.js',
BASE + 'static/widgets.js',
BASE + 'static/drafts.js',
BASE + 'static/my-work.js',
BASE + 'static/pick-work.js',
BASE + 'static/conversation.js',
BASE + 'static/issue-sheet.js',
BASE + 'static/create-issue-sheet.js',
BASE + 'static/pull-sheet.js',
BASE + 'static/review-sheet.js',
BASE + 'static/work-route.js',
BASE + 'static/context-poller.js',
];
self.addEventListener('install', event => {
@@ -12,7 +27,8 @@ self.addEventListener('install', event => {
self.addEventListener('activate', event => {
event.waitUntil(caches.keys().then(keys => Promise.all(
keys.filter(key => key !== CACHE).map(key => caches.delete(key))
keys.filter(key => key.startsWith('stackchain-dashboard-') && key !== CACHE)
.map(key => caches.delete(key))
)).then(() => self.clients.claim()));
});
@@ -20,8 +36,20 @@ self.addEventListener('fetch', event => {
const request = event.request;
if (request.method !== 'GET' || request.url.includes('/api/')) return;
const url = new URL(request.url);
if (url.origin !== self.location.origin || !url.pathname.startsWith(BASE)) return;
if (request.mode === 'navigate') {
event.respondWith(fetch(request));
event.respondWith(
fetch(request).then(async response => {
if (response.ok) {
const cache = await caches.open(CACHE);
await cache.put(BASE, response.clone());
}
return response;
}).catch(async () => {
const cache = await caches.open(CACHE);
return cache.match(BASE);
})
);
return;
}
if (url.origin === self.location.origin && SHELL.includes(url.pathname)) {