/**
 * SettingsPanel.jsx — System configuration dashboard.
 *
 * Sub-views: PID | Sensors | Network | Firmware | Diagnostics | Backup
 *
 * Props:
 *   subscribe, callService  — from useRosbridge
 *   connected               — rosbridge connection status
 *   wsUrl                   — current rosbridge WebSocket URL
 */

import { useState, useEffect, useRef, useCallback } from 'react';
import {
  useSettings, PID_PARAMS, SENSOR_PARAMS, PID_NODE,
  simulateStepResponse, validatePID,
} from '../hooks/useSettings.js';

const VIEWS = ['PID', 'Sensors', 'Network', 'Firmware', 'Diagnostics', 'Backup'];

function ValidationBadges({ warnings }) {
  if (!warnings?.length) return (
    <div className="flex items-center gap-1.5 text-xs text-green-400">
      <span className="w-1.5 h-1.5 rounded-full bg-green-400 inline-block"/>
      All gains within safe bounds
    </div>
  );
  return (
    <div className="space-y-1">
      {warnings.map((w, i) => (
        <div key={i} className={`flex items-start gap-2 text-xs rounded px-2 py-1 border ${
          w.level === 'error'
            ? 'bg-red-950 border-red-800 text-red-400'
            : 'bg-amber-950 border-amber-800 text-amber-300'
        }`}>
          <span className="shrink-0">{w.level === 'error' ? '✕' : '⚠'}</span>
          {w.msg}
        </div>
      ))}
    </div>
  );
}

function ApplyResult({ result }) {
  if (!result) return null;
  return (
    <div className={`text-xs rounded px-2 py-1 border ${
      result.ok
        ? 'bg-green-950 border-green-800 text-green-400'
        : 'bg-red-950 border-red-800 text-red-400'
    }`}>{result.ok ? '✓ ' : '✕ '}{result.msg}</div>
  );
}

function StepResponseCanvas({ kp, ki, kd }) {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const W = canvas.width, H = canvas.height;
    const PAD = { top: 12, right: 16, bottom: 24, left: 40 };
    const CW = W - PAD.left - PAD.right;
    const CH = H - PAD.top  - PAD.bottom;

    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = '#050510';
    ctx.fillRect(0, 0, W, H);

    const data = simulateStepResponse(kp, ki, kd);
    if (!data.length) return;

    const tail    = data.slice(-20);
    const maxTail = Math.max(...tail.map(d => Math.abs(d.theta)));
    const finalMax = Math.max(...data.map(d => Math.abs(d.theta)));
    const isUnstable    = finalMax > 45;
    const isOscillating = !isUnstable && maxTail > 1.0;
    const lineColor = isUnstable ? '#ef4444' : isOscillating ? '#f59e0b' : '#22c55e';

    const yMax = Math.min(90, Math.max(10, finalMax * 1.2));
    const yMin = -Math.min(5, yMax * 0.1);
    const tx = (t) => PAD.left + (t / 2.4) * CW;
    const ty = (v) => PAD.top + CH - ((v - yMin) / (yMax - yMin)) * CH;

    ctx.strokeStyle = '#0d1b2a'; ctx.lineWidth = 1;
    for (let v = -10; v <= 90; v += 5) {
      if (v < yMin || v > yMax) continue;
      ctx.beginPath(); ctx.moveTo(PAD.left, ty(v)); ctx.lineTo(PAD.left + CW, ty(v)); ctx.stroke();
    }
    for (let t = 0; t <= 2.4; t += 0.4) {
      ctx.beginPath(); ctx.moveTo(tx(t), PAD.top); ctx.lineTo(tx(t), PAD.top + CH); ctx.stroke();
    }

    ctx.strokeStyle = lineColor; ctx.lineWidth = 2;
    ctx.shadowBlur = isUnstable ? 0 : 4; ctx.shadowColor = lineColor;
    ctx.beginPath();
    data.forEach((d, i) => {
      const x = tx(d.t), y = ty(Math.max(yMin, Math.min(yMax, d.theta)));
      i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    });
    ctx.stroke(); ctx.shadowBlur = 0;

    ctx.fillStyle = '#4b5563'; ctx.font = '8px monospace'; ctx.textAlign = 'right';
    [0, 5, 10, 20, 45, 90].filter(v => v >= yMin && v <= yMax).forEach(v => {
      ctx.fillText(`${v}°`, PAD.left - 3, ty(v) + 3);
    });
    ctx.textAlign = 'center';
    [0, 0.5, 1.0, 1.5, 2.0, 2.4].forEach(t => {
      ctx.fillText(`${t.toFixed(1)}`, tx(t), PAD.top + CH + 14);
    });

    const label = isUnstable ? 'UNSTABLE' : isOscillating ? 'OSCILLATING' : 'STABLE';
    ctx.fillStyle = lineColor; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'right';
    ctx.fillText(label, PAD.left + CW, PAD.top + 10);
  }, [kp, ki, kd]);

  return (
    <canvas ref={canvasRef} width={380} height={140}
      className="w-full block rounded bg-gray-950 border border-gray-800" />
  );
}

