CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
pwa.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| eae38d1 | 1 | /** |
| 2 | * Block G1 — PWA (progressive web app) support. | |
| 3 | * | |
| 4 | * GET /manifest.webmanifest — app manifest (install prompt) | |
| 5 | * GET /sw.js — service worker (cache-first for static, network-first for HTML) | |
| 6 | * GET /icon.svg — monochrome logo used by the manifest | |
| 7 | * | |
| 8 | * The service worker deliberately keeps the cache small (static CSS-in-JS is | |
| 9 | * inlined so there's nothing to cache beyond the manifest + icon). HTML pages | |
| 10 | * fall through to the network; cached copies only serve offline fallback. | |
| 11 | * | |
| 12 | * Adding `<link rel="manifest" href="/manifest.webmanifest">` + a tiny SW | |
| 13 | * registration snippet to `Layout` turns any repo page into an installable | |
| 14 | * PWA on Chrome/Safari. | |
| 15 | */ | |
| 16 | ||
| 17 | import { Hono } from "hono"; | |
| 18 | ||
| 19 | const pwa = new Hono(); | |
| 20 | ||
| 21 | export const MANIFEST = { | |
| 22 | name: "Gluecron", | |
| 23 | short_name: "Gluecron", | |
| 24 | description: "AI-native code intelligence + git hosting", | |
| 25 | start_url: "/", | |
| 26 | scope: "/", | |
| 27 | display: "standalone", | |
| 28 | background_color: "#0d1117", | |
| 29 | theme_color: "#0d1117", | |
| 30 | icons: [ | |
| 31 | { | |
| 32 | src: "/icon.svg", | |
| 33 | sizes: "any", | |
| 34 | type: "image/svg+xml", | |
| 35 | purpose: "any maskable", | |
| 36 | }, | |
| 37 | ], | |
| 38 | categories: ["developer", "productivity"], | |
| 39 | } as const; | |
| 40 | ||
| 41 | pwa.get("/manifest.webmanifest", (c) => { | |
| 42 | c.header("content-type", "application/manifest+json"); | |
| 43 | c.header("cache-control", "public, max-age=3600"); | |
| 44 | return c.body(JSON.stringify(MANIFEST)); | |
| 45 | }); | |
| 46 | ||
| 47 | const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"> | |
| 48 | <rect width="128" height="128" rx="24" fill="#0d1117"/> | |
| 49 | <g fill="#58a6ff" font-family="monospace" font-size="58" font-weight="700" text-anchor="middle"> | |
| 50 | <text x="64" y="82">gc</text> | |
| 51 | </g> | |
| 52 | <circle cx="28" cy="28" r="5" fill="#3fb950"/> | |
| 53 | </svg>`; | |
| 54 | ||
| 55 | pwa.get("/icon.svg", (c) => { | |
| 56 | c.header("content-type", "image/svg+xml"); | |
| 57 | c.header("cache-control", "public, max-age=86400, immutable"); | |
| 58 | return c.body(ICON_SVG); | |
| 59 | }); | |
| 60 | ||
| 61 | /** | |
| 62 | * Bare-bones service worker. Offline behaviour: | |
| 63 | * - HTML → network first, cached response on failure, fallback offline page | |
| 64 | * - other → pass-through (the static CSS is inlined into the HTML, so there's | |
| 65 | * no cross-request asset worth caching for v1) | |
| 66 | */ | |
| 67 | export const SERVICE_WORKER_SRC = `// gluecron service worker — v1 | |
| 68 | const CACHE = 'gluecron-shell-v1'; | |
| 69 | const SHELL = ['/', '/manifest.webmanifest', '/icon.svg']; | |
| 70 | ||
| 71 | self.addEventListener('install', (e) => { | |
| 72 | e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).catch(() => {})); | |
| 73 | self.skipWaiting(); | |
| 74 | }); | |
| 75 | ||
| 76 | self.addEventListener('activate', (e) => { | |
| 77 | e.waitUntil( | |
| 78 | caches.keys().then((keys) => | |
| 79 | Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))) | |
| 80 | ) | |
| 81 | ); | |
| 82 | self.clients.claim(); | |
| 83 | }); | |
| 84 | ||
| 85 | self.addEventListener('fetch', (event) => { | |
| 86 | const req = event.request; | |
| 87 | if (req.method !== 'GET') return; | |
| 88 | const url = new URL(req.url); | |
| 89 | // Never intercept git, API, or auth endpoints — they must stay fresh. | |
| 90 | if ( | |
| 91 | url.pathname.includes('.git/') || | |
| 92 | url.pathname.startsWith('/api/') || | |
| 93 | url.pathname.startsWith('/login') || | |
| 94 | url.pathname.startsWith('/logout') || | |
| 95 | url.pathname.startsWith('/register') | |
| 96 | ) { | |
| 97 | return; | |
| 98 | } | |
| 99 | const wantsHtml = req.headers.get('accept')?.includes('text/html'); | |
| 100 | if (wantsHtml) { | |
| 101 | event.respondWith( | |
| 102 | fetch(req) | |
| 103 | .then((res) => { | |
| 104 | const copy = res.clone(); | |
| 105 | caches.open(CACHE).then((c) => c.put(req, copy)).catch(() => {}); | |
| 106 | return res; | |
| 107 | }) | |
| 108 | .catch(() => caches.match(req).then((hit) => hit || caches.match('/'))) | |
| 109 | ); | |
| 110 | } | |
| 111 | }); | |
| 112 | `; | |
| 113 | ||
| 114 | pwa.get("/sw.js", (c) => { | |
| 115 | c.header("content-type", "application/javascript"); | |
| 116 | c.header("cache-control", "public, max-age=60"); | |
| 117 | // Service-Worker-Allowed required for root-scope SW served from root | |
| 118 | c.header("service-worker-allowed", "/"); | |
| 119 | return c.body(SERVICE_WORKER_SRC); | |
| 120 | }); | |
| 121 | ||
| 122 | /** | |
| 123 | * Inline script registering the SW. Loaded once at the bottom of every page. | |
| 124 | * Kept tiny so we don't bloat TTI. | |
| 125 | */ | |
| 126 | export const PWA_REGISTER_SNIPPET = ` | |
| 127 | if ('serviceWorker' in navigator) { | |
| 128 | window.addEventListener('load', function() { | |
| 129 | navigator.serviceWorker.register('/sw.js').catch(function() {}); | |
| 130 | }); | |
| 131 | } | |
| 132 | `.trim(); | |
| 133 | ||
| 134 | export default pwa; |