@winspan/claude-forge 8.12.0 → 8.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Claude Forge Dashboard</title>
6
+ <title>Claude Forge 管理后台</title>
7
7
  <script src="vendor/chart.umd.min.js"></script>
8
8
  <style>
9
9
  /* === Reset & Theme === */
@@ -333,42 +333,42 @@
333
333
  <div class="brand-icon">CF</div>
334
334
  <div class="brand-text">Claude Forge</div>
335
335
  </div>
336
- <div class="nav-section-title">Overview</div>
336
+ <div class="nav-section-title">总览</div>
337
337
  <a onclick="nav('dashboard')" id="nav-dashboard" class="active">
338
338
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
339
- Dashboard
339
+ 仪表盘
340
340
  </a>
341
- <div class="nav-section-title">Activity</div>
341
+ <div class="nav-section-title">活动</div>
342
342
  <a onclick="nav('sessions')" id="nav-sessions">
343
343
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
344
- Sessions
344
+ 会话
345
345
  </a>
346
346
  <a onclick="nav('events')" id="nav-events">
347
347
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
348
- Events
348
+ 事件
349
349
  </a>
350
350
  <a onclick="nav('injections')" id="nav-injections">
351
351
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
352
- Injections
352
+ 注入
353
353
  </a>
354
354
  <a onclick="nav('live')" id="nav-live">
355
355
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/></svg>
356
- Live
356
+ 实时
357
357
  </a>
358
- <div class="nav-section-title">Configuration</div>
358
+ <div class="nav-section-title">配置</div>
359
359
  <a onclick="nav('rules')" id="nav-rules">
360
360
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
361
- Rules
361
+ 规则
362
362
  </a>
363
363
  </aside>
364
364
 
365
365
  <!-- Main -->
366
366
  <main class="main">
367
367
  <div class="topbar">
368
- <div class="page-title" id="topbar-title">Dashboard</div>
368
+ <div class="page-title" id="topbar-title">仪表盘</div>
369
369
  <div class="actions">
370
- <span id="daemon-status" class="badge badge-allow" style="font-size:0.75rem">Checking...</span>
371
- <button class="btn" onclick="refreshPage()">↻ Refresh</button>
370
+ <span id="daemon-status" class="badge badge-allow" style="font-size:0.75rem">检查中...</span>
371
+ <button class="btn" onclick="refreshPage()">↻ 刷新</button>
372
372
  </div>
373
373
  </div>
374
374
  <div class="container">
@@ -379,13 +379,13 @@
379
379
  <div class="grid-2" style="margin-bottom:1.25rem">
380
380
  <div class="panel">
381
381
  <div class="panel-header">
382
- <span class="panel-title">7-Day Activity</span>
382
+ <span class="panel-title">近 7 天活动</span>
383
383
  </div>
384
384
  <div class="panel-body"><div class="chart-wrap"><canvas id="chart-activity"></canvas></div></div>
385
385
  </div>
386
386
  <div class="panel">
387
387
  <div class="panel-header">
388
- <span class="panel-title">Tool Usage</span>
388
+ <span class="panel-title">工具使用分布</span>
389
389
  </div>
390
390
  <div class="panel-body"><div class="chart-wrap"><canvas id="chart-tools"></canvas></div></div>
391
391
  </div>
@@ -393,14 +393,14 @@
393
393
  <div class="grid-2">
394
394
  <div class="panel">
395
395
  <div class="panel-header">
396
- <span class="panel-title">Recent Sessions</span>
397
- <a onclick="nav('sessions')" style="font-size:0.8rem;color:var(--primary);cursor:pointer">View all →</a>
396
+ <span class="panel-title">最近会话</span>
397
+ <a onclick="nav('sessions')" style="font-size:0.8rem;color:var(--primary);cursor:pointer">查看全部 →</a>
398
398
  </div>
399
399
  <div id="dash-sessions"></div>
400
400
  </div>
401
401
  <div class="panel">
402
402
  <div class="panel-header">
403
- <span class="panel-title">Recent Activity</span>
403
+ <span class="panel-title">最近活动</span>
404
404
  </div>
405
405
  <div class="panel-body" id="dash-activity"></div>
406
406
  </div>
@@ -410,7 +410,7 @@
410
410
  <!-- Sessions -->
411
411
  <div id="page-sessions" class="page">
412
412
  <div class="toolbar">
413
- <input class="search-box" id="sessions-search" placeholder="Search sessions..." oninput="filterSessions()">
413
+ <input class="search-box" id="sessions-search" placeholder="搜索会话..." oninput="filterSessions()">
414
414
  </div>
415
415
  <div class="panel" id="sessions-panel">
416
416
  <div id="sessions-list"></div>
@@ -420,9 +420,9 @@
420
420
  <!-- Events -->
421
421
  <div id="page-events" class="page">
422
422
  <div class="toolbar">
423
- <input class="search-box" id="events-search" placeholder="Search by type, tool, project..." oninput="filterEvents()">
423
+ <input class="search-box" id="events-search" placeholder="按类型、工具、项目搜索..." oninput="filterEvents()">
424
424
  <select class="btn" id="events-type" onchange="filterEvents()">
425
- <option value="">All Types</option>
425
+ <option value="">全部类型</option>
426
426
  <option value="PreToolUse">PreToolUse</option>
427
427
  <option value="PostToolUse">PostToolUse</option>
428
428
  <option value="UserPromptSubmit">UserPromptSubmit</option>
