Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 9383f719c9b37ff76411858f
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/Baby.tsx
// The baby station screen: set up, pair parents, then a dimmed screen that keeps watching.
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
import { get, setMany, serverTimestamp } from "@bounded-sh/client";
import { me, explain } from "./lib/bounded";
import { navigate } from "./lib/router";
import { randomId } from "./lib/rtc";
import { SOUNDS, type Facing } from "./lib/protocol";
import { baby, type BabyConfig } from "./lib/store";
import { Station, openCamera, type Watcher } from "./station";
import { QrCode } from "./QrCode";
import { Icon, Sheet, toast, useNightChrome, useWakeLock } from "./ui";
type Phase = { kind: "setup" } | { kind: "starting" } | { kind: "error"; message: string } | { kind: "running"; station: Station; isNew: boolean };
function cameraError(err: unknown): string {
const name = (err as { name?: string })?.name;
if (name === "NotAllowedError" || name === "SecurityError") return "Monitor needs the camera and microphone. Allow them in the browser's site settings, then try again.";
if (name === "NotFoundError" || name === "OverconstrainedError") return "This device has no camera or microphone that Monitor can use.";
if (name === "NotReadableError") return "Another app is using the camera. Close it and try again.";
return explain(err);
}
/** The station's room, created on first use. A room this device can no longer open is replaced. */
async function ensureRoom(uid: string, name: string, camera: Facing): Promise<{ cfg: BabyConfig; isNew: boolean }> {
const saved = baby.get();
if (saved && (await get(`rooms/${saved.room}`))) return { cfg: saved, isNew: false };
const room = randomId(20);
await setMany([
{ path: `rooms/${room}`, document: { owner: uid, name, session: "", online: false, createdAt: serverTimestamp() } },
{ path: `rooms/${room}/joins/${uid}`, document: { role: "baby", name, at: serverTimestamp() } },
]);
const cfg = { room, name, camera };
baby.set(cfg);
return { cfg, isNew: true };
}
async function permitted(): Promise<boolean> {
try {
const [cam, mic] = await Promise.all(["camera", "microphone"].map((n) => navigator.permissions.query({ name: n as PermissionName })));
return cam.state === "granted" && mic.state === "granted";
} catch {
return false;
}
}
export function BabyPage() {
useNightChrome();
const saved = baby.get();
const [phase, setPhase] = useState<Phase>({ kind: saved ? "starting" : "setup" });
const [name, setName] = useState(saved?.name || "Baby's room");
const stationRef = useRef<Station | null>(null);
const start = useCallback(async (ctx: AudioContext) => {
setPhase({ kind: "starting" });
let stream: MediaStream | null = null;
try {
const facing = baby.get()?.camera || "user";
stream = await openCamera(facing);
const uid = await me();
const stationName = name.trim() || "Baby's room";
const { cfg, isNew } = await ensureRoom(uid, stationName, facing);
if (cfg.name !== stationName) baby.set({ ...cfg, name: stationName });
const station = new Station(cfg.room, uid, stationName, stream, cfg.camera, ctx);
await station.start();
stationRef.current = station;
setPhase({ kind: "running", station, isNew });
} catch (err) {
stream?.getTracks().forEach((t) => t.stop());
setPhase({ kind: "error", message: cameraError(err) });
}
}, [name]);
useEffect(() => {
void Station.clearLeftoverCode();
// Coming back to a station whose camera is already allowed starts it right away.
if (saved) void permitted().then((ok) => (ok ? start(new AudioContext()) : setPhase({ kind: "setup" })));
const off = () => { void stationRef.current?.stop(true); };
window.addEventListener("pagehide", off);
return () => { window.removeEventListener("pagehide", off); off(); };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (phase.kind === "running") return <StationView station={phase.station} isNew={phase.isNew} />;
return (
<div className="night">
<div className="night-page">
<TopBar title="Baby station" />
{phase.kind === "starting" ? (
<div className="center-fill"><span className="spinner" /> <span>Starting the camera...</span></div>
) : (
<div className="setup">
<h1 className="night-h1">Set up the baby station</h1>
<p className="night-lede">Use the phone that stays by the crib. Video and sound go straight to the parent's phone and are never recorded.</p>
<label className="field">
<span>Name this station</span>
<input className="input" value={name} maxLength={40} onChange={(e) => setName(e.target.value)} placeholder="Baby's room" />
</label>
{phase.kind === "error" && <p className="notice notice-error" role="alert">{phase.message}</p>}
<button className="btn btn-xl btn-block" type="button" onClick={() => { const ctx = new AudioContext(); void ctx.resume(); void start(ctx); }}>
<Icon name="camera" size={22} /> Turn on camera and microphone
</button>
<ul className="tips">
<li>Plug the phone into its charger.</li>
<li>Keep the phone and its cable at least 1 meter (3 feet) from the crib, out of reach.</li>
<li>Point the camera at the crib. You can switch cameras next.</li>
</ul>
</div>
)}
</div>
</div>
);
}
export function TopBar({ title, right, back = "/app" }: { title: string; right?: React.ReactNode; back?: string }) {
return (
<header className="bar">
<a className="bar-back" href={back} onClick={(e) => { e.preventDefault(); navigate(back); }} aria-label="Back">
<Icon name="back" size={22} />
</a>
<span className="bar-title">{title}</span>
<span className="bar-right">{right}</span>
</header>
);
}
function StationView({ station, isNew }: { station: Station; isNew: boolean }) {
const snap = useSyncExternalStore(station.subscribe, station.snapshot);
const [dim, setDim] = useState(false);
const [pairing, setPairing] = useState(isNew);
const [sheet, setSheet] = useState<"rename" | { remove: Watcher } | null>(null);
const awake = useWakeLock(true);
const preview = useRef<HTMLVideoElement>(null);
const meter = useRef<HTMLDivElement>(null);
const idle = useRef<number | undefined>(undefined);
const busy = pairing || sheet !== null;
useEffect(() => {
station.onLevel = (v) => { meter.current?.style.setProperty("--level", `${v}%`); };
const off = station.onJoin((n) => toast(`${n} is paired and can watch now.`));
return () => { station.onLevel = null; off(); };
}, [station]);
useEffect(() => {
if (preview.current && preview.current.srcObject !== station.preview) preview.current.srcObject = station.preview;
});
useEffect(() => {
const cfg = baby.get();
if (cfg && cfg.camera !== snap.camera) baby.set({ ...cfg, camera: snap.camera });
}, [snap.camera]);
// Dim after 20 seconds without a touch, unless a panel is open.
const poke = useCallback(() => {
station.unlock();
window.clearTimeout(idle.current);
if (!busy) idle.current = window.setTimeout(() => setDim(true), 20000);
}, [station, busy]);
useEffect(() => { poke(); return () => window.clearTimeout(idle.current); }, [poke]);
const watching = snap.watchers.filter((w) => w.connected);
const talking = snap.watchers.find((w) => w.talking);
const sound = SOUNDS.find((s) => s.name === snap.sound);
if (dim) {
return (
<button type="button" className="dimmed" onPointerDown={() => { station.unlock(); setDim(false); }} aria-label="Show controls">
<span className="dim-status">{watching.length ? `${watching.length} watching` : "Waiting for a parent"}</span>
<Clock />
{talking && <span className="dim-talk">{talking.name} is talking</span>}
{sound && <span className="dim-note">{sound.label} is playing</span>}
{!snap.listening && <span className="dim-warn">Tap once to turn on sound alerts</span>}
{snap.cameraStopped && <span className="dim-warn">The camera stopped. Tap to fix it.</span>}
<span className="dim-hint">Tap to show controls</span>
</button>
);
}
return (
<div className="night" onPointerDown={poke}>
<div className="night-page">
<TopBar title={baby.get()?.name || "Baby station"} right={<button className="btn btn-quiet btn-sm" type="button" onClick={() => setDim(true)}><Icon name="moon" size={18} /> Dim</button>} />
<div className="preview">
<video ref={preview} muted playsInline autoPlay />
<span className={`live-chip ${watching.length ? "on" : ""}`}>{watching.length ? `${watching.length} watching` : "Not watched yet"}</span>
{snap.canSwitch && (
<button className="preview-flip" type="button" onClick={() => void station.switchCamera().catch((e) => toast(cameraError(e)))} aria-label="Switch camera">
<Icon name="flip" size={22} />
</button>
)}
</div>
<p className="meter-label"><span>Sound in the room</span></p>
<div className="meter" ref={meter} aria-hidden="true"><span /></div>
{snap.cameraStopped && <p className="notice notice-error">The camera stopped, maybe because another app took it. <button className="link-btn" type="button" onClick={() => location.reload()}>Restart the station</button></p>}
{!snap.listening && <button type="button" className="notice notice-warn" onClick={() => station.unlock()}>Tap here to turn on sound alerts and talk back. Browsers need one tap first.</button>}
{!awake && <p className="notice">This browser cannot keep the screen on by itself. Set the phone's auto-lock to never while it is the baby station.</p>}
{talking && <p className="notice notice-ok">{talking.name} is talking to the baby.</p>}
{sound && (
<div className="notice row-between">
<span>{sound.label} is playing{snap.soundEnds ? `, stops at ${new Date(snap.soundEnds).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}` : ""}</span>
<button className="btn btn-sm btn-line" type="button" onClick={() => station.stopSound()}>Stop</button>
</div>
)}
<section className="panel-night">
<h2 className="night-h2">Parents</h2>
{snap.watchers.length ? (
<ul className="watchers">
{snap.watchers.map((w) => (
<li key={w.id}>
<span className={`dot ${w.connected ? "on" : ""}`} aria-hidden="true" />
<span className="grow">
<span className="watcher-name">{w.name}</span>
<span className="watcher-state">{w.talking ? "Talking" : w.connected ? "Watching now" : "Paired, not watching"}</span>
</span>
<button className="btn btn-sm btn-quiet" type="button" onClick={() => setSheet({ remove: w })}>Remove</button>
</li>
))}
</ul>
) : <p className="night-muted">No parents yet. Add a parent's phone to start watching.</p>}
</section>
<div className="stack-buttons">
<button className="btn btn-xl btn-block" type="button" onClick={() => setPairing(true)}><Icon name="plus" size={22} /> Add a parent</button>
<button className="btn btn-xl btn-block btn-line" type="button" onClick={() => setDim(true)}><Icon name="moon" size={22} /> Dim the screen</button>
</div>
<div className="footer-actions">
<button className="link-btn" type="button" onClick={() => setSheet("rename")}>Rename station</button>
<button className="link-btn danger" type="button" onClick={() => { void station.stop(true).then(() => navigate("/app")); }}>Turn off baby station</button>
</div>
</div>
{pairing && <PairPanel station={station} onClose={() => setPairing(false)} />}
{sheet === "rename" && <RenameSheet station={station} onClose={() => setSheet(null)} />}
{sheet && typeof sheet === "object" && (
<Sheet title={`Remove ${sheet.remove.name}?`} onClose={() => setSheet(null)}>
<p className="night-muted">It stops watching right away and cannot pair with this station again.</p>
<div className="sheet-actions">
<button className="btn btn-lg btn-danger-solid" type="button" onClick={() => {
const w = sheet.remove;
setSheet(null);
station.removeWatcher(w.id).then(() => toast(`${w.name} was removed.`), (e) => toast(explain(e)));
}}>Remove</button>
<button className="btn btn-lg btn-line" type="button" onClick={() => setSheet(null)}>Keep</button>
</div>
</Sheet>
)}
</div>
);
}
function Clock() {
const [now, setNow] = useState(() => new Date());
useEffect(() => { const t = setInterval(() => setNow(new Date()), 15000); return () => clearInterval(t); }, []);
return <span className="dim-clock">{now.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}</span>;
}
function PairPanel({ station, onClose }: { station: Station; onClose: () => void }) {
const [code, setCode] = useState<{ code: string; expires: number } | null>(null);
const [error, setError] = useState<string | null>(null);
const [left, setLeft] = useState(600);
const current = useRef<string | null>(null);
const fresh = useCallback(async () => {
setError(null);
if (current.current) void station.clearCode(current.current);
current.current = null;
setCode(null);
try {
const c = await station.newCode();
current.current = c.code;
setCode(c);
} catch (err) {
setError(explain(err));
}
}, [station]);
useEffect(() => {
void fresh();
// Each code pairs one parent: once someone pairs, the next parent gets a new code.
const off = station.onJoin(() => void fresh());
return () => { off(); if (current.current) void station.clearCode(current.current); };
}, [station, fresh]);
useEffect(() => {
if (!code) return;
const tick = () => setLeft(Math.max(0, Math.round((code.expires - Date.now()) / 1000)));
tick();
const t = setInterval(tick, 1000);
return () => clearInterval(t);
}, [code]);
const expired = code && left === 0;
const link = `${location.origin}/pair#${station.room}`;
return (
<Sheet title="Add a parent" onClose={onClose}>
<ol className="pair-steps">
<li>On the parent's phone or laptop, open <strong>{location.host}</strong> and choose <strong>Parent</strong>.</li>
<li>Enter this code, or scan the QR code with the phone's camera.</li>
</ol>
{error ? <p className="notice notice-error">{error}</p> : !code ? (
<div className="center-fill small"><span className="spinner" /> Making a code...</div>
) : expired ? (
<div className="code-expired">
<p>This code expired.</p>
<button className="btn btn-lg" type="button" onClick={() => void fresh()}>Get a new code</button>
</div>
) : (
<div className="code-box">
<p className="pair-code" aria-label={`Pairing code ${code.code.split("").join(" ")}`}>{code.code.slice(0, 3)}<span> </span>{code.code.slice(3)}</p>
<QrCode text={link} size={196} label="QR code to pair a parent device" />
<p className="code-left">Works for {Math.floor(left / 60)}:{String(left % 60).padStart(2, "0")} and pairs one device.</p>
</div>
)}
<div className="sheet-actions">
<button className="btn btn-lg btn-block" type="button" onClick={onClose}>Done</button>
</div>
</Sheet>
);
}
function RenameSheet({ station, onClose }: { station: Station; onClose: () => void }) {
const [value, setValue] = useState(baby.get()?.name || "");
const save = async () => {
const name = value.trim().slice(0, 40);
if (!name) return;
try {
await station.rename(name);
const cfg = baby.get();
if (cfg) baby.set({ ...cfg, name });
onClose();
} catch (err) {
toast(explain(err));
}
};
return (
<Sheet title="Rename station" onClose={onClose}>
<form onSubmit={(e) => { e.preventDefault(); void save(); }} className="stack">
<input className="input" autoFocus value={value} maxLength={40} onChange={(e) => setValue(e.target.value)} />
<button className="btn btn-lg btn-block" type="submit">Save</button>
</form>
</Sheet>
);
}
Change history
Move store links to openapps.free
Release with preview
Bind agent address in policy
Use OpenApps service URLs and add contribution strip (retry)
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync