Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
113c69e
feat(ui): Claude-design tokens — quieter dark, single rust accent
claude May 17, 2026
91183fd
feat(engine): beast-mode foundations — worker pool, firewall worker, …
claude May 17, 2026
6aa03ba
fix(engine): propagate active rules to worker; IDB versionchange hand…
claude May 17, 2026
0a219e3
refactor(ui): inline-hex sweep — every brand color now flows through …
claude May 17, 2026
0153355
feat(shell): Claude-design AppShell with 6 workspaces (behind ?shell=…
claude May 17, 2026
6467ec9
feat(shell): flip default to AppShell; retire duplicate discoverabili…
claude May 17, 2026
0494571
feat(engine): transformers.js MiniLM into a worker; embeddings cache …
claude May 17, 2026
548e4dd
feat(build): per-workspace bundle splits + lazy legacy shell
claude May 17, 2026
f4eb196
feat(pwa): offline-first sw + background sync + multi-tab + atomic sn…
claude May 17, 2026
2b71327
feat(engine): runtime capability probe (Beast #12, safe variant)
claude May 17, 2026
acd17e7
refactor(ui): severity color tables → CSS vars (Shell #2 follow-up)
claude May 17, 2026
4f4b8cf
refactor: retire legacy App.jsx; finalize per-panel code splits
claude May 17, 2026
53a111f
feat(offline): wire fetchOrQueue + offline indicator into Sync + Rooms
claude May 17, 2026
39d03fb
feat(pwa): File System Access API for Portability export
claude May 17, 2026
e2cd2ba
feat(multitab): SnapshotPanel listens to snapshot:changed broadcast
claude May 17, 2026
659536e
feat(multitab): extend Web Locks coverage to archive + dictionary + f…
claude May 17, 2026
5b68e9e
feat(multitab): theme changes broadcast cross-tab via multiTab channel
claude May 17, 2026
06f377c
feat(shell): wire Composer → workspace routing + Firewall auto-scan
claude May 17, 2026
15a38a0
feat(shell): finish Composer wiring for autopsy / rag / diff modes
claude May 17, 2026
cfefe16
feat(a11y): tabs are proper tablist, focus moves on workspace change
claude May 17, 2026
aa101d6
fix(shell): route deep-link params to correct workspace; tighten topb…
claude May 17, 2026
fab1538
feat(shell): workspace keep-alive — preserve panel state across tab s…
claude May 18, 2026
65a831a
feat(engine): Brain Evolve loop runs in a dedicated worker
claude May 18, 2026
f045631
feat(engine): Attack Evolve loop into a worker; share dispatcher with…
claude May 18, 2026
79376c9
fix(build): inline `new URL` for worker construction so Vite bundles …
claude May 18, 2026
31e4f98
feat(pwa): service-worker auto-update detection + reload prompt
claude May 18, 2026
05eba01
fix(multitab): same-tab fan-out + sticky bus + stale-read subscriptions
claude May 18, 2026
c39314c
feat(engine): runRedTeam + adversarial training into the firewall worker
claude May 19, 2026
b0bd74f
perf(shell): React.memo Topbar / Composer / WorkspaceTabs
claude May 19, 2026
efd070e
feat(engine): CodeBrain search workloads into a dedicated worker
claude May 19, 2026
8af8bf4
chore(perf): drop FeedbackPanel polling — live subscription handles it
claude May 19, 2026
bb71485
fix(pwa): gate controllerchange before showing 'new version' banner
claude May 19, 2026
d369e09
fix: 3 Codex review issues — palette workspace mount, onboarding retr…
claude May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions brainsnn-r3f-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
<meta name="twitter:image" content="https://brainsnn.com/api/og?title=BrainSNN&subtitle=Emotional+Payload+Intelligence" />

<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0b1224" />
<meta name="theme-color" content="#1a1815" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="BrainSNN" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

<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@400;500;600;700;800&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
Expand Down
193 changes: 154 additions & 39 deletions brainsnn-r3f-app/public/sw.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,192 @@
/**
* Layer 91 — BrainSNN Service Worker
* Layer 91 — BrainSNN Service Worker (Beast #11: offline-first + sync queue)
*
* Minimal offline shell: cache the app shell on install, network-first
* for API routes, cache-first for hashed asset files.
*
* Versioned cache key — bumping CACHE_VERSION on release invalidates
* old shells without needing a separate cache-busting scheme.
* Strategy:
* - SHELL precache on install (root + index.html + manifest)
* - Hashed /assets/* cache-first with stale-while-revalidate update
* - Workspace chunks fetch + cache lazily on first hit so every
* workspace works offline after one visit
* - API + dynamic share routes network-first, but POST/PUT/DELETE
* get enqueued via Background Sync when offline
* - SPA navigation falls back to cached index.html
* - Skip-waiting + clients.claim so updates take effect on next nav
*/

const CACHE_VERSION = 'brainsnn-v1';
const SHELL_URLS = ['/', '/index.html'];
const CACHE_VERSION = 'brainsnn-v2';
const PRECACHE_URLS = ['/', '/index.html', '/manifest.webmanifest'];
const QUEUE_TAG = 'brainsnn-write-queue';
const QUEUE_DB = 'brainsnn-sw';
const QUEUE_STORE = 'queue';

// ---------- precache ----------
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_VERSION).then((cache) => cache.addAll(SHELL_URLS)).catch(() => {}),
caches.open(CACHE_VERSION)
.then((c) => c.addAll(PRECACHE_URLS))
.catch(() => { /* offline first-install — fine */ })
.then(() => self.skipWaiting())
);
self.skipWaiting();
});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))),
).then(() => self.clients.claim()),
caches.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))))
.then(() => self.clients.claim())
);
});

// ---------- helpers ----------
function isApi(url) {
return url.pathname.startsWith('/api/') || url.pathname.startsWith('/r/') ||
url.pathname.startsWith('/i/') || url.pathname.startsWith('/q/') ||
url.pathname.startsWith('/a/') || url.pathname.startsWith('/d/') ||
url.pathname.startsWith('/x/') || url.pathname.startsWith('/t/') ||
url.pathname.startsWith('/n/') || url.pathname.startsWith('/v/') ||
url.pathname.startsWith('/w/') || url.pathname.startsWith('/b/');
return url.pathname.startsWith('/api/') ||
/^\/[rqidaxntvwb]\//.test(url.pathname);
}

function isAsset(url) {
return url.pathname.startsWith('/assets/');
}

async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_VERSION);
const hit = await cache.match(request);
const fetchPromise = fetch(request).then((resp) => {
if (resp && resp.status === 200) cache.put(request, resp.clone()).catch(() => {});
return resp;
}).catch(() => hit);
return hit || fetchPromise;
}

// ---------- IDB for background sync queue ----------
function openQueueDb() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(QUEUE_DB, 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(QUEUE_STORE)) {
db.createObjectStore(QUEUE_STORE, { keyPath: 'id', autoIncrement: true });
}
};
req.onsuccess = () => {
req.result.onversionchange = () => req.result.close();
resolve(req.result);
};
req.onerror = () => reject(req.error);
});
}

async function enqueueRequest(request) {
try {
const db = await openQueueDb();
const body = await request.clone().text();
const entry = {
url: request.url,
method: request.method,
headers: Array.from(request.headers.entries()),
body,
ts: Date.now()
};
await new Promise((resolve, reject) => {
const tx = db.transaction(QUEUE_STORE, 'readwrite');
const req = tx.objectStore(QUEUE_STORE).add(entry);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
});
if ('sync' in self.registration) {
try { await self.registration.sync.register(QUEUE_TAG); } catch { /* noop */ }
}
} catch { /* swallow — degrade gracefully */ }
}

async function replayQueue() {
const db = await openQueueDb();
const entries = await new Promise((resolve, reject) => {
const tx = db.transaction(QUEUE_STORE, 'readonly');
const req = tx.objectStore(QUEUE_STORE).getAll();
req.onsuccess = () => resolve(req.result || []);
req.onerror = () => reject(req.error);
});

for (const entry of entries) {
try {
const headers = new Headers(entry.headers);
const resp = await fetch(entry.url, { method: entry.method, headers, body: entry.body });
if (resp.ok) {
await new Promise((resolve, reject) => {
const tx = db.transaction(QUEUE_STORE, 'readwrite');
const req = tx.objectStore(QUEUE_STORE).delete(entry.id);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
});
}
} catch {
// Still offline / server unreachable — keep entry for next sync.
break;
}
}
}

self.addEventListener('sync', (event) => {
if (event.tag === QUEUE_TAG) event.waitUntil(replayQueue());
});

// ---------- fetch routing ----------
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
const { request } = event;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;

// API / dynamic routes — network first, no cache (we want fresh
// scores and OG images).
if (isApi(url)) {
event.respondWith(fetch(event.request).catch(() => new Response('offline', { status: 503 })));
// Non-GET to known API routes → queue if offline.
if (request.method !== 'GET' && isApi(url)) {
event.respondWith(
fetch(request.clone()).catch(async () => {
await enqueueRequest(request);
return new Response(JSON.stringify({ queued: true }), {
status: 202,
headers: { 'Content-Type': 'application/json' }
});
})
);
return;
}

// Hashed assets → cache first
if (url.pathname.startsWith('/assets/')) {
if (request.method !== 'GET') return;

// API reads — network first; cache the last response so /a/<hash>
// share cards still render offline.
if (isApi(url)) {
event.respondWith(
caches.match(event.request).then((hit) => {
if (hit) return hit;
return fetch(event.request).then((resp) => {
if (!resp || resp.status !== 200) return resp;
fetch(request).then((resp) => {
if (resp && resp.status === 200) {
const clone = resp.clone();
caches.open(CACHE_VERSION).then((c) => c.put(event.request, clone)).catch(() => {});
return resp;
});
}),
caches.open(CACHE_VERSION).then((c) => c.put(request, clone)).catch(() => {});
}
return resp;
}).catch(() => caches.match(request).then((hit) => hit || new Response('offline', { status: 503 })))
);
return;
}

// SPA shell — network first, fall back to cached index.html for
// offline navigations.
// Hashed assets → stale-while-revalidate so updated chunks land
// on the next reload without a hard refresh.
if (isAsset(url)) {
event.respondWith(staleWhileRevalidate(request));
return;
}

// SPA navigation — network first, cached index.html offline fallback.
event.respondWith(
fetch(event.request).then((resp) => {
fetch(request).then((resp) => {
if (resp && resp.status === 200 && url.pathname === '/') {
const clone = resp.clone();
caches.open(CACHE_VERSION).then((c) => c.put('/', clone)).catch(() => {});
}
return resp;
}).catch(() => caches.match('/index.html').then((hit) => hit || new Response('offline', { status: 503 }))),
}).catch(() => caches.match('/index.html').then((hit) => hit || new Response('offline', { status: 503 })))
);
});

// ---------- messaging (main thread → SW) ----------
self.addEventListener('message', (event) => {
if (event.data?.type === 'skipWaiting') self.skipWaiting();
if (event.data?.type === 'replayNow') event.waitUntil(replayQueue());
});
Loading