// ─── Dashboard screen ─── Wires up real data from /api/instance, /api/instance/usage/*, /api/invite-codes
function DashboardScreen({ setScreen, toast, instance, setInstance, onLogout }) {
  const [tab, setTab] = React.useState('overview');
  const [daily, setDaily] = React.useState([]);
  const [llmEvents, setLlmEvents] = React.useState([]);
  const [searchEvents, setSearchEvents] = React.useState([]);
  const [invites, setInvites] = React.useState({ codes: [], quota: 3, created: 0, remaining: 3 });
  const [actionLoading, setActionLoading] = React.useState('');

  async function refreshAll() {
    try {
      const [inst, d, llm, se, inv] = await Promise.all([
        apiRequest('/api/instance').catch(() => null),
        apiRequest('/api/instance/usage/daily').catch(() => []),
        apiRequest('/api/instance/usage/events?type=llm').catch(() => []),
        apiRequest('/api/instance/usage/events?type=search').catch(() => []),
        apiRequest('/api/invite-codes').catch(() => ({ codes: [], quota: 3, created: 0, remaining: 3 })),
      ]);
      if (inst) setInstance(inst);
      setDaily(Array.isArray(d) ? d : []);
      setLlmEvents(Array.isArray(llm) ? llm : []);
      setSearchEvents(Array.isArray(se) ? se : []);
      setInvites({
        codes: Array.isArray(inv.codes) ? inv.codes : [],
        quota: typeof inv.quota === 'number' ? inv.quota : 3,
        created: typeof inv.created === 'number' ? inv.created : 0,
        remaining: typeof inv.remaining === 'number' ? inv.remaining : 0,
      });
    } catch (ex) {
      if (ex.status === 401) onLogout();
    }
  }

  React.useEffect(() => { refreshAll(); }, []);

  async function doAction(action) {
    setActionLoading(action);
    try {
      await apiRequest(`/api/instance/${action}`, { method: 'POST' });
      toast(`操作成功: ${action}`);
      const inst = await apiRequest('/api/instance').catch(() => null);
      if (inst) setInstance(inst);
      if (action === 'restart' || action === 'start' || action === 'reset') {
        setScreen('booting');
      }
    } catch (ex) {
      toast(errText(ex.code || ex.message), 'error');
    } finally {
      setActionLoading('');
    }
  }

  function openConsole() {
    if (instance?.dashboard_url) {
      window.open(instance.dashboard_url, '_blank');
    }
  }

  async function generateInvite() {
    try {
      const data = await apiRequest('/api/invite-codes', { method: 'POST' });
      setInvites(s => ({
        ...s,
        codes: [{ code: data.code, created_at: data.created_at, used: false }, ...s.codes],
        created: s.created + 1,
        remaining: s.quota < 0 ? s.remaining : Math.max(0, s.remaining - 1),
      }));
      toast('已生成新邀请码');
    } catch (ex) {
      toast(errText(ex.code || ex.message), 'error');
    }
  }

  const ready = instance?.availability === 'ready';
  const uptime = formatUptime(instance?.created_at);
  const stats = computeStats(daily, uptime);

  return (
    <div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}>
      {/* Marquee status strip */}
      <div className="panel panel--flat mb-4" style={{ padding: '6px 12px', overflow: 'hidden', background: 'var(--ink)', color: 'var(--paper)' }}>
        <div className="marquee">
          <span>
            <span style={{ color: ready ? 'var(--lime)' : 'var(--amber)' }}>● {ready ? 'READY' : (instance?.availability || 'OFFLINE').toUpperCase()}</span>
            &nbsp;·&nbsp; 节点 {instance?.id ? ('xx-' + String(instance.id).slice(0, 6)) : '—'} &nbsp;·&nbsp;
            <span style={{ color: 'var(--shrimp)' }}>★ 今日</span> LLM {stats.today.requests} · Token {formatNum(stats.today.tokens)} · 搜索 {stats.today.searches} &nbsp;·&nbsp;
            <span style={{ color: 'var(--amber)' }}>◆ 总计</span> LLM {stats.total.requests} · Token {formatNum(stats.total.tokens)} &nbsp;·&nbsp;
            系统版本 v4.2.0 · {ready ? '运行正常' : '初始化中'} &nbsp;·&nbsp;
            <span style={{ color: ready ? 'var(--lime)' : 'var(--amber)' }}>● {ready ? 'READY' : 'INIT'}</span>
            &nbsp;·&nbsp; 节点 {instance?.id ? ('xx-' + String(instance.id).slice(0, 6)) : '—'} &nbsp;·&nbsp;
          </span>
        </div>
      </div>

      <div className="dashboard">
        {/* LEFT */}
        <div className="col-4" style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
          {/* Instance window */}
          <div className="win">
            <div className="win__bar">
              <div className="dots"><i/><i/><i/></div>
              <div className="title">my_instance</div>
              <div className="meta">{ready ? '●LIVE' : '·' + (instance?.status || 'INIT').toUpperCase()}</div>
            </div>
            <div className="win__body" style={{ padding: 16 }}>
              <div className="row mb-3" style={{ gap: 12 }}>
                <div style={{ flex: '0 0 auto', animation: 'shrimp-bob 3s ease-in-out infinite' }}>
                  <Shrimp size={72} mood={ready ? 'happy' : 'idle'}/>
                </div>
                <div className="col gap-2" style={{ flex: 1 }}>
                  <div className="tiny upper muted mono">云端电脑</div>
                  <div style={{ fontFamily: 'var(--font-display)', fontSize: 16 }}>xx-{String(instance?.id || '').slice(0, 6) || '------'}</div>
                  <span className={'chip ' + (ready ? 'chip--lime' : 'chip--amber')}>
                    <span className="dot"/> {ready ? 'READY' : (instance?.availability || 'LOADING').toUpperCase()}
                  </span>
                </div>
              </div>
              <button className="btn btn--primary btn--full" onClick={openConsole} disabled={!ready}>
                ▸ 打开控制台
              </button>
              <div className="row mt-3" style={{ gap: 6 }}>
                <button className="btn btn--sm flex-1" onClick={() => doAction('restart')} disabled={!ready || !!actionLoading}>
                  {actionLoading === 'restart' ? '...' : '重启'}
                </button>
                <button className="btn btn--sm flex-1" onClick={() => setScreen('files')} disabled={!ready}>文件</button>
                <button className="btn btn--sm flex-1" onClick={() => setScreen('term')} disabled={!ready || instance?.terminal_enabled === false}>终端</button>
              </div>
              <hr className="pxd"/>
              <div className="col gap-2 tiny mono muted">
                <div className="between"><span>STATUS</span><span style={{ color: 'var(--ink)' }}>{instance?.status || '—'}</span></div>
                <div className="between"><span>HEALTH</span><span style={{ color: 'var(--ink)' }}>{instance?.health_status || '—'}</span></div>
                <div className="between"><span>UPTIME</span><span style={{ color: 'var(--ink)' }}>{uptime.label}</span></div>
                <div className="between"><span>CREATED</span><span style={{ color: 'var(--ink)' }}>{formatDate(instance?.created_at)}</span></div>
              </div>
            </div>
          </div>

          {/* Bindings */}
          <div className="win">
            <div className="win__bar">
              <div className="dots"><i/><i/><i/></div>
              <div className="title">connections</div>
              <div className="meta">{countConnected(instance)}/2</div>
            </div>
            <div className="win__body" style={{ padding: 0 }}>
              <BindingRow kind="weixin" label="WeChat · 微信" instance={instance} setInstance={setInstance} ready={ready} toast={toast} color="var(--lime)"/>
              <BindingRow kind="feishu" label="Feishu · 飞书" instance={instance} setInstance={setInstance} ready={ready} toast={toast} color="var(--amber)"/>
            </div>
          </div>

          {/* Invite codes */}
          <div className="win">
            <div className="win__bar">
              <div className="dots"><i/><i/><i/></div>
              <div className="title">invites.my</div>
              <div className="meta">{invites.created}/{invites.quota < 0 ? '∞' : invites.quota}</div>
            </div>
            <div className="win__body" style={{ padding: 14 }}>
              <div className="between mb-2">
                <span className="tiny upper muted mono">剩余名额</span>
                <span className="chip chip--shrimp">{invites.quota < 0 ? '∞' : invites.remaining} / {invites.quota < 0 ? '∞' : invites.quota}</span>
              </div>
              <div className="col gap-2 mb-3">
                {invites.codes.length === 0 && (
                  <div className="tiny mono muted">{'// 还没生成过邀请码'}</div>
                )}
                {invites.codes.map(ic => (
                  <InviteRow key={ic.code} item={ic} toast={toast}/>
                ))}
              </div>
              <button
                className="btn btn--full btn--sm"
                onClick={generateInvite}
                disabled={invites.quota >= 0 && invites.remaining <= 0}
              >
                + 生成新邀请码
              </button>
            </div>
          </div>
        </div>

        {/* RIGHT */}
        <div className="col-8" style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
            <StatTile label="LLM 请求" value={formatNum(stats.total.requests)} sparks={stats.sparks.requests} variant="shrimp"/>
            <StatTile label="Token 用量" value={formatNum(stats.total.tokens)} sparks={stats.sparks.tokens} variant=""/>
            <StatTile label="搜索次数" value={formatNum(stats.total.searches)} sparks={stats.sparks.searches} variant="lime"/>
            <StatTile label="在线时长" value={uptime.short} sparks={stats.sparks.uptime} variant="azure"/>
          </div>

          <div className="win">
            <div className="win__bar">
              <div className="dots"><i/><i/><i/></div>
              <div className="title">usage.log</div>
              <div className="meta">LAST {daily.length}D</div>
            </div>
            <div className="win__body">
              <div className="tabs">
                {[['overview', '每日概览'], ['llm', 'LLM 事件'], ['search', '搜索事件']].map(([k, label]) => (
                  <button key={k} className={tab===k?'active':''} onClick={()=>setTab(k)}>{label}</button>
                ))}
              </div>
              {tab === 'overview' && <ChartOverview daily={daily}/>}
              {tab === 'llm' && <LlmEventTable rows={llmEvents}/>}
              {tab === 'search' && <SearchEventTable rows={searchEvents}/>}
            </div>
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
            <QuickCard icon="▣" title="文件管理" desc="浏览 /workspace, 上传下载" onClick={() => setScreen('files')} disabled={!ready}/>
            <QuickCard icon="▶_" title="Web 终端" desc="直接在浏览器里开 shell" onClick={() => setScreen('term')} disabled={!ready || instance?.terminal_enabled === false}/>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Binding row (WeChat/Feishu) ─── Handles QR flow with polling
function BindingRow({ kind, label, instance, setInstance, ready, toast, color }) {
  const [loading, setLoading] = React.useState(false);
  const [qrSrc, setQrSrc] = React.useState('');
  const [state, setState] = React.useState({
    status: (instance?.[`${kind}_status`]) || 'disconnected',
    message: '',
    session_key: '',
    qr_code_url: '',
  });
  const pollRef = React.useRef(0);

  // Sync from instance when it updates
  React.useEffect(() => {
    setState(s => ({ ...s, status: instance?.[`${kind}_status`] || 'disconnected' }));
  }, [instance, kind]);

  async function fetchStatus() {
    try {
      const data = await apiRequest(`/api/instance/${kind}/status`);
      setState(data);
      if (data.status === 'connecting' && data.session_key) startPolling(data.session_key);
    } catch { /* ignore */ }
  }

  async function start() {
    setLoading(true);
    try {
      const data = await apiRequest(`/api/instance/${kind}/login/start`, { method: 'POST' });
      setState({ ...data, status: 'connecting' });
      startPolling(data.session_key);
    } catch (ex) {
      toast(errText(ex.code || ex.message), 'error');
    } finally { setLoading(false); }
  }

  async function reconnect() {
    setLoading(true);
    try {
      const data = await apiRequest(`/api/instance/${kind}/reconnect`, { method: 'POST' });
      setState({ ...data, status: 'connecting' });
      startPolling(data.session_key);
    } catch (ex) {
      toast(errText(ex.code || ex.message), 'error');
    } finally { setLoading(false); }
  }

  async function disconnect() {
    setLoading(true);
    try {
      await apiRequest(`/api/instance/${kind}/disconnect`, { method: 'POST' });
      setState({ status: 'disconnected', message: '', session_key: '', qr_code_url: '' });
      setQrSrc('');
      toast(kind === 'weixin' ? '微信已断开' : '飞书已断开');
      const inst = await apiRequest('/api/instance').catch(() => null);
      if (inst) setInstance(inst);
    } catch (ex) {
      toast(errText(ex.code || ex.message), 'error');
    } finally { setLoading(false); }
  }

  function startPolling(sessionKey) {
    const myToken = ++pollRef.current;
    const delay = kind === 'weixin' ? 800 : 2000;
    const poll = async () => {
      if (myToken !== pollRef.current) return;
      try {
        const data = await apiRequest(`/api/instance/${kind}/login/wait`, {
          method: 'POST',
          body: JSON.stringify({ session_key: sessionKey }),
        });
        if (myToken !== pollRef.current) return;
        if (data.status === 'connected') {
          setState(s => ({ ...s, status: 'connected', qr_code_url: '' }));
          setQrSrc('');
          toast(kind === 'weixin' ? '微信连接成功' : '飞书连接成功');
          const inst = await apiRequest('/api/instance').catch(() => null);
          if (inst) setInstance(inst);
          return;
        }
        if (data.status === 'error') {
          setState(s => ({ ...s, status: 'error', message: data.message || '' }));
          toast(kind === 'weixin' ? '微信连接失败' : '飞书连接失败', 'error');
          return;
        }
      } catch { /* keep polling */ }
      setTimeout(poll, delay);
    };
    setTimeout(poll, 300);
  }

  // Render QR when qr_code_url changes
  React.useEffect(() => {
    if (!state.qr_code_url) { setQrSrc(''); return; }
    if (!window.QRCode?.toDataURL) return;
    window.QRCode.toDataURL(state.qr_code_url, {
      width: 220, margin: 2,
      color: { dark: '#16161d', light: '#f5efd8' },
    }).then(setQrSrc).catch(() => setQrSrc(''));
  }, [state.qr_code_url]);

  // Clean up polling on unmount
  React.useEffect(() => () => { pollRef.current++; }, []);

  const connected = state.status === 'connected';

  return (
    <>
      <div className="between" style={{ padding: '12px 16px', borderBottom: '1px dashed var(--paper-3)' }}>
        <div className="row" style={{ gap: 10 }}>
          <div style={{
            width: 28, height: 28,
            background: connected ? color : 'var(--paper-3)',
            border: '1.5px solid var(--line)',
            boxShadow: 'var(--sh-1)',
          }}/>
          <div className="col" style={{ gap: 2 }}>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 12 }}>{label}</div>
            <div className="tiny mono muted">
              {connected
                ? (state.account_id ? `account: ${state.account_id}` : '已连接')
                : (state.status === 'connecting' ? '扫码中...' : '未连接')}
            </div>
          </div>
        </div>
        {!ready ? (
          <span className="tiny mono muted">未就绪</span>
        ) : connected ? (
          <div className="row" style={{ gap: 4 }}>
            <button className="btn btn--sm" onClick={reconnect} disabled={loading}>重连</button>
            <button className="btn btn--sm" onClick={disconnect} disabled={loading}>断开</button>
          </div>
        ) : state.status === 'connecting' ? (
          <button className="btn btn--sm" onClick={disconnect} disabled={loading}>取消</button>
        ) : (
          <button className="btn btn--sm btn--primary" onClick={start} disabled={loading}>连接</button>
        )}
      </div>

      {state.status === 'connecting' && ready && (
        <div className="col" style={{ padding: '12px 16px', gap: 8, borderBottom: '1px dashed var(--paper-3)', background: 'var(--paper-2)' }}>
          <div className="tiny upper muted mono">请使用{kind === 'weixin' ? '微信' : '飞书'}扫描下方二维码</div>
          <div className="center" style={{ padding: 12, background: 'var(--paper)', border: '1.5px solid var(--line)', boxShadow: 'var(--sh-1)', borderRadius: 3 }}>
            {qrSrc ? (
              <img src={qrSrc} alt="QR" style={{ width: 180, height: 180, display: 'block', imageRendering: 'pixelated' }}/>
            ) : (
              <div style={{ width: 180, height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                <span className="tiny mono muted">生成二维码中...</span>
              </div>
            )}
          </div>
          <div className="tiny mono muted center">等待扫码确认中<span className="caret"/></div>
        </div>
      )}
    </>
  );
}

// ─── Invite row ───
function InviteRow({ item, toast }) {
  function copy() {
    const v = item.code;
    if (navigator.clipboard?.writeText) navigator.clipboard.writeText(v).then(()=>toast('已复制'));
    else {
      const el = document.createElement('textarea');
      el.value = v;
      document.body.appendChild(el);
      el.select();
      document.execCommand('copy');
      document.body.removeChild(el);
      toast('已复制');
    }
  }
  return (
    <div className="between" style={{ padding: '8px 10px', background: 'var(--paper-2)', borderRadius: 3, border: '1.5px solid var(--paper-3)' }}>
      <div className="col" style={{ gap: 1 }}>
        <span className="mono small" style={{ fontWeight: 600 }}>{item.code}</span>
        <span className="tiny muted mono">
          {item.used ? `已被 ${item.used_by_email || '他人'} 使用` : '未使用'}
        </span>
      </div>
      <div className="row" style={{ gap: 4 }}>
        <span className={'chip ' + (item.used ? 'chip--mute' : 'chip--lime')}>
          {item.used ? 'USED' : 'OPEN'}
        </span>
        {!item.used && (
          <button className="btn btn--sm btn--ghost" onClick={copy}>复制</button>
        )}
      </div>
    </div>
  );
}

function StatTile({ label, value, unit, sparks, variant }) {
  const cls = 'stat' + (variant ? ` stat--${variant}` : '');
  const max = Math.max(1, ...(sparks || [1]));
  return (
    <div className={cls}>
      <div className="stat__label">{label}</div>
      <div>
        <span className="stat__value">{value}</span>
        {unit && <span className="stat__unit">{unit}</span>}
      </div>
      <div className="stat__spark">
        {(sparks || []).map((v, i) => <i key={i} style={{ height: `${Math.max(6, (v/max)*100)}%` }}/>)}
      </div>
    </div>
  );
}

function QuickCard({ icon, title, desc, onClick, disabled }) {
  return (
    <button className="panel" onClick={onClick} disabled={disabled} style={{
      padding: 20, textAlign: 'left',
      cursor: disabled ? 'not-allowed' : 'pointer',
      opacity: disabled ? 0.55 : 1,
      display: 'flex', gap: 14, alignItems: 'center',
      fontFamily: 'inherit', width: '100%',
    }}>
      <div style={{
        width: 48, height: 48,
        background: 'var(--ink)', color: 'var(--paper)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: 'var(--font-display)', fontSize: 20,
        border: '1.5px solid var(--line)',
      }}>{icon}</div>
      <div className="col" style={{ gap: 2 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 14 }}>{title}</div>
        <div className="tiny mono muted">{desc}</div>
      </div>
      <div style={{ marginLeft: 'auto', fontFamily: 'var(--font-display)', color: 'var(--shrimp-d)' }}>→</div>
    </button>
  );
}

function ChartOverview({ daily }) {
  // Daily is sorted server-side; show up to last 14
  const data = (daily || []).slice(-14).map(d => ({
    d: (d.date || '').slice(5),  // "MM-DD"
    llm: d.llm_requests || 0,
    tok: Math.round((d.llm_total_tokens || 0) / 1000),
    s: (d.search_requests || 0) + (d.search_extract_requests || 0),
  }));
  if (data.length === 0) {
    return <div className="tiny mono muted" style={{ padding: 20 }}>{'// 暂无数据'}</div>;
  }
  const max = Math.max(1, ...data.map(d => Math.max(d.llm, d.tok, d.s)));
  return (
    <div>
      <div className="row" style={{ gap: 16, marginBottom: 12 }}>
        <LegendDot color="var(--shrimp)" label="LLM 请求"/>
        <LegendDot color="var(--azure)" label="Token (k)"/>
        <LegendDot color="var(--lime-d)" label="搜索"/>
      </div>
      <div style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${data.length}, 1fr)`,
        gap: 6, alignItems: 'end',
        height: 160,
        borderBottom: '1.5px solid var(--line)',
        padding: '0 4px',
      }}>
        {data.map((d, i) => (
          <div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%', justifyContent: 'flex-end' }}>
            <div style={{ display: 'flex', gap: 2, alignItems: 'flex-end', height: '100%', width: '100%', justifyContent: 'center' }}>
              <div title={`LLM ${d.llm}`} style={{ width: '30%', height: `${(d.llm/max)*100}%`, background: 'var(--shrimp)', border: '1.5px solid var(--line)' }}/>
              <div title={`Tok ${d.tok}k`} style={{ width: '30%', height: `${(d.tok/max)*100}%`, background: 'var(--azure)', border: '1.5px solid var(--line)' }}/>
              <div title={`Search ${d.s}`} style={{ width: '30%', height: `${(d.s/max)*100}%`, background: 'var(--lime-d)', border: '1.5px solid var(--line)' }}/>
            </div>
          </div>
        ))}
      </div>
      <div style={{
        display: 'grid', gridTemplateColumns: `repeat(${data.length}, 1fr)`, gap: 6,
        marginTop: 6, fontSize: 9, fontFamily: 'var(--font-mono)', color: 'var(--ink-mute)',
        textAlign: 'center',
      }}>
        {data.map((d, i) => <div key={i}>{d.d}</div>)}
      </div>
    </div>
  );
}
function LegendDot({ color, label }) {
  return (
    <div className="row" style={{ gap: 6 }}>
      <i style={{ display: 'inline-block', width: 12, height: 12, background: color, border: '1.5px solid var(--line)' }}/>
      <span className="tiny upper muted mono">{label}</span>
    </div>
  );
}

function LlmEventTable({ rows }) {
  if (!rows || rows.length === 0) {
    return <div className="tiny mono muted" style={{ padding: 20 }}>{'// 暂无事件'}</div>;
  }
  return (
    <table className="tbl">
      <thead><tr>
        <th>时间</th><th>模型</th><th>状态</th><th>延迟</th><th>Token</th>
      </tr></thead>
      <tbody>
        {rows.map(r => (
          <tr key={r.id}>
            <td>{formatTime(r.created_at)}</td>
            <td><span className="chip chip--shrimp" style={{ padding: '2px 6px' }}>{r.model || '—'}</span></td>
            <td>{r.status === 'success'
              ? <span style={{color:'var(--lime-d)'}}>● OK</span>
              : <span style={{color:'var(--shrimp-d)'}}>● {r.status}</span>}</td>
            <td className="mono">{r.latency_ms}ms</td>
            <td className="mono">{r.total_tokens || 0}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
function SearchEventTable({ rows }) {
  if (!rows || rows.length === 0) {
    return <div className="tiny mono muted" style={{ padding: 20 }}>{'// 暂无事件'}</div>;
  }
  return (
    <table className="tbl">
      <thead><tr>
        <th>时间</th><th>提供方</th><th>操作</th><th>结果数</th><th>状态</th>
      </tr></thead>
      <tbody>
        {rows.map(r => (
          <tr key={r.id}>
            <td>{formatTime(r.created_at)}</td>
            <td>{r.provider || '—'}</td>
            <td><span className="chip chip--lime" style={{ padding: '2px 6px' }}>{r.operation}</span></td>
            <td className="mono">{r.result_count || 0}</td>
            <td>{r.status === 'success'
              ? <span style={{color:'var(--lime-d)'}}>● OK</span>
              : <span style={{color:'var(--shrimp-d)'}}>● {r.status}</span>}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// ─── helpers ───
function formatUptime(iso) {
  if (!iso) return { label: '—', short: '—' };
  const start = new Date(iso).getTime();
  if (Number.isNaN(start)) return { label: '—', short: '—' };
  const ms = Date.now() - start;
  const d = Math.floor(ms / 86400000);
  const h = Math.floor((ms % 86400000) / 3600000);
  const m = Math.floor((ms % 3600000) / 60000);
  return {
    label: `${d}d ${h}h ${String(m).padStart(2, '0')}m`,
    short: d > 0 ? `${d}d ${h}h` : `${h}h ${m}m`,
  };
}
function formatDate(iso) {
  if (!iso) return '—';
  const dt = new Date(iso);
  if (Number.isNaN(dt.getTime())) return '—';
  const pad = n => String(n).padStart(2, '0');
  return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
}
function formatTime(iso) {
  if (!iso) return '—';
  const dt = new Date(iso);
  if (Number.isNaN(dt.getTime())) return '—';
  const pad = n => String(n).padStart(2, '0');
  return `${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
}
function formatNum(n) {
  const v = Number(n || 0);
  if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + 'M';
  if (v >= 1_000) return (v / 1_000).toFixed(1) + 'k';
  return String(v);
}
function countConnected(instance) {
  let c = 0;
  if (instance?.weixin_status === 'connected') c++;
  if (instance?.feishu_status === 'connected') c++;
  return c;
}
function computeStats(daily, uptime) {
  const arr = daily || [];
  const total = {
    requests: arr.reduce((s, d) => s + (d.llm_requests || 0), 0),
    tokens: arr.reduce((s, d) => s + (d.llm_total_tokens || 0), 0),
    searches: arr.reduce((s, d) => s + (d.search_requests || 0) + (d.search_extract_requests || 0), 0),
  };
  const today = arr[arr.length - 1] || {};
  // Sparklines: last 12 days, or pad with zeros
  const slice = arr.slice(-12);
  const pad = Array(Math.max(0, 12 - slice.length)).fill({});
  const series = [...pad, ...slice];
  return {
    total,
    today: {
      requests: today.llm_requests || 0,
      tokens: today.llm_total_tokens || 0,
      searches: (today.search_requests || 0) + (today.search_extract_requests || 0),
    },
    sparks: {
      requests: series.map(d => d.llm_requests || 0),
      tokens: series.map(d => Math.round((d.llm_total_tokens || 0) / 1000)),
      searches: series.map(d => (d.search_requests || 0) + (d.search_extract_requests || 0)),
      uptime: Array(12).fill(1).map((_, i) => i + 1),  // monotonic fake since uptime grows linearly
    },
  };
}

window.DashboardScreen = DashboardScreen;
