Add /simple dashboard (zero JS deps) + Chart.js fallback error handler

- /simple: lightweight HTML dashboard, no external JS, works instantly
  Shows current conditions, tomorrow forecast, ML predictions,
  5-day forecast table, Kelly sizing, typhoon signal
- /: added Chart.js CDN onerror fallback — shows clear message + /simple link
  if CDN is unreachable (common cause of 'everything says loading')
This commit is contained in:
ramseshk
2026-08-11 12:10:39 +08:00
parent 0f44b91350
commit 40f26cf232
+113 -1
View File
@@ -44,7 +44,8 @@ HTML_TEMPLATE = r'''
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HK Weather Prediction Market — Dev Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"
onerror="document.body.innerHTML='<h1 style=color:#58a6ff;text-align:center;padding-top:100px>Chart.js CDN failed to load</h1><p style=color:#8b949e;text-align:center>Try <a href=/simple style=color:#58a6ff>/simple</a> instead</p><p style=color:#8b949e;text-align:center>Or open http://localhost:5000/simple</p>'"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
@@ -766,6 +767,117 @@ def api_ml():
return jsonify({"error": str(e)}), 500
@app.route("/simple")
def simple():
"""Simple unstyled dashboard that always works."""
return render_template_string("""
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>HK Weather — Simple</title>
<style>
body{background:#0d1117;color:#c9d1d9;font:14px monospace;max-width:900px;margin:20px auto;padding:16px}
h1{color:#58a6ff;font-size:18px}
h2{color:#8b949e;font-size:13px;margin-top:16px}
.card{background:#161b22;border:1px solid#30363d;border-radius:6px;padding:12px;margin:8px 0}
.row{display:flex;justify-content:space-between;margin:2px 0}
.label{color:#8b949e}
.val{color:#f0f6fc;font-weight:700}
.green{color:#3fb950}.red{color:#f85149}.yellow{color:#d29922}.gray{color:#8b949e}
.bar{display:inline-block;height:14px;background:#3fb950;border-radius:2px;vertical-align:middle;margin-left:4px}
.err{color:#f85149}
</style></head><body>
<h1>HK Weather Prediction Market</h1>
<div id="ts" class="gray"></div>
<h2>Current Conditions (HKO)</h2>
<div class="card" id="current">Loading...</div>
<h2>Tomorrow Forecast</h2>
<div class="card" id="tomorrow">Loading...</div>
<h2>ML Model Predictions</h2>
<div class="card" id="ml">Loading...</div>
<h2>5-Day Forecast</h2>
<div class="card" id="forecast">Loading...</div>
<h2>Typhoon</h2>
<div class="card" id="typhoon">Loading...</div>
<h2>Kelly Sizing</h2>
<div class="card" id="kelly">Loading...</div>
<script>
async function load(){
try{
let r=await fetch('/api/dashboard');let d=await r.json();
document.getElementById('ts').textContent=d.fetch_time;
let c=d.current||{};
document.getElementById('current').innerHTML=
'<div class=row><span class=label>Temperature</span><span class=val>'+c.temperature+'°C</span></div>'+
'<div class=row><span class=label>Feels Like</span><span class=val>'+c.feels_like+'°C</span></div>'+
'<div class=row><span class=label>Humidity</span><span class=val>'+c.humidity+'%</span></div>'+
'<div class=row><span class=label>Wind</span><span class=val>'+c.wind_speed+' km/h</span></div>'+
'<div class=row><span class=label>Pressure</span><span class=val>'+c.pressure+' hPa</span></div>'+
(c.warning?'<div class=row><span class=label>Warning</span><span class=yellow>'+c.warning.substring(0,80)+'</span></div>':'');
let t=d.tomorrow||{};
document.getElementById('tomorrow').innerHTML=
'<div class=row><span class=label>'+t.date+'</span></div>'+
'<div class=row><span class=label>Tmax</span><span class=val>'+t.temp_max+'°C</span></div>'+
'<div class=row><span class=label>Tmin</span><span class=val>'+t.temp_min+'°C</span></div>'+
'<div class=row><span class=label>Rain Prob</span><span class=val>'+t.rain_prob+'%</span></div>'+
'<div class=row><span class=label>Rain Total</span><span class=val>'+t.rain_sum+'mm</span></div>'+
'<div class=row><span class=label>Wind Max</span><span class=val>'+t.wind_max+' km/h</span></div>'+
'<div class=row><span class=label>Gusts</span><span class=val>'+t.wind_gust+' km/h</span></div>';
let f=d.forecast||{};
let fc='<div style=overflow-x:auto><table style=font-size:12px;width:100%>'+
'<tr style=color:#8b949e><td>Date</td><td>Tmax</td><td>Tmin</td><td>Rain%</td><td>Rain mm</td><td>Wind</td><td>Gust</td></tr>';
for(let i=0;i<(f.dates||[]).length;i++){
fc+='<tr><td>'+f.dates[i]+'</td><td>'+f.tmax[i]+'</td><td>'+f.tmin[i]+'</td>'+
'<td>'+f.rain_prob[i]+'%</td><td>'+f.rain_sum[i]+'</td>'+
'<td>'+f.wind_max[i]+'</td><td>'+f.wind_gust[i]+'</td></tr>';
}
fc+='</table></div>';
document.getElementById('forecast').innerHTML=fc;
let k=d.kelly||[];
let kh='';
for(let i=0;i<k.length;i++){
let cl=k[i].active?(k[i].side=='buy_yes'?'green':'red'):'gray';
kh+='<div class=row><span>'+k[i].name+'</span><span class='+cl+'>'+k[i].side+' $'+k[i].size.toFixed(2)+'</span></div>';
}
document.getElementById('kelly').innerHTML=kh||'No signals';
let ty=d.typhoon||{};
document.getElementById('typhoon').textContent='Signal: T'+ty.signal;
}catch(e){
document.getElementById('current').innerHTML='<span class=err>Error: '+e.message+'</span>';
}
}
load();
// Load ML predictions separately
fetch('/api/ml').then(r=>r.json()).then(d=>{
let models=d.models||{};
let html='';
for(let[name,m] of Object.entries(models)){
let cl=m.calibrated>75?'green':(m.calibrated<25?'red':'yellow');
let w=m.calibrated>50?m.calibrated:100-m.calibrated;
html+='<div class=row style=margin:4px 0><span>'+m.description+'</span>'+
'<span><span class='+cl+'>'+m.calibrated.toFixed(1)+'%</span>'+
'<span class=bar style=width:'+(w*2)+'px></span>'+
'</span></div>';
}
document.getElementById('ml').innerHTML=html||'No ML models loaded';
}).catch(e=>{
document.getElementById('ml').innerHTML='<span class=err>ML unavailable (train first)</span>';
});
</script></body></html>""")
@app.route("/")
def index():
return render_template_string(HTML_TEMPLATE)