fix(ui,health): move source-health log to bottom of page; one row per source (latest)
- Health panel relocated below Backtest (last section) per owner — it's operational detail, not an investment view. - scheduler._load_source_health now returns only the LATEST entry per source key (was returning the full refresh history, so siamchart_vintages and price_snapshot appeared as 2 rows). Verified: 11 sources / 11 unique rows. - frontend v-for drops slice(0,20) and keys by source key. Verified in-browser: #health is the last section; 11 unique source rows.
This commit is contained in:
@@ -511,7 +511,12 @@ class AppDataScheduler:
|
||||
return "other"
|
||||
|
||||
def _load_source_health(self, limit: int = 200) -> list[dict]:
|
||||
"""Return the most recent source-health entries (for the API/UI)."""
|
||||
"""Return the most recent source-health entries (for the API/UI).
|
||||
|
||||
Only the LATEST entry per source key is returned (the owner's rule: the
|
||||
health panel should show one row per source with its last outcome, not a
|
||||
history of every refresh). Entries are ordered newest-first.
|
||||
"""
|
||||
import json
|
||||
path = self._snap_dir / "source_health.json"
|
||||
if not path.is_file():
|
||||
@@ -522,7 +527,21 @@ class AppDataScheduler:
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return data[:limit]
|
||||
# newest-first by "at" (the store writes append-only, newest last, so
|
||||
# iterate from the end); keep only the first occurrence of each key.
|
||||
seen: set = set()
|
||||
latest: list[dict] = []
|
||||
for entry in reversed(data):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
key = entry.get("key")
|
||||
if key is None or key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
latest.append(entry)
|
||||
if len(latest) >= limit:
|
||||
break
|
||||
return latest
|
||||
|
||||
@staticmethod
|
||||
def _now() -> str:
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0b1018" />
|
||||
<title>SET50 Signal Lab</title>
|
||||
<script type="module" crossorigin src="/assets/index-ePg0o_Uc.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C2MF9WUu.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BiR2h3Pv.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -698,42 +698,6 @@ onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRu
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel health-panel" id="health">
|
||||
<div class="panel-header signal-header">
|
||||
<div>
|
||||
<div class="section-kicker">สถานะการดึงข้อมูล</div>
|
||||
<h2>Log — สถานะแหล่งข้อมูล</h2>
|
||||
<p class="panel-subtitle">ผลการดึงข้อมูลครั้งล่าสุดของแต่ละแหล่ง โดยระบบวิเคราะห์สาเหตุให้อัตโนมัติ (เครือข่าย / หมดเวลา / หน้าเว็บเปลี่ยนโครงสร้าง / รูปแบบข้อมูล เป็นต้น) — กดปุ่ม "คัดลอก" เพื่อ copy สาเหตุไปแจ้ง/ตรวจสอบได้ทันที.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sourceHealth.length === 0" class="empty-research muted-cell">ยังไม่มี log — รอรอบ refresh ถัดไป (ปกติ ~ทุกวันสำหรับราคา, ~รายเดือน/ไตรมาสสำหรับปัจจัย).</div>
|
||||
<div v-else>
|
||||
<table class="source-table">
|
||||
<thead><tr><th>แหล่ง</th><th>ผลลัพธ์</th><th>สาเหตุ</th><th>เวลา</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(e, i) in sourceHealth.slice(0, 20)" :key="i">
|
||||
<td class="source-name">{{ e.label }}<div class="muted-cell" style="font-size:11px">{{ e.key }}</div></td>
|
||||
<td>
|
||||
<span v-if="e.ok" class="status-tag" style="background:#1a7f37;color:#fff">OK</span>
|
||||
<span v-else class="status-tag warning-tag">FAIL</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="e.ok">—</template>
|
||||
<template v-else>
|
||||
<div>{{ healthCategoryLabel(e.category) }}{{ appendHealthRationale(e) }}</div>
|
||||
<div v-if="e.detail" class="muted-cell" style="font-size:11px;word-break:break-word">{{ e.detail.slice(0, 160) }}</div>
|
||||
</template>
|
||||
</td>
|
||||
<td class="muted-cell">{{ e.at ? formatDate(e.at) : '—' }}</td>
|
||||
<td>
|
||||
<button v-if="!e.ok" class="primary-btn" style="padding:2px 8px" @click="copyHealthLine(e)">คัดลอกสาเหตุ</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sections 01-04 (tourism-only surprise, provenance, signal table, frozen research) removed per user: superseded by the 3-theme panel + sources table above. -->
|
||||
<section class="panel sim-panel" id="suggestion">
|
||||
<div class="panel-header signal-header">
|
||||
@@ -872,6 +836,45 @@ onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRu
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Health log moved to the very bottom of the page per owner (below
|
||||
Backtest): operational detail, not an investment view. The backend
|
||||
already returns ONE row per source (latest outcome only). -->
|
||||
<section class="panel health-panel" id="health">
|
||||
<div class="panel-header signal-header">
|
||||
<div>
|
||||
<div class="section-kicker">สถานะการดึงข้อมูล</div>
|
||||
<h2>Log — สถานะแหล่งข้อมูล</h2>
|
||||
<p class="panel-subtitle">ผลการดึงข้อมูลครั้งล่าสุดของแต่ละแหล่ง (1 แถวต่อแหล่ง) โดยระบบวิเคราะห์สาเหตุให้อัตโนมัติ (เครือข่าย / หมดเวลา / หน้าเว็บเปลี่ยนโครงสร้าง / รูปแบบข้อมูล เป็นต้น) — กดปุ่ม "คัดลอก" เพื่อ copy สาเหตุไปแจ้ง/ตรวจสอบได้ทันที.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sourceHealth.length === 0" class="empty-research muted-cell">ยังไม่มี log — รอรอบ refresh ถัดไป (ปกติ ~ทุกวันสำหรับราคา, ~รายเดือน/ไตรมาสสำหรับปัจจัย).</div>
|
||||
<div v-else>
|
||||
<table class="source-table">
|
||||
<thead><tr><th>แหล่ง</th><th>ผลลัพธ์</th><th>สาเหตุ</th><th>เวลา</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="e in sourceHealth" :key="e.key">
|
||||
<td class="source-name">{{ e.label }}<div class="muted-cell" style="font-size:11px">{{ e.key }}</div></td>
|
||||
<td>
|
||||
<span v-if="e.ok" class="status-tag" style="background:#1a7f37;color:#fff">OK</span>
|
||||
<span v-else class="status-tag warning-tag">FAIL</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="e.ok">—</template>
|
||||
<template v-else>
|
||||
<div>{{ healthCategoryLabel(e.category) }}{{ appendHealthRationale(e) }}</div>
|
||||
<div v-if="e.detail" class="muted-cell" style="font-size:11px;word-break:break-word">{{ e.detail.slice(0, 160) }}</div>
|
||||
</template>
|
||||
</td>
|
||||
<td class="muted-cell">{{ e.at ? formatDate(e.at) : '—' }}</td>
|
||||
<td>
|
||||
<button v-if="!e.ok" class="primary-btn" style="padding:2px 8px" @click="copyHealthLine(e)">คัดลอกสาเหตุ</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 05 / บันทึกการวิเคราะห์ removed per user (redundant with theme-panel narratives) -->
|
||||
</template>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user