@@ -162,6 +162,30 @@ function hm(iso) {
if ( idx < 0 || s . length < idx + 6 ) return s . slice ( 0 , 5 ) || '—' ;
return s . slice ( idx + 1 , idx + 6 ) ;
}
/* 大白话错误摘要:主表只放一行短句,完整原文放 title 悬停提示。
不猜业务结论,只做确定性文案映射 + 定长截断。 */
function plainError ( raw , max = 18 ) {
const s = String ( raw || '' ) ;
if ( ! s ) return '' ;
const rules = [
[ /UNIQUE constraint failed/i , '重复数据写入冲突' ] ,
[ /circuit open|breaker/i , '熔断保护中' ] ,
[ /timeout|timed out|ETIMEDOUT/i , '请求超时' ] ,
[ /row ratio/i , '行数校验未达标' ] ,
[ /field gate/i , '字段完整率未达标' ] ,
[ /empty official batch|no_rows_returned|empty_or_incomplete/i , '上游返回空数据' ] ,
[ /release group not switched/i , '发布组未切换' ] ,
[ /missing_fields/i , '返回缺字段' ] ,
[ /data_age_exceeds/i , '数据过期超阈值' ] ,
[ /blocked|captcha|安全验证|访问异常/i , '被上游拦截' ] ,
] ;
for ( const [ re , text ] of rules ) if ( re . test ( s ) ) return text ;
return s . length > max ? s . slice ( 0 , max ) + '…' : s ;
}
/* 单行截断单元格:class ellip( styles.css 生产补充段),title 放全文 */
function ellipHtml ( text , color , fontSize , title ) {
return ` <span class="ellip" style="color: ${ color } ;font-size: ${ fontSize || '' } " title=" ${ esc ( title ? ? text ) } "> ${ esc ( text ) } </span> ` ;
}
const p2 = ( x ) => String ( x ) . padStart ( 2 , '0' ) ;
/* ================= sparkline(真实历史:来自 provider_call_log) ================= */
@@ -377,7 +401,7 @@ function heroHtml(vm) {
` </div><span class="flex1"></span> ` +
` <div class="kpis"> ` +
kpiHtml ( '已发布数据集' , ` <span class="glow-mint"> ${ vm . publishedCount } </span><span style="color:#54637e;font-size:18px">/ ${ vm . totalOfficial } </span> ` , vm . unpublished . length ? ` ${ vm . unpublished . map ( ( d ) => DATASET _NAME [ d ] || d ) . join ( ' · ' ) } 未发布 ` : '全部已发布' ) +
kpiHtml ( '今日调用成功率' , vm . successRate == null ? '<span style="color:#54637e">—</span>' : ` <span> ${ vm . successRate . toFixed ( 1 ) } %</span> ` , null , 'mint' , vm . successRate != null ) +
kpiHtml ( '今日调用成功率' , vm . successRate == null ? '<span style="color:#54637e">—</span>' : ` <span data-rate > ${ vm . successRate . toFixed ( 1 ) } %</span> ` , null , 'mint' , vm . successRate != null ) +
kpiHtml ( '主站回退页面' , ` ${ vm . fallbackPages . length } 页 ` , vm . fallbackPages . length ? esc ( vm . fallbackPages [ 0 ] ) : '实时层主源正常' , vm . fallbackPages . length ? 'amb' : 'txt' ) +
kpiHtml ( 'EOD 尝试' , ` ${ vm . eod . attempts || 0 } 次 ` , '上限 5 · 23:30 止' , vm . eod . attempts ? 'rd' : 'txt' ) +
` </div></div> ` ;
@@ -389,7 +413,7 @@ function datasetStripHtml(vm) {
let note = '' ;
if ( d . time ) note += ` <span class="num" style="color:#8b9bb4"> ${ esc ( d . time ) } · </span> ` ;
if ( d . rows ) note += ` <span class="num"> ${ Number ( d . rows ) . toLocaleString ( 'en-US' ) } 行</span> ` ;
if ( d . err ) { const brief = String ( d . err ) . length > 22 ? String ( d . err ) . slice ( 0 , 22 ) + '…' : d . err ; note += ` <span style="color:#f87171" title=" ${ esc ( d . err ) } "> ${ esc ( brief ) } </span> ` ; }
if ( d . err ) note += ` <span class="ellip" style="color:#f87171" title=" ${ esc ( d . err ) } "> ${ esc ( plainError ( d . err , 16 ) ) } </span> ` ;
if ( ! d . time && ! d . rows && ! d . err ) note = '<span style="color:#54637e">今日尚未发布</span>' ;
return ` <div class="panel dsc ${ ring } " data-dataset=" ${ d . id } "> ` +
` <div class="dsc-top"><span class="dsc-name"> ${ esc ( d . name ) } </span> ${ pillHtml ( d . bucket ) } </div> ` +
@@ -403,11 +427,11 @@ function observatoryHtml(vm) {
const right = ` <span class="legend"> ${ Object . keys ( legendByProv ) . map ( ( p ) => ` <span> ${ ledHtml ( 'ok' ) } ${ legendByProv [ p ] } </span> ` ) . join ( '' ) } </span> ` ;
const rows = vm . observers . length ? vm . observers . map ( ( o ) => ` <tr> ` +
` <td style="color:#e8f1ff"> ${ esc ( o . name ) } </td> ` +
` <td style="color:#8b9bb4"> ${ o . provider === 'eastmoney' ? '东方财富' : '腾讯行情' } </td> ` +
` <td style="color:#8b9bb4;white-space:nowrap "> ${ o . provider === 'eastmoney' ? '东方财富' : '腾讯行情' } </td> ` +
` <td> ${ ageHtml ( o . lastOk ) } </td> ` +
` <td> ${ latHtml ( o . lat , o . bucket !== 'ok' ) } </td> ` +
` <td> ${ sparkHtml ( o . jid , o . bucket === 'slow' ? 'amb' : 'mint' , 64 , 16 ) } </td> ` +
` <td><span style="display:flex;align-items:center;gap:8px"> ${ pillHtml ( o . bucket ) } ${ o . note ? ` <span class="obs-note"> ${ esc ( o . note ) } </span> ` : '' } </span></td> ` +
` <td style="max-width:260px" ><span style="display:flex;align-items:center;gap:8px;min-width:0 "> ${ pillHtml ( o . bucket ) } ${ o . note ? ellipHtml ( plainError ( o . note , 20 ) , '' , '10px' , o . note ) : '' } </span></td> ` +
` </tr> ` ) . join ( '' ) : ` <tr><td colspan="6" class="empty-hint">尚无真实观测记录(等待首次探测/调用)</td></tr> ` ;
const body = ` <table class="dtable"><thead><tr><th>观察项</th><th>当前来源</th><th>缓存年龄</th><th>延迟</th><th>趋势</th><th>状态 · 备注</th></tr></thead><tbody> ${ rows } </tbody></table> ` ;
return panelHtml ( '实时观察层' , right , body , false ) ;
@@ -418,7 +442,7 @@ function siteImpactHtml(vm) {
const rows = bad . length ? bad . slice ( 0 , 5 ) . map ( ( s ) => ` <tr> ` +
` <td style="color:#e8f1ff;width:40%"> ${ esc ( s . page ) } </td> ` +
` <td style="width:20%"> ${ pillHtml ( s . bucket ) } </td> ` +
` <td style="font-size:11px;color:#54637e"> ${ s . note ? esc ( s . note ) : '—' } </td> ` +
` <td style="font-size:11px;color:#54637e;max-width:220px "> ${ s . note ? ellipHtml ( plainError ( s . note , 22 ) , '#54637e' , '11px' , s . note ) : '—' } </td> ` +
` <td style="text-align:right"><button class="tbtn mini" data-goto-lineage=" ${ esc ( s . dataset ) } ">查看血缘</button></td> ` +
` </tr> ` ) . join ( '' ) : ` <tr><td style="color:#e8f1ff;width:40%">全部页面正常</td><td> ${ pillHtml ( 'ok' ) } </td><td style="font-size:11px;color:#54637e">—</td><td></td></tr> ` ;
const footPages = vm . siteImpact . map ( ( s ) => ( { name : s . short , bucket : s . bucket } ) ) ;
@@ -479,7 +503,7 @@ function incidentsHtml(vm) {
return ` <div class="inc ${ it . sev } "> ${ tag } ` +
` <div style="flex:1;min-width:0"> ` +
` <div class="inc-title" ${ it . sev === 'off' ? ' style="color:#8b9bb4"' : '' } > ${ esc ( it . title ) } </div> ` +
` <div class="inc-meta"> ${ meta } </div> ` +
` <div class="inc-meta ellip" title=" ${ esc ( String ( it . meta ) . replace ( /<[^>]+>/g , '' ) ) } "> ${ meta } </div> ` +
` </div> ` +
` <button class="tbtn" style="flex:none" data-incident-action data-dataset=" ${ esc ( it . dataset || '' ) } " data-provider=" ${ esc ( it . provider || '' ) } ">去处理 →</button> ` +
` </div> ` ;
@@ -589,7 +613,7 @@ function sourceCardHtml(card) {
` <span>接口 <span class="num" style="color: ${ card . bucket === 'off' ? '#54637e' : '#22d3ee' } "> ${ card . usedCount } / ${ card . totalCount } </span> 在用</span> ` +
` <span class="msep">|</span><span>最后探测 ${ probeCell } </span> ` +
` <span class="msep">|</span><span>延迟 ${ latHtml ( card . latency ) } </span> ` +
( card . error ? ` <span class="msep">|</span><span style="color:#f87171">最近错误 ${ esc ( card . error ) } </span> ` : ( card . bucket !== 'off' ? ` <span class="msep">|</span><span style="color:#54637e">无最近错误</span> ` : '' ) ) +
( card . error ? ` <span class="msep">|</span><span style="color:#f87171;display:inline-flex;align-items:center;gap:4px;min-width:0 ">最近错误 ${ ellipHtml ( plainError ( card . error , 26 ) , '#f87171' , '11px' , card . error ) } </span> ` : ( card . bucket !== 'off' ? ` <span class="msep">|</span><span style="color:#54637e">无最近错误</span> ` : '' ) ) +
` </div> ` ;
if ( ui . open && card . bucket !== 'off' ) {
h += ` <div class="mx-grid"> ` + card . groups . map ( ( g ) => ` <div style="min-width:0"> ` +
@@ -600,7 +624,7 @@ function sourceCardHtml(card) {
` <td style="color:#8b9bb4;font-size:11px;white-space:nowrap"> ${ esc ( it . use ) } </td> ` +
` <td class="num" style="font-size:11px;color: ${ it . ok . includes ( '× ' ) || it . ok === '昨日' ? '#f87171' : '#8b9bb4' } "> ${ esc ( it . ok ) } </td> ` +
` <td style="font-size:11px"> ${ latHtml ( it . lat ) } </td> ` +
` <td style="white-space:nowrap"><span style="display:flex;align-items:center;gap:6px"> ${ pillHtml ( it . st , it . st === 'plan' && it . obs === false ? '已配置 · 待观测' : '' ) } ${ it . err ? ` <span style="font-size:10px;color:#f87171"> ${ esc ( it . err ) } </span> ` : '' } </span></td> ` +
` <td style="white-space:nowrap"><span style="display:flex;align-items:center;gap:6px"> ${ pillHtml ( it . st , it . st === 'plan' && it . obs === false ? '已配置 · 待观测' : '' ) } ${ it . err ? ellipHtml ( plainError ( it . err , 16 ) , '#f87171' , '10px' , it . err ) : '' } </span></td> ` +
` </tr> ` ) . join ( '' ) +
` </tbody></table></div> ` ) . join ( '' ) + ` </div> ` ;
}
@@ -765,7 +789,7 @@ function lineageGraphHtml(vm) {
const p = gpos [ 's:' + s . id ] ; if ( ! p ) return ;
h += ` <g data-nid=" ${ esc ( s . id ) } " data-gkind="src" style="cursor:pointer;transition:opacity .18s"> ` +
` <circle cx=" ${ p . x } " cy=" ${ p . y } " r="16" fill="rgba(34,211,238,.05)" stroke="#243152"/> ` +
` <circle cx=" ${ p . x } " cy=" ${ p . y } " r="5" fill=" ${ stColorOf ( s . st ) } " ${ s . st !== 'off' ? ' class="gnode-pulse"' : '' } /> ` +
` <circle cx=" ${ p . x } " cy=" ${ p . y } " r="5" fill=" ${ stColorOf ( s . st ) } " ${ s . st !== 'off' ? ` class="gnode-pulse" style="animation-delay: ${ ( vm . sources . indexOf ( s ) * 0.37 ) . toFixed ( 2 ) } s" ` : '' } /> ` +
` <text x=" ${ p . x + 24 } " y=" ${ p . y + 4 } " font-size="12" fill="#d7e1f0" font-weight="600"> ${ esc ( s . name ) } </text></g> ` ;
} ) ;
vm . datasets . forEach ( ( d ) => {
@@ -838,7 +862,7 @@ function renderLinRows(vm) {
` <td class="num" style="color:#8b9bb4;font-size:11px;white-space:nowrap"> ${ esc ( r . freq ) } </td> ` +
` <td class="num" style="font-size:11px;white-space:nowrap;color:#8b9bb4"> ${ r . ok ? esc ( hm ( r . ok ) ) : '—' } </td> ` +
` <td class="num" style="font-size:11px;color: ${ r . lat != null && r . lat > 1000 ? '#fbbf24' : '#8b9bb4' } "> ${ r . lat != null ? Number ( r . lat ) . toLocaleString ( 'en-US' ) + 'ms' : '—' } </td> ` +
` <td><span style="display:flex;align-items:center;gap:8px;white-space:nowrap"> ${ pillHtml ( r . st , stLabel ( r ) ) } ${ r . note && ! stLabel ( r ) ? ` <span style="font-size:10px;color:#54637e"> ${ esc ( r . note ) } </span> ` : '' } </span></td> ` +
` <td><span style="display:flex;align-items:center;gap:8px;white-space:nowrap"> ${ pillHtml ( r . st , stLabel ( r ) ) } ${ r . note && ! stLabel ( r ) ? ellipHtml ( plainError ( r . note , 20 ) , '#54637e' , '10px' , r . note ) : '' } </span></td> ` +
` </tr> ` ) . join ( '' ) || ` <tr><td colspan="9" class="empty-hint">无匹配记录 · 调整筛选条件</td></tr> ` ;
if ( bodyN ) bodyN . innerHTML = rows . map ( ( r ) => ` <tr class=" ${ cls ( r . st ) } "> ` +
` <td style="color:#e8f1ff;white-space:nowrap"> ${ esc ( r . page ) } <span class="sub" style="color:#8b9bb4"> ${ esc ( r . item ) } </span></td> ` +
@@ -846,7 +870,7 @@ function renderLinRows(vm) {
` <td style="color:#8b9bb4;font-size:11px;word-break:break-all"> ${ esc ( r . via ) } </td> ` +
` <td class="num" style="color:#8b9bb4;font-size:11px;white-space:nowrap"> ${ esc ( r . freq ) } <span class="sub"> ${ esc ( r . role ) } </span></td> ` +
` <td class="num" style="font-size:11px;white-space:nowrap;color:#8b9bb4"> ${ r . ok ? esc ( hm ( r . ok ) ) : '—' } <span class="sub"> ${ r . lat != null ? r . lat + 'ms' : '—' } </span></td> ` +
` <td> ${ pillHtml ( r . st , stLabel ( r ) ) } ${ r . note && ! stLabel ( r ) ? ` <span class="sub"> ${ esc ( r . note ) } </span> ` : '' } </td> ` +
` <td> ${ pillHtml ( r . st , stLabel ( r ) ) } ${ r . note && ! stLabel ( r ) ? ` <span class="sub ellip" title=" ${ esc ( r . note ) } "> ${ esc ( plainError ( r . note , 20 ) ) } </span> ` : '' } </td> ` +
` </tr> ` ) . join ( '' ) || ` <tr><td colspan="6" class="empty-hint">无匹配记录 · 调整筛选条件</td></tr> ` ;
}
function lineageHtml ( ) {
@@ -1212,6 +1236,16 @@ function paintSparks() {
}
const prevLat = new Map ( ) ;
function flashChangedLatencies ( ) {
document . querySelectorAll ( '[data-rate]' ) . forEach ( ( el ) => {
const v = el . textContent ;
const prev = prevLat . get ( el ) ;
if ( prev !== undefined && prev !== v ) {
el . classList . remove ( 'flash' ) ;
void el . offsetWidth ;
el . classList . add ( 'flash' ) ;
}
prevLat . set ( el , v ) ;
} ) ;
document . querySelectorAll ( '[data-lat]' ) . forEach ( ( el ) => {
const jid = el . closest ( '[data-jid]' ) ? el . closest ( '[data-jid]' ) . dataset . jid : ( el . closest ( 'tr' ) ? 'row:' + [ ... el . closest ( 'tr' ) . children ] . indexOf ( el ) : '' ) ;
const v = el . dataset . lat ;
@@ -1369,7 +1403,7 @@ async function startApp() {
renderPage ( pageFromHash ( ) ) ;
updateTape ( ) ; // 预装最近真实事件并开始滚动
updateClock ( ) ;
setInterval ( ( ) => { updateClock ( ) ; updateDynamics ( ) ; } , 200 ) ; // 打样 tick 节奏
setInterval ( ( ) => { if ( document . hidden ) return ; updateClock ( ) ; updateDynamics ( ) ; } , 200 ) ; // 打样 tick 节奏;页面隐藏时不做任何工作
await Poller . start ( ) ;
}