function GainSlider({ param, value, onChange }) {
  const pct = ((value - param.min) / (param.max - param.min)) * 100;
  return (
    <div className="space-y-0.5">
      <div className="flex items-center justify-between text-xs">
        <span className="text-gray-500">{param.label}</span>
        <input type="number"
          className="w-16 text-right bg-gray-900 border border-gray-700 rounded px-1 py-0.5 text-xs text-cyan-200 focus:outline-none focus:border-cyan-700"
          value={typeof value === 'boolean' ? value : Number(value).toFixed(param.step < 0.1 ? 3 : param.step < 1 ? 2 : 1)}
          step={param.step} min={param.min} max={param.max}
          onChange={e => onChange(param.key, param.type === 'bool' ? e.target.checked : parseFloat(e.target.value))}
        />
      </div>
      {param.type !== 'bool' && (
        <div className="relative h-1.5 bg-gray-800 rounded overflow-hidden">
          <div className="absolute h-full rounded" style={{ width: `${pct}%`, background: '#06b6d4' }} />
          <input type="range" className="absolute inset-0 w-full opacity-0 cursor-pointer h-full"
            min={param.min} max={param.max} step={param.step} value={value}
            onChange={e => onChange(param.key, parseFloat(e.target.value))} />
        </div>
      )}
    </div>
  );
}

function PIDView({ gains, setGains, applyPIDGains, applying, applyResult, connected }) {
  const [activeClass, setActiveClass] = useState('empty');
  const warnings = validatePID(gains);
  const classKeys = {
    empty: ['kp_empty','ki_empty','kd_empty'],
    light: ['kp_light','ki_light','kd_light'],
    heavy: ['kp_heavy','ki_heavy','kd_heavy'],
  };
  const overrideKeys = ['override_enabled','override_kp','override_ki','override_kd'];
  const clampKeys    = ['kp_min','kp_max','ki_min','ki_max','kd_min','kd_max'];
  const miscKeys     = ['control_rate','balance_setpoint_rad'];
  const handleChange = (key, val) => setGains(g => ({ ...g, [key]: val }));
  const previewKp = gains.override_enabled ? gains.override_kp : gains[`kp_${activeClass}`] ?? 15;
  const previewKi = gains.override_enabled ? gains.override_ki : gains[`ki_${activeClass}`] ?? 0.5;
  const previewKd = gains.override_enabled ? gains.override_kd : gains[`kd_${activeClass}`] ?? 1.5;
  const paramsByKey = Object.fromEntries(PID_PARAMS.map(p => [p.key, p]));

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-2 flex-wrap">
        <div className="text-cyan-700 text-xs font-bold tracking-widest">PID GAIN EDITOR</div>
        <span className={`text-xs px-1.5 py-0.5 rounded border ml-auto ${connected ? 'text-green-400 border-green-800' : 'text-gray-600 border-gray-700'}`}>
          {connected ? 'LIVE' : 'OFFLINE'}
        </span>
      </div>
      <div className="space-y-1">
        <div className="text-gray-600 text-xs">Simulated step response ({gains.override_enabled ? 'override' : activeClass} gains · 5° disturbance)</div>
        <StepResponseCanvas kp={previewKp} ki={previewKi} kd={previewKd} />
      </div>
      <div className="flex gap-0.5 border-b border-gray-800">
        {Object.keys(classKeys).map(c => (
          <button key={c} onClick={() => setActiveClass(c)}
            className={`px-3 py-1.5 text-xs font-bold tracking-wide border-b-2 transition-colors ${activeClass===c?'border-cyan-500 text-cyan-300':'border-transparent text-gray-600 hover:text-gray-300'}`}
          >{c.toUpperCase()}</button>
        ))}
        {[['override','border-amber-500 text-amber-300'],['clamp','border-purple-500 text-purple-300'],['misc','border-gray-400 text-gray-200']].map(([id, activeStyle]) => (
          <button key={id} onClick={() => setActiveClass(id)}
            className={`px-3 py-1.5 text-xs font-bold tracking-wide border-b-2 transition-colors ${activeClass===id?activeStyle:'border-transparent text-gray-600 hover:text-gray-300'}`}
          >{id.toUpperCase()}</button>
        ))}
      </div>
      <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
        {(activeClass==='override'?overrideKeys:activeClass==='clamp'?clampKeys:activeClass==='misc'?miscKeys:classKeys[activeClass]).map(key => {
          const p = paramsByKey[key]; if (!p) return null;
          return <GainSlider key={key} param={p} value={gains[key]??p.default} onChange={handleChange} />;
        })}
      </div>
      <ValidationBadges warnings={warnings} />
      <div className="flex gap-2 items-center flex-wrap">
        <button onClick={() => applyPIDGains()} disabled={applying||warnings.some(w=>w.level==='error')}
          className="px-4 py-1.5 rounded bg-cyan-950 border border-cyan-700 text-cyan-300 hover:bg-cyan-900 text-xs font-bold disabled:opacity-40">
          {applying?'Applying…':connected?'Apply to Robot':'Save Locally'}
        </button>
        {warnings.some(w=>w.level==='error')&&<span className="text-red-500 text-xs">Fix errors before applying</span>}
        <ApplyResult result={applyResult} />
      </div>
    </div>
  );
}

