/* MeshBay Web Client — Phase 4 * Communicates with: Hub API (auth, groups) + Node HTTP API (files, streaming) * Requires: hub at same origin, node at configured URL */ const HUB = ''; // same origin — hub serves this file // ── State ───────────────────────────────────────────────────────────────────── let state = { token: localStorage.getItem('mb_token') || null, refreshToken: localStorage.getItem('mb_rt') || null, username: localStorage.getItem('mb_user') || null, nodeUrl: localStorage.getItem('mb_node') || null, }; // ── Hub API helpers ─────────────────────────────────────────────────────────── async function hubGet(path) { const headers = state.token ? { Authorization: `Bearer ${state.token}` } : {}; const r = await fetch(HUB + path, { headers }); if (!r.ok) throw new Error(`${r.status} ${await r.text()}`); return r.json(); } async function hubPost(path, body) { const headers = { 'Content-Type': 'application/json', ...(state.token ? { Authorization: `Bearer ${state.token}` } : {}), }; const r = await fetch(HUB + path, { method: 'POST', headers, body: JSON.stringify(body) }); if (!r.ok) throw new Error(`${r.status} ${await r.text()}`); return r.json(); } // ── Auth ────────────────────────────────────────────────────────────────────── async function login(username, password) { const data = await hubPost('/v1/users/login', { username, password }); state.token = data.access_token; state.refreshToken = data.refresh_token; state.username = username; localStorage.setItem('mb_token', state.token); localStorage.setItem('mb_rt', state.refreshToken); localStorage.setItem('mb_user', username); return data; } async function register(username, email, password, pkEd, pkX) { return hubPost('/v1/users/register', { username, email, password, pk_user_ed25519: pkEd, pk_user_x25519: pkX, }); } function logout() { state = { token: null, refreshToken: null, username: null, nodeUrl: null }; localStorage.clear(); render(); } // ── Node API helpers ────────────────────────────────────────────────────────── async function nodeGet(path) { if (!state.nodeUrl) throw new Error('No node configured'); const sep = path.includes('?') ? '&' : '?'; const url = state.nodeUrl + path + (state.token ? `${sep}token=${state.token}` : ''); const r = await fetch(url); if (!r.ok) throw new Error(`Node ${r.status}`); return r.json(); } // ── Pages ───────────────────────────────────────────────────────────────────── async function pageHome() { const groups = await hubGet('/v1/groups'); const items = groups.groups.map(g => `
${esc(g.name)} ${g.join_policy}
`).join(''); return `

Public Groups

${items || '

No public groups yet.

'} ${state.nodeUrl ? `

My Node (${esc(state.nodeUrl)})

` : `

Connect to a Node

`}`; } async function pageGroup(groupId) { // TODO: fetch group info + node from hub return `

Group ${groupId} — coming soon

`; } async function pageNodeBrowser() { const data = await nodeGet('/index'); const rows = data.entries.map(e => ` ${esc(e.name)} ${e.type} ${fmtSize(e.size)} ${e.type === 'video' ? `` : ''} ⬇ Download `).join(''); return `

📁 ${esc(data.group_name)}

${data.entries.length} files — index v${data.version}

${rows}
NameTypeSizeActions
`; } function pageStream(fileId, name) { const src = `${state.nodeUrl}/hls/${fileId}/playlist.m3u8${state.token ? '?token=' + state.token : ''}`; return `

▶ ${esc(name)}


`; } // ── Actions ─────────────────────────────────────────────────────────────────── async function connectNode() { const url = document.getElementById('nodeUrl')?.value?.trim(); if (!url) return; state.nodeUrl = url; localStorage.setItem('mb_node', url); await pageNodeBrowser().then(setMain); } async function streamVideo(fileId, name) { setMain(pageStream(fileId, name)); } async function doLogin() { const u = document.getElementById('lu').value; const p = document.getElementById('lp').value; try { await login(u, p); render(); } catch(e) { alert('Login failed: ' + e.message); } } // ── Router / render ─────────────────────────────────────────────────────────── function setMain(html) { document.getElementById('main').innerHTML = html; } async function render() { const nav = document.getElementById('nav'); if (state.username) { nav.innerHTML = `MeshBay | Logged in as ${esc(state.username)} `; setMain('

Loading…

'); setMain(await pageHome()); } else { nav.innerHTML = 'MeshBay'; setMain(`

Login

No account? Register via the API for now.

`); } } // ── Utils ───────────────────────────────────────────────────────────────────── function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>') .replace(/"/g,'"').replace(/'/g,'''); } function fmtSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024**2) return (bytes/1024).toFixed(1) + ' KB'; if (bytes < 1024**3) return (bytes/1024**2).toFixed(1) + ' MB'; return (bytes/1024**3).toFixed(2) + ' GB'; } // ── Boot ────────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', render);