{"id":73,"date":"2019-06-12T22:31:58","date_gmt":"2019-06-12T22:31:58","guid":{"rendered":"https:\/\/www.tvcomm.co.uk\/g7izu\/?page_id=73"},"modified":"2026-03-31T20:45:37","modified_gmt":"2026-03-31T19:45:37","slug":"earth-geomagnetic-environment","status":"publish","type":"page","link":"https:\/\/www.tvcomm.co.uk\/g7izu\/welcome\/earth-geomagnetic-environment\/","title":{"rendered":"Earth Geomagnetic Environment"},"content":{"rendered":"\n<p>*** Changes to the data files supplied by <a href=\"http:\/\/Be aware that SWPC is decommissioning some of their solar wind JSON files effective 30 April. https:\/\/www.weather.gov\/media\/notification\/pdf_2026\/scn26-21_Data_Format_Changes_Impacting_SWPC_Products.pdf\" target=\"_blank\" rel=\"noreferrer noopener\">NOAA\/SWPC<\/a> may break the live displays on this page from approx. 31 March 2026. I will work to fix things as soon as possible. Additionally, the Solar Data Analysis Center (SDAC) and Space Physics Data Facility (SPDF) servers are physically moving location, impacting the collection and supply of SDO images since 8th March. <\/p>\n\n\n\n<div id=\"space-weather-indicators\" style=\"display:flex;gap:10px;justify-content:center;flex-wrap:wrap;font-size:1em;\">\n  <div class=\"indicator\" id=\"xrayBox\" role=\"region\" aria-label=\"X Ray flux\">\n    <h3>X-Rays<\/h3>\n    <div class=\"value\" id=\"xrayValue\">&#8212;<\/div>\n    <div class=\"desc\" id=\"xrayDesc\">Loading\u2026<\/div>\n  <\/div>\n\n  <div class=\"indicator\" id=\"rBox\" role=\"region\" aria-label=\"Radio Blackout scale\">\n    <h3>Radio Blackout<\/h3>\n    <div class=\"value\" id=\"rValue\">&#8212;<\/div>\n    <div class=\"desc\" id=\"rDesc\">Loading\u2026<\/div>\n  <\/div>\n\n  <div class=\"indicator\" id=\"electronBox\" role=\"region\" aria-label=\"Electron flux\">\n    <h3>Electrons<\/h3>\n    <div class=\"value\" id=\"electronValue\">&#8212;<\/div>\n    <div class=\"desc\" id=\"electronDesc\">Loading\u2026<\/div>\n  <\/div>\n\n  <div class=\"indicator\" id=\"srsBox\" role=\"region\" aria-label=\"Solar Radiation Storm scale\">\n    <h3>Solar Radiation<\/h3>\n    <div class=\"value\" id=\"srsValue\">&#8212;<\/div>\n    <div class=\"desc\" id=\"srsDesc\">Loading\u2026<\/div>\n  <\/div>\n\n  <div class=\"indicator\" id=\"kpBox\" role=\"region\" aria-label=\"Planetary K-index\">\n    <h3 id=\"kpTitle\">Kp Index<\/h3>\n    <div class=\"value\" id=\"kpValue\">&#8212;<\/div>\n    <div class=\"desc\" id=\"kpDesc\">Loading\u2026<\/div>\n  <\/div>\n\n  <div class=\"indicator\" id=\"kirunaBox\" role=\"region\" aria-label=\"Kiruna Substorm Index\">\n    <h3>Kiruna<\/h3>\n    <div class=\"value\" id=\"kirunaValue\">&#8212;<\/div>\n    <div class=\"desc\" id=\"kirunaDesc\">Substorm<\/div>\n  <\/div>\n<\/div>\n\n<style>\n  .indicator {\n    width:109px; \n    background:#111;\n    border-radius:10px;\n    text-align:center;\n    display:flex;\n    flex-direction:column;\n    justify-content:center;\n    align-items:center;\n    padding:6px 6px 8px 6px;\n    font-family:Arial, sans-serif;\n    box-shadow:0 0 8px rgba(0,0,0,0.5);\n    transition:background 0.35s, color 0.25s, box-shadow 0.35s;\n    min-height:60px;\n    font-size:0.75rem;\n    \/* ensure glow not clipped *\/\n    overflow: visible;\n  }\n  .indicator h3 { margin:2px 0 2px 0; font-size:11px; letter-spacing:0.2px; }\n  .indicator .value { font-size:21px; font-weight:bold; margin:2px 0; line-height:1; text-shadow:0 0 2px rgba(0,0,0,0.45); }\n  .indicator .desc { font-size:10px; margin-top:2px; opacity:0.95; }\n  #rBox .desc { font-size:10px; }\n\n  \/* Glow animation using CSS variable for color *\/\n  .alert-glow {\n    animation: glow 1s ease-in-out infinite alternate;\n    --glow-color: #ff0000;\n  }\n  @keyframes glow {\n    0% { box-shadow: 0 0 8px var(--glow-color, #ff0000), 0 0 12px rgba(255,0,0,0.3); }\n    100% { box-shadow: 0 0 16px var(--glow-color, #ff0000), 0 0 24px rgba(255,0,0,0.6); }\n  }\n\n  \/* Mobile friendly *\/\n  @media (max-width: 500px) {\n    .indicator { width:90px; font-size:0.5rem; }\n    .indicator .value { font-size:18px; }\n    .indicator h3 { font-size:9px; }\n    .indicator .desc { font-size:8px; }\n  }\n<\/style>\n\n<script>\n(function(){\n  const X_PRIMARY=\"https:\/\/www.tvcomm.co.uk\/g7izu\/files\/xrays-pri-1-day.json\";\n  const X_SECONDARY=\"https:\/\/www.tvcomm.co.uk\/g7izu\/files\/xrays-sec-1-day.json\";\n  const ELECTRON_FEED=\"https:\/\/www.tvcomm.co.uk\/g7izu\/files\/integral-electrons-1-day.json\";\n  const PROTON_FEED=\"https:\/\/www.tvcomm.co.uk\/g7izu\/files\/integral-protons-1-day.json\";\n  const KP_FEED=\"https:\/\/services.swpc.noaa.gov\/json\/planetary_k_index_1m.json\";\n  const KIRUNA_FEED=\"https:\/\/www.tvcomm.co.uk\/g7izu\/files\/rt_last_hour_1min_primary.csv\";\n  const ACE_EPAM_FEED=\"https:\/\/www.tvcomm.co.uk\/g7izu\/files\/ace-epam.txt\"; \/\/ New ACE EPAM data\n\n  const R_COLORS={R0:\"#2e7d32\",R1:\"#ffeb3b\",R2:\"#ff9800\",R3:\"#f44336\",R4:\"#b71c1c\",R5:\"#6a1b9a\"};\n  const X_COLORS={A:\"#1e88e5\",B:\"#43a047\",C:\"#ffeb3b\",M:\"#ef6c00\",X:\"#b71c1c\"};\n\n  \/\/ Electron color scale based on 38-53 keV flux (particles\/cm\u00b2-s-ster-MeV)\n  const ACE_E_COLORS = {\n    E0: \"#2e7d32\",      \/\/ < 1e3 - Quiet\n    E1: \"#1e88e5\",      \/\/ 1e3 - 1e4 - Elevated\n    E2: \"#FFAC1C\",      \/\/ 1e4 - 1e5 - Moderate\n    E3: \"#C04000\",      \/\/ 1e5 - 1e6 - Strong\n    E4: \"#c62828\",      \/\/ 1e6 - 1e7 - Severe\n    E5: \"#880e4f\"       \/\/ >= 1e7 - Extreme\n  };\n\n  \/\/ Proton color scale based on 47-68 keV flux\n  const ACE_P_COLORS = {\n    P0: \"#2e7d32\",      \/\/ < 1e4 - Quiet\n    P1: \"#1e88e5\",      \/\/ 1e4 - 1e5 - Elevated\n    P2: \"#FFAC1C\",      \/\/ 1e5 - 1e6 - Moderate\n    P3: \"#C04000\",      \/\/ 1e6 - 1e7 - Strong\n    P4: \"#c62828\",      \/\/ 1e7 - 1e8 - Severe\n    P5: \"#880e4f\"       \/\/ >= 1e8 - Extreme\n  };\n\n  function getContrastColor(hex){\n    if(!hex||hex[0]!=\"#\") return \"#fff\";\n    const c=hex.substring(1),rgb=parseInt(c,16);\n    const r=(rgb>>16)&0xff,g=(rgb>>8)&0xff,b=(rgb>>0)&0xff;\n    return (0.2126*r+0.7152*g+0.0722*b>160)?\"#000\":\"#fff\";\n  }\n\n  function animateValue(el,start,end,duration=500){\n    if(!el || isNaN(start) || isNaN(end)) return;\n    const range = end - start;\n    const stepTime = Math.abs(Math.floor(duration \/ (range || 1)));\n    let current = start;\n    const increment = end>start?1:-1;\n    const timer = setInterval(()=>{\n      current += increment;\n      el.textContent = current;\n      if(current === end) clearInterval(timer);\n    }, stepTime);\n  }\n\n  function setIndicator(id,value,color,desc){\n    const box=document.getElementById(id+\"Box\"); if(!box) return;\n    box.style.background=color||\"#111\";\n    box.style.color=getContrastColor(color||\"#111\");\n    const v=document.getElementById(id+\"Value\");\n    const d=document.getElementById(id+\"Desc\");\n    if(v) {\n      if(!isNaN(parseFloat(value))) animateValue(v,0,Math.round(parseFloat(value)));\n      else v.textContent=value||\"--\";\n    }\n    if(d) d.textContent=desc||\"\";\n\n    \/\/ Remove previous glow\n    box.classList.remove(\"alert-glow\");\n    box.style.setProperty('--glow-color','transparent');\n\n    \/\/ --- Modified glow conditions ---\n    let glowColor = null;\n    const descText = String(desc || \"\");\n    const valueText = String(value || \"\");\n\n    const moderateOrAbove = \/(moderate|strong|severe|extreme|major)\/i.test(descText) || \/(moderate|strong|severe|extreme|major)\/i.test(valueText);\n\n    let kpIsG3Plus = false;\n    if(id === \"kp\"){\n      const v = valueText.toUpperCase();\n      if(v.startsWith(\"G\")){\n        const n = parseInt(v.slice(1),10);\n        if(!isNaN(n) && n >= 3) kpIsG3Plus = true;\n      }\n      if(!kpIsG3Plus && \/Kp\\s*([7-9]|\\d{2,})\/i.test(descText)){\n        kpIsG3Plus = true;\n      }\n    }\n\n    if(moderateOrAbove || kpIsG3Plus){\n      glowColor = color || \"#ff0000\";\n      box.classList.add(\"alert-glow\");\n      box.style.setProperty('--glow-color',glowColor);\n    }\n  }\n\n  async function fetchJSON(urls){\n    for(const url of (Array.isArray(urls)?urls:[urls])){\n      try{ const r=await fetch(url,{cache:\"no-store\"}); if(r.ok) return await r.json(); }\n      catch(e){console.warn(\"fetch error\",url,e);}\n    }\n    return null;\n  }\n\n  \/\/ New function to parse ACE EPAM text data\n  async function parseACEEPAM() {\n    try {\n      const res = await fetch(ACE_EPAM_FEED, {cache: \"no-store\"});\n      const text = await res.text();\n      \n      \/\/ Find the data section (skip headers)\n      const lines = text.split('\\n').filter(line => \n        line.trim() && !line.startsWith('#') && !line.startsWith(':') && !line.includes('UT Date')\n      );\n      \n      if (lines.length === 0) return null;\n      \n      \/\/ Get the most recent data line (should be last line)\n      const lastLine = lines[lines.length - 1].trim();\n      const parts = lastLine.split(\/\\s+\/);\n      \n      \/\/ Parse the timestamp (columns 0-3: YR MO DA HHMM)\n      if (parts.length < 16) return null; \/\/ Need enough columns\n      \n      const year = parseInt(parts[0]);\n      const month = parseInt(parts[1]) - 1; \/\/ JS months are 0-indexed\n      const day = parseInt(parts[2]);\n      const timeStr = parts[3];\n      const hour = Math.floor(parseInt(timeStr) \/ 100);\n      const minute = parseInt(timeStr) % 100;\n      \n      const timestamp = new Date(year, month, day, hour, minute).getTime();\n      \n      \/\/ Parse electron fluxes (38-53 keV and 175-315 keV)\n      \/\/ Status columns are parts[4] and parts[7]\n      const electronStatus = parseInt(parts[4]);\n      const protonStatus = parseInt(parts[7]);\n      \n      if (electronStatus !== 0 || protonStatus !== 0) {\n        console.warn(\"ACE EPAM data has non-zero status flags\");\n        return null;\n      }\n      \n      \/\/ Parse fluxes - convert scientific notation to numbers\n      const electronFlux38_53 = parseFloat(parts[5]);   \/\/ 38-53 keV electrons\n      const electronFlux175_315 = parseFloat(parts[6]); \/\/ 175-315 keV electrons\n      const protonFlux47_68 = parseFloat(parts[8]);     \/\/ 47-68 keV protons\n      const protonFlux115_195 = parseFloat(parts[9]);   \/\/ 115-195 keV protons\n      const protonFlux310_580 = parseFloat(parts[10]);  \/\/ 310-580 keV protons\n      const protonFlux795_1193 = parseFloat(parts[11]); \/\/ 795-1193 keV protons\n      const protonFlux1060_1900 = parseFloat(parts[12]); \/\/ 1060-1900 keV protons\n      \n      \/\/ Check for missing data values\n      const missingValues = [-1.00e+05, -1.00];\n      const hasMissingData = [\n        electronFlux38_53, electronFlux175_315,\n        protonFlux47_68, protonFlux115_195, protonFlux310_580,\n        protonFlux795_1193, protonFlux1060_1900\n      ].some(val => missingValues.includes(val) || isNaN(val));\n      \n      if (hasMissingData) return null;\n      \n      return {\n        timestamp,\n        electrons: {\n          flux38_53: electronFlux38_53,\n          flux175_315: electronFlux175_315\n        },\n        protons: {\n          flux47_68: protonFlux47_68,\n          flux115_195: protonFlux115_195,\n          flux310_580: protonFlux310_580,\n          flux795_1193: protonFlux795_1193,\n          flux1060_1900: protonFlux1060_1900\n        },\n        anisotropy: parseFloat(parts[13]) \/\/ Last column\n      };\n      \n    } catch (error) {\n      console.warn(\"Error parsing ACE EPAM data:\", error);\n      return null;\n    }\n  }\n\n  \/\/ New function to classify ACE electron flux (38-53 keV)\n  function classifyACEElectron(flux) {\n    if (!flux || isNaN(flux) || flux <= 0) {\n      return {label: \"E0\", color: ACE_E_COLORS.E0, desc: \"No ACE data\"};\n    }\n    \n    \/\/ Convert to pfu-like scale (ACE fluxes are in particles\/cm\u00b2-s-ster-MeV)\n    \/\/ Rough scaling: 1e3 particles\/cm\u00b2-s-ster-MeV \u2248 1 pfu (very approximate)\n    const scaledFlux = flux;\n    \n    if (scaledFlux >= 1e7) return {label: \"E5\", color: ACE_E_COLORS.E5, desc: \"ACE: Extreme\"};\n    if (scaledFlux >= 1e6) return {label: \"E4\", color: ACE_E_COLORS.E4, desc: \"ACE: Severe\"};\n    if (scaledFlux >= 1e5) return {label: \"E3\", color: ACE_E_COLORS.E3, desc: \"ACE: Strong\"};\n    if (scaledFlux >= 1e4) return {label: \"E2\", color: ACE_E_COLORS.E2, desc: \"ACE: Moderate\"};\n    if (scaledFlux >= 1e3) return {label: \"E1\", color: ACE_E_COLORS.E1, desc: \"ACE: Elevated\"};\n    return {label: \"E0\", color: ACE_E_COLORS.E0, desc: \"ACE: Quiet\"};\n  }\n\n  \/\/ New function to classify ACE proton flux (47-68 keV as primary)\n  function classifyACEProton(flux) {\n    if (!flux || isNaN(flux) || flux <= 0) {\n      return {label: \"P0\", color: ACE_P_COLORS.P0, desc: \"No ACE data\"};\n    }\n    \n    \/\/ Proton scaling is different from electrons\n    if (flux >= 1e8) return {label: \"P5\", color: ACE_P_COLORS.P5, desc: \"ACE: Extreme\"};\n    if (flux >= 1e7) return {label: \"P4\", color: ACE_P_COLORS.P4, desc: \"ACE: Severe\"};\n    if (flux >= 1e6) return {label: \"P3\", color: ACE_P_COLORS.P3, desc: \"ACE: Strong\"};\n    if (flux >= 1e5) return {label: \"P2\", color: ACE_P_COLORS.P2, desc: \"ACE: Moderate\"};\n    if (flux >= 1e4) return {label: \"P1\", color: ACE_P_COLORS.P1, desc: \"ACE: Elevated\"};\n    return {label: \"P0\", color: ACE_P_COLORS.P0, desc: \"ACE: Quiet\"};\n  }\n\n  \/\/ Function to combine electron classifications from both sources\n  function combineElectronClassifications(integralData, aceData) {\n    \/\/ If both sources available, use the higher severity\n    if (!integralData && !aceData) {\n      return {label: \"--\", color: \"#111\", desc: \"No data\"};\n    }\n    \n    if (!aceData) {\n      \/\/ Only integral data available\n      return integralData;\n    }\n    \n    if (!integralData) {\n      \/\/ Only ACE data available\n      return aceData;\n    }\n    \n    \/\/ Both available - determine which is more severe\n    const severityOrder = {E0: 0, E1: 1, E2: 2, E3: 3, E4: 4, E5: 5};\n    const integralSeverity = severityOrder[integralData.label] || 0;\n    const aceSeverity = severityOrder[aceData.label] || 0;\n    \n    if (aceSeverity > integralSeverity) {\n      \/\/ ACE shows higher severity\n      return {\n        label: aceData.label,\n        color: aceData.color,\n        desc: integralData.desc + \" + \" + aceData.desc.split(\": \")[1]\n      };\n    } else {\n      \/\/ Integral shows higher or equal severity\n      return {\n        label: integralData.label,\n        color: integralData.color,\n        desc: integralData.desc + \" + ACE\"\n      };\n    }\n  }\n\n  \/\/ Function to combine proton classifications from both sources\n  function combineProtonClassifications(integralData, aceData) {\n    \/\/ If both sources available, use the higher severity\n    if (!integralData && !aceData) {\n      return {label: \"--\", color: \"#111\", desc: \"No data\"};\n    }\n    \n    if (!aceData) {\n      \/\/ Only integral data available\n      return integralData;\n    }\n    \n    if (!integralData) {\n      \/\/ Only ACE data available\n      return {\n        label: aceData.label,\n        color: aceData.color,\n        desc: \"ACE protons\"\n      };\n    }\n    \n    \/\/ Both available - determine which is more severe\n    const severityOrder = {S0: 0, S1: 1, S2: 2, S3: 3, S4: 4, S5: 5};\n    const integralSeverity = severityOrder[integralData.label] || 0;\n    const aceSeverity = aceData.label === \"P0\" ? 0 : \n                       aceData.label === \"P1\" ? 1 :\n                       aceData.label === \"P2\" ? 2 :\n                       aceData.label === \"P3\" ? 3 :\n                       aceData.label === \"P4\" ? 4 :\n                       aceData.label === \"P5\" ? 5 : 0;\n    \n    if (aceSeverity > 0 && aceSeverity >= integralSeverity) {\n      \/\/ ACE shows significant proton activity\n      return {\n        label: integralData.label,\n        color: integralData.color,\n        desc: integralData.desc + \" + ACE\"\n      };\n    } else {\n      \/\/ Integral shows higher or equal severity\n      return integralData;\n    }\n  }\n\nfunction classifyXray(flux){\n  if(!flux || isNaN(flux) || flux <= 0) \n    return {value:\"--\", color:\"#111\", desc:\"No data\"};\n\n  function roundDown1(x){\n    return Math.floor(x * 10) \/ 10;\n  }\n\n  let value=\"--\", color=\"#111\", desc=\"Background level\";\n\n  if (flux >= 2e-3) {\n    const level = roundDown1(flux \/ 2e-3);\n    value = `X${level.toFixed(1)}`;\n    color = X_COLORS.X;\n    desc = \"EXTREME\";\n    return {value, color, desc};\n  }\n  else if (flux >= 1e-3) {\n    const level = roundDown1(flux \/ 1e-3);\n    value = `X${level.toFixed(1)}`;\n    color = X_COLORS.M;\n    desc = \"SEVERE\";\n    return {value, color, desc};\n  }\n  else if (flux >= 1e-4) {\n    const level = roundDown1(flux \/ 1e-4);\n    value = `X${level.toFixed(1)}`;\n    color = X_COLORS.M;\n    desc = \"STRONG\";\n    return {value, color, desc};\n  }\n  else if (flux >= 5e-5) {\n    const level = roundDown1(flux \/ 1e-5);\n    value = `M${level.toFixed(1)}`;\n    color = X_COLORS.M;\n    desc = \"Moderate\";\n    return {value, color, desc};\n  }\n  else if (flux >= 1e-5) {\n    const level = roundDown1(flux \/ 1e-5);\n    value = `M${level.toFixed(1)}`;\n    color = X_COLORS.M;\n    desc = \"Active\";\n    return {value, color, desc};\n  }\n  else if (flux >= 1e-6) {\n    const level = roundDown1(flux \/ 1e-6);\n    value = `C${level.toFixed(1)}`;\n    color = X_COLORS.C;\n    desc = \"Low Active\";\n    return {value, color, desc};\n  }\n  else if (flux >= 1e-7) {\n    const level = roundDown1(flux \/ 1e-7);\n    value = `B${level.toFixed(1)}`;\n    color = X_COLORS.B;\n    desc = \"Quiet\";\n    return {value, color, desc};\n  }\n\n  return {value, color, desc};\n}\n\n  function deriveRFromX(flux){\n    let r=\"R0\",desc=\"Quiet\";\n    if(flux>=0.002){r=\"R5\";desc=\"Extreme\";}\n    else if(flux>=0.001){r=\"R4\";desc=\"Severe\";}\n    else if(flux>=0.0001){r=\"R3\";desc=\"Strong\";}\n    else if(flux>=0.00005){r=\"R2\";desc=\"Moderate\";}\n    else if(flux>=0.00001){r=\"R1\";desc=\"Minor\";}\n    const color=R_COLORS[r]||R_COLORS.R0;\n    return {r,color,desc};\n  }\n\n  function classifyElectron(f){\n    if(!f||isNaN(f)) return {label:\"E0\",color:\"#2e7d32\",desc:\"Background Level\"};\n    if(f>=1e7) return {label:\"E5\",color:\"#880e4f\",desc:\"EXTREME\"};\n    if(f>=1e6) return {label:\"E4\",color:\"#c62828\",desc:\"SEVERE\"};\n    if(f>=1e5) return {label:\"E3\",color:\"#C04000\",desc:\"STRONG\"};\n    if(f>=1e4) return {label:\"E2\",color:\"#FFAC1C\",desc:\"Moderate\"};\n    if(f>=1e3) return {label:\"E1\",color:\"#1e88e5\",desc:\"Minor\"};\n    if(f>=0.5e3) return {label:\"E0\",color:\"#26838C\",desc:\"Elevated (500-1k pfu)\"};\n    return {label:\"E0\",color:\"#2e7d32\",desc:\"Background Level\"};\n  }\n\n  function classifySRS(f){\n    if(!f||isNaN(f)) return {label:\"S0\",color:\"#2e7d32\",desc:\"Quiet\"};\n    if(f>=1e5) return {label:\"S5\",color:\"#880e4f\",desc:\"EXTREME\"};\n    if(f>=1e4) return {label:\"S4\",color:\"#d32f2f\",desc:\"SEVERE\"};\n    if(f>=1e3) return {label:\"S3\",color:\"#C04000\",desc:\"STRONG\"};\n    if(f>=1e2) return {label:\"S2\",color:\"#FFAC1C\",desc:\"Moderate\"};\n    if(f>=1e1) return {label:\"S1\",color:\"#1e88e5\",desc:\"Minor\"};\n    if(f>=0.5e1) return {label:\"S0\",color:\"#1e88e5\",desc:\"Elevated\"};\n    return {label:\"S0\",color:\"#2e7d32\",desc:\"Quiet\"};\n  }\n\n  async function updateAll(){\n    const twoHoursAgo = Date.now() - 2*3600*1000;\n    const sixHoursAgo = Date.now() - 6*3600*1000;\n\n    \/\/ Fetch ACE EPAM data first (will be used for electron\/proton comparisons)\n    const aceData = await parseACEEPAM();\n    const aceIsRecent = aceData && aceData.timestamp >= twoHoursAgo;\n    \n    \/\/ X-ray\n    const xdata = await fetchJSON([X_PRIMARY, X_SECONDARY]);\n    let latestFlux = null, latestTime = null;\n    if(xdata && xdata.length){\n      const parsed = xdata.map(d=>{\n        const t = new Date(d.time_tag||d.time).getTime();\n        return {flux:+d.flux||0, time: t};\n      }).filter(d=>!isNaN(d.time));\n      if(parsed.length){\n        const latestPoint = parsed.reduce((a,b)=>a.time>b.time?a:b);\n        latestFlux = latestPoint.flux;\n        latestTime = latestPoint.time;\n      }\n    }\n    if(latestFlux!==null && latestTime >= twoHoursAgo){\n      const xinfo=classifyXray(latestFlux);\n      setIndicator('xray',xinfo.value,xinfo.color,xinfo.desc);\n      const rinfo=deriveRFromX(latestFlux);\n      setIndicator('r',rinfo.r,rinfo.color,rinfo.desc+` (${xinfo.value})`);\n    }else{\n      setIndicator('xray',\"--\",\"#111\",\"No recent data\");\n      setIndicator('r',\"--\",\"#111\",\"No recent data\");\n    }\n\n    \/\/ Electron - now with ACE EPAM integration\n    const edata = await fetchJSON(ELECTRON_FEED);\n    let maxE = null, latestETime = null;\n    let integralElectronInfo = null;\n    \n    if(edata && edata.length){\n      const parsed = edata.map(d=>({flux:+d.flux||0, time:new Date(d.time_tag||d.time).getTime()})).filter(d=>!isNaN(d.time));\n      const recent = parsed.filter(d=>d.time>=twoHoursAgo);\n      if(recent.length){\n        maxE = Math.max(...recent.map(d=>d.flux));\n        latestETime = recent.reduce((a,b)=>a.time>b.time?a:b).time;\n        if(maxE !== null && latestETime >= twoHoursAgo){\n          integralElectronInfo = classifyElectron(maxE);\n        }\n      }\n    }\n    \n    \/\/ Get ACE electron classification\n    let aceElectronInfo = null;\n    if(aceIsRecent && aceData.electrons.flux38_53 > 0){\n      aceElectronInfo = classifyACEElectron(aceData.electrons.flux38_53);\n    }\n    \n    \/\/ Combine classifications\n    const combinedElectronInfo = combineElectronClassifications(integralElectronInfo, aceElectronInfo);\n    \n    if(combinedElectronInfo.label !== \"--\"){\n      setIndicator('electron', combinedElectronInfo.label, combinedElectronInfo.color, combinedElectronInfo.desc);\n    } else {\n      setIndicator('electron',\"--\",\"#111\",\"No recent data\");\n    }\n\n    \/\/ SRS\/Protons - now with ACE EPAM integration\n    const pdata = await fetchJSON(PROTON_FEED);\n    let maxP = null, latestPTime = null;\n    let integralProtonInfo = null;\n    \n    if(pdata && pdata.length){\n      const filtered = pdata.filter(d=>d.energy===\">=10 MeV\");\n      const parsed = filtered.map(d=>({flux:+d.flux||0, time:new Date(d.time_tag||d.time).getTime()})).filter(d=>!isNaN(d.time));\n      const recent = parsed.filter(d=>d.time>=twoHoursAgo);\n      if(recent.length){\n        maxP = Math.max(...recent.map(d=>d.flux));\n        latestPTime = recent.reduce((a,b)=>a.time>b.time?a:b).time;\n        if(maxP !== null && latestPTime >= twoHoursAgo){\n          integralProtonInfo = classifySRS(maxP);\n        }\n      }\n    }\n    \n    \/\/ Get ACE proton classification\n    let aceProtonInfo = null;\n    if(aceIsRecent && aceData.protons.flux47_68 > 0){\n      aceProtonInfo = classifyACEProton(aceData.protons.flux47_68);\n    }\n    \n    \/\/ Combine classifications\n    const combinedProtonInfo = combineProtonClassifications(integralProtonInfo, aceProtonInfo);\n    \n    if(combinedProtonInfo.label !== \"--\"){\n      setIndicator('srs', combinedProtonInfo.label, combinedProtonInfo.color, combinedProtonInfo.desc);\n    } else {\n      setIndicator('srs',\"--\",\"#111\",\"No recent data\");\n    }\n\n    \/\/ Kp - Using 1-minute feed (UPDATED)\n    const kpData = await fetchJSON(KP_FEED);\n    let latestKp = null;\n    let latestKpTime = null;\n    let latestKpIndex = null;\n    \n    if (kpData && kpData.length) {\n      \/\/ Get the most recent entry\n      const latest = kpData[kpData.length - 1];\n      latestKpTime = new Date(latest.time_tag).getTime();\n      latestKp = latest.kp;           \/\/ This is the G-scale value (e.g., \"1P\", \"2M\", \"0Z\")\n      latestKpIndex = latest.kp_index; \/\/ This is the integer Kp value (0-9)\n      \n      \/\/ Extract the G-scale number from the kp string (e.g., \"1P\" -> 1)\n      let gValue = parseInt(latestKp);\n      if (isNaN(gValue)) gValue = 0;\n      \n      \/\/ Determine G-scale and color\n      let gScale = \"G0\";\n      let color = \"#2e7d32\";\n      if (gValue >= 5) { gScale = \"G1\"; color = \"#ffeb3b\"; }\n      if (gValue >= 6) { gScale = \"G2\"; color = \"#ff9800\"; }\n      if (gValue >= 7) { gScale = \"G3\"; color = \"#f44336\"; }\n      if (gValue >= 8) { gScale = \"G4\"; color = \"#6a1b9a\"; }\n      if (gValue >= 9) { gScale = \"G5\"; color = \"#ab47bc\"; }\n      \n      \/\/ Update the title to show it's 1-minute data\n      document.getElementById(\"kpTitle\").innerText = `Geomag`;\n      \n      if (latestKpTime >= sixHoursAgo) {\n        \/\/ Display G-scale as main value, Kp index in brackets below\n        setIndicator('kp', `${gScale}`, color, `Kp ${latestKpIndex} (1 min data)`);\n      } else {\n        setIndicator('kp', \"--\", \"#111\", \"No recent data\");\n      }\n    } else {\n      setIndicator('kp', \"--\", \"#111\", \"No data\");\n    }\n\n    \/\/ Kiruna Substorm\n    try{\n      const res = await fetch(KIRUNA_FEED, {cache:\"no-store\"});\n      const text = await res.text();\n      const lines = text.split(\"\\n\").map(l=>l.trim()).filter(l=>l && !l.startsWith(\"DATE\"));\n      const recentLines = lines.slice(-60);\n      if(!recentLines.length) throw new Error(\"No lines\");\n      \n      const lastLine = recentLines[recentLines.length-1];\n      const lastParts = lastLine.split(\",\").map(s=>s.trim());\n      \n      let latestLineTime = null;\n      if(lastParts.length > 1){\n        const maybe = `${lastParts[0]} ${lastParts[1]}`;\n        const t = new Date(maybe).getTime();\n        if(!isNaN(t)) latestLineTime = t;\n      }\n      \n      if(!latestLineTime){\n        for(let i=recentLines.length-1;i>=0;i--){\n          const p = recentLines[i].split(\",\").map(s=>s.trim());\n          if(p.length>1){\n            const maybe = `${p[0]} ${p[1]}`;\n            const t = new Date(maybe).getTime();\n            if(!isNaN(t)){ latestLineTime = t; break; }\n          }\n        }\n      }\n\n      if(latestLineTime && latestLineTime < twoHoursAgo){\n        throw new Error(\"Kiruna data stale\");\n      }\n\n      let Hvals = [];\n      for(const line of recentLines){\n        const parts = line.split(\",\").map(s=>parseFloat(s));\n        if(parts[3]!==99999 && parts[4]!==99999 && parts[5]!==99999) Hvals.push({X:parts[3],Y:parts[4],Z:parts[5]});\n      }\n      if(!Hvals.length) throw new Error(\"No valid data\");\n      \n      const medianX = Hvals.map(v=>v.X).sort((a,b)=>a-b)[Math.floor(Hvals.length\/2)];\n      const medianY = Hvals.map(v=>v.Y).sort((a,b)=>a-b)[Math.floor(Hvals.length\/2)];\n      const medianZ = Hvals.map(v=>v.Z).sort((a,b)=>a-b)[Math.floor(Hvals.length\/2)];\n      const deltaH = Hvals.map(v=>Math.sqrt((v.X-medianX)**2 + (v.Y-medianY)**2 + (v.Z-medianZ)**2));\n      const maxDH = Math.max(...deltaH);\n\n      let label=\"Quiet\", color=\"#2e7d32\";\n      if(maxDH<50){label=\"Quiet\"; color=\"#2e7d32\";}\n      else if(maxDH<120){label=\"Minor Substorm\"; color=\"#ffeb3b\";}\n      else if(maxDH<210){label=\"Active Substorm\"; color=\"#ff9800\";}\n      else if(maxDH<360){label=\"Moderate Substorm\"; color=\"#f44336\";}\n      else if(maxDH<990){label=\"Strong Substorm\"; color=\"#6a1b9a\";}\n      else if(maxDH<1500){label=\"Major Substorm\"; color=\"#6a1b9a\";}\n      else {label=\"Severe Substorm\"; color=\"#880e4f\";}\n      setIndicator('kiruna',label,color,\"H-LAT 68\u00b0N 20\u00b0E\");\n    }catch(e){\n      setIndicator('kiruna',\"--\",\"#111\",\"No recent data\");\n    }\n  }\n\n  updateAll();\n  setInterval(updateAll,60000);\n\n  \/\/ ============================\n  \/\/ \ud83c\udfb5 Melodic Chime System (Moderate+ only)\n  \/\/ ============================\n  const audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n  let firstRun = true;\n  const previousValues = {};\n  let previousFlareClass = null;\n\n  function playChord(baseFreq) {\n    const now = audioCtx.currentTime;\n    const freqs = [baseFreq, baseFreq * 1.26, baseFreq * 1.5];\n    const duration = 1.6;\n    const volume = 0.15;\n\n    freqs.forEach(f => {\n      const osc = audioCtx.createOscillator();\n      const gain = audioCtx.createGain();\n      osc.type = \"sine\";\n      osc.frequency.value = f;\n      gain.gain.setValueAtTime(0.0001, now);\n      gain.gain.exponentialRampToValueAtTime(volume, now + 0.08);\n      gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);\n      osc.connect(gain).connect(audioCtx.destination);\n      osc.start(now);\n      osc.stop(now + duration + 0.05);\n    });\n  }\n\n  function freqScale(level) {\n    const lvl = level.toLowerCase();\n    if (\/moderate\/.test(lvl)) return 220;\n    if (\/strong\/.test(lvl)) return 600;\n    if (\/major\/.test(lvl)) return 950;\n    if (\/severe\/.test(lvl)) return 1200;\n    if (\/extreme\/.test(lvl)) return 1440;\n    return 0;\n  }\n\n  function detectChange(id, value, desc) {\n    if (firstRun) return false;\n    const prev = previousValues[id];\n    previousValues[id] = value;\n\n    if (id === \"xray\") {\n      const flareClass = (value && value[0]) || null;\n      if (flareClass && flareClass !== previousFlareClass) {\n        previousFlareClass = flareClass;\n        return true;\n      }\n      return false;\n    }\n\n    if (id === \"kp\") {\n      const prevNum = prev ? parseInt(prev.replace(\/\\D\/g, \"\"), 10) : null;\n      const currNum = value ? parseInt(value.replace(\/\\D\/g, \"\"), 10) : null;\n      return prevNum !== null && currNum !== null && prevNum !== currNum;\n    }\n\n    return prev && prev !== value;\n  }\n\n  const originalSetIndicator = setIndicator;\n  setIndicator = function(id, value, color, desc) {\n    originalSetIndicator(id, value, color, desc);\n\n    if (firstRun) return;\n\n    const text = (desc + \" \" + value).toLowerCase();\n    const isModeratePlus = \/(moderate|strong|severe|extreme|major)\/.test(text);\n\n    if (isModeratePlus && detectChange(id, value, desc)) {\n      const freq = freqScale(desc || value);\n      if (freq > 0) playChord(freq);\n    }\n  };\n\n  setTimeout(() => { firstRun = false; }, 8000);\n})();\n<\/script>\n\n\n\n<p>\u25b2 <strong>Warning Indicators &#8211; maximum values during the last hour (Kp 3 hours)<\/strong> \u266b<\/p>\n\n\n\n<!-- Space Weather Indicators: Density, Speed, Dynamic Pressure -->\n\n<style>\n  .sw-indicators { \n    display:flex; gap:12px; \n    line-height: 1.0;  \n    font-family:Arial,Helvetica,sans-serif; \n    align-items:stretch; \n  }\n  .sw-box {\n    width:260px;\n    padding:10px;\n    border-radius:10px;\n    background:#222;\n    color:#fff;\n    text-align:center;\n    box-shadow:0 0 8px rgba(0,0,0,0.35);\n    border:2px solid rgba(255,255,255,0.06);\n    transition: background-color 240ms ease, box-shadow 240ms ease, color 240ms ease;\n  }\n\n  .sw-box .role   { font-size:0.79em; opacity:0.9; }\n  .sw-box .title  { font-size:1.06em; font-weight:700; margin-top:4px; }\n  .sw-box .value  { font-size:1.52em; margin-top:8px; font-weight:700; }\n  .sw-box .label  { font-size:0.79em; margin-top:6px; opacity:0.95; }\n  .trend { margin-left:6px; font-size:0.82em; vertical-align:middle; }\n<\/style>\n\n<div id=\"sw-indicators\" class=\"sw-indicators\" aria-live=\"polite\">\n  <div id=\"densityBox\" class=\"sw-box\">\n    <div class=\"role\">Solar Wind<\/div>\n    <div class=\"title\">Density<\/div>\n    <div class=\"value\" id=\"densityValue\">&#8212;<\/div>\n    <div class=\"label\" id=\"densityLabel\">Loading\u2026<\/div>\n  <\/div>\n\n  <div id=\"speedBox\" class=\"sw-box\">\n    <div class=\"role\">Solar Wind<\/div>\n    <div class=\"title\">Speed<\/div>\n    <div class=\"value\" id=\"speedValue\">&#8212;<\/div>\n    <div class=\"label\" id=\"speedLabel\">Loading\u2026<\/div>\n  <\/div>\n\n  <div id=\"pressureBox\" class=\"sw-box\">\n    <div class=\"role\">Solar Wind<\/div>\n    <div class=\"title\">Dyn Pressure<\/div>\n    <div class=\"value\" id=\"pressureValue\">&#8212;<\/div>\n    <div class=\"label\" id=\"pressureLabel\">Loading\u2026<\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const DATA_URL = \"https:\/\/services.swpc.noaa.gov\/products\/solar-wind\/plasma-1-day.json\";\n  const UPDATE_INTERVAL_MS = 60000;\n  const TREND_LOOKBACK_SEC = 3600;\n\n  const LEVEL_COLORS = {\n    0:\"#0b6623\", 1:\"#0b6623\", 2:\"#d2bb07\",\n    3:\"#ff8c00\", 4:\"#d7262f\", 5:\"#6a0dad\"\n  };\n\n  const DENSITY_LABELS = [\"Very Low\",\"Low\",\"Moderate\",\"Elevated\",\"High\",\"Extreme\"];\n  const SPEED_LABELS   = [\"Calm\",\"Gentle\",\"Moderate\",\"Fast\",\"Very Fast\",\"Extreme\"];\n  const PRESSURE_LABELS= [\"Very Low\",\"Low\",\"Moderate\",\"Elevated\",\"High\",\"Extreme\"];\n\n  function contrastTextColor(hex){\n    const h=hex.replace(\"#\",\"\"), r=parseInt(h.substr(0,2),16),\n          g=parseInt(h.substr(2,2),16), b=parseInt(h.substr(4,2),16);\n    return (0.299*r+0.587*g+0.114*b) > 120 ? \"#000\" : \"#fff\";\n  }\n\n  const GLOW = [\n    \"0 0 11px rgba(11,102,35,0.55)\",\n    \"0 0 13px rgba(193,154,0,0.60)\",\n    \"0 0 14px rgba(255,212,0,0.65)\",\n    \"0 0 17px rgba(255,140,0,0.70)\",\n    \"0 0 19px rgba(215,38,47,0.75)\",\n    \"0 0 22px rgba(106,13,173,0.85)\"\n  ];\n\n  function applyColouring(boxId, lvl){\n    const box=document.getElementById(boxId);\n    const col=LEVEL_COLORS[lvl]; \n    const txt=contrastTextColor(col);\n    box.style.background=col;\n    box.style.color=txt;\n    box.style.boxShadow=GLOW[lvl];\n  }\n\n  function computeDynamicPressure_nPa(d, v){\n    const n=d*1e6, m=1.6726219e-27, rho=n*m, vel=v*1e3;\n    return 0.5*rho*vel*vel*1e9;\n  }\n\n  \/* Average of last N samples *\/\n  function averageLastN(rows, idx, n = 5) {\n    let sum = 0, count = 0;\n    for (let i = rows.length - 1; i >= 0 && count < n; i--) {\n      const v = parseFloat(rows[i][idx]);\n      if (isFinite(v)) { sum += v; count++; }\n    }\n    return count > 0 ? (sum \/ count) : null;\n  }\n\n  function computeAverage(rows, idx, cutoffMs){\n    let sum=0, count=0;\n    for (let i=rows.length-1; i>=0; i--){\n      const t=new Date(rows[i][0]).getTime();\n      if (t>=cutoffMs){\n        const v=parseFloat(rows[i][idx]);\n        if (isFinite(v)) { sum+=v; count++; }\n      } else break;\n    }\n    return count>0 ? (sum\/count) : null;\n  }\n\n  function trendGlyph(current, avg, deadband=0.05){\n    if (!isFinite(current) || !isFinite(avg)) return {glyph:\"\u25c4\u25ba\"};\n    const delta=(current-avg)\/avg;\n    if (delta>deadband) return {glyph:\"\u25b2\"};\n    if (delta<-deadband) return {glyph:\"\u25bc\"};\n    return {glyph:\"\u25c4\u25ba\"};\n  }\n\n  function densityLevel(x){ if(x<2)return 0;if(x<4)return 1;if(x<8)return 2;if(x<12)return 3;if(x<20)return 4;return 5; }\n  function speedLevel(x){ if(x<350)return 0;if(x<420)return 1;if(x<500)return 2;if(x<650)return 3;if(x<800)return 4;return 5; }\n  function pressureLevel(x){ if(x<0.5)return 0;if(x<1)return 1;if(x<2)return 2;if(x<4)return 3;if(x<8)return 4;return 5; }\n\n  async function updateIndicators(){\n    try{\n      const res=await fetch(DATA_URL,{cache:\"no-store\"});\n      const data=await res.json();\n      const header=data[0].map(h=>h.toLowerCase());\n      const rows=data.slice(1);\n\n      const iD=header.indexOf(\"density\"),\n            iS=header.indexOf(\"speed\");\n\n      const latest=rows[rows.length-1];\n      const tLatest=new Date(latest[0]).getTime();\n\n      \/* Use average of last 5 points *\/\n      const d = averageLastN(rows, iD, 5);\n      const v = averageLastN(rows, iS, 5);\n      const p = computeDynamicPressure_nPa(d, v);\n\n      \/* Average for last 15 minutes (unchanged) *\/\n      const cutoff=tLatest - TREND_LOOKBACK_SEC*1000;\n      const dAvg=computeAverage(rows,iD,cutoff);\n      const vAvg=computeAverage(rows,iS,cutoff);\n      const pAvg = dAvg!==null && vAvg!==null ? computeDynamicPressure_nPa(dAvg,vAvg) : null;\n\n      const dT=trendGlyph(d,dAvg).glyph,\n            vT=trendGlyph(v,vAvg).glyph,\n            pT=trendGlyph(p,pAvg).glyph;\n\n      densityValue.innerHTML  = d.toFixed(1)+\" \/cm\u00b3 <span class='trend'>\"+dT+\"<\/span>\";\n      speedValue.innerHTML    = v.toFixed(0)+\" km\/s <span class='trend'>\"+vT+\"<\/span>\";\n      pressureValue.innerHTML = p.toFixed(2)+\" nPa <span class='trend'>\"+pT+\"<\/span>\";\n\n      densityLabel.textContent  = DENSITY_LABELS[densityLevel(d)];\n      speedLabel.textContent    = SPEED_LABELS[speedLevel(v)];\n      pressureLabel.textContent = PRESSURE_LABELS[pressureLevel(p)];\n\n      applyColouring(\"densityBox\", densityLevel(d));\n      applyColouring(\"speedBox\",   speedLevel(v));\n      applyColouring(\"pressureBox\",pressureLevel(p));\n\n    }catch(e){ console.error(e); }\n  }\n\n  document.addEventListener(\"DOMContentLoaded\", ()=>{\n    updateIndicators();\n    setInterval(updateIndicators, UPDATE_INTERVAL_MS);\n  });\n\n})();\n<\/script>\n\n\n\n<p>\u25b2 <strong>Realtime Solar Wind at L1 (with 60min trend)<\/strong> <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div id=\"sw-graph-container\" style=\"width:100%; max-width:900px; margin:auto;\">\n  <canvas id=\"swDailyChart\" style=\"width:100%; height:350px;\"><\/canvas>\n<\/div>\n\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/luxon\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chartjs-adapter-luxon\"><\/script>\n\n<script>\nasync function loadSolarWind() {\n    try {\n        const url = \"https:\/\/services.swpc.noaa.gov\/products\/solar-wind\/plasma-1-day.json\";\n        const response = await fetch(url + \"?t=\" + Date.now());\n        const data = await response.json();\n\n        data.shift(); \/\/ remove header\n\n        let times = [];\n        let density = [];\n        let speed = [];\n\n        data.forEach(row => {\n            if(!row) return;\n            let t = row[0].replace(' ', 'T') + 'Z';\n            times.push(t);\n            density.push(row[1] !== null ? parseFloat(row[1]) : null);\n            speed.push(row[2] !== null ? parseFloat(row[2]) : null);\n        });\n\n        return { times, density, speed };\n    } catch (e) {\n        console.error(\"SWPC fetch error:\", e);\n        return null;\n    }\n}\n\nlet swChart = null;\n\nasync function drawSWChart() {\n    const sw = await loadSolarWind();\n    if (!sw) return;\n\n    const ctx = document.getElementById(\"swDailyChart\").getContext(\"2d\");\n\n    if (swChart) swChart.destroy();\n\n    \/\/ Dynamic maxes\n    const maxDensity = Math.max(Math.max(...sw.density.filter(v=>v!==null)), 20);\n    const maxSpeed   = Math.max(Math.max(...sw.speed.filter(v=>v!==null)), 500);\n\n    \/\/ -------------------------------------------------------------------\n    \/\/ ---- SUN \u2192 EARTH TRAVEL TIME CALCULATION (SMOOTHED LAST HOUR) ----\n    \/\/ -------------------------------------------------------------------\n\n    const now = Date.parse(sw.times[sw.times.length - 1]);\n    const oneHourAgo = now - 3600 * 1000;\n\n    \/\/ Collect last hour of available speed data\n    let recentSpeeds = [];\n    for (let i = sw.times.length - 1; i >= 0; i--) {\n        const t = Date.parse(sw.times[i]);\n        if (t < oneHourAgo) break;\n        if (sw.speed[i] !== null) recentSpeeds.push(sw.speed[i]);\n    }\n\n    \/\/ Average speed in the last hour\n    let smoothSpeed = null;\n    if (recentSpeeds.length > 0) {\n        smoothSpeed = recentSpeeds.reduce((a,b)=>a+b,0) \/ recentSpeeds.length;\n    }\n\n    \/\/ Compute travel time only if valid\n    let travelText = \"\";\n    if (smoothSpeed && smoothSpeed > 0) {\n        const AU_KM = 149597870; \/\/ 1 AU\n        const seconds = AU_KM \/ smoothSpeed;\n        const hours = seconds \/ 3600;\n\nlet days = Math.floor(hours \/ 24);\nlet remainingHours = Math.round(hours % 24);\n\n\/\/ Fix: prevent \"24 hours\" rollover\nif (remainingHours === 24) {\n    remainingHours = 0;\n    days += 1;\n}\n\ntravelText = ` |  Sun -> Earth Travel Time: ${days}d ${remainingHours}h`;\n\n    }\n\n    swChart = new Chart(ctx, {\n        type: \"line\",\n        data: {\n            labels: sw.times,\n            datasets: [\n                {\n                    label: \"Density (p\/cm\u00b3)\",\n                    data: sw.density,\n                    borderColor: \"#4aa3ff\",\n                    backgroundColor: \"rgba(74,163,255,0.2)\",\n                    yAxisID: \"yDensity\",\n                    pointRadius: 0,\n                    borderWidth: 1.5,\n                    tension: 0.2\n                },\n                {\n                    label: \"Speed (km\/s)\",\n                    data: sw.speed,\n                    borderColor: \"#ffcf40\",\n                    backgroundColor: \"rgba(255,207,64,0.2)\",\n                    yAxisID: \"ySpeed\",\n                    pointRadius: 0,\n                    borderWidth: 1.5,\n                    tension: 0.2\n                }\n            ]\n        },\n        options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            plugins: {\n                legend: {\n                    labels: { color: \"#ccc\" }\n                },\n                title: {\n                    display: true,\n                    text: \"Solar Wind Density & Speed (1-Day)  \" + travelText,\n                    color: \"#eee\",\n                    font: { size: 16 }\n                }\n            },\n            scales: {\n                x: {\n                    type: 'time',\n                    adapters: { date: { zone: 'utc' } },\n                    time: {\n                        unit: 'hour',\n                        stepSize: 1,\n                        displayFormats: { hour: 'HH:mm' }\n                    },\n                    ticks: { color: \"#bbb\" },\n                    grid: { color: \"rgba(255,255,255,0.05)\" }\n                },\n                yDensity: {\n                    type: \"linear\",\n                    position: \"left\",\n                    min: 0,\n                    max: maxDensity,\n                    ticks: { color: \"#4aa3ff\" },\n                    grid: { color: \"rgba(255,255,255,0.08)\" }\n                },\n                ySpeed: {\n                    type: \"linear\",\n                    position: \"right\",\n                    min: 0,\n                    max: maxSpeed,\n                    ticks: { color: \"#ffcf40\" },\n                    grid: { drawOnChartArea: false }\n                }\n            }\n        }\n    });\n}\n\ndrawSWChart();\nsetInterval(drawSWChart, 300000); \/\/ 5 minutes refresh\n<\/script>\n\n<style>\n#sw-graph-container {\n  background: #111;\n  padding: 10px;\n  border-radius: 8px;\n}\n<\/style>\n\n\n\n<p>\u25b2 <strong>ACE\/DISCOVR Solar Wind Density and Speed (Last 24 hours)<\/strong><br>**Gaps in the NASA data feeds are causing some issues with accuracy<\/p>\n\n\n\n<div id=\"bfield-container\" style=\"width:100%; max-width:900px; margin:auto;\">\n  <canvas id=\"bFieldChart\" style=\"width:100%; height:350px;\"><\/canvas>\n<\/div>\n\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/luxon\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chartjs-adapter-luxon\"><\/script>\n\n<script>\n(async function(){\n  const url = \"https:\/\/services.swpc.noaa.gov\/products\/solar-wind\/mag-1-day.json\";\n  let chart = null;\n\n  async function loadMag() {\n    try {\n      const response = await fetch(url + \"?t=\" + Date.now());\n      const data = await response.json();\n      data.shift();\n\n      let times = [], bx = [], by = [], bz = [], bt = [];\n\n      data.forEach(row => {\n        if(!row) return;\n        let t = row[0].replace(' ', 'T') + 'Z';\n        times.push(t);\n        bx.push(row[1] !== null ? parseFloat(row[1]) : null);\n        by.push(row[2] !== null ? parseFloat(row[2]) : null);\n        bz.push(row[3] !== null ? parseFloat(row[3]) : null);\n        bt.push(row[6] !== null ? parseFloat(row[6]) : null); \/\/ Bt is column 7 (index 6)\n      });\n\n      function smooth(arr, n = 5) {\n        return arr.map((v,i) => {\n          let chunk = arr.slice(Math.max(0,i-n+1), i+1).filter(x=>x!==null);\n          return chunk.length ? chunk.reduce((a,b)=>a+b,0)\/chunk.length : null;\n        });\n      }\n\n      return { times, bx: smooth(bx,5), by: smooth(by,5), bz: smooth(bz,5), bt: smooth(bt,5) };\n    } catch(e) {\n      console.error(\"MAG fetch error:\", e);\n      return null;\n    }\n  }\n\n  async function updateChart() {\n    const sw = await loadMag();\n    if(!sw) return;\n\n    const allValues = [...sw.bx, ...sw.by, ...sw.bz, ...sw.bt].filter(v => v!==null);\n    const minY = Math.min(-20, Math.min(...allValues));\n    const maxY = Math.max(20, Math.max(...allValues));\n\n    \/\/ ---- Bt CALCULATION ----\n    const lastBx = sw.bx[sw.bx.length - 1];\n    const lastBy = sw.by[sw.by.length - 1];\n    const lastBz = sw.bz[sw.bz.length - 1];\n    const Bt = Math.sqrt(lastBx*lastBx + lastBy*lastBy + lastBz*lastBz);\n\n    \/\/ ---- 1-HOUR TREND ARROW ----\n    const now = Date.parse(sw.times[sw.times.length - 1]);\n    const oneHourAgo = now - 3600 * 1000;\n\n    \/\/ find closest value to 1 hour ago\n    let index1h = sw.times.findIndex(t => Date.parse(t) >= oneHourAgo);\n    if (index1h < 0) index1h = 0;\n\n    const bx1 = sw.bx[index1h];\n    const by1 = sw.by[index1h];\n    const bz1 = sw.bz[index1h];\n    const Bt1 = Math.sqrt(bx1*bx1 + by1*by1 + bz1*bz1);\n\n    let trend = \"\";\n    if (Bt1 > 0) {\n      const change = (Bt - Bt1) \/ Bt1 * 100;\n      if (change > 5) trend = \" nT \u25b2\";\n      else if (change < -5) trend = \"nT \u25bc\";\n      else trend = \"nT\";\n    }\n\n    const BtText = Bt.toFixed(2) + trend;\n\n    const ctx = document.getElementById(\"bFieldChart\").getContext(\"2d\");\n\n    \/\/ If chart doesn't exist, create it\n    if (!chart) {\n      chart = new Chart(ctx, {\n        type: 'line',\n        data: {\n          labels: sw.times,\n          datasets: [\n            { label:\"Bx\", data: sw.bx, borderColor:\"#ff4444\", pointRadius:0, borderWidth:1.3, tension:0.2 },\n            { label:\"By\", data: sw.by, borderColor:\"#4488ff\", pointRadius:0, borderWidth:1.3, tension:0.2 },\n            { label:\"Bz\", data: sw.bz, borderColor:\"#ffff55\", pointRadius:0, borderWidth:1.3, tension:0.2 },\n            { label:\"Bt\", data: sw.bt, borderColor:\"#44ff44\", pointRadius:0, borderWidth:1.3, tension:0.2 }\n          ]\n        },\n        options: {\n          responsive:true,\n          maintainAspectRatio:false,\n          plugins: {\n            legend:{ labels:{ color:\"#ccc\" } },\n            title:{\n              display:true,\n              text:\"Interplanetary Magnetic Field \u2013 Bx \/ By \/ Bz \/ Bt (1 Day)   |   Latest IMF Total (Bt) : \" + BtText,\n              color:\"#eee\",\n              font:{ size:16 }\n            }\n          },\n          scales:{\n            x:{\n              type:'time',\n              adapters:{ date:{ zone:'utc' } },\n              time:{\n                unit:'hour',\n                stepSize:2,\n                displayFormats:{ hour:'HH:mm' }\n              },\n              ticks:{ color:\"#bbb\" },\n              grid:{ color:\"rgba(255,255,255,0.05)\" }\n            },\n            y:{\n              min:minY,\n              max:maxY,\n              ticks:{ color:\"#eee\" },\n              grid:{ color:\"rgba(255,255,255,0.1)\" },\n              title:{\n                display:true,\n                text:'Degrees',\n                color:'#eee',\n                font:{ size:14 },\n                align:'centre',\n                position:'start'\n              }\n            }\n          },\n          interaction:{ mode:'index', intersect:false }\n        }\n      });\n    } else {\n      \/\/ Update existing chart\n      chart.data.labels = sw.times;\n      chart.data.datasets[0].data = sw.bx;\n      chart.data.datasets[1].data = sw.by;\n      chart.data.datasets[2].data = sw.bz;\n      chart.data.datasets[3].data = sw.bt;\n      chart.options.scales.y.min = minY;\n      chart.options.scales.y.max = maxY;\n      chart.options.plugins.title.text = \"Interplanetary Magnetic Field \u2013 Bx \/ By \/ Bz \/ Bt (1 Day)   |   Latest IMF Total (Bt) : \" + BtText;\n      chart.update();\n    }\n  }\n\n  \/\/ Initial load\n  await updateChart();\n  \n  \/\/ Set up interval to update every minute (60000 ms)\n  setInterval(updateChart, 60000);\n\n})();\n<\/script>\n\n<style>\n#bfield-container {\n  background:#111;\n  padding:10px;\n  border-radius:8px;\n}\n<\/style>\n\n\n\n<style>\n  .alert-box {\n    border: 4px solid #6BADCE;\n    background: #FFFFFF;\n    border-radius: 12px;\n    padding: 16px 20px;\n    max-width: 720px;\n    margin: 8px auto;\n    font-family: 'Roboto', 'Segoe UI', sans-serif;\n    color: #000;\n    box-shadow: 0 6px 18px rgba(0,0,0,0.08);\n    transition: border-color 0.3s ease;\n  }\n\n  .alert-box h4 {\n    margin-top: 0;\n    text-align: center;\n  }\n\n  .alert-list {\n    list-style: none;\n    padding-left: 0;\n    margin: 6px 0 0 0;\n  }\n\n  .alert-item {\n    font-size: 1.05rem;\n    color: #000;\n  }\n\n  .alert-highlight {\n    font-weight: bold;\n    padding: 3px 5px;\n    border-radius: 4px;\n    font-size: 1.15rem;\n    display: inline-block;\n    margin-bottom: 4px;\n    background: transparent !important;\n  }\n\n  .alert-times {\n    font-size: 1.05rem;\n    margin-top: 2px;\n    color: #000;\n    display: block;\n  }\n<\/style>\n\n<div class=\"alert-box\" id=\"swAlerts\">\n  <h4 id=\"swAlertsTitle\">Space Weather Alerts (NOAA\/SWPC Last 12 Hours)<\/h4>\n  <ul class=\"alert-list\" id=\"alertList\">\n    <li>Loading alerts\u2026<\/li>\n  <\/ul>\n<\/div>\n\n<script>\n(function(){\n  const ALERT_URL = \"https:\/\/services.swpc.noaa.gov\/products\/alerts.json\";\n  const MS_1H = 60*60*1000;\n  const MS_1M = 60*1000;\n\n  const G_LEVEL_COLORS = {\n    \"G1\": \"#0b6623\",\n    \"G2\": \"#ffd400\",\n    \"G3\": \"#ff8c00\",\n    \"G4\": \"#d7262f\",\n    \"G5\": \"#6a0dad\"\n  };\n\n  const DEFAULT_BOX_COLOR = \"#6BADCE\";\n  const BRIGHTEN = 0.8;\n\n  let currentHours = 12; \n  const MAX_ALERTS = 6;\n  const MIN_HOURS = 3;\n  const MAX_HOURS = 12;\n\n  function brightenColor(hex, amt){\n    let c = hex.replace(\"#\",\"\");\n    let r = parseInt(c.substring(0,2),16);\n    let g = parseInt(c.substring(2,4),16);\n    let b = parseInt(c.substring(4,6),16);\n    r = Math.min(255, r + 255*amt);\n    g = Math.min(255, g + 255*amt);\n    b = Math.min(255, b + 255*amt);\n    return \"#\" +\n      Math.round(r).toString(16).padStart(2,\"0\") +\n      Math.round(g).toString(16).padStart(2,\"0\") +\n      Math.round(b).toString(16).padStart(2,\"0\");\n  }\n\n  function getContrastColor(bg){\n    let c = bg.replace(\"#\",\"\");\n    let r = parseInt(c.substring(0,2),16);\n    let g = parseInt(c.substring(2,4),16);\n    let b = parseInt(c.substring(4,6),16);\n    let yiq = (r*299 + g*587 + b*114) \/ 1000;\n    return yiq >= 128 ? \"#000\" : \"#fff\";\n  }\n\n  function formatIssueTime(issueDatetime){\n    if(!issueDatetime) return \"\";\n    const d = new Date(issueDatetime);\n    const y = d.getUTCFullYear();\n    const m = String(d.getUTCMonth()+1).padStart(2,\"0\");\n    const day = String(d.getUTCDate()).padStart(2,\"0\");\n    const h = String(d.getUTCHours()).padStart(2,\"0\");\n    const min = String(d.getUTCMinutes()).padStart(2,\"0\");\n    return `${y}-${m}-${day} ${h}:${min} UTC`;\n  }\n\n  \/* ============================\n     CORRECTED MESSAGE PARSER\n     ============================ *\/\n  function parseMessage(message, issueDatetime){\n    message = message.replace(\/\\r\\n\/g, \"\\n\");\n\n    \/* FIXED: Better regex to match the actual format in the JSON *\/\n    let lineMatch = message.match(\/^(CANCEL\\s+WATCH|WATCH|EXTENDED\\s+)?(CONTINUED\\s+ALERT|ALERT|WARNING|SUMMARY|CANCEL|CANCELLATION|CANCELLED):.*$\/im);\n    if (!lineMatch) {\n      \/* Try alternative pattern that matches the actual message structure *\/\n      lineMatch = message.match(\/(CANCEL\\s+WATCH|WATCH|ALERT|WARNING|SUMMARY|CANCEL|CANCELLATION|CANCELLED):[^\\n]*\/i);\n    }\n    \n    let mainLineText = lineMatch ? lineMatch[0].trim() : \"(No ALERT\/WARNING\/SUMMARY\/WATCH)\";\n\n    let forecastMatch = message.match(\/Forecast:\\s*([\\s\\S]*?)(?=\\n[A-Z][A-Za-z ]+?:|$)\/i);\n    let forecastText = forecastMatch ? forecastMatch[1].trim().replace(\/\\s+\/g,\" \") : \"\";\n\n    let commentMatch = message.match(\/Comment:\\s*(.*)\/i);\n    let commentText = commentMatch ? commentMatch[1].trim() : \"\";\n\n    let fromMatch = message.match(\/Valid From:\\s*(.*)\/i);\n    let toMatch = message.match(\/Valid To:\\s*(.*)\/i);\n    let validText = \"\";\n    if(fromMatch || toMatch){\n      validText = \" |\";\n      if(fromMatch) validText += ` Valid from ${fromMatch[1].trim()}`;\n      if(toMatch) validText += ` to ${toMatch[1].trim()}`;\n    }\n\n    \/* ---- NEW EXTRA FIELDS ---- *\/\n    let activeMatch = message.match(\/Active Warning:\\s*(.*)\/i);\n    let scaleFullMatch = message.match(\/NOAA Scale:\\s*(.*)\/i);\n    let thresholdMatch = message.match(\/Threshold Reached:\\s*(.*)\/i);\n\n    let extraBits = [];\n    if(activeMatch) extraBits.push(`Active Warning: ${activeMatch[1].trim()}`);\n    if(scaleFullMatch) extraBits.push(`${scaleFullMatch[1].trim()}`);\n    if(thresholdMatch) extraBits.push(`Threshold reached: ${thresholdMatch[1].trim()}`);\n\n    if(extraBits.length){\n      mainLineText += \" | \" + extraBits.join(\" | \");\n    }\n\n    let scaleKeyMatch = message.match(\/NOAA Scale:\\s*([A-Z0-9-]+)\/i);\n    let scale = scaleKeyMatch ? scaleKeyMatch[1].split(\" \")[0].trim() : \"\";\n\n    let itemBorderColor = DEFAULT_BOX_COLOR;\n    let itemBgColor = \"#f9f9f9\";\n    let highlightTextColor = \"#000\";\n\n    if(scale && G_LEVEL_COLORS[scale]){\n      itemBorderColor = G_LEVEL_COLORS[scale];\n      let baseBg = brightenColor(G_LEVEL_COLORS[scale], BRIGHTEN);\n      itemBgColor = brightenColor(G_LEVEL_COLORS[scale], BRIGHTEN + 0.05);\n      highlightTextColor = getContrastColor(baseBg);\n    }\n\n    if(\/CANCEL|CANCELLATION|CANCELLED\/i.test(mainLineText)){\n      itemBorderColor = \"#888\";\n      itemBgColor = \"#f0f0f0\";\n      highlightTextColor = \"#333\";\n    }\n\n    let html = `<span class=\"alert-highlight\" style=\"color:${highlightTextColor}\">${mainLineText}${validText}<\/span>`;\n    if(commentText) html += `<br><span class=\"alert-times\">Comment: ${commentText}<\/span>`;\n    if(forecastText) html += `<br><span class=\"alert-times\">Forecast: ${forecastText}<\/span>`;\n    html += `<br><span class=\"alert-times\">--- Issued: ${formatIssueTime(issueDatetime)}<\/span>`;\n\n    return { html, itemBorderColor, itemBgColor };\n  }\n\n  async function loadAlerts(){\n    const listEl = document.getElementById(\"alertList\");\n    const alertBox = document.getElementById(\"swAlerts\");\n    const titleEl = document.getElementById(\"swAlertsTitle\");\n\n    if (listEl.innerHTML.indexOf(\"Loading alerts\") === -1 && listEl.innerHTML.indexOf(\"Error\") === -1) {\n      listEl.insertAdjacentHTML('afterbegin',\n        '<li style=\"font-style: italic; font-size: 0.9em; margin-bottom: 5px;\">Refreshing data...<\/li>'\n      );\n    }\n\n    try{\n      const res = await fetch(ALERT_URL,{cache:\"no-store\"});\n      const data = await res.json();\n      const now = new Date();\n      let displayHours = currentHours;\n      let recent = [];\n\n      while(displayHours >= MIN_HOURS){\n        recent = data.filter(a => new Date(a.issue_datetime).getTime() >= now - displayHours*MS_1H);\n        if(recent.length <= MAX_ALERTS || displayHours === MIN_HOURS){\n          currentHours = displayHours;\n          titleEl.innerText = `Space Weather Alerts (NOAA\/SWPC Last ${currentHours} Hours)`;\n          break;\n        }\n        displayHours--;\n      }\n\n      if(currentHours < MAX_HOURS){\n        const recentMax = data.filter(a => new Date(a.issue_datetime).getTime() >= now - (currentHours+1)*MS_1H);\n        if(recentMax.length <= MAX_ALERTS) currentHours++;\n      }\n\n      renderAlerts(recent);\n\n    }catch(e){\n      console.error(e);\n      listEl.innerHTML = \"<li>Error loading alerts. Please wait for reload.<\/li>\";\n    }\n\n    function renderAlerts(recent){\n      if(recent.length === 0){\n        listEl.innerHTML = `<li>No warnings or alerts in the last ${currentHours} hours.<\/li>`;\n        alertBox.style.borderColor = DEFAULT_BOX_COLOR;\n        return;\n      }\n\n      let severeMessages = recent.filter(a => \/(EXTENDED\\s+)?(ALERT|WARNING)\/i.test(a.message));\n      let boxBorderColor = DEFAULT_BOX_COLOR;\n\n      if(severeMessages.length > 0){\n        let newest = severeMessages.reduce((a,b)=> new Date(a.issue_datetime)>new Date(b.issue_datetime)?a:b);\n        let sm = newest.message.match(\/NOAA Scale:\\s*([A-Z0-9-]+)\/i);\n        if(sm && G_LEVEL_COLORS[sm[1].trim()]){\n          boxBorderColor = G_LEVEL_COLORS[sm[1].trim()];\n        }\n      }\n\n      alertBox.style.borderColor = boxBorderColor;\n      listEl.innerHTML = \"\";\n\n      recent.forEach(a=>{\n        let li = document.createElement(\"li\");\n        li.className = \"alert-item\";\n        const parsed = parseMessage(a.message, a.issue_datetime);\n        li.style.cssText = `\n          margin: 10px 0;\n          padding: 8px 10px;\n          border-radius: 6px;\n          background: ${parsed.itemBgColor};\n          border-left: 6px solid ${parsed.itemBorderColor};\n          box-shadow: 0 1px 3px rgba(0,0,0,0.05);\n        `;\n        li.innerHTML = parsed.html;\n        listEl.appendChild(li);\n      });\n    }\n  }\n\n  document.addEventListener(\"DOMContentLoaded\", ()=>{\n    loadAlerts();\n    setInterval(loadAlerts, MS_1M);\n  });\n})();\n<\/script>\n\n\n\n<p>\u25b2 <strong>ACE\/DISCOVR Solar Wind<\/strong> <strong>Bx, By and Bz Components (last 24 hours)<\/strong><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"509\" height=\"289\" src=\"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-content\/uploads\/2025\/12\/solarwindmag.jpg\" alt=\"\" class=\"wp-image-3233\" srcset=\"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-content\/uploads\/2025\/12\/solarwindmag.jpg 509w, https:\/\/www.tvcomm.co.uk\/g7izu\/wp-content\/uploads\/2025\/12\/solarwindmag-300x170.jpg 300w\" sizes=\"auto, (max-width: 509px) 100vw, 509px\" \/><\/figure>\n<\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"http:\/\/www2.irf.se\/maggraphs\/maglinux_xyz_abs.png\" alt=\"\"\/><figcaption class=\"wp-element-caption\">\u25b2 SKN Kiruna Magnetogram (<a href=\"https:\/\/www.tgo.uit.no\/\" target=\"_blank\" rel=\"noreferrer noopener\">last 24 hours<\/a>)<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/www.tvcomm.co.uk\/g7izu\/Autosave\/drap-latest.png\" alt=\"https:\/\/services.swpc.noaa.gov\/images\/animations\/d-rap\/global\/d-rap\/latest.png?\"\/><\/figure>\n\n\n\n<p>\u25b2 The D-Region Absorption Product addresses the operational impact of the solar X-ray flux and SEP events on HF radio communication. Long-range communications using high frequency (HF) radio waves (3 \u2013 30 MHz) depend on reflection of the signals in the ionosphere. Radio waves are typically reflected near the peak of the F2 layer (~300 km altitude), but along the path to the F2 peak and back the radio wave signal suffers attenuation due to absorption by the intervening ionosphere. The model is used as guidance to understand the HF radio degradation and blackouts this can cause. The&nbsp;<a href=\"https:\/\/www.swpc.noaa.gov\/products\/d-region-absorption-predictions-d-rap\" target=\"_blank\" rel=\"noreferrer noopener\">SWPC page<\/a>&nbsp;includes an animated version of the map.<\/p>\n\n\n\n<div style=\"text-align: center; background: #111; padding: 10px; border-radius: 6px;\">\n  <h3 style=\"color: white; margin-bottom: 10px;\">GOES Magnetometers (Last 6 Hours)<\/h3>\n  <div style=\"width: 100%; max-width: 550px; margin: auto; background: #222; border-radius: 5px; padding: 5px;\">\n    <canvas class=\"magChartCanvas\" width=\"560\" height=\"350\"><\/canvas>\n  <\/div>\n<\/div>\n\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/luxon\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chartjs-adapter-luxon\"><\/script>\n<script>\n(function(){\n  \/\/ generate unique ID for this chart\n  const canvas = document.querySelector('.magChartCanvas');\n  const chartId = 'magChart_' + Math.random().toString(36).substr(2, 9);\n  canvas.id = chartId;\n\n  let chartInstance;\n\n  async function fetchMagnetometerData() {\n    try {\n      const [primaryResponse, secondaryResponse] = await Promise.all([\n        fetch('https:\/\/services.swpc.noaa.gov\/json\/goes\/primary\/magnetometers-1-day.json'),\n        fetch('https:\/\/services.swpc.noaa.gov\/json\/goes\/secondary\/magnetometers-1-day.json')\n      ]);\n\n      const primaryData = await primaryResponse.json();   \/\/ GOES-19\n      const secondaryData = await secondaryResponse.json(); \/\/ GOES-18\n\n      const now = new Date();\n      const sixHoursAgo = new Date(now.getTime() - 6 * 60 * 60 * 1000);\n\n      const sat19 = primaryData.filter(d => d.satellite === 19 && new Date(d.time_tag) >= sixHoursAgo);\n      const sat18 = secondaryData.filter(d => d.satellite === 18 && new Date(d.time_tag) >= sixHoursAgo);\n\n      const timeSet = new Set();\n      sat18.forEach(d => timeSet.add(d.time_tag));\n      sat19.forEach(d => timeSet.add(d.time_tag));\n      const timeLabels = Array.from(timeSet).sort();\n\n      const map18 = new Map(sat18.map(d => [d.time_tag, d.Hp]));\n      const map19 = new Map(sat19.map(d => [d.time_tag, d.Hp]));\n\n      const hp18 = timeLabels.map(t => map18.get(t) ?? null);\n      const hp19 = timeLabels.map(t => map19.get(t) ?? null);\n\n      updateChart(timeLabels, hp19, hp18);\n    } catch (error) {\n      console.error('Error fetching magnetometer data:', error);\n    }\n  }\n\n  function calculateYAxisMax(data19, data18) {\n    const allValues = [...data19, ...data18].filter(v => v !== null);\n    const maxValue = Math.max(...allValues);\n    return Math.max(Math.ceil(maxValue * 1.1), 200);\n  }\n\n  function createChart(labels, data19, data18) {\n    const ctx = document.getElementById(chartId).getContext('2d');\n    const yMax = calculateYAxisMax(data19, data18);\n\n    chartInstance = new Chart(ctx, {\n      type: 'line',\n      data: {\n        labels: labels,\n        datasets: [\n          {\n            label: 'GOES-19',\n            data: data19,\n            borderColor: 'cyan',\n            backgroundColor: 'rgba(0, 255, 255, 0.2)',\n            borderWidth: 1.5,\n            pointRadius: 0,\n            tension: 0.1\n          },\n          {\n            label: 'GOES-18',\n            data: data18,\n            borderColor: 'red',\n            backgroundColor: 'rgba(255, 0, 0, 0.2)',\n            borderWidth: 1.5,\n            pointRadius: 0,\n            tension: 0.1\n          }\n        ]\n      },\n      options: {\n        responsive: true,\n        maintainAspectRatio: false,\n        scales: {\n          x: {\n            type: 'time',\n            time: {\n              unit: 'hour',\n              tooltipFormat: 'HH:mm',\n              displayFormats: { hour: 'HH:mm' }\n            },\n            ticks: { color: 'white', font: { size: 10 } },\n            grid: { color: '#444' }\n          },\n          y: {\n            min: 0,\n            max: yMax,\n            title: {\n              display: true,\n              text: 'nT',\n              color: 'white'\n            },\n            ticks: { color: 'white', font: { size: 10 } },\n            grid: { color: '#444' }\n          }\n        },\n        plugins: {\n          legend: {\n            labels: {\n              color: 'white',\n              font: { size: 11 },\n              padding: 8\n            }\n          }\n        }\n      }\n    });\n  }\n\n  function updateChart(labels, data19, data18) {\n    const yMax = calculateYAxisMax(data19, data18);\n\n    if (!chartInstance) {\n      createChart(labels, data19, data18);\n    } else {\n      chartInstance.data.labels = labels;\n      chartInstance.data.datasets[0].data = data19;\n      chartInstance.data.datasets[1].data = data18;\n      chartInstance.options.scales.y.max = yMax;\n      chartInstance.update();\n    }\n  }\n\n  fetchMagnetometerData();\n  setInterval(fetchMagnetometerData, 60000);\n})();\n<\/script>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div style=\"text-align:center; max-width:800px; margin:auto;\">\n    <img id=\"enlil-frame\" style=\"width:100%; height:auto; border:1px solid #333;\" alt=\"ENLIL animation frame\"\/>\n    \n    <!-- Controls -->\n    <div style=\"margin-top:8px; font-family:Arial, sans-serif;\">\n        <button id=\"stepBackBtn\" style=\"padding:4px 8px; font-size:12px;\">\u2039<\/button>\n        <button id=\"playBtn\"     style=\"padding:4px 8px; font-size:12px;\">\u25b6<\/button>\n        <button id=\"pauseBtn\"    style=\"padding:4px 8px; font-size:12px;\">\u23f8<\/button>\n        <button id=\"stepFwdBtn\"  style=\"padding:4px 8px; font-size:12px;\">\u203a<\/button>\n    <\/div>\n<\/div>\n\n<script>\n(async () => {\n    const jsonUrl = \"https:\/\/services.swpc.noaa.gov\/products\/animations\/enlil.json\";\n    const baseUrl = \"https:\/\/services.swpc.noaa.gov\";\n\n    let frames = [];\n    let index = 0;\n    let timer = null;\n    const frameDelay = 150; \/\/ ms \u2013 adjust for speed\n\n    const imgEl = document.getElementById(\"enlil-frame\");\n    const playBtn = document.getElementById(\"playBtn\");\n    const pauseBtn = document.getElementById(\"pauseBtn\");\n    const stepBackBtn = document.getElementById(\"stepBackBtn\");\n    const stepFwdBtn = document.getElementById(\"stepFwdBtn\");\n\n    try {\n        const res = await fetch(jsonUrl);\n        const data = await res.json();\n\n        \/\/ Convert JSON list \u2192 array of full URLs\n        frames = data.map(f => baseUrl + f.url);\n\n        \/\/ Preload images\n        frames.forEach(url => { const i = new Image(); i.src = url; });\n    } catch (e) {\n        console.error(\"Failed to load ENLIL JSON\", e);\n        return;\n    }\n\n    function showFrame() {\n        imgEl.src = frames[index];\n    }\n\n    function nextFrame() {\n        index = (index + 1) % frames.length;\n        showFrame();\n    }\n\n    function prevFrame() {\n        index = (index - 1 + frames.length) % frames.length;\n        showFrame();\n    }\n\n    function play() {\n        if (timer) return;\n        timer = setInterval(nextFrame, frameDelay);\n    }\n\n    function pause() {\n        clearInterval(timer);\n        timer = null;\n    }\n\n    \/\/ Button behavior\n    playBtn.onclick = play;\n    pauseBtn.onclick = pause;\n    stepFwdBtn.onclick = () => { pause(); nextFrame(); };\n    stepBackBtn.onclick = () => { pause(); prevFrame(); };\n\n    \/\/ Start on first frame\n    showFrame();\n\n    \/\/ \ud83d\udd25 Auto-play on load\n    play();\n})();\n<\/script>\n\n\n\n<p>\u25b2 <strong>NOAA SWPC ENLIL Solar Wind Model<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<figure class=\"wp-block-video\"><video controls src=\"https:\/\/swe.ssa.esa.int\/DOCS\/portal_images\/uk_ral_euhforia_earth.mp4?\"><\/video><\/figure>\n\n\n\n<p>\u25b2 <strong>ESA European Solar Wind Model<\/strong> (<a href=\"https:\/\/swe.ssa.esa.int\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">https:\/\/swe.ssa.esa.int\/<\/a>) <br>Near-Earth solar wind forecast (EUHFORIA)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div id=\"ae-chart\"><\/div>\n<script>\n(function() {\n  const now = new Date();\n  const yyyy = now.getUTCFullYear();\n  const mm = String(now.getUTCMonth() + 1).padStart(2, '0');\n  const dd = String(now.getUTCDate()).padStart(2, '0');\n  const imgUrl = `https:\/\/wdc.kugi.kyoto-u.ac.jp\/ae_realtime\/today\/rtae_${yyyy}${mm}${dd}.png`;\n\n  const img = document.createElement('img');\n  img.src = imgUrl + '?t=' + Date.now(); \/\/ cache buster\n  img.alt = `Kyoto AE Realtime - ${yyyy}-${mm}-${dd}`;\n  img.style.maxWidth = '100%';\n  img.style.border = '1px solid #444';\n  img.style.borderRadius = '6px';\n\n  document.getElementById('ae-chart').appendChild(img);\n})();\n<\/script>\n\n\n\n<p><strong><strong>\u25b2<\/strong><\/strong> <strong>Kyoto Realtime Auroral Electrojet AE-Index (current day)<\/strong><\/p>\n\n\n\n<div id=\"dst-chart\"><\/div>\n<script>\n(function() {\n  const now = new Date();\n  const utcYear = String(now.getUTCFullYear()).slice(-2); \/\/ last two digits of year\n  const utcMonth = String(now.getUTCMonth() + 1).padStart(2, '0'); \/\/ month 01\u201312\n  const imgUrl = `https:\/\/wdc.kugi.kyoto-u.ac.jp\/dst_realtime\/presentmonth\/dst${utcYear}${utcMonth}.png`;\n\n  const img = document.createElement('img');\n  img.src = imgUrl + '?t=' + Date.now(); \/\/ prevent caching\n  img.alt = `Kyoto Dst Realtime - ${utcMonth}\/${utcYear}`;\n  img.style.maxWidth = '100%';\n  img.style.border = '1px solid #444';\n  img.style.borderRadius = '6px';\n\n  document.getElementById('dst-chart').appendChild(img);\n})();\n<\/script>\n\n\n\n<p><strong><strong>\u25b2<\/strong><\/strong> <strong>Kyoto Disturbance Storm Index (current month)<\/strong><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><img decoding=\"async\" src=\"http:\/\/www-app3.gfz-potsdam.de\/kp_index\/nowcast_Hp30_bar.gif\" alt=\"\"\/><figcaption class=\"wp-element-caption\"> \u00a9&nbsp;Helmholtz Centre Potsdam <\/figcaption><\/figure>\n<\/div>\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/services.swpc.noaa.gov\/images\/notifications-in-effect-timeline.png?time=1623851281000\" alt=\"\"\/><figcaption class=\"wp-element-caption\">\u25b2 <a href=\"https:\/\/www.swpc.noaa.gov\/products\/notifications-timeline\">https:\/\/www.swpc.noaa.gov\/products\/notifications-timeline<\/a><\/figcaption><\/figure>\n\n\n\n<p>-Link to Auroral Electrojet Index Realtime \"Quick look\" (Kyoto, Japan) <br><a rel=\"noreferrer noopener\" href=\"http:\/\/wdc.kugi.kyoto-u.ac.jp\/ae_realtime\/today\/today.html\" target=\"_blank\">http:\/\/wdc.kugi.kyoto-u.ac.jp\/ae_realtime\/today\/today.html<\/a> <\/p>\n\n\n\n<p><strong>Satellite solar wind data<\/strong> <br>ACE Satellite at L1 - gives up to 50 minutes' warning of an event at Earth.<br>Bz = solar wind magnetic polarity (negative\/south is good for aurora).<br>Sharp density and speed increases indicate possible CME incoming to Earth.<\/p>\n\n\n\n<p>A&nbsp;<strong>solar proton event<\/strong>&nbsp;(<strong>SPE<\/strong>), or \"<strong>proton storm<\/strong>\", occurs when particles (mostly&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Proton\">protons<\/a>) emitted by the Sun become accelerated either close to the Sun during a flare or in interplanetary space by&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Coronal_mass_ejection\">CME<\/a>&nbsp;shocks. The events can include other nuclei such as helium ions and&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/HZE_ions\">HZE ions<\/a>. These particles cause multiple effects. They can penetrate the Earth's magnetic field and cause&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Ionization\">ionization<\/a>&nbsp;in the&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Ionosphere\">ionosphere<\/a>. The effect is similar to auroral events, except that protons rather than electrons are involved. Energetic protons are a significant radiation hazard to spacecraft and astronauts. <\/p>\n\n\n\n<p>A&nbsp;<strong>geomagnetic storm<\/strong>&nbsp;(commonly referred to as a&nbsp;<strong>solar storm<\/strong>) is a temporary disturbance of the&nbsp;Earth's&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Magnetosphere\">magnetosphere<\/a> caused by a&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Solar_wind\">solar wind<\/a>&nbsp;shock wave and\/or cloud of magnetic field that interacts with the&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Earth%27s_magnetic_field\">Earth's magnetic field<\/a>. The increase in the solar wind pressure initially compresses the magnetosphere. The solar wind's magnetic field interacts with the Earth's magnetic field and transfers an increased energy into the magnetosphere. Both interactions cause an increase in plasma movement through the magnetosphere (driven by increased electric fields inside the magnetosphere) and an increase in electric current in the magnetosphere and&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Ionosphere\">ionosphere<\/a>. <\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tvcomm.co.uk\/g7izu\/Autosave\/hsd-now.gif\" alt=\"\"\/><figcaption class=\"wp-element-caption\">\u25b2 UK GEOMAGNETIC ACTIVITY TODAY  <strong><strong>\u25bc<\/strong><\/strong> UK GEOMAGNETIC ACTIVITY YESTERDAY<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tvcomm.co.uk\/g7izu\/Autosave\/hsd-now-1.gif\" alt=\"\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tvcomm.co.uk\/g7izu\/Autosave\/ukmags.gif\" alt=\"\"\/><figcaption class=\"wp-element-caption\">\u25b2\u25bc <strong>British Geological Survey UK<\/strong><\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tvcomm.co.uk\/g7izu\/Autosave\/kandkp_bar.jpg\" alt=\"\"\/><figcaption class=\"wp-element-caption\">\u25b2 <strong>BGS K and Kp Index at UK Observatories<\/strong><\/figcaption><\/figure>\n\n\n\n<p>Live data from BGS: <a href=\"http:\/\/www.geomag.bgs.ac.uk\/data_service\/space_weather\/current_conditions.html\">Link<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/space.umd.edu\/pm\/latest2day.png\"><img decoding=\"async\" src=\"https:\/\/space.umd.edu\/pm\/latest2day.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<p>\u25b2 <strong>SOHO latest solar wind data<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>The page is refreshed once per five minutes.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>*** Changes to the data files supplied by NOAA\/SWPC may break the live displays on this page from approx. 31 March 2026. I will work to fix things as soon as possible. Additionally, the Solar Data Analysis Center (SDAC) and Space Physics Data Facility (SPDF) servers are physically moving location, impacting the collection and supply [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":4,"menu_order":20,"comment_status":"closed","ping_status":"closed","template":"","meta":{"ngg_post_thumbnail":0,"footnotes":""},"class_list":["post-73","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages\/73","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/comments?post=73"}],"version-history":[{"count":294,"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages\/73\/revisions"}],"predecessor-version":[{"id":3509,"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages\/73\/revisions\/3509"}],"up":[{"embeddable":true,"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages\/4"}],"wp:attachment":[{"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/media?parent=73"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}