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

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 `
`; } function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } void init();