/* TaskBoard.jsx — Task Board (Workspace › Tasks), real-wired & account-scoped.

   Replaces the pure front-end MOCK that lived at Pages2.jsx `function Tasks()`
   (const cols/cards, hardcoded stat literals, universal + unauthed — a live
   instance of Catch #40 / OL-056 and the account-scoping gap called out in
   _TASK-BOARD-ENHANCEMENT-SPEC.md §2/§8). This is the B0 render shell.

   ── Read-of-record (spec §6) ────────────────────────────────────────────────
   The board renders SaaS-side, engine-derived work items. The normalized render
   source is `site_asset_findings` (LIVE, RLS by account_id) extended with a
   `task_items` overlay (assignee / due / manual + cross-producer items). Both are
   served by ONE account-scoped endpoint that Lane 1 (api) ships as OL-099:

     GET /task-board?site_id=&status=&limit=   (requireJWT('wpsb-app'))
       200 { items: WorkItem[], counts?: { open, due_soon, overdue, completed_30d } }
       404 / 501 / { code:'MIGRATION_PENDING' | 'PROVISIONING' | 'NOT_IMPLEMENTED' }
           → the endpoint is not deployed in this environment yet.

   WorkItem (normalized — spec §4 "work-item shape"):
     { id, site_id, site, account_id, source_engine, source_finding_id,
       category, tags[], severity, pri, title, description, suggested_action,
       provenance_tier, effector_eligible, effector_op_type, status,
       external_ref, assignee, due, due_at, resolution_source }

   This app-only PR does NOT build the endpoint (write-lock: app only). Until
   OL-099 lands, the loader resolves the missing endpoint to an honest
   "coming online" state — NEVER a fabricated zero-board. The moment Lane 1
   ships /task-board with rows, the board populates automatically.

   ── Honest by construction (Radical Transparency, master §3 #27) ─────────────
   - Five phases: loading / loaded / empty / error / signedout — plus a distinct
     `provisioning` phase for "endpoint not deployed yet". A fetch failure or a
     non-2xx is an ERROR, never the empty state (Catch #40); empty is ONLY a 200
     with an empty item list.
   - Stat counts are DERIVED from the real items (or server-supplied `counts`),
     never hardcoded. Counts of real rows are honest facts, not estimates, so
     they carry no "estimated" label; a value/time-saved roll-up (which WOULD be
     estimated) is deferred to B3 and intentionally not rendered here.
   - Provenance enforced on READ: each item renders the provenance_tier the server
     returned (column compare, not inference). Default-deny — an absent tier is
     treated as tier-2 (private/owner). A tier-1 (public/competitive) item is
     badged; this consumer never widens tier (the endpoint scopes by account_id).
   - No silent drop: an item whose status does not map to a known lane lands in
     the Backlog lane and is counted in an "unmapped" note (spec §3 default-deny —
     a finding with no normalized intake path is a producer gap, made visible).

   ── Account scoping (spec §8 — ACTION REQUIRED at B0) ────────────────────────
   The endpoint is account-scoped via requireJWT('wpsb-app'); a customer /
   internal_partner sees only its OWN account's items. Staff (SA / support /
   admin / dev) see a given account's board via the token's resolved account
   context (View-As impersonation stamps app_metadata) — never a universal
   cross-account firehose. Nav visibility (tasks is in every NAV_<ROLE> +
   ALLOWED.<role>) never widens API scope; the server self-gates every read.

   ── Analytics (spec §8) ──────────────────────────────────────────────────────
   task.board.viewed / task.item.created / task.item.status_changed /
   task.item.pushed / task.item.fixed_via_effector / task.integration.connected
   are reserved for server-side trackEvent wiring on the api stream — NOT emitted
   from this app-only PR (same posture as ScanHistory.jsx).

   Pattern: IIFE, window.TaskBoard, CSS variables only (v2 tokens), WCAG 2.1 AA.
*/
(function () {
  'use strict';

  const { useState, useEffect, useMemo, useCallback } = React;
  const tx = (k, v, f) => (window.WPSB && window.WPSB.t) ? window.WPSB.t(k, v, f) : (f != null ? f : k);

  /* THE ONE shared empty state (wiz-canon §4.4, design/ActivationPrimitives.jsx).
     Was a local fork until the L11 consolidation. ActivationPrimitives.jsx loads
     at index.html:645, ahead of this file (:691), so the binding is resolved.
     The fork's ctaLabel/onCta + secondaryLabel/onSecondary pairs map onto the
     canonical primary/secondary descriptors; `icon` takes a rendered node
     because WIcon lacks warn/tasks. */
  const EmptyState = window.WPSB.Activation.EmptyState;

  const VERSION = '1.0.0';

  /* Canonical base resolution — matches ScanHistory/AuditData/Estimator. Never
     resolve to '' (that sends the fetch to the app's own Vercel origin → 404). */
  function apiBase() {
    return (window.WPSBD && window.WPSBD.apiBase)
      || (window.WPSB_CONFIG && window.WPSB_CONFIG.RAILWAY_URL)
      || window.RAILWAY_URL
      || 'https://api.wpsitebeam.io';
  }
  function authToken() {
    return (window.WPSB && window.WPSB.getToken && window.WPSB.getToken()) || null;
  }
  function switchTab(tab) {
    if (window.WPSBD && window.WPSBD.switchTab) window.WPSBD.switchTab(tab);
  }

  /* ── Lane model ───────────────────────────────────────────────────────────
     Canonical lanes + a synonym map so a finding-derived status (open/resolved)
     and a task_items workflow status (in_progress/closed) both resolve to a lane.
     An unknown status resolves to 'backlog' and is surfaced, never dropped. */
  const LANES = [
    { id: 'backlog', title: 'Backlog',     tone: '' },
    { id: 'todo',    title: 'To Do',       tone: 'beam' },
    { id: 'doing',   title: 'In Progress', tone: 'warn' },
    { id: 'review',  title: 'Review',      tone: 'beam' },
    { id: 'done',    title: 'Done',        tone: 'ok' },
  ];
  const STATUS_TO_LANE = {
    backlog: 'backlog', open: 'backlog', new: 'backlog', pending: 'backlog', triage: 'backlog',
    todo: 'todo', queued: 'todo', ready: 'todo', accepted: 'todo',
    doing: 'doing', in_progress: 'doing', 'in-progress': 'doing', active: 'doing', working: 'doing',
    review: 'review', reviewing: 'review', verifying: 'review', qa: 'review',
    done: 'done', resolved: 'done', complete: 'done', completed: 'done', closed: 'done', fixed: 'done',
  };
  function laneOf(item) {
    const s = String(item.status || '').toLowerCase();
    if (STATUS_TO_LANE[s]) return { lane: STATUS_TO_LANE[s], mapped: true };
    /* No workflow status → derive from finding lifecycle: resolved → done else backlog. */
    if (item.resolved_at || item.resolution_source) return { lane: 'done', mapped: true };
    if (s) return { lane: 'backlog', mapped: false };  /* unknown status: visible, flagged */
    return { lane: 'backlog', mapped: true };
  }

  const SEVERITY_TO_PRI = {
    critical: 'high', serious: 'high', high: 'high',
    moderate: 'med', medium: 'med', med: 'med',
    minor: 'low', info: 'low', low: 'low',
  };
  function priOf(item) {
    if (item.pri && SEVERITY_TO_PRI[String(item.pri).toLowerCase()]) return SEVERITY_TO_PRI[String(item.pri).toLowerCase()];
    const s = String(item.severity || '').toLowerCase();
    return SEVERITY_TO_PRI[s] || 'med';
  }
  const PRI_TONE = { high: 'bad', med: 'warn', low: '' };
  const PRI_RANK = { high: 0, med: 1, low: 2 };

  /* Defensive field readers — tolerate snake/camel so a server rename can't blank. */
  function pick(o, keys) {
    for (let i = 0; i < keys.length; i++) { if (o && o[keys[i]] != null && o[keys[i]] !== '') return o[keys[i]]; }
    return null;
  }
  function itemTitle(it) { return pick(it, ['title', 'message', 't', 'name']) || tx('page.taskboard.untitled', null, '(untitled)'); }
  function itemSite(it)  { return pick(it, ['site', 'host', 'domain', 'canonical_url', 'site_url', 'url']); }
  function itemTags(it)  {
    const t = pick(it, ['tags']);
    if (Array.isArray(t)) return t;
    const cat = pick(it, ['category', 'finding_kind', 'source_engine', 'kind']);
    return cat ? [String(cat)] : [];
  }
  function itemAssignee(it) { return pick(it, ['assignee', 'assignee_initials', 'owner']); }
  function itemDue(it)      { return pick(it, ['due', 'due_label']); }
  function itemDueAt(it)    { return pick(it, ['due_at', 'dueAt', 'due_date']); }
  function itemId(it)       { return pick(it, ['id', 'source_finding_id', 'external_ref']) || Math.random().toString(36).slice(2); }
  function itemTier(it)     { const t = pick(it, ['provenance_tier', 'provenanceTier']); return t == null ? 2 : Number(t); }
  function itemEffector(it) { return !!(it.effector_eligible || it.effectorEligible); }

  function prettyHost(u) {
    const raw = String(u || '');
    if (!raw || raw === '—') return null;
    try { return new URL(/^https?:\/\//.test(raw) ? raw : 'https://' + raw).hostname.replace(/^www\./, ''); }
    catch (_) { return raw.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/[\/?#].*$/, ''); }
  }
  function fmtDue(it) {
    const label = itemDue(it);
    if (label) return String(label);
    const at = itemDueAt(it);
    if (!at) return null;
    try { return new Date(at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); }
    catch (_) { return null; }
  }
  const DAY = 86400000;
  function dueBucket(it) {
    /* → 'overdue' | 'soon' (<=7d) | null. Only from a real due_at; a label alone
       ('Today') is not machine-comparable, so it is not counted as overdue. */
    const at = itemDueAt(it);
    if (!at) return null;
    const t = new Date(at).getTime();
    if (isNaN(t)) return null;
    const now = Date.now();
    if (t < now) return 'overdue';
    if (t - now <= 7 * DAY) return 'soon';
    return null;
  }

  /* ── Shared bits ──────────────────────────────────────────────────────────── */
  function ProvBadge({ tier }) {
    if (Number(tier) === 1) {
      return <span className="tag" title={tx('page.taskboard.tier1_title', null, 'Tier 1 — public/anonymous data (e.g. a competitive benchmark). Not private to this account.')}>{tx('page.taskboard.tier1_badge', null, 'TIER 1')}</span>;
    }
    return null; /* tier-2 (owner) is the default, unbadged, to keep cards quiet. */
  }

  function Skeleton() {
    return (
      <div className="card">
        <div className="card-body">
          {[0, 1, 2, 3].map(i => (
            <div key={i} aria-hidden="true"
              style={{ height: 40, borderRadius: 'var(--r-sm, 8px)', marginBottom: 10, background: 'var(--surface-2)', opacity: 1 - i * 0.18 }} />
          ))}
          <div style={{ fontSize: '.78rem', color: 'var(--dim)' }}>{tx('page.taskboard.loading', null, 'Loading task board…')}</div>
        </div>
      </div>
    );
  }

  /* ── Card ─────────────────────────────────────────────────────────────────── */
  function TaskCard({ item }) {
    const pri = priOf(item);
    const host = prettyHost(itemSite(item));
    const tags = itemTags(item);
    const assignee = itemAssignee(item);
    const due = fmtDue(item);
    const bucket = dueBucket(item);
    return (
      <div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, padding: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 6 }}>
          <span className="mono" style={{ fontSize: '.66rem', color: 'var(--muted)' }}>{String(itemId(item)).slice(0, 16)}</span>
          <span style={{ display: 'inline-flex', gap: 4, alignItems: 'center' }}>
            <ProvBadge tier={itemTier(item)} />
            <span className={'tag ' + PRI_TONE[pri]} style={{ fontSize: '.62rem', padding: '1px 6px' }}>{tx('page.taskboard.pri.' + pri, null, pri.toUpperCase())}</span>
          </span>
        </div>
        <div style={{ fontSize: '.82rem', fontWeight: 500, lineHeight: 1.4 }}>{itemTitle(item)}</div>
        {host && <div style={{ fontSize: '.7rem', color: 'var(--dim)' }}><Icon name="sites" size={11} /> {host}</div>}
        {tags.length > 0 && (
          <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
            {tags.slice(0, 4).map((t, i) => <span key={i} className="tag" style={{ fontSize: '.62rem', padding: '1px 5px' }}>{String(t)}</span>)}
          </div>
        )}
        {(assignee || due || itemEffector(item)) && (
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 2, gap: 6 }}>
            <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center' }}>
              {assignee && (
                <span style={{ width: 20, height: 20, borderRadius: '50%', background: 'var(--beam-dim)', color: 'var(--beam)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '.6rem', fontWeight: 700 }}>
                  {String(assignee).slice(0, 2).toUpperCase()}
                </span>
              )}
              {itemEffector(item) && (
                <span className="tag ok" style={{ fontSize: '.56rem', padding: '1px 5px' }} title={tx('page.taskboard.fixable_title', null, 'A one-click Fix is available for this item once the effector button ships (B2).')}>{tx('page.taskboard.fixable_badge', null, 'FIXABLE')}</span>
              )}
            </span>
            {due && <span className="mono" style={{ fontSize: '.66rem', color: bucket === 'overdue' ? 'var(--red)' : (bucket === 'soon' ? 'var(--warn)' : 'var(--dim)') }}>{due}</span>}
          </div>
        )}
      </div>
    );
  }

  /* ── Surface root ─────────────────────────────────────────────────────────── */
  function TaskBoard() {
    const [phase, setPhase] = useState('loading'); /* loading|loaded|empty|error|signedout|provisioning */
    const [items, setItems] = useState([]);
    const [serverCounts, setServerCounts] = useState(null);
    const [view, setView] = useState('kanban');
    const [site, setSite] = useState('__all__');

    const load = useCallback(async () => {
      const token = authToken();
      if (!token) { setPhase('signedout'); return; }
      setPhase('loading');
      try {
        const r = await fetch(apiBase() + '/task-board', { headers: { Authorization: 'Bearer ' + token } });
        /* 404 / 501 = the endpoint isn't deployed here yet → provisioning, NOT
           error and NOT empty (the render shell ships before OL-099). */
        if (r.status === 404 || r.status === 501) { setPhase('provisioning'); return; }
        let j = null; try { j = await r.json(); } catch (_) { j = null; }
        if (!r.ok) {
          const code = j && j.code;
          if (code === 'MIGRATION_PENDING' || code === 'PROVISIONING' || code === 'NOT_IMPLEMENTED') { setPhase('provisioning'); return; }
          setPhase('error'); return;   /* a real failure is an error, never empty */
        }
        const list = Array.isArray(j && j.items) ? j.items
          : (Array.isArray(j && j.tasks) ? j.tasks : (Array.isArray(j && j.findings) ? j.findings : []));
        setItems(list);
        setServerCounts(j && j.counts && typeof j.counts === 'object' ? j.counts : null);
        setPhase(list.length ? 'loaded' : 'empty');
      } catch (_) {
        setPhase('error');
      }
    }, []);

    useEffect(() => { load(); }, [load]);

    /* Site universe derived from real items. */
    const siteOptions = useMemo(() => {
      const set = new Set();
      items.forEach(it => { const h = prettyHost(itemSite(it)); if (h) set.add(h); });
      return ['__all__', ...Array.from(set).sort()];
    }, [items]);

    const visible = useMemo(() => (
      site === '__all__' ? items : items.filter(it => prettyHost(itemSite(it)) === site)
    ), [items, site]);

    /* Bucket into lanes (visible + priority-sorted). */
    const { byLane, unmapped } = useMemo(() => {
      const groups = {}; LANES.forEach(l => { groups[l.id] = []; });
      let un = 0;
      visible.forEach(it => { const r = laneOf(it); groups[r.lane].push(it); if (!r.mapped) un++; });
      Object.keys(groups).forEach(k => groups[k].sort((a, b) => PRI_RANK[priOf(a)] - PRI_RANK[priOf(b)]));
      return { byLane: groups, unmapped: un };
    }, [visible]);

    /* Derived stats — honest counts of real rows (never hardcoded, never estimated).
       Server-supplied counts win when present; otherwise derived from items. */
    const stats = useMemo(() => {
      const open = visible.filter(it => laneOf(it).lane !== 'done').length;
      const dueSoon = visible.filter(it => dueBucket(it) === 'soon').length;
      const overdue = visible.filter(it => dueBucket(it) === 'overdue').length;
      const done = byLane.done.length;
      return {
        open: serverCounts && serverCounts.open != null ? serverCounts.open : open,
        dueSoon: serverCounts && serverCounts.due_soon != null ? serverCounts.due_soon : dueSoon,
        overdue: serverCounts && serverCounts.overdue != null ? serverCounts.overdue : overdue,
        completed: serverCounts && serverCounts.completed_30d != null ? serverCounts.completed_30d : done,
      };
    }, [visible, byLane, serverCounts]);

    const header = (
      <PageHead crumb={tx('page.taskboard.crumb', null, 'Workflow')} title={tx('page.taskboard.title', null, 'Task Board')}
        sub={tx('page.taskboard.sub', null, 'Work items derived from your Scanner findings, Redirects, and Estimator proposals — scoped to your account. One board across every site.')}
        actions={<>
          {siteOptions.length > 1 && (
            <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 10px', background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 6 }}>
              <Icon name="sites" size={12} />
              <select value={site} onChange={e => setSite(e.target.value)} aria-label={tx('page.taskboard.filter_by_site', null, 'Filter by site')}
                style={{ background: 'transparent', border: 'none', color: 'var(--text)', fontSize: '.78rem', fontFamily: 'var(--font-mono)', outline: 'none', cursor: 'pointer' }}>
                {siteOptions.map(x => <option key={x} value={x} style={{ background: 'var(--surface)', color: 'var(--text)' }}>{x === '__all__' ? tx('page.taskboard.all_sites', null, 'All sites') : x}</option>)}
              </select>
            </div>
          )}
          <div className="tw-seg" role="tablist" aria-label={tx('page.taskboard.board_view', null, 'Board view')}>
            {[['kanban', tx('page.taskboard.view.kanban', null, 'Kanban')], ['list', tx('page.taskboard.view.list', null, 'List')], ['gantt', tx('page.taskboard.view.gantt', null, 'Timeline')]].map(([id, lbl]) => (
              <button key={id} role="tab" aria-selected={view === id} className={view === id ? 'active' : ''} onClick={() => setView(id)}>{lbl}</button>
            ))}
          </div>
          <button className="btn btn-ghost btn-sm" onClick={load}><Icon name="refresh" size={13} />{tx('page.taskboard.refresh', null, 'Refresh')}</button>
        </>} />
    );

    let body;
    if (phase === 'loading') {
      body = <Skeleton />;
    } else if (phase === 'signedout') {
      body = <EmptyState icon={<Icon name="lock" size={30} />} title={tx('page.taskboard.signedout_title', null, 'Sign in to view your task board')} body={tx('page.taskboard.signedout_body', null, 'Task items are private to your account.')} />;
    } else if (phase === 'error') {
      body = <EmptyState icon={<Icon name="warn" size={30} />} title={tx('page.taskboard.error_title', null, 'Couldn’t load the task board')}
        body={tx('page.taskboard.error_body', null, 'The board service couldn’t be reached just now — this is a load error, not an empty board. Retry below.')}
        primary={{ label: tx('page.taskboard.retry', null, 'Retry'), onClick: load }} />;
    } else if (phase === 'provisioning') {
      /* Honest "coming online" — the /task-board endpoint (Lane 1 / OL-099) isn’t
         deployed in this environment yet. NOT a fabricated zero-board. */
      body = (
        <EmptyState icon={<Icon name="clock" size={30} />} title={tx('page.taskboard.provisioning_title', null, 'Task board is coming online')}
          body={tx('page.taskboard.provisioning_body', null, 'Your work items are generated from Scanner findings, Redirects, and Estimator proposals. The board service is being provisioned — items will appear here automatically as soon as it’s live, no action needed.')}
          primary={{ label: tx('page.taskboard.open_scanner', null, 'Open Site Scanner'), onClick: () => switchTab('scanner') }}
          secondary={{ label: tx('page.taskboard.retry', null, 'Retry'), onClick: load }} />
      );
    } else if (phase === 'empty') {
      body = (
        <EmptyState icon={<Icon name="tasks" size={30} />} title={tx('page.taskboard.empty_title', null, 'No open work items')}
          body={tx('page.taskboard.empty_body', null, 'Nothing needs attention right now. As your Scanner surfaces findings, or you add redirects and proposals, work items appear here automatically — newest and highest-priority first.')}
          primary={{ label: tx('page.taskboard.run_scan', null, 'Run a scan'), onClick: () => switchTab('scanner') }} />
      );
    } else {
      body = (
        <>
          <div className="grid grid-4" style={{ marginBottom: 16 }}>
            <div className="stat"><div className="stat-lbl">{tx('page.taskboard.stat.open', null, 'Open')}</div><div className="stat-val">{stats.open}</div></div>
            <div className="stat"><div className="stat-lbl">{tx('page.taskboard.stat.due_this_week', null, 'Due this week')}</div><div className={'stat-val' + (stats.dueSoon ? ' warn' : '')}>{stats.dueSoon}</div></div>
            <div className="stat"><div className="stat-lbl">{tx('page.taskboard.stat.overdue', null, 'Overdue')}</div><div className={'stat-val' + (stats.overdue ? ' bad' : '')}>{stats.overdue}</div></div>
            <div className="stat"><div className="stat-lbl">{tx('page.taskboard.stat.completed_30d', null, 'Completed (30d)')}</div><div className={'stat-val' + (stats.completed ? ' ok' : '')}>{stats.completed}</div></div>
          </div>

          {unmapped > 0 && (
            <div style={{ marginBottom: 14, borderRadius: 10, border: '1px solid var(--border)', borderLeft: '4px solid var(--warn)', background: 'var(--surface-2)', padding: '10px 14px', fontSize: '.76rem', color: 'var(--dim)' }}>
              <Icon name="info" size={12} /> {unmapped === 1
                ? tx('page.taskboard.unmapped_one', { n: unmapped }, '{n} item arrived with a status we don’t map to a lane yet — shown in ')
                : tx('page.taskboard.unmapped_many', { n: unmapped }, '{n} items arrived with a status we don’t map to a lane yet — shown in ')}<strong>{tx('page.taskboard.lane.backlog', null, 'Backlog')}</strong>{tx('page.taskboard.unmapped_tail', null, ' so nothing is dropped.')}
            </div>
          )}

          {view === 'kanban' && (
            <div className="kanban" style={{ gridTemplateColumns: `repeat(${LANES.length}, minmax(0, 1fr))` }}>
              {LANES.map(c => (
                <div key={c.id} className="card" style={{ background: 'var(--surface-2)' }}>
                  <div className="card-head" style={{ padding: '10px 12px' }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                      <span className={'tag ' + c.tone}>{tx('page.taskboard.lane.' + c.id, null, c.title)}</span>
                      <span className="mono" style={{ fontSize: '.7rem', color: 'var(--dim)' }}>{byLane[c.id].length}</span>
                    </div>
                  </div>
                  <div className="card-body" style={{ padding: 8, display: 'flex', flexDirection: 'column', gap: 8 }}>
                    {byLane[c.id].length === 0
                      ? <div style={{ fontSize: '.7rem', color: 'var(--dim)', textAlign: 'center', padding: '10px 0' }}>—</div>
                      : byLane[c.id].map(it => <TaskCard key={itemId(it)} item={it} />)}
                  </div>
                </div>
              ))}
            </div>
          )}

          {view === 'list' && (
            <div className="card">
              <div className="card-body" style={{ padding: 0, overflowX: 'auto' }}>
                <table className="table" style={{ minWidth: 720 }}>
                  <thead><tr>
                    <th scope="col">{tx('page.taskboard.col.id', null, 'ID')}</th><th scope="col">{tx('page.taskboard.col.title', null, 'Title')}</th><th scope="col">{tx('page.taskboard.col.site', null, 'Site')}</th>
                    <th scope="col">{tx('page.taskboard.col.status', null, 'Status')}</th><th scope="col">{tx('page.taskboard.col.priority', null, 'Priority')}</th><th scope="col">{tx('page.taskboard.col.assignee', null, 'Assignee')}</th><th scope="col">{tx('page.taskboard.col.due', null, 'Due')}</th>
                  </tr></thead>
                  <tbody>
                    {LANES.flatMap(c => byLane[c.id].map(it => {
                      const pri = priOf(it); const host = prettyHost(itemSite(it)); const due = fmtDue(it);
                      return (
                        <tr key={itemId(it)}>
                          <td className="mono" style={{ fontSize: '.74rem', color: 'var(--muted)' }}>{String(itemId(it)).slice(0, 16)}</td>
                          <td style={{ fontWeight: 500 }}>{itemTitle(it)}</td>
                          <td className="mono" style={{ fontSize: '.74rem', color: 'var(--dim)' }}>{host || '—'}</td>
                          <td><span className={'tag ' + c.tone}>{tx('page.taskboard.lane.' + c.id, null, c.title)}</span></td>
                          <td><span className={'tag ' + PRI_TONE[pri]}>{tx('page.taskboard.pri.' + pri, null, pri.toUpperCase())}</span></td>
                          <td>{itemAssignee(it) ? String(itemAssignee(it)).slice(0, 2).toUpperCase() : '—'}</td>
                          <td className="mono" style={{ fontSize: '.74rem' }}>{due || '—'}</td>
                        </tr>
                      );
                    }))}
                  </tbody>
                </table>
              </div>
            </div>
          )}

          {view === 'gantt' && (
            <div className="card">
              <div className="card-head"><h2 className="card-title">{tx('page.taskboard.in_flight', null, 'In flight')}</h2></div>
              <div className="card-body">
                {(() => {
                  const flight = [...byLane.todo, ...byLane.doing, ...byLane.review];
                  if (flight.length === 0) return <div style={{ fontSize: '.8rem', color: 'var(--dim)' }}>{tx('page.taskboard.nothing_in_flight', null, 'Nothing in flight right now.')}</div>;
                  return flight.slice(0, 12).map((it, i) => {
                    const title = itemTitle(it); const due = fmtDue(it);
                    return (
                      <div key={itemId(it)} style={{ display: 'grid', gridTemplateColumns: '200px 1fr 90px', gap: 10, alignItems: 'center', padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
                        <div style={{ fontSize: '.82rem', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{title}</div>
                        <div style={{ height: 8, background: 'var(--surface-3)', borderRadius: 4, position: 'relative' }}>
                          <div style={{ position: 'absolute', left: `${(i * 7) % 60}%`, width: `${20 + (i % 5) * 6}%`, height: '100%', background: 'var(--beam)', borderRadius: 4 }} />
                        </div>
                        <span className="mono" style={{ fontSize: '.7rem', color: 'var(--dim)', textAlign: 'right' }}>{due || tx('page.taskboard.tbd', null, 'TBD')}</span>
                      </div>
                    );
                  });
                })()}
                <div style={{ fontSize: '.7rem', color: 'var(--dim)', marginTop: 10, lineHeight: 1.5 }}>
                  {tx('page.taskboard.gantt_note', null, 'Bars are a relative in-flight ordering, not a dated schedule — a true Gantt appears once items carry start/end dates.')}
                </div>
              </div>
            </div>
          )}
        </>
      );
    }

    return <div>{header}<div style={{ marginTop: 16 }}>{body}</div></div>;
  }

  window.TaskBoard = TaskBoard;
  window.WPSB = window.WPSB || {};
  window.WPSB.TaskBoard = TaskBoard;
  console.log('[WPSB TaskBoard] loaded v' + VERSION + ' — real account-scoped board, tri-state + provisioning. Endpoint: GET /task-board (OL-099).');
})();
