import { html, useState, useEffect, useCallback, useRef, } from './vendor/htm-preact.js'; import { t } from './i18n.js'; import * as platform from './platform.js'; import { Icon } from './icon.js'; // ── Node management (D5) ──────────────────────────────────────────────────── // // Electron-only: talks to the local node daemon via its loopback HTTP API // (platform.node.call), not over MNP/WebRTC. The MNP protocol types remain // for potential future browser-side use. function NodeServicePanel({ onChanged }) { const [info, setInfo] = useState(null); const [busy, setBusy] = useState(''); const [err, setErr] = useState(''); const refresh = useCallback(async () => { try { const r = await platform.node.service.status(); setInfo(r); setErr(''); } catch (e) { setErr(platform.bridgeMessage(e)); } }, []); useEffect(() => { if (!platform.node.service.available) return; refresh(); const timer = setInterval(refresh, 5000); return () => clearInterval(timer); }, [refresh]); const act = useCallback(async (name, fn) => { setBusy(name); setErr(''); try { await fn(); await refresh(); if (onChanged) onChanged(); } catch (e) { setErr(platform.bridgeMessage(e)); } finally { setBusy(''); } }, [refresh, onChanged]); if (!platform.node.service.available) return null; if (!info || info.supported === false) { return html`
${error}
| ${t('node.overview_version')} | ${nodeInfo.version || '—'} |
| ${t('node.overview_node_id')} | ${nodeInfo.endpoint_hint || nodeInfo.pk_node_ed25519 || '—'} |
| ${t('node.overview_quic_port')} | ${nodeInfo.quic_port || '—'} |
| ${t('node.overview_hub')} | ${nodeInfo.hub_url || '—'} |
${t('node.gek_public_hint')}
`) : html`${t('node.gek_init_hint')}
`}${t('node.roster_empty')}
` : html`| ${t('node.roster_user')} | ${t('node.roster_role')} | ${t('node.roster_status')} | ${t('node.roster_via')} | |
|---|---|---|---|---|
| ${m.username || m.user_id.slice(0, 12)} | ${m.role} | ${m.status} | ${m.pinned_via || ''} | ${m.pk_ed25519 && html` `} |
${t('node.roster_invites', { n: roster.invites.length })}
`}${t('node.node_roster_hint')}
${!nodeRoster ? html`${t('node.loading')}
` : (nodeRoster.members || []).length === 0 ? html`${t('node.roster_empty')}
` : html`| ${t('node.roster_user')} | ${t('node.roster_role')} | ${t('node.roster_status')} | ${t('node.roster_scope')} | ${t('node.roster_via')} | |
|---|---|---|---|---|---|
| ${m.username || m.user_id.slice(0, 12)} | ${m.role} | ${m.status} | ${m.group_id ? m.group_id.slice(0, 8) : t('node.roster_scope_node')} | ${m.pinned_via || ''} | ${m.pk_ed25519 && html` `} |
${t('node.loading')}
` : peers.length === 0 ? html`${t('node.peers_empty')}
` : html`| ${t('node.peers_user')} | ${t('node.peers_ip')} | ${t('node.peers_group')} | ${t('node.peers_state')} |
|---|---|---|---|
| ${p.username || (p.user_id || '').slice(0, 12) || '—'} | ${p.remote_ip || '—'} | ${p.group_name || (p.group_id ? p.group_id.slice(0, 8) : '—')} | ${p.state || '—'} |
${t('node.loading')}
` : audit.length === 0 ? html`${t('node.audit_empty')}
` : html`| ${t('node.audit_time')} | ${t('node.audit_event')} | ${t('node.audit_user')} | ${t('node.audit_ip')} | ${t('node.audit_group')} | ${t('node.audit_detail')} |
|---|---|---|---|---|---|
| ${new Date(e.timestamp * 1000).toLocaleString()} | ${e.event} | ${e.username || (e.user_id || '').slice(0, 8)} | ${e.ip || '—'} | ${e.group_name || (e.group_id ? e.group_id.slice(0, 8) : '—')} | ${e.detail || ''} |
${t('node.loading')}
` : html`${t('node.maintenance_cache', { n: cacheCount })}
`}${t('node.settings_edit_hint')}
${t('node.stun_hint')}
${editStun.length === 0 ? html`${t('node.stun_empty')}
` : html`${editIce.length === 0 ? t('node.ice_auto_hint') : t('node.ice_manual_hint')}
${editIce.length > 0 && html`${t('node.denylist_empty')}
` : html`${t('node.denylist_count', { n: denylist.count })}
${(denylist.users || []).map(u => html`${t('node.unlink_hint')}