import { useState, useEffect, useMemo } from "react"; const DailyComponent = () => { const storageKey = "daily-component-v1"; const pushUrl = "/api/v1/json_data"; const autosyncMs = 30000; const [state, setState] = useState(() => { try { const saved = localStorage.getItem(storageKey); return saved ? JSON.parse(saved) : { version: 1, items: initialItems(), lastSyncAt: null, lastSyncOk: null }; } catch { return { version: 1, items: initialItems(), lastSyncAt: null, lastSyncOk: null }; } }); const [showOnlyActive, setShowOnlyActive] = useState(false); const [expanded, setExpanded] = useState({}); const [syncing, setSyncing] = useState(false); const [syncError, setSyncError] = useState(null); // persist to localStorage useEffect(() => { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} }, [state, storageKey]); // autosync useEffect(() => { if (!autosyncMs || autosyncMs < 5000) return; const t = setInterval(() => { pushNow(); }, autosyncMs); return () => clearInterval(t); // eslint-disable-next-line react-hooks/exhaustive-deps }, [state]); const toggleActive = (id) => { setState((s) => ({ ...s, items: mapItemTree(s.items, id, (it) => ({ ...it, active: !it.active })), })); }; const incProgress = (id, delta) => { setState((s) => ({ ...s, items: mapItemTree(s.items, id, (it) => ({ ...it, progress: clamp(it.progress + delta, 0, 100), })), })); }; const updateNote = (id, note) => { setState((s) => ({ ...s, items: mapItemTree(s.items, id, (it) => ({ ...it, note })), })); }; const toggleExpand = (id) => setExpanded((e) => ({ ...e, [id]: !(e[id] ?? true) })); const pushNow = async () => { setSyncing(true); setSyncError(null); try { const payload = { type: "daily-component/state", at: new Date().toISOString(), storageKey, data: state, stats: summarize(state.items), }; const res = await fetch(pushUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); setState((s) => ({ ...s, lastSyncAt: new Date().toISOString(), lastSyncOk: true })); } catch (e) { setState((s) => ({ ...s, lastSyncAt: new Date().toISOString(), lastSyncOk: false })); setSyncError(e.message || String(e)); } finally { setSyncing(false); } }; const overall = useMemo(() => { const flat = flatten(state.items).filter((x) => (showOnlyActive ? x.active : true)); const total = flat.reduce((a, it) => a + computeDisplayProgress(it), 0); const pct = flat.length ? Math.round(total / flat.length) : 0; const activeCount = flatten(state.items).filter((x) => x.active).length; return { pct, count: flat.length, activeCount }; }, [state.items, showOnlyActive]); return (

Daily

Avg {overall.pct}% Items {overall.count} Active {overall.activeCount}
); }; export default DailyComponent; /* ---------- helpers ---------- */ const clamp = (v, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, v)); const uid = () => Math.random().toString(36).slice(2, 9); const initialItems = () => [ { id: uid(), title: "CEC", active: true, note: "", progress: 0 }, { id: uid(), title: "Boarding Assistant", active: true, note: "", progress: 0, children: [ { id: uid(), title: "Camera fix", active: true, note: "", progress: 0 }, { id: uid(), title: "Belt+Watch", active: true, note: "", progress: 0, children: [ { id: uid(), title: "Image gen", active: true, note: "", progress: 0 }, { id: uid(), title: "Backend update", active: true, note: "", progress: 0 }, ], }, ], }, { id: uid(), title: "Concierge", active: true, note: "", progress: 0, children: [ { id: uid(), title: "Microphone", active: true, note: "", progress: 0, children: [ { id: uid(), title: "Look at reviews", active: true, note: "", progress: 0 }, { id: uid(), title: "See what kind of support can there be", active: true, note: "", progress: 0 }, ], }, { id: uid(), title: "Overview video removing", active: true, note: "", progress: 0 }, ], }, { id: uid(), title: "IrLive", active: true, note: "", progress: 0, children: [ { id: uid(), title: "Unzip", active: true, note: "", progress: 0 }, { id: uid(), title: "Detect faces", active: true, note: "", progress: 0 }, { id: uid(), title: "Qwen experiment", active: true, note: "", progress: 0 }, ], }, { id: uid(), title: "Misc", active: true, note: "", progress: 0 }, ]; const mapItemTree = (items, id, fn) => items.map((it) => { if (it.id === id) return fn({ ...it }); if (it.children?.length) return { ...it, children: mapItemTree(it.children, id, fn) }; return it; }); const computeDisplayProgress = (it) => { if (!it.children || it.children.length === 0) return it.progress; const vals = it.children.map(computeDisplayProgress); return Math.round(vals.reduce((a, b) => a + b, 0) / vals.length); }; const flatten = (items, out = []) => { items.forEach((it) => { out.push(it); if (it.children?.length) flatten(it.children, out); }); return out; }; const summarize = (items) => { const flat = flatten(items); const byTitle = {}; flat.forEach((it) => { (byTitle[it.title] ||= []).push({ active: it.active, progress: it.progress }); }); const summary = Object.entries(byTitle).map(([title, arr]) => ({ title, n: arr.length, active: arr.filter((x) => x.active).length, avg: Math.round(arr.reduce((a, b) => a + b.progress, 0) / arr.length), })); const avg = flat.length ? Math.round(flat.reduce((a, b) => a + b.progress, 0) / flat.length) : 0; return { count: flat.length, avg, summary }; }; /* ---------- tree ---------- */ const Tree = ({ items, showOnlyActive, expanded, onToggleExpand, onToggleActive, onIncProgress, onUpdateNote }) => { return ( ); }; const TreeRow = ({ item, depth, showOnlyActive, expanded, onToggleExpand, onToggleActive, onIncProgress, onUpdateNote, }) => { const hasChildren = !!(item.children && item.children.length); const isExpanded = expanded[item.id] ?? true; const displayPct = computeDisplayProgress(item); if (showOnlyActive && !item.active) return null; return (
  • {hasChildren ? ( ) : (
    )}
    {displayPct}%
    onUpdateNote(item.id, e.target.value)} placeholder="Add a quick note…" className="w-full text-sm px-2 py-1 border rounded-lg bg-white focus:outline-none" />
    {hasChildren && isExpanded && (
      {item.children.map((ch) => ( ))}
    )}
  • ); };