/* AXNAP-200 THEME START */ :root{ --bg:#071a2f; --bg2:#0b2441; --surface:rgba(8,27,48,.84); --surface2:rgba(8,27,48,.72); --card:#0f2d4d; --card2:#13365d; --line:rgba(31,124,255,.14); --text:#e8f3ff; --muted:#8fb3d9; --green:#b6e14d; --green2:#39d0b7; --blue:#1f7cff; --cyan:#3bd3ff; --yellow:#ffc857; --red:#ff5c6c; --violet:#b56cff; --shadow:0 10px 28px rgba(0,0,0,.18); --radius:20px; } html[data-theme="light"]{ --bg:#eef5fb; --bg2:#dfeaf7; --surface:rgba(255,255,255,.86); --surface2:rgba(248,251,255,.92); --card:#ffffff; --card2:#f7fbff; --line:rgba(17,63,130,.10); --text:#12263f; --muted:#5b7898; --green:#7fbf2a; --green2:#23b39c; --blue:#1f7cff; --cyan:#209fd8; --yellow:#d2a22e; --red:#d35563; --violet:#8c5bd3; --shadow:0 8px 22px rgba(30,52,90,.10); } /* AXNAP-200 THEME END */ *{box-sizing:border-box} html,body{margin:0;padding:0} body{ font-family:Arial, Helvetica, sans-serif; color:var(--text); background: radial-gradient(circle at top left, rgba(57,208,183,.06), transparent 24%), radial-gradient(circle at top right, rgba(31,124,255,.08), transparent 28%), linear-gradient(180deg, var(--bg) 0%, var(--bg2) 100%); } .ax-shell{ width:min(1480px, calc(100% - 20px)); margin:14px auto 28px; } .ax-panel{ background:linear-gradient(180deg, var(--surface), var(--surface2)); border:1px solid var(--line); border-radius:22px; box-shadow:var(--shadow); margin-bottom:14px; } .ax-card{ background:linear-gradient(180deg, var(--surface), var(--surface2)); border:1px solid var(--line); border-radius:20px; box-shadow:var(--shadow); } .ax-btn{ min-height:44px; padding:10px 14px; border:none; border-radius:12px; cursor:pointer; font-size:13px; font-weight:700; color:var(--text); background:rgba(255,255,255,.08); transition:.18s ease; } .ax-btn:hover{ transform:translateY(-1px); background:rgba(255,255,255,.16); } .ax-header{ display:flex; justify-content:space-between; gap:16px; align-items:flex-start; padding:16px 18px; } .ax-brand{ display:flex; align-items:center; gap:14px; } .ax-brand-logo{ width:48px; height:48px; object-fit:contain; display:block; } .ax-brand-title{ margin:0; font-size:30px; font-weight:800; line-height:1; letter-spacing:.5px; background:linear-gradient(90deg, var(--green), var(--green2), var(--blue)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; } .ax-brand-sub{ margin-top:6px; color:var(--muted); font-size:13px; } .ax-header-right{ min-width:320px; display:flex; flex-direction:column; gap:8px; align-items:flex-end; } .ax-toolbar{ display:flex; flex-wrap:wrap; gap:8px; } .ax-timezones{ display:flex; gap:16px; flex-wrap:wrap; } .ax-zone-time{ min-width:74px; text-align:right; } .ax-zone-label{ color:var(--muted); font-size:11px; } .ax-zone-value{ font-weight:700; font-size:16px; } .ax-hero-wrap{ display:grid; grid-template-columns:2fr 1fr; gap:14px; } .ax-hero-main{ padding:22px; min-height:188px; background: linear-gradient(135deg, rgba(182,225,77,.10), rgba(57,208,183,.12), rgba(31,124,255,.16)), linear-gradient(180deg, var(--card2), var(--card)); border:1px solid rgba(31,124,255,.16); border-radius:24px; box-shadow:var(--shadow); } .ax-hero-grid{ display:grid; grid-template-columns:1.15fr 1fr .95fr; gap:18px; align-items:center; height:100%; } .ax-hero-left{ display:flex; align-items:flex-start; gap:14px; } .ax-hero-icon{ font-size:46px; line-height:1; } .ax-hero-risk{ font-size:14px; text-transform:uppercase; letter-spacing:1px; font-weight:800; margin-bottom:10px; } .ax-hero-maintext{ font-size:40px; font-weight:900; line-height:1.05; margin-bottom:8px; } .ax-hero-sub{ font-size:17px; color:var(--text); opacity:.94; margin-bottom:10px; } .ax-hero-actions{ display:flex; gap:10px; flex-wrap:wrap; } .ax-hero-center{ text-align:center; border-left:1px solid rgba(255,255,255,.08); border-right:1px solid rgba(255,255,255,.08); padding:0 14px; } .ax-hero-temp{ font-size:56px; font-weight:900; line-height:1; margin-bottom:8px; } .ax-hero-loc{font-size:14px;margin-bottom:6px} .ax-hero-coords{font-size:11px;color:var(--muted);line-height:1.5} .ax-hero-right{ text-align:right; font-size:13px; } .ax-hero-trend{ margin-bottom:12px; line-height:1.7; } .ax-hero-update{ color:var(--muted); font-size:11px; } .ax-hero-side{ padding:22px; border-radius:24px; } .ax-mini{ color:var(--muted); font-size:13px; margin-bottom:4px; } .ax-big{ font-size:30px; font-weight:800; line-height:1.1; } .ax-progress{ width:100%; height:12px; margin-top:10px; border-radius:999px; overflow:hidden; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.08); } .ax-progress-fill{ height:100%; width:0%; border-radius:999px; background:linear-gradient(90deg, var(--green), var(--green2), var(--blue)); transition:width .25s ease; } .ax-caption{ margin-top:8px; color:var(--muted); font-size:12px; } .ax-status-grid{ display:grid; grid-template-columns:repeat(5,1fr); gap:10px; } .ax-status-box{ padding:12px; border-radius:16px; min-height:86px; } .ax-k{ color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.6px; margin-bottom:8px; } .ax-v{ font-size:19px; font-weight:800; line-height:1.2; } .ax-t{ margin-top:8px; color:var(--muted); font-size:11px; line-height:1.4; } .ax-band{ padding:14px; } .ax-band-head{ display:flex; justify-content:space-between; gap:10px; align-items:center; margin-bottom:12px; flex-wrap:wrap; } .ax-band-title{ font-size:13px; text-transform:uppercase; letter-spacing:.8px; font-weight:800; } .ax-band-note{ color:var(--muted); font-size:12px; } .ax-live-grid{ display:grid; grid-template-columns:repeat(7,minmax(0,1fr)); gap:10px; } .ax-live-box{ min-height:92px; padding:12px; border-radius:16px; cursor:pointer; transition:.18s ease; background:rgba(15,45,77,.66); border:1px solid rgba(31,124,255,.12); } .ax-live-box:hover{ transform:translateY(-2px); border-color:rgba(57,208,183,.32); } .ax-live-box.indoor{ background:rgba(57,208,183,.06); border-color:rgba(57,208,183,.16); } .ax-live-k{ color:var(--muted); font-size:11px; margin-bottom:8px; text-transform:uppercase; letter-spacing:.5px; } .ax-live-v{ font-size:20px; font-weight:800; line-height:1.15; } .ax-live-t{ color:var(--muted); font-size:11px; margin-top:8px; line-height:1.4; } .ax-section-head{ display:flex; justify-content:space-between; gap:10px; align-items:center; margin:18px 0 10px; flex-wrap:wrap; } .ax-section-title{ font-size:13px; text-transform:uppercase; letter-spacing:1px; font-weight:800; } .ax-section-sub{ color:var(--muted); font-size:12px; } .ax-grid{ display:grid; grid-template-columns:repeat(12,1fr); gap:14px; margin-bottom:14px; } .ax-span-3{grid-column:span 3} .ax-span-6{grid-column:span 6} .ax-span-12{grid-column:span 12} .ax-module-card{ min-height:126px; padding:18px; position:relative; transition:.18s ease; } .ax-module-card:hover{ transform:translateY(-2px); border-color:rgba(57,208,183,.35); box-shadow:0 12px 28px rgba(0,0,0,.25); } .ax-label{ color:var(--muted); font-size:14px; margin-bottom:10px; } .ax-value-row{ display:flex; justify-content:space-between; align-items:flex-end; gap:10px; } .ax-value{ font-size:28px; font-weight:800; line-height:1.1; } .ax-trend{ min-width:48px; text-align:right; font-size:22px; font-weight:800; color:var(--cyan); } .ax-sub{ color:var(--muted); font-size:13px; line-height:1.45; margin-top:10px; } .ax-main{ font-size:30px; font-weight:800; margin-bottom:8px; } .ax-risk-low{color:var(--blue)} .ax-risk-medium{color:var(--yellow)} .ax-risk-high{color:var(--red)} .ax-pills{ display:flex; flex-wrap:wrap; gap:10px; margin-top:6px; } .ax-pill{ padding:8px 12px; border-radius:999px; background:rgba(31,124,255,.08); border:1px solid rgba(31,124,255,.18); font-size:14px; } .ax-timeline{ padding:18px; min-height:310px; } .ax-timeline-head{ display:flex; justify-content:space-between; gap:10px; align-items:center; margin-bottom:10px; flex-wrap:wrap; } #ax-timeline-canvas{ width:100%; height:240px; display:block; } .ax-three{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:14px; margin-bottom:14px; } .ax-info-card{ padding:18px; } .ax-info-title{ font-size:20px; font-weight:800; margin-bottom:8px; } .ax-info-sub{ color:var(--muted); font-size:14px; line-height:1.5; margin-bottom:14px; } .ax-choice-row{ display:flex; gap:10px; flex-wrap:wrap; } .ax-tool-grid{ display:grid; grid-template-columns:repeat(8,1fr); gap:12px; margin-bottom:14px; } .ax-tool{ min-height:92px; padding:14px 10px; text-align:center; display:flex; flex-direction:column; justify-content:center; align-items:center; gap:8px; cursor:pointer; transition:.18s ease; } .ax-tool:hover{ transform:translateY(-2px); border-color:rgba(57,208,183,.35); } .ax-tool-ico{font-size:24px} .ax-tool-txt{font-size:12px;font-weight:800} .ax-footer{ margin-top:10px; padding:12px; border-radius:12px; text-align:center; font-size:13px; color:var(--muted); background:rgba(4,27,49,.60); } .ax-hidden{display:none !important} @media (max-width:1280px){ .ax-status-grid{grid-template-columns:repeat(3,1fr)} .ax-live-grid{grid-template-columns:repeat(4,minmax(0,1fr))} .ax-tool-grid{grid-template-columns:repeat(4,1fr)} } @media (max-width:1100px){ .ax-hero-wrap{grid-template-columns:1fr} .ax-hero-grid{grid-template-columns:1fr} .ax-hero-center{ border-left:none; border-right:none; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); padding:14px 0; } .ax-hero-right{text-align:left} .ax-three{grid-template-columns:1fr} .ax-span-3,.ax-span-6,.ax-span-12{grid-column:span 12} } @media (max-width:900px){ .ax-live-grid{grid-template-columns:repeat(2,minmax(0,1fr))} .ax-tool-grid{grid-template-columns:repeat(3,1fr)} .ax-status-grid{grid-template-columns:repeat(2,1fr)} } @media (max-width:720px){ .ax-shell{width:min(100% - 12px, 1480px)} .ax-header{flex-direction:column;align-items:stretch} .ax-header-right{align-items:flex-start;min-width:0} .ax-toolbar,.ax-timezones{justify-content:flex-start} .ax-brand-title{font-size:24px} .ax-hero-maintext{font-size:30px} .ax-hero-temp{font-size:46px} .ax-tool-grid{grid-template-columns:repeat(2,1fr)} } @media (max-width:520px){ .ax-status-grid{grid-template-columns:1fr} } /* AXNAP-000 CORE START */ (function(){ const storage = { get(key, fallback){ try{ const raw = localStorage.getItem(key); return raw === null ? fallback : JSON.parse(raw); }catch(_){ return fallback; } }, set(key, value){ localStorage.setItem(key, JSON.stringify(value)); } }; const state = { summary: null, timeline: [], modules: [], moduleMap: new Map(), categories: new Map(), themeMode: storage.get("axnap_theme_mode", "auto"), pressureUnit: storage.get("axnap_pressure_unit", "mmHg"), tempUnit: storage.get("axnap_temp_unit", "C"), windUnit: storage.get("axnap_wind_unit", "kmh"), rainUnit: storage.get("axnap_rain_unit", "mm"), currentLocation: storage.get("axnap_location", "Danube Camp"), forecastMode: storage.get("axnap_forecast_mode", "Standard"), hiddenModules: storage.get("axnap_hidden_modules", []), showHidden: false, showAll: false }; const zoneMap = { header: "ax-zone-header", toolbar: "ax-zone-toolbar", hero: "ax-zone-hero", status: "ax-zone-status", live: "ax-zone-live", dashboard: "ax-zone-dashboard", timeline: "ax-zone-timeline", ai: "ax-zone-ai", share: "ax-zone-share", locations: "ax-zone-locations", settings: "ax-zone-settings" }; function q(id){ return document.getElementById(id); } function isNum(v){ return typeof v === "number" && !isNaN(v); } function clamp(n,min,max){ return Math.max(min, Math.min(max, n)); } function cap(s){ if(!s) return "--"; s = String(s); return s.charAt(0).toUpperCase() + s.slice(1); } function hPaToMmHg(v){ return v * 0.750061683; } function hPaToInHg(v){ return v * 0.0295299830714; } function cToF(v){ return (v * 9 / 5) + 32; } function kmhToMs(v){ return v / 3.6; } function kmhToMph(v){ return v * 0.621371; } function mmToIn(v){ return v * 0.0393701; } function fmtPressure(v){ if(!isNum(v)) return "--"; if(state.pressureUnit === "mmHg") return `${hPaToMmHg(v).toFixed(1)} mmHg`; if(state.pressureUnit === "inHg") return `${hPaToInHg(v).toFixed(2)} inHg`; return `${v.toFixed(1)} hPa`; } function fmtPressureDelta(v){ if(!isNum(v)) return "Δ --"; if(state.pressureUnit === "mmHg") return `Δ ${hPaToMmHg(v).toFixed(1)} mmHg`; if(state.pressureUnit === "inHg") return `Δ ${hPaToInHg(v).toFixed(2)} inHg`; return `Δ ${v.toFixed(1)} hPa`; } function fmtTemp(v){ if(!isNum(v)) return "--"; if(state.tempUnit === "F") return `${cToF(v).toFixed(1)} °F`; return `${v.toFixed(1)} °C`; } function fmtTempBig(v){ if(!isNum(v)) return "--°"; if(state.tempUnit === "F") return `${Math.round(cToF(v))}°`; return `${Math.round(v)}°`; } function fmtWind(v){ if(!isNum(v)) return "--"; if(state.windUnit === "ms") return `${kmhToMs(v).toFixed(1)} m/s`; if(state.windUnit === "mph") return `${kmhToMph(v).toFixed(1)} mph`; return `${v.toFixed(1)} km/h`; } function fmtWindDelta(v){ if(!isNum(v)) return "Δ --"; if(state.windUnit === "ms") return `Δ ${kmhToMs(v).toFixed(1)} m/s`; if(state.windUnit === "mph") return `Δ ${kmhToMph(v).toFixed(1)} mph`; return `Δ ${v.toFixed(1)} km/h`; } function fmtRain(v){ if(!isNum(v)) return "--"; if(state.rainUnit === "in") return `${mmToIn(v).toFixed(2)} in`; return `${v.toFixed(1)} mm`; } function fmtHumidity(v){ return isNum(v) ? `${v.toFixed(1)} %` : "--"; } function fmtConfidence(v){ if(!isNum(v)) return "--"; if(v <= 1) return `${Math.round(v * 100)}%`; return `${Math.round(v)}%`; } function confidencePct(v){ if(!isNum(v)) return 0; return clamp(v <= 1 ? Math.round(v * 100) : Math.round(v), 0, 100); } function applyTheme(){ let theme = "dark"; if(state.themeMode === "light") theme = "light"; else if(state.themeMode === "dark") theme = "dark"; else{ const h = new Date().getHours(); theme = (h >= 7 && h < 19) ? "light" : "dark"; } document.documentElement.setAttribute("data-theme", theme); } function updateClocks(){ const now = new Date(); const targets = [ ["ax-time-local", null], ["ax-time-utc", "UTC"], ["ax-time-shenzhen", "Asia/Shanghai"], ["ax-time-toronto", "America/Toronto"] ]; targets.forEach(([id, tz])=>{ const el = q(id); if(!el) return; el.textContent = new Intl.DateTimeFormat('en-GB', { ...(tz ? { timeZone: tz } : {}), hour:'2-digit', minute:'2-digit', hour12:false }).format(now); }); } function pickWeatherIcon(now, forecast15){ const rain = now?.rain || 0; const humidity = now?.humidity || 0; const wind = now?.wind || 0; const text = String(forecast15 || "").toLowerCase(); if(rain > 0.2) return "🌧️"; if(text.includes("storm")) return "⛈️"; if(text.includes("rain")) return "🌦️"; if(humidity > 90) return "☁️"; if(wind > 20) return "🌬️"; return "⛅"; } function describePressure(trend){ const delta = trend?.pressure?.delta; const arrow = trend?.pressure?.arrow || "→"; if(arrow === "↓" && isNum(delta) && delta < -1.5) return "Pressure falling"; if(arrow === "↑" && isNum(delta) && delta > 1.5) return "Pressure rising"; if(arrow === "↓") return "Pressure slightly down"; if(arrow === "↑") return "Pressure slightly up"; return "Pressure stable"; } function describeWind(now, trend){ const wind = now?.wind || 0; const arrow = trend?.wind?.arrow || "→"; if(wind > 25) return "Strong wind"; if(wind > 15) return "Moderate wind"; if(arrow === "↑") return "Wind increasing"; if(arrow === "↓") return "Wind easing"; return "Light wind"; } function describeRain(now, forecast15){ const rain = now?.rain || 0; if(rain > 2) return "Heavy rain"; if(rain > 0.2) return "Active rain"; if(rain > 0) return "Light rain"; if(String(forecast15 || "").toLowerCase().includes("rain")) return "Rain possible"; return "No rain"; } function estimateCloudBuild(now, trend, forecast15){ const humidity = now?.humidity || 0; const pressureArrow = trend?.pressure?.arrow || "→"; const text = String(forecast15 || "").toLowerCase(); if(text.includes("storm")) return ["Storm build", "Rapid instability"]; if(text.includes("rain")) return ["Deteriorating", "Rain chance rising"]; if(humidity > 90 && pressureArrow === "↓") return ["Deteriorating", "Cloud build likely"]; if(humidity > 80) return ["Stable", "Humidity elevated"]; return ["Stable", "No major visual change"]; } function computeDewPoint(temp, humidity){ if(!isNum(temp) || !isNum(humidity)) return null; return +(temp - ((100 - humidity) / 5)).toFixed(1); } function pickLightning(summary){ const candidates = [ summary?.lightning, summary?.lightning_sensor, summary?.lightning_data, summary?.now?.lightning, summary?.now?.lightning_distance, summary?.now?.lightning_count ]; const found = candidates.find(v => v !== undefined && v !== null); if(found === undefined) return null; if(typeof found === "object"){ return { count: found.count ?? found.strikes ?? found.total ?? 0, distance: found.distance ?? found.km ?? found.last_distance ?? null }; } if(typeof found === "number"){ return { count: found, distance: null }; } return null; } function buildVerdict(now, forecast15, forecast1h, riskText, events, trend){ const rain = now?.rain || 0; const humidity = now?.humidity || 0; const wind = now?.wind || 0; const e = Array.isArray(events) ? events.map(x => String(x).toLowerCase()) : []; const pArrow = trend?.pressure?.arrow || "→"; if(rain > 0.2) return "Rain active"; if(String(riskText).toUpperCase() === "HIGH") return "High local instability"; if(e.some(x => x.includes("lightning"))) return "Lightning nearby"; if(pArrow === "↓" && wind > 15) return "Weather changing"; if(e.some(x => x.includes("rain"))) return "Rain risk"; if(humidity > 88) return "Dense cloud air"; if(forecast15 && forecast15 !== forecast1h) return cap(forecast15); return "Stable local conditions"; } function buildSubtitle(now, forecast15, forecast1h, trend){ const rain = now?.rain || 0; const pressureArrow = trend?.pressure?.arrow || "→"; const windArrow = trend?.wind?.arrow || "→"; const humidityArrow = trend?.humidity?.arrow || "→"; if(rain > 0.2) return "Local rain in progress"; if(pressureArrow === "↓" && windArrow === "↑") return "Pressure falling, wind increasing"; if(humidityArrow === "↑" && pressureArrow === "↓") return "Moisture rising, possible local change"; if(forecast1h && forecast1h !== forecast15) return cap(forecast1h); if(forecast15) return cap(forecast15); return "No rapid change detected"; } function buildNextChange(now, trend, forecast15, forecast1h){ const pressureArrow = trend?.pressure?.arrow || "→"; const windArrow = trend?.wind?.arrow || "→"; const humidityArrow = trend?.humidity?.arrow || "→"; const rain = now?.rain || 0; const f15 = String(forecast15 || "").toLowerCase(); const f1 = String(forecast1h || "").toLowerCase(); if(rain > 0.2) return ["Rain ongoing", "Local rain currently detected"]; if(f15.includes("storm")) return ["Storm risk", "Possible stronger instability ahead"]; if(f15.includes("rain")) return ["Rain possible", "Rain signal detected in short forecast"]; if(pressureArrow === "↓" && windArrow === "↑") return ["Wind increase", "Pressure drop suggests local change"]; if(humidityArrow === "↑") return ["Cloud build", "Humidity rising across local station"]; if(f1 && f1 !== f15) return [cap(f1), "Short-term local forecast evolution"]; return ["No rapid change", "Stable trend in current window"]; } function buildAI(now, trend, forecast15, lightning, confidence){ const lines = []; const rain = now?.rain || 0; const windArrow = trend?.wind?.arrow || "→"; const humidityArrow = trend?.humidity?.arrow || "→"; if(rain > 0.2) lines.push("Local rain detected"); else lines.push(describeRain(now, forecast15)); if(windArrow === "↑") lines.push("Wind is increasing"); else if(windArrow === "↓") lines.push("Wind is easing"); else lines.push(describeWind(now, trend)); if(lightning?.count > 0){ if(isNum(lightning.distance)) lines.push(`Lightning sensor reports ${lightning.distance} km`); else lines.push("Lightning activity detected"); } else if(humidityArrow === "↑"){ lines.push("Humidity trend supports cloud growth"); } else { lines.push("No local storm trigger detected"); } return [ lines[0] || "Local conditions stable", lines[1] || cap(forecast15 || "Stable"), `Confidence ${confidence}` ]; } function register(mod){ if(!mod || !mod.id || !mod.category) return; if(mod.replace && state.moduleMap.has(mod.id)){ const old = state.moduleMap.get(mod.id); const idx = state.modules.findIndex(x => x.id === old.id); if(idx >= 0) state.modules.splice(idx, 1); } state.modules.push(mod); state.moduleMap.set(mod.id, mod); if(!state.categories.has(mod.category)) state.categories.set(mod.category, []); state.categories.get(mod.category).push(mod); } function unregister(id){ state.modules = state.modules.filter(m => m.id !== id); state.moduleMap.delete(id); for(const [k, arr] of state.categories.entries()){ state.categories.set(k, arr.filter(m => m.id !== id)); } } function hideModule(id){ if(!state.hiddenModules.includes(id)) state.hiddenModules.push(id); storage.set("axnap_hidden_modules", state.hiddenModules); applyVisibility(); } function showModule(id){ state.hiddenModules = state.hiddenModules.filter(x => x !== id); storage.set("axnap_hidden_modules", state.hiddenModules); applyVisibility(); } function toggleHidden(){ state.showHidden = !state.showHidden; applyVisibility(); } function toggleAll(){ state.showAll = !state.showAll; applyVisibility(); } function applyVisibility(){ state.modules.forEach(mod => { const el = q(`ax-module-${mod.id}`); if(!el) return; const hidden = state.hiddenModules.includes(mod.id); el.classList.remove("ax-hidden"); el.style.opacity = "1"; if(state.showAll){ if(hidden) el.style.opacity = ".65"; return; } if(hidden && !state.showHidden){ el.classList.add("ax-hidden"); return; } if(hidden && state.showHidden){ el.style.opacity = ".65"; } }); } function render(){ Object.values(zoneMap).forEach(id => { const el = q(id); if(el) el.innerHTML = ""; }); state.modules.forEach(mod => { const zoneId = zoneMap[mod.category]; const zone = q(zoneId); if(!zone) return; const wrapper = document.createElement("div"); wrapper.id = `ax-module-${mod.id}`; wrapper.className = mod.wrapperClass || ""; wrapper.innerHTML = typeof mod.render === "function" ? mod.render(api) : ""; zone.appendChild(wrapper); if(typeof mod.init === "function"){ mod.init(api); } }); applyVisibility(); } function update(summary, timeline){ state.summary = summary || state.summary; state.timeline = timeline || state.timeline; state.modules.forEach(mod => { if(typeof mod.update === "function"){ mod.update(api, state.summary, state.timeline); } }); } async function loadData(){ try{ const [summaryRes, timelineRes] = await Promise.all([ fetch('/api/v1/summary', { cache: 'no-store' }), fetch('/api/v1/timeline', { cache: 'no-store' }) ]); if(!summaryRes.ok) throw new Error("summary http " + summaryRes.status); if(!timelineRes.ok) throw new Error("timeline http " + timelineRes.status); const summary = await summaryRes.json(); const timeline = await timelineRes.json(); update(summary, timeline); }catch(err){ console.error("AX NAP load failed", err); const el1 = q("ax-status-live"); const el2 = q("ax-status-update"); if(el1) el1.textContent = "WARNING"; if(el2) el2.textContent = "UI error / partial data"; } } const api = { state, storage, q, isNum, cap, register, unregister, hideModule, showModule, toggleHidden, toggleAll, render, update, loadData, applyTheme, updateClocks, fmtPressure, fmtPressureDelta, fmtTemp, fmtTempBig, fmtWind, fmtWindDelta, fmtRain, fmtHumidity, fmtConfidence, confidencePct, pickWeatherIcon, describePressure, describeWind, describeRain, estimateCloudBuild, computeDewPoint, pickLightning, buildVerdict, buildSubtitle, buildNextChange, buildAI }; window.AXNAP = api; applyTheme(); setInterval(updateClocks, 1000); })(); /* AXNAP-000 CORE END */ /* AXNAP-010 LOADER START */ (function(){ const modules = [ "./modules/hero.module.js", "./modules/status.module.js", "./modules/live.module.js", "./modules/dashboard.module.js", "./modules/timeline.module.js", "./modules/ai.module.js", "./modules/share.module.js", "./modules/locations.module.js" ]; function loadScript(src){ return new Promise((resolve, reject) => { const s = document.createElement("script"); s.src = src; s.onload = resolve; s.onerror = reject; document.body.appendChild(s); }); } async function boot(){ for(const src of modules){ await loadScript(src); } AXNAP.render(); AXNAP.updateClocks(); await AXNAP.loadData(); setInterval(()=>AXNAP.loadData(), 5000); } if(document.readyState === "loading"){ document.addEventListener("DOMContentLoaded", boot); }else{ boot(); } })(); /* AXNAP-010 LOADER END */ /* AXNAP-300 HERO START */ AXNAP.register({ id: "hero-main", category: "hero", render(api){ return `
Local situation
Waiting for data...
AX NAP is analyzing local conditions
--°
${api.state.currentLocation} / Local station
GPS lock, coordinates and altitude configurable
Pressure
Wind
Humidity
update --
Next change
--
No rapid change detected
Confidence
--
AI display confidence
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const risk = summary?.risk || {}; const forecast = summary?.forecast || {}; const events = Array.isArray(summary?.events) ? summary.events : []; const riskText = String(risk?.risk || "LOW").toUpperCase(); const verdict = api.buildVerdict(now, forecast?.forecast_15m, forecast?.forecast_1h, riskText, events, trend); const subtitle = api.buildSubtitle(now, forecast?.forecast_15m, forecast?.forecast_1h, trend); const [nextMain, nextSub] = api.buildNextChange(now, trend, forecast?.forecast_15m, forecast?.forecast_1h); const confidence = api.confidencePct(forecast?.confidence); const card = api.q("ax-hero-main-card"); if(riskText === "HIGH"){ card.style.background = "linear-gradient(135deg, rgba(255,92,108,.88), rgba(31,124,255,.20)), linear-gradient(180deg, #13365d, #0f2d4d)"; }else if(riskText === "MEDIUM"){ card.style.background = "linear-gradient(135deg, rgba(255,200,87,.84), rgba(31,124,255,.20)), linear-gradient(180deg, #13365d, #0f2d4d)"; }else{ card.style.background = "linear-gradient(135deg, rgba(182,225,77,.10), rgba(57,208,183,.12), rgba(31,124,255,.16)), linear-gradient(180deg, #13365d, #0f2d4d)"; } api.q("ax-hero-risk").textContent = riskText === "HIGH" ? "High risk" : riskText === "MEDIUM" ? "Medium risk" : "Low risk"; api.q("ax-hero-maintext").textContent = verdict; api.q("ax-hero-sub").textContent = subtitle; api.q("ax-hero-icon").textContent = api.pickWeatherIcon(now, forecast?.forecast_15m); api.q("ax-hero-temp").textContent = api.fmtTempBig(now?.temp); api.q("ax-hero-loc").textContent = `${api.state.currentLocation} / Local station`; api.q("ax-hero-trend-pressure").textContent = trend?.pressure?.arrow || "→"; api.q("ax-hero-trend-wind").textContent = trend?.wind?.arrow || "→"; api.q("ax-hero-trend-humidity").textContent = trend?.humidity?.arrow || "→"; api.q("ax-hero-update").textContent = `update ${now?.time || "--"}`; api.q("ax-next-main").textContent = nextMain; api.q("ax-next-sub").textContent = nextSub; api.q("ax-confidence-mini").textContent = api.fmtConfidence(forecast?.confidence); api.q("ax-confidence-fill").style.width = `${confidence}%`; } }); /* AXNAP-300 HERO END */ /* AXNAP-300 HERO START */ AXNAP.register({ id: "hero-main", category: "hero", render(api){ return `
Local situation
Waiting for data...
AX NAP is analyzing local conditions
--°
${api.state.currentLocation} / Local station
GPS lock, coordinates and altitude configurable
Pressure
Wind
Humidity
update --
Next change
--
No rapid change detected
Confidence
--
AI display confidence
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const risk = summary?.risk || {}; const forecast = summary?.forecast || {}; const events = Array.isArray(summary?.events) ? summary.events : []; const riskText = String(risk?.risk || "LOW").toUpperCase(); const verdict = api.buildVerdict(now, forecast?.forecast_15m, forecast?.forecast_1h, riskText, events, trend); const subtitle = api.buildSubtitle(now, forecast?.forecast_15m, forecast?.forecast_1h, trend); const [nextMain, nextSub] = api.buildNextChange(now, trend, forecast?.forecast_15m, forecast?.forecast_1h); const confidence = api.confidencePct(forecast?.confidence); const card = api.q("ax-hero-main-card"); if(riskText === "HIGH"){ card.style.background = "linear-gradient(135deg, rgba(255,92,108,.88), rgba(31,124,255,.20)), linear-gradient(180deg, #13365d, #0f2d4d)"; }else if(riskText === "MEDIUM"){ card.style.background = "linear-gradient(135deg, rgba(255,200,87,.84), rgba(31,124,255,.20)), linear-gradient(180deg, #13365d, #0f2d4d)"; }else{ card.style.background = "linear-gradient(135deg, rgba(182,225,77,.10), rgba(57,208,183,.12), rgba(31,124,255,.16)), linear-gradient(180deg, #13365d, #0f2d4d)"; } api.q("ax-hero-risk").textContent = riskText === "HIGH" ? "High risk" : riskText === "MEDIUM" ? "Medium risk" : "Low risk"; api.q("ax-hero-maintext").textContent = verdict; api.q("ax-hero-sub").textContent = subtitle; api.q("ax-hero-icon").textContent = api.pickWeatherIcon(now, forecast?.forecast_15m); api.q("ax-hero-temp").textContent = api.fmtTempBig(now?.temp); api.q("ax-hero-loc").textContent = `${api.state.currentLocation} / Local station`; api.q("ax-hero-trend-pressure").textContent = trend?.pressure?.arrow || "→"; api.q("ax-hero-trend-wind").textContent = trend?.wind?.arrow || "→"; api.q("ax-hero-trend-humidity").textContent = trend?.humidity?.arrow || "→"; api.q("ax-hero-update").textContent = `update ${now?.time || "--"}`; api.q("ax-next-main").textContent = nextMain; api.q("ax-next-sub").textContent = nextSub; api.q("ax-confidence-mini").textContent = api.fmtConfidence(forecast?.confidence); api.q("ax-confidence-fill").style.width = `${confidence}%`; } }); /* AXNAP-300 HERO END */ /* AXNAP-300 HERO START */ AXNAP.register({ id: "hero-main", category: "hero", render(api){ return `
Local situation
Waiting for data...
AX NAP is analyzing local conditions
--°
${api.state.currentLocation} / Local station
GPS lock, coordinates and altitude configurable
Pressure
Wind
Humidity
update --
Next change
--
No rapid change detected
Confidence
--
AI display confidence
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const risk = summary?.risk || {}; const forecast = summary?.forecast || {}; const events = Array.isArray(summary?.events) ? summary.events : []; const riskText = String(risk?.risk || "LOW").toUpperCase(); const verdict = api.buildVerdict(now, forecast?.forecast_15m, forecast?.forecast_1h, riskText, events, trend); const subtitle = api.buildSubtitle(now, forecast?.forecast_15m, forecast?.forecast_1h, trend); const [nextMain, nextSub] = api.buildNextChange(now, trend, forecast?.forecast_15m, forecast?.forecast_1h); const confidence = api.confidencePct(forecast?.confidence); const card = api.q("ax-hero-main-card"); if(riskText === "HIGH"){ card.style.background = "linear-gradient(135deg, rgba(255,92,108,.88), rgba(31,124,255,.20)), linear-gradient(180deg, #13365d, #0f2d4d)"; }else if(riskText === "MEDIUM"){ card.style.background = "linear-gradient(135deg, rgba(255,200,87,.84), rgba(31,124,255,.20)), linear-gradient(180deg, #13365d, #0f2d4d)"; }else{ card.style.background = "linear-gradient(135deg, rgba(182,225,77,.10), rgba(57,208,183,.12), rgba(31,124,255,.16)), linear-gradient(180deg, #13365d, #0f2d4d)"; } api.q("ax-hero-risk").textContent = riskText === "HIGH" ? "High risk" : riskText === "MEDIUM" ? "Medium risk" : "Low risk"; api.q("ax-hero-maintext").textContent = verdict; api.q("ax-hero-sub").textContent = subtitle; api.q("ax-hero-icon").textContent = api.pickWeatherIcon(now, forecast?.forecast_15m); api.q("ax-hero-temp").textContent = api.fmtTempBig(now?.temp); api.q("ax-hero-loc").textContent = `${api.state.currentLocation} / Local station`; api.q("ax-hero-trend-pressure").textContent = trend?.pressure?.arrow || "→"; api.q("ax-hero-trend-wind").textContent = trend?.wind?.arrow || "→"; api.q("ax-hero-trend-humidity").textContent = trend?.humidity?.arrow || "→"; api.q("ax-hero-update").textContent = `update ${now?.time || "--"}`; api.q("ax-next-main").textContent = nextMain; api.q("ax-next-sub").textContent = nextSub; api.q("ax-confidence-mini").textContent = api.fmtConfidence(forecast?.confidence); api.q("ax-confidence-fill").style.width = `${confidence}%`; } }); /* AXNAP-300 HERO END */ /* AXNAP-400 STATUS START */ AXNAP.register({ id: "header-main", category: "header", render(api){ return `

AX NAP

Nature • AI • Pulse
ONLINE
Updating...
Local
--:--
UTC
--:--
Shenzhen
--:--
Toronto
--:--
`; } }); AXNAP.register({ id: "status-grid", category: "status", render(api){ return `
Location
${api.state.currentLocation}
Current selected station view
Gateway
GW2000 online
Indoor + multi-sensor hub
Sensors
WS90 + WH57
Weather + lightning profile
Forecast mode
${api.state.forecastMode}
Upgrade with photos or quick answers
Share
Ready
Location / report / forecast share
`; }, update(api, summary){ const now = summary?.now || {}; api.q("ax-status-live").textContent = summary?.online === false ? "WARNING" : "ONLINE"; api.q("ax-status-update").textContent = now?.time ? `Last update: ${now.time}` : "No fresh station timestamp"; api.q("ax-status-location").textContent = api.state.currentLocation; api.q("ax-status-forecast-mode").textContent = api.state.forecastMode; } }); /* AXNAP-400 STATUS END */ /* AXNAP-500 LIVE START */ AXNAP.register({ id: "live-band", category: "live", render(){ return `
Live local band
Outdoor + indoor real-time state
OUT temp
--
Outdoor station
IN temp
--
Gateway indoor
OUT humidity
--
Δ --
IN humidity
--
Indoor climate
Wind
--
Δ --
Pressure
--
Δ --
Rain
--
Local rain state
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const indoorTemp = summary?.indoor?.temp ?? now?.indoor_temp ?? now?.inside_temp ?? now?.tempin ?? null; const indoorHumidity = summary?.indoor?.humidity ?? now?.indoor_humidity ?? now?.inside_humidity ?? now?.humidityin ?? null; api.q("ax-live-out-temp").textContent = api.fmtTemp(now?.temp); api.q("ax-live-in-temp").textContent = api.fmtTemp(indoorTemp); api.q("ax-live-out-hum").textContent = api.fmtHumidity(now?.humidity); api.q("ax-live-in-hum").textContent = api.fmtHumidity(indoorHumidity); api.q("ax-live-wind").textContent = api.fmtWind(now?.wind); api.q("ax-live-pressure").textContent = api.fmtPressure(now?.pressure); api.q("ax-live-rain").textContent = api.fmtRain(now?.rain); api.q("ax-live-out-hum-trend").textContent = api.isNum(trend?.humidity?.delta) ? `Δ ${trend.humidity.delta.toFixed(1)} %` : "Δ --"; api.q("ax-live-wind-trend").textContent = api.fmtWindDelta(trend?.wind?.delta); api.q("ax-live-pressure-trend").textContent = api.fmtPressureDelta(trend?.pressure?.delta); api.q("ax-live-rain-trend").textContent = api.isNum(now?.rain) && now.rain > 0 ? "Rain detected" : "Dry"; } }); /* AXNAP-500 LIVE END */ /* AXNAP-500 LIVE START */ AXNAP.register({ id: "live-band", category: "live", render(){ return `
Live local band
Outdoor + indoor real-time state
OUT temp
--
Outdoor station
IN temp
--
Gateway indoor
OUT humidity
--
Δ --
IN humidity
--
Indoor climate
Wind
--
Δ --
Pressure
--
Δ --
Rain
--
Local rain state
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const indoorTemp = summary?.indoor?.temp ?? now?.indoor_temp ?? now?.inside_temp ?? now?.tempin ?? null; const indoorHumidity = summary?.indoor?.humidity ?? now?.indoor_humidity ?? now?.inside_humidity ?? now?.humidityin ?? null; api.q("ax-live-out-temp").textContent = api.fmtTemp(now?.temp); api.q("ax-live-in-temp").textContent = api.fmtTemp(indoorTemp); api.q("ax-live-out-hum").textContent = api.fmtHumidity(now?.humidity); api.q("ax-live-in-hum").textContent = api.fmtHumidity(indoorHumidity); api.q("ax-live-wind").textContent = api.fmtWind(now?.wind); api.q("ax-live-pressure").textContent = api.fmtPressure(now?.pressure); api.q("ax-live-rain").textContent = api.fmtRain(now?.rain); api.q("ax-live-out-hum-trend").textContent = api.isNum(trend?.humidity?.delta) ? `Δ ${trend.humidity.delta.toFixed(1)} %` : "Δ --"; api.q("ax-live-wind-trend").textContent = api.fmtWindDelta(trend?.wind?.delta); api.q("ax-live-pressure-trend").textContent = api.fmtPressureDelta(trend?.pressure?.delta); api.q("ax-live-rain-trend").textContent = api.isNum(now?.rain) && now.rain > 0 ? "Rain detected" : "Dry"; } }); /* AXNAP-500 LIVE END *//* AXNAP-500 LIVE START */ AXNAP.register({ id: "live-band", category: "live", render(){ return `
Live local band
Outdoor + indoor real-time state
OUT temp
--
Outdoor station
IN temp
--
Gateway indoor
OUT humidity
--
Δ --
IN humidity
--
Indoor climate
Wind
--
Δ --
Pressure
--
Δ --
Rain
--
Local rain state
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const indoorTemp = summary?.indoor?.temp ?? now?.indoor_temp ?? now?.inside_temp ?? now?.tempin ?? null; const indoorHumidity = summary?.indoor?.humidity ?? now?.indoor_humidity ?? now?.inside_humidity ?? now?.humidityin ?? null; api.q("ax-live-out-temp").textContent = api.fmtTemp(now?.temp); api.q("ax-live-in-temp").textContent = api.fmtTemp(indoorTemp); api.q("ax-live-out-hum").textContent = api.fmtHumidity(now?.humidity); api.q("ax-live-in-hum").textContent = api.fmtHumidity(indoorHumidity); api.q("ax-live-wind").textContent = api.fmtWind(now?.wind); api.q("ax-live-pressure").textContent = api.fmtPressure(now?.pressure); api.q("ax-live-rain").textContent = api.fmtRain(now?.rain); api.q("ax-live-out-hum-trend").textContent = api.isNum(trend?.humidity?.delta) ? `Δ ${trend.humidity.delta.toFixed(1)} %` : "Δ --"; api.q("ax-live-wind-trend").textContent = api.fmtWindDelta(trend?.wind?.delta); api.q("ax-live-pressure-trend").textContent = api.fmtPressureDelta(trend?.pressure?.delta); api.q("ax-live-rain-trend").textContent = api.isNum(now?.rain) && now.rain > 0 ? "Rain detected" : "Dry"; } }); /* AXNAP-500 LIVE END */ /* AXNAP-600 DASHBOARD START */ AXNAP.register({ id: "dashboard-grid", category: "dashboard", render(){ return `
Dashboard
Minimal by default • add the rest from library
Wind
--
Δ --
Gust
--
Recent gust activity
Pressure
--
Δ --
Outdoor humidity
--
Δ --
Indoor temp
--
🏠
Indoor gateway sensor
Indoor humidity
--
🏠
Indoor climate
Dew point
--
💧
Calculated from temp / humidity
Cloud trend
--
☁️
Estimated local sky build
Forecast 15 min
--
Waiting for data...
Forecast 1h
--
Local trend adjusted
Risk
LOW
Risk score: --
Confidence
--
Forecast reliability
Lightning
No activity
Local lightning sensor / external input
Events
No significant events
Storm build
CALM score 0
Rain indicator
☁️ dry
No local rain detected
Sky Watch
Stable sky
Cloud movement: -- • Storm risk: low
History quick access
Now • 24h • 7d
Open detailed history, event context and long-term patterns
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const risk = summary?.risk || {}; const storm = summary?.storm || {}; const forecast = summary?.forecast || {}; const events = Array.isArray(summary?.events) ? summary.events : []; const lightning = api.pickLightning(summary); const indoorTemp = summary?.indoor?.temp ?? now?.indoor_temp ?? now?.inside_temp ?? now?.tempin ?? null; const indoorHumidity = summary?.indoor?.humidity ?? now?.indoor_humidity ?? now?.inside_humidity ?? now?.humidityin ?? null; const dewPoint = summary?.derived?.dew_point ?? now?.dew_point ?? api.computeDewPoint(now?.temp, now?.humidity); const [cloudValue, cloudSub] = api.estimateCloudBuild(now, trend, forecast?.forecast_15m); const riskText = String(risk?.risk || "LOW").toUpperCase(); api.q("ax-card-wind-value").textContent = api.fmtWind(now?.wind); api.q("ax-card-wind-trend").textContent = trend?.wind?.arrow || "→"; api.q("ax-card-wind-sub").textContent = api.fmtWindDelta(trend?.wind?.delta); api.q("ax-card-gust-value").textContent = api.fmtWind(now?.gust); api.q("ax-card-pressure-value").textContent = api.fmtPressure(now?.pressure); api.q("ax-card-pressure-trend").textContent = trend?.pressure?.arrow || "→"; api.q("ax-card-pressure-sub").textContent = api.fmtPressureDelta(trend?.pressure?.delta); api.q("ax-card-humidity-value").textContent = api.fmtHumidity(now?.humidity); api.q("ax-card-humidity-trend").textContent = trend?.humidity?.arrow || "→"; api.q("ax-card-humidity-sub").textContent = api.isNum(trend?.humidity?.delta) ? `Δ ${trend.humidity.delta.toFixed(1)} %` : "Δ --"; api.q("ax-card-indoor-temp-value").textContent = api.fmtTemp(indoorTemp); api.q("ax-card-indoor-humidity-value").textContent = api.fmtHumidity(indoorHumidity); api.q("ax-card-dew-value").textContent = api.isNum(dewPoint) ? api.fmtTemp(dewPoint) : "--"; api.q("ax-card-cloud-value").textContent = cloudValue; api.q("ax-card-cloud-sub").textContent = cloudSub; api.q("ax-card-forecast15-value").textContent = api.cap(forecast?.forecast_15m || "--"); api.q("ax-card-forecast15-sub").textContent = `1h: ${api.cap(forecast?.forecast_1h || "--")}`; api.q("ax-card-forecast1h-value").textContent = api.cap(forecast?.forecast_1h || "--"); const riskEl = api.q("ax-card-risk-value"); riskEl.textContent = riskText; riskEl.className = `ax-main ${riskText === "HIGH" ? "ax-risk-high" : riskText === "MEDIUM" ? "ax-risk-medium" : "ax-risk-low"}`; api.q("ax-card-risk-sub").textContent = `Risk score: ${risk?.score ?? 0}`; api.q("ax-card-confidence-value").textContent = api.fmtConfidence(forecast?.confidence); if(lightning?.count > 0){ api.q("ax-card-lightning-value").textContent = api.isNum(lightning.distance) ? `${lightning.count} strikes • ${lightning.distance} km` : `${lightning.count} strikes`; api.q("ax-card-lightning-sub").textContent = api.isNum(lightning.distance) ? "Lightning activity detected nearby" : "Lightning activity detected"; }else{ api.q("ax-card-lightning-value").textContent = "No activity"; api.q("ax-card-lightning-sub").textContent = "No recent lightning detected"; } const pills = api.q("ax-card-events-pills"); pills.innerHTML = ""; if(!events.length){ pills.innerHTML = `
No significant events
`; }else{ events.forEach(ev=>{ const d = document.createElement("div"); d.className = "ax-pill"; d.textContent = ev; pills.appendChild(d); }); } const level = Number(storm?.level || 0); const levels = [8,32,64,100]; let pct = levels[level] || 8; if(riskText === "HIGH" && pct < 64) pct = 64; if(riskText === "MEDIUM" && pct < 32) pct = 32; api.q("ax-card-storm-fill").style.width = `${pct}%`; api.q("ax-card-storm-label").textContent = storm?.label || "CALM"; api.q("ax-card-storm-score").textContent = `score ${storm?.score ?? 0}`; api.q("ax-card-rain-value").textContent = api.isNum(now?.rain) && now.rain > 2 ? "⛈ heavy" : api.isNum(now?.rain) && now.rain > 0.2 ? "🌧 moderate" : api.isNum(now?.rain) && now.rain > 0 ? "🌦 light" : "☁️ dry"; api.q("ax-card-rain-sub").textContent = api.isNum(now?.rain) && now.rain > 0 ? `${api.fmtRain(now.rain)} detected` : "No local rain detected"; api.q("ax-card-skywatch-value").textContent = `${cloudValue} sky`; api.q("ax-card-skywatch-sub").textContent = `Cloud movement: ${trend?.wind?.arrow || "→"} • Storm risk: ${riskText.toLowerCase()}`; } }); /* AXNAP-600 DASHBOARD END */ /* AXNAP-700 TIMELINE START */ AXNAP.register({ id: "timeline-main", category: "timeline", render(){ return `
Timeline
Wind / gust / pressure / lightning markers
`; }, update(api, summary, timeline){ const lightning = api.pickLightning(summary); const safe = Array.isArray(timeline) ? timeline : []; const canvas = api.q("ax-timeline-canvas"); if(!canvas) return; const ctx = canvas.getContext("2d"); const ratio = window.devicePixelRatio || 1; const width = canvas.clientWidth || 1200; const height = 240; canvas.width = Math.floor(width * ratio); canvas.height = Math.floor(height * ratio); ctx.setTransform(ratio, 0, 0, ratio, 0, 0); ctx.clearRect(0, 0, width, height); const pad = {top:16,right:16,bottom:28,left:16}; const innerW = width - pad.left - pad.right; const innerH = height - pad.top - pad.bottom; for(let i=0;i<5;i++){ const y = pad.top + (innerH / 4) * i; ctx.strokeStyle = "rgba(255,255,255,0.08)"; ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(width - pad.right, y); ctx.stroke(); } if(safe.length < 2){ ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.font = "14px Arial"; ctx.fillText("Not enough data for timeline", pad.left, height / 2); return; } const winds = safe.map(x => Number(x.wind || 0)); const gusts = safe.map(x => Number(x.gust || 0)); const pressures = safe.map(x => Number(x.pressure || 0)); const maxWind = Math.max(...winds, ...gusts, 1); const maxPressure = Math.max(...pressures); const minPressure = Math.min(...pressures); const pressureSpan = Math.max(maxPressure - minPressure, 0.8); const xAt = i => pad.left + (i / (safe.length - 1)) * innerW; const yWind = v => pad.top + innerH - ((v / Math.max(maxWind,1)) * innerH); const yPressure = v => pad.top + innerH - (((v - minPressure) / pressureSpan) * innerH); function drawLine(values, yFn, color){ ctx.beginPath(); values.forEach((v,i)=>{ const x = xAt(i); const y = yFn(v); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.strokeStyle = color; ctx.lineWidth = 2.2; ctx.stroke(); } drawLine(winds, yWind, "#3bd3ff"); drawLine(gusts, yWind, "#ffc857"); drawLine(pressures, yPressure, "#b6e14d"); if(lightning?.count > 0){ ctx.strokeStyle = "#b56cff"; ctx.lineWidth = 1.5; ctx.setLineDash([6,6]); const x = pad.left + innerW * 0.82; ctx.beginPath(); ctx.moveTo(x, pad.top); ctx.lineTo(x, height - pad.bottom); ctx.stroke(); ctx.setLineDash([]); } } }); window.addEventListener("resize", () => { if(AXNAP.state.summary){ const mod = AXNAP.state.moduleMap.get("timeline-main"); if(mod && mod.update) mod.update(AXNAP, AXNAP.state.summary, AXNAP.state.timeline); } }); /* AXNAP-700 TIMELINE END */ /* AXNAP-800 AI START */ AXNAP.register({ id: "ai-panels", category: "ai", render(){ return `
Improve Forecast
Standard forecast works without user input. For better accuracy, answer quick questions and optionally add sky photos with compass guidance.
Forecast feedback
Was yesterday forecast accurate? Optional learning input for the DB.
AI Analysis
Local weather intelligence from live trend + event interpretation.
Waiting for analysis...
Local weather intelligence pending
Confidence pending
`; }, update(api, summary){ const now = summary?.now || {}; const trend = summary?.trend || now?.trend || {}; const forecast = summary?.forecast || {}; const lightning = api.pickLightning(summary); const ai = api.buildAI(now, trend, forecast?.forecast_15m, lightning, api.fmtConfidence(forecast?.confidence)); api.q("ax-ai-line-1").textContent = ai[0]; api.q("ax-ai-line-2").textContent = ai[1]; api.q("ax-ai-line-3").textContent = ai[2]; } }); /* AXNAP-800 AI END */ /* AXNAP-900 SHARE START */ AXNAP.register({ id: "share-main", category: "share", render(){ return `
Shared report preview
The shared page focuses on the report itself, with subtle AX NAP branding.
Weather report • Danube Camp
Current state: waiting for data...
Update: --
Trend: --
Powered by AX NAP Open full view • Learn more • QR available
Tools / launcher
Touch-friendly quick access
📡
Radar
📷
Cameras
🛰️
Satellite
Lightning map
📈
History
🧭
Routes
📍
POI
📂
Reports
`; }, update(api, summary){ const now = summary?.now || {}; const forecast = summary?.forecast || {}; const trend = summary?.trend || now?.trend || {}; const events = Array.isArray(summary?.events) ? summary.events : []; const risk = summary?.risk || {}; const verdict = api.buildVerdict(now, forecast?.forecast_15m, forecast?.forecast_1h, String(risk?.risk || "LOW").toUpperCase(), events, trend); api.q("ax-share-preview").innerHTML = `${api.state.currentLocation}
` + `Condition: ${verdict}
` + `Temp: ${api.fmtTemp(now?.temp)} • Wind: ${api.fmtWind(now?.wind)} • Pressure: ${api.fmtPressure(now?.pressure)}
` + `Update: ${now?.time || "--"}
` + `Branding remains discreet.`; } }); /* AXNAP-900 SHARE END */ /* AXNAP-900 SHARE START */ AXNAP.register({ id: "share-main", category: "share", render(){ return `
Shared report preview
The shared page focuses on the report itself, with subtle AX NAP branding.
Weather report • Danube Camp
Current state: waiting for data...
Update: --
Trend: --
Powered by AX NAP Open full view • Learn more • QR available
Tools / launcher
Touch-friendly quick access
📡
Radar
📷
Cameras
🛰️
Satellite
Lightning map
📈
History
🧭
Routes
📍
POI
📂
Reports
`; }, update(api, summary){ const now = summary?.now || {}; const forecast = summary?.forecast || {}; const trend = summary?.trend || now?.trend || {}; const events = Array.isArray(summary?.events) ? summary.events : []; const risk = summary?.risk || {}; const verdict = api.buildVerdict(now, forecast?.forecast_15m, forecast?.forecast_1h, String(risk?.risk || "LOW").toUpperCase(), events, trend); api.q("ax-share-preview").innerHTML = `${api.state.currentLocation}
` + `Condition: ${verdict}
` + `Temp: ${api.fmtTemp(now?.temp)} • Wind: ${api.fmtWind(now?.wind)} • Pressure: ${api.fmtPressure(now?.pressure)}
` + `Update: ${now?.time || "--"}
` + `Branding remains discreet.`; } }); /* AXNAP-900 SHARE END */ /* AXNAP-900 SHARE START */ AXNAP.register({ id: "share-main", category: "share", render(){ return `
Shared report preview
The shared page focuses on the report itself, with subtle AX NAP branding.
Weather report • Danube Camp
Current state: waiting for data...
Update: --
Trend: --
Powered by AX NAP Open full view • Learn more • QR available
Tools / launcher
Touch-friendly quick access
📡
Radar
📷
Cameras
🛰️
Satellite
Lightning map
📈
History
🧭
Routes
📍
POI
📂
Reports
`; }, update(api, summary){ const now = summary?.now || {}; const forecast = summary?.forecast || {}; const trend = summary?.trend || now?.trend || {}; const events = Array.isArray(summary?.events) ? summary.events : []; const risk = summary?.risk || {}; const verdict = api.buildVerdict(now, forecast?.forecast_15m, forecast?.forecast_1h, String(risk?.risk || "LOW").toUpperCase(), events, trend); api.q("ax-share-preview").innerHTML = `${api.state.currentLocation}
` + `Condition: ${verdict}
` + `Temp: ${api.fmtTemp(now?.temp)} • Wind: ${api.fmtWind(now?.wind)} • Pressure: ${api.fmtPressure(now?.pressure)}
` + `Update: ${now?.time || "--"}
` + `Branding remains discreet.`; } }); /* AXNAP-900 SHARE END */ /* AXNAP-1000 LOCATIONS START */ AXNAP.register({ id: "locations-main", category: "locations", render(api){ return `
Locations
Current V1 model: manual multi-location selector. Route and POI logic can build on top of this without changing the core layout.
`; } }); /* AXNAP-1000 LOCATIONS END */