<!doctype html>

<html lang="zh-CN">

<head>

  <meta charset="utf-8" />

  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <title>LifeSync</title>

  <style>

    :root{

      --bg:#0b0f14;

      --panel:#0f1621;

      --panel2:#0c121b;

      --text:#e9f0ff;

      --muted:#8ea0bd;

      --border:rgba(255,255,255,.08);

      --good:#38d39f;

      --warn:#ffb020;

      --bad:#ff5c5c;

      --shadow: 0 10px 30px rgba(0,0,0,.35);

      --radius:18px;

    }

    *{box-sizing:border-box}

    body{

      margin:0;

      font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "PingFang SC","Hiragino Sans GB","Microsoft YaHei", sans-serif;

      background:

        radial-gradient(900px 600px at 20% -10%, rgba(80,140,255,.18), transparent 55%),

        radial-gradient(800px 500px at 95% 10%, rgba(56,211,159,.14), transparent 55%),

        radial-gradient(900px 700px at 30% 120%, rgba(255,176,32,.10), transparent 55%),

        var(--bg);

      color:var(--text);

    }

    .wrap{

      max-width: 1100px;

      margin: 0 auto;

      padding: 22px 18px 40px;

    }

    .topbar{

      display:flex; align-items:center; justify-content:space-between;

      margin-bottom:16px;

    }

    .brand{

      display:flex; gap:12px; align-items:center;

    }

    .logo{

      width:40px; height:40px; border-radius:14px;

      background: linear-gradient(135deg, rgba(80,140,255,.9), rgba(56,211,159,.85));

      box-shadow: var(--shadow);

    }

    .title h1{font-size:18px; margin:0}

    .title p{margin:2px 0 0; color:var(--muted); font-size:12px}

    .actions{display:flex; gap:10px; align-items:center}

    .pill{

      background: rgba(255,255,255,.06);

      border:1px solid var(--border);

      color:var(--muted);

      padding:8px 10px;

      border-radius:999px;

      font-size:12px;

      display:flex; gap:8px; align-items:center;

    }

    .btn{

      background: rgba(255,255,255,.06);

      border:1px solid var(--border);

      color:var(--text);

      padding:9px 12px;

      border-radius:999px;

      font-size:12px;

      cursor:pointer;

    }

    .btn:hover{background: rgba(255,255,255,.09)}

    .grid{

      display:grid;

      grid-template-columns: 1.2fr 1fr 1fr;

      gap:14px;

      margin-top:14px;

    }

    @media (max-width: 980px){

      .grid{grid-template-columns:1fr; }

    }

    .card{

      background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));

      border:1px solid var(--border);

      border-radius: var(--radius);

      box-shadow: var(--shadow);

      padding:16px;

      position:relative;

      overflow:hidden;

    }

    .card:before{

      content:"";

      position:absolute; inset:-2px;

      background: radial-gradient(600px 180px at 30% 0%, rgba(80,140,255,.18), transparent 60%);

      pointer-events:none;

    }

    .card h2{

      position:relative;

      margin:0 0 10px;

      font-size:14px;

      letter-spacing:.2px;

      color: #dce6ff;

      display:flex; align-items:center; justify-content:space-between;

    }

    .kpi{

      position:relative;

      display:flex;

      gap:14px;

      align-items:flex-end;

      margin-top:6px;

    }

    .kpi .big{

      font-size:34px;

      font-weight:700;

      line-height:1;

      letter-spacing: .2px;

    }

    .kpi .unit{

      color:var(--muted);

      font-size:12px;

      padding-bottom:4px;

    }

    .subgrid{

      position:relative;

      display:grid;

      grid-template-columns: repeat(3, 1fr);

      gap:10px;

      margin-top:14px;

    }

    .mini{

      background: rgba(0,0,0,.18);

      border:1px solid var(--border);

      border-radius: 14px;

      padding:10px;

    }

    .mini .label{color:var(--muted); font-size:11px}

    .mini .val{margin-top:6px; font-size:16px; font-weight:650}

    .mini .val small{color:var(--muted); font-weight:500}

    .spark{

      position:relative;

      margin-top:14px;

      background: rgba(0,0,0,.16);

      border:1px solid var(--border);

      border-radius: 14px;

      padding:10px;

    }

    .spark .row{

      display:flex; justify-content:space-between; align-items:center;

      margin-bottom:8px;

    }

    .spark .row .label{color:var(--muted); font-size:11px}

    .spark svg{width:100%; height:48px; display:block}

    .muted{color:var(--muted)}

    .warnbar{

      margin-top:14px;

      padding:10px 12px;

      border-radius: 14px;

      border:1px solid rgba(255,176,32,.28);

      background: rgba(255,176,32,.08);

      color:#ffd79a;

      font-size:12px;

      position:relative;

    }

    .errbar{

      margin-top:14px;

      padding:10px 12px;

      border-radius: 14px;

      border:1px solid rgba(255,92,92,.28);

      background: rgba(255,92,92,.08);

      color:#ffb3b3;

      font-size:12px;

      position:relative;

      white-space:pre-wrap;

    }

    .footer{

      margin-top:16px;

      color:var(--muted);

      font-size:12px;

      display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap;

    }

    code{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;}

  </style>

