{"id":47,"date":"2019-06-12T15:39:44","date_gmt":"2019-06-12T15:39:44","guid":{"rendered":"https:\/\/www.tvcomm.co.uk\/g7izu\/?page_id=47"},"modified":"2026-03-31T20:45:01","modified_gmt":"2026-03-31T19:45:01","slug":"current-space-weather","status":"publish","type":"page","link":"https:\/\/www.tvcomm.co.uk\/g7izu\/welcome\/current-space-weather\/","title":{"rendered":"Live Solar Events"},"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 Maximum values during the last hour (Kp 3 hours) \u266b<\/p>\n\n\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\/\/ Use DOMContentLoaded with proper cleanup to avoid conflicts\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      document.getElementById('densityValue').innerHTML  = d.toFixed(1)+\" \/cm\u00b3 <span class='trend'>\"+dT+\"<\/span>\";\n      document.getElementById('speedValue').innerHTML    = v.toFixed(0)+\" km\/s <span class='trend'>\"+vT+\"<\/span>\";\n      document.getElementById('pressureValue').innerHTML = p.toFixed(2)+\" nPa <span class='trend'>\"+pT+\"<\/span>\";\n\n      document.getElementById('densityLabel').textContent  = DENSITY_LABELS[densityLevel(d)];\n      document.getElementById('speedLabel').textContent    = SPEED_LABELS[speedLevel(v)];\n      document.getElementById('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(\"Indicators error:\", e); }\n  }\n\n  \/\/ Initialize when DOM is ready\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', function init() {\n      updateIndicators();\n      setInterval(updateIndicators, UPDATE_INTERVAL_MS);\n      document.removeEventListener('DOMContentLoaded', init);\n    });\n  } else {\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 60 min trend)<\/strong><\/p>\n\n\n\n<div id=\"solar-flux\">\n    <div style=\"width: 98%; height: 400px; margin: auto; background-color: black; padding: 10px; border-radius: 10px;\">\n        <canvas id=\"fluxChart\"><\/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\n<script>\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n    let chart;\n    let currentGraphIndex = 0;\n\n    const graphConfigs = [\n        { \n            title: \"X-Ray Flux Level (GOES 18 & 19)\",\n            urls: [\n                { url: \"https:\/\/services.swpc.noaa.gov\/json\/goes\/primary\/xrays-1-day.json\", color: \"cyan\", label: \"GOES 18 Primary\" },\n                { url: \"https:\/\/services.swpc.noaa.gov\/json\/goes\/secondary\/xrays-1-day.json\", color: \"yellow\", label: \"GOES 19 Secondary\" }\n            ],\n            scaleType: \"logarithmic\",\n            minMax: { min: 1e-7, minMax: 5e-6 },\n            parseValue: entry => parseFloat(entry.flux),\n            labelFormatter: value => convertToFlareClass(value),\n            tooltipFormatter: value => convertToFlareClass(value)\n        },\n        { \n            title: \"Electron Flux (ACE EPAM & GOES 19)\", \n            urls: [\n                { url: \"https:\/\/services.swpc.noaa.gov\/json\/goes\/primary\/integral-electrons-1-day.json\", color: \"orange\", label: \"GOES 19 Integral\" },\n                { url: \"https:\/\/services.swpc.noaa.gov\/json\/ace\/epam\/ace_epam_5m.json\", isACE: true, particleType: \"electrons\" }\n            ],\n            scaleType: \"logarithmic\",\n            minMax: { min: 1, minMax: 1e6 },\n            parseValue: entry => parseFloat(entry.flux),\n            labelFormatter: value => formatCompactNumber(value),\n            tooltipFormatter: value => formatCompactNumber(value)\n        },\n        { \n            title: \"Solar Proton Flux (ACE EPAM & GOES 19)\", \n            urls: [\n                { url: \"https:\/\/services.swpc.noaa.gov\/json\/goes\/primary\/integral-protons-1-day.json\", color: \"cyan\", label: \"GOES 19 Integral\" },\n                { url: \"https:\/\/services.swpc.noaa.gov\/json\/ace\/epam\/ace_epam_5m.json\", isACE: true, particleType: \"protons\" }\n            ],\n            scaleType: \"logarithmic\",\n            minMax: { min: 1, minMax: 1e5 },\n            parseValue: entry => parseFloat(entry.flux),\n            labelFormatter: value => formatCompactNumber(value),\n            tooltipFormatter: value => formatCompactNumber(value)\n        }\n    ];\n\n    \/\/ ACE EPAM energy channel colors - ONLY PROTON COLORS CHANGED\n    const aceColors = {\n        electrons: [\"#FF6B6B\", \"#4ECDC4\"], \/\/ Red, Teal for electron channels - UNCHANGED\n        protons: [\"#FF0000\", \"#FFA500\", \"#FFFF00\", \"#FFFFFF\", \"#00FF00\"] \/\/ CHANGED: Red, Orange, Yellow, White, Green\n    };\n\n    \/\/ ACE EPAM channel mappings\n    const aceChannelMappings = {\n        electrons: [\n            { field: \"de1\", label: \"38-53 keV Electrons\", colorIndex: 0 },\n            { field: \"de4\", label: \"175-315 keV Electrons\", colorIndex: 1 }\n        ],\n        protons: [\n            { field: \"p1\", label: \"47-68 keV Protons\", colorIndex: 0 },\n            { field: \"p2\", label: \"115-195 keV Protons\", colorIndex: 1 },\n            { field: \"p3\", label: \"310-580 keV Protons\", colorIndex: 2 },\n            { field: \"p4\", label: \"795-1193 keV Protons\", colorIndex: 3 },\n            { field: \"p5\", label: \"1060-1900 keV Protons\", colorIndex: 4 }\n        ]\n    };\n\n    function convertToFlareClass(value) {\n        if (value >= 1e-4) return `X${(value \/ 1e-4).toFixed(1)}`;\n        if (value >= 1e-5) return `M${(value \/ 1e-5).toFixed(1)}`;\n        if (value >= 1e-6) return `C${(value \/ 1e-6).toFixed(1)}`;\n        if (value >= 1e-7) return `B${(value \/ 1e-7).toFixed(1)}`;\n        return `A${(value \/ 1e-8).toFixed(1)}`;\n    }\n\n    function formatCompactNumber(value) {\n        if (value >= 1e6) return (value\/1e6).toFixed(1) + \"M\";\n        if (value >= 1e3) return (value\/1e3).toFixed(1) + \"K\";\n        return value.toFixed(1);\n    }\n\n    \/\/ Parse ACE EPAM JSON data - ONLY FIX: Force UTC on timestamps\n    async function parseACEJSONData(url, particleType) {\n        try {\n            const response = await fetch(url);\n            const data = await response.json();\n            \n            \/\/ Filter to last 24 hours for performance\n            const now = new Date();\n            const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);\n            \/\/ FIX: Add 'Z' to force UTC interpretation for ACE data\n            const filteredData = data.filter(entry => new Date(entry.time_tag + 'Z') >= twentyFourHoursAgo);\n            \n            \/\/ Group data by channel\n            const channelData = {};\n            const channels = aceChannelMappings[particleType];\n            \n            channels.forEach(channel => {\n                channelData[channel.label] = filteredData.map(entry => ({\n                    time_tag: entry.time_tag + 'Z', \/\/ FIX: Force UTC\n                    flux: parseFloat(entry[channel.field])\n                })).filter(entry => !isNaN(entry.flux) && entry.flux > 0);\n            });\n            \n            return channelData;\n        } catch (error) {\n            console.error(\"Error parsing ACE JSON data:\", error);\n            return {};\n        }\n    }\n\n    \/\/ Interpolate data to match timestamps - NO CHANGES to GOES handling\n    function interpolateDataToTimestamps(sourceData, targetTimestamps) {\n        if (!sourceData || sourceData.length === 0) return Array(targetTimestamps.length).fill(NaN);\n        \n        const sourceTimes = sourceData.map(d => new Date(d.time_tag).getTime());\n        const sourceValues = sourceData.map(d => d.flux);\n        const interpolated = [];\n        \n        for (const targetTime of targetTimestamps) {\n            const target = new Date(targetTime).getTime();\n            \n            \/\/ Find surrounding data points\n            let beforeIdx = -1, afterIdx = -1;\n            for (let i = 0; i < sourceTimes.length; i++) {\n                if (sourceTimes[i] <= target) beforeIdx = i;\n                if (sourceTimes[i] >= target) {\n                    afterIdx = i;\n                    break;\n                }\n            }\n            \n            if (beforeIdx === -1 && afterIdx === -1) {\n                interpolated.push(NaN);\n            } else if (beforeIdx === -1) {\n                interpolated.push(sourceValues[afterIdx]);\n            } else if (afterIdx === -1) {\n                interpolated.push(sourceValues[beforeIdx]);\n            } else if (beforeIdx === afterIdx) {\n                interpolated.push(sourceValues[beforeIdx]);\n            } else {\n                \/\/ Linear interpolation\n                const t1 = sourceTimes[beforeIdx];\n                const t2 = sourceTimes[afterIdx];\n                const v1 = sourceValues[beforeIdx];\n                const v2 = sourceValues[afterIdx];\n                \n                const weight = (target - t1) \/ (t2 - t1);\n                interpolated.push(v1 + weight * (v2 - v1));\n            }\n        }\n        \n        return interpolated;\n    }\n\n    async function fetchFluxData() {\n        const config = graphConfigs[currentGraphIndex];\n        try {\n            const datasets = [];\n            const now = new Date();\n            const sixHoursAgo = new Date(now.getTime() - 6 * 60 * 60 * 1000);\n            let allTimestamps = new Set();\n            let overallMax = config.minMax.min;\n\n            \/\/ First, collect all data sources\n            const sourceData = [];\n            \n            for (const source of config.urls) {\n                if (source.isACE) {\n                    \/\/ Handle ACE JSON data - ONLY ACE data gets UTC forcing\n                    const aceData = await parseACEJSONData(source.url, source.particleType);\n                    \n                    for (const [channelLabel, data] of Object.entries(aceData)) {\n                        data.forEach(entry => {\n                            if (new Date(entry.time_tag) >= sixHoursAgo) {\n                                allTimestamps.add(entry.time_tag);\n                            }\n                        });\n                        \n                        \/\/ Find the channel mapping for color\n                        const channelMapping = aceChannelMappings[source.particleType].find(ch => ch.label === channelLabel);\n                        const color = aceColors[source.particleType][channelMapping ? channelMapping.colorIndex : 0];\n                        \n                        sourceData.push({\n                            label: channelLabel,\n                            data: data,\n                            color: color\n                        });\n                    }\n                } else {\n                    \/\/ Handle standard JSON data (GOES) - NO CHANGES, keep original\n                    const response = await fetch(source.url);\n                    const data = await response.json();\n                    \n                    const filteredData = data.filter(entry => new Date(entry.time_tag) >= sixHoursAgo);\n                    filteredData.forEach(entry => allTimestamps.add(entry.time_tag));\n                    \n                    sourceData.push({\n                        label: source.label,\n                        data: filteredData,\n                        color: source.color\n                    });\n                }\n            }\n\n            \/\/ Sort timestamps and create time bins (10-minute intervals)\n            const sortedTimestamps = Array.from(allTimestamps)\n                .map(ts => new Date(ts))\n                .sort((a, b) => a - b);\n            \n            if (sortedTimestamps.length === 0) return;\n\n            const timeBins = [];\n            let binStart = new Date(sortedTimestamps[0]);\n            binStart.setMinutes(Math.floor(binStart.getMinutes() \/ 10) * 10);\n            binStart.setSeconds(0);\n            binStart.setMilliseconds(0);\n            \n            const binLabels = [];\n            while (binStart <= sortedTimestamps[sortedTimestamps.length - 1]) {\n                timeBins.push(binStart.getTime());\n                binLabels.push(binStart.toISOString());\n                binStart = new Date(binStart.getTime() + 10 * 60 * 1000);\n            }\n\n            \/\/ Process each data source and interpolate to time bins\n            for (const source of sourceData) {\n                let intervalMaxData = [];\n                \n                for (let i = 0; i < timeBins.length; i++) {\n                    const binStart = timeBins[i];\n                    const binEnd = binStart + 10 * 60 * 1000;\n                    \n                    let maxInBin = 0;\n                    for (const entry of source.data) {\n                        const entryTime = new Date(entry.time_tag).getTime();\n                        if (entryTime >= binStart && entryTime < binEnd) {\n                            const fluxValue = config.parseValue(entry);\n                            maxInBin = Math.max(maxInBin, fluxValue);\n                            overallMax = Math.max(overallMax, fluxValue);\n                        }\n                    }\n                    intervalMaxData.push(Math.max(maxInBin, config.minMax.min));\n                }\n                \n                \/\/ Shorten labels for proton channels to save space\n                let displayLabel = source.label;\n                if (config.title.includes(\"Proton\")) {\n                    displayLabel = source.label\n                        .replace('keV', 'k')\n                        .replace('MeV', 'M')\n                        .replace('-', '-')\n                        .replace(' ', '');\n                }\n                \n                datasets.push({\n                    label: displayLabel,\n                    data: intervalMaxData,\n                    borderColor: source.color,\n                    backgroundColor: `${source.color}33`,\n                    borderWidth: 1.5,\n                    pointRadius: 1,\n                    tension: 0.3\n                });\n            }\n\n            let yMax = Math.max(overallMax * 1.5, config.minMax.minMax);\n            updateChart(binLabels, datasets, config, yMax);\n\n        } catch (error) {\n            console.error(\"Error fetching data:\", error);\n        }\n    }\n\n    function createChart(labels, datasets, config, yMax) {\n        const ctx = document.getElementById(\"fluxChart\").getContext(\"2d\");\n        \n        \/\/ Configure legend based on the graph type\n        const isProtonGraph = config.title.includes(\"Proton\");\n        \n        chart = new Chart(ctx, {\n            type: \"line\",\n            data: { labels: labels, datasets: datasets },\n            options: {\n                responsive: true,\n                maintainAspectRatio: false,\n                plugins: {\n                    title: {\n                        display: true,\n                        text: config.title,\n                        font: { size: 18, weight: \"bold\" },\n                        color: \"white\",\n                        padding: { bottom: 10 }\n                    },\n                    legend: {\n                        display: true,\n                        position: 'top',\n                        align: 'center',\n                        labels: {\n                            color: \"white\",\n                            font: { \n                                size: isProtonGraph ? 9 : 11,\n                                weight: 'normal'\n                            },\n                            padding: isProtonGraph ? 8 : 20,\n                            boxWidth: 12,\n                            boxHeight: 12,\n                            usePointStyle: false,\n                            generateLabels: chart => chart.data.datasets.map(ds => ({\n                                text: ds.label,\n                                fillStyle: ds.borderColor,\n                                strokeStyle: ds.borderColor,\n                                fontColor: ds.borderColor,\n                                lineWidth: 1,\n                                pointStyle: 'rectRot'\n                            }))\n                        },\n                        onClick: null\n                    },\n                    tooltip: {\n                        mode: 'index',\n                        intersect: false,\n                        callbacks: {\n                            label: context => {\n                                const value = context.raw;\n                                const dataset = context.dataset;\n                                if (isNaN(value) || value <= config.minMax.min) {\n                                    return `${dataset.label}: No data`;\n                                }\n                                return `${dataset.label}: ${config.tooltipFormatter(value)}`;\n                            }\n                        }\n                    }\n                },\n                scales: {\n                    x: {\n                        type: \"time\",\n                        time: { \n                            unit: \"hour\", \n                            tooltipFormat: \"HH:mm\", \n                            displayFormats: { hour: \"HH:mm\" } \n                        },\n                        adapters: { date: { zone: \"UTC\" } },\n                        title: { \n                            display: true, \n                            text: \"Time (UTC)\", \n                            color: \"white\", \n                            font: { size: 14, weight: \"bold\" } \n                        },\n                        ticks: { color: \"white\", font: { size: 12 } },\n                        grid: { color: \"#444\" }\n                    },\n                    y: {\n                        type: config.scaleType,\n                        min: config.minMax.min,\n                        max: yMax,\n                        ticks: {\n                            color: \"white\",\n                            font: { size: 10 },\n                            callback: function(value) {\n                                return config.labelFormatter(value);\n                            }\n                        },\n                        grid: {\n                            color: function(context) {\n                                const logValue = Math.log10(context.tick.value);\n                                if (Number.isInteger(logValue)) return \"#888\";\n                                return \"#444\";\n                            },\n                            lineWidth: function(context) {\n                                const logValue = Math.log10(context.tick.value);\n                                return Number.isInteger(logValue) ? 1.5 : 0.8;\n                            }\n                        }\n                    }\n                },\n                interaction: {\n                    intersect: false,\n                    mode: 'index'\n                }\n            }\n        });\n    }\n\n    function updateChart(labels, datasets, config, yMax) {\n        if (!chart) {\n            createChart(labels, datasets, config, yMax);\n        } else {\n            const isProtonGraph = config.title.includes(\"Proton\");\n            chart.options.plugins.legend.labels.font.size = isProtonGraph ? 9 : 11;\n            \n            chart.data.labels = labels;\n            chart.data.datasets = datasets;\n            chart.options.plugins.title.text = config.title;\n            chart.options.scales.y.type = config.scaleType;\n            chart.options.scales.y.min = config.minMax.min;\n            chart.options.scales.y.max = yMax;\n            chart.options.scales.y.ticks.callback = function(value) {\n                return config.labelFormatter(value);\n            };\n            \n            chart.options.plugins.tooltip.callbacks.label = context => {\n                const value = context.raw;\n                const dataset = context.dataset;\n                if (isNaN(value) || value <= config.minMax.min) {\n                    return `${dataset.label}: No data`;\n                }\n                return `${dataset.label}: ${config.tooltipFormatter(value)}`;\n            };\n            \n            chart.update();\n        }\n    }\n\n    function rotateGraphs() {\n        currentGraphIndex = (currentGraphIndex + 1) % graphConfigs.length;\n        fetchFluxData();\n    }\n\n    fetchFluxData();\n    setInterval(fetchFluxData, 180000); \/\/ Refresh every 3 minutes\n    setInterval(rotateGraphs, 15000); \/\/ Rotate graphs every 15 seconds\n});\n<\/script>\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<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<style>\n  #sdo-image-rotator {\n    position: relative;\n    width: 100%;\n    max-width: 1024px;\n    margin: auto;\n    aspect-ratio: 1 \/ 1;\n    overflow: hidden;\n  }\n\n  .sdo-image {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: opacity 1s ease-in-out;\n    opacity: 0;\n    z-index: 1;\n  }\n\n  .sdo-image.active {\n    opacity: 1;\n    z-index: 2;\n  }\n<\/style>\n\n<div id=\"sdo-image-rotator\">\n  <img decoding=\"async\" id=\"imgA\" class=\"sdo-image active\" src=\"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/094\/latest.png\" alt=\"SDO Image A\">\n  <img decoding=\"async\" id=\"imgB\" class=\"sdo-image\" src=\"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/131\/latest.png\" alt=\"SDO Image B\">\n<\/div>\n\n<script>\n  const images = [\n    \"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/094\/latest.png\",\n    \"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/131\/latest.png\",\n    \"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/171\/latest.png\",\n    \"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/195\/latest.png\",\n    \"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/284\/latest.png\",\n    \"https:\/\/services.swpc.noaa.gov\/images\/animations\/suvi\/primary\/304\/latest.png\",\n    \"https:\/\/solarwww.mtk.nao.ac.jp\/mitaka_solar\/latest\/Ha_i_000_m_today.jpg\"\n  ];\n\n  let index = 2;\n  let isAActive = true;\n  const imgA = document.getElementById('imgA');\n  const imgB = document.getElementById('imgB');\n\n  function crossfade() {\n    const current = isAActive ? imgA : imgB;\n    const next = isAActive ? imgB : imgA;\n\n    \/\/ Preload the next image first\n    const preload = new Image();\n    preload.onload = () => {\n      next.src = images[index];\n      index = (index + 1) % images.length;\n\n      \/\/ Trigger crossfade\n      next.classList.add('active');\n      current.classList.remove('active');\n      isAActive = !isAActive;\n    };\n    preload.src = images[index];\n  }\n\n  setInterval(crossfade, 10000);\n<\/script>\n\n\n\n<p>\u25b2 <strong>NASA\/SDO\/GOES\/Mitaka Solar Disc (image rotator) <\/strong><\/p>\n\n\n\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/ovsa.njit.edu\/flaremon\/goes6-hour.png\" alt=\"\"\/><\/figure>\n<\/figure>\n\n\n<div class=\"wp-block-image is-resized\">\n<figure class=\"alignleft\"><img decoding=\"async\" src=\"https:\/\/services.swpc.noaa.gov\/images\/animations\/d-rap\/global\/d-rap\/latest.png\" alt=\"\" class=\"wp-image-14\"\/><\/figure>\n<\/div>\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/ovsa.njit.edu\/flaremon\/XSP_latest.png\" alt=\"\"\/><\/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=\"\"\/><\/figure>\n\n\n\n<p>\u25b2 The <strong>DRAP D-Region Absorption Product<\/strong> 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<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/swe.ssa.esa.int\/DOCS\/portal_images\/gr_iaasars_hesperia_umasep_500.gif?\" alt=\"\"\/><\/figure>\n\n\n\n<p>\u25b2 Provided by: Institute for Astronomy, Astrophysics, Space Applications &amp; Remote Sensing<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img decoding=\"async\" src=\"http:\/\/services.swpc.noaa.gov\/images\/ace-mag-swepam-24-hour.gif\" alt=\"\"\/><\/figure>\n<\/div>\n\n\n<p>\u25b2 <strong>NASA\/ACE Satellite at L1<\/strong> <strong>(early warning)<\/strong><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img decoding=\"async\" src=\"https:\/\/services.swpc.noaa.gov\/images\/ace-epam-24-hour.gif\" alt=\"\"\/><\/figure>\n<\/div>\n\n\n<p>\u25b2 <strong>NASA\/ACE Satellite at L1 (early warning)<\/strong><br><a href=\"https:\/\/www.swpc.noaa.gov\/products\/ace-real-time-solar-wind\" target=\"_blank\" rel=\"noreferrer noopener\">More ACE Data here<\/a><br><a href=\"https:\/\/www.swpc.noaa.gov\/products\/real-time-solar-wind\" target=\"_blank\" rel=\"noreferrer noopener\">DISCOVR Real-time Solar Wind Data is here<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>What is particles\/cm\u00b2\/s\/sr?<\/strong> <br>It means:<br>The number of electrons or protons crossing <strong>1 cm\u00b2 of detector area<\/strong>, <strong>per second<\/strong>, <strong>per steradian of viewing angle<\/strong>. This makes the quantity independent of the detector\u2019s size and orientation, so different instruments can be compared. The detectors are on the ACE and Discovr satellites located at the Earth-Sun Lagrange Point L1.<br><br><em>Page updated once per 3 minutes<\/em><\/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":10,"comment_status":"closed","ping_status":"closed","template":"","meta":{"ngg_post_thumbnail":0,"footnotes":""},"class_list":["post-47","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages\/47","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=47"}],"version-history":[{"count":277,"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages\/47\/revisions"}],"predecessor-version":[{"id":3508,"href":"https:\/\/www.tvcomm.co.uk\/g7izu\/wp-json\/wp\/v2\/pages\/47\/revisions\/3508"}],"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=47"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}