// pages.jsx — 모든 페이지 컴포넌트
// Cycles · Portfolio · Orders · Problems · Schedule · News · Memory · Backtest · System

const { useState: useP, useMemo: useM, useEffect: useE } = React;

/* ════════════════════════════════════════════════════════
   페이지 헤더 (공통)
   ════════════════════════════════════════════════════════ */
function PageHead({ title, sub, actions, right }) {
  return (
    <div className="page-head">
      <div className="page-head-left">
        <h1>{title}</h1>
        {sub && <div className="page-head-sub mono">{sub}</div>}
      </div>
      {(actions || right) && <div className="page-head-actions">{actions}{right}</div>}
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   사이클 페이지
   ════════════════════════════════════════════════════════ */
function parseCycleSummary(lastMessage) {
  if (!lastMessage) return null;
  // last_message is the Codex final assistant message. By convention it's JSON.
  // It may be truncated with "…" suffix; try parsing the trimmed version.
  let txt = lastMessage.trim();
  if (txt.endsWith('…')) txt = txt.slice(0, -1);
  try { return JSON.parse(txt); } catch (_) {}
  // Try extracting first {...} block
  const m = txt.match(/\{[\s\S]*\}/);
  if (m) { try { return JSON.parse(m[0]); } catch (_) {} }
  return null;
}

function decisionCounts(decisions) {
  const c = { BUY: 0, SELL: 0, HOLD: 0, UNCERTAIN: 0 };
  for (const d of decisions || []) {
    const a = (d.action || '').toUpperCase();
    if (c[a] != null) c[a]++;
  }
  return c;
}

function adaptCycleEvents(events, baseUtcIso) {
  const base = baseUtcIso ? new Date(baseUtcIso) : new Date();
  const out = [];
  let i = 0;
  for (const e of events || []) {
    i++;
    const t = new Date(+base + i * 800);
    if (e.kind === 'message') {
      out.push({ id: e.id || 'm' + i, kind: 'message', t, summary: (e.text || '').slice(0, 240) });
    } else if (e.kind === 'tool') {
      let result = e.output_preview;
      try { result = JSON.parse(e.output_preview); } catch (_) {}
      const argsPreview = e.arguments ? Object.entries(e.arguments).slice(0, 3).map(([k, v]) => `${k}=${typeof v === 'object' ? '…' : String(v).slice(0, 24)}`).join(', ') : '';
      out.push({
        id: e.id || 't' + i, kind: 'tool', t,
        tool: e.tool || e.server || 'tool',
        args: e.arguments || {},
        result,
        summary: argsPreview ? `(${argsPreview})` : (e.status || ''),
      });
    } else if (e.kind === 'cmd') {
      out.push({
        id: e.id || 'c' + i, kind: 'cmd', t,
        cmd: e.command || '',
        output: e.output_preview || '',
        summary: (e.command || '').split('\n')[0].slice(0, 80),
      });
    } else if (e.kind === 'web_search') {
      out.push({
        id: e.id || 'w' + i, kind: 'tool', t,
        tool: 'web_search', args: { query: e.query }, result: null,
        summary: `“${(e.query || '').slice(0, 80)}”`,
      });
    } else if (e.kind === 'file_change') {
      out.push({
        id: e.id || 'f' + i, kind: 'cmd', t,
        cmd: 'file_change',
        output: JSON.stringify(e.changes || [], null, 2),
        summary: 'file_change: ' + (e.changes?.[0]?.path || ''),
      });
    }
    // skip thread_started / turn_started / turn_completed for display
  }
  return out;
}

function CyclesPage() {
  const cyclesQ = useApi(() => API.cycles.list({ limit: 30 }), [], { refreshMs: 60000 });
  const items = (cyclesQ.data && cyclesQ.data.items) || [];
  const [selectedId, setSelected] = useP(null);

  useE(() => {
    if (!selectedId && items.length > 0) setSelected(items[0].cycle_id);
  }, [items, selectedId]);

  const [filter, setFilter] = useP('all');
  const filtered = items.filter(c => filter === 'all' ? true : filter === 'kr' ? c.market === 'KR' : c.market === 'US' || ['NASDAQ', 'NYSE', 'AMEX'].includes(c.market));
  const selected = items.find(c => c.cycle_id === selectedId) || items[0];

  if (cyclesQ.loading && items.length === 0) {
    return <div className="cycles-page"><PageHead title="사이클" /><div className="empty" style={{ padding: 40 }}>로딩 중…</div></div>;
  }
  if (items.length === 0) {
    return <div className="cycles-page"><PageHead title="사이클" sub="/cycles" /><div className="empty" style={{ padding: 40 }}>아직 사이클 로그가 없음</div></div>;
  }

  return (
    <div className="cycles-page">
      <PageHead
        title="사이클"
        sub={`/cycles · ${items.length} runs · 최신 ${fmtRelativeTime(items[0].mtime_utc)}`}
        right={<Segmented size="sm" value={filter} onChange={setFilter} options={[
          { value: 'all', label: '전체' }, { value: 'kr', label: 'KR' }, { value: 'us', label: 'US' },
        ]} />}
      />

      <div className="cycles-layout">
        <aside className="cycles-list">
          <div className="cycles-list-head">
            <span className="mono">최근 {filtered.length}개</span>
            <span className="mono" style={{ color: 'var(--text-4)' }}>{items.length}</span>
          </div>
          {filtered.map(c => {
            const sum = parseCycleSummary(c.last_message);
            const counts = decisionCounts(sum?.decisions);
            const total = counts.BUY + counts.SELL + counts.HOLD + counts.UNCERTAIN;
            const tokIn = c.usage?.input_tokens || 0;
            return (
              <button
                key={c.cycle_id}
                className={cx('cycles-list-item', c.cycle_id === selectedId && 'active')}
                onClick={() => setSelected(c.cycle_id)}
              >
                <div className="cycles-list-item-line1">
                  <MarketBadge market={c.market === 'US' ? 'NASDAQ' : c.market} />
                  <span className="mono" style={{ fontSize: 12, color: 'var(--text)' }}>
                    {c.started_at_utc ? new Date(c.started_at_utc).toLocaleDateString('ko-KR', { month: 'numeric', day: 'numeric', weekday: 'short' }) : '—'}
                  </span>
                  <span className="mono" style={{ marginLeft: 'auto', fontSize: 10.5, color: 'var(--text-3)' }}>
                    {c.started_at_utc ? fmtTimeShort(new Date(c.started_at_utc)) : ''}
                  </span>
                </div>
                {total > 0 && (
                  <div className="cycles-list-item-line2">
                    <ActionBar counts={counts} />
                  </div>
                )}
                <div className="cycles-list-item-line3 mono">
                  <span>{total}결정</span>
                  {counts.UNCERTAIN > 0 && <span style={{ color: 'var(--uncertain)' }}>⚠ {counts.UNCERTAIN}</span>}
                  <span style={{ marginLeft: 'auto', color: 'var(--text-4)' }}>
                    {tokIn ? Math.round(tokIn / 1000) + 'K' : '—'}
                  </span>
                </div>
              </button>
            );
          })}
        </aside>

        <section className="cycles-detail">
          {selected && <CycleDetail item={selected} />}
        </section>
      </div>
    </div>
  );
}

function CycleDetail({ item }) {
  const tl = useApi(() => API.cycles.timeline(item.cycle_id), [item.cycle_id]);
  const summary = useM(() => parseCycleSummary(item.last_message), [item]);
  const decisions = summary?.decisions || [];
  const counts = decisionCounts(decisions);
  const total = counts.BUY + counts.SELL + counts.HOLD + counts.UNCERTAIN;

  const events = tl.data?.events || [];
  const adaptedEvents = useM(() => adaptCycleEvents(events, item.started_at_utc), [events, item]);

  const toolCounts = useM(() => {
    const c = {};
    for (const e of events) {
      if (e.kind === 'tool') {
        const name = (e.server ? e.server + ':' : '') + (e.tool || 'tool');
        c[name] = (c[name] || 0) + 1;
      } else if (e.kind === 'cmd') {
        c['shell'] = (c['shell'] || 0) + 1;
      } else if (e.kind === 'web_search') {
        c['web_search'] = (c['web_search'] || 0) + 1;
      }
    }
    return c;
  }, [events]);

  return (
    <div className="cycle-detail">
      <div className="cycle-detail-head">
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
            <MarketBadge market={item.market === 'US' ? 'NASDAQ' : item.market} />
            <h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, letterSpacing: '-0.015em', whiteSpace: 'nowrap' }}>
              {item.market} 사이클
            </h2>
            {summary?.paper && <PaperBadge />}
          </div>
          <div className="mono" style={{ fontSize: 11.5, color: 'var(--text-3)', letterSpacing: 0 }}>
            {item.started_at_utc ? fmtAbsTime(new Date(item.started_at_utc)) : '—'} ·
            토큰 in {Math.round((item.usage?.input_tokens || 0) / 1000)}K / out {Math.round((item.usage?.output_tokens || 0) / 1000)}K ·
            <span style={{ fontSize: 10.5 }}> {item.cycle_id}</span>
          </div>
        </div>
        <ActionBar counts={counts} />
      </div>

      <div className="card">
        <div className="card-head">
          <div className="card-title">결정 <span className="card-title-sub">{total}건</span></div>
          <div style={{ display: 'flex', gap: 12, fontSize: 11, fontFamily: 'var(--font-mono)' }}>
            <span style={{ color: 'var(--buy)' }}>매수 {counts.BUY}</span>
            <span style={{ color: 'var(--sell)' }}>매도 {counts.SELL}</span>
            <span style={{ color: 'var(--text-3)' }}>관망 {counts.HOLD}</span>
            {counts.UNCERTAIN > 0 && <span style={{ color: 'var(--uncertain)' }}>검토 {counts.UNCERTAIN}</span>}
          </div>
        </div>
        {decisions.length > 0 ? (
          <div className="decisions-list">
            {decisions.map((d, i) => (
              <div key={d.symbol + ':' + i} className={cx('decision-row', d.action === 'UNCERTAIN' && 'uncertain')}>
                <ActionBadge action={d.action} size="sm" />
                <span className="sym mono">{d.symbol}</span>
                <MarketBadge market={d.market || item.market} />
                <span className="rationale">{d.rationale || d.reason || ''}</span>
                {d.qty != null && <span className="qty mono">×{d.qty}</span>}
              </div>
            ))}
          </div>
        ) : (
          <div className="empty">결정 JSON을 파싱할 수 없음 (last_message 형식이 다름)</div>
        )}
      </div>

      <div className="cycle-grid-2">
        <div className="card">
          <div className="card-head">
            <div className="card-title">활동 타임라인 <span className="card-title-sub">/cycle.events · {events.length}</span></div>
          </div>
          {tl.loading ? <div className="empty">로딩 중…</div> :
            tl.error ? <div className="empty" style={{ color: 'var(--warn)' }}>{tl.error.message}</div> :
            adaptedEvents.length === 0 ? <div className="empty">표시할 이벤트 없음</div> :
            <LLMTimeline events={adaptedEvents} />}
        </div>

        <div className="cycle-side">
          <div className="card">
            <div className="card-head">
              <div className="card-title">도구 호출 빈도 <span className="card-title-sub">/cycle.tools</span></div>
              <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>
                {Object.values(toolCounts).reduce((a, b) => a + b, 0)}회
              </span>
            </div>
            {Object.keys(toolCounts).length === 0 ? (
              <div className="empty" style={{ padding: 16, fontSize: 11.5 }}>없음</div>
            ) : (
              <ToolHistogram counts={toolCounts} />
            )}
          </div>

          <div className="card">
            <div className="card-head">
              <div className="card-title">사이클 메타</div>
            </div>
            <div className="cycle-meta-list mono">
              <div><span>시작</span><span>{item.started_at_utc ? fmtAbsTime(new Date(item.started_at_utc)) : '—'}</span></div>
              <div><span>크기</span><span>{fmtBytes(item.size_bytes)}</span></div>
              <div><span>토큰 in</span><span>{((item.usage?.input_tokens || 0) / 1000).toFixed(1)}K</span></div>
              <div><span>토큰 out</span><span>{((item.usage?.output_tokens || 0) / 1000).toFixed(1)}K</span></div>
              <div><span>cached</span><span>{((item.usage?.cached_input_tokens || 0) / 1000).toFixed(1)}K</span></div>
              <div><span>reasoning</span><span>{((item.usage?.reasoning_output_tokens || 0) / 1000).toFixed(1)}K</span></div>
              <div><span>ID</span><span style={{ fontSize: 10.5 }}>{item.cycle_id}</span></div>
            </div>
          </div>
        </div>
      </div>

      {summary && (
        <div className="card">
          <div className="card-head">
            <div className="card-title">최종 결정 JSON <span className="card-title-sub">/cycle.result</span></div>
          </div>
          <JSONViewer data={summary} />
        </div>
      )}
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   포트폴리오 페이지
   ════════════════════════════════════════════════════════ */
function PieDonut({ data, size = 200 }) {
  const total = data.reduce((a, b) => a + b.pct, 0) || 100;
  const r = size / 2 - 8;
  const cx_ = size / 2, cy = size / 2;
  let acc = 0;
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
      {data.map((d, i) => {
        const start = acc / total * 2 * Math.PI - Math.PI/2;
        acc += d.pct;
        const end = acc / total * 2 * Math.PI - Math.PI/2;
        const large = (end - start) > Math.PI ? 1 : 0;
        const x1 = cx_ + r * Math.cos(start), y1 = cy + r * Math.sin(start);
        const x2 = cx_ + r * Math.cos(end),   y2 = cy + r * Math.sin(end);
        const path = `M ${cx_},${cy} L ${x1},${y1} A ${r},${r} 0 ${large} 1 ${x2},${y2} Z`;
        return <path key={i} d={path} fill={d.color} stroke="var(--bg-elev-1)" strokeWidth="2" />;
      })}
      <circle cx={cx_} cy={cy} r={r*0.55} fill="var(--bg-elev-1)" />
    </svg>
  );
}

function PortfolioPage() {
  // Fallback only — actual rate comes from /api/fx/rates (Toss-backed, 60s
  // server cache). Picked deliberately stale-low so any UI showing the
  // fallback stands out vs the real ~1500 won range.
  const FX_FALLBACK = 1380;
  const [market, setMarket] = useP('all');
  const [modal, setModal] = useP(null);

  const summary   = useApi(() => API.portfolio.summary(),   [], { refreshMs: 60000 });
  const positions = useApi(() => API.portfolio.positions(), [], { refreshMs: 60000 });
  const goals     = useApi(() => API.goals.list(),          [], { refreshMs: 60000 });
  const fxQ       = useApi(() => API.fx.rates(),            [], { refreshMs: 300000 });

  const _num = (s) => { const v = parseFloat(s); return isNaN(v) ? 0 : v; };

  const fxRate  = fxQ.data && fxQ.data.ok !== false ? Number(fxQ.data.krw_per_usd) : null;
  const fxFresh = !!fxRate;
  const FX      = fxRate || FX_FALLBACK;

  const totals = useM(() => {
    const s = summary.data || {};
    const kr = s.kr || {}, us = s.us || {};
    const krEquity = kr.ok === false ? 0 : _num(kr.summary?.total_equity_krw);
    const krPnl    = kr.ok === false ? 0 : _num(kr.summary?.unrealized_pnl_krw);
    const usEquityUSD = us.ok === false ? 0 : _num(us.summary?.total_equity_usd);
    const usPnlUSD    = us.ok === false ? 0 : _num(us.summary?.unrealized_pnl_usd);
    return {
      krEquity, krPnl,
      usEquityUSD, usPnlUSD,
      usEquityKRW: usEquityUSD * FX,
      total: krEquity + usEquityUSD * FX,
      krOk: kr.ok !== false, usOk: us.ok !== false,
    };
  }, [summary.data, FX]);

  const holdings = useM(() => {
    const p = positions.data || {};
    const kr = (p.kr && p.kr.ok !== false) ? (p.kr.positions || []) : [];
    const us = (p.us && p.us.ok !== false) ? (p.us.positions || []) : [];
    const map = (arr, side) => arr.map(x => ({
      sym: x.symbol,
      name: x.name || x.symbol,
      market: x.market || (side === 'kr' ? 'KR' : 'NASDAQ'),
      qty: _num(x.qty), avg: _num(x.avg_price), cur: _num(x.current_price),
      marketValue: _num(x.market_value), pnl: _num(x.pnl), pct: _num(x.pnl_pct),
      currency: side === 'kr' ? 'KRW' : 'USD',
    }));
    return [...map(kr, 'kr'), ...map(us, 'us')];
  }, [positions.data]);

  const rows = useM(() => holdings.filter(h =>
    market === 'all' ? true : market === 'kr' ? h.market === 'KR' : h.market !== 'KR'
  ), [holdings, market]);

  const goalItems = (goals.data && goals.data.items) || [];
  const goalKr = goalItems.find(g => g.market === 'KR');
  const goalUs = goalItems.find(g => g.market === 'US');

  return (
    <div className="portfolio-page">
      <PageHead
        title="포트폴리오"
        sub={`/portfolio · ${holdings.length}개 보유 · 평가액 ${fmtKRWFull(Math.round(totals.total))}원 (US는 $1=${FX.toLocaleString(undefined,{maximumFractionDigits:2})}₩ ${fxFresh ? '실시간' : '대체값'})`}
      />

      {/* Goals — growth target per market (anchor → target, +pct%) */}
      <div className="card goals-card">
        <div className="card-head">
          <div className="card-title">성장 목표 <span className="card-title-sub">/goals · +pct% per market</span></div>
          <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{goalItems.length}/2 설정</span>
        </div>
        <div className="goals-grid">
          {['KR', 'US'].map((m) => (
            <GoalBlock
              key={m}
              market={m}
              item={m === 'KR' ? goalKr : goalUs}
              onReanchor={async (mkt) => {
                if (!confirm(`${mkt} 시장의 anchor를 현재 자산으로 다시 잡습니다.`)) return;
                const r = await API.goals.configSet({ market: mkt, reset_anchor: true });
                if (!r.ok) { alert('재설정 실패: ' + (r.error?.message || 'unknown')); return; }
                goals.reload();
              }}
            />
          ))}
        </div>
      </div>

      {/* Composition (simple horizontal bars) */}
      <div className="card">
        <div className="card-head">
          <div className="card-title">자산 구성 <span className="card-title-sub">/portfolio.allocation · 평가액 비중</span></div>
        </div>
        <CompositionBars
          slices={[
            { label: 'KR 주식', value: totals.krEquity, color: 'var(--m-kr)', currency: 'KRW' },
            { label: 'US 주식', value: totals.usEquityKRW, color: 'var(--m-us)', currency: 'KRW', sub: `$${totals.usEquityUSD.toFixed(2)}` },
          ].filter(s => s.value > 0)}
        />
        {!totals.krOk && (
          <div className="mono" style={{ fontSize: 11, color: 'var(--warn)', marginTop: 8 }}>KR 데이터 오류 — KIS 토큰 상태 확인</div>
        )}
        {!totals.usOk && (
          <div className="mono" style={{ fontSize: 11, color: 'var(--warn)', marginTop: 4 }}>US 데이터 오류 — KIS 토큰 상태 확인</div>
        )}
      </div>

      {/* Equity timeline placeholder */}
      <div className="card">
        <div className="card-head">
          <div className="card-title">평가액 시계열 <span className="card-title-sub">/portfolio.timeline · 히스토리 적립 중</span></div>
        </div>
        <div className="empty" style={{ padding: 32, flexDirection: 'column', gap: 6 }}>
          <span style={{ fontSize: 22 }}>📈</span>
          <div className="mono" style={{ fontSize: 11.5, color: 'var(--text-3)' }}>일별 스냅샷 저장 후 표시</div>
        </div>
      </div>

      {/* Holdings */}
      <div className="card">
        <div className="card-head">
          <div className="card-title">보유 종목 <span className="card-title-sub">/portfolio.holdings · {rows.length}</span></div>
          <Segmented size="sm" value={market} onChange={setMarket} options={[
            { value: 'all', label: '전체' }, { value: 'kr', label: 'KR' }, { value: 'us', label: 'US' },
          ]} />
        </div>
        {positions.loading && holdings.length === 0 ? (
          <div className="empty" style={{ padding: 24 }}>로딩 중…</div>
        ) : rows.length === 0 ? (
          <div className="empty" style={{ padding: 24 }}>보유 종목 없음</div>
        ) : (
          <div className="ptable">
            <div className="ptable-head">
              <span>종목</span>
              <span className="hide-mobile">수량 / 평균가</span>
              <span className="hide-mobile">현재가</span>
              <span>평가액</span>
              <span>손익</span>
            </div>
            {rows.map(h => {
              const evalNative = h.qty * h.cur;
              return (
                <button key={`${h.market}:${h.sym}`} className="ptable-row" onClick={() => setModal(h)}>
                  <span className="ptable-name">
                    <MarketBadge market={h.market} />
                    <span className="ptable-name-text">
                      <b>{h.name}</b>
                      <span className="mono">{h.sym}</span>
                    </span>
                  </span>
                  <span className="ptable-mid mono hide-mobile">
                    {h.qty}{h.currency === 'KRW' ? '주' : ''}<br />
                    @{h.currency === 'KRW' ? fmtKRWFull(h.avg) : '$' + h.avg.toFixed(2)}
                  </span>
                  <span className="ptable-mid mono hide-mobile">
                    {h.currency === 'KRW' ? fmtKRWFull(h.cur) : '$' + h.cur.toFixed(2)}
                  </span>
                  <span className="ptable-val mono tnum">
                    {h.currency === 'KRW' ? fmtKRW(evalNative) + '원' : '$' + evalNative.toFixed(2)}
                  </span>
                  <span className="ptable-pnl">
                    <DeltaChip value={h.pct} size="md" />
                    <span className="mono tnum" style={{ display: 'block', marginTop: 4, fontSize: 11, color: h.pnl >= 0 ? 'var(--buy)' : 'var(--sell)' }}>
                      {h.pnl >= 0 ? '+' : ''}{h.currency === 'KRW' ? fmtKRWFull(Math.round(h.pnl)) + '원' : h.pnl.toFixed(2) + '$'}
                    </span>
                  </span>
                </button>
              );
            })}
          </div>
        )}
      </div>

      {modal && <HoldingModal holding={modal} onClose={() => setModal(null)} />}
    </div>
  );
}

function GoalBlock({ market, item, onReanchor }) {
  // Shape (goal_preview / goal_config_get):
  //   pct, anchor_equity, target_equity, anchor_set_at_utc,
  //   last_advanced_at_utc, currency, status
  // Computed (only when /api/goals could fetch live equity):
  //   current_equity, distance_to_target, progress_pct, drawdown_pct,
  //   would_advance
  const isSet = item && item.status === 'set';
  const hasLive = isSet && item.current_equity != null;
  const currency = item ? item.currency : (market === 'KR' ? 'KRW' : 'USD');
  const fmt = currency === 'KRW'
    ? (v) => fmtKRWFull(Math.round(v)) + '원'
    : (v) => '$' + Number(v).toFixed(2);

  const pct = item ? parseFloat(item.pct || '0') : 0;
  const anchor = item ? parseFloat(item.anchor_equity || '0') : 0;
  const target = item ? parseFloat(item.target_equity || '0') : 0;
  const cur = hasLive ? parseFloat(item.current_equity || '0') : anchor;
  const progress = hasLive && target > 0
    ? Math.max(0, Math.min(100, (cur / target) * 100))
    : 0;
  const willAdvance = hasLive && (item.would_advance || progress >= 100);
  // Single accent color for everything — no defensive-mode coloring anymore.
  const color = willAdvance ? 'var(--success)' : 'var(--accent)';

  return (
    <div className="goal-block">
      <div className="goal-block-head">
        <MarketBadge market={market === 'US' ? 'NASDAQ' : 'KR'} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <b className="goal-block-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
            <span>{market === 'KR' ? '한국' : '미국'}</span>
            {isSet && (
              <span
                className="status-pill mono"
                style={{
                  color: 'var(--accent)',
                  background: 'color-mix(in oklch, var(--accent) 12%, transparent)',
                  borderColor: 'color-mix(in oklch, var(--accent) 30%, transparent)',
                  fontSize: 10,
                  padding: '1px 6px',
                  letterSpacing: 0,
                }}
                title={`매 달성마다 anchor=현재 자산, 다음 target=anchor +${pct}%`}
              >
                +{pct}%
              </span>
            )}
          </b>
          <span className="goal-block-sub mono">
            {isSet ? `→ ${fmt(target)}` : '아직 목표가 설정되지 않음'}
          </span>
        </div>
        {isSet && onReanchor && (
          <button
            className="subtle-link"
            title="현재 자산을 새 anchor로 잡기"
            onClick={() => onReanchor(market)}
            style={{ fontSize: 10.5 }}
          >
            re-anchor
          </button>
        )}
      </div>
      {isSet ? (
        <div className="goal-block-body" style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: 12 }}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
              <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>달성률</span>
              <b className="mono tnum" style={{ fontSize: 22, fontWeight: 700, color, lineHeight: 1, letterSpacing: '-0.02em' }}>
                {hasLive ? progress.toFixed(1) : '—'}
                <span style={{ fontSize: 13, fontWeight: 500, marginLeft: 2 }}>%</span>
              </b>
            </div>
            <div
              className="goal-progress-bar"
              role="progressbar"
              aria-valuenow={Math.round(progress)}
              aria-valuemin={0}
              aria-valuemax={100}
              aria-label={`목표 달성률 ${progress.toFixed(1)}%`}
            >
              <span style={{ width: progress + '%', background: color }} />
              <em className="tnum">
                {fmt(cur)} <span style={{ color: 'var(--text-3)', fontWeight: 400, margin: '0 4px' }}>/</span> {fmt(target)}
              </em>
            </div>
            {hasLive && parseFloat(item.distance_to_target || '0') > 0 && (
              <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)', display: 'flex', justifyContent: 'space-between', gap: 8 }}>
                <span>남은 금액</span>
                <span className="tnum" style={{ color: 'var(--text)' }}>
                  +{fmt(parseFloat(item.distance_to_target))}
                </span>
              </div>
            )}
            {willAdvance && (
              <div className="mono" style={{ fontSize: 11, color: 'var(--success)', display: 'flex', justifyContent: 'space-between', gap: 8 }}>
                <span>✓ 다음 사이클에서 목표 갱신 예정</span>
              </div>
            )}
          </div>
          <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', display: 'flex', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
            <span>anchor {fmt(anchor)}</span>
            {item.last_advanced_at_utc && <span title={item.last_advanced_at_utc}>직전 갱신 {fmtRelativeTime(item.last_advanced_at_utc)}</span>}
          </div>
        </div>
      ) : (
        <div className="empty" style={{ padding: 16, fontSize: 11.5, flexDirection: 'column', alignItems: 'stretch', gap: 10 }}>
          <div style={{ textAlign: 'center' }}>
            아직 anchor가 없습니다. 다음 시장 마감 Codex 사이클이 자동으로 잡거나, 지금 직접 잡을 수 있습니다.
          </div>
          {onReanchor && (
            <button
              onClick={() => onReanchor(market)}
              style={{
                padding: '8px 12px',
                fontSize: 12,
                fontWeight: 600,
                borderRadius: 'var(--r-2)',
                background: 'var(--accent)',
                color: 'oklch(0.99 0 0)',
                border: 'none',
                cursor: 'pointer',
              }}
              title="현재 자산을 anchor로 잡기"
            >
              지금 anchor 설정
            </button>
          )}
          <div className="mono" style={{ fontSize: 10, color: 'var(--text-4)', textAlign: 'center' }}>
            기본 +10% · KR 5만원 단위 / US $5 단위로 반올림
          </div>
        </div>
      )}
    </div>
  );
}

