From 90e63e3384154277ee39b42a3398e9430ef4208d Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Tue, 25 Aug 2026 15:59:02 +0700 Subject: [PATCH] [verified] Add Thai multi-theme panel to dashboard (3 themes + combined summary) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Theme panel (ธีม): shows tourism/auto_credit/refining_energy with Thai labels, frequency chips (monthly/quarterly), and real factor reads (tourism surprise, car sales YoY, TOP net profit) from /api/v1/themes - Combined count badge; responsive 1-col on mobile - Loads /api/v1/themes in loadDashboard (already wired backend) - Verified: npm build passes, browser shows 3 Thai cards with live values --- frontend/src/App.vue | 48 +++++++++++++++++++++++++++++++++++++++++- frontend/src/style.css | 15 +++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 53754ac..b6d684d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,6 +5,7 @@ const summary = ref(null) const observations = ref(null) const signalData = ref(null) const factorData = ref(null) +const themeData = ref(null) const dividendOnly = ref(false) const sortKey = ref('signal_score') const sortDirection = ref('desc') @@ -34,6 +35,11 @@ const factorAvailable = computed(() => factorData.value?.available ?? false) const dividendCount = computed(() => factorData.value?.dividend_count ?? 0) const signalScoreCount = computed(() => factorRows.value.filter((f) => f.signal_side === 'LONG' || f.signal_side === 'SHORT').length) +// Multi-theme combined data from /api/v1/themes. +const themes = computed(() => themeData.value?.themes ?? []) +const combinedBoard = computed(() => themeData.value?.board ?? []) +const combinedCount = computed(() => themeData.value?.combined_count ?? 0) + // Rows filtered to dividend-paying names only when the toggle is on. const filteredFactorRows = computed(() => { let rows = factorRows.value @@ -187,11 +193,12 @@ async function loadDashboard() { loading.value = true error.value = '' try { - const [summaryBody, observationBody, signalBody, factorBody, ledgerBody, sessionBody, backtestBody, researchBody] = await Promise.all([ + const [summaryBody, observationBody, signalBody, factorBody, themeBody, ledgerBody, sessionBody, backtestBody, researchBody] = await Promise.all([ fetchJson('/api/v1/dashboard/summary'), fetchJson('/api/v1/factors/tourism/observations'), fetchJson('/api/v1/signals'), fetchJson('/api/v1/factors'), + fetchJson('/api/v1/themes'), fetchJson('/api/v1/paper/ledger'), fetchJson('/api/v1/auth/paper', { credentials: 'include' }), fetchBacktestReadiness(), @@ -201,6 +208,7 @@ async function loadDashboard() { observations.value = observationBody signalData.value = signalBody factorData.value = factorBody + themeData.value = themeBody ledger.value = ledgerBody paperAuthenticated.value = Boolean(sessionBody.authenticated) paperAuthMode.value = sessionBody.mode || 'token' @@ -362,6 +370,44 @@ onMounted(loadDashboard) +
+
+
+
Alternative factors · ข้อมูลไทย
+

ธีม (Themes)

+

ภาพรวม alternative factors ของไทย — แต่ละธีมมีความถี่ข้อมูลต่างกัน (monthly / quarterly) ดังนั้นอย่าเทียบเป็นจุดเวลาเดียวกัน.

+
+ รวม {{ combinedCount }} symbols +
+
+
+
+ {{ theme.frequency }} + {{ theme.label_en }} +
+
{{ theme.label_th }}
+
{{ theme.source }}
+
+ +
{{ theme.read?.error || 'ไม่พบข้อมูล' }}
+
+
+
+
+
diff --git a/frontend/src/style.css b/frontend/src/style.css index 6a6dadf..ce1248d 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -109,6 +109,21 @@ tbody tr:hover { background: rgba(255,255,255,.025); } .side-pill.short { color: var(--red); background: var(--red-soft); } .side-pill.neutral { color: var(--amber); background: var(--amber-soft); } .reason-code { display: inline-block; margin: 2px 3px 2px 0; padding: 4px 5px; border: 1px solid var(--line-bright); border-radius: 4px; color: var(--faint); font: 9px 'DM Mono', monospace; } + +/* ---- Theme panel (multi-theme) ---- */ +.theme-panel { margin-top: 18px; } +.theme-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.theme-card { border: 1px solid var(--line-bright); border-radius: 10px; padding: 14px; background: var(--card); } +.theme-card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } +.theme-chip { background: var(--accent-soft, rgba(63,161,255,0.12)); color: var(--accent); font: 9px 'DM Mono', monospace; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; } +.theme-label-en { font: 600 12px/1.4 var(--font); color: var(--foreground); } +.theme-label-th { font-weight: 700; margin-bottom: 2px; } +.theme-source { font: 10px 'DM Mono', monospace; color: var(--faint); margin-bottom: 10px; } +.theme-read-value { font-size: 22px; font-weight: 700; color: var(--foreground); } +.theme-read-meta { font-size: 11px; color: var(--faint); margin-top: 2px; } +.theme-read-error { font-size: 11px; color: var(--danger, #e5484d); } + +@media (max-width: 720px) { .theme-grid { grid-template-columns: 1fr; } } .row-action, .primary-button { padding: 8px 10px; border: 1px solid var(--line-bright); border-radius: 5px; color: var(--text); background: transparent; font-size: 10px; white-space: nowrap; transition: .2s ease; } .row-action:hover { color: var(--mint); border-color: var(--mint); }