function SensorsView({ sensors, setSensors, applySensorParams, applying, applyResult, connected }) {
  const handleChange = (key, val) => setSensors(s => ({ ...s, [key]: val }));
  const grouped = {};
  SENSOR_PARAMS.forEach(p => { if (!grouped[p.node]) grouped[p.node] = []; grouped[p.node].push(p); });

  return (
    <div className="space-y-4">
      <div className="text-cyan-700 text-xs font-bold tracking-widest">SENSOR CONFIGURATION</div>
      {Object.entries(grouped).map(([node, params]) => (
        <div key={node} className="bg-gray-950 border border-gray-800 rounded-lg p-3 space-y-3">
          <div className="text-gray-500 text-xs font-bold font-mono">{node}</div>
          <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
            {params.map(p => p.type === 'bool' ? (
              <label key={p.key} className="flex items-center gap-2 text-xs cursor-pointer">
                <div onClick={() => handleChange(p.key, !sensors[p.key])}
                  className={`w-8 h-4 rounded-full relative cursor-pointer transition-colors ${sensors[p.key]?'bg-cyan-700':'bg-gray-700'}`}>
                  <span className={`absolute top-0.5 w-3 h-3 rounded-full bg-white transition-all ${sensors[p.key]?'left-4':'left-0.5'}`}/>
                </div>
                <span className="text-gray-400">{p.label}</span>
              </label>
            ) : (
              <GainSlider key={p.key} param={p} value={sensors[p.key]??p.default} onChange={handleChange} />
            ))}
          </div>
        </div>
      ))}
      <div className="flex gap-2 items-center flex-wrap">
        <button onClick={() => applySensorParams()} disabled={applying}
          className="px-4 py-1.5 rounded bg-cyan-950 border border-cyan-700 text-cyan-300 hover:bg-cyan-900 text-xs font-bold disabled:opacity-40">
          {applying?'Applying…':connected?'Apply to Robot':'Save Locally'}
        </button>
        <ApplyResult result={applyResult} />
      </div>
    </div>
  );
}

