/* global React, WORKS, CLAIMS, SOCIETIES, TXN, Ic, Pill, Section, Btn, Term */
// ───────────────────────────────────────────────────────────────────────────
// audit-log.jsx
//
// ScreenAudit — workspace-wide audit / activity log.
//
// Structurally mirrors astro.audit_log + astro.signature_audit_log +
// astro.work_share_audit_log: every record has actor (user|system|integration),
// entity (kind + id + label), action (CRUD verb or domain verb), timestamp,
// IP, and optional before/after JSONB diffs.
//
// Designed as the workspace's compliance surface: every CWR send, every claim
// filed, every share change, every agreement countersigned, every member
// invite, every export — all in one filterable, exportable timeline.
//
// Reads live entity arrays (WORKS, CLAIMS, SOCIETIES, TXN, AGREEMENTS) and
// constructs a deterministic 30-day stream so any reference back to a real
// entity stays consistent.
// ───────────────────────────────────────────────────────────────────────────

(function () {
  const { useState, useMemo, useEffect, useRef } = React;

  // tiny deterministic PRNG (matches workspace-screens.jsx)
  const seed = (s) => {
    let h = 2166136261;
    for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
    return () => { h = Math.imul(h ^ (h >>> 15), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return ((h ^= h >>> 16) >>> 0) / 4294967296; };
  };
  const fmtNum = (n) => Number(n).toLocaleString();
  const pad2 = (n) => String(n).padStart(2, '0');

  // ═══════════════════════════════════════════════════════════════════════
  // Action vocabulary — what verbs appear in the log, grouped by domain.
  // Each verb has a category, a default actor profile, and optional diff
  // template fn. Categories drive the filter chips.
  // ═══════════════════════════════════════════════════════════════════════

  const CATS = {
    catalog:   { label: 'Catalog',     ink: 'var(--ink)',         dot: 'var(--ink-2)' },
    rights:    { label: 'Rights',      ink: 'var(--accent)',      dot: 'var(--accent)' },
    royalties: { label: 'Royalties',   ink: '#3a6a3a',            dot: '#3a6a3a' },
    transmissions: { label: 'Transmissions', ink: '#1f4ed8',     dot: '#1f4ed8' },
    agreements: { label: 'Agreements', ink: '#7a3aa8',            dot: '#7a3aa8' },
    members:   { label: 'Members',     ink: 'var(--ink-2)',       dot: 'var(--ink-2)' },
    integrations: { label: 'Integrations', ink: 'var(--ink-3)',  dot: 'var(--ink-3)' },
    workspace: { label: 'Workspace',   ink: 'var(--ink-3)',       dot: 'var(--ink-3)' },
    security:  { label: 'Security',    ink: 'var(--danger)',      dot: 'var(--danger)' },
  };

  const ACTORS = [
    { id: 'a.cohen',    name: 'Avery Cohen',   role: 'Owner',         email: 'a.cohen@pluralis.com',    ip: '73.21.114.88' },
    { id: 'k.davis',    name: 'Kennedy Davis', role: 'Rights Admin',  email: 'k.davis@pluralis.com',    ip: '88.10.44.201' },
    { id: 'm.lee',      name: 'Marlowe Lee',   role: 'Rights Admin',  email: 'm.lee@pluralis.com',      ip: '73.21.114.88' },
    { id: 'r.peters',   name: 'Rae Peters',    role: 'Royalties',     email: 'r.peters@pluralis.com',   ip: '162.4.18.77' },
    { id: 'p.tibbets',  name: 'Priya Tibbets', role: 'A&R',           email: 'p.tibbets@pluralis.com',  ip: '202.55.190.18' },
    { id: 'j.alvarez',  name: 'José Alvarez',  role: 'Catalog Ops',   email: 'j.alvarez@pluralis.com',  ip: '73.21.114.88' },
    { id: 's.knight',   name: 'Sasha Knight',  role: 'Legal',         email: 's.knight@pluralis.com',   ip: '64.39.72.155' },
    { id: 'system',     name: 'System',        role: 'Service',       email: 'system@astro',            ip: '—' },
    { id: 'matcher.bot',name: 'Matcher · v2.1',role: 'Service',       email: 'matcher@astro',           ip: '—' },
    { id: 'parser.bot', name: 'Parser · v3.4', role: 'Service',       email: 'parser@astro',            ip: '—' },
    { id: 'cwr.svc',    name: 'CWR service',   role: 'Service',       email: 'cwr@astro',               ip: '—' },
    { id: 'webhook.svc',name: 'Webhook · prod',role: 'Service',       email: 'hooks@astro',             ip: '34.96.44.18' },
  ];
  const ACTOR_BY_ID = Object.fromEntries(ACTORS.map(a => [a.id, a]));
  const isService = (id) => /^(system|.*\.bot|.*\.svc)$/.test(id);

  const ACTIONS = {
    // catalog
    'work.create':      { cat: 'catalog', label: 'created work', verb: 'CREATE' },
    'work.update':      { cat: 'catalog', label: 'updated work', verb: 'UPDATE' },
    'work.merge':       { cat: 'catalog', label: 'merged works', verb: 'MERGE' },
    'work.delete':      { cat: 'catalog', label: 'deleted work', verb: 'DELETE' },
    'recording.create': { cat: 'catalog', label: 'created recording', verb: 'CREATE' },
    'recording.link':   { cat: 'catalog', label: 'linked recording → work', verb: 'LINK' },
    'release.create':   { cat: 'catalog', label: 'created release', verb: 'CREATE' },
    'release.takedown': { cat: 'catalog', label: 'requested takedown', verb: 'UPDATE' },
    'identifier.add':   { cat: 'catalog', label: 'added identifier', verb: 'CREATE' },
    'identifier.merge': { cat: 'catalog', label: 'merged duplicate identifier', verb: 'MERGE' },
    // rights
    'split.update':     { cat: 'rights', label: 'updated split', verb: 'UPDATE' },
    'claim.file':       { cat: 'rights', label: 'filed claim', verb: 'CREATE' },
    'claim.respond':    { cat: 'rights', label: 'responded to claim', verb: 'UPDATE' },
    'claim.resolve':    { cat: 'rights', label: 'resolved claim', verb: 'UPDATE' },
    'claim.escalate':   { cat: 'rights', label: 'escalated claim', verb: 'UPDATE' },
    'conflict.flag':    { cat: 'rights', label: 'flagged conflict', verb: 'CREATE' },
    'territory.update': { cat: 'rights', label: 'updated territory', verb: 'UPDATE' },
    // transmissions
    'cwr.generate':     { cat: 'transmissions', label: 'generated CWR batch', verb: 'CREATE' },
    'cwr.submit':       { cat: 'transmissions', label: 'submitted CWR', verb: 'SUBMIT' },
    'cwr.ack':          { cat: 'transmissions', label: 'received ack', verb: 'ACK' },
    'cwr.reject':       { cat: 'transmissions', label: 'received rejection', verb: 'REJECT' },
    'ddex.deliver':     { cat: 'transmissions', label: 'delivered DDEX ERN', verb: 'SUBMIT' },
    // royalties
    'statement.import': { cat: 'royalties', label: 'imported statement', verb: 'CREATE' },
    'statement.match':  { cat: 'royalties', label: 'matched lines', verb: 'UPDATE' },
    'statement.recon':  { cat: 'royalties', label: 'reconciled period', verb: 'UPDATE' },
    'pool.claim':       { cat: 'royalties', label: 'filed black-box claim', verb: 'CREATE' },
    'pool.forfeit':     { cat: 'royalties', label: 'forfeited pool window', verb: 'UPDATE' },
    'payout.run':       { cat: 'royalties', label: 'ran payout', verb: 'CREATE' },
    // agreements
    'agreement.create': { cat: 'agreements', label: 'created agreement', verb: 'CREATE' },
    'agreement.sign':   { cat: 'agreements', label: 'countersigned', verb: 'UPDATE' },
    'agreement.expire': { cat: 'agreements', label: 'expiry alert', verb: 'NOTICE' },
    'agreement.terminate': { cat: 'agreements', label: 'terminated', verb: 'UPDATE' },
    // members
    'member.invite':    { cat: 'members', label: 'invited member', verb: 'CREATE' },
    'member.role':      { cat: 'members', label: 'changed role', verb: 'UPDATE' },
    'member.remove':    { cat: 'members', label: 'removed member', verb: 'DELETE' },
    // integrations
    'webhook.fire':     { cat: 'integrations', label: 'fired webhook', verb: 'EVENT' },
    'webhook.fail':     { cat: 'integrations', label: 'webhook failed', verb: 'EVENT' },
    'token.create':     { cat: 'integrations', label: 'minted API token', verb: 'CREATE' },
    'token.rotate':     { cat: 'integrations', label: 'rotated token', verb: 'UPDATE' },
    // workspace
    'export.run':       { cat: 'workspace', label: 'ran export', verb: 'EXPORT' },
    'view.save':        { cat: 'workspace', label: 'saved view', verb: 'CREATE' },
    'settings.update':  { cat: 'workspace', label: 'updated settings', verb: 'UPDATE' },
    // security
    'auth.login':       { cat: 'security', label: 'signed in', verb: 'AUTH' },
    'auth.fail':        { cat: 'security', label: 'failed sign-in', verb: 'AUTH' },
    'auth.mfa':         { cat: 'security', label: 'changed MFA device', verb: 'UPDATE' },
    'auth.impersonate': { cat: 'security', label: 'started support impersonation', verb: 'AUTH' },
  };

  // ═══════════════════════════════════════════════════════════════════════
  // SYNTHETIC LOG GENERATION
  // 30 days back, evenly distributed but with a fat tail today + yesterday.
  // ═══════════════════════════════════════════════════════════════════════
  function buildLog() {
    const out = [];
    const today = new Date('2026-04-30T16:35:00Z');
    const works = (typeof window.WORKS !== 'undefined' ? window.WORKS : (typeof WORKS !== 'undefined' ? WORKS : []));
    const claims = (typeof window.CLAIMS !== 'undefined' ? window.CLAIMS : (typeof CLAIMS !== 'undefined' ? CLAIMS : []));
    const socs = (typeof window.SOCIETIES !== 'undefined' ? window.SOCIETIES : (typeof SOCIETIES !== 'undefined' ? SOCIETIES : []));
    const txn = (typeof window.TXN !== 'undefined' ? window.TXN : (typeof TXN !== 'undefined' ? TXN : []));
    const ags = (typeof window.AGREEMENTS !== 'undefined' ? window.AGREEMENTS : []);
    const recs = (typeof window.RECORDINGS !== 'undefined' ? window.RECORDINGS : []);
    const releases = (typeof window.RELEASES !== 'undefined' ? window.RELEASES : []);

    let _eventSeq = 0;
    const push = (rec) => {
      _eventSeq++;
      out.push({
        id: `EV-${rec.t.toISOString().slice(2, 10).replace(/-/g, '')}-${String(_eventSeq).padStart(5, '0')}`,
        ...rec,
      });
    };

    // ───────── 1) CWR transmissions: every batch in TXN gets create+submit+(ack|reject)
    txn.forEach((t, i) => {
      const dayBack = i % 5; // spread across last 5 days
      const base = new Date(today.getTime() - dayBack * 86400000 - 6 * 3600000);
      const tCreate = new Date(base.getTime() - 90 * 60000);
      const tSubmit = new Date(base.getTime() - 30 * 60000);
      const tAck    = new Date(base.getTime() + (i + 1) * 18 * 60000);
      push({ t: tCreate, actor: 'cwr.svc', action: 'cwr.generate',
        entity: { kind: 'cwr', id: t.id, label: t.file },
        ctx: `${t.count} works · ${t.recv}`,
        diff: { batch: t.id, transactions: t.count, society: t.recv, version: t.file.includes('V21') ? 'V21' : 'V22' },
      });
      push({ t: tSubmit, actor: 'cwr.svc', action: 'cwr.submit',
        entity: { kind: 'cwr', id: t.id, label: t.file },
        ctx: `→ ${t.recv} · SFTP`,
      });
      if (t.status === 'rejected') {
        push({ t: tAck, actor: 'cwr.svc', action: 'cwr.reject',
          entity: { kind: 'cwr', id: t.id, label: t.file },
          ctx: `${t.recv} · NWR-VL-022 · share≠100%`,
          severity: 'high',
        });
      } else if (t.status === 'acknowledged') {
        push({ t: tAck, actor: 'cwr.svc', action: 'cwr.ack',
          entity: { kind: 'cwr', id: t.id, label: t.file },
          ctx: `${t.recv} · ${t.count} processed`,
        });
      }
    });

    // ───────── 2) Claims: each CLAIMS row generates 1-3 events
    claims.forEach((c, i) => {
      const base = new Date(today.getTime() - (c.age || i) * 86400000);
      push({ t: new Date(base.getTime() - 4 * 3600000), actor: 'matcher.bot', action: 'conflict.flag',
        entity: { kind: 'claim', id: c.id, label: c.work || c.iswc },
        ctx: `${c.party} · ${c.pct}% disputed`,
        severity: c.severity || 'mid',
      });
      push({ t: base, actor: i % 2 === 0 ? 'k.davis' : 'm.lee', action: 'claim.file',
        entity: { kind: 'claim', id: c.id, label: c.work || c.iswc },
        ctx: `vs ${c.party} via ${c.method?.toUpperCase() || 'CWR'}`,
        diff: { claimant: c.claimant, counterparty: c.party, share_disputed: c.pct, method: c.method },
        severity: c.severity || 'mid',
      });
      if (c.status === 'resolved') {
        push({ t: new Date(base.getTime() + 36 * 3600000), actor: i % 2 === 0 ? 'k.davis' : 'a.cohen', action: 'claim.resolve',
          entity: { kind: 'claim', id: c.id, label: c.work || c.iswc },
          ctx: 'agreement reached',
        });
      } else if (c.status === 'open' && (c.severity === 'high')) {
        push({ t: new Date(base.getTime() + 12 * 3600000), actor: 'a.cohen', action: 'claim.escalate',
          entity: { kind: 'claim', id: c.id, label: c.work || c.iswc },
          ctx: '→ Legal · response due 5d',
          severity: 'high',
        });
      }
    });

    // ───────── 3) Splits / share updates: a handful per day on real works
    for (let d = 0; d < 30; d++) {
      const r = seed('splits·' + d);
      const events = 1 + Math.floor(r() * 3);
      for (let e = 0; e < events; e++) {
        if (works.length === 0) break;
        const w = works[Math.floor(r() * works.length)];
        if (!w) continue;
        const t = new Date(today.getTime() - d * 86400000 - r() * 86400000);
        const before = [60 + Math.floor(r() * 10), 40 - Math.floor(r() * 10)];
        const after = [before[0] + (r() < 0.5 ? 5 : -5), before[1] + (r() < 0.5 ? -5 : 5)];
        const actor = ['m.lee', 'k.davis', 'j.alvarez', 'a.cohen'][Math.floor(r() * 4)];
        push({ t, actor, action: 'split.update',
          entity: { kind: 'work', id: w.id || w.iswc, label: w.title },
          ctx: `${before.join('/')}\u00a0→\u00a0${after.join('/')}`,
          diff: { before: { writer_a: before[0], writer_b: before[1] }, after: { writer_a: after[0], writer_b: after[1] } },
        });
      }
    }

    // ───────── 4) Catalog mutations
    for (let d = 0; d < 30; d++) {
      const r = seed('cat·' + d);
      // works created
      if (r() < 0.5 && works.length > 0) {
        const w = works[Math.floor(r() * works.length)];
        push({ t: new Date(today.getTime() - d * 86400000 - r() * 86400000),
          actor: r() < 0.4 ? 'a.cohen' : 'j.alvarez', action: 'work.create',
          entity: { kind: 'work', id: w.id || w.iswc, label: w.title },
          ctx: `${w.iswc || 'no ISWC'} · ${w.writers || 1} writer${w.writers === 1 ? '' : 's'}`,
        });
      }
      // recordings
      if (r() < 0.7 && recs.length > 0) {
        const rec = recs[Math.floor(r() * recs.length)];
        push({ t: new Date(today.getTime() - d * 86400000 - r() * 86400000),
          actor: 'p.tibbets', action: 'recording.create',
          entity: { kind: 'recording', id: rec.id, label: rec.title },
          ctx: `${rec.isrc || 'no ISRC'} · ${rec.artist || ''}`,
        });
      }
      // releases
      if (r() < 0.3 && releases.length > 0) {
        const rel = releases[Math.floor(r() * releases.length)];
        push({ t: new Date(today.getTime() - d * 86400000 - r() * 86400000),
          actor: 'p.tibbets', action: 'release.create',
          entity: { kind: 'release', id: rel.id, label: rel.title },
          ctx: `${rel.label || ''} · ${rel.upc || 'no UPC'}`,
        });
      }
      // identifier merges
      if (r() < 0.25) {
        push({ t: new Date(today.getTime() - d * 86400000 - r() * 86400000),
          actor: 'matcher.bot', action: 'identifier.merge',
          entity: { kind: 'work', id: 'IDM-' + d, label: 'duplicate ISWC merged' },
          ctx: `confidence 98.${Math.floor(r() * 99)}%`,
        });
      }
    }

    // ───────── 5) Royalties: imports, recon, payouts, pool claims
    for (let d = 0; d < 30; d++) {
      const r = seed('roy·' + d);
      if (r() < 0.4 && socs.length > 0) {
        const soc = socs[Math.floor(r() * socs.length)];
        const rows = 8000 + Math.floor(r() * 90000);
        const usd = 12000 + Math.floor(r() * 280000);
        push({ t: new Date(today.getTime() - d * 86400000 - r() * 86400000),
          actor: 'parser.bot', action: 'statement.import',
          entity: { kind: 'statement', id: `STMT-${soc.acronym}-Q${1 + Math.floor(d/22)}`, label: `${soc.acronym} ${d < 15 ? 'Q1 2026' : 'Q4 2025'}` },
          ctx: `${fmtNum(rows)} rows · ${'$' + fmtNum(usd)}`,
          diff: { rows, usd, society: soc.acronym, format: ['CWR ack', 'XML', 'CSV'][Math.floor(r() * 3)] },
        });
      }
      if (r() < 0.2) {
        push({ t: new Date(today.getTime() - d * 86400000 - r() * 86400000),
          actor: 'r.peters', action: 'statement.recon',
          entity: { kind: 'statement', id: `RECON-${d}`, label: `period reconciled` },
          ctx: 'variance < 0.5%',
        });
      }
      if (r() < 0.15) {
        push({ t: new Date(today.getTime() - d * 86400000 - r() * 86400000),
          actor: 'r.peters', action: 'pool.claim',
          entity: { kind: 'pool', id: `POOL-${d}`, label: 'unallocated · GEMA Q4' },
          ctx: '$' + fmtNum(2000 + Math.floor(r() * 18000)) + ' claimed',
        });
      }
      if (d % 14 === 0) {
        push({ t: new Date(today.getTime() - d * 86400000),
          actor: 'a.cohen', action: 'payout.run',
          entity: { kind: 'payout', id: `PR-${pad2(d/14 + 1)}`, label: `bi-weekly payout` },
          ctx: '$' + fmtNum(180000 + Math.floor(Math.random() * 60000)) + ' to 41 payees',
        });
      }
    }

    // ───────── 6) Agreements
    if (ags.length > 0) {
      ags.slice(0, 10).forEach((ag, i) => {
        const t = new Date(today.getTime() - i * 3 * 86400000 - 4 * 3600000);
        push({ t, actor: 's.knight', action: 'agreement.sign',
          entity: { kind: 'agreement', id: ag.id, label: `${ag.kind || 'Agreement'} · ${ag.party || ''}` },
          ctx: `v${ag.version || 1} · DocuSign · ${ag.term || '5y'} term`,
        });
      });
      // expiring renewal alerts
      ags.slice(0, 4).forEach((ag, i) => {
        push({ t: new Date(today.getTime() - i * 86400000 - 2 * 3600000), actor: 'system', action: 'agreement.expire',
          entity: { kind: 'agreement', id: ag.id, label: ag.kind || 'Agreement' },
          ctx: `90d to expiry · ${ag.party || ''}`,
          severity: 'mid',
        });
      });
    }

    // ───────── 7) DDEX ERN deliveries
    for (let d = 0; d < 6; d++) {
      const dsps = ['Spotify','Apple Music','Amazon','YouTube Music','Tidal','Deezer','Pandora','SoundCloud'];
      push({ t: new Date(today.getTime() - d * 5 * 86400000 - 2 * 3600000),
        actor: 'cwr.svc', action: 'ddex.deliver',
        entity: { kind: 'ddex', id: `ERN-${d+1}`, label: `release wave ${d+1}` },
        ctx: `${dsps.slice(0, 4 + (d%3)).join(', ')} · 32 releases`,
      });
    }

    // ───────── 8) Members + security
    push({ t: new Date(today.getTime() - 8 * 3600000), actor: 'a.cohen', action: 'member.invite',
      entity: { kind: 'member', id: 'a.patel', label: 'a.patel@pluralis.com' },
      ctx: 'role: Rights Admin · expires 7d',
      diff: { email: 'a.patel@pluralis.com', role: 'Rights Admin' },
    });
    push({ t: new Date(today.getTime() - 32 * 3600000), actor: 'a.cohen', action: 'member.role',
      entity: { kind: 'member', id: 'r.peters', label: 'Rae Peters' },
      ctx: 'Royalties → Royalties Lead',
      diff: { before: 'Royalties', after: 'Royalties Lead' },
    });
    push({ t: new Date(today.getTime() - 9 * 86400000), actor: 'a.cohen', action: 'member.remove',
      entity: { kind: 'member', id: 'former.staff', label: 'former.staff@pluralis.com' },
      ctx: 'access revoked · session terminated',
    });

    // ───────── 9) Logins / auth (last few days, mostly mundane)
    for (let d = 0; d < 14; d++) {
      const r = seed('auth·' + d);
      ACTORS.filter(a => !isService(a.id)).forEach((a, i) => {
        if (r() < 0.6) {
          push({ t: new Date(today.getTime() - d * 86400000 - i * 1.7 * 3600000),
            actor: a.id, action: 'auth.login',
            entity: { kind: 'session', id: `SESS-${d}-${i}`, label: a.name },
            ctx: `${['Chrome','Firefox','Safari'][Math.floor(r()*3)]} · macOS · ${a.ip}`,
          });
        }
      });
      if (r() < 0.2) {
        push({ t: new Date(today.getTime() - d * 86400000 - 5 * 3600000),
          actor: 'm.lee', action: 'auth.fail',
          entity: { kind: 'session', id: `FAIL-${d}`, label: 'm.lee@pluralis.com' },
          ctx: '3 attempts · throttled · IP 88.10.x.x',
          severity: 'mid',
        });
      }
    }

    // ───────── 10) Webhooks + tokens + exports
    for (let d = 0; d < 30; d++) {
      const r = seed('hooks·' + d);
      const fires = 4 + Math.floor(r() * 8);
      for (let f = 0; f < Math.min(fires, 3); f++) {
        const ok = r() < 0.92;
        push({ t: new Date(today.getTime() - d * 86400000 - f * 4.1 * 3600000),
          actor: 'webhook.svc', action: ok ? 'webhook.fire' : 'webhook.fail',
          entity: { kind: 'webhook', id: `WH-${d}-${f}`, label: ['royalty.parsed','claim.opened','cwr.acknowledged','release.delivered'][Math.floor(r()*4)] },
          ctx: ok ? '→ hooks.pluralis.com (200) · 142ms' : '→ hooks.pluralis.com (504) · timeout',
          severity: ok ? null : 'mid',
        });
      }
    }
    push({ t: new Date(today.getTime() - 14 * 86400000), actor: 'a.cohen', action: 'token.create',
      entity: { kind: 'token', id: 'ASTRO_PROD_R2', label: 'ASTRO_PROD_R2' },
      ctx: 'scope: catalog:read royalties:read · expires 2026-05-14',
    });
    push({ t: new Date(today.getTime() - 7 * 86400000), actor: 'a.cohen', action: 'export.run',
      entity: { kind: 'export', id: 'EXP-2026-04-23', label: 'full catalog ZIP' },
      ctx: '18,422 works · 41,209 recordings · 4.7GB · DDEX+CWR+CSV',
    });
    push({ t: new Date(today.getTime() - 22 * 3600000), actor: 'a.cohen', action: 'settings.update',
      entity: { kind: 'settings', id: 'workspace.royalty', label: 'royalty pipeline' },
      ctx: 'changed FX provider: ECB → OpenExchangeRates',
      diff: { fx_provider: { before: 'ECB', after: 'OpenExchangeRates' } },
    });
    push({ t: new Date(today.getTime() - 4 * 86400000), actor: 's.knight', action: 'auth.impersonate',
      entity: { kind: 'session', id: 'IMP-001', label: 'support session · k.davis@pluralis.com' },
      ctx: 'Anthropic support · ticket #4422 · 22min',
      severity: 'high',
    });

    // sort newest-first
    out.sort((a, b) => b.t - a.t);
    return out;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // FORMAT HELPERS
  // ═══════════════════════════════════════════════════════════════════════
  const NOW = new Date('2026-04-30T16:35:00Z');
  function relTime(t) {
    const ms = NOW - t;
    const m = ms / 60000;
    if (m < 1) return 'just now';
    if (m < 60) return `${Math.round(m)}m ago`;
    const h = m / 60;
    if (h < 24) return `${Math.round(h)}h ago`;
    const d = h / 24;
    if (d < 30) return `${Math.round(d)}d ago`;
    return t.toISOString().slice(0, 10);
  }
  function fmtTime(t) {
    return `${pad2(t.getUTCHours())}:${pad2(t.getUTCMinutes())}`;
  }
  function fmtDate(t) {
    return t.toISOString().slice(0, 10);
  }
  function dayKey(t) {
    return t.toISOString().slice(0, 10);
  }
  function dayLabel(key) {
    const d = new Date(key + 'T00:00:00Z');
    const today = NOW.toISOString().slice(0, 10);
    const yest = new Date(NOW.getTime() - 86400000).toISOString().slice(0, 10);
    if (key === today) return 'TODAY';
    if (key === yest)  return 'YESTERDAY';
    const opts = { weekday: 'long', month: 'long', day: 'numeric' };
    return d.toLocaleDateString('en-US', opts).toUpperCase();
  }

  // entity icon
  const ENTITY_ICON = {
    work: 'W', recording: 'R', release: 'L', claim: 'C', cwr: 'T',
    statement: '$', pool: '◇', payout: '◆', agreement: 'A',
    member: '@', session: '•', token: 'K', webhook: '⌁',
    ddex: 'D', export: '↓', settings: '⚙',
  };

  // ═══════════════════════════════════════════════════════════════════════
  // FILTER UTILS
  // ═══════════════════════════════════════════════════════════════════════
  function applyFilters(events, f) {
    return events.filter(e => {
      if (f.cat !== 'all' && ACTIONS[e.action]?.cat !== f.cat) return false;
      if (f.actor !== 'all') {
        if (f.actor === 'humans' && isService(e.actor)) return false;
        if (f.actor === 'services' && !isService(e.actor)) return false;
        if (f.actor !== 'humans' && f.actor !== 'services' && e.actor !== f.actor) return false;
      }
      if (f.range !== 'all') {
        const cutoff = NOW.getTime() - ({ '24h': 1, '7d': 7, '30d': 30 }[f.range] || 30) * 86400000;
        if (e.t.getTime() < cutoff) return false;
      }
      if (f.severity === 'high' && e.severity !== 'high') return false;
      if (f.q) {
        const q = f.q.toLowerCase();
        const hay = [
          e.id, e.actor, e.action, ACTIONS[e.action]?.label,
          e.entity?.id, e.entity?.label, e.ctx,
        ].filter(Boolean).join(' ').toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
  }

  function groupByDay(events) {
    const map = {};
    events.forEach(e => {
      const k = dayKey(e.t);
      (map[k] ||= []).push(e);
    });
    return Object.entries(map).sort((a, b) => a[0] < b[0] ? 1 : -1);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // SUB-COMPONENTS
  // ═══════════════════════════════════════════════════════════════════════
  function FilterChip({ active, onClick, count, children, dot }) {
    return (
      <button onClick={onClick} className="ff-mono upper" data-active={active ? 'true' : 'false'}
        style={{
          padding: '6px 12px', fontSize: 10, fontWeight: 600, letterSpacing: '.08em',
          background: active ? 'var(--ink)' : 'transparent',
          color: active ? 'var(--bg)' : 'var(--ink-2)',
          border: '1px solid ' + (active ? 'var(--ink)' : 'var(--rule)'),
          cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap',
        }}>
        {dot && <span style={{ width: 6, height: 6, background: dot, display: 'inline-block' }} />}
        {children}
        {count != null && <span style={{ opacity: 0.6, fontWeight: 400 }}>· {count}</span>}
      </button>
    );
  }

  function DiffPanel({ diff }) {
    if (!diff) return null;
    const isBA = diff.before && diff.after;
    if (isBA) {
      const keys = Array.from(new Set([...Object.keys(diff.before || {}), ...Object.keys(diff.after || {})]));
      return (
        <div className="ff-mono" style={{ fontSize: 11, marginTop: 10, border: '1px solid var(--rule)', overflow: 'hidden' }}>
          <div style={{ display: 'grid', gridTemplateColumns: '90px 1fr 1fr', background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)',
            padding: '6px 10px', fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em', fontWeight: 600 }}>
            <span>FIELD</span><span>BEFORE</span><span>AFTER</span>
          </div>
          {keys.map(k => (
            <div key={k} style={{ display: 'grid', gridTemplateColumns: '90px 1fr 1fr',
              padding: '6px 10px', borderBottom: '1px solid var(--rule-soft)', fontSize: 11 }}>
              <span style={{ color: 'var(--ink-3)' }}>{k}</span>
              <span style={{ background: 'rgba(196,68,68,0.06)', padding: '0 6px', textDecoration: 'line-through', color: 'var(--ink-2)' }}>
                {String(diff.before?.[k] ?? '—')}
              </span>
              <span style={{ background: 'rgba(58,106,58,0.08)', padding: '0 6px', color: 'var(--ink)' }}>
                {String(diff.after?.[k] ?? '—')}
              </span>
            </div>
          ))}
        </div>
      );
    }
    // simple key-value diff
    return (
      <div className="ff-mono" style={{ fontSize: 11, marginTop: 10, border: '1px solid var(--rule)', overflow: 'hidden' }}>
        {Object.entries(diff).map(([k, v]) => {
          if (v && typeof v === 'object' && (v.before !== undefined || v.after !== undefined)) {
            return (
              <div key={k} style={{ display: 'grid', gridTemplateColumns: '90px 1fr 1fr', padding: '6px 10px', borderBottom: '1px solid var(--rule-soft)' }}>
                <span style={{ color: 'var(--ink-3)' }}>{k}</span>
                <span style={{ background: 'rgba(196,68,68,0.06)', padding: '0 6px', textDecoration: 'line-through', color: 'var(--ink-2)' }}>{String(v.before ?? '—')}</span>
                <span style={{ background: 'rgba(58,106,58,0.08)', padding: '0 6px' }}>{String(v.after ?? '—')}</span>
              </div>
            );
          }
          return (
            <div key={k} style={{ display: 'grid', gridTemplateColumns: '120px 1fr', padding: '6px 10px', borderBottom: '1px solid var(--rule-soft)' }}>
              <span style={{ color: 'var(--ink-3)' }}>{k}</span>
              <span>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</span>
            </div>
          );
        })}
      </div>
    );
  }

  function EventRow({ e, expanded, onToggle, go }) {
    const action = ACTIONS[e.action] || { cat: 'workspace', label: e.action, verb: 'EVENT' };
    const cat = CATS[action.cat] || CATS.workspace;
    const actor = ACTOR_BY_ID[e.actor] || { name: e.actor, role: 'Unknown', email: e.actor, ip: '—' };
    const sev = e.severity;
    const navToEntity = () => {
      if (!e.entity) return;
      const k = e.entity.kind;
      if (k === 'work' && go) go('work', { id: e.entity.id, title: e.entity.label });
      else if (k === 'claim' && go) go('claims');
      else if (k === 'cwr' && go) go('cwr');
      else if (k === 'agreement' && go) go('agreement', { id: e.entity.id });
      else if (k === 'member' && go) go('settings', { tab: 'members' });
      else if (k === 'pool' && go) go('blackbox');
      else if (k === 'statement' && go) go('royalties');
      else if (k === 'release' && go) go('catalog', { tab: 'releases' });
    };

    return (
      <>
        <div onClick={onToggle}
          style={{
            display: 'grid',
            gridTemplateColumns: '64px 110px 130px 1fr 130px 80px 28px',
            gap: 12, padding: '10px 16px', alignItems: 'center',
            borderBottom: '1px solid var(--rule-soft)',
            cursor: 'pointer',
            background: expanded ? 'var(--bg-2)' : 'transparent',
            position: 'relative',
          }}
          onMouseEnter={ev => { if (!expanded) ev.currentTarget.style.background = 'var(--bg-2)'; }}
          onMouseLeave={ev => { if (!expanded) ev.currentTarget.style.background = 'transparent'; }}>

          {/* severity bar */}
          {sev === 'high' && <span style={{ position:'absolute', left:0, top:0, bottom:0, width:3, background:'var(--danger)' }} />}
          {sev === 'mid' && <span style={{ position:'absolute', left:0, top:0, bottom:0, width:3, background:'var(--accent)' }} />}

          {/* time */}
          <div className="ff-mono num" style={{ fontSize: 11, color: 'var(--ink-3)' }}>
            {fmtTime(e.t)}
            <div style={{ fontSize: 9, color: 'var(--ink-4)', marginTop: 2 }}>{relTime(e.t)}</div>
          </div>

          {/* actor */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
            <span className="ff-mono upper" style={{
              width: 22, height: 22, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              background: isService(actor.id || e.actor) ? 'transparent' : 'var(--ink)',
              color: isService(actor.id || e.actor) ? 'var(--ink-2)' : 'var(--bg)',
              border: isService(actor.id || e.actor) ? '1px dashed var(--rule)' : 'none',
              fontSize: 9, fontWeight: 600, flexShrink: 0,
            }}>
              {isService(actor.id || e.actor) ? '⚙' : (actor.name || e.actor).slice(0, 2).toUpperCase()}
            </span>
            <div style={{ minWidth: 0 }}>
              <div className="ff-mono" style={{ fontSize: 11, fontWeight: 600, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                {actor.id || e.actor}
              </div>
              <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.06em' }}>
                {actor.role || '—'}
              </div>
            </div>
          </div>

          {/* action */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
            <span className="ff-mono upper" style={{
              fontSize: 9, fontWeight: 700, letterSpacing: '.08em',
              padding: '2px 6px', border: '1px solid ' + cat.dot, color: cat.ink,
              flexShrink: 0,
            }}>{cat.label}</span>
            <span className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-2)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
              {action.label}
            </span>
          </div>

          {/* entity */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
            {e.entity && (
              <>
                <span className="ff-mono" style={{
                  width: 18, height: 18, display:'inline-flex', alignItems:'center', justifyContent:'center',
                  background: 'var(--bg-2)', border: '1px solid var(--rule)', fontSize: 10, fontWeight: 600, color: 'var(--ink-3)', flexShrink:0,
                }}>{ENTITY_ICON[e.entity.kind] || '•'}</span>
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ fontSize: 13, fontWeight: 500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                    {e.entity.label}
                  </div>
                  <div className="ff-mono" style={{ fontSize: 10, color:'var(--ink-3)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                    {e.ctx || e.entity.id}
                  </div>
                </div>
              </>
            )}
          </div>

          {/* event id */}
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-4)', textAlign: 'right', overflow:'hidden', textOverflow:'ellipsis' }}>
            {e.id}
          </div>

          {/* verb */}
          <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em', textAlign: 'right' }}>
            {action.verb}
          </div>

          {/* expand chevron */}
          <span className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-3)', textAlign: 'center', transition: 'transform .12s', transform: expanded ? 'rotate(90deg)' : 'rotate(0)' }}>›</span>
        </div>

        {/* expanded detail */}
        {expanded && (
          <div style={{ padding: '14px 16px 22px', background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)' }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 28 }}>
              <div>
                <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em', marginBottom: 8 }}>EVENT</div>
                <table className="ff-mono" style={{ width: '100%', fontSize: 11, borderCollapse: 'collapse' }}>
                  <tbody>
                    <tr><td style={cellL}>id</td><td style={cellR}>{e.id}</td></tr>
                    <tr><td style={cellL}>timestamp</td><td style={cellR}>{e.t.toISOString()}</td></tr>
                    <tr><td style={cellL}>action</td><td style={cellR}>{e.action}</td></tr>
                    <tr><td style={cellL}>category</td><td style={cellR}>{cat.label}</td></tr>
                    <tr><td style={cellL}>verb</td><td style={cellR}>{action.verb}</td></tr>
                    {sev && <tr><td style={cellL}>severity</td><td style={cellR}><Pill tone={sev === 'high' ? 'danger' : 'warn'} dot>{sev.toUpperCase()}</Pill></td></tr>}
                  </tbody>
                </table>
                <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em', marginTop: 16, marginBottom: 8 }}>ACTOR</div>
                <table className="ff-mono" style={{ width: '100%', fontSize: 11, borderCollapse: 'collapse' }}>
                  <tbody>
                    <tr><td style={cellL}>identity</td><td style={cellR}>{actor.name || e.actor}</td></tr>
                    <tr><td style={cellL}>email</td><td style={cellR}>{actor.email || '—'}</td></tr>
                    <tr><td style={cellL}>role</td><td style={cellR}>{actor.role || '—'}</td></tr>
                    <tr><td style={cellL}>ip</td><td style={cellR}>{actor.ip || '—'}</td></tr>
                    <tr><td style={cellL}>kind</td><td style={cellR}>{isService(e.actor) ? 'service / system' : 'human · authenticated'}</td></tr>
                  </tbody>
                </table>
              </div>
              <div>
                <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em', marginBottom: 8 }}>ENTITY</div>
                {e.entity ? (
                  <table className="ff-mono" style={{ width: '100%', fontSize: 11, borderCollapse: 'collapse' }}>
                    <tbody>
                      <tr><td style={cellL}>kind</td><td style={cellR}>{e.entity.kind}</td></tr>
                      <tr><td style={cellL}>id</td><td style={cellR}>{e.entity.id}</td></tr>
                      <tr><td style={cellL}>label</td><td style={cellR}>{e.entity.label}</td></tr>
                      {e.ctx && <tr><td style={cellL}>context</td><td style={cellR}>{e.ctx}</td></tr>}
                    </tbody>
                  </table>
                ) : <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>—</div>}
                {e.diff && <DiffPanel diff={e.diff} />}
              </div>
            </div>
            <div style={{ display: 'flex', gap: 10, marginTop: 18, flexWrap: 'wrap' }}>
              {e.entity && navToEntity && (
                <Btn variant="secondary" size="sm" icon={<Ic.Right/>} onClick={(ev) => { ev.stopPropagation(); navToEntity(); }}>
                  Open {e.entity.kind}
                </Btn>
              )}
              <Btn variant="ghost" size="sm" icon={<Ic.File/>} onClick={(ev) => {
                ev.stopPropagation();
                const text = JSON.stringify({ id: e.id, t: e.t.toISOString(), actor: e.actor, action: e.action, entity: e.entity, ctx: e.ctx, diff: e.diff }, null, 2);
                navigator.clipboard?.writeText(text);
                window.dispatchEvent(new CustomEvent('astro-toast',{detail:{msg:'Event JSON copied',tone:'ok'}}));
              }}>Copy JSON</Btn>
              <Btn variant="ghost" size="sm" icon={<Ic.Bell/>} onClick={(ev) => {
                ev.stopPropagation();
                window.dispatchEvent(new CustomEvent('astro-toast',{detail:{msg:`Subscribed to ${e.entity?.kind || 'this'} events`,tone:'ok'}}));
              }}>Subscribe</Btn>
              {!isService(e.actor) && (
                <Btn variant="ghost" size="sm" icon={<Ic.User/>} onClick={(ev) => {
                  ev.stopPropagation();
                  window.dispatchEvent(new CustomEvent('astro-toast',{detail:{msg:`Filtered to ${e.actor}`,tone:'ok'}}));
                }}>All by {e.actor}</Btn>
              )}
            </div>
          </div>
        )}
      </>
    );
  }
  const cellL = { padding: '4px 0', color: 'var(--ink-3)', width: 100, fontSize: 10, letterSpacing: '.04em', textTransform: 'uppercase', verticalAlign: 'top' };
  const cellR = { padding: '4px 0', fontSize: 11, wordBreak: 'break-word' };

  // ═══════════════════════════════════════════════════════════════════════
  // KPI strip
  // ═══════════════════════════════════════════════════════════════════════
  function AuditKpis({ events }) {
    const total = events.length;
    const today = events.filter(e => e.t.getTime() > NOW.getTime() - 86400000).length;
    const week = events.filter(e => e.t.getTime() > NOW.getTime() - 7 * 86400000).length;
    const services = events.filter(e => isService(e.actor)).length;
    const humans = total - services;
    const high = events.filter(e => e.severity === 'high').length;
    const tiles = [
      { l: 'EVENTS · 24H', v: fmtNum(today), sub: 'live stream' },
      { l: 'EVENTS · 7D', v: fmtNum(week), sub: `${fmtNum(total)} · 30d` },
      { l: 'BY HUMANS', v: fmtNum(humans), sub: `${Math.round(humans/total*100)}% of total` },
      { l: 'BY SERVICES', v: fmtNum(services), sub: `${Math.round(services/total*100)}% of total` },
      { l: 'HIGH-SEV', v: fmtNum(high), sub: high ? 'review urgent' : 'clean', tone: high ? 'warn' : 'ok' },
    ];
    return (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', border: '1px solid var(--rule)', marginBottom: 24 }}>
        {tiles.map((t, i) => (
          <div key={i} style={{
            padding: '18px 20px', borderRight: i < tiles.length - 1 ? '1px solid var(--rule)' : 'none',
            background: t.tone === 'warn' ? 'rgba(217,119,87,0.04)' : 'var(--paper, var(--bg))',
          }}>
            <div className="ff-mono upper" style={{ fontSize: 9, fontWeight: 600, letterSpacing: '.10em', color: 'var(--ink-3)', marginBottom: 6 }}>{t.l}</div>
            <div className="ff-display num" style={{ fontSize: 30, fontWeight: 700, letterSpacing: '-.02em', lineHeight: 1, color: t.tone === 'warn' ? 'var(--accent)' : 'var(--ink)' }}>{t.v}</div>
            <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 6 }}>{t.sub}</div>
          </div>
        ))}
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // ACTIVITY STRIP — last 30d sparkbars by category
  // ═══════════════════════════════════════════════════════════════════════
  function ActivityStrip({ events }) {
    // bucket per day per cat
    const days = [];
    for (let i = 29; i >= 0; i--) {
      const d = new Date(NOW.getTime() - i * 86400000);
      days.push(d.toISOString().slice(0, 10));
    }
    const byDay = Object.fromEntries(days.map(d => [d, 0]));
    events.forEach(e => {
      const k = dayKey(e.t);
      if (k in byDay) byDay[k]++;
    });
    const max = Math.max(...Object.values(byDay), 1);
    return (
      <div style={{ border: '1px solid var(--rule)', padding: '18px 20px', marginBottom: 24 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 14 }}>
          <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.10em' }}>30-DAY ACTIVITY · EVENTS PER DAY</div>
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>peak {fmtNum(max)} · avg {fmtNum(Math.round(events.length / 30))}/d</div>
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: 64 }}>
          {days.map(d => {
            const v = byDay[d];
            const h = (v / max) * 60;
            const isToday = d === NOW.toISOString().slice(0, 10);
            return (
              <div key={d} title={`${d} · ${fmtNum(v)} events`}
                style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', height: '100%', minWidth: 0 }}>
                <div style={{
                  height: h || 1,
                  background: isToday ? 'var(--accent)' : 'var(--ink-2)',
                  opacity: isToday ? 1 : 0.85,
                }}/>
              </div>
            );
          })}
        </div>
        <div className="ff-mono" style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, color: 'var(--ink-4)', marginTop: 6, letterSpacing: '.04em' }}>
          <span>{days[0]}</span>
          <span>{days[15]}</span>
          <span>TODAY</span>
        </div>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // MAIN
  // ═══════════════════════════════════════════════════════════════════════
  function ScreenAudit({ go, payload }) {
    const all = useMemo(buildLog, []);
    const [f, setF] = useState({ cat: 'all', actor: 'all', range: '7d', severity: 'all', q: '' });
    const [expanded, setExpanded] = useState(null);
    const [page, setPage] = useState(0);
    const PER_PAGE = 80;

    // accept ?actor= or ?entity= deep-link
    useEffect(() => {
      if (!payload) return;
      if (payload.actor) setF(s => ({ ...s, actor: payload.actor, range: '30d' }));
      if (payload.q) setF(s => ({ ...s, q: payload.q, range: '30d' }));
    }, [payload]);

    const filtered = useMemo(() => applyFilters(all, f), [all, f]);
    const visible = filtered.slice(0, (page + 1) * PER_PAGE);
    const grouped = useMemo(() => groupByDay(visible), [visible]);

    // count by cat for chips (using all, not filtered, so chips don't disappear)
    const catCounts = useMemo(() => {
      const c = {};
      const ranged = applyFilters(all, { ...f, cat: 'all' });
      ranged.forEach(e => {
        const k = ACTIONS[e.action]?.cat || 'workspace';
        c[k] = (c[k] || 0) + 1;
      });
      return c;
    }, [all, f]);

    const toast = (m) => window.dispatchEvent(new CustomEvent('astro-toast', { detail: { msg: m, tone: 'ok' } }));

    const exportCsv = () => {
      const rows = [['id','timestamp','actor','action','category','entity_kind','entity_id','entity_label','ctx','severity']];
      filtered.forEach(e => {
        const cat = ACTIONS[e.action]?.cat || 'workspace';
        rows.push([
          e.id, e.t.toISOString(), e.actor, e.action, cat,
          e.entity?.kind || '', e.entity?.id || '', e.entity?.label || '', e.ctx || '', e.severity || '',
        ]);
      });
      const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(',')).join('\n');
      try {
        const blob = new Blob([csv], { type: 'text/csv' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url; a.download = `astro-audit-${NOW.toISOString().slice(0,10)}.csv`;
        document.body.appendChild(a); a.click(); a.remove();
        URL.revokeObjectURL(url);
        toast(`Exported ${rows.length - 1} events as CSV`);
      } catch (err) {
        toast(`Export failed (${err.message})`);
      }
    };

    return (
      <div>
        {/* Header */}
        <div style={{ marginBottom: 24 }}>
          <Term path={['workspace','audit']}/>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: 14, gap: 24, flexWrap: 'wrap' }}>
            <div>
              <h1 className="heading-swap ff-display" style={{ fontSize: 56, fontWeight: 700, letterSpacing: '-.03em', lineHeight: 0.95, margin: 0 }}>
                Audit log
              </h1>
              <p className="ff-mono" style={{ fontSize: 12, color: 'var(--ink-2)', marginTop: 12, maxWidth: 700, lineHeight: 1.55 }}>
                Every change to the catalog, every transmission to a society, every claim filed and every door opened —
                immutable, queryable, exportable. The compliance surface for {fmtNum((window.CATALOG_STATS?.works?.total) || 18422)} works under management.
              </p>
            </div>
            <div style={{ display: 'flex', gap: 10 }}>
              <Btn variant="secondary" size="sm" icon={<Ic.Down/>} onClick={exportCsv}>Export CSV</Btn>
              <Btn variant="ghost" size="sm" icon={<Ic.Bell/>} onClick={() => toast('Subscribed to high-severity audit events')}>Subscribe</Btn>
            </div>
          </div>
        </div>

        <AuditKpis events={all} />
        <ActivityStrip events={all} />

        {/* Filter bar */}
        <div style={{ border: '1px solid var(--rule)', padding: '14px 16px', marginBottom: 18 }}>
          <div style={{ display: 'flex', gap: 16, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }}>
            {/* search */}
            <div style={{ position: 'relative', flex: '1 1 280px', minWidth: 240 }}>
              <input
                value={f.q}
                onChange={e => setF(s => ({ ...s, q: e.target.value }))}
                placeholder="Filter events… (id, actor, work, claim, ISWC, IP)"
                className="ff-mono"
                style={{ width:'100%', padding:'8px 12px 8px 32px', border:'1px solid var(--rule)', background:'var(--bg)', fontSize:12, outline:'none', boxSizing:'border-box' }}
              />
              <span style={{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)', color:'var(--ink-3)', fontSize:14 }}>⌕</span>
            </div>
            {/* range */}
            <div style={{ display:'inline-flex', border:'1px solid var(--rule)' }}>
              {['24h','7d','30d','all'].map((r, i) => (
                <button key={r} onClick={() => setF(s => ({ ...s, range: r }))}
                  className="ff-mono upper" data-active={f.range === r}
                  style={{
                    padding:'6px 12px', fontSize:10, fontWeight:600, letterSpacing:'.08em',
                    background: f.range === r ? 'var(--ink)' : 'transparent',
                    color: f.range === r ? 'var(--bg)' : 'var(--ink-2)',
                    border:'none', borderLeft: i ? '1px solid var(--rule)' : 'none', cursor:'pointer',
                  }}>{r === 'all' ? 'ALL' : r.toUpperCase()}</button>
              ))}
            </div>
            {/* actor type */}
            <div style={{ display:'inline-flex', border:'1px solid var(--rule)' }}>
              {[
                {k:'all', l:'EVERYONE'},
                {k:'humans', l:'HUMANS'},
                {k:'services', l:'SERVICES'},
              ].map((a, i) => (
                <button key={a.k} onClick={() => setF(s => ({ ...s, actor: a.k }))}
                  className="ff-mono upper" data-active={f.actor === a.k}
                  style={{
                    padding:'6px 12px', fontSize:10, fontWeight:600, letterSpacing:'.08em',
                    background: f.actor === a.k ? 'var(--ink)' : 'transparent',
                    color: f.actor === a.k ? 'var(--bg)' : 'var(--ink-2)',
                    border:'none', borderLeft: i ? '1px solid var(--rule)' : 'none', cursor:'pointer',
                  }}>{a.l}</button>
              ))}
            </div>
            {/* severity */}
            <button onClick={() => setF(s => ({ ...s, severity: s.severity === 'high' ? 'all' : 'high' }))}
              className="ff-mono upper" data-active={f.severity === 'high'}
              style={{
                padding:'6px 12px', fontSize:10, fontWeight:600, letterSpacing:'.08em',
                background: f.severity === 'high' ? 'var(--danger)' : 'transparent',
                color: f.severity === 'high' ? '#fff' : 'var(--danger)',
                border:'1px solid var(--danger)', cursor:'pointer',
              }}>HIGH-SEV ONLY</button>
            {(f.cat !== 'all' || f.actor !== 'all' || f.q || f.severity !== 'all' || f.range !== '7d') && (
              <button onClick={() => setF({ cat:'all', actor:'all', range:'7d', severity:'all', q:'' })}
                className="ff-mono upper"
                style={{ padding:'6px 12px', fontSize:10, fontWeight:600, letterSpacing:'.08em',
                  background:'transparent', color:'var(--ink-3)', border:'1px solid var(--rule)', cursor:'pointer' }}>
                CLEAR
              </button>
            )}
          </div>
          {/* category chips */}
          <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
            <FilterChip active={f.cat === 'all'} onClick={() => setF(s => ({ ...s, cat: 'all' }))}
              count={Object.values(catCounts).reduce((a,b)=>a+b,0)}>ALL</FilterChip>
            {Object.entries(CATS).map(([k, c]) => (
              <FilterChip key={k} active={f.cat === k} onClick={() => setF(s => ({ ...s, cat: k }))}
                count={catCounts[k] || 0} dot={c.dot}>{c.label.toUpperCase()}</FilterChip>
            ))}
            {/* actor pickers */}
            <span style={{ width:1, background:'var(--rule)', margin:'0 4px' }}/>
            {ACTORS.filter(a => !isService(a.id)).slice(0, 6).map(a => (
              <FilterChip key={a.id} active={f.actor === a.id} onClick={() => setF(s => ({ ...s, actor: a.id }))}>
                @{a.id}
              </FilterChip>
            ))}
          </div>
        </div>

        {/* Result count */}
        <div className="ff-mono upper" style={{ fontSize: 11, color: 'var(--ink-3)', letterSpacing: '.08em', marginBottom: 12, display: 'flex', justifyContent: 'space-between' }}>
          <span>SHOWING {fmtNum(visible.length)} OF {fmtNum(filtered.length)} EVENTS{filtered.length !== all.length ? ` · FROM ${fmtNum(all.length)} TOTAL` : ''}</span>
          <span>SORT · NEWEST FIRST</span>
        </div>

        {/* Table header */}
        <div className="ff-mono upper" style={{
          display: 'grid', gridTemplateColumns: '64px 110px 130px 1fr 130px 80px 28px',
          gap: 12, padding: '8px 16px', fontSize: 9, color: 'var(--ink-3)',
          letterSpacing: '.10em', fontWeight: 600,
          background: 'var(--bg-2)', border: '1px solid var(--rule)', borderBottom: 'none',
        }}>
          <span>TIME</span><span>ACTOR</span><span>ACTION</span><span>ENTITY</span>
          <span style={{ textAlign: 'right' }}>EVENT ID</span>
          <span style={{ textAlign: 'right' }}>VERB</span>
          <span/>
        </div>

        {/* Grouped rows */}
        <div style={{ border: '1px solid var(--rule)' }}>
          {filtered.length === 0 ? (
            <div style={{ padding: '60px 20px', textAlign: 'center', color: 'var(--ink-3)' }}>
              <div className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.08em', marginBottom: 8 }}>NO MATCHING EVENTS</div>
              <div style={{ fontSize: 13 }}>Try widening your filters or clearing the search.</div>
            </div>
          ) : grouped.map(([day, evs]) => (
            <div key={day}>
              <div className="ff-mono upper" style={{
                position: 'sticky', top: 0, zIndex: 2,
                background: 'var(--paper, var(--bg))', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)',
                padding: '10px 16px', fontSize: 10, fontWeight: 700, letterSpacing: '.10em', color: 'var(--ink-3)',
                display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
              }}>
                <span>{dayLabel(day)} · {day}</span>
                <span style={{ color: 'var(--ink-4)' }}>{fmtNum(evs.length)} EVENTS</span>
              </div>
              {evs.map(e => (
                <EventRow key={e.id} e={e} expanded={expanded === e.id}
                  onToggle={() => setExpanded(expanded === e.id ? null : e.id)} go={go}/>
              ))}
            </div>
          ))}
        </div>

        {/* Load more */}
        {visible.length < filtered.length && (
          <div style={{ display: 'flex', justifyContent: 'center', marginTop: 18 }}>
            <Btn variant="secondary" size="sm" onClick={() => setPage(p => p + 1)}>
              Load {Math.min(PER_PAGE, filtered.length - visible.length)} more
            </Btn>
          </div>
        )}

        {/* Footer / retention */}
        <div className="ff-mono" style={{ marginTop: 32, padding: '20px 24px', border: '1px solid var(--rule)', background: 'var(--bg-2)',
          display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24, fontSize: 11, color: 'var(--ink-3)' }}>
          <div>
            <div className="upper" style={{ fontSize: 9, fontWeight: 600, letterSpacing: '.10em', color: 'var(--ink-2)', marginBottom: 6 }}>RETENTION</div>
            <div>7y · immutable · WORM storage</div>
          </div>
          <div>
            <div className="upper" style={{ fontSize: 9, fontWeight: 600, letterSpacing: '.10em', color: 'var(--ink-2)', marginBottom: 6 }}>HASH</div>
            <div style={{ fontFamily: 'monospace', fontSize: 10 }}>SHA-256 chained · verified hourly</div>
          </div>
          <div>
            <div className="upper" style={{ fontSize: 9, fontWeight: 600, letterSpacing: '.10em', color: 'var(--ink-2)', marginBottom: 6 }}>EXPORT</div>
            <div>CSV · JSONL · SIEM webhook</div>
          </div>
          <div>
            <div className="upper" style={{ fontSize: 9, fontWeight: 600, letterSpacing: '.10em', color: 'var(--ink-2)', marginBottom: 6 }}>COMPLIANCE</div>
            <div>SOC 2 · ISO 27001 · GDPR Art. 30</div>
          </div>
        </div>
      </div>
    );
  }

  window.ScreenAudit = ScreenAudit;
})();
