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"
|
return "other"
|
||||||
|
|
||||||
def _load_source_health(self, limit: int = 200) -> list[dict]:
|
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
|
import json
|
||||||
path = self._snap_dir / "source_health.json"
|
path = self._snap_dir / "source_health.json"
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
@@ -522,7 +527,21 @@ class AppDataScheduler:
|
|||||||
return []
|
return []
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
return []
|
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
|
@staticmethod
|
||||||
def _now() -> str:
|
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="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#0b1018" />
|
<meta name="theme-color" content="#0b1018" />
|
||||||
<title>SET50 Signal Lab</title>
|
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-BiR2h3Pv.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -698,42 +698,6 @@ onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRu
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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. -->
|
<!-- 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">
|
<section class="panel sim-panel" id="suggestion">
|
||||||
<div class="panel-header signal-header">
|
<div class="panel-header signal-header">
|
||||||
@@ -872,6 +836,45 @@ onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRu
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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) -->
|
<!-- 05 / บันทึกการวิเคราะห์ removed per user (redundant with theme-panel narratives) -->
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user