</head>

<body>

  <div class="wrap">

    <div class="topbar">

      <div class="brand">

        <div class="logo"></div>

        <div class="title">

          <h1>LifeSync</h1>

          <p id="subtitle">Home · Loading…</p>

        </div>

      </div>

      <div class="actions">

        <div class="pill" id="rangePill">—</div>

        <button class="btn" id="refreshBtn">刷新</button>

      </div>

    </div>


    <div class="grid">

      <!-- Health -->

      <div class="card">

        <h2>健康概览 <span class="muted">7</span></h2>

        <div class="kpi">

          <div class="big" id="kpiSteps">—</div>

          <div class="unit">平均步数/</div>

        </div>


        <div class="subgrid">

          <div class="mini">

            <div class="label">睡眠</div>

            <div class="val" id="kpiSleep">— <small>小时</small></div>

          </div>

          <div class="mini">

            <div class="label">饮水</div>

            <div class="val" id="kpiWater">— <small>ml</small></div>

          </div>

          <div class="mini">

            <div class="label">HRV(mean)</div>

            <div class="val" id="kpiHrv">— <small>ms</small></div>

          </div>

        </div>


        <div class="subgrid">

          <div class="mini">

            <div class="label">活动消耗</div>

            <div class="val" id="kpiKcal">— <small>kcal</small></div>

          </div>

          <div class="mini">

            <div class="label">有效天数</div>

            <div class="val" id="kpiDaysWith">— <small>/7</small></div>

          </div>

          <div class="mini">

            <div class="label">数据源</div>

            <div class="val"><small id="healthSource">Notion</small></div>

          </div>

        </div>


        <div class="spark">

          <div class="row"><div class="label">步数趋势</div><div class="muted" id="stepsTrend">—</div></div>

          <svg id="sparkSteps" viewBox="0 0 100 40" preserveAspectRatio="none"></svg>

        </div>


        <div class="spark">

          <div class="row"><div class="label">饮水趋势</div><div class="muted" id="waterTrend">—</div></div>

          <svg id="sparkWater" viewBox="0 0 100 40" preserveAspectRatio="none"></svg>

        </div>


        <div class="warnbar" id="healthHint" style="display:none;"></div>

      </div>


      <!-- Finance -->

      <div class="card">

        <h2>财务概览 <span class="muted">7</span></h2>

        <div class="kpi">

          <div class="big" id="kpiSpend">—</div>

          <div class="unit">支出合计</div>

        </div>


        <div class="spark">

          <div class="row"><div class="label">支出趋势</div><div class="muted" id="spendTrend">—</div></div>

          <svg id="sparkSpend" viewBox="0 0 100 40" preserveAspectRatio="none"></svg>

        </div>


        <div class="warnbar" id="financeHint" style="display:none;"></div>

      </div>


      <!-- Tasks -->

      <div class="card">

        <h2>任务概览 <span class="muted">全量</span></h2>

        <div class="kpi">

          <div class="big" id="kpiTasksPending">—</div>

          <div class="unit">待办</div>

        </div>


        <div class="subgrid">

          <div class="mini">

            <div class="label">总任务</div>

            <div class="val" id="kpiTasksTotal">—</div>

          </div>

          <div class="mini">

            <div class="label">已完成</div>

            <div class="val" id="kpiTasksDone">—</div>

          </div>

          <div class="mini">

            <div class="label">完成率</div>

            <div class="val" id="kpiTasksRate">— <small>%</small></div>

          </div>

        </div>


        <div class="warnbar" id="tasksHint" style="display:none;"></div>

      </div>

    </div>


    <div class="errbar" id="errorBox" style="display:none;"></div>


    <div class="footer">

      <div>API: <code id="apiBase">—</code></div>

      <div>Updated: <span id="updatedAt">—</span></div>

    </div>

  </div>