@@ -431,7 +431,7 @@
431
431
  </div>
432
432
  <div class="panel">
433
433
  <table>
434
- <thead><tr><th>Time</th><th>Type</th><th>Tool</th><th>Project</th></tr></thead>
434
+ <thead><tr><th>时间</th><th>类型</th><th>工具</th><th>项目</th></tr></thead>
435
435
  <tbody id="events-body"></tbody>
436
436
  </table>
437
437
  </div>
@@ -440,9 +440,9 @@
440
440
  <!-- Injections -->
441
441
  <div id="page-injections" class="page">
442
442
  <div class="toolbar">
443
- <input class="search-box" id="inj-search" placeholder="Search injections..." oninput="filterInjections()">
443
+ <input class="search-box" id="inj-search" placeholder="搜索注入内容..." oninput="filterInjections()">
444
444
  <select class="btn" id="inj-handler" onchange="filterInjections()">
445
- <option value="">All Handlers</option>
445
+ <option value="">全部 Handler</option>
446
446
  <option value="UserPromptSubmitHandler">UserPromptSubmit</option>
447
447
  <option value="PreToolUseHandler">PreToolUse</option>
448
448
  <option value="PostToolUseHandler">PostToolUse</option>
@@ -456,9 +456,9 @@
456
456
  <!-- Live -->
457
457
  <div id="page-live" class="page">
458
458
  <div class="toolbar">
459
- <span id="live-status" class="badge badge-warn">Disconnected</span>
460
- <button class="btn btn-primary" id="live-btn" onclick="toggleLive()">Connect</button>
461
- <button class="btn" onclick="document.getElementById('live-log').innerHTML=''">Clear</button>
459
+ <span id="live-status" class="badge badge-warn">未连接</span>
460
+ <button class="btn btn-primary" id="live-btn" onclick="toggleLive()">连接</button>
461
+ <button class="btn" onclick="document.getElementById('live-log').innerHTML=''">清空</button>
462
462
  </div>
463
463
  <div class="live-log" id="live-log"></div>
464
464
  </div>
@@ -466,7 +466,7 @@
466
466
  <!-- Rules -->
467
467
  <div id="page-rules" class="page">
468
468
  <div class="toolbar">
469
- <input class="search-box" id="rules-search" placeholder="Search rules..." oninput="filterRules()">
469
+ <input class="search-box" id="rules-search" placeholder="搜索规则..." oninput="filterRules()">
470
470
  </div>
471
471
  <div class="panel" id="rules-panel">
472
472
  <div id="rules-list"></div>
@@ -482,7 +482,7 @@
482
482
  <!-- Drawer -->
483
483
  <div class="drawer" id="drawer">
484
484
  <div class="drawer-header">
485
- <div class="drawer-title" id="drawer-title">Detail</div>
485
+ <div class="drawer-title" id="drawer-title">详情</div>
486
486
  <button class="drawer-close" onclick="closeDrawer()">
487
487
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
488
488
  </button>