function CompositionBars({ slices }) {
  if (!slices || slices.length === 0) {
    return <div className="empty" style={{ padding: 16 }}>표시할 자산 없음</div>;
  }
  const total = slices.reduce((s, x) => s + x.value, 0) || 1;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      <div style={{ display: 'flex', height: 14, borderRadius: 99, overflow: 'hidden', background: 'var(--bg-inset)' }}>
        {slices.map(s => (
          <div key={s.label} style={{ background: s.color, width: ((s.value / total) * 100) + '%' }} title={s.label} />
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
        {slices.map(s => (
          <div key={s.label} className="mono" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--text-2)', fontSize: 11.5 }}>
              <span className="cal-dot" style={{ background: s.color }} />{s.label}
            </span>
            <span className="tnum" style={{ fontSize: 12 }}>
              {fmtKRW(s.value)}원
              {s.sub && <span style={{ color: 'var(--text-4)', marginLeft: 4 }}>({s.sub})</span>}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

function HoldingModal({ holding, onClose }) {
  const [tab, setTab] = useP('notes');
  const isKR = holding.market === 'KR';

  const memScope = { scope: 'stock', symbol: holding.sym, market: isKR ? 'KR' : 'US' };
  const memory = useApi(() => API.memory.read(memScope), [holding.sym, holding.market]);
  const news = useApi(() => API.news.stock({ symbol: holding.sym, market: isKR ? 'KR' : 'US', limit: 20 }), [holding.sym, holding.market], { skip: tab !== 'news' });

  useE(() => {
    const h = (e) => e.key === 'Escape' && onClose();
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [onClose]);

  const memBody = memory.data?.body || '';
  const memUpd = memory.data?.updated_at_utc;
  const newsItems = news.data?.items || [];
  const evalNative = holding.qty * holding.cur;

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <header className="modal-head">
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
            <MarketBadge market={holding.market} />
            <div style={{ minWidth: 0 }}>
              <h3 style={{ margin: 0, fontSize: 17, fontWeight: 600, letterSpacing: '-0.015em' }}>{holding.name}</h3>
              <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{holding.sym}</div>
            </div>
          </div>
          <button className="icon-btn" onClick={onClose} aria-label="닫기">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12" /></svg>
          </button>
        </header>

        <div className="modal-summary">
          <div className="modal-summary-item">
            <span>현재가</span>
            <b className="mono tnum">{isKR ? fmtKRWFull(holding.cur) + '원' : '$' + holding.cur.toFixed(4)}</b>
          </div>
          <div className="modal-summary-item">
            <span>평균가</span>
            <b className="mono tnum">{isKR ? fmtKRWFull(holding.avg) + '원' : '$' + holding.avg.toFixed(4)}</b>
          </div>
          <div className="modal-summary-item">
            <span>평가액</span>
            <b className="mono tnum">{isKR ? fmtKRW(evalNative) + '원' : '$' + evalNative.toFixed(2)}</b>
          </div>
          <div className="modal-summary-item">
            <span>손익률</span>
            <DeltaChip value={holding.pct} size="md" />
          </div>
        </div>

        <div className="modal-tabs">
          <Segmented value={tab} onChange={setTab} options={[
            { value: 'notes', label: `Codex 노트${memBody ? ' · ' + (memBody.length / 1024).toFixed(1) + 'KB' : ''}` },
            { value: 'news',  label: `뉴스` },
          ]} />
        </div>

        <div className="modal-body">
          {tab === 'notes' && (
            memory.loading ? <div className="empty">로딩 중…</div>
              : memory.error ? <div className="empty" style={{ color: 'var(--warn)' }}>{memory.error.message}</div>
              : memBody ? (
                <div>
                  <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', marginBottom: 8 }}>
                    {(memBody.length / 1024).toFixed(1)}KB · {memUpd ? fmtRelativeTime(memUpd) : '—'}
                  </div>
                  <pre className="mono" style={{ background: 'var(--bg-inset)', border: '1px solid var(--border-subtle)', borderRadius: 8, padding: '10px 12px', fontSize: 11.5, lineHeight: 1.55, color: 'var(--text-2)', whiteSpace: 'pre-wrap', margin: 0, maxHeight: 320, overflow: 'auto' }}>
                    {memBody}
                  </pre>
                </div>
              ) : <div className="empty">아직 종목 노트 없음 (Codex가 기록하면 표시)</div>
          )}
          {tab === 'news' && (
            news.loading ? <div className="empty">로딩 중…</div>
              : news.error ? <div className="empty" style={{ color: 'var(--warn)' }}>{news.error.message}</div>
              : newsItems.length === 0 ? <div className="empty">관련 뉴스 없음</div>
              : (
                <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                  {newsItems.map(n => (
                    <div key={n.news_id} className="news-row">
                      <SentimentBadge sentiment={n.sentiment} />
                      <div className="news-row-main">
                        <div className="news-headline">{n.title}</div>
                        <div className="news-meta mono">{n.source} · {fmtRelativeTime(n.published_at_utc)}</div>
                      </div>
                    </div>
                  ))}
                </div>
              )
          )}
        </div>
      </div>
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   주문 페이지 (3 탭)
   ════════════════════════════════════════════════════════ */
function StatusPill({ status }) {
  const k = (status || '').toString().toUpperCase();
  const meta = {
    'FILLED':           { c: 'var(--success)',  soft: 'var(--success-soft)',  label: '체결' },
    'QUEUED':           { c: 'var(--text-2)',   soft: 'var(--bg-elev-2)',     label: '대기' },
    'SUBMITTED':        { c: 'var(--info)',     soft: 'var(--info-soft)',     label: '제출' },
    'CANCEL_REQUESTED': { c: 'var(--warn)',     soft: 'var(--warn-soft)',     label: '취소 요청' },
    'CANCELLED':        { c: 'var(--text-4)',   soft: 'var(--bg-elev-2)',     label: '취소됨' },
    'FAILED':           { c: 'var(--critical)', soft: 'var(--critical-soft)', label: '실패' },
    // legacy mock labels
    '체결':       { c: 'var(--success)',  soft: 'var(--success-soft)',  label: '체결' },
    '대기':       { c: 'var(--text-2)',   soft: 'var(--bg-elev-2)',     label: '대기' },
    '처리 중':    { c: 'var(--info)',     soft: 'var(--info-soft)',     label: '처리 중' },
    '실패':       { c: 'var(--critical)', soft: 'var(--critical-soft)', label: '실패' },
    'PAPER':      { c: 'var(--warn)',     soft: 'var(--warn-soft)',     label: 'PAPER' },
    '취소됨':     { c: 'var(--text-4)',   soft: 'var(--bg-elev-2)',     label: '취소됨' },
  }[k] || { c: 'var(--text-3)', soft: 'var(--bg-elev-2)', label: status || '—' };
  return (
    <span className="status-pill mono" style={{ color: meta.c, background: meta.soft, borderColor: `color-mix(in oklch, ${meta.c} 28%, transparent)` }}>
      {meta.label}
    </span>
  );
}

function OrdersPage() {
  const [tab, setTab] = useP('queued');
  const [marketTab, setMarketTab] = useP('KR');

  // Intents = lite-queue rows (Codex → worker)
  const intentsQ = useApi(() => API.orders.intents({ limit: 200 }), [], { refreshMs: 30000 });
  const intents = (intentsQ.data && intentsQ.data.items) || [];

  // Reservations from PyKis side (worker-fired orders)
  const resvQ = useApi(() => API.orders.reservations(marketTab), [marketTab], { refreshMs: tab === 'submitted' ? 30000 : 0, skip: tab !== 'submitted' });

  const groupedByStatus = useM(() => {
    const g = { QUEUED: [], SUBMITTED: [], FILLED: [], CANCEL_REQUESTED: [], CANCELLED: [], FAILED: [] };
    for (const it of intents) {
      const s = (it.status || '').toUpperCase();
      if (g[s]) g[s].push(it);
    }
    return g;
  }, [intents]);

  const queued = groupedByStatus.QUEUED;
  const filledHistory = useM(() => intents.filter(i => ['FILLED', 'CANCELLED', 'FAILED'].includes((i.status || '').toUpperCase())), [intents]);

  return (
    <div className="orders-page">
      <PageHead title="주문" sub="/orders · 큐 + KIS 미체결 + 히스토리" />

      <div className="tabs">
        <Segmented value={tab} onChange={setTab} options={[
          { value: 'queued', label: `큐 ${groupedByStatus.QUEUED.length}` },
          { value: 'submitted', label: `KIS 미체결` },
          { value: 'history', label: `이력 ${filledHistory.length}` },
        ]} />
      </div>

      {tab === 'queued' && (
        <div className="kanban">
          {[
            { id: 'QUEUED', label: '대기 중', color: 'var(--text-2)' },
            { id: 'SUBMITTED', label: '제출됨', color: 'var(--info)' },
            { id: 'CANCEL_REQUESTED', label: '취소 요청', color: 'var(--warn)' },
            { id: 'FAILED', label: '실패', color: 'var(--critical)' },
          ].map(col => {
            const items = groupedByStatus[col.id] || [];
            return (
              <div key={col.id} className="kanban-col">
                <div className="kanban-col-head">
                  <span className="kanban-col-dot" style={{ background: col.color }} />
                  <b>{col.label}</b>
                  <span className="mono kanban-col-count">{items.length}</span>
                </div>
                <div className="kanban-col-body">
                  {items.length === 0 && <div className="kanban-empty">—</div>}
                  {items.map(o => {
                    const target = o.schedule_at_utc ? new Date(o.schedule_at_utc) : null;
                    const overdue = target && +target < Date.now();
                    const isQueued = col.id === 'QUEUED';
                    return (
                      <div key={o.id} className="kanban-card">
                        <div className="kanban-card-head">
                          <ActionBadge action={o.side} size="sm" />
                          <span className="mono" style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--text-3)' }}>#{o.id}</span>
                        </div>
                        <div className="kanban-card-sym mono">{o.symbol}</div>
                        <div className="kanban-card-meta mono">
                          <MarketBadge market={o.market} />
                          <span style={{ marginLeft: 'auto' }}>×{o.qty}</span>
                        </div>
                        <div className="kanban-card-eta">
                          {isQueued ? (
                            <>
                              <div className="kanban-card-eta-label mono">{overdue ? '예정 시각 경과' : '실행까지'}</div>
                              <div className="kanban-card-eta-time mono tnum" style={overdue ? { color: 'var(--warn)' } : undefined}>
                                {target ? (overdue ? '대기 중' : (() => { const d = (+target - Date.now()) / 1000; return d < 3600 ? Math.floor(d / 60) + 'm' : Math.floor(d / 3600) + 'h ' + Math.floor((d % 3600) / 60) + 'm'; })()) : '—'}
                              </div>
                            </>
                          ) : (
                            <div className="kanban-card-eta-label mono" style={col.id === 'FAILED' ? { color: 'var(--critical)' } : undefined}>{col.id === 'SUBMITTED' ? '제출 완료' : col.id === 'FAILED' ? '실행 실패' : '취소 요청됨'}</div>
                          )}
                          <div className="mono" style={{ fontSize: 10, color: 'var(--text-4)', marginTop: 2 }}>
                            {target ? fmtAbsTime(target) : '—'}
                          </div>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
      )}

      {tab === 'submitted' && (
        <div className="card">
          <div className="card-head">
            <div className="card-title">KIS 미체결 <span className="card-title-sub">/orders.reservations</span></div>
            <Segmented size="sm" value={marketTab} onChange={setMarketTab} options={[
              { value: 'KR', label: 'KR' }, { value: 'US', label: 'US' },
            ]} />
          </div>
          {resvQ.loading ? <div className="empty">로딩 중…</div> :
            resvQ.error ? <div className="empty" style={{ color: 'var(--warn)' }}>{resvQ.error.message}</div> :
            (() => {
              const submitted = (resvQ.data?.submitted) || [];
              const queuedAlt = (resvQ.data?.queued) || [];
              if (submitted.length === 0 && queuedAlt.length === 0) {
                return <div className="empty">{marketTab} 시장에 미체결 주문 없음</div>;
              }
              return (
                <div className="dtable">
                  <div className="dtable-head dtable-row-hist">
                    <span>시각</span><span>시장</span><span>종목</span><span>구분</span><span>수량</span><span>상태</span><span>참조 ID</span>
                  </div>
                  {[...queuedAlt, ...submitted].map((o, idx) => (
                    <div key={(o.reservation_id || 'r') + ':' + idx} className="dtable-row dtable-row-hist">
                      <span className="mono" style={{ fontSize: 11 }}>{o.schedule_at_utc ? fmtAbsTime(new Date(o.schedule_at_utc)).slice(5, 16) : '—'}</span>
                      <span><MarketBadge market={o.instrument?.market || marketTab} /></span>
                      <span className="mono">{o.instrument?.symbol || '—'}</span>
                      <span><ActionBadge action={o.side} size="sm" /></span>
                      <span className="mono tnum">{o.qty}</span>
                      <span><StatusPill status={o.status} /></span>
                      <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{o.reservation_id || '—'}</span>
                    </div>
                  ))}
                </div>
              );
            })()
          }
        </div>
      )}

      {tab === 'history' && (
        <div className="card">
          <div className="card-head">
            <div className="card-title">실행 이력 <span className="card-title-sub">최근 {filledHistory.length}건</span></div>
          </div>
          {filledHistory.length === 0 ? (
            <div className="empty">기록 없음</div>
          ) : (
            <div className="dtable">
              <div className="dtable-head dtable-row-hist">
                <span>시각</span><span>시장</span><span>종목</span><span>구분</span><span>수량</span><span>상태</span><span>참조 ID</span>
              </div>
              {filledHistory.map(o => (
                <div key={o.id} className="dtable-row dtable-row-hist">
                  <span className="mono" style={{ fontSize: 11 }}>{o.updated_at_utc ? fmtAbsTime(new Date(o.updated_at_utc)).slice(5, 16) : '—'}</span>
                  <span><MarketBadge market={o.market} /></span>
                  <span className="mono">{o.symbol}</span>
                  <span><ActionBadge action={o.side} size="sm" /></span>
                  <span className="mono tnum">{o.qty}</span>
                  <span><StatusPill status={o.status} /></span>
                  <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>#{o.id}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   문제 로그 페이지
   ════════════════════════════════════════════════════════ */
function parseProblemBody(body) {
  if (!body) return { message: '', context: null };
  // body shape from PROJECT_PROBLEMS.md:
  //   **Message**: <message>
  //
  //   **Context**: <pretty JSON or "-">
  //
  //   ---
  const messageMatch = body.match(/\*\*Message\*\*:\s*([\s\S]*?)(?:\n\s*\n\*\*Context\*\*|\n---|$)/);
  const message = messageMatch ? messageMatch[1].trim() : body.trim().slice(0, 200);
  const ctxMatch = body.match(/\*\*Context\*\*:\s*```json\n([\s\S]*?)\n```/);
  let context = null;
  if (ctxMatch) {
    try { context = JSON.parse(ctxMatch[1]); } catch (_) { context = ctxMatch[1]; }
  } else {
    const ctxPlain = body.match(/\*\*Context\*\*:\s*([\s\S]*?)(?:\n---|$)/);
    if (ctxPlain && ctxPlain[1].trim() !== '-') context = ctxPlain[1].trim();
  }
  return { message, context };
}

function ProblemsPage() {
  const [activeSet, setActiveSet] = useP(new Set(['CRITICAL', 'ERROR', 'WARN']));
  const [q, setQ] = useP('');
  const [openMap, setOpenMap] = useP({});

  const problemsQ = useApi(() => API.problems.list({ limit: 200 }), [], { refreshMs: 60000 });
  const all = (problemsQ.data && problemsQ.data.items) || [];

  // Normalize each problem into a stable shape
  const items = useM(() => all.map((p, idx) => {
    const parsed = parseProblemBody(p.body);
    return {
      id: (p.timestamp_utc || 'p') + ':' + idx,
      sev: (p.severity || '').toUpperCase(),
      msg: parsed.message,
      ctx: parsed.context,
      source: p.source || 'unknown',
      t: p.timestamp_utc ? new Date(p.timestamp_utc) : null,
    };
  }), [all]);

  const counts = useM(() => {
    const c = { CRITICAL: 0, ERROR: 0, WARN: 0, INFO: 0 };
    items.forEach(p => { if (c[p.sev] != null) c[p.sev]++; });
    return c;
  }, [items]);

  const filtered = items.filter(p =>
    activeSet.has(p.sev) &&
    (q ? ((p.msg || '').toLowerCase().includes(q.toLowerCase()) || (p.source || '').toLowerCase().includes(q.toLowerCase())) : true)
  );

  const toggle = (s) => {
    const next = new Set(activeSet);
    if (next.has(s)) next.delete(s); else next.add(s);
    setActiveSet(next);
  };

  return (
    <div className="problems-page">
      <PageHead title="문제 로그" sub={`/problems · ${items.length}건 · ${problemsQ.loading ? '로딩 중' : '60s 마다 새로고침'}`} />

      <div className="filters-bar">
        <div className="filter-chips">
          {['CRITICAL', 'ERROR', 'WARN', 'INFO'].map(s => (
            <button key={s} className={cx('filter-chip', activeSet.has(s) && 'active')} onClick={() => toggle(s)}>
              <SeverityBadge level={s} />
              <span className="mono">{counts[s]}</span>
            </button>
          ))}
        </div>
        <input
          className="search-input mono"
          placeholder="메시지 / 출처 검색…"
          value={q}
          onChange={(e) => setQ(e.target.value)}
        />
      </div>

      {items.length === 0 ? (
        <div className="card"><div className="empty" style={{ padding: 30 }}>아직 기록된 문제 없음</div></div>
      ) : (
        <div className="problems-list">
          {filtered.map(p => (
            <div key={p.id} className="problem-card">
              <button className="problem-card-head" onClick={() => setOpenMap(m => ({ ...m, [p.id]: !m[p.id] }))}>
                <SeverityBadge level={p.sev} />
                <div className="problem-card-text">
                  <div className="problem-card-msg">{p.msg}</div>
                  <div className="problem-card-meta mono">
                    <span>{p.t ? fmtAbsTime(p.t) : '—'}</span>
                    <span>·</span>
                    <span style={{ color: 'var(--accent)' }}>{p.source}</span>
                    <span>·</span>
                    <span>{p.t ? fmtRelativeTime(p.t.toISOString()) : ''}</span>
                  </div>
                </div>
                <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)' }}>{openMap[p.id] ? '▾' : '▸'}</span>
              </button>
              {openMap[p.id] && p.ctx != null && (
                <div className="problem-card-body">
                  {typeof p.ctx === 'object' ? <JSONViewer data={p.ctx} /> : <pre className="mono" style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{p.ctx}</pre>}
                </div>
              )}
            </div>
          ))}
          {filtered.length === 0 && <div className="empty">조건에 맞는 문제 없음</div>}
        </div>
      )}

      <div className="problems-foot mono">
        해결한 항목은 <code>PROJECT_PROBLEMS.md</code>를 직접 편집해서 지우세요.
      </div>
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   스케줄 페이지
   ════════════════════════════════════════════════════════ */
function SchedulePage() {
  const today = useM(() => new Date(), []);
  const scheduleQ = useApi(() => API.schedule.read(), [], { refreshMs: 60000 });
  const items = (scheduleQ.data && scheduleQ.data.items) || [];

  const jobs = useM(() => items.map(it => {
    const kind = scheduleKind(it.label);
    const next = cronNextRun(it.cron_expression, today);
    const lastT = it.last_run_utc ? new Date(it.last_run_utc) : null;
    return { ...it, kind, color: KIND_COLOR[kind] || KIND_COLOR.other, next, lastT };
  }), [items, today]);

  const startOfWeek = new Date(today); startOfWeek.setDate(today.getDate() - today.getDay());
  const days = Array.from({ length: 14 }, (_, i) => { const d = new Date(startOfWeek); d.setDate(d.getDate() + i); return d; });

  // For week calendar — compute next occurrences within 14 days
  const jobsByDay = useM(() => {
    const m = {};
    for (const j of jobs) {
      if (!j.cron_expression || j.cron_expression === '@reboot') continue;
      // walk forward day-by-day
      let cursor = new Date(today);
      for (let i = 0; i < 14; i++) {
        const next = cronNextRun(j.cron_expression, cursor);
        if (!next) break;
        const dayKey = next.toDateString();
        if (!m[dayKey]) m[dayKey] = [];
        if (!m[dayKey].find(x => x.label === j.label)) m[dayKey].push(j);
        cursor = new Date(+next + 60000);
        if (+next > +days[days.length - 1] + 86400000) break;
      }
    }
    return m;
  }, [jobs, today, days]);

  // 24h dial jobs (today ± 1 day)
  const dialJobs = useM(() => {
    const out = [];
    for (const j of jobs) {
      if (j.lastT) out.push({ t: j.lastT, label: j.label + ' (완료)', color: j.color, key: j.label + '-last' });
      if (j.next) out.push({ t: j.next, label: j.label, color: j.color, key: j.label + '-next' });
    }
    return out;
  }, [jobs]);

  const nextJob = jobs.slice().sort((a, b) => (a.next ? +a.next : Infinity) - (b.next ? +b.next : Infinity)).find(j => j.next && j.next > today);

  return (
    <div className="schedule-page">
      <PageHead title="스케줄" sub={`/schedule · ${jobs.length}개 잡 · cron 기반`} />

      <div className="sched-top-grid">
        <div className="card">
          <div className="card-head">
            <div className="card-title">24시간 시계 <span className="card-title-sub">/clock · 오늘</span></div>
            <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{fmtAbsTime(today).slice(0, 16)}</span>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
            <ClockDial now={today} jobs={dialJobs} size={240} />
            <div className="cal-legend mono" style={{ justifyContent: 'center' }}>
              <span><span className="cal-dot" style={{ background: KIND_COLOR.kr_cycle }} />KR 사이클</span>
              <span><span className="cal-dot" style={{ background: KIND_COLOR.us_cycle }} />US 사이클</span>
              <span><span className="cal-dot" style={{ background: KIND_COLOR.kr_trade }} />KR 워커</span>
              <span><span className="cal-dot" style={{ background: KIND_COLOR.us_trade }} />US 워커</span>
              <span><span className="cal-dot" style={{ background: KIND_COLOR.backtest }} />백테스트</span>
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-head">
            <div className="card-title">다음 자동 실행</div>
            {nextJob && <span className="cal-dot" style={{ background: nextJob.color }} />}
          </div>
          {nextJob ? (
            <div className="sched-next-big">
              <div className="sched-next-count mono">
                <span className="big tnum">{relUntil(nextJob.next, today)}</span>
                <span className="small">후</span>
              </div>
              <div className="sched-next-name">{nextJob.label}</div>
              <div className="sched-next-time mono">{fmtAbsTime(nextJob.next)}</div>
              <div className="mono" style={{ marginTop: 12, padding: '8px 10px', background: 'var(--bg-inset)', borderRadius: 'var(--r-2)', border: '1px solid var(--border-subtle)', fontSize: 10.5, color: 'var(--text-3)' }}>
                cron · <span style={{ color: 'var(--text-2)' }}>{nextJob.cron_expression}</span>
              </div>
              {nextJob.lastT && (
                <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 10, paddingTop: 10, borderTop: '1px dashed var(--border-subtle)' }}>
                  <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>마지막 실행</span>
                  <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-2)' }}>{fmtRelativeTime(nextJob.lastT.toISOString())}</span>
                </div>
              )}
            </div>
          ) : (
            <div className="empty">예약된 잡 없음</div>
          )}
        </div>
      </div>

      <div className="card">
        <div className="card-head">
          <div className="card-title">주간 캘린더 <span className="card-title-sub">이번 주 + 다음 주</span></div>
        </div>
        <div className="cal-grid">
          {['일', '월', '화', '수', '목', '금', '토'].map(d => (
            <div key={d} className="cal-dow mono">{d}</div>
          ))}
          {days.map(d => {
            const isToday = d.toDateString() === today.toDateString();
            const isPast = d < today && !isToday;
            const list = jobsByDay[d.toDateString()] || [];
            return (
              <div key={d.toString()} className={cx('cal-day', isToday && 'today', isPast && 'past')}>
                <div className="cal-day-num mono">{d.getDate()}</div>
                <div className="cal-day-dots">
                  {list.slice(0, 4).map((j, i) => <span key={i} className="cal-dot-job" style={{ background: j.color }} title={j.label} />)}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      <div className="card">
        <div className="card-head">
          <div className="card-title">잡 카드 <span className="card-title-sub">{jobs.length}개</span></div>
        </div>
        {jobs.length === 0 ? <div className="empty" style={{ padding: 20 }}>등록된 잡 없음</div> : (
          <div className="job-grid">
            {jobs.map(j => (
              <div key={j.label} className="job-card">
                <div className="job-card-head">
                  <span className="cal-dot" style={{ background: j.color }} />
                  <b>{j.label}</b>
                  <span className="mono" style={{ marginLeft: 'auto', fontSize: 10, color: j.lastT ? 'var(--success)' : 'var(--text-3)' }}>
                    {j.lastT ? '실행 기록 있음' : '미실행'}
                  </span>
                </div>
                <div className="job-card-body">
                  <div className="job-card-row"><span>크론</span><span className="mono">{j.cron_expression}</span></div>
                  <div className="job-card-row"><span>다음 실행</span><span className="mono">{j.next ? relUntil(j.next, today) + ' 후' : '—'}</span></div>
                  <div className="job-card-row"><span>마지막 실행</span><span className="mono">{j.lastT ? fmtRelativeTime(j.lastT.toISOString()) : '—'}</span></div>
                  <div className="job-card-row"><span>로그 크기</span><span className="mono">{fmtBytes(j.log_bytes)}</span></div>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   뉴스 페이지
   ════════════════════════════════════════════════════════ */
function SentimentBadge({ sentiment }) {
  const meta = {
    POSITIVE:     { label: 'POS',    c: 'var(--buy)' },
    NEGATIVE:     { label: 'NEG',    c: 'var(--sell)' },
    NEUTRAL:      { label: 'NEU',    c: 'var(--text-3)' },
    NOT_DETECTED: { label: 'N/D',    c: 'var(--text-4)' },
    SHORT_NEWS:   { label: 'SHORT',  c: 'var(--info)' },
  }[sentiment] || { label: sentiment, c: 'var(--text-3)' };
  return (
    <span className="sent-badge mono" style={{ color: meta.c, borderColor: `color-mix(in oklch, ${meta.c} 30%, transparent)`, background: `color-mix(in oklch, ${meta.c} 10%, transparent)` }}>
      {meta.label}
    </span>
  );
}

function sentimentColor(s) {
  const k = (s || '').toUpperCase();
  if (k === 'POSITIVE') return 'var(--buy)';
  if (k === 'NEGATIVE') return 'var(--sell)';
  if (k === 'NEUTRAL') return 'var(--text-3)';
  return 'var(--text-4)';
}

function NewsCard({ n, onOpen, readBadge }) {
  return (
    <article className="news-pin" style={{ '--strip': sentimentColor(n.sentiment) }} onClick={onOpen}>
      <div className="news-pin-strip" />
      <div className="news-pin-body">
        <div className="news-pin-meta mono">
          <SentimentBadge sentiment={n.sentiment} />
          {n.related_stocks && n.related_stocks[0] && <MarketBadge market={n.related_stocks[0].market || 'KR'} />}
          <span style={{ color: 'var(--text-3)' }}>{n.source || n.source_feed || '—'}</span>
          <span style={{ color: 'var(--text-4)' }}>·</span>
          <span style={{ color: 'var(--text-3)' }}>{fmtRelativeTime(n.published_at_utc)}</span>
          {readBadge && <span className="mono" style={{ marginLeft: 'auto', fontSize: 9.5, color: 'var(--accent)' }}>Codex 읽음</span>}
        </div>
        <div className="news-pin-headline">{n.title}</div>
        {n.summary && <div style={{ marginTop: 6, fontSize: 11.5, color: 'var(--text-3)', lineHeight: 1.5 }}>{n.summary.slice(0, 140)}</div>}
      </div>
    </article>
  );
}

function NewsDetailModal({ newsId, onClose }) {
  const q = useApi(() => API.news.get(newsId), [newsId]);
  useE(() => {
    const h = (e) => e.key === 'Escape' && onClose();
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [onClose]);
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <header className="modal-head">
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
            <SentimentBadge sentiment={q.data?.sentiment} />
            <div style={{ minWidth: 0 }}>
              <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, lineHeight: 1.3 }}>{q.data?.title || newsId}</h3>
              <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{q.data?.source || ''} · {q.data?.published_at_utc ? fmtAbsTime(new Date(q.data.published_at_utc)) : '—'}</div>
            </div>
          </div>
          <button className="icon-btn" onClick={onClose}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12" /></svg>
          </button>
        </header>
        <div style={{ padding: '0 16px 16px', maxHeight: '60vh', overflow: 'auto' }}>
          {q.loading ? <div className="empty">로딩 중…</div> :
            q.error ? <div className="empty" style={{ color: 'var(--warn)' }}>{q.error.message}</div> :
            (
              <>
                {q.data?.summary && <p style={{ fontSize: 12.5, color: 'var(--text-2)', lineHeight: 1.6, margin: '12px 0' }}>{q.data.summary}</p>}
                <pre className="mono" style={{ background: 'var(--bg-inset)', border: '1px solid var(--border-subtle)', borderRadius: 8, padding: '12px 14px', fontSize: 11.5, lineHeight: 1.55, color: 'var(--text-2)', whiteSpace: 'pre-wrap', margin: 0 }}>
                  {q.data?.body || '— 본문 없음'}
                </pre>
                {q.data?.related_stocks_resolved && q.data.related_stocks_resolved.length > 0 && (
                  <div style={{ marginTop: 12 }}>
                    <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', marginBottom: 6 }}>관련 종목</div>
                    <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                      {q.data.related_stocks_resolved.map(s => (
                        <span key={s.market + ':' + s.symbol} className="mono" style={{ background: 'var(--bg-elev-2)', border: '1px solid var(--border-subtle)', borderRadius: 6, padding: '4px 8px', fontSize: 11 }}>
                          <MarketBadge market={s.market} /> {s.symbol}{s.stock_name ? ` · ${s.stock_name}` : ''}
                        </span>
                      ))}
                    </div>
                  </div>
                )}
              </>
            )}
        </div>
      </div>
    </div>
  );
}

function NewsPage() {
  const [tab, setTab] = useP('market');
  const [readKind, setReadKind] = useP('opened');
  const [q, setQ] = useP('');
  const [debouncedQ, setDebouncedQ] = useP('');
  const [openId, setOpenId] = useP(null);

  useE(() => { const id = setTimeout(() => setDebouncedQ(q), 300); return () => clearTimeout(id); }, [q]);

  const marketQ = useApi(() => API.news.market({ limit: 60 }), [], { refreshMs: 60000, skip: tab !== 'market' });
  const readsQ  = useApi(() => API.news.reads({ limit: 60, reader: 'codex', kind: readKind }), [readKind], { refreshMs: 60000, skip: tab !== 'codex' });
  const searchQ = useApi(() => debouncedQ.trim() ? API.news.search({ query: debouncedQ.trim(), limit: 60 }) : Promise.resolve({ ok: true, data: { ok: true, count: 0, items: [] } }), [debouncedQ], { skip: tab !== 'search' });

  const marketItems = (marketQ.data && marketQ.data.items) || [];
  const rawReads = (readsQ.data && readsQ.data.items) || [];
  const searchItems = (searchQ.data && searchQ.data.items) || [];

  // The same headline is re-read every cycle (Codex lists it each run), so
  // collapse to one card per news_id keeping the most recent read (rows arrive
  // newest-first from the backend).
  const reads = useM(() => {
    const seen = new Set();
    const out = [];
    for (const r of rawReads) {
      if (seen.has(r.news_id)) continue;
      seen.add(r.news_id);
      out.push(r);
    }
    return out;
  }, [rawReads]);

  // codex-read news_ids for the badge
  const readSet = useM(() => new Set(reads.map(r => r.news_id)), [reads]);

  return (
    <div className="news-page">
      <PageHead title="뉴스" sub="/news · 시장 + 검색 + Codex 읽음 이력" />

      <div className="tabs">
        <Segmented value={tab} onChange={setTab} options={[
          { value: 'market', label: `시장 ${marketItems.length}` },
          { value: 'codex', label: 'Codex 읽음' },
          { value: 'search', label: '검색' },
        ]} />
      </div>

      {tab === 'market' && (
        marketQ.loading ? <div className="empty" style={{ padding: 30 }}>로딩 중…</div> :
        marketQ.error ? <div className="empty" style={{ color: 'var(--warn)', padding: 20 }}>{marketQ.error.message}</div> :
        marketItems.length === 0 ? <div className="empty" style={{ padding: 30 }}>수집된 시장 뉴스 없음</div> :
        <div className="news-waterfall">
          {marketItems.map(n => <NewsCard key={n.news_id} n={n} readBadge={readSet.has(n.news_id)} onOpen={() => setOpenId(n.news_id)} />)}
        </div>
      )}

      {tab === 'codex' && (
        <div>
          <div style={{ marginBottom: 12 }}>
            <Segmented size="sm" value={readKind} onChange={setReadKind} options={[
              { value: 'opened', label: '본문 열람' },
              { value: 'surfaced', label: '목록 노출' },
            ]} />
            <p className="mono" style={{ marginTop: 8, fontSize: 10.5, color: 'var(--text-4)' }}>
              {readKind === 'opened'
                ? 'Codex가 news_get으로 본문까지 연 뉴스'
                : 'Codex가 목록/검색에서 제목·요약만 본 뉴스 (본문 미열람)'}
            </p>
          </div>
          {readsQ.loading ? <div className="empty" style={{ padding: 30 }}>로딩 중…</div> :
            reads.length === 0 ? <div className="empty" style={{ padding: 30 }}>{readKind === 'opened' ? 'Codex가 본문까지 연 뉴스가 없습니다' : 'Codex가 목록에서 확인한 뉴스가 없습니다'}</div> :
            <div className="news-waterfall">
              {reads.map(r => (
                <article key={r.read_id} className="news-pin" style={{ '--strip': sentimentColor(r.sentiment) }} onClick={() => setOpenId(r.news_id)}>
                  <div className="news-pin-strip" />
                  <div className="news-pin-body">
                    <div className="news-pin-meta mono">
                      <SentimentBadge sentiment={r.sentiment} />
                      <span style={{ color: 'var(--text-3)' }}>{r.source || '—'}</span>
                      <span style={{ color: 'var(--text-4)' }}>·</span>
                      <span style={{ color: 'var(--accent)' }}>{fmtRelativeTime(r.read_at_utc)} {readKind === 'opened' ? '열람' : '노출'}</span>
                    </div>
                    <div className="news-pin-headline">{r.title}</div>
                  </div>
                </article>
              ))}
            </div>
          }
        </div>
      )}

      {tab === 'search' && (
        <div>
          <input className="search-input mono" placeholder="키워드 검색 (FTS)…" value={q} onChange={(e) => setQ(e.target.value)} style={{ marginBottom: 12 }} />
          {!debouncedQ.trim() ? <div className="empty" style={{ padding: 20 }}>검색어를 입력하세요</div> :
            searchQ.loading ? <div className="empty">검색 중…</div> :
            searchQ.error ? <div className="empty" style={{ color: 'var(--warn)' }}>{searchQ.error.message}</div> :
            searchItems.length === 0 ? <div className="empty">결과 없음</div> :
            <div className="news-waterfall">
              {searchItems.map(n => <NewsCard key={n.news_id} n={n} readBadge={readSet.has(n.news_id)} onOpen={() => setOpenId(n.news_id)} />)}
            </div>
          }
        </div>
      )}

      {openId && <NewsDetailModal newsId={openId} onClose={() => setOpenId(null)} />}
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   메모리 페이지
   ════════════════════════════════════════════════════════ */
function MemoryStickerCard({ label, scope, tint, tilt, payload, loading }) {
  const body = payload?.body || '';
  const upd = payload?.updated_at_utc;
  const cap = 50000; // matches _BODY_MAX in stl/memory.py
  const usedPct = body ? Math.min(100, (body.length / cap) * 100) : 0;
  return (
    <div className={cx('sticker', tint, `tilt-${tilt}`)}>
      <div className="sticker-pin" />
      <div className="sticker-head">
        <b className="sticker-title">{label}</b>
        <span className="mono sticker-scope">{scope}</span>
      </div>
      <pre className="sticker-body mono">
        {loading ? '로딩 중…' : (body || '— 비어 있음')}
      </pre>
      <div className="sticker-foot">
        <div className="token-bar"><div className="token-bar-fill" style={{ width: usedPct + '%' }} /></div>
        <div className="sticker-foot-row mono">
          <span>{(body.length / 1024).toFixed(1)} / {Math.round(cap / 1024)} KB</span>
          <span>{upd ? fmtRelativeTime(upd) : '—'}</span>
        </div>
      </div>
    </div>
  );
}

function MemoryPage() {
  const [modal, setModal] = useP(null);

  const mainQ      = useApi(() => API.memory.read({ scope: 'main' }), []);
  const krMarketQ  = useApi(() => API.memory.read({ scope: 'kr_market' }), []);
  const usMarketQ  = useApi(() => API.memory.read({ scope: 'us_market' }), []);
  const stocksQ    = useApi(() => API.memory.stocks(), [], { refreshMs: 60000 });

  const stocks = (stocksQ.data && stocksQ.data.items) || [];

  return (
    <div className="memory-page">
      <PageHead title="메모리" sub="/memory · Codex가 누적해온 학습 노트 (스코프별 markdown)" />

      <div className="sticker-board">
        <MemoryStickerCard label="공통 메모"    scope="main"      tint="sticker-yellow" tilt={0} payload={mainQ.data}     loading={mainQ.loading} />
        <MemoryStickerCard label="KR 시장 메모" scope="kr_market" tint="sticker-red"    tilt={1} payload={krMarketQ.data} loading={krMarketQ.loading} />
        <MemoryStickerCard label="US 시장 메모" scope="us_market" tint="sticker-blue"   tilt={2} payload={usMarketQ.data} loading={usMarketQ.loading} />
      </div>

      <div className="card">
        <div className="card-head">
          <div className="card-title">종목별 메모 <span className="card-title-sub">{stocks.length}개 · scope=stock</span></div>
        </div>
        {stocksQ.loading && stocks.length === 0 ? (
          <div className="empty" style={{ padding: 20 }}>로딩 중…</div>
        ) : stocks.length === 0 ? (
          <div className="empty" style={{ padding: 20 }}>아직 종목별 메모 없음 (Codex가 작성하면 자동으로 표시)</div>
        ) : (
          <div className="sticker-mini-grid">
            {stocks.map((m, i) => {
              const cap = 50000;
              const pct = m.body_len ? Math.min(100, (m.body_len / cap) * 100) : 0;
              return (
                <button key={`${m.market}:${m.symbol}`} className={cx('sticker-mini', `tilt-${i % 4}`, m.market === 'KR' ? 'sticker-mini-kr' : 'sticker-mini-us')} onClick={() => setModal(m)}>
                  <div className="sticker-mini-pin" />
                  <div className="sticker-mini-head">
                    <MarketBadge market={m.market} />
                    <span className="mono" style={{ fontSize: 9.5, color: 'var(--text-3)', marginLeft: 'auto', letterSpacing: 0 }}>
                      {(m.body_len / 1024).toFixed(1)}KB
                    </span>
                  </div>
                  <b className="sticker-mini-name">{m.symbol}</b>
                  <span className="mono sticker-mini-sym">{m.market}</span>
                  <div className="sticker-mini-foot">
                    <div className="token-bar" style={{ height: 3 }}>
                      <div className="token-bar-fill" style={{ width: pct + '%' }} />
                    </div>
                    <span className="mono" style={{ fontSize: 9.5, color: 'var(--text-4)', letterSpacing: 0 }}>{fmtRelativeTime(m.updated_at_utc)}</span>
                  </div>
                </button>
              );
            })}
          </div>
        )}
      </div>

      {modal && <StockMemoryModal stock={modal} onClose={() => setModal(null)} />}
    </div>
  );
}

function StockMemoryModal({ stock, onClose }) {
  const q = useApi(() => API.memory.read({ scope: 'stock', symbol: stock.symbol, market: stock.market }), [stock]);
  useE(() => {
    const h = (e) => e.key === 'Escape' && onClose();
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [onClose]);
  const body = q.data?.body || '';
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <header className="modal-head">
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
            <MarketBadge market={stock.market} />
            <div>
              <h3 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>{stock.symbol}</h3>
              <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>memory:stock:{stock.market}:{stock.symbol}</div>
            </div>
          </div>
          <button className="icon-btn" onClick={onClose} aria-label="닫기">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12" /></svg>
          </button>
        </header>
        <pre className="mono" style={{ background: 'var(--bg-inset)', border: '1px solid var(--border-subtle)', borderRadius: 8, padding: '14px 16px', fontSize: 11.5, lineHeight: 1.55, color: 'var(--text-2)', whiteSpace: 'pre-wrap', margin: 16, maxHeight: '60vh', overflow: 'auto' }}>
          {q.loading ? '로딩 중…' : (body || '— 비어 있음')}
        </pre>
      </div>
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   백테스트 페이지
   ════════════════════════════════════════════════════════ */
function BacktestDetail({ month, onClose }) {
  const q = useApi(() => API.backtests.get(month), [month]);
  useE(() => {
    const h = (e) => e.key === 'Escape' && onClose();
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [onClose]);
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <header className="modal-head">
          <h3 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>백테스트 {month}</h3>
          <button className="icon-btn" onClick={onClose}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12" /></svg>
          </button>
        </header>
        <div style={{ padding: '8px 16px 16px', maxHeight: '64vh', overflow: 'auto' }}>
          {q.loading ? <div className="empty">로딩 중…</div> :
            q.error ? <div className="empty" style={{ color: 'var(--warn)' }}>{q.error.message}</div> :
            q.data?.summary ? <JSONViewer data={q.data.summary} /> :
            <div className="empty">데이터 없음</div>}
        </div>
      </div>
    </div>
  );
}

function BacktestPage() {
  const listQ = useApi(() => API.backtests.list({ limit: 24 }), [], { refreshMs: 300000 });
  const items = (listQ.data && listQ.data.items) || [];
  const [openMonth, setOpenMonth] = useP(null);

  return (
    <div className="backtest-page">
      <PageHead title="백테스트" sub={`/backtest · 월별 시뮬레이션 · ${items.length}건`} />

      {listQ.loading && items.length === 0 ? <div className="empty" style={{ padding: 40 }}>로딩 중…</div> :
        items.length === 0 ? (
          <div className="card">
            <div className="empty" style={{ padding: 40, flexDirection: 'column', gap: 8 }}>
              <span style={{ fontSize: 24 }}>📊</span>
              <div style={{ fontSize: 13 }}>아직 백테스트 기록 없음</div>
              <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>매월 1일 00:00 UTC에 자동 실행</div>
            </div>
          </div>
        ) : (
          <div className="bt-grid">
            {items.map(b => (
              <button key={b.month} className="bt-card" onClick={() => setOpenMonth(b.month)} style={{ textAlign: 'left', cursor: 'pointer' }}>
                <div className="bt-card-head">
                  <div className="bt-card-ym mono">{b.month}</div>
                  <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{fmtBytes(b.bytes)}</span>
                </div>
                <div className="empty" style={{ padding: 16, fontSize: 11.5 }}>리포트 본문은 클릭해서 확인</div>
                <div className="bt-card-foot mono">
                  <span>생성 {fmtRelativeTime(b.created_at_utc)}</span>
                  <span>·</span>
                  <span>갱신 {fmtRelativeTime(b.updated_at_utc)}</span>
                </div>
              </button>
            ))}
          </div>
        )
      }

      {openMonth && <BacktestDetail month={openMonth} onClose={() => setOpenMonth(null)} />}
    </div>
  );
}

/* ════════════════════════════════════════════════════════
   시스템 페이지
   ════════════════════════════════════════════════════════ */
function CodexUsageCard({ usage, loading }) {
  if (loading && !usage) return <div className="empty" style={{ padding: 18 }}>Codex 사용량 로딩 중…</div>;
  if (!usage || usage.ok === false) return (
    <div className="empty" style={{ padding: 18 }}>
      <div style={{ fontSize: 12 }}>Codex 사용량 기록 없음</div>
      <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-4)', marginTop: 4 }}>
        {usage?.error?.message || '~/.codex/sessions 에 아직 token_count 이벤트 없음'}
      </div>
    </div>
  );
  const rows = [
    { label: '5시간 윈도', win: usage.primary },
    { label: '주간 윈도', win: usage.secondary },
  ];
  const toneFor = (p) => p == null ? 'var(--text-3)' : p >= 90 ? 'var(--critical)' : p >= 70 ? 'var(--warn)' : 'var(--accent)';
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', display: 'flex', justifyContent: 'space-between' }}>
        <span>{usage.plan_type || '—'} · {usage.rate_limit_reached_type ? `한도 도달: ${usage.rate_limit_reached_type}` : '한도 도달 없음'}</span>
        <span>{usage.updated_at_utc ? fmtRelativeTime(usage.updated_at_utc) : ''}</span>
      </div>
      {rows.map(r => {
        const used = r.win && typeof r.win.used_percent === 'number' ? r.win.used_percent : null;
        const color = toneFor(used);
        const w = used == null ? 0 : Math.max(0, Math.min(100, used));
        return (
          <div key={r.label} style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
              <span style={{ fontSize: 12, color: 'var(--text-2)' }}>{r.label}</span>
              <span className="mono tnum" style={{ fontSize: 13, color, fontWeight: 600 }}>{used == null ? '—' : used.toFixed(1) + '%'}</span>
            </div>
            <div className="codex-mini-bar" style={{ height: 6 }}>
              <span style={{ width: w + '%', background: color }} />
            </div>
            <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-4)', display: 'flex', justifyContent: 'space-between' }}>
              <span>{r.win?.window_minutes ? `${r.win.window_minutes}분 단위` : ''}</span>
              <span>{r.win?.resets_at_utc ? `리셋 ${fmtRelativeTime(r.win.resets_at_utc)}` : ''}</span>
            </div>
          </div>
        );
      })}
      {(() => {
        // total_token_usage / last_token_usage are dicts: { input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens, total_tokens }
        const lastTotal = usage.last_token_usage && typeof usage.last_token_usage === 'object'
          ? usage.last_token_usage.total_tokens : usage.last_token_usage;
        const allTotal  = usage.total_token_usage && typeof usage.total_token_usage === 'object'
          ? usage.total_token_usage.total_tokens : usage.total_token_usage;
        const ctx       = usage.model_context_window;
        if (lastTotal == null && allTotal == null && ctx == null) return null;
        return (
          <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', display: 'flex', flexWrap: 'wrap', gap: 12, paddingTop: 10, borderTop: '1px dashed var(--border-subtle)' }}>
            {lastTotal != null && <span title="직전 응답에서 사용한 총 토큰">last: {(lastTotal / 1000).toFixed(1)}K</span>}
            {allTotal != null  && <span title="세션 누적 토큰">total: {(allTotal / 1000).toFixed(1)}K</span>}
            {ctx != null       && <span title="모델 컨텍스트 윈도 크기">ctx: {(ctx / 1000).toFixed(0)}K</span>}
            {usage.source && <span style={{ marginLeft: 'auto', color: 'var(--text-4)' }} title={usage.source}>{(usage.source).slice(0, 24)}{usage.source.length > 24 ? '…' : ''}</span>}
          </div>
        );
      })()}
    </div>
  );
}

function SystemPage() {
  const stateQ = useApi(() => API.system.state(), [], { refreshMs: 10000 });
  const usageQ = useApi(() => API.system.codexUsage(), [], { refreshMs: 30000 });

  const s = stateQ.data || {};
  const r = s.resources || {};
  const cpu = r.cpu || {};
  const mem = r.memory || {};
  const swap = r.swap || {};
  const disk = s.disk || {};
  const host = s.host || {};
  const db = s.db || {};
  const logs = s.logs || {};

  const toneFor = (pct) => pct == null ? 'var(--text-3)' : pct >= 80 ? 'var(--critical)' : pct >= 60 ? 'var(--uncertain)' : 'var(--success)';
  const memUsedPct = mem.used_pct;
  const swapUsedPct = swap.used_pct;
  const swapEnabled = (swap.total_bytes || 0) > 0;
  const diskUsedPct = disk.data_used_pct;
  const cpuPct = cpu.percent;

  return (
    <div className="system-page">
      <PageHead title="시스템 상태" sub="/system · 진단 + Codex 사용량" />

      <div className="sys-banner">
        <div className="sys-banner-left">
          {s.paper_trading === false ? (
            <>
              <span className="status-pill mono" style={{ color: 'var(--critical)', background: 'var(--critical-soft)', borderColor: 'color-mix(in oklch, var(--critical) 30%, transparent)' }}>LIVE</span>
              <span className="mono" style={{ marginLeft: 12, color: 'var(--text-2)', fontSize: 12 }}>
                실거래 모드 — 모든 주문이 실제로 체결됩니다.
              </span>
            </>
          ) : (
            <>
              <PaperBadge />
              <span className="mono" style={{ marginLeft: 12, color: 'var(--text-2)', fontSize: 12 }}>
                모의 운영 모드 활성. 실제 체결이 발생하지 않습니다.
              </span>
            </>
          )}
        </div>
        <span className="mono" style={{ color: 'var(--text-3)', fontSize: 11 }}>
          v{s.version || '—'} · uptime {fmtUptime(host.uptime_seconds)} · proc {fmtUptime(host.process_uptime_seconds)}
        </span>
      </div>

      <div className="sys-grid">
        <div className="card">
          <div className="card-head"><div className="card-title">{swapEnabled ? 'CPU / 메모리 / 스왑 / 디스크' : 'CPU / 메모리 / 디스크'}</div></div>
          <div className="server-grid" style={{ marginBottom: 0 }}>
            <div className="server-gauge">
              <GaugeRing pct={cpuPct == null ? 0 : cpuPct} color={toneFor(cpuPct)} />
              <div className="server-gauge-label">
                <b>CPU</b>
                <span className="mono">{cpu.cores || '?'}코어 · L1 {cpu.load_1m != null ? cpu.load_1m.toFixed(2) : '—'}</span>
              </div>
            </div>
            <div className="server-gauge">
              <GaugeRing pct={memUsedPct == null ? 0 : memUsedPct} color={toneFor(memUsedPct)} />
              <div className="server-gauge-label">
                <b>RAM</b>
                <span className="mono">{mem.used_bytes != null ? (mem.used_bytes / 1e9).toFixed(1) : '?'}/{mem.total_bytes != null ? (mem.total_bytes / 1e9).toFixed(0) : '?'}GB</span>
              </div>
            </div>
            {swapEnabled && (
              <div className="server-gauge" title="가상메모리 (swap)">
                <GaugeRing pct={swapUsedPct == null ? 0 : swapUsedPct} color={toneFor(swapUsedPct)} />
                <div className="server-gauge-label">
                  <b>Swap</b>
                  <span className="mono">{swap.used_bytes != null ? (swap.used_bytes / 1e9).toFixed(1) : '?'}/{swap.total_bytes != null ? (swap.total_bytes / 1e9).toFixed(0) : '?'}GB</span>
                </div>
              </div>
            )}
            <div className="server-gauge">
              <GaugeRing pct={diskUsedPct == null ? 0 : diskUsedPct} color={toneFor(diskUsedPct)} />
              <div className="server-gauge-label">
                <b>Disk</b>
                <span className="mono">{disk.data_free_bytes != null ? ((disk.data_total_bytes - disk.data_free_bytes) / 1e9).toFixed(1) : '?'}/{disk.data_total_bytes != null ? (disk.data_total_bytes / 1e9).toFixed(0) : '?'}GB</span>
              </div>
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-head"><div className="card-title">Codex 사용량</div></div>
          <CodexUsageCard usage={usageQ.data} loading={usageQ.loading} />
        </div>

        <div className="card">
          <div className="card-head"><div className="card-title">호스트</div></div>
          <div className="syslist">
            <div className="sysitem">
              <span className="sysitem-ic">SYS</span>
              <div className="sysitem-text">
                <div style={{ display: 'flex', justifyContent: 'space-between' }}><b>플랫폼</b><span className="mono">{host.platform || '—'}</span></div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>Python {host.python_version || '—'}</span>
              </div>
            </div>
            <div className="sysitem">
              <span className="sysitem-ic">UPT</span>
              <div className="sysitem-text">
                <div style={{ display: 'flex', justifyContent: 'space-between' }}><b>uptime</b><span className="mono">{fmtUptime(host.uptime_seconds)}</span></div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>backend uptime {fmtUptime(host.process_uptime_seconds)}</span>
              </div>
            </div>
            <div className="sysitem">
              <span className="sysitem-ic">L5</span>
              <div className="sysitem-text">
                <div style={{ display: 'flex', justifyContent: 'space-between' }}><b>load avg</b>
                  <span className="mono tnum">
                    {cpu.load_1m != null ? cpu.load_1m.toFixed(2) : '—'} · {cpu.load_5m != null ? cpu.load_5m.toFixed(2) : '—'} · {cpu.load_15m != null ? cpu.load_15m.toFixed(2) : '—'}
                  </span>
                </div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>1m / 5m / 15m</span>
              </div>
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-head"><div className="card-title">SQLite 테이블</div></div>
          <div className="syslist">
            <div className="sysitem">
              <span className="sysitem-ic">DB</span>
              <div className="sysitem-text">
                <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                  <b>{db.path ? db.path.split('/').pop() : 'runs.db'}</b>
                  <span className="mono tnum">{fmtBytes(db.size_bytes)}</span>
                </div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{db.exists === false ? '파일 없음' : (db.tables || []).length + '개 테이블'}</span>
              </div>
            </div>
            {(db.tables || []).map(t => (
              <div key={t.name} className="sysitem" style={{ paddingLeft: 8 }}>
                <span className="sysitem-ic" style={{ background: 'var(--bg-inset)', color: 'var(--text-3)' }}>·</span>
                <div className="sysitem-text">
                  <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                    <span className="mono" style={{ fontSize: 11.5 }}>{t.name}</span>
                    <span className="mono tnum" style={{ fontSize: 11.5, color: 'var(--text-2)' }}>{t.row_count != null ? t.row_count.toLocaleString() : '—'}</span>
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><div className="card-title">로그</div></div>
          <div className="syslist">
            <div className="sysitem">
              <span className="sysitem-ic">LOG</span>
              <div className="sysitem-text">
                <div style={{ display: 'flex', justifyContent: 'space-between' }}><b>{logs.dir ? logs.dir.split('/').slice(-2).join('/') : 'data/logs'}</b><span className="mono tnum">{fmtBytes(logs.total_bytes)}</span></div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{logs.file_count || 0}개 파일</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  CyclesPage, PortfolioPage, OrdersPage, ProblemsPage,
  SchedulePage, NewsPage, MemoryPage, BacktestPage, SystemPage,
  SentimentBadge, StatusPill, GaugeRing,
});
