#!/usr/bin/env python3
from __future__ import annotations

import json
from pathlib import Path


ROOT = Path(__file__).resolve().parent
OUTPUT = ROOT / "sales-project-operations-dashboard.html"


def build() -> None:
    payload = {
        "api_url": "https://zhct.yyangpt.cn/api/dataCenterDashboard",
    }

    template = r'''<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>智慧营养健康餐厅｜全国项目数据中心</title>
<link rel="stylesheet" href="assets/bootstrap-icons.min.css">
<style>
@font-face{font-family:bootstrap-icons;src:url("assets/bootstrap-icons.woff2") format("woff2");font-display:block}
:root{--bg:#020b23;--panel:#071b43;--panel2:#092352;--line:#0d77c9;--cyan:#20dcff;--blue:#2589ff;--mint:#39edc5;--yellow:#ffd64d;--violet:#9c7cff;--text:#eaf8ff;--muted:#7ea5c4}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#010716;color:var(--text);font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif}
body{display:flex;align-items:center;justify-content:center}.viewport{position:relative;width:100vw;height:100vh;overflow:hidden}.screen{position:absolute;width:1920px;height:1080px;transform-origin:0 0;background:#020b23;box-shadow:0 0 80px #001330 inset}
.top{height:78px;position:relative;display:grid;grid-template-columns:520px 1fr 520px;align-items:center;border-bottom:2px solid #0d6cc1;background:#06173a}
.top:after{content:"";position:absolute;left:520px;right:520px;bottom:-2px;height:4px;background:#18bfff;box-shadow:0 0 20px #18bfff}
.brand{height:54px;padding-left:28px;display:flex;align-items:center}.brand img{width:250px;height:42px;object-fit:contain;object-position:left center;filter:drop-shadow(0 0 9px rgba(255,94,30,.22))}.title{text-align:center;font-size:35px;font-weight:800;letter-spacing:8px;text-shadow:0 0 18px #159dff}.clock{text-align:right;padding-right:28px}.clock strong{font-size:25px;font-variant-numeric:tabular-nums}.clock span{margin-left:16px;color:#8eb3d1;font-size:13px}
.filters{position:absolute;left:28px;right:28px;top:88px;height:40px;display:flex;align-items:center;gap:10px;z-index:15}.filter-group{height:36px;display:flex;align-items:center;border:1px solid #125b91;background:#061937}.filter-group label{padding:0 10px;color:#5e9fc3;font-size:12px;border-right:1px solid #125b91;white-space:nowrap}.filter-control{height:34px;border:0;outline:0;padding:0 12px;background:#061937;color:#d8f5ff;font-size:13px;cursor:pointer;color-scheme:dark}.project-control{width:260px}.date-control{width:142px}.meal-control{width:118px}.date-separator{color:#4f89a9;font-size:12px}.filter-reset{height:36px;padding:0 16px;border:1px solid #125b91;background:#061937;color:#80b5d4;cursor:pointer}.filter-reset:hover{border-color:#22d9ff;color:#fff}.service-state{margin-left:auto;color:#65abc9;font-size:12px}.service-state i{margin-right:6px;color:#39edc5}.service-state.error,.service-state.error i{color:#ff8b70}
.main{height:1002px;padding:58px 18px 18px;display:grid;grid-template-columns:470px 1fr 470px;gap:16px}.column{display:grid;gap:14px;min-height:0}.left{grid-template-rows:278px 300px 1fr}.center{grid-template-rows:635px 1fr}.right{grid-template-rows:610px 1fr}
.panel{position:relative;min-height:0;border:0;background:rgba(6,24,59,.72);box-shadow:0 10px 30px rgba(0,20,65,.22)}.panel-head{height:46px;display:flex;align-items:center;padding:0 16px;border-bottom:1px solid rgba(36,157,223,.34);background:transparent}.panel-head h2{margin:0;font-size:17px;letter-spacing:1px}.panel-head h2:before{content:"";display:inline-block;width:4px;height:18px;margin-right:10px;vertical-align:-3px;background:var(--cyan);box-shadow:0 0 10px var(--cyan)}
.source-btn{position:relative;margin-left:auto;width:28px;height:28px;border:0;background:transparent;color:#66c9f1;font-size:17px;cursor:help}.source-btn:hover,.source-btn:focus{color:#fff}.source-tip{display:none;position:absolute;right:0;top:31px;width:330px;padding:12px 14px;border:1px solid #1687cf;background:#04122f;color:#a9cce2;font-size:12px;line-height:1.75;text-align:left;z-index:80;box-shadow:0 12px 30px rgba(0,0,0,.55)}.source-btn:hover .source-tip,.source-btn:focus .source-tip{display:block}.source-tip b{color:#47dbff}
.chart{width:100%;height:calc(100% - 46px)}
.hero{position:relative;overflow:hidden;background:rgba(3,14,43,.42);box-shadow:none}.hero-image{position:absolute;z-index:1;width:625px;height:625px;object-fit:contain;left:50%;top:30px;transform:translateX(-50%);filter:saturate(1.06) brightness(1.06);mix-blend-mode:screen}.hero-title{position:absolute;z-index:5;top:18px;left:0;right:0;text-align:center;font-size:20px;letter-spacing:5px;color:#aeeaff;text-shadow:0 0 12px #159dff}.hero-source{position:absolute;right:14px;top:12px;z-index:20}
.kpi{position:absolute;z-index:3;width:215px;height:108px;padding:14px 17px;border:0;border-left:3px solid currentColor;background:rgba(5,25,64,.7);box-shadow:0 12px 28px rgba(0,0,0,.16)}.kpi .label{font-size:14px;color:#a4c9dc}.kpi .value{margin-top:4px;font-size:38px;line-height:1.16;font-weight:850;color:currentColor;text-shadow:0 0 16px currentColor;font-variant-numeric:tabular-nums}.kpi .unit{font-size:14px;margin-left:5px}.kpi .sub{margin-top:3px;color:#6e98b7;font-size:11px}.k1{left:24px;top:80px;color:#24dfff}.k2{right:24px;top:80px;color:#3cedc0}.k3{left:24px;top:238px;color:#ffb350}.k4{right:24px;top:238px;color:#a98aff}.k5{left:24px;top:396px;color:#42a5ff}.k5 .value{font-size:32px;white-space:nowrap}.k6{right:24px;top:396px;color:#ff63cf}
.hero-status{position:absolute;left:50%;bottom:11px;transform:translateX(-50%);display:flex;gap:25px}.status-pill{padding:8px 4px;border:0;border-top:2px solid #15669d;background:transparent;color:#7fa9c5;font-size:12px}.status-pill b{margin-left:8px;color:#eaffff;font-size:20px}
.audit-grid{height:calc(100% - 46px);display:grid;grid-template-columns:repeat(4,1fr);gap:0;padding:16px 20px}.audit{display:flex;align-items:center;justify-content:center;gap:16px;padding:12px;border:0;border-right:1px solid rgba(31,119,173,.36);background:transparent}.audit:last-child{border-right:0}.audit i{font-size:35px;color:#2cdbff;text-shadow:0 0 12px rgba(44,219,255,.4)}.audit span{display:block;color:#7ea6c2;font-size:12px}.audit strong{display:block;margin-top:3px;font-size:31px;color:#eaffff;font-variant-numeric:tabular-nums}.audit small{font-size:11px;color:#6385a1}
.rank-list{padding:8px 16px}.rank-item{position:relative;display:grid;grid-template-columns:42px 1fr 120px;align-items:center;gap:8px;height:58px;margin-bottom:5px;padding:0 12px;background:transparent;border-bottom:1px solid rgba(28,105,157,.28);cursor:pointer;overflow:hidden}.rank-track{position:absolute;left:0;top:8px;bottom:8px;background:rgba(32,220,255,.1);border-left:3px solid #20dcff;z-index:0}.rank-item>*:not(.rank-track){position:relative;z-index:1}.rank-item:hover{background:rgba(11,50,104,.55)}.rank-no{font-weight:800;color:#4da9e8}.rank-item:nth-child(1) .rank-no{color:#ff5f6d}.rank-item:nth-child(2) .rank-no{color:#ffc34a}.rank-item:nth-child(3) .rank-no{color:#48a8ff}.rank-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px}.rank-value{text-align:right;color:#36ddff;font-size:15px;font-weight:800;font-variant-numeric:tabular-nums}.rank-value small{display:block;color:#6d92ac;font-size:10px;font-weight:400;margin-top:3px}
.nutrition-wrap{height:calc(100% - 46px);display:grid;grid-template-columns:205px 1fr;gap:12px;padding:13px}.quality-gauge{display:flex;flex-direction:column;align-items:center;justify-content:center;border-right:0}.quality-gauge i{font-size:48px;color:#35e5c3;text-shadow:0 0 14px rgba(53,229,195,.42)}.quality-gauge strong{font-size:38px;color:#35e5c3;margin-top:6px}.quality-gauge span{color:#78a4bf;font-size:12px}.quality-bars{display:flex;flex-direction:column;justify-content:center;gap:10px}.quality-row{display:grid;grid-template-columns:1fr 120px 45px;gap:8px;align-items:center;font-size:11px}.quality-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar{height:10px;background:#04122c}.bar b{display:block;height:100%;background:#22d8ff;box-shadow:0 0 9px rgba(34,216,255,.55)}.quality-pct{text-align:right;color:#7eeaff}
@media(max-aspect-ratio:4/3){.screen{transform-origin:0 0}}
</style>
</head>
<body>
<div class="viewport"><div class="screen" id="screen">
  <header class="top"><div class="brand"><img src="assets/cpt-logo-orange.png" alt="CPT 康比特"></div><div class="title">全国项目数据中心</div><div class="clock"><strong id="clock">--:--:--</strong><span id="today">----</span></div></header>
  <nav class="filters" aria-label="数据筛选">
    <div class="filter-group"><label for="projectFilter">项目</label><select id="projectFilter" class="filter-control project-control"><option value="">全部项目</option></select></div>
    <div class="filter-group"><label for="dateFrom">日期</label><input id="dateFrom" class="filter-control date-control" type="date"><span class="date-separator">至</span><input id="dateTo" class="filter-control date-control" type="date"></div>
    <div class="filter-group"><label for="mealFilter">餐段</label><select id="mealFilter" class="filter-control meal-control"><option value="0">全部餐段</option></select></div>
    <button id="filterReset" class="filter-reset" type="button"><i class="bi bi-arrow-counterclockwise"></i> 重置</button>
    <div id="serviceState" class="service-state"><i class="bi bi-database-check"></i><span>正在读取中央统计库</span></div>
  </nav>
  <main class="main">
    <section class="column left">
      <article class="panel"><div class="panel-head"><h2>项目运营状态</h2><button class="source-btn"><i class="bi bi-info-circle"></i><span class="source-tip"><b>数据来源</b><br>data_center.dc_project<br>口径：PHP 每日任务维护的项目接入与运营状态<br>接口不会连接项目业务库<br>更新：<span class="source-snapshot">读取中</span></span></button></div><div id="statusChart" class="chart"></div></article>
      <article class="panel"><div class="panel-head"><h2>近 12 个月交易趋势</h2><button class="source-btn"><i class="bi bi-info-circle"></i><span class="source-tip"><b>数据来源</b><br>data_center.dc_daily_meal_stats<br>口径：筛选范围内的人民币订单金额；不等同合同额、回款或公司营收<br>接口不会连接项目业务库<br>更新：<span class="source-snapshot">读取中</span></span></button></div><div id="trendChart" class="chart"></div></article>
      <article class="panel"><div class="panel-head"><h2>早午晚夜使用结构</h2><button class="source-btn"><i class="bi bi-info-circle"></i><span class="source-tip"><b>数据来源</b><br>data_center.dc_daily_meal_stats<br>口径：PHP 汇总时将餐段编码 1/2/3/4 映射为早/午/晚/夜宵<br>接口不会连接项目业务库<br>更新：<span class="source-snapshot">读取中</span></span></button></div><div id="mealChart" class="chart"></div></article>
    </section>
    <section class="column center">
      <article class="panel hero">
        <div class="hero-title">智慧营养健康餐厅 · 项目运营态势</div>
        <button class="source-btn hero-source"><i class="bi bi-info-circle"></i><span class="source-tip"><b>数据来源</b><br>data_center.dc_project_snapshot、dc_daily_meal_stats<br>数据由 PHP 每日任务从项目库汇总写入<br>本页面与查询接口只访问 data_center<br>更新：<span class="source-snapshot">读取中</span></span></button>
        <img class="hero-image" src="assets/smart-cafeteria-hologram.png" alt="智慧餐厅全息数据中心">
        <div class="kpi k1"><div class="label">项目总数</div><div class="value" id="kProjects">—</div><div class="sub">生产配置项目</div></div>
        <div class="kpi k2"><div class="label">数据已接入</div><div class="value" id="kConnected">—</div><div class="sub" id="kConnectRate">接入率读取中</div></div>
        <div class="kpi k3"><div class="label">用户记录总量</div><div class="value" id="kUsers">—</div><div class="sub">项目内去重，跨项目可能重复</div></div>
        <div class="kpi k4"><div class="label">订单数量</div><div class="value" id="kOrders">—</div><div class="sub">随项目、日期、餐段筛选</div></div>
        <div class="kpi k5"><div class="label">人民币交易金额</div><div class="value" id="kAmount">—</div><div class="sub">订单金额，非合同额或营收</div></div>
        <div class="kpi k6"><div class="label">已有订单项目</div><div class="value" id="kActive">—</div><div class="sub">筛选范围订单数大于 0</div></div>
        <div class="hero-status"><div class="status-pill">已有订单<b id="sActive">—</b></div><div class="status-pill">已接入无订单<b id="sIdle">—</b></div><div class="status-pill">暂未接入<b id="sOffline">—</b></div></div>
      </article>
      <article class="panel"><div class="panel-head"><h2>数据资产与质量</h2><button class="source-btn"><i class="bi bi-info-circle"></i><span class="source-tip"><b>数据来源</b><br>data_center.dc_daily_meal_stats、dc_sync_run<br>订单、金额和菜品明细按项目、日期、餐段幂等汇总<br>接口不会连接项目业务库<br>更新：<span class="source-snapshot">读取中</span></span></button></div><div class="audit-grid">
        <div class="audit"><i class="bi bi-database"></i><div><span>订单菜品明细</span><strong id="auditDetail">—</strong><small>可关联订单的明细行</small></div></div>
        <div class="audit"><i class="bi bi-calendar3"></i><div><span>每日餐段记录</span><strong id="auditDaily">—</strong><small>项目 × 日期 × 餐段</small></div></div>
        <div class="audit"><i class="bi bi-check2-circle"></i><div><span>最近同步成功</span><strong id="auditRecon">—</strong><small>成功项目 / 配置项目</small></div></div>
        <div class="audit"><i class="bi bi-diagram-3"></i><div><span>项目目录唯一</span><strong id="auditUnique">—</strong><small>无重复配置键</small></div></div>
      </div></article>
    </section>
    <section class="column right">
      <article class="panel"><div class="panel-head"><h2>项目交易成果 TOP 8</h2><button class="source-btn"><i class="bi bi-info-circle"></i><span class="source-tip"><b>数据来源</b><br>data_center.dc_daily_meal_stats、dc_project<br>口径：筛选范围内人民币订单金额降序；不等同合同额、回款或公司营收<br>接口不会连接项目业务库<br>更新：<span class="source-snapshot">读取中</span></span></button></div><div id="rankList" class="rank-list"></div></article>
      <article class="panel"><div class="panel-head"><h2>营养数据可用性</h2><button class="source-btn"><i class="bi bi-info-circle"></i><span class="source-tip"><b>数据来源</b><br>data_center.dc_daily_meal_stats<br>口径：(菜品明细行-未匹配行)/菜品明细行，按当前筛选范围汇总<br>接口不会连接项目业务库<br>更新：<span class="source-snapshot">读取中</span></span></button></div><div class="nutrition-wrap"><div class="quality-gauge"><i class="bi bi-heart-pulse"></i><strong id="qualityAvg">—</strong><span>有明细项目平均匹配率</span></div><div id="qualityBars" class="quality-bars"></div></div></article>
    </section>
  </main>
</div></div>
<script src="assets/echarts.min.js"></script>
<script>
const CONFIG=__DATA__;
const API_URL=new URLSearchParams(location.search).get('api')||CONFIG.api_url;
const C={cyan:'#20dcff',blue:'#2589ff',mint:'#39edc5',yellow:'#ffd64d',violet:'#9c7cff',pink:'#ff63cf',grid:'rgba(72,151,205,.18)',text:'#9bc0d7'};
const charts={status:echarts.init(document.getElementById('statusChart')),trend:echarts.init(document.getElementById('trendChart')),meal:echarts.init(document.getElementById('mealChart'))};
let optionsLoaded=false,defaults=null,loading=false;
function fit(){const s=document.getElementById('screen'),scale=Math.min(innerWidth/1920,innerHeight/1080);s.style.transform=`scale(${scale})`;s.style.left=`${(innerWidth-1920*scale)/2}px`;s.style.top=`${(innerHeight-1080*scale)/2}px`;Object.values(charts).forEach(c=>c.resize())}
function fmt(v){if(v===null||v===undefined)return '—';if(v>=100000000)return (v/100000000).toFixed(2)+'亿';if(v>=10000)return (v/10000).toFixed(v>=1000000?0:2)+'万';return Math.round(v).toLocaleString('zh-CN')}
function setKpi(id,value,unit=''){document.getElementById(id).innerHTML=`${value}<span class="unit">${unit}</span>`}
function setState(text,error=false){const el=document.getElementById('serviceState');el.classList.toggle('error',error);el.querySelector('i').className=error?'bi bi-exclamation-circle':'bi bi-database-check';el.querySelector('span').textContent=text}
function filterParams(){const p=new URLSearchParams(),project=document.getElementById('projectFilter').value,from=document.getElementById('dateFrom').value,to=document.getElementById('dateTo').value,meal=document.getElementById('mealFilter').value;if(project)p.set('project',project);if(from)p.set('date_from',from);if(to)p.set('date_to',to);if(meal&&meal!=='0')p.set('meal_segment',meal);return p}
function loadOptions(data){
 const p=document.getElementById('projectFilter'),m=document.getElementById('mealFilter');
 p.innerHTML='<option value="">全部项目</option>'+data.filter_options.projects.map(x=>`<option value="${x.value}">${x.label}</option>`).join('');
 m.innerHTML=data.filter_options.meal_segments.map(x=>`<option value="${x.value}">${x.label}</option>`).join('');
 document.getElementById('dateFrom').min=data.filter_options.date_min;document.getElementById('dateFrom').max=data.filter_options.date_max;document.getElementById('dateTo').min=data.filter_options.date_min;document.getElementById('dateTo').max=data.filter_options.date_max;
 defaults={project:'',date_from:data.filters.date_from,date_to:data.filters.date_to,meal_segment:'0'};optionsLoaded=true;
}
function applyFilterValues(data){document.getElementById('projectFilter').value=data.filters.project||'';document.getElementById('dateFrom').value=data.filters.date_from;document.getElementById('dateTo').value=data.filters.date_to;document.getElementById('mealFilter').value=String(data.filters.meal_segment||0)}
async function fetchDashboard(){
 if(loading)return;loading=true;setState('正在读取中央统计库');
 try{const params=filterParams(),response=await fetch(API_URL+(params.toString()?'?'+params.toString():''),{headers:{Accept:'application/json'},cache:'no-store'}),result=await response.json();if(!response.ok||result.code!==0)throw new Error(result.message||'查询失败');const data=result.data;if(!optionsLoaded)loadOptions(data);applyFilterValues(data);render(data);setState(`中央统计库已更新 · ${data.snapshot_at||'暂无时间'}`);}
 catch(error){setState('统计服务暂不可用 · '+error.message,true);}
 finally{loading=false}
}
function render(data){
 const s=data.summary,statusMap=Object.fromEntries(data.status_mix.map(x=>[x.name,x.value]));
 setKpi('kProjects',s.project_total,'个');setKpi('kConnected',s.connected_projects,'个');document.getElementById('kConnectRate').textContent=`接入率 ${s.project_total?(s.connected_projects/s.project_total*100).toFixed(1):0}%`;
 setKpi('kUsers',fmt(s.user_count));setKpi('kOrders',fmt(s.order_count));setKpi('kAmount',(s.order_amount/10000).toFixed(2),'万元');setKpi('kActive',s.active_projects,'个');
 document.getElementById('sActive').textContent=statusMap['已有订单']||0;document.getElementById('sIdle').textContent=statusMap['已接入无订单']||0;document.getElementById('sOffline').textContent=statusMap['暂未接入']||0;
 const colors={'已有订单':C.cyan,'已接入无订单':C.yellow,'暂未接入':'#335f8f'},statusData=data.status_mix.map(x=>({...x,itemStyle:{color:colors[x.name]||C.blue}}));
 charts.status.setOption({backgroundColor:'transparent',tooltip:{trigger:'item',formatter:'{b}<br>{c} 个（{d}%）'},title:{text:String(s.project_total),subtext:'项目',left:'31.5%',top:'36%',textAlign:'center',textStyle:{color:'#effcff',fontSize:29,fontWeight:800},subtextStyle:{color:C.text,fontSize:12}},legend:{right:18,top:'center',orient:'vertical',textStyle:{color:C.text,fontSize:12},itemGap:18},series:[{type:'pie',radius:['53%','76%'],center:['32%','53%'],label:{show:false},data:statusData}]},true);
 const trend=data.monthly_trend.slice(-12),months=trend.map(x=>x.month.slice(5)+'月'),mvals=trend.map(x=>+(x.order_amount/10000).toFixed(2));
 charts.trend.setOption({tooltip:{trigger:'axis',valueFormatter:v=>v+' 万元'},grid:{left:62,right:20,top:30,bottom:42},xAxis:{type:'category',data:months,axisLine:{lineStyle:{color:'#1b5a88'}},axisLabel:{color:C.text}},yAxis:{type:'value',name:'万元',nameTextStyle:{color:C.text},splitLine:{lineStyle:{color:C.grid}},axisLabel:{color:C.text}},series:[{type:'line',smooth:true,symbol:'circle',symbolSize:6,data:mvals,lineStyle:{width:3,color:C.mint},itemStyle:{color:C.cyan},areaStyle:{color:'rgba(32,220,255,.16)'}}]},true);
 const meals=data.meal_segments;charts.meal.setOption({tooltip:{trigger:'axis',axisPointer:{type:'shadow'},valueFormatter:v=>v.toLocaleString()+' 单'},grid:{left:70,right:35,top:26,bottom:35},xAxis:{type:'value',splitLine:{lineStyle:{color:C.grid}},axisLabel:{color:C.text}},yAxis:{type:'category',data:meals.map(x=>x.label),axisLine:{show:false},axisTick:{show:false},axisLabel:{color:C.text,fontSize:13}},series:[{type:'bar',data:meals.map(x=>x.order_count),barWidth:17,itemStyle:{color:C.cyan,borderRadius:[0,8,8,0]},label:{show:true,position:'right',color:'#bcefff',formatter:p=>fmt(p.value)}}]},true);
 const ranked=data.project_ranking,rankMax=ranked[0]?.order_amount||1;document.getElementById('rankList').innerHTML=ranked.length?ranked.map((p,i)=>`<div class="rank-item" title="${p.project_name}｜${fmt(p.order_count)} 单"><div class="rank-track" style="width:${Math.max(2,p.order_amount/rankMax*100)}%"></div><div class="rank-no">NO.${i+1}</div><div class="rank-name">${p.project_name}</div><div class="rank-value">${p.order_amount>=10000?(p.order_amount/10000).toFixed(2)+' 万':p.order_amount.toFixed(2)+' 元'}<small>${fmt(p.order_count)} 单</small></div></div>`).join(''):'<div style="padding:60px;text-align:center;color:#668fa9">当前筛选范围暂无交易数据</div>';
 const q=data.nutrition_coverage.slice(0,6),avg=q.length?q.reduce((a,p)=>a+p.coverage,0)/q.length:0;document.getElementById('qualityAvg').textContent=q.length?(avg*100).toFixed(1)+'%':'—';document.getElementById('qualityBars').innerHTML=q.length?q.map(p=>`<div class="quality-row"><div class="quality-name" title="${p.project_name}">${p.project_name}</div><div class="bar"><b style="width:${Math.max(1,p.coverage*100)}%"></b></div><div class="quality-pct">${(p.coverage*100).toFixed(1)}%</div></div>`).join(''):'<div style="color:#668fa9">当前筛选范围暂无营养明细</div>';
 const sync=data.quality.latest_sync;document.getElementById('auditDetail').textContent=s.dish_detail_rows>=10000?(s.dish_detail_rows/10000).toFixed(2)+'万':s.dish_detail_rows.toLocaleString('zh-CN');document.getElementById('auditDaily').textContent=data.quality.daily_rows.toLocaleString('zh-CN');document.getElementById('auditRecon').textContent=sync?sync.project_success+' / '+sync.project_total:'—';document.getElementById('auditUnique').textContent=s.project_total+' / '+s.project_total;document.querySelectorAll('.source-snapshot').forEach(el=>el.textContent=data.snapshot_at||'暂无时间');
}
['projectFilter','dateFrom','dateTo','mealFilter'].forEach(id=>document.getElementById(id).addEventListener('change',fetchDashboard));document.getElementById('filterReset').addEventListener('click',()=>{if(!defaults)return;document.getElementById('projectFilter').value=defaults.project;document.getElementById('dateFrom').value=defaults.date_from;document.getElementById('dateTo').value=defaults.date_to;document.getElementById('mealFilter').value=defaults.meal_segment;fetchDashboard()});
function tick(){const d=new Date();document.getElementById('clock').textContent=d.toLocaleTimeString('zh-CN',{hour12:false});document.getElementById('today').textContent=d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit',weekday:'short'})}setInterval(tick,1000);tick();
addEventListener('resize',fit);fit();fetchDashboard();
</script>
</body></html>'''
    OUTPUT.write_text(template.replace("__DATA__", json.dumps(payload, ensure_ascii=False, separators=(",", ":"))), encoding="utf-8")
    print(OUTPUT)


if __name__ == "__main__":
    build()
