import { useState, useEffect, useRef, useCallback } from "preact/hooks";
let meshLoaded = false;
let meshLoadPromise = null;
function ensureMeshClient() {
if (meshLoaded) return Promise.resolve();
if (meshLoadPromise) return meshLoadPromise;
meshLoadPromise = new Promise((resolve, reject) => {
if (window.MobuxMesh) {
meshLoaded = true;
return resolve();
}
const s = document.createElement("script");
s.src = "/static/mesh-client.js";
s.async = false;
s.onload = () => {
meshLoaded = true;
resolve();
};
s.onerror = () => reject(new Error("Failed to load mesh-client.js"));
document.body.appendChild(s);
});
return meshLoadPromise;
}
function getMesh() {
return window.MobuxMesh || null;
}
function CredDialog({ peer, note, onConfirm, onCancel }) {
const dialogRef = useRef(null);
const userRef = useRef(null);
const pinRef = useRef(null);
useEffect(() => {
dialogRef.current?.showModal();
userRef.current?.focus();
}, []);
const submit = (e) => {
e.preventDefault();
const user = (userRef.current?.value || "").trim();
const pin = (pinRef.current?.value || "").trim();
if (!user || !pin) return;
onConfirm(user, pin);
};
return (
<dialog
ref={dialogRef}
class="session-dialog"
onCancel={(e) => {
e.preventDefault();
onCancel();
}}
>
<form method="dialog" onSubmit={submit}>
<h3>Sign in to {peer}</h3>
{note && (
<p class="hint" style="padding:0 0 8px;text-align:left">
{note}
</p>
)}
<input ref={userRef} placeholder="user" autocomplete="off" required />
<input
ref={pinRef}
placeholder="PIN"
type="password"
inputmode="numeric"
autocomplete="off"
required
/>
<div class="dialog-actions">
<button type="button" class="btn-cancel" onClick={onCancel}>
Cancel
</button>
<button type="submit" class="btn-create">
Connect
</button>
</div>
</form>
</dialog>
);
}
export function HostPicker() {
const [ready, setReady] = useState(false);
const [selectedPeer, setSelectedPeer] = useState("");
const [peers, setPeers] = useState([]);
const [credDialog, setCredDialog] = useState(null);
useEffect(() => {
ensureMeshClient()
.then(async () => {
const m = getMesh();
if (m) setSelectedPeer(m.getPeer() || "");
try {
const res = await fetch("/api/peers");
if (res.ok) {
const body = await res.json();
setPeers(body.peers || []);
}
} catch (_) {}
setReady(true);
})
.catch(() => {
setReady(true);
});
}, []);
const notifyPeerChanged = useCallback(() => {
window.dispatchEvent(new CustomEvent("mobux:peer-changed"));
if (typeof window.refreshSessions === "function") window.refreshSessions();
}, []);
const promptCred = useCallback((peer, note = null) => {
return new Promise((resolve) => {
const m = getMesh();
setCredDialog({
peer,
note,
onConfirm: (user, pin) => {
m?.setPeerCred(peer, user, pin);
setCredDialog(null);
resolve(true);
},
onCancel: () => {
setCredDialog(null);
resolve(false);
},
});
});
}, []);
const selectPeer = useCallback(
async (peer) => {
const m = getMesh();
if (!m) return;
if (!peer) {
m.setPeer("");
setSelectedPeer("");
notifyPeerChanged();
return;
}
m.setPeer(peer);
if (!m.getPeerCred(peer)) {
const ok = await promptCred(peer);
if (!ok) {
m.setPeer("");
setSelectedPeer("");
notifyPeerChanged();
return;
}
}
setSelectedPeer(m.getPeer() || "");
notifyPeerChanged();
},
[notifyPeerChanged, promptCred],
);
useEffect(() => {
const handler = async (e) => {
const m = getMesh();
if (!m) return;
const peer = e.detail?.peer || m.getPeer();
if (!peer) return;
const note = m.getPeerCred(peer)
? "Authentication failed — please re-enter your PIN."
: null;
const ok = await promptCred(peer, note);
if (ok) {
setSelectedPeer(peer);
notifyPeerChanged();
window.dispatchEvent(
new CustomEvent("mobux:peer-cred-stored", { detail: { peer } }),
);
} else {
m.setPeer("");
setSelectedPeer("");
notifyPeerChanged();
window.dispatchEvent(
new CustomEvent("mobux:peer-cred-cancelled", { detail: { peer } }),
);
}
};
window.addEventListener("mobux:peer-auth-required", handler);
return () =>
window.removeEventListener("mobux:peer-auth-required", handler);
}, [promptCred, notifyPeerChanged]);
const handleChange = useCallback(
async (e) => {
await selectPeer(e.target.value);
},
[selectPeer],
);
if (!ready) return null;
return (
<div class="spa-host-picker">
<select
class="host-select"
value={selectedPeer}
onChange={handleChange}
aria-label="Active host"
>
<option value="">This host</option>
{peers.map((p) => {
const peerId = `${p.host}:${p.port}`;
const label =
p.reachable === false ? `${p.name} (unreachable)` : p.name;
return (
<option
key={peerId}
value={peerId}
disabled={p.reachable === false}
>
{label}
</option>
);
})}
</select>
{credDialog && (
<CredDialog
peer={credDialog.peer}
note={credDialog.note}
onConfirm={credDialog.onConfirm}
onCancel={credDialog.onCancel}
/>
)}
</div>
);
}