- 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.
Two root causes of tables and panels spilling past the window frame:
1. Global 'table { min-width: 850px }' forced EVERY table (source/score/modal/
backtest) to 850px regardless of container -> now min-width:0 so tables
shrink to 100%; only .factor-table (stock board, inside .table-wrap)
keeps its 850px and scrolls. Small tables get table-layout:fixed +
overflow-wrap on mobile so long content wraps instead of pushing out.
2. Grid/flex tracks lacked minmax(0,1fr) / min-width:0, so wide grid items
(KPI numbers, theme cards) pushed the whole row past the viewport at
768-1040px (doc overflow 208px). Fixed: all grid-template-columns use
minmax(0,1fr); .content is now flex:1 1 auto + min-width:0; KPI/theme/bt
cards get min-width:0 + overflow-wrap.
Verified programmatically in-browser: document overflow = 0 at 320/360/500/
600/720/760/768/820/900/1040/1200/1440px; score-table + source-table over=0
at all widths (incl inside the per-source modal). Board scrolls in .table-wrap
as designed. Backend suite 369 green (CSS-only change).
After reviewer suggestion: per theme, display sum(contribution)/sum(|weight|) =
theme surprise so the owner can confirm the source audit matches the real score.
Q2 flexible scoring: _fetch_with_cache now degrades instead of raising
DashboardError — a source that fails with no cached value returns {} so the
theme scorer drops that source's factors; a previously-good value is kept as
stale by the daily cache. Verified: all-sources-down still builds 13 themes.
Q3 per-source audit: new themes.factor_source_breakdown(fetched, theme) shows
per factor source/raw/normalized/weight/contribution; dashboard exposes
fetch_data + factor_sources; per-symbol modal renders symbolDetail.factor_sources
(e.g. retail: te_thailand ยอดขายปลีก -14.5 -> -1.0 x 0.7 = -0.7).
Suite 369 green; independent review passed: true.
Q1 (HAR for deferred sources) spike recorded: method works, REIC needs deeper
interaction; NBTC 403 likely unbpassable without a session.
- define missing design tokens (--card/--foreground/--accent/--font/--text-2)
that theme/modal components referenced but :root never declared (they rendered
transparent/wrong color)
- zero section-panel padding so .signal-header is the single top-spacing source
(was double 22px+22px on theme/lineage/health/sim/backtest panels)
- theme cards now surface the new source reads (banks rate, retail sales YoY,
nonbank household debt, property prices, telecom business confidence)
Add self-contained Docker deployment for EasyPanel / docker-compose:
- Dockerfile: python:3.11-slim + nginx. Serves the PREBUILT Vue SPA
(frontend/dist, committed) via nginx and reverse-proxies /api to the Flask
backend on 127.0.0.1:5000. No node/npm in the image (avoids Vite/npm
flakiness in Docker builds). VOLUME /app/backend/data for persistence
(factor vintages, dividend ledger, forward runs, prices). HEALTHCHECK hits
/api/v1/dashboard/summary. Exposes :80.
- deploy/nginx.conf: SPA fallback + /api proxy to 127.0.0.1:5000 + gzip.
- deploy/entrypoint.sh: starts Flask (HOST=0.0.0.0:5000) + nginx foreground,
forwards signals.
- docker-compose.yml: single service 'set50', port 8080:80, mounts
./backend/data for persistence, healthcheck.
- .dockerignore excludes .git/.venv/backend/data/node_modules/docs.
- .gitignore now tracks frontend/dist/ (prebuilt bundle required by the
image); backend/data stays untracked.
- Backend runtime verified: test_client GET /api/v1/dashboard/summary=200
(the HEALTHCHECK path). entrypoint sh -n, compose YAML parse, and all
Dockerfile-referenced files exist. NOTE: no local docker here, so the
image itself was not built — that happens on EasyPanel.