feat(config): add comprehensive field documentation with info buttons

- Add helper text and info (?) buttons to every field in Config.tsx
- Add section descriptions at the top of each config section
- Battery thresholds now show voltage equivalents (e.g., "30% ≈ 3.60V")
- NWS severity dropdown shows descriptions per option
- Alert rules grouped by category with full explanations
- Add InfoButton popover component for detailed field documentation
- Add info buttons to Environment.tsx RF propagation panels
- VOLTAGE_MAP and getVoltageApprox helper for Li-ion voltage lookup

Researched defaults and descriptions include:
- Li-ion voltage curve (4.20V=100%, 3.60V=30%, 3.50V=15%, 3.40V=7%)
- LoRa channel utilization (firmware throttles at 25%, issues at 50%)
- Packet flood detection (normal 1-5/min, suspicious >10/min)
- NWS severity levels with actionable descriptions
- Tropospheric ducting M-units/km refractivity gradients
- NOAA Space Weather R/S/G scales

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
zvx-echo6 2026-05-13 09:55:03 -06:00
commit 9369bd684f
2 changed files with 1085 additions and 175 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { useEffect, useState, useRef } from 'react'
import { import {
Cloud, Cloud,
Sun, Sun,
@ -10,7 +10,42 @@ import {
Wind, Wind,
Flame, Flame,
Mountain, Mountain,
HelpCircle,
} from 'lucide-react' } from 'lucide-react'
// Info button component with popover
function InfoButton({ info }: { info: string }) {
const [show, setShow] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setShow(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
return (
<div className="relative inline-block" ref={ref}>
<button
type="button"
onClick={() => setShow(!show)}
className="ml-1 text-slate-500 hover:text-slate-300 transition-colors"
aria-label="More information"
>
<HelpCircle size={14} />
</button>
{show && (
<div className="absolute z-50 left-0 mt-1 w-64 p-3 bg-[#1a1f2e] border border-[#2a3548] rounded-lg shadow-xl text-xs text-slate-300 leading-relaxed">
{info}
</div>
)}
</div>
)
}
import { import {
fetchEnvStatus, fetchEnvStatus,
fetchEnvActive, fetchEnvActive,
@ -150,6 +185,7 @@ function SolarIndicesPanel({ swpc }: { swpc: SWPCStatus | null }) {
<h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2"> <h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
<Sun size={14} /> <Sun size={14} />
Solar/Geomagnetic Indices Solar/Geomagnetic Indices
<InfoButton info="Space weather data from NOAA SWPC. Solar Flux Index (SFI) indicates HF propagation quality. Kp index measures geomagnetic disturbance. Higher values can degrade or enhance radio propagation." />
</h2> </h2>
<div className="text-slate-500">Data not available</div> <div className="text-slate-500">Data not available</div>
</div> </div>
@ -176,12 +212,16 @@ function SolarIndicesPanel({ swpc }: { swpc: SWPCStatus | null }) {
<h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2"> <h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
<Sun size={14} /> <Sun size={14} />
Solar/Geomagnetic Indices Solar/Geomagnetic Indices
<InfoButton info="Space weather data from NOAA SWPC. Solar Flux Index (SFI) indicates HF propagation quality (higher=better). Kp index measures geomagnetic disturbance (lower=better). R/S/G scales: R=Radio Blackout, S=Solar Radiation, G=Geomagnetic Storm." />
</h2> </h2>
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
{/* SFI */} {/* SFI */}
<div className="bg-bg-hover rounded-lg p-3"> <div className="bg-bg-hover rounded-lg p-3">
<div className="text-xs text-slate-500 mb-1">Solar Flux Index</div> <div className="text-xs text-slate-500 mb-1 flex items-center">
Solar Flux Index
<InfoButton info="10.7cm solar radio flux. <70=poor HF, 70-90=fair, 90-120=good, 120-150=very good, >150=excellent. Measured daily at noon UTC." />
</div>
<div className="text-2xl font-mono text-slate-100"> <div className="text-2xl font-mono text-slate-100">
{swpc.sfi?.toFixed(0) ?? '—'} {swpc.sfi?.toFixed(0) ?? '—'}
</div> </div>
@ -190,7 +230,10 @@ function SolarIndicesPanel({ swpc }: { swpc: SWPCStatus | null }) {
{/* Kp */} {/* Kp */}
<div className="bg-bg-hover rounded-lg p-3"> <div className="bg-bg-hover rounded-lg p-3">
<div className="text-xs text-slate-500 mb-1">Planetary K-Index</div> <div className="text-xs text-slate-500 mb-1 flex items-center">
Planetary K-Index
<InfoButton info="Geomagnetic disturbance scale 0-9. Kp 0-2=quiet, 3-4=unsettled, 5=minor storm (G1), 6=moderate (G2), 7=strong (G3), 8=severe (G4), 9=extreme (G5). Higher Kp degrades HF at high latitudes." />
</div>
<div className={`text-2xl font-mono ${getKpColor(swpc.kp_current)}`}> <div className={`text-2xl font-mono ${getKpColor(swpc.kp_current)}`}>
{swpc.kp_current?.toFixed(1) ?? '—'} {swpc.kp_current?.toFixed(1) ?? '—'}
</div> </div>
@ -251,6 +294,7 @@ function DuctingPanel({ ducting }: { ducting: DuctingStatus | null }) {
<h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2"> <h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
<Wind size={14} /> <Wind size={14} />
Tropospheric Ducting Tropospheric Ducting
<InfoButton info="Atmospheric conditions that trap VHF/UHF signals, allowing propagation far beyond normal line-of-sight. Measured as refractivity gradient (dM/dz) in M-units/km. Negative values indicate ducting." />
</h2> </h2>
<div className="text-slate-500">Data not available</div> <div className="text-slate-500">Data not available</div>
</div> </div>
@ -281,11 +325,15 @@ function DuctingPanel({ ducting }: { ducting: DuctingStatus | null }) {
<h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2"> <h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
<Wind size={14} /> <Wind size={14} />
Tropospheric Ducting Tropospheric Ducting
<InfoButton info="Atmospheric conditions that trap VHF/UHF signals, allowing propagation far beyond normal line-of-sight. Ducting can extend 900 MHz Meshtastic range significantly. Surface ducts form near the ground; elevated ducts form aloft." />
</h2> </h2>
{/* Condition */} {/* Condition */}
<div className="bg-bg-hover rounded-lg p-4 mb-4"> <div className="bg-bg-hover rounded-lg p-4 mb-4">
<div className="text-xs text-slate-500 mb-1">Condition</div> <div className="text-xs text-slate-500 mb-1 flex items-center">
Condition
<InfoButton info="Normal: Standard refraction. Super-refraction: Signals bend more than normal, slightly extended range. Ducting: Signals trapped in atmospheric layer, significantly extended range possible." />
</div>
<div className={`text-xl font-medium ${getConditionColor(ducting.condition)}`}> <div className={`text-xl font-medium ${getConditionColor(ducting.condition)}`}>
{formatCondition(ducting.condition)} {formatCondition(ducting.condition)}
</div> </div>