@@ -506,7 +506,7 @@ function nav(page) {
506
506
  const pageEl = document.getElementById('page-' + page);
507
507
  if (navEl) navEl.classList.add('active');
508
508
  if (pageEl) pageEl.classList.add('active');
509
- const titles = { dashboard:'Dashboard', sessions:'Sessions', events:'Events', injections:'Injections', live:'Live', rules:'Rules' };
509
+ const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入', live:'实时', rules:'规则' };
510
510
  document.getElementById('topbar-title').textContent = titles[page] || page;
511
511
  closeDrawer();
512
512
  if (page === 'dashboard') loadDashboard();
@@ -553,7 +553,7 @@ function empty(msg) {
553
553
  return `<div class="empty"><div class="empty-icon">📭</div><div class="empty-text">${msg}</div></div>`;
554
554
  }
555
555
  function loading() {
556
- return `<div class="loading">Loading...</div>`;
556
+ return `<div class="loading">加载中...</div>`;
557
557
  }
558
558
 
559
559
  // === Dashboard ===
@@ -569,23 +569,23 @@ async function loadDashboard() {
569
569
 
570
570
  // Stat cards
571
571
  const uptime = Math.floor(status.uptime);
572
- const uptimeStr = uptime < 60 ? uptime + 's' : Math.floor(uptime/60) + 'm';
572
+ const uptimeStr = uptime < 60 ? uptime + '' : Math.floor(uptime/60) + ' 分钟';
573
573
  const mem = Math.round(status.memory.heapUsed / 1024 / 1024);
574
574
  document.getElementById('dash-stats').innerHTML = `
575
- <div class="stat-card"><div class="label">PID</div><div class="value">${status.pid}</div></div>
576
- <div class="stat-card"><div class="label">Uptime</div><div class="value">${uptimeStr}</div></div>
577
- <div class="stat-card"><div class="label">Memory</div><div class="value">${mem}MB</div></div>
578
- <div class="stat-card"><div class="label">Events</div><div class="value">${status.eventCount}</div></div>
575
+ <div class="stat-card"><div class="label">进程 ID</div><div class="value">${status.pid}</div></div>
576
+ <div class="stat-card"><div class="label">运行时长</div><div class="value">${uptimeStr}</div></div>
577
+ <div class="stat-card"><div class="label">内存占用</div><div class="value">${mem}MB</div></div>
578
+ <div class="stat-card"><div class="label">事件总数</div><div class="value">${status.eventCount}</div></div>
579
579
  `;
580
- document.getElementById('daemon-status').textContent = 'Running';
580
+ document.getElementById('daemon-status').textContent = '运行中';
581
581
  document.getElementById('daemon-status').className = 'badge badge-allow';
582
582
 
583
583
  // Charts
584
584
  renderActivityChart(stats.dailyActivity || []);
585
585
  renderToolChart(stats.toolUsage || {});
586
586
  } catch {
587
- document.getElementById('dash-stats').innerHTML = `<div class="stat-card"><div class="label">Status</div><div class="value" style="font-size:1rem;color:var(--red)">Offline</div></div>`;
588
- document.getElementById('daemon-status').textContent = 'Offline';
587
+ document.getElementById('dash-stats').innerHTML = `<div class="stat-card"><div class="label">状态</div><div class="value" style="font-size:1rem;color:var(--red)">离线</div></div>`;
588
+ document.getElementById('daemon-status').textContent = '离线';
589
589
  document.getElementById('daemon-status').className = 'badge badge-block';
590
590
  }
591
591
 
@@ -594,10 +594,10 @@ async function loadDashboard() {
594
594
  const res = await fetch(API + '/api/sessions?limit=5');
595
595
  const sessions = await res.json();
596
596
  document.getElementById('dash-sessions').innerHTML = sessions.length === 0
597
- ? empty('No sessions yet')
597
+ ? empty('暂无会话')
598
598
  : sessions.map(s => sessionListItem(s)).join('');
599
599
  } catch {
600
- document.getElementById('dash-sessions').innerHTML = empty('Failed to load');
600
+ document.getElementById('dash-sessions').innerHTML = empty('加载失败');
601
601
  }
602
602
 
603
603
  // Recent activity
@@ -605,7 +605,7 @@ async function loadDashboard() {
605
605
  const res = await fetch(API + '/api/events?limit=10');
606
606
  const events = await res.json();
607
607
  document.getElementById('dash-activity').innerHTML = events.length === 0
608
- ? empty('No activity yet')
608
+ ? empty('暂无活动')
609
609
  : events.map(e => `
610
610
  <div class="activity-item">
611
611
  <div class="activity-dot activity-dot-${hookColor(e.hook_type)}"></div>
@@ -629,7 +629,7 @@ function renderActivityChart(data) {
629
629
  type: 'line',
630
630
  data: {
631
631
  labels: data.map(d => d.date),
632
- datasets: [{ label: 'Events', data: data.map(d => d.count), borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,0.08)', tension: 0.3, fill: true }]
632
+ datasets: [{ label: '事件数', data: data.map(d => d.count), borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,0.08)', tension: 0.3, fill: true }]
633
633
  },
634
634
  options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } }, x: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } } } }
635
635
  });
@@ -652,16 +652,16 @@ function renderToolChart(data) {
652
652
  function toggleLive() {
653
653
  if (liveSource) {
654
654
  liveSource.close(); liveSource = null;
655
- document.getElementById('live-status').textContent = 'Disconnected';
655
+ document.getElementById('live-status').textContent = '未连接';
656
656
  document.getElementById('live-status').className = 'badge badge-warn';
657
- document.getElementById('live-btn').textContent = 'Connect';
657
+ document.getElementById('live-btn').textContent = '连接';
658
658
  document.getElementById('live-btn').className = 'btn btn-primary';
659
659
  return;
660
660
  }
661
661
  liveSource = new EventSource(API + '/api/events/stream');
662
- document.getElementById('live-status').textContent = 'Live';
662
+ document.getElementById('live-status').textContent = '实时中';
663
663
  document.getElementById('live-status').className = 'badge badge-live';
664
- document.getElementById('live-btn').textContent = 'Disconnect';
664
+ document.getElementById('live-btn').textContent = '断开连接';
665
665
  document.getElementById('live-btn').className = 'btn';
666
666
  liveSource.onmessage = function(ev) {
667
667
  try {
@@ -678,133 +678,207 @@ function toggleLive() {
678
678
  } catch {}
679
679
  };
680
680
  liveSource.onerror = function() {
681
- document.getElementById('live-status').textContent = 'Error';
681
+ document.getElementById('live-status').textContent = '连接错误';
682
682
  document.getElementById('live-status').className = 'badge badge-block';
683
683
  };
684
684
  }
685
685
 
686
- // === Init ===
687
- nav('dashboard');
688
- </script>
689
- </body>
690
- </html>
691
- async function loadRules() {
692
- document.getElementById('rules-list').innerHTML = loading();
686
+ // === Sessions ===
687
+ function sessionListItem(s) {
688
+ const prompt = (s.first_prompt || '(无提示词)').slice(0, 60);
689
+ return `<div class="list-item fade-in" onclick="openSessionDrawer('${s.session_id}')">
690
+ <div style="flex:1;min-width:0">
691
+ <div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${prompt}</div>
692
+ <div style="font-size:0.75rem;color:var(--text-dim)">${fmt(s.start_time)} · ${s.event_count} 个事件 · ${s.session_id.slice(0,8)}</div>
693
+ </div>
694
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
695
+ </div>`;
696
+ }
697
+
698
+ async function loadSessions() {
699
+ document.getElementById('sessions-list').innerHTML = loading();
693
700
  try {
694
- const res = await fetch(API + '/api/rules');
695
- const data = await res.json();
696
- allRules = [];
697
- Object.entries(data).forEach(([id, conv]) => {
698
- (conv.rules || []).forEach(r => allRules.push({ ...r, _convId: id, _convName: conv.name }));
699
- });
700
- renderRules(allRules);
701
+ const res = await fetch(API + '/api/sessions?limit=50');
702
+ allSessions = await res.json();
703
+ renderSessions(allSessions);
701
704
  } catch {
702
- document.getElementById('rules-list').innerHTML = empty('Failed to load rules');
705
+ document.getElementById('sessions-list').innerHTML = empty('加载会话失败');
703
706
  }
704
707
  }
705
708
 
706
- function renderRules(list) {
707
- document.getElementById('rules-list').innerHTML = list.length === 0
708
- ? empty('No rules found')
709
- : list.map(r => `<div class="list-item fade-in" onclick="openRuleDrawer(${JSON.stringify(r).replace(/"/g,'&quot;')})">
710
- <div style="flex:1;min-width:0">
711
- <div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${r.name || r.id || 'Unnamed'}</div>
712
- <div style="font-size:0.75rem;color:var(--text-dim)">${r._convName || r._convId} ${r.tags ? '· ' + r.tags.join(', ') : ''}</div>
713
- </div>
714
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
715
- </div>`).join('');
709
+ function renderSessions(list) {
710
+ document.getElementById('sessions-list').innerHTML = list.length === 0
711
+ ? empty('未找到会话')
712
+ : list.map(s => sessionListItem(s)).join('');
716
713
  }
717
714
 
718
- function filterRules() {
719
- const q = document.getElementById('rules-search').value.toLowerCase();
720
- renderRules(allRules.filter(r =>
721
- (r.name||'').toLowerCase().includes(q) ||
722
- (r.description||'').toLowerCase().includes(q) ||
723
- (r._convName||'').toLowerCase().includes(q) ||
724
- (r.tags||[]).some(t => t.toLowerCase().includes(q))
715
+ function filterSessions() {
716
+ const q = document.getElementById('sessions-search').value.toLowerCase();
717
+ renderSessions(allSessions.filter(s =>
718
+ (s.first_prompt || '').toLowerCase().includes(q) ||
719
+ s.session_id.toLowerCase().includes(q)
725
720
  ));
726
721
  }
727
722
 
728
- function openRuleDrawer(r) {
729
- const tags = (r.tags||[]).map(t => `<span class="tag">${t}</span>`).join('');
730
- const html = `
731
- <div class="detail-section"><div class="detail-label">Convention</div><div class="detail-value">${r._convName || r._convId}</div></div>
732
- <div class="detail-section"><div class="detail-label">Name</div><div class="detail-value" style="font-weight:600">${r.name || '—'}</div></div>
733
- ${tags ? `<div class="detail-section"><div class="detail-label">Tags</div><div class="detail-value">${tags}</div></div>` : ''}
734
- <div class="detail-section"><div class="detail-label">Description</div><div class="detail-value">${r.description || '—'}</div></div>
735
- ${r.when ? `<div class="detail-section"><div class="detail-label">Condition</div><pre class="detail-code">${r.when}</pre></div>` : ''}
736
- ${r.message ? `<div class="detail-section"><div class="detail-label">Message</div><div class="detail-value">${r.message}</div></div>` : ''}
737
- ${r.operator_guidance ? `<div class="detail-section"><div class="detail-label">Guidance</div><div class="detail-value" style="white-space:pre-wrap">${r.operator_guidance}</div></div>` : ''}
738
- ${r.doc_ref ? `<div class="detail-section"><div class="detail-label">Docs</div><div class="detail-value"><a href="${r.doc_ref}" target="_blank" style="color:var(--primary)">${r.doc_ref}</a></div></div>` : ''}
739
- `;
740
- openDrawer(r.name || 'Rule', html);
741
- }
742
- async function loadInjections() {
743
- document.getElementById('inj-list').innerHTML = loading();
723
+ let currentSessionData = null;
724
+
725
+ async function openSessionDrawer(sessionId) {
726
+ openDrawer('会话详情', loading());
744
727
  try {
745
- const res = await fetch(API + '/api/injections?limit=100');
746
- allInjections = await res.json();
747
- renderInjections(allInjections);
748
- } catch {
749
- document.getElementById('inj-list').innerHTML = empty('Failed to load');
728
+ const res = await fetch(API + '/api/sessions/' + sessionId + '/detail');
729
+ const data = await res.json();
730
+ currentSessionData = data;
731
+ const s = data.session;
732
+ let html = '<div class="detail-section"><div class="detail-label">会话 ID</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + s.session_id + '</div></div>';
733
+ html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(s.start_time) + ' → ' + fmt(s.end_time) + '</div></div>';
734
+ html += '<div class="detail-section"><div class="detail-label">首条提示词</div><div class="detail-value">' + (s.first_prompt || '—') + '</div></div>';
735
+ html += '<div class="detail-section"><div class="detail-label">任务(' + data.tasks.length + ')</div>';
736
+ data.tasks.forEach((t, i) => {
737
+ const tools = t.events.filter(e => e.tool_name).length;
738
+ html += '<div onclick="openTaskDrawer(' + i + ')" style="padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.5rem;border:1px solid var(--border);cursor:pointer;transition:all 0.15s" onmouseover="this.style.borderColor=\'var(--primary)\';this.style.background=\'var(--primary-soft)\'" onmouseout="this.style.borderColor=\'var(--border)\';this.style.background=\'var(--bg-secondary)\'">';
739
+ html += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px"><div style="font-weight:500;font-size:0.875rem;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:0.5rem">任务 ' + (i+1) + ':' + t.title + '</div><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>';
740
+ html += '<div style="font-size:0.75rem;color:var(--text-dim)">' + fmtTime(t.start_time) + ' · ' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件</div>';
741
+ if (t.summary.commits.length > 0) html += '<div style="font-size:0.75rem;color:var(--green);margin-top:4px">✓ ' + t.summary.commits.length + ' 次提交</div>';
742
+ html += '</div>';
743
+ });
744
+ html += '</div>';
745
+ openDrawer('会话 · ' + s.session_id.slice(0,8), html);
746
+ } catch(e) {
747
+ openDrawer('会话详情', empty('加载失败:' + e.message));
750
748
  }
751
749
  }
752
750
 
753
- function renderInjections(list) {
754
- document.getElementById('inj-list').innerHTML = list.length === 0
755
- ? empty('No injections found')
756
- : list.map(inj => `<div class="list-item fade-in" onclick="openInjDrawer(${JSON.stringify(inj).replace(/"/g,'&quot;')})">
757
- <div style="flex:1;min-width:0">
758
- <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:2px">
759
- <span class="badge badge-info">${inj.injection_type}</span>
760
- <span style="font-size:0.75rem;color:var(--text-dim)">${inj.source_handler}</span>
761
- </div>
762
- <div style="font-size:0.8rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${inj.content.slice(0,80)}</div>
763
- <div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">${fmt(inj.timestamp)} · ${inj.session_id.slice(0,8)}</div>
764
- </div>
765
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
766
- </div>`).join('');
767
- }
751
+ function openTaskDrawer(taskIndex) {
752
+ if (!currentSessionData || !currentSessionData.tasks[taskIndex]) return;
753
+ const t = currentSessionData.tasks[taskIndex];
754
+ const tools = t.events.filter(e => e.tool_name).length;
755
+
756
+ let html = '<div class="detail-section"><div class="detail-label">标题</div><div class="detail-value" style="font-weight:600">' + t.title + '</div></div>';
757
+ html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(t.start_time) + (t.end_time ? ' → ' + fmt(t.end_time) : '') + '</div></div>';
758
+ html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件变更 · ' + t.summary.commits.length + ' 次提交</div></div>';
759
+
760
+ // User prompts
761
+ if (t.prompts && t.prompts.length > 0) {
762
+ html += '<div class="detail-section"><div class="detail-label">用户提示词(' + t.prompts.length + ')</div>';
763
+ t.prompts.forEach(p => {
764
+ html += '<div style="padding:0.625rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
765
+ html += '<div style="color:var(--text-dim);font-size:0.7rem;margin-bottom:2px">' + fmtTime(p.timestamp) + '</div>';
766
+ html += '<div style="white-space:pre-wrap;word-break:break-word">' + (p.content || '—') + '</div>';
767
+ html += '</div>';
768
+ });
769
+ html += '</div>';
770
+ }
768
771
 
769
- function filterInjections() {
770
- const q = document.getElementById('inj-search').value.toLowerCase();
771
- const h = document.getElementById('inj-handler').value;
772
- renderInjections(allInjections.filter(inj =>
773
- (!h || inj.source_handler === h) &&
774
- (!q || inj.content.toLowerCase().includes(q) || inj.injection_type.toLowerCase().includes(q))
775
- ));
776
- }
772
+ // Tool usage summary
773
+ if (t.summary.toolUsage && Object.keys(t.summary.toolUsage).length > 0) {
774
+ html += '<div class="detail-section"><div class="detail-label">工具使用分布</div><div class="detail-value">';
775
+ Object.entries(t.summary.toolUsage).sort((a,b) => b[1]-a[1]).forEach(([name, count]) => {
776
+ html += '<span class="tag" style="margin-bottom:4px">' + name + ' × ' + count + '</span>';
777
+ });
778
+ html += '</div></div>';
779
+ }
777
780
 
778
- function openInjDrawer(inj) {
779
- const html = `
780
- <div class="detail-section"><div class="detail-label">Type</div><div class="detail-value"><span class="badge badge-info">${inj.injection_type}</span></div></div>
781
- <div class="detail-section"><div class="detail-label">Handler</div><div class="detail-value">${inj.source_handler}</div></div>
782
- <div class="detail-section"><div class="detail-label">Time</div><div class="detail-value">${fmt(inj.timestamp)}</div></div>
783
- <div class="detail-section"><div class="detail-label">Session</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">${inj.session_id}</div></div>
784
- <div class="detail-section"><div class="detail-label">Content</div><pre class="detail-code">${inj.content}</pre></div>
785
- `;
786
- openDrawer(inj.injection_type, html);
781
+ // Files changed
782
+ if (t.summary.filesChanged && t.summary.filesChanged.length > 0) {
783
+ html += '<div class="detail-section"><div class="detail-label">变更的文件(' + t.summary.filesChanged.length + ')</div>';
784
+ t.summary.filesChanged.forEach(f => {
785
+ const short = f.split('/').slice(-2).join('/');
786
+ html += '<div style="font-family:monospace;font-size:0.75rem;color:var(--text-muted);padding:2px 0" title="' + f + '">' + short + '</div>';
787
+ });
788
+ html += '</div>';
789
+ }
790
+
791
+ // Commits
792
+ if (t.summary.commits && t.summary.commits.length > 0) {
793
+ html += '<div class="detail-section"><div class="detail-label">提交记录(' + t.summary.commits.length + ')</div>';
794
+ t.summary.commits.forEach(c => {
795
+ html += '<div style="padding:0.5rem;background:var(--green-soft);border-radius:var(--radius-sm);margin-bottom:0.25rem;font-size:0.8rem;color:var(--text)"><span style="color:var(--green)">✓</span> ' + c.message + '</div>';
796
+ });
797
+ html += '</div>';
798
+ }
799
+
800
+ // Injections
801
+ if (t.injections && t.injections.length > 0) {
802
+ html += '<div class="detail-section"><div class="detail-label">注入内容(' + t.injections.length + ')</div>';
803
+ t.injections.forEach(inj => {
804
+ const escaped = inj.content.replace(/</g, '&lt;').replace(/>/g, '&gt;');
805
+ html += '<div style="padding:0.625rem;background:var(--primary-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--primary-border)">';
806
+ html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:4px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.7rem;color:var(--text-dim)">' + inj.source_handler + ' · ' + fmtTime(inj.timestamp) + '</span></div>';
807
+ html += '<div style="font-size:0.75rem;color:var(--text);white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto;font-family:ui-monospace,monospace">' + escaped + '</div>';
808
+ html += '</div>';
809
+ });
810
+ html += '</div>';
811
+ }
812
+
813
+ // Quality issues
814
+ if (t.qualityIssues && t.qualityIssues.length > 0) {
815
+ html += '<div class="detail-section"><div class="detail-label">质量问题(' + t.qualityIssues.length + ')</div>';
816
+ t.qualityIssues.forEach(q => {
817
+ const sev = q.severity || 'info';
818
+ const badgeClass = sev === 'must' ? 'badge-block' : sev === 'should' ? 'badge-warn' : 'badge-info';
819
+ const sevLabel = sev === 'must' ? '必须' : sev === 'should' ? '建议' : '提示';
820
+ html += '<div style="padding:0.5rem;background:var(--yellow-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
821
+ html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + sevLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (q.file_path || '').split('/').slice(-2).join('/') + '</span></div>';
822
+ html += '<div style="color:var(--text)">' + (q.description || q.message || '') + '</div>';
823
+ html += '</div>';
824
+ });
825
+ html += '</div>';
826
+ }
827
+
828
+ // Governance decisions
829
+ if (t.decisions && t.decisions.length > 0) {
830
+ html += '<div class="detail-section"><div class="detail-label">治理决策(' + t.decisions.length + ')</div>';
831
+ t.decisions.forEach(d => {
832
+ const lv = d.level || 'info';
833
+ const badgeClass = lv === 'block' ? 'badge-block' : lv === 'warn' ? 'badge-warn' : lv === 'allow' ? 'badge-allow' : 'badge-info';
834
+ const lvLabel = lv === 'block' ? '阻断' : lv === 'warn' ? '警告' : lv === 'allow' ? '放行' : lv === 'confirm' ? '确认' : lv;
835
+ html += '<div style="padding:0.5rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
836
+ html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + lvLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (d.rule_id || '') + '</span></div>';
837
+ html += '<div style="color:var(--text-muted)">' + (d.reason || '—') + '</div>';
838
+ html += '</div>';
839
+ });
840
+ html += '</div>';
841
+ }
842
+
843
+ // Recent events
844
+ if (t.events && t.events.length > 0) {
845
+ html += '<div class="detail-section"><div class="detail-label">事件(展示 ' + Math.min(t.events.length, 30) + ' / ' + t.events.length + ')</div>';
846
+ t.events.slice(0, 30).forEach(e => {
847
+ const inputPreview = e.tool_input ? (JSON.stringify(e.tool_input).slice(0, 80) + (JSON.stringify(e.tool_input).length > 80 ? '...' : '')) : '';
848
+ html += '<div style="padding:0.5rem;border-bottom:1px solid var(--border);font-size:0.75rem">';
849
+ html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px">';
850
+ html += '<span style="color:var(--text-dim)">' + fmtTime(e.timestamp) + '</span>';
851
+ html += badgeHook(e.hook_type);
852
+ if (e.tool_name) html += '<span style="font-family:monospace;color:var(--text-muted)">' + e.tool_name + '</span>';
853
+ html += '</div>';
854
+ if (inputPreview) html += '<div style="font-family:monospace;color:var(--text-dim);font-size:0.7rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + inputPreview + '</div>';
855
+ html += '</div>';
856
+ });
857
+ html += '</div>';
858
+ }
859
+
860
+ // Back button
861
+ html = '<div style="margin-bottom:1rem"><button class="btn" onclick="openSessionDrawer(\'' + currentSessionData.session.session_id + '\')">← 返回会话</button></div>' + html;
862
+
863
+ openDrawer('任务:' + t.title.slice(0, 40), html);
787
864
  }
865
+
866
+ // === Events ===
788
867
  async function loadEvents() {
789
- document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="loading">Loading...</td></tr>';
868
+ document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="loading">加载中...</td></tr>';
790
869
  try {
791
870
  const res = await fetch(API + '/api/events?limit=100');
792
871
  allEvents = await res.json();
793
872
  renderEvents(allEvents);
794
873
  } catch {
795
- document.getElementById('events-body').innerHTML = '<tr><td colspan="4">' + empty('Failed to load') + '</td></tr>';
874
+ document.getElementById('events-body').innerHTML = '<tr><td colspan="4">' + empty('加载失败') + '</td></tr>';
796
875
  }
797
876
  }
798
877
 
799
878
  function renderEvents(list) {
800
879
  document.getElementById('events-body').innerHTML = list.length === 0
801
- ? '<tr><td colspan="4">' + empty('No events') + '</td></tr>'
802
- : list.map(e => `<tr onclick="openEventDrawer(${JSON.stringify(e).replace(/"/g,'&quot;')})">
803
- <td style="color:var(--text-dim);font-size:0.8rem">${fmtTime(e.timestamp)}</td>
804
- <td>${badgeHook(e.hook_type)}</td>
805
- <td style="font-family:monospace;font-size:0.8rem">${e.tool_name || '—'}</td>
806
- <td style="color:var(--text-dim);font-size:0.8rem">${(e.project_path||'').split('/').pop() || '—'}</td>
807
- </tr>`).join('');
880
+ ? '<tr><td colspan="4">' + empty('暂无事件') + '</td></tr>'
881
+ : list.map(e => '<tr onclick="openEventDrawer(' + JSON.stringify(e).replace(/"/g,'&quot;') + ')"><td style="color:var(--text-dim);font-size:0.8rem">' + fmtTime(e.timestamp) + '</td><td>' + badgeHook(e.hook_type) + '</td><td style="font-family:monospace;font-size:0.8rem">' + (e.tool_name || '—') + '</td><td style="color:var(--text-dim);font-size:0.8rem">' + ((e.project_path||'').split('/').pop() || '—') + '</td></tr>').join('');
808
882
  }
809
883
 
810
884
  function filterEvents() {
@@ -817,84 +891,76 @@ function filterEvents() {
817
891
  }
818
892
 
819
893
  function openEventDrawer(e) {
820
- const html = `
821
- <div class="detail-section"><div class="detail-label">Type</div><div class="detail-value">${badgeHook(e.hook_type)}</div></div>
822
- <div class="detail-section"><div class="detail-label">Time</div><div class="detail-value">${fmt(e.timestamp)}</div></div>
823
- <div class="detail-section"><div class="detail-label">Tool</div><div class="detail-value" style="font-family:monospace">${e.tool_name || '—'}</div></div>
824
- <div class="detail-section"><div class="detail-label">Project</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">${e.project_path || '—'}</div></div>
825
- <div class="detail-section"><div class="detail-label">Session</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">${e.session_id || '—'}</div></div>
826
- ${e.tool_input ? `<div class="detail-section"><div class="detail-label">Input</div><pre class="detail-code">${JSON.stringify(e.tool_input, null, 2)}</pre></div>` : ''}
827
- `;
828
- openDrawer(e.hook_type + ' · ' + (e.tool_name || 'event'), html);
829
- }
830
- function sessionListItem(s) {
831
- const prompt = (s.first_prompt || '(no prompt)').slice(0, 60);
832
- return `<div class="list-item fade-in" onclick="openSessionDrawer('${s.session_id}')">
833
- <div style="flex:1;min-width:0">
834
- <div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${prompt}</div>
835
- <div style="font-size:0.75rem;color:var(--text-dim)">${fmt(s.start_time)} · ${s.event_count} events · ${s.session_id.slice(0,8)}</div>
836
- </div>
837
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
838
- </div>`;
894
+ let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value">' + badgeHook(e.hook_type) + '</div></div>';
895
+ html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(e.timestamp) + '</div></div>';
896
+ html += '<div class="detail-section"><div class="detail-label">工具</div><div class="detail-value" style="font-family:monospace">' + (e.tool_name || '—') + '</div></div>';
897
+ html += '<div class="detail-section"><div class="detail-label">项目</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.project_path || '—') + '</div></div>';
898
+ html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.session_id || '—') + '</div></div>';
899
+ if (e.tool_input) html += '<div class="detail-section"><div class="detail-label">输入参数</div><pre class="detail-code">' + JSON.stringify(e.tool_input, null, 2) + '</pre></div>';
900
+ openDrawer(e.hook_type + ' · ' + (e.tool_name || '事件'), html);
839
901
  }
840
902
 
841
- async function loadSessions() {
842
- document.getElementById('sessions-list').innerHTML = loading();
903
+ // === Injections ===
904
+ async function loadInjections() {
905
+ document.getElementById('inj-list').innerHTML = loading();
843
906
  try {
844
- const res = await fetch(API + '/api/sessions?limit=50');
845
- allSessions = await res.json();
846
- renderSessions(allSessions);
907
+ const res = await fetch(API + '/api/injections?limit=100');
908
+ allInjections = await res.json();
909
+ renderInjections(allInjections);
847
910
  } catch {
848
- document.getElementById('sessions-list').innerHTML = empty('Failed to load sessions');
911
+ document.getElementById('inj-list').innerHTML = empty('加载失败');
849
912
  }
850
913
  }
851
914
 
852
- function renderSessions(list) {
853
- document.getElementById('sessions-list').innerHTML = list.length === 0
854
- ? empty('No sessions found')
855
- : list.map(s => sessionListItem(s)).join('');
915
+ function renderInjections(list) {
916
+ document.getElementById('inj-list').innerHTML = list.length === 0 ? empty('暂无注入记录') : list.map(inj => '<div class="list-item fade-in" onclick="openInjDrawer(' + JSON.stringify(inj).replace(/"/g,'&quot;') + ')"><div style="flex:1;min-width:0"><div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:2px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.75rem;color:var(--text-dim)">' + inj.source_handler + '</span></div><div style="font-size:0.8rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + inj.content.slice(0,80) + '</div><div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">' + fmt(inj.timestamp) + ' · ' + inj.session_id.slice(0,8) + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
856
917
  }
857
918
 
858
- function filterSessions() {
859
- const q = document.getElementById('sessions-search').value.toLowerCase();
860
- renderSessions(allSessions.filter(s =>
861
- (s.first_prompt || '').toLowerCase().includes(q) ||
862
- s.session_id.toLowerCase().includes(q)
863
- ));
919
+ function filterInjections() {
920
+ const q = document.getElementById('inj-search').value.toLowerCase();
921
+ const h = document.getElementById('inj-handler').value;
922
+ renderInjections(allInjections.filter(inj => (!h || inj.source_handler === h) && (!q || inj.content.toLowerCase().includes(q) || inj.injection_type.toLowerCase().includes(q))));
864
923
  }
865
924
 
866
- async function openSessionDrawer(sessionId) {
867
- openDrawer('Session Detail', loading());
925
+ function openInjDrawer(inj) {
926
+ let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value"><span class="badge badge-info">' + inj.injection_type + '</span></div></div>';
927
+ html += '<div class="detail-section"><div class="detail-label">Handler</div><div class="detail-value">' + inj.source_handler + '</div></div>';
928
+ html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(inj.timestamp) + '</div></div>';
929
+ html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + inj.session_id + '</div></div>';
930
+ html += '<div class="detail-section"><div class="detail-label">注入内容</div><pre class="detail-code">' + inj.content + '</pre></div>';
931
+ openDrawer(inj.injection_type, html);
932
+ }
933
+
934
+ // === Rules ===
935
+ async function loadRules() {
936
+ document.getElementById('rules-list').innerHTML = loading();
868
937
  try {
869
- const res = await fetch(API + '/api/sessions/' + sessionId + '/detail');
870
- const data = await res.json();
871
- const s = data.session;
872
- let html = `
873
- <div class="detail-section">
874
- <div class="detail-label">Session ID</div>
875
- <div class="detail-value" style="font-family:monospace;font-size:0.8rem">${s.session_id}</div>
876
- </div>
877
- <div class="detail-section">
878
- <div class="detail-label">Time</div>
879
- <div class="detail-value">${fmt(s.start_time)} → ${fmt(s.end_time)}</div>
880
- </div>
881
- <div class="detail-section">
882
- <div class="detail-label">First Prompt</div>
883
- <div class="detail-value">${s.first_prompt || '—'}</div>
884
- </div>
885
- <div class="detail-section">
886
- <div class="detail-label">Tasks (${data.tasks.length})</div>`;
887
- data.tasks.forEach((t, i) => {
888
- const tools = t.events.filter(e => e.tool_name).length;
889
- html += `<div style="padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.5rem;border:1px solid var(--border)">
890
- <div style="font-weight:500;font-size:0.875rem;margin-bottom:4px">Task ${i+1}: ${t.title}</div>
891
- <div style="font-size:0.75rem;color:var(--text-dim)">${fmtTime(t.start_time)} · ${tools} tools · ${t.summary.filesChanged.length} files</div>
892
- ${t.summary.commits.length > 0 ? `<div style="font-size:0.75rem;color:var(--green);margin-top:4px">✓ ${t.summary.commits.length} commit(s)</div>` : ''}
893
- </div>`;
894
- });
895
- html += `</div>`;
896
- openDrawer('Session · ' + s.session_id.slice(0,8), html);
897
- } catch(e) {
898
- openDrawer('Session Detail', empty('Failed to load: ' + e.message));
938
+ const res = await fetch(API + '/api/rules');
939
+ allRules = await res.json();
940
+ renderRules(allRules);
941
+ } catch {
942
+ document.getElementById('rules-list').innerHTML = empty('加载规则失败');
899
943
  }
900
944
  }
945
+
946
+ function renderRules(list) {
947
+ document.getElementById('rules-list').innerHTML = list.length === 0 ? empty('未找到规则') : list.map(r => '<div class="list-item fade-in" onclick="openRuleDrawer(' + JSON.stringify(r).replace(/"/g,'&quot;') + ')"><div style="flex:1;min-width:0"><div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">' + (r.name || r.id) + '</div><div style="font-size:0.75rem;color:var(--text-dim)">' + (r.description || '') + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
948
+ }
949
+
950
+ function filterRules() {
951
+ const q = document.getElementById('rules-search').value.toLowerCase();
952
+ renderRules(allRules.filter(r => (r.name||'').toLowerCase().includes(q) || (r.description||'').toLowerCase().includes(q)));
953
+ }
954
+
955
+ function openRuleDrawer(r) {
956
+ let html = '<div class="detail-section"><div class="detail-label">名称</div><div class="detail-value" style="font-weight:600">' + (r.name || '—') + '</div></div>';
957
+ html += '<div class="detail-section"><div class="detail-label">描述</div><div class="detail-value">' + (r.description || '—') + '</div></div>';
958
+ if (r.stats) html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">触发 ' + (r.stats.totalTriggers || 0) + ' 次 | 阻断 ' + (r.stats.blockCount || 0) + ' 次 | 警告 ' + (r.stats.warnCount || 0) + ' 次</div></div>';
959
+ openDrawer(r.name || '规则', html);
960
+ }
961
+
962
+ // === Init ===
963
+ nav('dashboard');
964
+ </script>
965
+ </body>
966
+ </html>