ef22e92ac8
Command palette (cmd+K) with fuzzy nav, ticket search, people lookup and action entries (new ticket, logout, show shortcuts). Opens from keyboard or user dropdown. Global keyboard shortcuts via a small useShortcut/useLeaderShortcut hook: `?` help overlay, `c` new ticket, `g d|t|m|n|s` leader nav. Tickets list: j/k cursor, Enter open, x toggle select. TicketDetail: `e` edit, `r` focus comment composer. All guarded against firing inside text fields. @mention autocomplete in the comment composer (MentionTextarea) with arrow-key nav and Tab/Enter insert. Rendered comments and audit log rewrite @username tokens to links pointing at that user's assignee filter; unknown usernames left as plain text. Mobile sweep: TicketDetail sidebar stacks below content on <md, Settings profile grid collapses to one column, admin tables get horizontal scroll with a 640px min width, CTI 3-column grid stacks vertically on <md, New ticket severity/assignee grid same. PWA: manifest.webmanifest, SVG icon, minimal network-first service worker for the app shell (never caches /api/*), registered in production builds only. Theme-color meta + manifest link in index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
59 lines
1.6 KiB
JavaScript
59 lines
1.6 KiB
JavaScript
// Minimal service worker: offline shell only. No data caching.
|
|
const CACHE = 'ticketing-shell-v1';
|
|
const SHELL = ['/', '/icon.svg', '/manifest.webmanifest'];
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE).then((cache) => cache.addAll(SHELL)),
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
|
|
),
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request;
|
|
if (req.method !== 'GET') return;
|
|
|
|
const url = new URL(req.url);
|
|
// Never cache API calls — they must always hit the server
|
|
if (url.pathname.startsWith('/api/')) return;
|
|
|
|
// Network-first for navigation, fall back to cached shell
|
|
if (req.mode === 'navigate') {
|
|
event.respondWith(
|
|
fetch(req)
|
|
.then((res) => {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then((c) => c.put('/', clone));
|
|
return res;
|
|
})
|
|
.catch(() => caches.match('/')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Cache-first for static assets
|
|
if (url.origin === self.location.origin) {
|
|
event.respondWith(
|
|
caches.match(req).then((cached) => {
|
|
if (cached) return cached;
|
|
return fetch(req).then((res) => {
|
|
if (res.ok && res.type === 'basic') {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then((c) => c.put(req, clone));
|
|
}
|
|
return res;
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
});
|