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:
+113
-1
@@ -44,7 +44,8 @@ HTML_TEMPLATE = r'''
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>HK Weather Prediction Market — Dev Dashboard</title>
|
<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>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body {
|
body {
|
||||||
@@ -766,6 +767,117 @@ def api_ml():
|
|||||||
return jsonify({"error": str(e)}), 500
|
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("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template_string(HTML_TEMPLATE)
|
return render_template_string(HTML_TEMPLATE)
|
||||||
|
|||||||
Reference in New Issue
Block a user