Files
2026-06-30 18:30:24 +00:00

177 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Weather - The Dark Elite</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<script src="https://unpkg.com/pmtiles@3.2.1/dist/pmtiles.js"></script>
<script src="https://unpkg.com/protomaps-themes-base@4.3.0/dist/protomaps-themes-base.js"></script>
<style>
:root { --bg:#060708; --border:rgba(80,90,100,0.2); --accent:#22d3ee; --text:#e5e7eb; --muted:#9ca3af; }
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--text); font-family:system-ui,sans-serif; min-height:100vh; }
.container { max-width:1200px; margin:0 auto; padding:2rem; }
header { display:flex; justify-content:space-between; align-items:center; padding-bottom:1.5rem; border-bottom:1px solid var(--border); margin-bottom:2rem; }
header h1 { margin:0; font-size:1.5rem; letter-spacing:0.2em; text-transform:uppercase; color:var(--accent); text-shadow:0 0 12px rgba(34,211,238,0.4); }
header a { color:var(--muted); text-decoration:none; font-size:0.85rem; }
header a:hover { color:var(--accent); }
.status { font-size:0.75rem; color:var(--muted); font-family:ui-monospace,monospace; }
.status.live { color:#22c55e; }
.status.cached { color:#f59e0b; }
.current { display:grid; grid-template-columns:auto 1fr; gap:2rem; align-items:center; padding:2rem; background:linear-gradient(135deg,rgba(18,20,24,0.9),rgba(10,11,13,0.95)); border:1px solid var(--border); border-radius:8px; margin-bottom:2rem; }
.current .temp { font-size:5rem; font-weight:200; color:var(--accent); text-shadow:0 0 20px rgba(34,211,238,0.3); line-height:1; }
.current .meta { display:grid; grid-template-columns:repeat(2,1fr); gap:0.75rem; }
.current .meta strong { color:var(--muted); font-weight:400; text-transform:uppercase; font-size:0.7rem; letter-spacing:0.15em; display:block; margin-bottom:0.2rem; }
.section-title { font-size:0.75rem; letter-spacing:0.25em; text-transform:uppercase; color:var(--accent); border-left:3px solid var(--accent); padding-left:0.75rem; margin:2rem 0 1rem 0; display:flex; justify-content:space-between; align-items:center; }
.section-title button { background:rgba(18,20,24,0.9); border:1px solid var(--border); color:var(--text); padding:0.3rem 0.8rem; font-size:0.7rem; cursor:pointer; border-radius:3px; letter-spacing:0.1em; }
.section-title button:hover { border-color:var(--accent); color:var(--accent); }
#radar-wrap { position:relative; height:500px; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
#radar-map { width:100%; height:100%; }
#radar-time { position:absolute; bottom:10px; left:10px; background:rgba(6,7,8,0.85); padding:0.4rem 0.8rem; border:1px solid var(--border); border-radius:4px; font-family:ui-monospace,monospace; font-size:0.8rem; color:var(--accent); z-index:5; }
.forecast { display:grid; grid-template-columns:repeat(7,1fr); gap:0.75rem; }
.day { background:linear-gradient(135deg,rgba(18,20,24,0.85),rgba(10,11,13,0.9)); border:1px solid var(--border); border-radius:6px; padding:1rem; text-align:center; transition:border-color 0.15s; }
.day:hover { border-color:rgba(34,211,238,0.4); }
.day .dow { font-size:0.75rem; color:var(--muted); text-transform:uppercase; letter-spacing:0.15em; margin-bottom:0.5rem; }
.day .hi { font-size:1.4rem; color:var(--text); }
.day .lo { font-size:0.85rem; color:var(--muted); }
.day .precip { font-size:0.7rem; color:#60a5fa; margin-top:0.4rem; }
.alerts { display:flex; flex-direction:column; gap:0.5rem; }
.alert { background:rgba(220,38,38,0.1); border:1px solid rgba(220,38,38,0.3); border-left:3px solid #dc2626; border-radius:4px; padding:0.75rem 1rem; font-size:0.85rem; }
.alert strong { color:#fca5a5; text-transform:uppercase; font-size:0.7rem; letter-spacing:0.15em; }
.empty { color:var(--muted); font-style:italic; font-size:0.85rem; }
@media (max-width:768px) { .forecast { grid-template-columns:repeat(3,1fr); } .current { grid-template-columns:1fr; } .current .temp { font-size:3.5rem; } #radar-wrap { height:350px; } }
</style>
</head>
<body>
<div class="container">
<header>
<div><h1>Weather Intel</h1><div id="loc" class="status">Loading...</div></div>
<div style="text-align:right"><a href="/">← Hub</a><br><span id="status" class="status">Initializing</span></div>
</header>
<div class="current"><div class="temp" id="temp">--</div><div class="meta">
<div><strong>Conditions</strong><span id="cond">--</span></div>
<div><strong>Feels Like</strong><span id="feels">--</span></div>
<div><strong>Humidity</strong><span id="hum">--</span></div>
<div><strong>Wind</strong><span id="wind">--</span></div>
<div><strong>Pressure</strong><span id="pres">--</span></div>
<div><strong>Visibility</strong><span id="vis">--</span></div>
</div></div>
<div class="section-title">Live Radar<button id="play-btn">Pause</button></div>
<div id="radar-wrap"><div id="radar-map"></div><div id="radar-time">--</div></div>
<div class="section-title">Active Alerts</div><div id="alerts" class="alerts"><div class="empty">No active alerts</div></div>
<div class="section-title">7-Day Forecast</div><div id="forecast" class="forecast"></div>
</div>
<script>
const LAT=32.1268, LON=-96.9469, LOC="Milford, TX";
const WMO={0:"Clear",1:"Mostly Clear",2:"Partly Cloudy",3:"Overcast",45:"Foggy",48:"Foggy",51:"Light Drizzle",53:"Drizzle",55:"Heavy Drizzle",61:"Light Rain",63:"Rain",65:"Heavy Rain",71:"Light Snow",73:"Snow",75:"Heavy Snow",80:"Showers",81:"Heavy Showers",82:"Violent Showers",95:"Thunderstorm",96:"T-storm w/ Hail",99:"T-storm w/ Hail"};
const DOW=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
document.getElementById("loc").textContent = LOC + " // " + LAT.toFixed(4) + "," + LON.toFixed(4);
function setStatus(t,cls){const e=document.getElementById("status");e.textContent=t;e.className="status "+(cls||"");}
async function fetchWeather(){
try {
const url="https://api.open-meteo.com/v1/forecast?latitude="+LAT+"&longitude="+LON+"&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,pressure_msl,wind_speed_10m,wind_direction_10m,visibility&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=America/Chicago&forecast_days=7";
const r=await fetch(url); if(!r.ok) throw new Error(r.status);
const d=await r.json();
localStorage.setItem("wx_cache",JSON.stringify({data:d,ts:Date.now()}));
render(d); setStatus("Live // "+new Date().toLocaleTimeString(),"live");
} catch(e) {
const c=localStorage.getItem("wx_cache");
if(c){const o=JSON.parse(c);render(o.data);setStatus("Cached // "+new Date(o.ts).toLocaleString(),"cached");}
else setStatus("Offline - no cache","cached");
}
}
function render(d){
const c=d.current;
document.getElementById("temp").textContent=Math.round(c.temperature_2m)+"\u00B0";
document.getElementById("cond").textContent=WMO[c.weather_code]||"Unknown";
document.getElementById("feels").textContent=Math.round(c.apparent_temperature)+"\u00B0F";
document.getElementById("hum").textContent=c.relative_humidity_2m+"%";
const dir=["N","NE","E","SE","S","SW","W","NW"][Math.round(c.wind_direction_10m/45)%8];
document.getElementById("wind").textContent=Math.round(c.wind_speed_10m)+" mph "+dir;
document.getElementById("pres").textContent=c.pressure_msl.toFixed(1)+" hPa";
document.getElementById("vis").textContent=c.visibility?Math.round(c.visibility/1609)+" mi":"--";
const fc=document.getElementById("forecast"); fc.innerHTML="";
for(let i=0;i<7;i++){
const dt=new Date(d.daily.time[i]+"T00:00");
fc.innerHTML+='<div class="day"><div class="dow">'+DOW[dt.getDay()]+'</div><div class="hi">'+Math.round(d.daily.temperature_2m_max[i])+'\u00B0</div><div class="lo">'+Math.round(d.daily.temperature_2m_min[i])+'\u00B0</div><div class="precip">'+(d.daily.precipitation_probability_max[i]||0)+'% precip</div></div>';
}
}
async function fetchAlerts(){
try {
const r=await fetch("https://api.weather.gov/alerts/active?point="+LAT+","+LON);
if(!r.ok) throw new Error(r.status);
const d=await r.json();
const el=document.getElementById("alerts");
if(!d.features||!d.features.length){el.innerHTML='<div class="empty">No active alerts</div>';return;}
el.innerHTML=d.features.slice(0,5).map(f=>'<div class="alert"><strong>'+f.properties.event+'</strong><br>'+(f.properties.headline||"")+'</div>').join("");
} catch(e){}
}
const protocol=new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles",protocol.tile);
let radarMap, radarFrames=[], radarIdx=0, radarTimer=null, radarPlaying=true;
function initRadar(){
radarMap=new maplibregl.Map({
container:"radar-map",
style:{version:8,
glyphs:"https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf",
sprite:"https://protomaps.github.io/basemaps-assets/sprites/v4/dark",
sources:{protomaps:{type:"vector",url:"pmtiles:///maps/base.pmtiles"}},
layers:protomaps_themes_base.default("protomaps","dark")
},
center:[LON,LAT], zoom:6
});
radarMap.on("load",()=>{
new maplibregl.Marker({color:"#22d3ee"}).setLngLat([LON,LAT]).addTo(radarMap);
loadRadarFrames();
});
}
async function loadRadarFrames(){
try {
const r=await fetch("https://api.rainviewer.com/public/weather-maps.json");
const d=await r.json();
const newFrames=[...d.radar.past, ...d.radar.nowcast].map(f=>({time:f.time,path:d.host+f.path}));
radarFrames.forEach((_,i)=>{
if(radarMap.getLayer("radar-"+i)) radarMap.removeLayer("radar-"+i);
if(radarMap.getSource("radar-"+i)) radarMap.removeSource("radar-"+i);
});
radarFrames=newFrames;
radarFrames.forEach((f,i)=>{
radarMap.addSource("radar-"+i,{type:"raster",tiles:[f.path+"/256/{z}/{x}/{y}/2/1_1.png"],tileSize:256});
radarMap.addLayer({id:"radar-"+i,type:"raster",source:"radar-"+i,paint:{"raster-opacity":0}});
});
radarIdx=0; showFrame(0);
if(radarPlaying && !radarTimer) startLoop();
} catch(e){ document.getElementById("radar-time").textContent="Radar offline"; }
}
function showFrame(i){
radarFrames.forEach((f,j)=>{ if(radarMap.getLayer("radar-"+j)) radarMap.setPaintProperty("radar-"+j,"raster-opacity",j===i?0.75:0); });
const t=new Date(radarFrames[i].time*1000);
const isFuture=radarFrames[i].time*1000>Date.now();
document.getElementById("radar-time").textContent=(isFuture?"FORECAST ":"")+t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});
}
function startLoop(){
radarTimer=setInterval(()=>{radarIdx=(radarIdx+1)%radarFrames.length;showFrame(radarIdx);},500);
}
document.getElementById("play-btn").addEventListener("click",e=>{
radarPlaying=!radarPlaying;
if(radarPlaying){startLoop();e.target.textContent="Pause";}
else{clearInterval(radarTimer);radarTimer=null;e.target.textContent="Play";}
});
initRadar(); fetchWeather(); fetchAlerts();
setInterval(fetchWeather,600000); setInterval(fetchAlerts,300000); setInterval(loadRadarFrames,600000);
</script>
</body>
</html>