fix: add health checks and repair dashboard contracts
This commit is contained in:
+44
-32
@@ -118,6 +118,26 @@ def save_cache(data):
|
||||
atomic_write_json(CACHE_PATH, data)
|
||||
|
||||
|
||||
@app.get("/health/live")
|
||||
def health_live():
|
||||
"""Report that the API process is responsive."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/health/ready")
|
||||
def health_ready():
|
||||
"""Report readiness only after a usable score has been persisted."""
|
||||
scored = load_cache().get("_scored", {})
|
||||
score = scored.get("composite_score")
|
||||
count = scored.get("scored_count", 0)
|
||||
if score is None or count < 1:
|
||||
return JSONResponse(
|
||||
{"status": "not_ready", "reason": "no usable persisted score"},
|
||||
status_code=503,
|
||||
)
|
||||
return {"status": "ready", "score": score, "scored_metrics": count}
|
||||
|
||||
|
||||
def append_history(score_data):
|
||||
"""Append a daily score entry to history."""
|
||||
entry = {
|
||||
@@ -982,8 +1002,8 @@ function highlightMetricPeriods(metricKey, currentRaw, margin) {
|
||||
|
||||
// Build an array of {date, rawValue} for the selected metric
|
||||
const metricSeries = fullDailyScores
|
||||
.filter(d => d.metrics && d.metrics[metricKey] != null)
|
||||
.map(d => ({ date: d.date, value: d.metrics[metricKey], isSimilar: Math.abs(d.metrics[metricKey] - currentRaw) <= margin }));
|
||||
.filter(d => d.metric_values && d.metric_values[metricKey] != null)
|
||||
.map(d => ({ date: d.date, value: d.metric_values[metricKey], isSimilar: Math.abs(d.metric_values[metricKey] - currentRaw) <= margin }));
|
||||
|
||||
// Store for use in chart rendering
|
||||
window._highlightMetric = { key: metricKey, series: metricSeries, currentRaw, margin };
|
||||
@@ -1206,7 +1226,27 @@ function renderHistoryFromData(history) {
|
||||
});
|
||||
}
|
||||
|
||||
// Load backtest daily scores for the chart
|
||||
// Load backtest daily scores and historical context with one request.
|
||||
function renderHistoricalContext(ctx) {
|
||||
if (!ctx) return;
|
||||
const el = document.getElementById('histContext');
|
||||
const txt = document.getElementById('histContextText');
|
||||
let html = 'Score <strong>' + ctx.current_score + '</strong> is in the <strong style="color:#22d3ee">top ' + (100 - ctx.percentile).toFixed(1) + '%</strong> historically.<br>';
|
||||
const fmtR = (v) => v == null ? null : (v >= 0 ? '+' : '') + v.toFixed(1) + '%';
|
||||
const cR = (v) => v >= 0 ? '#22c55e' : '#ef4444';
|
||||
const periods = [
|
||||
['30d', ctx.avg_30d_return], ['90d', ctx.avg_90d_return],
|
||||
['180d', ctx.avg_180d_return], ['1yr', ctx.avg_1yr_return]
|
||||
];
|
||||
const parts = [];
|
||||
for (const [label, val] of periods) {
|
||||
if (val != null) parts.push('<strong style="color:' + cR(val) + '">' + label + ': ' + fmtR(val) + '</strong>');
|
||||
}
|
||||
if (parts.length) html += 'Average returns from this level: ' + parts.join(' · ');
|
||||
txt.innerHTML = html;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
async function loadBacktestChart() {
|
||||
try {
|
||||
const r = await fetch('/api/backtest?mode=' + currentMode);
|
||||
@@ -1215,6 +1255,7 @@ async function loadBacktestChart() {
|
||||
fullDailyScores = data.chart_data;
|
||||
applyChartRange(currentRange);
|
||||
}
|
||||
renderHistoricalContext(data.current_context);
|
||||
} catch(e) { console.error('Backtest chart load failed:', e); }
|
||||
}
|
||||
|
||||
@@ -1343,35 +1384,6 @@ function setMode(mode) {
|
||||
drawScoreRing(0);
|
||||
poll();
|
||||
setInterval(poll, 30000);
|
||||
|
||||
// Load historical context from backtest
|
||||
(async function() {
|
||||
try {
|
||||
const r = await fetch('/api/backtest/status');
|
||||
const s = await r.json();
|
||||
if (!s.exists) return;
|
||||
const br = await fetch('/api/backtest');
|
||||
const bt = await br.json();
|
||||
if (bt.error || !bt.current_context) return;
|
||||
const ctx = bt.current_context;
|
||||
const el = document.getElementById('histContext');
|
||||
const txt = document.getElementById('histContextText');
|
||||
let html = 'Score <strong>' + ctx.current_score + '</strong> is in the <strong style="color:#22d3ee">top ' + (100 - ctx.percentile).toFixed(1) + '%</strong> historically.<br>';
|
||||
const fmtR = (v) => v == null ? null : (v >= 0 ? '+' : '') + v.toFixed(1) + '%';
|
||||
const cR = (v) => v >= 0 ? '#22c55e' : '#ef4444';
|
||||
const periods = [
|
||||
['30d', ctx.avg_30d_return], ['90d', ctx.avg_90d_return],
|
||||
['180d', ctx.avg_180d_return], ['1yr', ctx.avg_1yr_return]
|
||||
];
|
||||
let parts = [];
|
||||
for (const [label, val] of periods) {
|
||||
if (val != null) parts.push('<strong style="color:' + cR(val) + '">' + label + ': ' + fmtR(val) + '</strong>');
|
||||
}
|
||||
if (parts.length) html += 'Average returns from this level: ' + parts.join(' · ');
|
||||
txt.innerHTML = html;
|
||||
el.style.display = 'block';
|
||||
} catch(e) { /* backtest data not available yet */ }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
Reference in New Issue
Block a user