import "./styles.css"; import { App } from "@capacitor/app"; import { normalizeAddress, probeServer } from "./connect"; import { loadServers, markConnected, newServerId, removeServer, upsertServer, type SavedServer, } from "./storage"; const root = document.getElementById("app"); if (!root) throw new Error("Missing #app root"); let servers: SavedServer[] = []; let busy = false; async function init() { servers = await loadServers(); render(); // Android hardware back button: while on the launcher there's nothing to go // back to, so exit the app. (Once navigated onto a server origin, the WebView // handles its own history.) App.addListener("backButton", () => { void App.exitApp(); }); } function setBusy(value: boolean) { busy = value; render(); } async function handleAdd(form: HTMLFormElement) { const data = new FormData(form); const addressRaw = String(data.get("address") ?? ""); const username = String(data.get("username") ?? "").trim(); const labelRaw = String(data.get("label") ?? "").trim(); const normalized = normalizeAddress(addressRaw); if (!normalized.ok) { showError(normalized.error); return; } setBusy(true); const probe = await probeServer(normalized.origin); if (!probe.ok) { setBusy(false); showError(probe.error); return; } const server: SavedServer = { id: newServerId(), label: labelRaw || normalized.host, address: normalized.origin, username: username || undefined, }; servers = await upsertServer(server); setBusy(false); await connect(server); } async function connect(server: SavedServer) { setBusy(true); const probe = await probeServer(server.address); if (!probe.ok) { setBusy(false); showError(probe.error); return; } await markConnected(server.id); // Navigate the WebView onto the server's own origin. From here the existing // Homeio web app runs first-party: its `/login` page handles username + // password (and TOTP), and the session cookie is stored for this origin. window.location.href = server.address; } async function handleRemove(id: string) { servers = await removeServer(id); render(); } function showError(message: string) { const el = document.getElementById("error"); if (el) { el.textContent = message; el.hidden = false; } } function render() { const sorted = [...servers].sort( (a, b) => (b.lastConnectedAt ?? 0) - (a.lastConnectedAt ?? 0), ); root!.innerHTML = ` Homeio Connect to your home server over Tailscale ${ sorted.length ? ` ${sorted.map(renderServerCard).join("")} ` : `No servers yet. Add your Homeio server below.` } Add a server Server address Name (optional) Username (optional) ${busy ? "Connecting…" : "Add & Connect"} You'll sign in with your password on the next screen. Make sure the Tailscale app is connected first. `; const form = document.getElementById("add-form") as HTMLFormElement | null; form?.addEventListener("submit", (e) => { e.preventDefault(); if (!busy) void handleAdd(form); }); root!.querySelectorAll("[data-connect]").forEach((btn) => { btn.addEventListener("click", () => { const server = servers.find((s) => s.id === btn.dataset.connect); if (server && !busy) void connect(server); }); }); root!.querySelectorAll("[data-remove]").forEach((btn) => { btn.addEventListener("click", () => { const id = btn.dataset.remove; if (id) void handleRemove(id); }); }); } function renderServerCard(server: SavedServer): string { const meta = server.username ? `${server.username} · ${server.address}` : server.address; return ` ${escapeHtml(server.label)} ${escapeHtml(meta)} ✕ `; } function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } void init();
Connect to your home server over Tailscale
No servers yet. Add your Homeio server below.
You'll sign in with your password on the next screen. Make sure the Tailscale app is connected first.