@@ -8,8 +8,6 @@ const SHARE_ANALYTICS_JSON = join(OUT_DIR, "share-analytics.json");
88const OUTPUT_INDEX_HTML = join ( OUT_DIR , "index.html" ) ;
99const OUTPUT_HTML = join ( OUT_DIR , "contributor-card-dashboard.html" ) ;
1010
11- const PAGES_ANALYTICS_URL =
12- "https://dash.cloudflare.com/64ad4569ffd912432d6b86d5656484c4/pages/view/open-design-landing/analytics" ;
1311const DASHBOARD_PUBLIC_BASE = "https://nexu-io.github.io/open-design-bot-sandbox" ;
1412const SHARE_ANALYTICS_JSON_PUBLIC_URL = `${ DASHBOARD_PUBLIC_BASE } /share-analytics.json` ;
1513const SHARE_ANALYTICS_CSV_PUBLIC_URL = `${ DASHBOARD_PUBLIC_BASE } /share-analytics.csv` ;
@@ -109,13 +107,12 @@ function countRows(rows: Record<string, number>): string {
109107 . join ( "" ) ;
110108}
111109
112- function recentRows ( events : CardEventRow [ ] ) : string {
113- return events . slice ( - 12 ) . reverse ( ) . map ( ( event ) => `<tr>
110+ function allRows ( events : CardEventRow [ ] ) : string {
111+ return [ ... events ] . reverse ( ) . map ( ( event ) => `<tr>
114112 <td>${ escapeHtml ( event . createdAt . replace ( "T" , " " ) . replace ( "Z" , "" ) ) } </td>
115113 <td>@${ escapeHtml ( event . recipient ) } </td>
116114 <td>${ escapeHtml ( event . tierName ) } </td>
117115 <td>${ escapeHtml ( event . surface ) } </td>
118- <td>${ event . duplicateKind === "unique" ? "Unique" : "Historical duplicate" } </td>
119116 <td><a href="${ escapeHtml ( event . commentUrl ) } ">#${ escapeHtml ( event . threadNumber ) } </a></td>
120117 </tr>` ) . join ( "" ) ;
121118}
@@ -195,8 +192,7 @@ function renderShareAnalyticsPanel(
195192 ${ stat ( "Public JSON" , "Not generated" , "needs CF token" , "缺 CF token" ) }
196193 ${ stat ( "Public CSV" , "Not generated" , "needs CF token" , "缺 CF token" ) }
197194 </div>
198- <p><a class="cta" href="${ PAGES_ANALYTICS_URL } " target="_blank" rel="noreferrer">Open Cloudflare Pages Analytics →</a></p>
199- </section>` ;
195+ </section>` ;
200196 }
201197 const win = analytics . windows . last30Days ;
202198 const daily = win . daily ;
@@ -227,7 +223,6 @@ function renderShareAnalyticsPanel(
227223 <li><a href="${ SHARE_ANALYTICS_CSV_PUBLIC_URL } " target="_blank" rel="noreferrer"><code>${ SHARE_ANALYTICS_CSV_PUBLIC_URL } </code></a> — <code>=IMPORTDATA(...)</code> in Google Sheets · Sheets 一行公式接入</li>
228224 <li>Refreshed hourly by <code>contributor-dashboard.yml</code> · 由 <code>contributor-dashboard.yml</code> 每小时刷新</li>
229225 </ul>
230- <p><a class="cta" href="${ PAGES_ANALYTICS_URL } " target="_blank" rel="noreferrer">Open Cloudflare Pages Analytics →</a></p>
231226 </section>` ;
232227}
233228
@@ -239,10 +234,6 @@ function main() {
239234 const events = parseCsv ( readFileSync ( EVENTS_CSV , "utf8" ) ) as unknown as CardEventRow [ ] ;
240235 const summary = JSON . parse ( readFileSync ( SUMMARY_JSON , "utf8" ) ) as Summary ;
241236 const shareAnalytics = loadShareAnalytics ( ) ;
242- const duplicateRows = Object . entries ( summary . duplicateRecipients )
243- . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
244- . map ( ( [ recipient , count ] ) => `<tr><td>@${ escapeHtml ( recipient ) } </td><td>${ count } </td><td>historical duplicate burst</td></tr>` )
245- . join ( "" ) ;
246237
247238 const cardsWithShareLink = events . filter ( ( event ) => event . shareUrl ) . length ;
248239 const shareAnalyticsPanel = renderShareAnalyticsPanel ( shareAnalytics , cardsWithShareLink ) ;
@@ -259,7 +250,7 @@ function main() {
259250 h1 { font-size: clamp(36px, 6vw, 72px); line-height:.95; letter-spacing:-.06em; margin:0 0 10px; }
260251 h2 { margin:0 0 18px; }
261252 p, small, .muted { color:#94a3b8; line-height:1.65; }
262- .stats { display:grid; grid-template-columns:repeat(4 , minmax(0 , 1fr)); gap:16px; margin:24px 0; }
253+ .stats { display:grid; grid-template-columns:repeat(auto-fit , minmax(200px , 1fr)); gap:16px; margin:24px 0; }
263254 .stat, .panel { border:1px solid rgba(148,163,184,.24); border-radius:24px; background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03)); box-shadow:0 24px 80px rgba(2,6,23,.38); }
264255 .stat { min-height:140px; padding:20px; display:flex; flex-direction:column; justify-content:space-between; }
265256 .stat span { color:#94a3b8; }
@@ -273,8 +264,10 @@ function main() {
273264 th, td { padding:11px 10px; border-bottom:1px solid rgba(148,163,184,.24); text-align:left; font-size:14px; }
274265 th { color:#94a3b8; }
275266 a { color:#22d3ee; text-decoration:none; }
276- a.cta { display:inline-block; margin-top:8px; padding:12px 22px; border-radius:14px; background:linear-gradient(90deg, #22d3ee, #fbbf24); color:#0f172a; font-weight:700; letter-spacing:.01em; }
277- a.cta:hover { filter:brightness(1.08); }
267+ .pagination { display:flex; align-items:center; gap:12px; margin-top:16px; justify-content:center; }
268+ .pagination button { padding:8px 16px; border:1px solid rgba(148,163,184,.4); border-radius:8px; background:rgba(255,255,255,.06); color:#f8fafc; cursor:pointer; font-size:13px; }
269+ .pagination button:hover { background:rgba(34,211,238,.15); border-color:#22d3ee; }
270+ .pagination span { color:#94a3b8; font-size:13px; }
278271 ul { margin:6px 0 14px 22px; padding:0; }
279272 @media (max-width:900px) { .stats, .grid { grid-template-columns:1fr; } main { width:min(100vw - 28px, 720px); } }
280273 </style>
@@ -286,16 +279,8 @@ function main() {
286279 <p class="muted">Generated 生成时间: ${ escapeHtml ( summary . generatedAt ) } </p>
287280
288281 <section class="stats">
289- ${ stat ( "Deduped Cards Posted" , summary . dedupedCardEvents , "business reporting count" , "用于汇报的去重口径" ) }
290282 ${ stat ( "Raw Cards Posted" , summary . rawCardComments , "visible bot comments" , "真实可见 bot 评论数" ) }
291283 ${ stat ( "Unique Recipients" , summary . uniqueRecipients , "contributors who received a card" , "收到卡片的去重贡献者" ) }
292- ${ stat ( "Historical Duplicates" , summary . historicalDuplicateCount , "duplicate burst events excluded from headline metrics" , "已从核心指标排除的历史重复卡" ) }
293- </section>
294-
295- <section class="panel">
296- <h2>Historical Duplicate Bursts 历史重复发卡</h2>
297- <p class="muted">These rows came from the old workflow race condition. They remain visible on GitHub for audit, but are excluded from deduped headline metrics.<br>这些记录来自旧 workflow 并发问题。GitHub 上仍可见用于审计,但不会进入去重后的核心汇报指标。</p>
298- <table><thead><tr><th>Recipient</th><th>Duplicate cards</th><th>Reason</th></tr></thead><tbody>${ duplicateRows || `<tr><td colspan="3">No duplicate burst detected.</td></tr>` } </tbody></table>
299284 </section>
300285
301286 <section class="grid">
@@ -306,9 +291,34 @@ function main() {
306291 ${ shareAnalyticsPanel }
307292
308293 <section class="panel">
309- <h2>Recent Card Comments 最近的卡片评论</h2>
310- <table><thead><tr><th>Time</th><th>Recipient</th><th>Tier</th><th>Surface</th><th>Dedupe</th><th>Thread</th></tr></thead><tbody>${ recentRows ( events ) } </tbody></table>
294+ <h2>Card Comments 卡片评论(共 ${ events . length } 条)</h2>
295+ <div id="card-table-wrap">
296+ <table><thead><tr><th>Time</th><th>Recipient</th><th>Tier</th><th>Surface</th><th>Thread</th></tr></thead><tbody id="card-tbody">${ allRows ( events ) } </tbody></table>
297+ </div>
298+ <div class="pagination" id="pagination"></div>
311299 </section>
300+ <script>
301+ (function(){
302+ const PAGE_SIZE = 20;
303+ const tbody = document.getElementById('card-tbody');
304+ const allRows = Array.from(tbody.querySelectorAll('tr'));
305+ const totalPages = Math.ceil(allRows.length / PAGE_SIZE);
306+ let currentPage = 1;
307+ function render() {
308+ const start = (currentPage - 1) * PAGE_SIZE;
309+ const end = start + PAGE_SIZE;
310+ allRows.forEach((row, i) => { row.style.display = (i >= start && i < end) ? '' : 'none'; });
311+ const pag = document.getElementById('pagination');
312+ let html = '';
313+ if (currentPage > 1) html += '<button onclick="window.__goPage(' + (currentPage - 1) + ')">← 上一页</button>';
314+ html += '<span>第 ' + currentPage + ' / ' + totalPages + ' 页</span>';
315+ if (currentPage < totalPages) html += '<button onclick="window.__goPage(' + (currentPage + 1) + ')">下一页 →</button>';
316+ pag.innerHTML = html;
317+ }
318+ window.__goPage = function(p) { currentPage = p; render(); document.getElementById('card-table-wrap').scrollIntoView({behavior:'smooth'}); };
319+ render();
320+ })();
321+ </script>
312322</main>
313323</body>
314324</html>` ;
0 commit comments