<script>

  // === CONFIG ===

  // If your API is on another domain, set it here:

  // e.g. const API_BASE = "https://gpt-dashboard.city0920.workers.dev";

  const API_BASE = "https://gpt-dashboard.city0920.workers.dev";


  document.getElementById("apiBase").textContent = API_BASE;


  const $ = (id) => document.getElementById(id);


  function fmt(n, digits=0){

    if(n === null || n === undefined || Number.isNaN(n)) return "—";

    const x = Number(n);

    if(!Number.isFinite(x)) return "—";

    return x.toLocaleString(undefined, { maximumFractionDigits: digits, minimumFractionDigits: digits });

  }


  function pct(a,b){

    if(!b || b<=0) return null;

    return Math.round((a/b)*1000)/10;

  }


  function buildSpark(svgEl, values){

    // values: array numbers or null

    const xs = values.map(v => (Number.isFinite(v) ? v : null));

    const valid = xs.filter(v => Number.isFinite(v));

    svgEl.innerHTML = "";

    if(valid.length < 2){

      // draw baseline

      svgEl.innerHTML = `<path d="M0,30 L100,30" fill="none" stroke="rgba(255,255,255,.18)" stroke-width="2" />`;

      return;

    }

    let min = Math.min(...valid);

    let max = Math.max(...valid);

    if(max === min){ max = min + 1; }


    const pts = xs.map((v, i) => {

      const x = (i/(xs.length-1))*100;

      if(!Number.isFinite(v)) return null;

      const y = 36 - ((v - min)/(max - min))*30; // 6..36

      return {x,y};

    }).filter(Boolean);


    if(pts.length < 2){

      svgEl.innerHTML = `<path d="M0,30 L100,30" fill="none" stroke="rgba(255,255,255,.18)" stroke-width="2" />`;

      return;

    }


    const d = pts.map((p,i)=> (i===0?`M${p.x},${p.y}`:`L${p.x},${p.y}`)).join(" ");

    const area = `M${pts[0].x},40 ` + pts.map(p=>`L${p.x},${p.y}`).join(" ") + ` L${pts[pts.length-1].x},40 Z`;


    svgEl.innerHTML = `

      <path d="${area}" fill="rgba(80,140,255,.12)"></path>

      <path d="${d}" fill="none" stroke="rgba(80,140,255,.85)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"></path>

    `;

  }


  function showError(err){

    $("errorBox").style.display = "block";

    $("errorBox").textContent = err;

  }


  function clearError(){

    $("errorBox").style.display = "none";

    $("errorBox").textContent = "";

  }


  async function load(){

    clearError();

    $("subtitle").textContent = "Home · Loading…";


    // NOTE: 固化 days=7

    const url = `${API_BASE}/api/snapshot?days=7&nocache=1`;


    try{

      const res = await fetch(url, { method:"GET" });

      const json = await res.json();

      if(!json.ok) throw new Error(json.error || "Snapshot failed");


      const d = json.data;

      const range = d.range;

      $("rangePill").textContent = `${range.start} ${range.end} · ${range.days}d`;

      $("subtitle").textContent = "Home · Ready";

      $("updatedAt").textContent = new Date(d.generatedAt).toLocaleString();


      // Health

      const hs = d.health?.summary || {};

      $("kpiSteps").textContent = fmt(hs.steps?.avg, 0);

      $("kpiSleep").innerHTML = `${fmt(hs.sleepHours?.avg, 1)} <small>小时</small>`;

      $("kpiWater").innerHTML = `${fmt(hs.waterMl?.avg, 0)} <small>ml</small>`;

      $("kpiHrv").innerHTML = `${fmt(hs.hrv?.mean?.avg, 1)} <small>ms</small>`;

      $("kpiKcal").innerHTML = `${fmt(hs.kcal?.avg, 0)} <small>kcal</small>`;

      $("kpiDaysWith").innerHTML = `${fmt(hs.daysWithData ?? 0, 0)} <small>/7</small>`;

      $("healthSource").textContent = d.health?.source?.mode === "data_source" ? "Notion DS" : "Notion DB";


      const hd = (d.health?.daily || []);

      const stepsSeries = hd.map(x => x.steps ?? null);

      const waterSeries = hd.map(x => x.waterMl ?? null);


      buildSpark($("sparkSteps"), stepsSeries);

      buildSpark($("sparkWater"), waterSeries);


      $("stepsTrend").textContent = stepsSeries.filter(v=>Number.isFinite(v)).length ? `min ${fmt(Math.min(...stepsSeries.filter(v=>Number.isFinite(v))),0)} · max ${fmt(Math.max(...stepsSeries.filter(v=>Number.isFinite(v))),0)}` : "—";

      $("waterTrend").textContent = waterSeries.filter(v=>Number.isFinite(v)).length ? `min ${fmt(Math.min(...waterSeries.filter(v=>Number.isFinite(v))),0)} · max ${fmt(Math.max(...waterSeries.filter(v=>Number.isFinite(v))),0)}` : "—";


      // show hints

      const hints = [];

      if((hs.sleepHours?.avg ?? 0) && hs.sleepHours.avg < 6) hints.push("睡眠均值低于 6 小时,建议优先补足恢复。");

      if((hs.waterMl?.avg ?? 0) && hs.waterMl.avg < 1500) hints.push("饮水均值偏低(<1500ml),可尝试分段小口补水。");

      $("healthHint").style.display = hints.length ? "block" : "none";

      $("healthHint").textContent = hints.join(" ");


      // Finance

      const fs = d.finance?.summary || {};

      $("kpiSpend").textContent = `¥ ${fmt(fs.spending ?? 0, 2)}`;


      const fd = (d.finance?.daily || []);

      const spendSeries = fd.map(x => x.spending ?? null);

      buildSpark($("sparkSpend"), spendSeries);

      $("spendTrend").textContent = spendSeries.filter(v=>Number.isFinite(v)).length ? `max ¥${fmt(Math.max(...spendSeries.filter(v=>Number.isFinite(v))),2)}` : "—";


      if((fs.spending ?? 0) === 0){

        $("financeHint").style.display = "block";

        $("financeHint").textContent = "7天支出为 0:如果你近期未导入账单或今日库中无记录,这是正常现象。";

      } else {

        $("financeHint").style.display = "none";

      }


      // Tasks

      const ts = d.tasks || {};

      $("kpiTasksPending").textContent = fmt(ts.pending ?? 0, 0);

      $("kpiTasksTotal").textContent = fmt(ts.total ?? 0, 0);

      $("kpiTasksDone").textContent = fmt(ts.completed ?? 0, 0);

      const rate = pct(ts.completed ?? 0, ts.total ?? 0);

      $("kpiTasksRate").innerHTML = `${fmt(rate ?? 0, 1)} <small>%</small>`;


      $("tasksHint").style.display = "block";

      $("tasksHint").textContent = `数据源:${ts.source?.dataSourceId ? ("DS " + ts.source.dataSourceId.slice(0,4) + "..." + ts.source.dataSourceId.slice(-4)) : "—"};完成字段:${ts.schema?.doneCheckboxProp || "—"}`;


    }catch(e){

      showError(String(e && e.message ? e.message : e));

      $("subtitle").textContent = "Home · Error";

    }

  }


  $("refreshBtn").addEventListener("click", load);

  load();

</script>

</body>

</html>