monitor-free.openapps.xyz

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.
Owned app · rev 11d38ef641b0
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.
← All files
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
11d38ef641 · 17 hours ago
Release with preview
41c8af3ee5 · 19 hours ago
Bind agent address in policy
5b2d792529 · 19 hours ago
Use OpenApps service URLs and add contribution strip (retry)
be4477dcb3 · 19 hours ago
Bounded source sync
5c9ec00c29 · 2 days ago
Bounded source sync
b33aa231f4 · 2 days ago
Bounded source sync
6cb67b3c8e · 2 days ago
Bounded source sync
e054acaa29 · 2 days ago