diff --git a/app.js b/app.js index 5a028f6..6ff872f 100644 --- a/app.js +++ b/app.js @@ -209,6 +209,92 @@ document.getElementById('debug-toggle').addEventListener('click', () => { } }); +// === SOVEREIGNTY SCORE === +/** + * Computes a sovereignty score (0–100) based on how local-first the current + * environment is. Factors: local host, WS on localhost, service worker, and + * persistent local storage. + */ +async function computeSovereigntyScore() { + const factors = []; + + // Factor 1 — local hosting (40 pts) + const isLocalHost = ['localhost', '127.0.0.1', '::1'].includes(location.hostname); + factors.push({ + label: 'LOCAL HOST', + pts: isLocalHost ? 40 : 0, + max: 40, + status: isLocalHost ? 'on' : 'off', + }); + + // Factor 2 — WebSocket on local server (30 pts) + const wsHostLocal = typeof wsClient !== 'undefined' && wsClient.url + ? ['localhost', '127.0.0.1'].some(h => wsClient.url.includes(h)) + : isLocalHost; // fall back to page host + factors.push({ + label: 'LOCAL WS', + pts: wsHostLocal ? 30 : 0, + max: 30, + status: wsHostLocal ? 'on' : 'off', + }); + + // Factor 3 — Service worker / offline capable (20 pts) + let hasSW = false; + if ('serviceWorker' in navigator) { + try { + const reg = await navigator.serviceWorker.getRegistration(); + hasSW = !!reg; + } catch (_) { /* ignore */ } + } + factors.push({ + label: 'OFFLINE SW', + pts: hasSW ? 20 : 0, + max: 20, + status: hasSW ? 'on' : 'warn', + }); + + // Factor 4 — Local storage accessible (10 pts) + let hasStorage = false; + try { + localStorage.setItem('_sov_check', '1'); + localStorage.removeItem('_sov_check'); + hasStorage = true; + } catch (_) { /* blocked */ } + factors.push({ + label: 'LOCAL STORE', + pts: hasStorage ? 10 : 0, + max: 10, + status: hasStorage ? 'on' : 'off', + }); + + const total = factors.reduce((sum, f) => sum + f.pts, 0); + return { total, factors }; +} + +function renderSovereigntyScore({ total, factors }) { + const scoreEl = document.getElementById('sov-score-value'); + const barEl = document.getElementById('sov-bar-fill'); + const listEl = document.getElementById('sov-factors'); + + scoreEl.textContent = total; + barEl.style.width = `${total}%`; + + barEl.className = 'sov-bar-fill'; + if (total < 40) barEl.classList.add('sov-low'); + else if (total < 70) barEl.classList.add('sov-mid'); + else barEl.classList.add('sov-high'); + + listEl.innerHTML = ''; + for (const f of factors) { + const li = document.createElement('li'); + if (f.pts > 0) li.classList.add('active'); + li.innerHTML = `${f.label}`; + listEl.appendChild(li); + } +} + +computeSovereigntyScore().then(renderSovereigntyScore); + // === WEBSOCKET CLIENT === import { wsClient } from './ws-client.js'; diff --git a/index.html b/index.html index 26344f3..009cddc 100644 --- a/index.html +++ b/index.html @@ -41,6 +41,14 @@ [Tab] to exit + +