feat(scheduler): per-source cadence + source-health log with failure diagnosis + UI copy

This commit is contained in:
Kunthawat Greethong
2026-08-28 11:41:48 +07:00
parent 105ab86bf6
commit 14b2aeff7c
7 changed files with 363 additions and 32 deletions

File diff suppressed because one or more lines are too long

18
frontend/dist/assets/index-DOVbTpbx.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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-CUQk4rwt.js"></script>
<script type="module" crossorigin src="/assets/index-DOVbTpbx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-z73iDem3.css">
</head>
<body>

View File

@@ -53,6 +53,9 @@ const factorRows = computed(() => factorData.value?.factors ?? [])
// real multi-theme dashboard (3 themes + macro + sources)
const dashboardThemes = computed(() => dashData.value?.themes ?? [])
const dashboardSources = computed(() => dashData.value?.sources ?? [])
// data scheduler source-health log (refresh cadence + failure diagnosis)
const sourceHealth = ref([])
const sourceHealthLoaded = ref(false)
const dashboardMacro = computed(() => dashData.value?.macro ?? {})
const sourceCount = computed(() => dashboardSources.value.length)
const factorCount = computed(() => dashData.value?.source_summary?.factor_keys ?? sourceCount.value)
@@ -403,6 +406,34 @@ async function loadBacktestReadiness() {
} catch { btReadiness.value = null }
}
async function loadSourceHealth() {
try {
const body = await fetchJson('/api/v1/scheduler/sources')
sourceHealth.value = body.sources || []
} catch { sourceHealth.value = [] }
sourceHealthLoaded.value = true
}
const healthCategoryLabel = (cat) => ({
ok: 'ปกติ', network: 'เครือข่ายขัดข้อง', timeout: 'หมดเวลา',
http: 'HTTP error', parse: 'รูปแบบข้อมูลผิด', structure: 'หน้าเว็บเปลี่ยนโครงสร้าง',
auth: 'สิทธิ์/ยืนยันตัวตน', other: 'อื่น ๆ',
})[cat] || cat
async function copyHealthLine(entry) {
const line = `[${entry.at}] ${entry.label} (${entry.key}) — ${entry.ok ? 'OK' : 'FAIL: ' + healthCategoryLabel(entry.category)} ${entry.detail ? '| ' + entry.detail : ''}`
try {
await navigator.clipboard.writeText(line)
notice.value = `คัดลอกสาเหตุของ ${entry.key} แล้ว`
} catch {
notice.value = line // fallback: show raw text as a notice
}
}
function appendHealthRationale(entry) {
return entry.ok ? '' : ` (สาเหตุน่าจะ: ${healthCategoryLabel(entry.category)})`
}
async function runBacktest() {
btLoading.value = true
btResult.value = null
@@ -489,7 +520,7 @@ async function recordPaperEntry() {
}
}
onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRuns(), loadBacktestReadiness()]) })
onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRuns(), loadBacktestReadiness(), loadSourceHealth()]) })
</script>
<template>
@@ -510,6 +541,7 @@ onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRu
<a class="nav-item" href="#signals"><span class="nav-glyph"></span>ญญาณ</a>
<a class="nav-item" href="#factors"><span class="nav-glyph"></span>จจ</a>
<a class="nav-item" href="#lineage"><span class="nav-glyph"></span>มาขอม</a>
<a class="nav-item" href="#health"><span class="nav-glyph"></span>สถานะขอม</a>
<a class="nav-item" href="#research-run"><span class="nav-glyph"></span>งานว</a>
<a class="nav-item" href="#simulation"><span class="nav-glyph"></span>จำลอง</a>
</nav>
@@ -684,6 +716,42 @@ 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="simulation">
<div class="panel-header signal-header">