function NetworkView({ wsUrl, connected }) {
  const [ddsDomain, setDdsDomain] = useState(() => parseInt(localStorage.getItem('saltybot_dds_domain')||'0',10));
  const [saved, setSaved] = useState(false);
  const urlObj = (() => { try { return new URL(wsUrl); } catch { return null; } })();
  const saveDDS = () => { localStorage.setItem('saltybot_dds_domain', String(ddsDomain)); setSaved(true); setTimeout(()=>setSaved(false),2000); };

  return (
    <div className="space-y-4">
      <div className="text-cyan-700 text-xs font-bold tracking-widest">NETWORK SETTINGS</div>
      <div className="bg-gray-950 border border-gray-800 rounded-lg p-3 space-y-2">
        <div className="text-gray-500 text-xs font-bold">ROSBRIDGE WEBSOCKET</div>
        <div className="grid grid-cols-2 gap-2 text-xs">
          {[['URL', wsUrl, 'text-cyan-300 font-mono truncate'], ['Host', urlObj?.hostname??'—', 'text-gray-300 font-mono'],
            ['Port', urlObj?.port??'9090', 'text-gray-300 font-mono'],
            ['Status', connected?'CONNECTED':'DISCONNECTED', connected?'text-green-400':'text-red-400']
          ].map(([k, v, cls]) => (<><div key={k} className="text-gray-600">{k}</div><div className={cls}>{v}</div></>))}
        </div>
      </div>
      <div className="bg-gray-950 border border-gray-800 rounded-lg p-3 space-y-3">
        <div className="text-gray-500 text-xs font-bold">ROS2 DDS DOMAIN</div>
        <div className="flex items-center gap-3">
          <label className="text-gray-500 text-xs w-24">Domain ID</label>
          <input type="number" min="0" max="232"
            className="w-20 bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-gray-200 focus:outline-none focus:border-cyan-700"
            value={ddsDomain} onChange={e=>setDdsDomain(parseInt(e.target.value)||0)} />
          <button onClick={saveDDS} className="px-3 py-1 rounded bg-cyan-950 border border-cyan-700 text-cyan-300 hover:bg-cyan-900 text-xs">Save</button>
          {saved && <span className="text-green-400 text-xs">Saved</span>}
        </div>
        <div className="text-gray-700 text-xs">Set ROS_DOMAIN_ID on the robot to match. Range 0–232.</div>
      </div>
      <div className="bg-gray-950 border border-gray-800 rounded-lg p-3 space-y-1 text-xs text-gray-600">
        <div className="text-gray-500 font-bold text-xs">REFERENCE PORTS</div>
        <div>Web dashboard: <code className="text-gray-400">8080</code></div>
        <div>Rosbridge: <code className="text-gray-400">ws://&lt;host&gt;:9090</code></div>
        <div>RTSP: <code className="text-gray-400">rtsp://&lt;host&gt;:8554/panoramic</code></div>
        <div>MJPEG: <code className="text-gray-400">http://&lt;host&gt;:8080/stream?topic=/camera/panoramic/compressed</code></div>
      </div>
    </div>
  );
}

