// App root β€” sticky header (date + tab bar), data fetching, push registration. const { api, buildSeries, TOKEN } = window.HC_DATA; const SeriesContext = window.SeriesContext; function PushBell({ theme }) { const p = theme.palette; const [state, setState] = React.useState('checking'); const [err, setErr] = React.useState(null); React.useEffect(() => { pushStatus().then(setState).catch(() => setState('off')); }, []); async function tap() { if (state === 'on' || state === 'blocked' || state === 'unsupported' || state === 'enabling') return; setErr(null); setState('enabling'); try { await enablePush(); setState('on'); } catch (e) { setErr(e.message || 'failed'); setState(Notification && Notification.permission === 'denied' ? 'blocked' : 'off'); } } // Don't render if push isn't supported on this device at all. if (state === 'unsupported') return null; const label = state === 'on' ? 'on' : state === 'enabling' ? '…' : state === 'blocked' ? 'blocked' : 'enable push'; const color = state === 'on' ? p.training : state === 'blocked' ? p.warning : p.accent; return ( ); } function TabBar({ active, onChange, theme }) { const p = theme.palette; const tabs = [['today', 'today'], ['log', 'log'], ['trends', 'trends'], ['ask', 'ask']]; return (
{tabs.map(([k, label]) => ( ))}
); } function fmtHeaderDate(iso, dayName, dayType) { if (!iso) return { left: 'β€”', right: '' }; const dt = new Date(iso + 'T00:00:00'); const months = ['January','February','March','April','May','June', 'July','August','September','October','November','December']; const left = `${dayName || ''} ${months[dt.getMonth()]} ${dt.getDate()}`.trim(); const rightMap = { wfh: 'wfh day', office: 'office day', rest: 'rest day', saturday: 'saturday', sunday: 'sunday', }; const right = rightMap[dayType] || ''; return { left, right }; } // ── Push registration ────────────────────────────────────────────── // iOS Safari requires a user gesture to trigger Notification.requestPermission(). // Calling it on app load silently fails. We register the service worker on // load so it's installed, but defer subscription until the user taps a button. async function ensureServiceWorker() { if (!('serviceWorker' in navigator)) return null; try { return await navigator.serviceWorker.register('/sw.js'); } catch (e) { console.warn('sw register failed:', e); return null; } } async function enablePush() { const reg = await ensureServiceWorker(); if (!reg) throw new Error('service worker unsupported'); if (!('PushManager' in window)) throw new Error('push unsupported'); if (!('Notification' in window)) throw new Error('notifications unsupported'); if (Notification.permission === 'default') { const perm = await Notification.requestPermission(); if (perm !== 'granted') throw new Error('permission denied'); } else if (Notification.permission === 'denied') { throw new Error('permission denied'); } const keyRes = await api('/api/vapid-public-key'); if (!keyRes || !keyRes.key) throw new Error('no vapid key'); let sub = await reg.pushManager.getSubscription(); if (!sub) { sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(keyRes.key), }); } await api('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub.toJSON()) }); return sub; } async function pushStatus() { if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) { return 'unsupported'; } if (Notification.permission === 'denied') return 'blocked'; if (Notification.permission !== 'granted') return 'off'; try { const reg = await navigator.serviceWorker.getRegistration('/sw.js'); if (!reg) return 'off'; const sub = await reg.pushManager.getSubscription(); return sub ? 'on' : 'off'; } catch (e) { return 'off'; } } function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = atob(base64); return Uint8Array.from([...raw].map((c) => c.charCodeAt(0))); } // ── App ──────────────────────────────────────────────────────────── function MissingToken({ theme }) { const p = theme.palette; const [input, setInput] = React.useState(''); function save() { let t = input.trim(); if (!t) return; // Accept full URL or just the token string. try { const u = new URL(t); const tok = u.searchParams.get('token'); if (tok) t = tok; } catch (e) { /* not a URL, use as-is */ } window.HC_DATA.saveToken(t); } return (
Token needed
Paste your install URL or the token below. Saved to this device only.