Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

pwa.tsBlame134 lines · 1 contributor
eae38d1Claude1/**
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
17import { Hono } from "hono";
18
19const pwa = new Hono();
20
21export 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
41pwa.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
47const 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
55pwa.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 */
c475ee6Claude67export const SERVICE_WORKER_SRC = `// gluecron service worker — v3 (Visual Impact pass)
68const CACHE = 'gluecron-shell-v3';
eae38d1Claude69const SHELL = ['/', '/manifest.webmanifest', '/icon.svg'];
70
71self.addEventListener('install', (e) => {
72 e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).catch(() => {}));
73 self.skipWaiting();
74});
75
76self.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
85self.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
114pwa.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 */
126export const PWA_REGISTER_SNIPPET = `
127if ('serviceWorker' in navigator) {
128 window.addEventListener('load', function() {
129 navigator.serviceWorker.register('/sw.js').catch(function() {});
130 });
131}
132`.trim();
133
134export default pwa;