function FirmwareView({ firmwareInfo, startFirmwareWatch, connected }) {
  useEffect(() => { if (!connected) return; const unsub = startFirmwareWatch(); return unsub; }, [connected, startFirmwareWatch]);
  const formatUptime = (s) => { if (!s) return '—'; return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`; };
  const rows = [
    ['STM32 Firmware', firmwareInfo?.stm32Version ?? '—'],
    ['Jetson SW',      firmwareInfo?.jetsonVersion ?? '—'],
    ['Last OTA Update',firmwareInfo?.lastOtaDate   ?? '—'],
    ['Hostname',       firmwareInfo?.hostname      ?? '—'],
    ['ROS Distro',     firmwareInfo?.rosDistro     ?? '—'],
    ['Uptime',         formatUptime(firmwareInfo?.uptimeS)],
  ];
  return (
    <div className="space-y-4">
      <div className="flex items-center gap-2">
        <div className="text-cyan-700 text-xs font-bold tracking-widest">FIRMWARE INFO</div>
        {!connected && <span className="text-gray-600 text-xs">(connect to robot to fetch live info)</span>}
      </div>
      <div className="bg-gray-950 border border-gray-800 rounded-lg overflow-hidden">
        {rows.map(([label, value], i) => (
          <div key={label} className={`flex items-center px-4 py-2.5 text-xs ${i%2===0?'bg-gray-950':'bg-[#070712]'}`}>
            <span className="text-gray-500 w-36 shrink-0">{label}</span>
            <span className={`font-mono ${value==='—'?'text-gray-700':'text-cyan-300'}`}>{value}</span>
          </div>
        ))}
      </div>
      {!firmwareInfo && connected && (
        <div className="text-amber-700 text-xs border border-amber-900 rounded p-2">
          Waiting for /diagnostics… Ensure firmware diagnostics keys (stm32_fw_version etc.) are published.
        </div>
      )}
    </div>
  );
}

function DiagnosticsView({ exportDiagnosticsBundle, subscribe, connected }) {
  const [diagData, setDiagData]     = useState(null);
  const [balanceData, setBalanceData] = useState(null);
  const [exporting, setExporting]   = useState(false);

  useEffect(() => {
    if (!connected || !subscribe) return;
    const u1 = subscribe('/diagnostics', 'diagnostic_msgs/DiagnosticArray', msg => setDiagData(msg));
    const u2 = subscribe('/saltybot/balance_state', 'std_msgs/String', msg => {
      try { setBalanceData(JSON.parse(msg.data)); } catch {}
    });
    return () => { u1?.(); u2?.(); };
  }, [connected, subscribe]);

  const errorCount = (diagData?.status??[]).filter(s=>s.level>=2).length;
  const warnCount  = (diagData?.status??[]).filter(s=>s.level===1).length;

  const handleExport = () => {
    setExporting(true);
    exportDiagnosticsBundle(balanceData?{'live':{balanceState:balanceData}}:{}, {});
    setTimeout(()=>setExporting(false), 1000);
  };

  return (
    <div className="space-y-4">
      <div className="text-cyan-700 text-xs font-bold tracking-widest">DIAGNOSTICS EXPORT</div>
      <div className="grid grid-cols-3 gap-3">
        {[['ERRORS',errorCount,errorCount?'text-red-400':'text-gray-600'],['WARNINGS',warnCount,warnCount?'text-amber-400':'text-gray-600'],['NODES',diagData?.status?.length??0,'text-gray-400']].map(([l,c,cl])=>(
          <div key={l} className="bg-gray-950 border border-gray-800 rounded p-3 text-center">
            <div className={`text-2xl font-bold ${cl}`}>{c}</div>
            <div className="text-gray-600 text-xs mt-0.5">{l}</div>
          </div>
        ))}
      </div>
      <div className="bg-gray-950 border border-gray-800 rounded-lg p-4 space-y-3">
        <div className="text-gray-400 text-sm font-bold">Download Diagnostics Bundle</div>
        <div className="text-gray-600 text-xs">Bundle: PID gains, sensor settings, firmware info, live /diagnostics, balance state, timestamp.</div>
        <button onClick={handleExport} disabled={exporting}
          className="px-4 py-2 rounded bg-cyan-950 border border-cyan-700 text-cyan-300 hover:bg-cyan-900 text-xs font-bold disabled:opacity-40">
          {exporting?'Preparing…':'Download JSON Bundle'}
        </button>
        <div className="text-gray-700 text-xs">
          For rosbag: <code className="text-gray-600">ros2 bag record -o saltybot_diag /diagnostics /saltybot/balance_state /odom</code>
        </div>
      </div>
      {diagData?.status?.length>0 && (
        <div className="space-y-1 max-h-64 overflow-y-auto">
          {[...diagData.status].sort((a,b)=>(b.level??0)-(a.level??0)).map((s,i)=>(
            <div key={i} className={`flex items-center gap-2 px-3 py-1.5 rounded border text-xs ${s.level>=2?'bg-red-950 border-red-800':s.level===1?'bg-amber-950 border-amber-800':'bg-gray-950 border-gray-800'}`}>
              <div className={`w-1.5 h-1.5 rounded-full shrink-0 ${s.level>=2?'bg-red-400':s.level===1?'bg-amber-400':'bg-green-400'}`}/>
              <span className="text-gray-300 font-bold truncate flex-1">{s.name}</span>
              <span className={s.level>=2?'text-red-400':s.level===1?'text-amber-400':'text-gray-600'}>{s.message||(s.level===0?'OK':`L${s.level}`)}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function BackupView({ exportSettingsJSON, importSettingsJSON }) {
  const [importText, setImportText] = useState('');
  const [showImport, setShowImport] = useState(false);
  const [msg, setMsg] = useState(null);
  const showMsg = (text, ok=true) => { setMsg({text,ok}); setTimeout(()=>setMsg(null),4000); };

  const handleExport = () => {
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([exportSettingsJSON()],{type:'application/json'}));
    a.download = `saltybot-settings-${new Date().toISOString().slice(0,10)}.json`;
    a.click(); showMsg('Settings exported');
  };
  const handleImport = () => {
    try { importSettingsJSON(importText); setImportText(''); setShowImport(false); showMsg('Settings imported'); }
    catch(e) { showMsg('Import failed: '+e.message, false); }
  };

  return (
    <div className="space-y-4 max-w-lg">
      <div className="text-cyan-700 text-xs font-bold tracking-widest">BACKUP & RESTORE</div>
      <div className="bg-gray-950 border border-gray-800 rounded-lg p-4 space-y-3">
        <div className="text-gray-400 text-sm font-bold">Export Settings</div>
        <div className="text-gray-600 text-xs">Saves PID gains, sensor config and UI preferences to JSON.</div>
        <button onClick={handleExport} className="px-4 py-2 rounded bg-cyan-950 border border-cyan-700 text-cyan-300 hover:bg-cyan-900 text-xs font-bold">Export JSON</button>
      </div>
      <div className="bg-gray-950 border border-gray-800 rounded-lg p-4 space-y-3">
        <div className="text-gray-400 text-sm font-bold">Restore Settings</div>
        <div className="text-gray-600 text-xs">Load a previously exported settings JSON. Click Apply after import to push to the robot.</div>
        <button onClick={() => setShowImport(s=>!s)} className="px-4 py-2 rounded border border-gray-700 text-gray-400 hover:text-gray-200 text-xs">
          {showImport?'Cancel':'Import JSON…'}
        </button>
        {showImport && (
          <div className="space-y-2">
            <textarea rows={8} className="w-full bg-gray-900 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-200 focus:outline-none font-mono"
              placeholder="Paste exported settings JSON here…" value={importText} onChange={e=>setImportText(e.target.value)} />
            <button disabled={!importText.trim()} onClick={handleImport}
              className="px-3 py-1 rounded bg-cyan-950 border border-cyan-700 text-cyan-300 hover:bg-cyan-900 text-xs disabled:opacity-40">Restore</button>
          </div>
        )}
      </div>
      {msg && (
        <div className={`text-xs rounded px-2 py-1 border ${msg.ok?'bg-green-950 border-green-800 text-green-400':'bg-red-950 border-red-800 text-red-400'}`}>{msg.text}</div>
      )}
      <div className="bg-gray-950 border border-gray-800 rounded-lg p-3 text-xs text-gray-600">
        Settings stored in localStorage key <code className="text-gray-500">saltybot_settings_v1</code>. Export to persist across browsers.
      </div>
    </div>
  );
}

export function SettingsPanel({ subscribe, callService, connected = false, wsUrl = '' }) {
  const [view, setView] = useState('PID');
  const settings = useSettings({ callService, subscribe });

  return (
    <div className="space-y-4">
      <div className="flex gap-0.5 border-b border-gray-800 overflow-x-auto">
        {VIEWS.map(v => (
          <button key={v} onClick={() => setView(v)}
            className={`px-3 py-2 text-xs font-bold tracking-wider whitespace-nowrap border-b-2 transition-colors ${
              view===v ? 'border-cyan-500 text-cyan-300' : 'border-transparent text-gray-600 hover:text-gray-300'
            }`}>{v.toUpperCase()}</button>
        ))}
      </div>
      {view==='PID'         && <PIDView gains={settings.gains} setGains={settings.setGains} applyPIDGains={settings.applyPIDGains} applying={settings.applying} applyResult={settings.applyResult} connected={connected} />}
      {view==='Sensors'     && <SensorsView sensors={settings.sensors} setSensors={settings.setSensors} applySensorParams={settings.applySensorParams} applying={settings.applying} applyResult={settings.applyResult} connected={connected} />}
      {view==='Network'     && <NetworkView wsUrl={wsUrl} connected={connected} />}
      {view==='Firmware'    && <FirmwareView firmwareInfo={settings.firmwareInfo} startFirmwareWatch={settings.startFirmwareWatch} connected={connected} />}
      {view==='Diagnostics' && <DiagnosticsView exportDiagnosticsBundle={settings.exportDiagnosticsBundle} subscribe={subscribe} connected={connected} />}
      {view==='Backup'      && <BackupView exportSettingsJSON={settings.exportSettingsJSON} importSettingsJSON={settings.importSettingsJSON} />}
    </div>
  );
}
