// TodoPage — v07zz221
// One prioritized "Needs attention" inbox: unresolved notes, unresolved review
// comments, and failed generations. Matches Hugo's mockup — a summary-card row
// over a single list (no tabs). Each row shows a real THUMBNAIL of the shot /
// asset it refers to, and Open opens that item's modal RIGHT HERE (on top of the
// To-Do page) instead of navigating away — shot modal, asset modal, or a video
// review modal.
function TodoPage() {
  const fetcher = window.authFetch || fetch;
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [busy, setBusy] = React.useState({});
  const [expanded, setExpanded] = React.useState({});
  const [filter, setFilter] = React.useState(null);  // v07zz226 — null=all | 'note' | 'review' | 'generation' | 'ready'; each summary card focuses one type
  const [assetVer, setAssetVer] = React.useState(0);  // v07zz240 — bumps when an asset status changes (in-place mutation of window.__appData) so the milestone tally re-runs

  const load = React.useCallback(() => {
    fetcher("/api/todo").then(r => r.ok ? r.json() : null).then(d => { setData(d || { items: [], summary: {} }); setLoading(false); }).catch(() => { setData({ items: [], summary: {} }); setLoading(false); });
  }, []);
  React.useEffect(() => { load(); }, [load]);
  React.useEffect(() => {
    const onSse = (e) => { const t = e.detail && e.detail.type; if (t === "note_added" || t === "note_resolved" || t === "review_changed" || t === "generate") load(); if (t === "asset_status_changed") setAssetVer(v => v + 1); };
    window.addEventListener("paradise-sse", onSse);
    const onFocus = () => load();
    window.addEventListener("focus", onFocus);
    // v07zz240 — asset status changes mutate window.__appData in place (no new
    // ref), so bump a version to force the milestone tally to recompute.
    const onAsset = () => setAssetVer(v => v + 1);
    window.addEventListener("paradise-asset-lock-changed", onAsset);
    return () => { window.removeEventListener("paradise-sse", onSse); window.removeEventListener("focus", onFocus); window.removeEventListener("paradise-asset-lock-changed", onAsset); };
  }, [load]);

  const items = (data && data.items) || [];
  const ready = (data && data.ready) || [];
  const summary = (data && data.summary) || {};
  // v07zz224 — nudge the sidebar "Needs attention" badge to refetch after a
  // To-Do mutation so the count updates without waiting for a server SSE.
  const pingBadge = () => { try { window.dispatchEvent(new CustomEvent("paradise-sse", { detail: { type: "todo_changed" } })); } catch (_) {} };

  // ── thumbnail resolution (from window.__appData — same image logic the rest
  //    of the app uses, so thumbnails are warm/instant) ──────────────────────
  const appData = window.__appData || {};
  const shotsById = React.useMemo(() => {
    const m = {}; for (const s of (appData.shots || [])) m[s.id] = s; return m;
  }, [appData.shots]);
  const shotImg = (s) => (s && (s.video_poster || (s.image_paths && (s.image_paths.selected || s.image_paths.first_pass)) || s.image || s.hero)) || null;
  const assetImg = (a) => (a && (a.cover_url || a.image || (Array.isArray(a.references) && a.references[0] && a.references[0].url))) || null;

  // ── v07zz240 — Milestone readiness: "what's left for the next milestone". ──
  // Computes the next upcoming schedule milestone (else the next phase end) and
  // tallies every production asset by workflow status (Approved / WIP / Retake /
  // First Pass) so Hugo can see, against a date, which assets are done vs left.
  const fmtMsDate = (iso) => { try { return new Date(iso + "T00:00:00").toLocaleDateString(undefined, { month: "short", day: "numeric" }); } catch (_) { return iso; } };
  const milestone = React.useMemo(() => {
    const sch = appData.schedule || {};
    const today = new Date(); today.setHours(0, 0, 0, 0);
    const parse = (d) => { const t = new Date(String(d) + "T00:00:00"); return isNaN(t.getTime()) ? null : t; };
    let next = null;
    const ms = (sch.milestones || []).map(m => ({ ...m, _d: parse(m.date) })).filter(m => m._d);
    const upcoming = ms.filter(m => m._d >= today).sort((a, b) => a._d - b._d);
    if (upcoming.length) next = { name: upcoming[0].name, date: upcoming[0]._d, raw: upcoming[0].date };
    if (!next) {
      const ph = (sch.phases || []).map(p => ({ ...p, _d: parse(p.end) })).filter(p => p._d && p._d >= today).sort((a, b) => a._d - b._d);
      if (ph.length) next = { name: ph[0].name, date: ph[0]._d, raw: ph[0].end, isPhase: true };
    }
    const norm = window.normAssetStatus || ((s) => (["first_pass", "wip", "retake", "approved"].includes(String(s || "").toLowerCase()) ? String(s).toLowerCase() : "first_pass"));
    // 15 Sep 2026 — the project's own categories (the film-doc four for Paradise Found).
    const cats = (!window.__isDefaultProject || window.__isDefaultProject()) ? [
      { key: "characters", label: "Characters" },
      { key: "animals", label: "Animals" },
      { key: "locations", label: "Locations" },
      { key: "props", label: "Props" },
    ] : (window.__projectCategories ? window.__projectCategories() : []).map(c => ({ key: c.id, label: c.label }));
    const tally = { approved: 0, wip: 0, retake: 0, first_pass: 0, total: 0 };
    const perCat = [];
    const leftoverChars = [];
    for (const c of cats) {
      const arr = (appData.assets && Array.isArray(appData.assets[c.key]) ? appData.assets[c.key] : []).filter(a => !a.archived && !a.deleted);
      let approved = 0;
      for (const a of arr) {
        const st = norm(a.status);
        tally[st] = (tally[st] || 0) + 1; tally.total++;
        if (st === "approved") approved++;
        else if (c.key === "characters") leftoverChars.push({ id: a.id || a.slug, name: a.name, status: st });
      }
      perCat.push({ key: c.key, label: c.label, approved, total: arr.length });
    }
    const days = next ? Math.round((next.date.getTime() - today.getTime()) / 86400000) : null;
    return { next, days, tally, perCat, leftoverChars };
  }, [appData.assets, appData.schedule, assetVer]);
  const openAsset = (kind, id) => { try { if (window.__openAssetItem && id) window.__openAssetItem(kind, id); } catch (_) {} };
  const findAsset = (kind, id) => {
    const PLURAL = { character: "characters", animal: "animals", location: "locations", prop: "props", historical: "historical", ref: "refs", archival: "archival" };
    // 15 Sep 2026 — a templated project's kind may already be its category id (instruments).
    const key = PLURAL[kind] || ((kind && appData.assets && Array.isArray(appData.assets[kind])) ? kind : (kind ? kind + "s" : null));
    const pool = (appData.assets && Array.isArray(appData.assets[key])) ? appData.assets[key] : [];
    if (!id) return null;
    const exact = pool.find(a => a.id === id || a.slug === id || a.folder === id || a.name === id);
    if (exact) return exact;
    // fuzzy: a gen's asset_slug ("audubon") may not equal the asset's full slug
    // ("john-james-audubon") — match on normalised name/slug containment.
    const norm = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9]/g, "");
    const want = norm(id);
    if (!want) return null;
    return pool.find(a => { const ns = norm(a.slug), ni = norm(a.id), nn = norm(a.name); return (ns && (ns.includes(want) || want.includes(ns))) || (ni && (ni.includes(want) || want.includes(ni))) || (nn && (nn.includes(want) || want.includes(nn))); }) || null;
  };
  const assetKindOf = (it) => {
    // note entity_type for assets is "asset_<kind>"; gens carry asset_category.
    if (it.type === "generation" && it.is_asset_gen) return it.asset_category || null;
    const et = String(it.entity_type || "");
    return et.startsWith("asset_") ? et.slice(6) : null;
  };
  const thumbFor = (it) => {
    try {
      if (it.type === "review") { const s = shotsById[it.review_shot_id]; return s ? shotImg(s) : null; }
      if (it.type === "generation") {
        if (it.is_asset_gen) { const a = findAsset(it.asset_category, it.asset_slug); return a ? assetImg(a) : null; }
        const s = shotsById[it.shot_id]; return s ? shotImg(s) : null;
      }
      if (it.type === "note") {
        if (it.entity_type === "shot") { const s = shotsById[it.entity_id]; return s ? shotImg(s) : null; }
        const k = assetKindOf(it); if (k) { const a = findAsset(k, it.entity_id); return a ? assetImg(a) : null; }
      }
    } catch (_) {}
    return null;
  };

  // ── open the referenced item AS A MODAL on top of this page ──────────────
  const openItem = (it) => {
    try {
      const nav = window.__nav || {};
      if (it.type === "review") {
        if (window.__openReview && it.review_id) { window.__openReview(it.review_id, it.timecode); return; }
        // fallback: deep-link to the Review page
        if (it.review_id) { window.__pendingReviewId = it.review_id; if (it.timecode != null) window.__pendingReviewSeek = { id: it.review_id, t: Number(it.timecode) || 0 }; }
        (nav.setView || window.__navigate)("review"); return;
      }
      if (it.type === "generation") {
        if (it.is_asset_gen) {
          const k = it.asset_category, id = it.asset_slug;
          if (k && id && window.__openAssetItem) { window.__openAssetItem(k, id); return; }
          (nav.setView || window.__navigate)("generate"); return;
        }
        if (it.shot_id && it.shot_id !== "__ASSET_GEN__" && nav.openShot) { nav.openShot(it.shot_id); return; }
        return;
      }
      if (it.type === "note") {
        if (it.entity_type === "shot" && nav.openShot) { nav.openShot(it.entity_id); return; }
        const k = assetKindOf(it);
        if (k && window.__openAssetItem) { window.__openAssetItem(k, it.entity_id); return; }
      }
    } catch (_) {}
  };

  const removeItem = (it, summaryKey) => {
    setData(d => ({ ...d,
      items: d.items.filter(x => x.id !== it.id),
      summary: { ...d.summary, total_items: Math.max(0, (d.summary.total_items || 1) - 1), [summaryKey]: Math.max(0, (d.summary[summaryKey] || 1) - 1) },
    }));
  };
  const resolveNote = (it) => {
    if (!it.note_id || busy[it.note_id]) return;
    setBusy(b => ({ ...b, [it.note_id]: true }));
    fetcher(`/api/notes/${it.note_id}/resolve`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ resolved: true }) })
      .then(r => { if (r.ok) { removeItem(it, "notes_unresolved"); pingBadge(); } }).catch(() => {})
      .finally(() => setBusy(b => { const n = { ...b }; delete n[it.note_id]; return n; }));
  };
  const resolveReview = (it) => {
    if (!it.comment_id || busy["vc-" + it.comment_id]) return;
    setBusy(b => ({ ...b, ["vc-" + it.comment_id]: true }));
    fetcher(`/api/video-comments/${it.comment_id}/resolve`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ resolved: true }) })
      .then(r => { if (r.ok) { removeItem(it, "reviews_unresolved"); pingBadge(); } }).catch(() => {})
      .finally(() => setBusy(b => { const n = { ...b }; delete n["vc-" + it.comment_id]; return n; }));
  };
  const retryGen = (it) => {
    if (!it.gen_id || busy["gen-" + it.gen_id]) return;
    setBusy(b => ({ ...b, ["gen-" + it.gen_id]: "retry" }));
    fetcher(`/api/generation/${it.gen_id}/retry`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
      .then(r => { if (r.ok) { removeItem(it, "generation_failed"); pingBadge(); } }).catch(() => {})
      .finally(() => setBusy(b => { const n = { ...b }; delete n["gen-" + it.gen_id]; return n; }));
  };
  // v07zz224 — Discard a FAILED generation (soft-cancel + tombstone via DELETE).
  // Only failed gens are discardable (per Hugo: notes/reviews use Resolve only).
  const discardGen = (it) => {
    if (!it.gen_id || busy["gen-" + it.gen_id]) return;
    setBusy(b => ({ ...b, ["gen-" + it.gen_id]: "discard" }));
    fetcher(`/api/generation/${it.gen_id}`, { method: "DELETE" })
      .then(r => { if (r.ok) { removeItem(it, "generation_failed"); pingBadge(); } }).catch(() => {})
      .finally(() => setBusy(b => { const n = { ...b }; delete n["gen-" + it.gen_id]; return n; }));
  };
  // v07zz224 — Ready-to-generate row → open the Generate page in VIDEO mode for
  // that shot (same deep-link the ShotDetailModal "Generate" pill uses).
  const openReady = (it) => {
    try {
      window.__pendingGenerateShotId = it.shot_id || it.id;
      window.__pendingGenerateMode = "video";
      try { window.dispatchEvent(new CustomEvent("paradise-pending-generate-shot")); } catch (_) {}
      ((window.__nav && window.__nav.setView) || window.__navigate)("generate");
    } catch (_) {}
  };

  const relTime = (iso) => { try { const d = new Date(iso); const s = (Date.now() - d.getTime()) / 1000; if (s < 60) return "just now"; if (s < 3600) return Math.floor(s / 60) + "m ago"; if (s < 86400) return Math.floor(s / 3600) + "h ago"; return Math.floor(s / 86400) + "d ago"; } catch (_) { return ""; } };
  const fmtClock = (sec) => { const s = Math.max(0, Math.floor(Number(sec) || 0)); const m = Math.floor(s / 60); return m + ":" + String(s % 60).padStart(2, "0"); };

  const Ico = ({ d }) => <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">{d}</svg>;
  const TYPE_META = {
    note:       { label: "Note",              cls: "note",  icon: <Ico d={<><path d="M4 4h12l4 4v12H4z"/><path d="M8 12h8M8 16h5"/></>}/> },
    review:     { label: "Review comment",    cls: "review", icon: <Ico d={<path d="M21 12a8 8 0 0 1-8 8H7l-4 3V6a3 3 0 0 1 3-3h7a8 8 0 0 1 8 8z"/>}/> },
    generation: { label: "Failed generation", cls: "gen",   icon: <Ico d={<><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></>}/> },
  };
  const READY_ICON = <Ico d={<path d="M13 2 4.5 13H11l-1 9 8.5-11H12z"/>}/>;  // lightning bolt
  // v07zz226 — order + each card focuses its own type when clicked.
  const SUMMARY = [
    { key: "notes_unresolved",   filter: "note",       label: "Open notes",                 cls: "note",   icon: TYPE_META.note.icon },
    { key: "reviews_unresolved", filter: "review",     label: "Unresolved review comments", cls: "review", icon: TYPE_META.review.icon },
    { key: "generation_failed",  filter: "generation", label: "Failed generations",         cls: "gen",    icon: TYPE_META.generation.icon },
    { key: "ready_to_generate",  filter: "ready",      label: "Ready to generate",          cls: "ready",  icon: READY_ICON },
  ];
  const FILTER_LABEL = { note: "Open notes", review: "Unresolved review comments", generation: "Failed generations", ready: "Ready to generate" };
  const shownItems = (filter && filter !== "ready") ? items.filter(it => it.type === filter) : items;
  const toggleFilter = (f) => setFilter(cur => cur === f ? null : f);

  return (
    <section className="view-page view-page--scroll todo-view todo-v2">
      <div className="todo-head">
        <div className="todo-eyebrow">TO-DO</div>
        <div className="todo-title">Needs attention</div>
        <div className="todo-subtitle">{items.length} item{items.length === 1 ? "" : "s"} need your attention across notes, review comments, and generations.</div>
      </div>

      <div className="todo-summary">
        {SUMMARY.map(c => {
          const active = filter === c.filter;
          return (
            <div key={c.key}
              className={"todo-stat todo-stat--" + c.cls + " todo-stat--clickable" + (active ? " is-active" : "")}
              onClick={() => toggleFilter(c.filter)}
              role="button" tabIndex={0}
              title={active ? "Click to show everything" : "Click to focus " + c.label}
              onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleFilter(c.filter); } }}>
              <span className="todo-stat-ico">{c.icon}</span>
              <div className="todo-stat-body">
                <div className="todo-stat-num">{summary[c.key] || 0}</div>
                <div className="todo-stat-label">{c.label}</div>
              </div>
            </div>
          );
        })}
      </div>

      {/* v07zz240 — Milestone readiness: what's left for the next milestone. */}
      {milestone.tally.total > 0 && (() => {
        const STATUSES = window.ASSET_STATUSES || [
          { id: "approved", label: "Approved", dot: "var(--st-hero)" }, { id: "wip", label: "WIP", dot: "var(--st-wip)" },
          { id: "retake", label: "Retake", dot: "var(--st-retake)" }, { id: "first_pass", label: "First Pass", dot: "var(--st-first-pass)" },
        ];
        const t = milestone.tally;
        const order = ["approved", "wip", "retake", "first_pass"];
        const byId = Object.fromEntries(STATUSES.map(s => [s.id, s]));
        const pct = t.total ? Math.round(t.approved / t.total * 100) : 0;
        const urgent = milestone.days != null && milestone.days <= 3;
        return (
          <div className="todo-milestone">
            <div className="todo-ms-head">
              <div className="todo-ms-headl">
                <div className="todo-ms-eyebrow">{milestone.next ? (milestone.next.isPhase ? "NEXT PHASE" : "NEXT MILESTONE") : "ASSET READINESS"}</div>
                <div className="todo-ms-title">{milestone.next ? milestone.next.name : "Asset readiness"}</div>
                {milestone.next && (
                  <div className={"todo-ms-date" + (urgent ? " is-urgent" : "")}>
                    {fmtMsDate(milestone.next.raw)}
                    {milestone.days != null && <span className="todo-ms-days"> · {milestone.days <= 0 ? "due now" : milestone.days + " day" + (milestone.days === 1 ? "" : "s") + " left"}</span>}
                  </div>
                )}
              </div>
              <div className="todo-ms-headr">
                <div className="todo-ms-bignum">{t.approved}<span>/{t.total}</span></div>
                <div className="todo-ms-bigcap">assets approved · {pct}%</div>
              </div>
            </div>
            <div className="todo-ms-bar" title={pct + "% approved"}>
              {order.map(k => { const n = t[k] || 0; if (!n) return null; const s = byId[k] || {}; return <span key={k} className="todo-ms-bar-seg" style={{ width: (n / t.total * 100) + "%", background: s.dot || "var(--grey-6)" }} title={(s.label || k) + ": " + n} />; })}
            </div>
            <div className="todo-ms-legend">
              {order.map(k => { const s = byId[k] || {}; return <span key={k} className="todo-ms-chip"><span className="todo-ms-dot" style={{ background: s.dot }} />{s.label || k} <b>{t[k] || 0}</b></span>; })}
            </div>
            <div className="todo-ms-cats">
              {milestone.perCat.map(c => (
                <div key={c.key} className="todo-ms-cat">
                  <div className="todo-ms-cat-top"><span className="todo-ms-cat-name">{c.label}</span><span className="todo-ms-cat-num">{c.approved}<i>/{c.total}</i></span></div>
                  <div className="todo-ms-cat-bar"><span style={{ width: (c.total ? c.approved / c.total * 100 : 0) + "%" }} /></div>
                </div>
              ))}
            </div>
            {milestone.leftoverChars.length > 0 && (
              <div className="todo-ms-left">
                <div className="todo-ms-left-cap">Characters not yet approved ({milestone.leftoverChars.length})</div>
                <div className="todo-ms-chips">
                  {milestone.leftoverChars.map(c => { const s = byId[c.status] || {}; return (
                    <button key={c.id} type="button" className="todo-ms-leftchip" onClick={() => openAsset("character", c.id)} title={"Status: " + (s.label || c.status) + " — open " + c.name}>
                      <span className="todo-ms-dot" style={{ background: s.dot }} />{c.name}
                    </button>
                  ); })}
                </div>
              </div>
            )}
          </div>
        );
      })()}

      {filter === "ready" ? (
        ready.length === 0 ? (
          <div className="todo-list">
            <div className="todo-filterbar"><span>Showing <b>Ready to generate</b></span><button type="button" className="todo-filter-clear" onClick={() => setFilter(null)}>Show all</button></div>
            <div className="todo-empty">No shots are ready to generate yet — hero a frame on a shot to make it ready to animate.</div>
          </div>
        ) : (
          <div className="todo-list">
            <div className="todo-filterbar"><span>Showing <b>Ready to generate</b> — a hero frame is set and no video exists yet.</span><button type="button" className="todo-filter-clear" onClick={() => setFilter(null)}>Show all</button></div>
            {ready.map(it => {
              const s = shotsById[it.shot_id]; const thumb = s ? shotImg(s) : null;
              return (
                <article key={it.id} className="todo-row todo-row--ready" onClick={() => openReady(it)}>
                  <div className={"todo-row-thumb" + (thumb ? " has-img" : " todo-row-thumb--ready")}
                    style={thumb ? { backgroundImage: `url(${window.thumbUrl ? window.thumbUrl(thumb, 160) : thumb})` } : undefined}>
                    {!thumb && <span className="todo-row-thumb-ico">{READY_ICON}</span>}
                  </div>
                  <div className="todo-row-main">
                    <div className="todo-row-type todo-row-type--ready">Ready to generate</div>
                    <div className="todo-row-title">{it.title}</div>
                    <div className="todo-row-sub">Hero frame set · no video yet</div>
                  </div>
                  <div className="todo-row-actions" onClick={(e) => e.stopPropagation()}>
                    <button type="button" className="todo-btn todo-btn--primary" onClick={() => openReady(it)}>Generate video</button>
                  </div>
                </article>
              );
            })}
          </div>
        )
      ) : loading ? (
        <div className="todo-empty">Loading…</div>
      ) : items.length === 0 ? (
        <div className="todo-empty">✓ Nothing needs your attention — you're caught up.</div>
      ) : (
        <div className="todo-list">
          {filter && <div className="todo-filterbar"><span>Showing <b>{FILTER_LABEL[filter]}</b></span><button type="button" className="todo-filter-clear" onClick={() => setFilter(null)}>Show all</button></div>}
          {shownItems.length === 0 ? (
            <div className="todo-empty">Nothing in this category — you're caught up here.</div>
          ) : shownItems.map(it => {
            const tm = TYPE_META[it.type] || TYPE_META.note;
            const genBusy = it.type === "generation" ? busy["gen-" + it.gen_id] : null;
            const thumb = thumbFor(it);
            const isExpanded = !!expanded[it.id];
            const openLabel = it.type === "review" ? "Open in review" : (it.type === "generation" && it.is_asset_gen) ? "Open" : "Open";
            return (
              <article key={it.id} className={"todo-row todo-row--" + tm.cls} onClick={() => openItem(it)}>
                <div className={"todo-row-thumb" + (thumb ? " has-img" : (" todo-row-thumb--" + tm.cls))}
                  style={thumb ? { backgroundImage: `url(${window.thumbUrl ? window.thumbUrl(thumb, 160) : thumb})` } : undefined}>
                  {!thumb && <span className="todo-row-thumb-ico">{tm.icon}</span>}
                </div>
                <div className="todo-row-main">
                  <div className={"todo-row-type todo-row-type--" + tm.cls}>{tm.label}</div>
                  <div className="todo-row-title">{it.title}{it.type === "review" && it.timecode != null ? ` at ${fmtClock(it.timecode)}` : ""}</div>
                  {it.subtitle && <div className="todo-row-sub">{it.subtitle}</div>}
                  {it.type === "generation" && it.error_detail && (
                    <button type="button" className="todo-link-btn" onClick={(e) => { e.stopPropagation(); setExpanded(p => ({ ...p, [it.id]: !p[it.id] })); }}>{isExpanded ? "Hide details" : "Show details"}</button>
                  )}
                  {it.type === "generation" && isExpanded && it.error_detail && (
                    <pre className="todo-error-detail" onClick={(e) => e.stopPropagation()}>{it.error_detail}</pre>
                  )}
                  <div className="todo-row-meta">
                    {it.author && <span className="todo-row-author">{it.author}</span>}
                    {it.created_at && <span className="todo-row-time">{relTime(it.created_at)}</span>}
                  </div>
                </div>
                <div className="todo-row-actions" onClick={(e) => e.stopPropagation()}>
                  {it.type === "generation" && it.can_retry && (
                    <button type="button" className="todo-btn" disabled={!!genBusy} onClick={() => retryGen(it)}>{genBusy === "retry" ? "…" : "Retry"}</button>
                  )}
                  {it.type === "generation" && (
                    <button type="button" className="todo-btn todo-btn--ghost" disabled={!!genBusy} onClick={() => discardGen(it)}>{genBusy === "discard" ? "…" : "Discard"}</button>
                  )}
                  <button type="button" className="todo-btn todo-btn--primary" onClick={() => openItem(it)}>{openLabel}</button>
                  {it.type === "note" && (
                    <button type="button" className="todo-btn todo-btn--primary" disabled={!!busy[it.note_id]} onClick={() => resolveNote(it)}>{busy[it.note_id] ? "…" : "Resolve"}</button>
                  )}
                  {it.type === "review" && (
                    <button type="button" className="todo-btn todo-btn--primary" disabled={!!busy["vc-" + it.comment_id]} onClick={() => resolveReview(it)}>{busy["vc-" + it.comment_id] ? "…" : "Resolve"}</button>
                  )}
                </div>
              </article>
            );
          })}
          {!filter && shownItems.length > 0 && <div className="todo-caughtup">✓ Nothing else needs your attention — you're caught up.</div>}
        </div>
      )}
    </section>
  );
}
window.TodoPage = TodoPage;
