feat(web): SPA shell — gate, settings, today placeholder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
marcuspaico
2026-08-17 14:18:05 -07:00
parent f33d395b71
commit 5fbbf0676f
14 changed files with 411 additions and 3 deletions

35
web/src/pages/Gate.tsx Normal file
View File

@@ -0,0 +1,35 @@
import { useState } from "react";
import { api } from "../api";
export function Gate({ needsSetup, onDone }: { needsSetup: boolean; onDone: () => void }) {
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
try {
if (needsSetup) await api.setup(password);
await api.login(password);
onDone();
} catch (err) {
setError(err instanceof Error ? err.message : "failed");
}
};
return (
<main className="gate">
<h1>Helios</h1>
<p>{needsSetup ? "Create the password for this instance." : "Enter your password."}</p>
<form onSubmit={submit}>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={needsSetup ? "New password (min 8 chars)" : "Password"}
autoFocus
/>
<button type="submit">{needsSetup ? "Create password" : "Log in"}</button>
</form>
{error && <p className="error">{error}</p>}
</main>
);
}

View File

@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import type { SettingsResponse } from "@helios/shared";
import { api } from "../api";
export function Settings() {
const [current, setCurrent] = useState<SettingsResponse | null>(null);
const [llmBaseUrl, setLlmBaseUrl] = useState("");
const [llmModel, setLlmModel] = useState("");
const [llmKey, setLlmKey] = useState("");
const [status, setStatus] = useState<string | null>(null);
useEffect(() => {
api.getSettings().then((s) => {
setCurrent(s);
setLlmBaseUrl(s.llmBaseUrl);
setLlmModel(s.llmModel);
});
}, []);
const save = async (e: React.FormEvent) => {
e.preventDefault();
setStatus(null);
try {
await api.putSettings({ llmBaseUrl, llmModel, ...(llmKey ? { llmKey } : {}) });
setLlmKey("");
setCurrent(await api.getSettings());
setStatus("Saved");
} catch (err) {
setStatus(err instanceof Error ? err.message : "failed");
}
};
if (!current) return <p>Loading…</p>;
return (
<section>
<h2>AI provider</h2>
<p>Any OpenAI-compatible endpoint. OpenRouter by default; point at Ollama for a fully local setup.</p>
<form onSubmit={save}>
<label>Base URL <input value={llmBaseUrl} onChange={(e) => setLlmBaseUrl(e.target.value)} /></label>
<label>Model <input value={llmModel} onChange={(e) => setLlmModel(e.target.value)} /></label>
<label>
API key{current.llmKeyMasked ? ` (saved: ${current.llmKeyMasked})` : ""}
<input type="password" value={llmKey} onChange={(e) => setLlmKey(e.target.value)} placeholder="paste to replace" />
</label>
<button type="submit">Save</button>
</form>
{status && <p>{status}</p>}
</section>
);
}

8
web/src/pages/Today.tsx Normal file
View File

@@ -0,0 +1,8 @@
export function Today() {
return (
<section>
<h2>Today</h2>
<p>No data yet. Connect a wearable in Settings once connectors land (M2).</p>
</section>
);
}