/* global React */
const { useState, useEffect, useMemo, useRef } = React;

// ===========================================================================
// Router — simple hash router
// ===========================================================================
function useHashRoute() {
  const [route, setRoute] = useState(() => window.location.hash.replace(/^#\/?/, "") || "skills");
  useEffect(() => {
    const onHash = () => setRoute(window.location.hash.replace(/^#\/?/, "") || "skills");
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);
  const parts = route.split("/");
  return { route, page: parts[0], arg: parts[1], setRoute: (r) => { window.location.hash = "#/" + r; } };
}

// ===========================================================================
// Inline expanded skill panel — kept for landing-grid clicks
// ===========================================================================
function SkillExpanded({ skill, sameCategory, onClose, onOpenFull }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  if (!skill) return null;
  return (
    <div className="skill-expanded" data-cat={skill.cat}>
      <div className="skill-expanded-head">
        <div className="skill-expanded-title">
          <span className="skill-icon" style={{fontSize:15}}>{skill.icon}</span>
          <span className="drawer-name">{skill.id}</span>
          <span className={"skill-health h-" + skill.health}/>
          <span className="trigger-chip">
            <span className="mono" style={{fontSize:10, color:"var(--muted-foreground)", marginRight:6}}>TRIGGER</span>
            <span className="mono">{skill.id}</span>
          </span>
        </div>
        <div style={{display:"flex", gap:6, alignItems:"center"}}>
          <button className="btn btn-primary btn-sm" onClick={() => onOpenFull(skill.id)}>
            Open full page <Icon name="arrow-right" size={12}/>
          </button>
          <button className="btn btn-ghost-icon" onClick={onClose} aria-label="Close"><Icon name="x" size={14}/></button>
        </div>
      </div>
      <div className="skill-expanded-blurb">{skill.blurb}</div>
      <div className="skill-expanded-grid">
        <div>
          <div className="se-col-label">Actor → Auditor</div>
          <div className="actor-auditor-mini">
            <div className="aam-cell aam-actor">
              <div className="aam-lbl mono">ACTOR</div>
              <div className="aam-body">state/lib/{skill.id}.ts</div>
            </div>
            <div className="aam-arrow" aria-hidden="true"/>
            <div className="aam-cell aam-auditor">
              <div className="aam-lbl mono">AUDITOR</div>
              <div className="aam-body">{skill.imp > 60 ? "inspect.ts · gemini-3.1-pro" : <em style={{color:"var(--muted-foreground)"}}>no auditor wired</em>}</div>
            </div>
          </div>
        </div>
        <div>
          <div className="se-col-label">Eval (last 20 runs)</div>
          <div className="se-sparkline">
            {Array.from({length:20}).map((_,i) => {
              const h = 30 + ((skill.imp + i * 7) % 70);
              return <div key={i} className="se-spark-bar" style={{height: h+"%"}}/>;
            })}
          </div>
          <div style={{marginTop:10, fontSize:12, color:"var(--muted-foreground)"}}>
            <span className="mono" style={{color:"oklch(0.62 0.15 145)", fontSize:13}}>● mean 0.92</span>
            <span className="mono" style={{marginLeft:10}}>last run 20h ago</span>
          </div>
        </div>
        <div>
          <div className="se-col-label">Same category</div>
          <div style={{display:"flex", gap:6, flexWrap:"wrap", marginTop:6}}>
            {sameCategory.slice(0,8).map(s => <span key={s.id} className="se-chip">{s.id}</span>)}
          </div>
        </div>
      </div>
    </div>
  );
}

// ===========================================================================
// Skill detail page
// ===========================================================================
function SkillDetailPage({ id, setRoute }) {
  const data = window.SNAPPY_DATA;
  const skill = data.skills.find(s => s.id === id);
  const [section, setSection] = useState("at-a-glance");
  const [auditOpen, setAuditOpen] = useState(false);
  const [auditFindings, setAuditFindings] = useState([]);
  if (!skill) {
    return <div className="container section"><p>Skill "{id}" not found. <a className="footer-link" href="#/skills">Back to catalog</a></p></div>;
  }

  // derive harness state from skill shape
  const hasAuditor = skill.imp > 60;
  const harness = [
    { k:"ACTOR", state:"present",
      body: <>state/lib/{skill.id}.ts — primary export runs on trigger.</> },
    { k:"AUDITOR", state: hasAuditor ? "present" : "not present",
      body: hasAuditor
        ? <>state/lib/{skill.id}/inspect.ts — gemini-3.1-pro with responseJsonSchema.</>
        : <>Eval is manual — operator reviews rows in <code>pending-eval.ndjson</code>.</> },
    { k:"LOOP", state:"present",
      body: <>Loader body regenerated on self-heal turn via UserPromptSubmit.</> },
    { k:"DRAIN", state: skill.evals ? "present" : "inferred",
      body: <>regen-pending → <code>pid-drain.ts</code> → POST /_push.</> },
    { k:"EVAL LOG", state: skill.evals ? "present" : "not applicable",
      body: skill.evals
        ? <>Row per run in <code>state/log/evals.ndjson</code>.</>
        : <>No eval rows yet — this skill hasn't run on the current box.</> },
  ];
  const present = harness.filter(h => h.state === "present").length;

  // pipeline — use real-looking step names per category
  const pipelineSteps = pipelineFor(skill);

  const toc = [
    { group: "SECTIONS", items: [
      { id: "findings", label: "findings" },
      { id: "at-a-glance", label: "at a glance" },
      { id: "harness", label: "harness" },
      { id: "pipeline", label: "pipeline" },
      { id: "skill-md", label: "SKILL.md" },
      { id: "agents-md", label: "AGENTS.md" },
      { id: "api-ts", label: "api.ts" },
      { id: "scripts", label: "scripts" },
      { id: "last-run", label: "last run" },
      { id: "eval-contract", label: "eval contract" },
      { id: "export", label: "export" },
    ]},
    { group: "IN SKILL.MD", items: [
      { id: "steps", label: "Steps" },
      { id: "eval", label: "Eval" },
      { id: "gotchas", label: "Gotchas" },
      { id: "examples", label: "Examples" },
    ]},
  ];
  const jump = (sid) => {
    setSection(sid);
    const el = document.getElementById("sec-"+sid);
    if (el) window.scrollTo({ top: el.offsetTop - 80, behavior: "smooth" });
  };

  const runAudit = () => {
    setAuditOpen(true);
    setAuditFindings([]);
    const sample = [
      { sev:"info",  area:"frontmatter", msg:`trigger phrase "${skill.id.replace(/-/g," ")}" is ≤ 3 tokens — OK.` },
      { sev: hasAuditor ? "info" : "warn", area:"actor-auditor",
        msg: hasAuditor
          ? `Actor (${skill.id}.ts) and Auditor (inspect.ts) use different models — invariant holds.`
          : `No auditor wired. This skill cannot self-grade; every run requires a human row.` },
      { sev:"info",  area:"depends_on", msg:`settings declared; 1 credential read via env("${skill.id.toUpperCase().replace(/-/g,"_")}_KEY").` },
      { sev: skill.evals ? "info" : "warn", area:"eval-log",
        msg: skill.evals
          ? `Last 7 days: ${skill.evals} rows. Shape-check passing.`
          : `No eval rows in last 7 days. Runnable but unverified.` },
      { sev:"info",  area:"loader-size", msg:`agents.md body is ~${(skill.imp/20|0)+1} KB — under the 4 KB splice budget.` },
    ];
    sample.forEach((f, i) => setTimeout(() => setAuditFindings(arr => [...arr, f]), 180 * (i+1)));
  };

  return (
    <div className="detail-page">
      <aside className="detail-toc">
        {toc.map(g => (
          <div key={g.group} className="toc-group">
            <div className="toc-group-label">{g.group}</div>
            {g.items.map(it => (
              <button key={it.id} className={"toc-item" + (section===it.id?" active":"")} onClick={()=>jump(it.id)}>{it.label}</button>
            ))}
          </div>
        ))}
      </aside>

      <main className="detail-main">
        <div className="detail-head">
          <div>
            <div className="detail-meta-chips">
              <span className={"cat-chip cat-" + skill.cat}>{skill.cat}</span>
              <span className="tier-chip">personal</span>
              <span className="muted mono" style={{fontSize:11.5}}>2 files</span>
              <span className="muted mono" style={{fontSize:11.5}}>{skill.evals ? `${skill.evals.split("/")[1]} recent evals` : "no evals"}</span>
              <span className="trigger-chip"><span className="mono" style={{fontSize:10, color:"var(--muted-foreground)", marginRight:6}}>TRIGGER</span><span className="mono">{skill.id.replace(/-/g," ")}</span></span>
            </div>
            <h1 className="detail-title mono">{skill.id}</h1>
            <p className="detail-blurb">{skill.blurb}</p>
          </div>
          <div style={{display:"flex", gap:8, flexShrink:0}}>
            <button className="btn btn-outline btn-sm" onClick={()=>setRoute("skills")}>
              <Icon name="arrow-right" size={12} style={{transform:"rotate(180deg)"}}/> Back
            </button>
            <button className="btn btn-primary btn-sm" onClick={()=>jump("export")}>
              <Icon name="copy" size={12}/> Export
            </button>
          </div>
        </div>

        {/* AI audit card */}
        <section id="sec-findings" className="audit-card">
          <div className="audit-head">
            <span className="audit-icon accent">✦</span>
            <div>
              <div className="audit-title">AI audit</div>
              <div className="audit-sub mono">claude-sonnet-4.6 · read-only structural review</div>
            </div>
            <button className="btn btn-primary btn-sm" style={{marginLeft:"auto"}} onClick={runAudit}>
              {auditOpen ? "Re-run audit" : "Run audit"}
            </button>
          </div>
          <p className="audit-body">
            Reads SKILL.md + AGENTS.md + the harness graph and streams a JSONL report. Read-only — no files touched.
            Flags missing auditors, broken refs, loader bloat, and weak eval coverage. Uses your OpenRouter key.
          </p>
          {auditOpen && (
            <div className="audit-findings">
              {auditFindings.length === 0 && <div className="muted" style={{fontSize:12.5}}>Streaming findings…</div>}
              {auditFindings.map((f, i) => (
                <div key={i} className={"finding finding-" + f.sev}>
                  <span className="finding-sev mono">{f.sev.toUpperCase()}</span>
                  <span className="finding-area mono">{f.area}</span>
                  <span className="finding-msg">{f.msg}</span>
                </div>
              ))}
            </div>
          )}
        </section>

        {/* at a glance */}
        <section id="sec-at-a-glance" className="detail-section">
          <div className="detail-section-head">
            <span className="eyebrow accent mono">at a glance</span>
            <span className="muted mono" style={{fontSize:11}}>— PARSED FROM {skill.id.toUpperCase()}.MD</span>
          </div>
          <table className="glance-table">
            <tbody>
              <tr>
                <td className="gt-label">ACTOR</td><td className="mono" style={{fontSize:12}}>state/lib/{skill.id}.ts</td>
                <td className="gt-label">AUDITOR</td><td className="mono" style={{fontSize:12}}>{hasAuditor ? "inspect.ts · gemini-3.1-pro" : <em style={{color:"var(--muted-foreground)"}}>not wired</em>}</td>
                <td className="gt-label">EVAL MODE</td><td>{skill.evals ? "auto" : "manual"}</td>
              </tr>
              <tr>
                <td className="gt-label">CATEGORY</td><td style={{textTransform:"capitalize"}}>{skill.cat}</td>
                <td className="gt-label">STAGES</td><td>{pipelineSteps.length}</td>
                <td className="gt-label">DEPENDS ON</td>
                <td>
                  <span className="se-chip">settings</span>
                  {skill.cat === "integrations" && <span className="se-chip" style={{marginLeft:4}}>openrouter</span>}
                  {skill.cat === "content" && <span className="se-chip" style={{marginLeft:4}}>image</span>}
                </td>
              </tr>
            </tbody>
          </table>
        </section>

        {/* HARNESS */}
        <section id="sec-harness" className="detail-section detail-bordered">
          <div className="detail-section-head">
            <span className="eyebrow accent mono">harness</span>
            <span className="muted mono" style={{fontSize:11}}>— THE OPERATING FRAME EVERY SNAPPY-OS SKILL RUNS INSIDE</span>
            <span className={"harness-badge mono pres-" + (present >= 4 ? "hi" : present >= 2 ? "mid" : "lo")} style={{marginLeft:"auto"}}>
              {present}/5 PRESENT
            </span>
          </div>
          <p className="detail-body">
            Every skill ships with the same five-part frame. An <b>actor</b> produces, a <i>different</i> <b>auditor</b> grades
            (<code>program.md §5</code>), a <b>self-correcting loop</b> heals loader gaps on every turn, a <b>regen drain</b> rewrites
            loaders asynchronously, and every run lands a row in the <b>eval log</b>. Absence is taught — greyed cells are load-bearing.
          </p>
          <div className="harness-grid">
            {harness.map((x, i) => {
              const cell = (
                <div className={"harness-cell v-" + x.state.replace(/\s/g,"-")}>
                  <div className="hc-label mono">{x.k}</div>
                  <div className="hc-state mono">{x.state}</div>
                  <div className="hc-body">{x.body}</div>
                </div>
              );
              if (i === 0) {
                return (
                  <React.Fragment key={x.k}>
                    {cell}
                    <AuditorArrow hasAuditor={hasAuditor}/>
                  </React.Fragment>
                );
              }
              return <React.Fragment key={x.k}>{cell}</React.Fragment>;
            })}
          </div>
          <div className="harness-foot mono">
            actor ≠ auditor — whatever produces output cannot also grade it.
          </div>
        </section>

        {/* PIPELINE */}
        <section id="sec-pipeline" className="detail-section detail-bordered">
          <div className="detail-section-head">
            <span className="eyebrow accent mono">pipeline</span>
            <span className="muted mono" style={{fontSize:11}}>— ACTUAL STEP NAMES FROM ## Steps</span>
          </div>
          <div className="pipeline-flow">
            {pipelineSteps.map((s, i) => (
              <React.Fragment key={i}>
                <div className={"pipe-pill pipe-" + s.kind}>
                  <div className="pipe-pill-kind mono">{s.kind}</div>
                  <div className="pipe-pill-name">{s.name}</div>
                </div>
                {i < pipelineSteps.length - 1 && <span className="pipe-arrow">→</span>}
              </React.Fragment>
            ))}
          </div>
          {hasAuditor && (
            <div className="pipeline-loop-hint mono">
              ↻ auditor fails → loop back to actor (max 3 iterations) · passes → emit eval row
            </div>
          )}
        </section>

        {/* Code panels */}
        {["skill-md","agents-md","api-ts","scripts","last-run","eval-contract"].map(sid => (
          <section key={sid} id={"sec-"+sid} className="detail-section detail-bordered">
            <div className="detail-section-head"><span className="eyebrow accent mono">{sid.replace("-",".")}</span></div>
            <pre className="detail-code">{sampleFor(sid, skill, hasAuditor)}</pre>
          </section>
        ))}

        {/* Export panel */}
        <section id="sec-export" className="detail-section detail-bordered export-panel">
          <div className="detail-section-head">
            <span className="eyebrow accent mono">export</span>
            <span className="muted mono" style={{fontSize:11}}>— PORTABLE ARTIFACT</span>
          </div>
          <div className="export-grid">
            <div>
              <button className="btn btn-primary btn-lg" style={{width:"100%"}}>
                <Icon name="copy" size={14}/> Download {skill.id}.zip
              </button>
              <div className="export-meta mono">
                14.8 KB · sha256 <span style={{color:"var(--muted-foreground)"}}>a1b2c3d4…</span>
              </div>
              <p className="muted" style={{fontSize:11.5, lineHeight:1.55, margin:"10px 0 0"}}>
                Companion files declared in frontmatter <code>depends_on</code> are <b>not</b> bundled — bring your own credentials and your own <code>state/lib/*</code> modules. Read <code>MANIFEST.json</code> for the full list.
              </p>
            </div>
            <div>
              <div className="se-col-label">Bundle manifest</div>
              <pre className="detail-code" style={{margin:0, fontSize:11}}>{`${skill.id}/
  SKILL.md
  AGENTS.md
  bin/${skill.id}/
    run.sh
    smoke.sh
  lib/${skill.id}.ts${hasAuditor ? `
  lib/${skill.id}/inspect.ts` : ""}
  MANIFEST.json
  README.md      (auto-generated)`}</pre>
            </div>
          </div>
        </section>
      </main>
    </div>
  );
}

function AuditorArrow({ hasAuditor }) {
  if (hasAuditor) {
    return (
      <div className="harness-arrow harness-arrow-solid" aria-hidden="true">
        <div className="hva-track"/>
        <div className="hva-head">▸</div>
      </div>
    );
  }
  return (
    <div className="harness-arrow harness-arrow-broken" aria-hidden="true">
      <div className="hva-track hva-track-dashed"/>
      <div className="hva-caption mono">no auditor wired</div>
    </div>
  );
}

function pipelineFor(skill) {
  // Return concrete-looking step names by category.
  if (skill.cat === "content" && skill.id === "image") {
    return [
      { kind:"actor",   name:"generate-iterate.sh" },
      { kind:"auditor", name:"inspect.ts" },
      { kind:"gate",    name:"score ≥ 0.8?" },
      { kind:"emit",    name:"publish + eval row" },
    ];
  }
  if (skill.cat === "channels" || skill.cat === "content") {
    return [
      { kind:"actor", name:`${skill.id}.run()` },
      { kind:"auditor", name:"voice.checkTone()" },
      { kind:"emit", name:"evals.ndjson" },
      { kind:"push", name:"POST /_push" },
    ];
  }
  if (skill.cat === "orchestrator" || skill.cat === "core" || skill.cat === "system") {
    return [
      { kind:"read", name:"program.md" },
      { kind:"read", name:"state/index.md" },
      { kind:"actor", name:`${skill.id}.run()` },
      { kind:"emit", name:"evals.ndjson" },
      { kind:"push", name:"POST /_push" },
    ];
  }
  return [
    { kind:"actor", name:`${skill.id}.run()` },
    { kind:"emit", name:"evals.ndjson" },
    { kind:"push", name:"POST /_push" },
  ];
}

function sampleFor(sid, skill, hasAuditor) {
  if (sid === "skill-md") return `---
trigger: ${skill.id.replace(/-/g," ")}
category: ${skill.cat}
status: live
depends_on: [settings${skill.cat === "integrations" ? ", openrouter" : ""}]
---
# ${skill.id}

${skill.blurb}

## Steps
1. Read program.md (PID contract).
2. Splice state/skills/${skill.id}.agents.md into system prompt.
3. Run state/lib/${skill.id}.ts primary export.
${hasAuditor ? `4. Grade with inspect.ts (gemini-3.1-pro).\n5. Append eval row; loop if score < 0.8.` : `4. Append eval row (manual review).`}

## Gotchas
- Actor ≠ auditor. Do not grade with the same model that produced.
- Loader body must stay under 4 KB (splice budget).
`;
  if (sid === "agents-md") return `---
description: "Triggers on prompt mention of '${skill.id.replace(/-/g," ")}'."
inject: [program.md, state/index.md]
---
When the user asks for ${skill.id.replace(/-/g," ")}, prefer state/lib/${skill.id}.ts
over rolling a fresh client. Credentials via env("KEY") only.

ALWAYS: actor ≠ auditor. Never let the writer grade the write.
`;
  if (sid === "api-ts") return `// state/lib/${skill.id}.ts
export async function ${camel(skill.id)}(input: ${camel(skill.id)}Input): Promise<${camel(skill.id)}Output> {
  const key = env("${skill.id.toUpperCase().replace(/-/g,"_")}_KEY"); // throws if unset
  // …
}
`;
  if (sid === "scripts") return `state/bin/${skill.id}/run.sh
state/bin/${skill.id}/smoke.sh${hasAuditor ? `
state/bin/${skill.id}/generate-iterate.sh` : ""}`;
  if (sid === "last-run") return `{
  "ts": "2026-04-21T18:32:01Z",
  "skill": "${skill.id}",
  "status": "pass",
  "ms": 412,
  "actor_model": "${skill.cat === "content" ? "nano-banana-2" : "gpt-5"}",
  "auditor_model": "${hasAuditor ? "gemini-3.1-pro" : "null (manual)"}",
  "score": 0.94
}`;
  return `// eval.contract.ts
export const contract = {
  mode: "${skill.evals ? "auto" : "manual"}",
  shape: { id: "string", status: "pass|fail", ms: "number", score: "number" },
  threshold: 0.8,
};`;
}
function camel(s) { return s.split("-").map((w,i) => i===0 ? w : w[0].toUpperCase()+w.slice(1)).join(""); }

// ===========================================================================
// AI loader-composer page
// ===========================================================================
function AIPage() {
  const data = window.SNAPPY_DATA;
  const S = window.useSnappyState();
  const [prompt, setPrompt] = useState("");
  const [messages, setMessages] = useState(() => {
    try { return JSON.parse(localStorage.getItem("snappy_ai_chat") || "[]"); } catch(e){ return []; }
  });
  const [showComposed, setShowComposed] = useState(false);
  const [model, setModel] = useState("claude-haiku-4-5");
  const [running, setRunning] = useState(false);
  const [mode, setMode] = useState("chat"); // "chat" | "compose-skill"
  const models = ["claude-haiku-4-5", "claude-sonnet-4-6", "claude-opus-4-7", "gpt-5", "gemini-3.1-pro"];

  useEffect(() => {
    try { localStorage.setItem("snappy_ai_chat", JSON.stringify(messages.slice(-24))); } catch(e){}
  }, [messages]);

  const matched = useMemo(() => {
    const text = prompt.toLowerCase();
    if (!text.trim()) return [];
    return data.skills.filter(s =>
      text.includes(s.id.replace(/-/g," ")) ||
      text.includes(s.id) ||
      s.id.split("-").some(w => w.length > 3 && text.includes(w))
    ).slice(0, 6);
  }, [prompt, data.skills]);

  const run = async () => {
    if (!prompt.trim() || running) return;
    const user = prompt.trim();
    const usedSkills = matched.map(m => m.id);
    const composed = matched.map(s =>
      `<skill-context name="${s.id}">\n  ${s.blurb}\n  PREFER state/lib/${s.id}.ts. Actor ≠ Auditor.\n</skill-context>`
    ).join("\n\n");
    setMessages(m => [...m, { role:"user", text:user, skills: usedSkills }]);
    setPrompt("");
    setRunning(true);
    const systemPrompt = mode === "compose-skill"
      ? `You are the Snappy skill composer. Given a user intent, output a single SKILL.md with YAML frontmatter (trigger, category, env_keys, depends_on) and sections ## Steps, ## Gotchas, ## Examples. Markdown only, no preamble.`
      : `You are embedded inside the Snappy runtime. The following skill loaders are spliced in for this turn:\n\n${composed || "(no matching loaders)"}\n\nRespond to the user. Be concise. If you recommend an action that needs an env key, name it.`;
    try {
      const reply = await window.claude.complete({
        messages: [
          { role:"user", content: `[system]\n${systemPrompt}\n\n[user]\n${user}` },
        ],
      });
      setMessages(m => [...m, { role:"assistant", text:reply, skills:usedSkills, mode }]);
    } catch (err) {
      setMessages(m => [...m, { role:"assistant", text:"(error — "+(err?.message||err)+")", skills:[], error:true }]);
    }
    setRunning(false);
  };

  const saveSkillFromMessage = (text) => {
    try {
      const sk = S.parseMarkdownSkill(text, ("ai-" + Date.now()) + ".md");
      S.installSkill(sk);
      alert("Saved skill: " + sk.id + " — open Settings to fill env keys.");
    } catch(e) { alert("Could not parse as a skill: " + e.message); }
  };

  const clear = () => { setMessages([]); try { localStorage.removeItem("snappy_ai_chat"); } catch(e){} };

  return (
    <div className="container section" style={{maxWidth:960, margin:"0 auto"}}>
      <div style={{paddingBottom:12}}>
        <h1 className="h1" style={{margin:"0 0 8px", fontSize:"32px"}}>
          AI lens <span className="eyebrow-pill">LOADER-COMPOSER · LIVE CHAT</span>
        </h1>
        <p className="lead" style={{maxWidth:720, margin:0, fontSize:14}}>
          Chat with the runtime. Trigger phrases in your message splice matching <code>.agents.md</code> loaders into the system prompt.
          Ask it to write a new skill, and save the reply straight into your catalog.
        </p>
      </div>

      <div className="ai-chat-card">
        <div className="ai-chat-head">
          <div className="ai-tabs">
            <button className={"ai-tab" + (mode==="chat"?" active":"")} onClick={()=>setMode("chat")}>Chat</button>
            <button className={"ai-tab" + (mode==="compose-skill"?" active":"")} onClick={()=>setMode("compose-skill")}>Compose skill</button>
          </div>
          <select className="input btn-sm" style={{height:28, width:160, fontSize:12}} value={model} onChange={e=>setModel(e.target.value)}>
            {models.map(m => <option key={m} value={m}>{m}</option>)}
          </select>
          <button className="btn btn-ghost-icon btn-sm" onClick={clear} title="clear chat" style={{marginLeft:"auto"}}>
            <Icon name="x" size={13}/>
          </button>
        </div>

        <div className="ai-chat-stream">
          {messages.length === 0 && (
            <div className="ai-chat-empty">
              <div className="mono eyebrow accent" style={{fontSize:11, marginBottom:6}}>EMPTY SESSION</div>
              <div style={{fontSize:13, color:"var(--muted-foreground)", maxWidth:520}}>
                {mode === "chat"
                  ? "Ask the runtime something. Any trigger phrase you use will light up the relevant loader below."
                  : "Describe a workflow. The composer returns a SKILL.md you can save straight into the catalog."}
              </div>
              <div className="ai-suggestions">
                {(mode === "chat"
                  ? ["summarize my last meeting and drop it in notion",
                     "draft a slack message to the team about tomorrow's deploy",
                     "what skills would fire if I said 'audit my stripe mrr'?"]
                  : ["a skill that posts daily LinkedIn updates from my drafts",
                     "a skill that watches GitHub for failing CI and pings Slack",
                     "a skill that summarizes Gmail threads into Airtable"]
                ).map(s => (
                  <button key={s} className="ai-suggestion" onClick={()=>setPrompt(s)}>{s}</button>
                ))}
              </div>
            </div>
          )}
          {messages.map((m, i) => (
            <div key={i} className={"ai-msg ai-msg-" + m.role}>
              <div className="ai-msg-meta mono">
                {m.role === "user" ? "you" : (m.mode === "compose-skill" ? "composer · " + model : "runtime · " + model)}
                {m.skills && m.skills.length > 0 && (
                  <span style={{marginLeft:8}}>
                    {m.skills.map(s => <span key={s} className="ai-msg-skill">{s}</span>)}
                  </span>
                )}
              </div>
              <div className="ai-msg-body">
                {m.role === "assistant" && m.mode === "compose-skill"
                  ? <pre className="detail-code" style={{margin:0, maxHeight:320, overflow:"auto"}}>{m.text}</pre>
                  : <div style={{whiteSpace:"pre-wrap", fontSize:13.5, lineHeight:1.55}}>{m.text}</div>
                }
                {m.role === "assistant" && m.mode === "compose-skill" && m.text.includes("---") && (
                  <div style={{marginTop:8, display:"flex", gap:6}}>
                    <button className="btn btn-primary btn-sm" onClick={()=>saveSkillFromMessage(m.text)}>
                      <Icon name="plus" size={12}/> Save to catalog
                    </button>
                    <button className="btn btn-outline btn-sm" onClick={()=>navigator.clipboard.writeText(m.text)}>
                      <Icon name="copy" size={12}/> Copy
                    </button>
                  </div>
                )}
              </div>
            </div>
          ))}
          {running && (
            <div className="ai-msg ai-msg-assistant">
              <div className="ai-msg-meta mono">runtime · {model}</div>
              <div className="ai-msg-body"><span className="caret">▌</span> thinking…</div>
            </div>
          )}
        </div>

        {matched.length > 0 && (
          <div className="ai-loader-preview">
            <span className="mono eyebrow accent" style={{fontSize:10.5}}>WILL SPLICE {matched.length}:</span>
            {matched.map(s => (
              <span key={s.id} className="ai-loader-chip" title={s.blurb}>
                <span className="skill-icon" style={{fontSize:10, marginRight:3}}>{s.icon}</span>
                {s.id}
              </span>
            ))}
            <button className="btn btn-ghost-icon btn-sm" onClick={()=>setShowComposed(v=>!v)} style={{marginLeft:"auto"}} title="preview composed prompt">
              <Icon name={showComposed?"x":"copy"} size={12}/>
            </button>
          </div>
        )}

        {showComposed && matched.length > 0 && (
          <pre className="detail-code ai-composed-preview">
{matched.map(s =>
`<skill-context name="${s.id}">
  ${s.blurb}
  PREFER state/lib/${s.id}.ts. Actor ≠ Auditor.
</skill-context>`).join("\n\n")}

<user>
${prompt}
</user>`}
          </pre>
        )}

        <div className="ai-compose">
          <textarea className="composer-input" rows={2} value={prompt}
                    placeholder={mode === "chat" ? "Ask the runtime something…" : "Describe the skill you want…"}
                    onChange={e=>setPrompt(e.target.value)}
                    onKeyDown={e => {
                      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); run(); }
                    }}/>
          <button className="btn btn-primary" onClick={run} disabled={running || !prompt.trim()}>
            <Icon name="play" size={12}/> {running ? "…" : mode === "chat" ? "Send" : "Compose"}
          </button>
        </div>
        <div className="ai-hint mono">⌘↵ to send · chat + composed prompt stay in your browser</div>
      </div>
    </div>
  );
}

// ===========================================================================
// Settings page
// ===========================================================================
function SettingsPage() {
  const S = window.useSnappyState();
  const [tier, setTier] = useState("admin");
  const [filter, setFilter] = useState("all");
  const [query, setQuery] = useState("");
  const [dotenvOpen, setDotenvOpen] = useState(false);
  const [dotenvText, setDotenvText] = useState("");
  const [pasteFlash, setPasteFlash] = useState([]);

  // Build provider rows from SNAPPY_STATE.env (live) — each key is a row.
  // Merge canonical metadata (provider name, group, skills) with any
  // user-installed skill env keys that may not be in the canonical list.
  const rowsFor = (group) => {
    const out = [];
    Object.entries(S.env).forEach(([key, entry]) => {
      const canon = S.CANONICAL_ENV[key];
      // derive metadata for non-canonical keys (from installed skills)
      if (!canon) {
        const owner = S.installedSkills.find(sk => (sk.env_keys||[]).includes(key));
        if (!owner) return; // orphan
        if (group !== "integrations") return; // non-canonical keys live under "integrations"
        const users = S.usersOfKey(key);
        out.push({
          id: key.toLowerCase(),
          name: owner.id.replace(/-/g," ").replace(/\b\w/g, c=>c.toUpperCase()),
          key,
          state: entry.value ? entry.state : "unset",
          skills: users,
          value: entry.value,
          fromIntegration: owner.fromIntegration || owner.id,
        });
        return;
      }
      if (canon.group !== group) return;
      out.push({
        id: canon.provider.toLowerCase().replace(/\s+/g,"-"),
        name: canon.provider,
        key,
        state: entry.value ? entry.state : "unset",
        skills: S.usersOfKey(key),
        value: entry.value,
      });
    });
    return out.sort((a,b) => a.name.localeCompare(b.name));
  };
  const ai = rowsFor("ai");
  const platforms = rowsFor("platforms");
  const infra = rowsFor("infra");
  const integrations = rowsFor("integrations"); // user-installed skill keys
  const all = [...ai, ...platforms, ...infra, ...integrations];

  const count = (st) => all.filter(x => x.state === st).length;
  const coverage = {
    set: count("live") + count("stale") + count("failing"),
    live: count("live"),
    stale: count("stale"),
    unset: count("unset"),
  };

  const match = (it) => {
    if (filter !== "all") {
      if (filter === "unset" && it.state !== "unset") return false;
      if (filter === "ai" && !ai.includes(it)) return false;
      if (filter === "platforms" && !platforms.includes(it)) return false;
      if (filter === "infra" && !infra.includes(it)) return false;
      if (filter === "integrations" && !integrations.includes(it)) return false;
    }
    if (!query) return true;
    const q = query.toLowerCase();
    return it.name.toLowerCase().includes(q) || it.key.toLowerCase().includes(q);
  };

  const doPaste = () => {
    const added = S.pasteDotenv(dotenvText);
    setPasteFlash(added);
    setDotenvText("");
    setDotenvOpen(false);
    setTimeout(()=>setPasteFlash([]), 2800);
  };

  return (
    <div className="container section" style={{maxWidth:1120, margin:"0 auto"}}>
      <h1 className="h1" style={{margin:"0 0 8px", fontSize:"32px"}}>Settings</h1>
      <p className="lead" style={{maxWidth:740, margin:"0 0 28px", fontSize:14}}>
        Settings is the environment file of Snappy. Every value on this page is a named variable a skill reads via <code>env("KEY")</code>.
        Missing values throw; fallbacks are forbidden.
      </p>

      <div className="settings-hero">
        <div className="settings-identity">
          <div style={{display:"flex", alignItems:"center", gap:12, marginBottom:10}}>
            <div className="avatar">R</div>
            <div>
              <div style={{fontWeight:600}}>robert</div>
              <div className="muted mono" style={{fontSize:12}}>role: {tier}</div>
            </div>
          </div>
          <p className="muted" style={{fontSize:12.5, lineHeight:1.55, margin:"0 0 10px"}}>
            {tier === "master"    && "Master can rotate keys and rebuild the catalog."}
            {tier === "admin"     && "Admin can edit their own tenant's credentials."}
            {tier === "member"    && "Member can read the catalog and compose loader previews."}
            {tier === "anonymous" && "Anonymous can only view public skills."}
          </p>
          <ul className="mini-list">
            <li><Icon name="check" size={12}/> Writes go through the gateway's SETTINGS_STORE KV</li>
            <li><Icon name="check" size={12}/> Keys never reach the browser — tests are server-side probes</li>
            <li><Icon name="check" size={12}/> Scoped to tenant ID; no cross-tenant read</li>
          </ul>
        </div>
        <div className="settings-env-tab">
          <div className="mono" style={{fontSize:11, color:"var(--muted-foreground)", letterSpacing:".08em"}}>NON-SECRET ENV</div>
          <pre className="detail-code" style={{marginTop:8, fontSize:11, maxHeight:180, overflow:"auto"}}>
{`ENV=production
GATEWAY=skills.snappy.ai
REGION=tor1
TENANT=robert
CATALOG=/.well-known/skills/index.json
# secrets never render here`}</pre>
        </div>
      </div>

      {/* tier strip — the four valid tiers */}
      <div className="tier-tabs">
        {[
          {k:"anonymous", cap:"Public skills only"},
          {k:"member",    cap:"Read + compose"},
          {k:"admin",     cap:"Edit own creds"},
          {k:"master",    cap:"Rotate + rebuild"},
        ].map(t => (
          <button key={t.k} className={"tier-tab" + (tier===t.k?" active":"")} onClick={()=>setTier(t.k)}>
            <div className="mono" style={{fontSize:11, color:"var(--muted-foreground)", textTransform:"uppercase"}}>{t.k}</div>
            <div style={{fontSize:12.5, marginTop:2}}>{t.cap}</div>
          </button>
        ))}
      </div>

      {/* SOT ribbon — 3 cells, numbered */}
      <div className="sot-banner">
        <div style={{display:"flex", alignItems:"baseline", gap:10, flexWrap:"wrap"}}>
          <span className="eyebrow accent mono">WHERE DO CREDENTIALS ACTUALLY LIVE?</span>
          <span className="muted" style={{fontSize:12}}>Three places, depending on which layer is reading.</span>
        </div>
        <div className="sot-grid">
          <div className="sot-cell">
            <div className="sot-num mono">1</div>
            <div className="sot-eyebrow mono">CANONICAL · local agent runtime</div>
            <div className="mono" style={{fontSize:12.5, fontWeight:600}}>~/projects/snappy-os/.env.cache</div>
            <div className="muted" style={{fontSize:12, lineHeight:1.5}}>Read by <code>env("KEY")</code> when Claude Code, Codex, or openclaw runs a skill locally. Git-ignored. You edit this by hand.</div>
          </div>
          <div className="sot-cell">
            <div className="sot-num mono">2</div>
            <div className="sot-eyebrow mono">PER-TENANT · gateway runtime</div>
            <div className="mono" style={{fontSize:12.5, fontWeight:600}}>SETTINGS_STORE KV (Cloudflare)</div>
            <div className="muted" style={{fontSize:12, lineHeight:1.5}}>Read by the worker when a tenant's request reaches this page or the BYO-key AI proxy. Scoped to your tenant ID.</div>
          </div>
          <div className="sot-cell">
            <div className="sot-num mono">3</div>
            <div className="sot-eyebrow mono">BACK-COMPAT · symlink</div>
            <div className="mono" style={{fontSize:12.5, fontWeight:600}}>~/.claude/skills/snappy-settings/.env.cache</div>
            <div className="muted" style={{fontSize:12, lineHeight:1.5}}>→ points at canonical. Legacy kernel code looked here. Do not flip the symlink direction.</div>
          </div>
        </div>
      </div>

      {/* coverage tiles */}
      <div className="coverage-row">
        <button className={"coverage-tile" + (filter==="all"?" active":"")} onClick={()=>setFilter("all")}>
          <div className="ct-num mono">{coverage.set}</div>
          <div className="ct-lbl mono">SET</div>
          <div className="muted" style={{fontSize:11}}>of {all.length} total</div>
        </button>
        <button className={"coverage-tile ct-live" + (filter==="all"?"":"")} onClick={()=>setFilter("all")}>
          <div className="ct-num mono">{coverage.live}</div>
          <div className="ct-lbl mono">LIVE</div>
          <div className="muted" style={{fontSize:11}}>last-tested passed</div>
        </button>
        <button className="coverage-tile ct-stale">
          <div className="ct-num mono">{coverage.stale}</div>
          <div className="ct-lbl mono">STALE</div>
          <div className="muted" style={{fontSize:11}}>&gt; 24h since last probe</div>
        </button>
        <button className={"coverage-tile ct-unset" + (filter==="unset"?" active":"")} onClick={()=>setFilter("unset")}>
          <div className="ct-num mono">{coverage.unset}</div>
          <div className="ct-lbl mono">UNSET</div>
          <div className="muted" style={{fontSize:11}}>no value stored</div>
        </button>
      </div>

      {/* filter toolbar + paste dotenv */}
      <div className="settings-toolbar">
        <div className="search" style={{maxWidth:320}}>
          <span className="search-icon"><Icon name="search" size={14}/></span>
          <input className="search-input" placeholder="Filter credentials…" value={query} onChange={e=>setQuery(e.target.value)}/>
        </div>
        <div className="lens-group">
          {["all","ai","platforms","infra","integrations","unset"].map(f => (
            <button key={f} className={"lens-btn" + (filter===f?" active":"")} onClick={()=>setFilter(f)}>
              {f}
            </button>
          ))}
        </div>
        <button className="btn btn-outline btn-sm" onClick={()=>setDotenvOpen(v => !v)} style={{marginLeft:"auto"}}>
          <Icon name="copy" size={12}/> {dotenvOpen ? "Cancel" : "Paste .env"}
        </button>
      </div>

      {dotenvOpen && (
        <div className="dotenv-drawer">
          <div className="dotenv-head">
            <span className="eyebrow accent mono">PASTE DOTENV</span>
            <span className="muted" style={{fontSize:12}}>One KEY=value per line. Anything we don't recognize becomes a new row.</span>
          </div>
          <textarea className="dotenv-input" rows={7}
                    placeholder={"OPENROUTER_API_KEY=sk-or-...\nNOTION_TOKEN=secret_...\nSLACK_BOT_TOKEN=xoxb-..."}
                    value={dotenvText} onChange={e=>setDotenvText(e.target.value)}/>
          <div className="dotenv-actions">
            <button className="btn btn-primary btn-sm" onClick={doPaste} disabled={!dotenvText.trim()}>Import</button>
            <button className="btn btn-outline btn-sm" onClick={()=>{setDotenvText(""); setDotenvOpen(false);}}>Cancel</button>
            <span className="muted mono" style={{fontSize:11, marginLeft:"auto"}}>Written to browser localStorage (demo). Prod paths: .env.cache + SETTINGS_STORE KV.</span>
          </div>
        </div>
      )}

      {pasteFlash.length > 0 && (
        <div className="paste-flash">
          <span className="mono accent">✓ imported {pasteFlash.length} key{pasteFlash.length===1?"":"s"}:</span>
          {pasteFlash.map(k => <span key={k} className="se-chip" style={{marginLeft:4}}>{k}</span>)}
        </div>
      )}

      {coverage.set === 0 && integrations.length === 0
        ? <EmptyCreds/>
        : (
          <>
            <ProviderList title="AI providers" sub="Model APIs. Every ai-* verb pipelines through one of these. Note: actor and auditor usually use different providers." items={ai.filter(match)}/>
            <ProviderList title="Platforms & Services" sub="Outbound + inbound. Xano, Slack, Notion, LinkedIn, FreshBooks, …" items={platforms.filter(match)}/>
            <ProviderList title="Infrastructure" sub="Deployment + source control + storage. Vercel, GitHub, Cloudflare, DO Spaces." items={infra.filter(match)}/>
            {integrations.length > 0 && (
              <ProviderList
                title="From installed integrations"
                sub="Auto-surfaced when you drop a skill or install an integration tile. Fill these to light up the skill."
                items={integrations.filter(match)}
                flash={pasteFlash}
              />
            )}
          </>
        )
      }

      <div className="ref-grid">
        <div className="ref-cell">
          <div className="eyebrow mono" style={{marginBottom:8}}>HTTP SNIPPETS</div>
          <pre className="detail-code" style={{fontSize:11, maxHeight:140, overflow:"auto"}}>
{`# bash
curl -H "authorization: bearer $SNP_KEY" \\
  https://skills.snappy.ai/.well-known/skills/index.json

# node
await fetch(url, { headers: { authorization: \`bearer \${key}\` } })

# python
requests.get(url, headers={"authorization": f"bearer {key}"})`}</pre>
        </div>
        <div className="ref-cell">
          <div className="eyebrow mono" style={{marginBottom:8}}>KEYBOARD</div>
          <div style={{fontSize:12.5, color:"var(--muted-foreground)", lineHeight:1.9}}>
            <div>Focus search <span className="search-kbd" style={{marginRight:0}}>/</span></div>
            <div>Save edit <span className="search-kbd" style={{marginRight:0}}>⌘</span> + <span className="search-kbd" style={{marginRight:0}}>S</span></div>
            <div>Close / re-mask <span className="search-kbd" style={{marginRight:0}}>esc</span></div>
            <div>Focus rings <span className="search-kbd" style={{marginRight:0}}>Tab</span></div>
          </div>
        </div>
        <div className="ref-cell">
          <div className="eyebrow mono" style={{marginBottom:8}}>FILES TO REMEMBER</div>
          <div style={{fontSize:12.5, color:"var(--muted-foreground)", lineHeight:1.8}}>
            <div><code>~/projects/snappy-os/.env.cache</code></div>
            <div><code>SETTINGS_STORE</code> (Cloudflare KV)</div>
            <div><code>state/lint/settings-audit.ts</code></div>
          </div>
        </div>
      </div>
    </div>
  );
}

function EmptyCreds() {
  return (
    <div className="empty-creds">
      <h3 className="h3" style={{margin:"0 0 10px", fontSize:18}}>No credentials set yet.</h3>
      <p style={{fontSize:13, lineHeight:1.6, color:"var(--muted-foreground)", maxWidth:620}}>
        Every skill reads its dependencies via <code>env("KEY")</code>. Missing values throw at read-time — there are no fallbacks.
        Because <b>actor ≠ auditor</b> is the system-wide invariant, most skills need <b>two</b> credentials: one for the generator,
        one for the judge. A typical pairing: <code>NANOBANANA_KEY</code> (writes the image) + <code>GOOGLE_API_KEY</code> (grades it).
      </p>
      <button className="btn btn-primary" style={{marginTop:14}}>
        Quick start: paste your OpenRouter key
      </button>
    </div>
  );
}

function ProviderList({ title, sub, items, flash }) {
  if (!items.length) return null;
  return (
    <div className="provider-section">
      <div className="eyebrow mono" style={{marginTop:28}}>{title.toUpperCase()}</div>
      <div className="muted" style={{fontSize:12.5, margin:"4px 0 12px"}}>{sub}</div>
      <div className="provider-list">
        {items.map(it => (
          <ProviderRow key={it.key} it={it} justPasted={(flash||[]).includes(it.key)}/>
        ))}
      </div>
    </div>
  );
}

function ProviderRow({ it, justPasted }) {
  const S = window.useSnappyState();
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(it.value || "");
  const [reveal, setReveal] = useState(false);
  const testing = it.state === "testing";

  useEffect(() => { setDraft(it.value || ""); }, [it.value]);

  const save = () => {
    S.setEnvValue(it.key, draft.trim());
    setEditing(false);
    setReveal(false);
  };
  const cancel = () => {
    setDraft(it.value || "");
    setEditing(false);
  };
  const runTest = (e) => { e.stopPropagation(); S.testEnv(it.key); };

  const displayValue = it.value
    ? (reveal ? it.value : ("•".repeat(Math.min(12, Math.max(8, it.value.length - 4))) + it.value.slice(-4)))
    : "— unset —";

  return (
    <div className={"provider-row state-" + it.state + (justPasted ? " just-pasted" : "")}>
      <span className="provider-stripe"/>
      <span style={{fontWeight:600, minWidth:120}}>{it.name}</span>
      <span className="mono muted" style={{fontSize:11.5, minWidth:140}}>{it.key}</span>
      {editing ? (
        <input
          autoFocus
          className="input btn-sm provider-edit-input"
          value={draft}
          onChange={e=>setDraft(e.target.value)}
          onKeyDown={e => {
            if (e.key === "Enter") save();
            if (e.key === "Escape") cancel();
          }}
          placeholder={`paste ${it.key}…`}
          style={{flex:1, minWidth:180, height:28, fontFamily:"var(--font-mono)", fontSize:12}}
        />
      ) : (
        <span
          className="mono provider-value"
          style={{flex:1, minWidth:140, maxWidth:260, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", fontSize:12, color: it.value ? "var(--foreground)":"var(--muted-foreground)"}}
          onClick={() => it.value && setReveal(r => !r)}
          title={it.value ? (reveal ? "click to mask" : "click to reveal") : "not set"}
        >
          {displayValue}
        </span>
      )}
      <span className={"pill pill-" + it.state}>{it.state}</span>
      {it.skills.length > 0 && (
        <span className="powers-chips">
          <span className="muted mono" style={{fontSize:10.5, marginRight:4}}>powers {it.skills.length}:</span>
          {it.skills.slice(0,3).map(s => <a key={s} href={"#/skill/"+s} className="se-chip powers-chip">{s}</a>)}
          {it.skills.length > 3 && <span className="se-chip powers-chip-more">+{it.skills.length-3}</span>}
        </span>
      )}
      <span style={{marginLeft:"auto", display:"flex", gap:6, alignItems:"center"}}>
        <button className="btn btn-outline btn-sm" onClick={runTest} disabled={testing || !it.value}>
          {testing ? "Testing…" : "Test"}
        </button>
        {editing ? (
          <>
            <button className="btn btn-primary btn-sm" onClick={save}>Save</button>
            <button className="btn btn-outline btn-sm" onClick={cancel}>Cancel</button>
          </>
        ) : (
          <button className="btn btn-outline btn-sm" onClick={()=>setEditing(true)}>Edit</button>
        )}
      </span>
    </div>
  );
}

// ===========================================================================
// Linter page — P0-P3 frictions + regen queue
// ===========================================================================
function LinterPage() {
  const data = window.SNAPPY_DATA;
  const [scanning, setScanning] = useState(false);
  const [lintType, setLintType] = useState("all");
  const [sevFilter, setSevFilter] = useState(null);
  const [expanded, setExpanded] = useState(null);

  const rows = useMemo(() => data.skills.map(s => {
    const hash = (s.id.length * 7 + s.imp) % 17;
    const p0 = hash === 0 ? 1 : 0;
    const p1 = (hash === 1 || hash === 2) ? 1 : 0;
    const p2 = (hash % 5 === 3) ? 2 : 0;
    const p3 = (hash % 3 === 0) ? 1 : 0;
    const total = p0*100 + p1*30 + p2*10 + p3*3;
    // 14-day friction spark
    const spark = Array.from({length:14}).map((_,i) => 1 + ((hash + i*3) % 7));
    return { ...s, p0, p1, p2, p3, total, spark };
  }), [data.skills]);

  const sorted = useMemo(() => [...rows].sort((a,b) => b.total - a.total), [rows]);
  const visible = sevFilter
    ? sorted.filter(r => r[sevFilter] > 0)
    : sorted.filter(r => r.total > 0);

  const tot = (k) => rows.reduce((a,r) => a+r[k], 0);
  const regenQueue = rows.filter(r => r.p0 + r.p1 > 0).length;

  const run = () => {
    setScanning(true);
    setTimeout(() => setScanning(false), 1200);
  };

  return (
    <div className="container section" style={{maxWidth:1280, margin:"0 auto"}}>
      <h1 className="h1" style={{margin:"0 0 6px", fontSize:"30px"}}>Skill Linter</h1>
      <p className="lead" style={{maxWidth:680, margin:"0 0 20px", fontSize:14}}>
        Structural lint + friction rollup across all published skills. P0 blocks publish; P1 enters the regen queue; P2/P3 trail.
      </p>

      <div className="lint-bar">
        <button className="btn btn-primary" onClick={run} disabled={scanning}>
          {scanning ? "Scanning…" : "Run scan"}
        </button>
        <select className="input btn-sm" value={lintType} onChange={e=>setLintType(e.target.value)} style={{height:32, width:200}}>
          <option value="all">all lints</option>
          <option>check</option>
          <option>prose-sidecar-drift</option>
          <option>docs-drift</option>
        </select>
        <span className="muted mono" style={{fontSize:12, marginLeft:"auto"}}>Last run: 2026-04-21 06:08:37 UTC</span>
      </div>

      <div className="lint-tiles">
        <button className={"lint-tile sev-p0" + (sevFilter==="p0"?" active":"")} onClick={()=>setSevFilter(sevFilter==="p0"?null:"p0")}>
          <div className="lint-tile-n">{tot("p0")}</div>
          <div className="lint-tile-l mono">P0 — BLOCKS PUBLISH</div>
        </button>
        <button className={"lint-tile sev-p1" + (sevFilter==="p1"?" active":"")} onClick={()=>setSevFilter(sevFilter==="p1"?null:"p1")}>
          <div className="lint-tile-n">{tot("p1")}</div>
          <div className="lint-tile-l mono">P1 — REGEN QUEUE</div>
        </button>
        <button className={"lint-tile sev-p2" + (sevFilter==="p2"?" active":"")} onClick={()=>setSevFilter(sevFilter==="p2"?null:"p2")}>
          <div className="lint-tile-n">{tot("p2")}</div>
          <div className="lint-tile-l mono">P2 — SOON</div>
        </button>
        <button className={"lint-tile sev-p3" + (sevFilter==="p3"?" active":"")} onClick={()=>setSevFilter(sevFilter==="p3"?null:"p3")}>
          <div className="lint-tile-n">{tot("p3")}</div>
          <div className="lint-tile-l mono">P3 — TRAIL</div>
        </button>
        <div className="lint-tile sev-regen">
          <div className="lint-tile-n">{regenQueue}</div>
          <div className="lint-tile-l mono">REGEN QUEUE DEPTH</div>
        </div>
      </div>

      {visible.length === 0 ? (
        <div className="lint-clean">
          <div className="mono accent" style={{fontSize:14, marginBottom:6}}>◎ ALL CLEAN</div>
          <div style={{fontSize:14}}>{rows.length} skills · 0 frictions.</div>
        </div>
      ) : (
        <table className="lint-table">
          <thead>
            <tr>
              <th>SKILL</th><th>P0</th><th>P1</th><th>P2</th><th>P3</th>
              <th>LAST LINTED</th><th style={{width:160}}>FRICTIONS · 14d</th><th></th>
            </tr>
          </thead>
          <tbody>
            {visible.map(r => (
              <React.Fragment key={r.id}>
                <tr className={"lint-row" + (expanded===r.id?" expanded":"")} onClick={()=>setExpanded(expanded===r.id?null:r.id)}>
                  <td className="mono"><a href={"#/skill/"+r.id} onClick={e=>e.stopPropagation()} className="footer-link" style={{color:"var(--foreground)"}}>{r.id}</a></td>
                  <td className={r.p0?"sev-p0-txt":"muted"}>{r.p0||"—"}</td>
                  <td className={r.p1?"sev-p1-txt":"muted"}>{r.p1||"—"}</td>
                  <td className={r.p2?"sev-p2-txt":"muted"}>{r.p2||"—"}</td>
                  <td className={r.p3?"sev-p3-txt":"muted"}>{r.p3||"—"}</td>
                  <td className="muted mono" style={{fontSize:11}}>{r.age === "today" ? "20h ago" : "3d ago"}</td>
                  <td>
                    <div className="friction-spark">
                      {r.spark.map((v,i) => <span key={i} className="fs-bar" style={{height: (v*10)+"%"}}/>)}
                    </div>
                  </td>
                  <td className="muted mono" style={{fontSize:11}}>{expanded===r.id ? "▾" : "▸"}</td>
                </tr>
                {expanded === r.id && (
                  <tr className="lint-detail-row">
                    <td colSpan={8}>
                      <div className="lint-detail">
                        {r.p0 > 0 && <FrictionRow sev="P0" area="frontmatter" expected={`trigger phrase ≤ 3 tokens`} actual={`"${r.id.replace(/-/g," ")} generate and inspect"`} repro={`npx snappy lint ${r.id}`}/>}
                        {r.p1 > 0 && <FrictionRow sev="P1" area="actor-auditor" expected="auditor with different model than actor" actual={`both use gpt-5`} repro={`npx snappy lint ${r.id} --rule actor-neq-auditor`}/>}
                        {r.p2 > 0 && <FrictionRow sev="P2" area="eval-log" expected="≥ 1 eval row in last 7 days" actual="0 rows" repro={`cat state/log/evals.ndjson | grep ${r.id}`}/>}
                        {r.p3 > 0 && <FrictionRow sev="P3" area="docs-drift" expected="## Gotchas present" actual="section missing" repro={`npx snappy lint ${r.id} --rule docs-drift`}/>}
                      </div>
                    </td>
                  </tr>
                )}
              </React.Fragment>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

function FrictionRow({ sev, area, expected, actual, repro }) {
  return (
    <div className={"friction-row friction-" + sev.toLowerCase()}>
      <span className={"finding-sev mono sev-" + sev.toLowerCase()+"-bg"}>{sev}</span>
      <div className="friction-main">
        <div className="friction-head">
          <span className="mono" style={{fontSize:11, color:"var(--muted-foreground)"}}>{area}</span>
        </div>
        <div className="friction-grid">
          <div><span className="mono muted" style={{fontSize:10.5}}>EXPECTED</span><div style={{fontSize:12.5}}>{expected}</div></div>
          <div><span className="mono muted" style={{fontSize:10.5}}>ACTUAL</span><div style={{fontSize:12.5}}>{actual}</div></div>
        </div>
        <div className="friction-repro mono">$ {repro}</div>
      </div>
    </div>
  );
}

// ===========================================================================
// Docs stub
// ===========================================================================
function DocsPage() {
  const docs = [
    { kind:"SCHEMA",  file:"program.md", lead:"What snappy-os IS.",
      body:"Loops, verbs, evals, the PID contract — the file every agent reads first.", href:"#/docs/program" },
    { kind:"CATALOG", file:"state/index.md", lead:"What exists and where.",
      body:"Living pointer to every skill, lib, bin, lint, recipe — the index the agent consults after program.md.", href:"#/docs/index" },
    { kind:"RUNTIME", file:"CLAUDE.md", lead:"How to execute.",
      body:"Repo wiring for snappy-os itself — credential paths, hook install order, rules of operation.", href:"#/docs/claude" },
  ];
  return (
    <div className="container section" style={{maxWidth:1120, margin:"0 auto"}}>
      <h1 className="h1" style={{margin:"0 0 8px", fontSize:"30px"}}>Docs</h1>
      <p className="lead" style={{maxWidth:680, fontSize:14}}>
        The three canonical files, the five-part skill frame, the PID loop. In that order.
      </p>
      <div className="pillars" style={{marginTop:24}}>
        {docs.map(d => (
          <a key={d.file} href={d.href} className="pillar doc-pillar">
            <div className="pillar-eyebrow">{d.kind}</div>
            <div className="pillar-title">{d.file}</div>
            <div className="pillar-what muted">{d.lead}</div>
            <p className="pillar-body">{d.body}</p>
            <div style={{marginTop:14, paddingTop:12, borderTop:"1px solid var(--border)", display:"flex", justifyContent:"space-between", alignItems:"center"}}>
              <span className="mono" style={{fontSize:11, color:"var(--muted-foreground)"}}>Read this</span>
              <Icon name="arrow-right" size={12}/>
            </div>
          </a>
        ))}
      </div>
    </div>
  );
}

Object.assign(window, { useHashRoute, SkillExpanded, SkillDetailPage, AIPage, SettingsPage, LinterPage, DocsPage });
