#ifndef INDEX_H
#define INDEX_H_H
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html lang="ko"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>CANFD Logger</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;font-size:13px}
nav{background:#161b22;border-bottom:1px solid #30363d;display:flex;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
nav::-webkit-scrollbar{display:none}
nav .nav-title{color:#43cea2;font-weight:700;font-size:1.0em;padding:10px 14px;white-space:nowrap;border-bottom:2px solid transparent}
nav a{display:inline-flex;align-items:center;padding:10px 13px;text-decoration:none;color:#8b949e;font-size:.78em;font-weight:500;border-bottom:2px solid transparent;white-space:nowrap;transition:color .2s,border-color .2s}
nav a:hover{color:#e6edf3}
nav a.active{color:#43cea2;border-bottom-color:#43cea2}
nav .nav-right{margin-left:auto;display:flex;align-items:center;padding:0 14px}
.title{color:#e6edf3;font-weight:700;font-size:15px;margin-right:8px}
.container{padding:12px;display:grid;gap:10px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:12px}
.card h3{font-size:12px;color:#8b949e;margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px}
.stat{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #21262d}
.stat:last-child{border:none} .stat-label{color:#8b949e} .stat-val{color:#e6edf3;font-weight:600}
.stat-val.ok{color:#3fb950} .stat-val.warn{color:#d29922} .stat-val.err{color:#f85149}
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600}
.badge-green{background:#033a16;color:#3fb950;border:1px solid #238636}
.badge-red{background:#3d0000;color:#f85149;border:1px solid #da3633}
.badge-blue{background:#051d4d;color:#58a6ff;border:1px solid #1f6feb}
.badge-orange{background:#3d2200;color:#d29922;border:1px solid #9e6a03}
.badge-purple{background:#2d1b69;color:#a371f7;border:1px solid #6e40c9}
.btn{padding:6px 14px;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:.2s}
.btn-green{background:#238636;color:#fff} .btn-green:hover{background:#2ea043}
.btn-red{background:#da3633;color:#fff} .btn-red:hover{background:#f85149}
.btn-blue{background:#1f6feb;color:#fff} .btn-blue:hover{background:#388bfd}
.btn-gray{background:#21262d;color:#c9d1d9;border:1px solid #30363d} .btn-gray:hover{background:#30363d}
.btn:disabled{opacity:.4;cursor:not-allowed}
.log-ctrl{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px}
select{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:12px}
table{width:100%;border-collapse:collapse;font-size:11px}
th{background:#21262d;color:#8b949e;padding:6px 8px;text-align:left;font-weight:600;position:sticky;top:0}
td{padding:5px 8px;border-bottom:1px solid #21262d;font-family:monospace}
tr:hover td{background:#1c2128}
.tbl-wrap{max-height:380px;overflow-y:auto;border-radius:6px;border:1px solid #30363d}
.fd-tag{display:inline-block;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:700;margin-left:3px}
.fd-tag.fd{background:#2d1b69;color:#a371f7} .fd-tag.brs{background:#033a16;color:#3fb950}
.fd-tag.ext{background:#051d4d;color:#58a6ff}
.progress-bar{background:#21262d;border-radius:4px;height:8px;overflow:hidden}
.progress-fill{height:100%;background:#1f6feb;transition:.5s;border-radius:4px}
.progress-fill.warn{background:#d29922} .progress-fill.err{background:#f85149}
.ws-status{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}
.ws-ok{background:#3fb950} .ws-err{background:#f85149}
input[type=text]{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:4px 8px;border-radius:6px;font-size:12px;width:100%}
@media(max-width:700px){.grid2,.grid3{grid-template-columns:1fr}}
</style>
</head><body>
<nav>
  <span class="nav-title">⚡ CANFD Logger</span>
  <a href="/" class="active">대시보드</a>
  <a href="/transmit">송신</a>
  <a href="/graph">그래프</a>
  <a href="/graph-view">그래프 뷰어</a>
  <a href="/settings">설정</a>
  <span class="nav-right">
    <span class="ws-status ws-err" id="wsLed"></span>
    <span id="wsLabel" style="font-size:11px;color:#8b949e">연결 중...</span>
  </span>
</nav>

<div class="container">
  <!-- 상태 카드 -->
  <div class="grid3">
    <div class="card">
      <h3>CAN FD 상태</h3>
      <div class="stat"><span class="stat-label">속도 설정</span><span class="stat-val" id="speedName">-</span></div>
      <div class="stat"><span class="stat-label">수신 모드</span><span id="listenBadge"><span class="badge badge-blue">-</span></span></div>
      <div class="stat"><span class="stat-label">총 수신</span><span class="stat-val" id="totalMsg">0</span></div>
      <div class="stat"><span class="stat-label">Msg/s</span><span class="stat-val ok" id="msgPerSec">0</span></div>
      <div class="stat"><span class="stat-label">버스 부하</span><span class="stat-val" id="busLoad">0%</span></div>
      <div class="stat"><span class="stat-label">총 송신</span><span class="stat-val" id="totalTx">0</span></div>
    </div>
    <div class="card">
      <h3>링버퍼 / SD</h3>
      <div class="stat"><span class="stat-label">SD 카드</span><span id="sdBadge"></span></div>
      <div class="stat"><span class="stat-label">로깅 상태</span><span id="logBadge"></span></div>
      <div class="stat"><span class="stat-label">파일 크기</span><span class="stat-val" id="fileSize">-</span></div>
      <div class="stat"><span class="stat-label">현재 파일</span><span class="stat-val" id="curFile" style="font-size:10px;word-break:break-all">-</span></div>
      <div style="margin-top:8px">
        <div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px">
          <span style="color:#8b949e">링버퍼</span><span id="ringLabel" style="color:#8b949e">0/4096</span>
        </div>
        <div class="progress-bar"><div class="progress-fill" id="ringFill" style="width:0%"></div></div>
      </div>
      <div class="stat" style="margin-top:8px"><span class="stat-label">Drop</span><span class="stat-val err" id="dropped">0</span></div>
    </div>
    <div class="card">
      <h3>시간 / 시스템</h3>
      <div class="stat"><span class="stat-label">시각 동기화</span><span id="timeBadge"></span></div>
      <div class="stat"><span class="stat-label">RTC</span><span id="rtcBadge"></span></div>
      <div class="stat"><span class="stat-label">NTP 동기 수</span><span class="stat-val" id="syncCount">0</span></div>
      <div class="stat"><span class="stat-label">RTC 동기 수</span><span class="stat-val" id="rtcSyncCount">0</span></div>
      <div class="stat"><span class="stat-label">현재 시각</span><span class="stat-val" id="curTime" style="font-size:11px">-</span></div>
      <div class="stat"><span class="stat-label">AP IP (항상 접속가능)</span><span class="stat-val ok" id="apIP" style="font-size:11px">-</span></div>
      <div class="stat"><span class="stat-label">STA IP</span><span class="stat-val" id="staIP" style="font-size:11px;color:#8b949e">-</span></div>
      <div style="margin-top:8px;display:flex;gap:6px">
        <button class="btn btn-gray" onclick="syncTime()">📱 시간 동기화</button>
        <button class="btn btn-gray" onclick="clearStats()">🔄 통계 초기화</button>
      </div>
    </div>
  </div>

  <!-- 로깅 제어 -->
  <div class="card">
    <h3>로깅 제어</h3>
    <div class="log-ctrl">
      <select id="fmtSel">
        <option value="bin">BIN (고속/최대 성능)</option>
        <option value="csv">CSV (텍스트)</option>
        <option value="pcap">PCAP (Wireshark)</option>
      </select>
      <button class="btn btn-green" id="startBtn" onclick="startLog()">▶ 시작</button>
      <button class="btn btn-red"   id="stopBtn"  onclick="stopLog()" disabled>■ 중지</button>
      <button class="btn btn-blue"  onclick="getFiles()">📂 파일 목록</button>
      <button class="btn btn-gray"  onclick="hwReset()">🔌 재시작</button>
    </div>
    <div id="logInfo" style="font-size:11px;color:#8b949e;margin-top:4px"></div>
  </div>

  <!-- 메시지 모니터 -->
  <div class="card">
    <h3>CAN FD 메시지 모니터 (최근 30개 ID)</h3>
    <div class="tbl-wrap">
      <table id="msgTable">
        <thead><tr>
          <th>CAN ID</th><th>타입</th><th>DLC</th><th>길이</th><th>수신 수</th><th>데이터 (hex)</th>
        </tr></thead>
        <tbody id="msgBody"></tbody>
      </table>
    </div>
  </div>

  <!-- 파일 목록 -->
  <div class="card" id="fileCard" style="display:none">
    <h3>SD 파일 목록</h3>
    <div class="tbl-wrap">
      <table id="fileTable">
        <thead><tr><th>파일명</th><th>크기</th><th>코멘트</th><th>동작</th></tr></thead>
        <tbody id="fileBody"></tbody>
      </table>
    </div>
  </div>
</div>

<script>
let ws, reconnTimer;
function fmtBytes(b){if(b<1024)return b+'B';if(b<1048576)return(b/1024).toFixed(1)+'KB';return(b/1048576).toFixed(2)+'MB';}
function fmtNum(n){return n>=1000000?(n/1000000).toFixed(2)+'M':n>=1000?(n/1000).toFixed(1)+'K':n;}

function connect(){
  ws=new WebSocket('ws://'+location.hostname+':81/');
  ws.onopen=()=>{
    document.getElementById('wsLed').className='ws-status ws-ok';
    document.getElementById('wsLabel').textContent='연결됨';
    clearTimeout(reconnTimer);
  };
  ws.onclose=()=>{
    document.getElementById('wsLed').className='ws-status ws-err';
    document.getElementById('wsLabel').textContent='연결 끊김';
    reconnTimer=setTimeout(connect,3000);
  };
  ws.onmessage=e=>{
    try{ handleMsg(JSON.parse(e.data)); }catch(ex){}
  };
}

function handleMsg(d){
  if(d.type==='update'){
    // 속도/모드
    document.getElementById('speedName').textContent=d.speedName||'-';
    document.getElementById('listenBadge').innerHTML=d.listenOnly?
      '<span class="badge badge-orange">Listen Only</span>':
      '<span class="badge badge-green">Normal</span>';
    // 통계
    document.getElementById('totalMsg').textContent=fmtNum(d.totalMsg||0);
    document.getElementById('msgPerSec').textContent=(d.msgPerSec||0).toLocaleString();
    document.getElementById('busLoad').textContent=(d.busLoad||0)+'%';
    document.getElementById('totalTx').textContent=fmtNum(d.totalTx||0);
    // SD
    document.getElementById('sdBadge').innerHTML=d.sdReady?
      '<span class="badge badge-green">준비됨</span>':'<span class="badge badge-red">없음</span>';
    document.getElementById('logBadge').innerHTML=d.logging?
      '<span class="badge badge-green">● 로깅 중</span>':'<span class="badge badge-red">정지</span>';
    document.getElementById('fileSize').textContent=fmtBytes(d.fileSize||0);
    document.getElementById('curFile').textContent=d.currentFile||'-';
    document.getElementById('logInfo').textContent=d.logging?
      '형식: '+d.logFormat+' | '+fmtBytes(d.fileSize||0):'';
    // 링버퍼
    let used=d.ringUsed||0, size=d.ringSize||4096;
    let pct=Math.round(used/size*100);
    document.getElementById('ringLabel').textContent=used+'/'+size;
    let fill=document.getElementById('ringFill');
    fill.style.width=pct+'%';
    fill.className='progress-fill'+(pct>80?' err':pct>50?' warn':'');
    document.getElementById('dropped').textContent=(d.dropped||0).toLocaleString();
    // 시간
    document.getElementById('timeBadge').innerHTML=d.timeSync?
      '<span class="badge badge-green">동기됨</span>':'<span class="badge badge-red">미동기</span>';
    document.getElementById('rtcBadge').innerHTML=d.rtcAvail?
      '<span class="badge badge-blue">감지됨</span>':'<span class="badge badge-red">없음</span>';
    document.getElementById('syncCount').textContent=d.syncCount||0;
    document.getElementById('rtcSyncCount').textContent=d.rtcSyncCnt||0;
    document.getElementById('curTime').textContent=d.timestamp?new Date(d.timestamp*1000).toLocaleString('ko-KR'):'';
    if(d.apIP) document.getElementById('apIP').textContent=d.apIP;
    if(d.staIP) document.getElementById('staIP').textContent=d.staConnected?d.staIP:'미연결';
    // 버튼
    document.getElementById('startBtn').disabled=!!d.logging;
    document.getElementById('stopBtn').disabled=!d.logging;
    // 메시지 테이블
    if(d.messages) updateTable(d.messages);
  }
  else if(d.type==='files') renderFiles(d.files||[]);
}

let msgMap={};
function updateTable(msgs){
  msgs.forEach(m=>{msgMap[m.id]=m;});
  let ids=Object.keys(msgMap).sort((a,b)=>parseInt(a)-parseInt(b));
  let html='';
  ids.forEach(idKey=>{
    let m=msgMap[idKey];
    let idStr=m.ext?'<span class="fd-tag ext">EXT</span>0x'+(m.id>>>0).toString(16).toUpperCase().padStart(8,'0'):
                    '0x'+(m.id>>>0).toString(16).toUpperCase().padStart(3,'0');
    let type=(m.fd?'<span class="fd-tag fd">FD</span>':'CAN')+
             (m.brs?'<span class="fd-tag brs">BRS</span>':'');
    let dat=(m.data||[]).map(b=>b.toString(16).toUpperCase().padStart(2,'0')).join(' ');
    if(m.len>16) dat+=' ...';
    html+=`<tr><td>${idStr}</td><td>${type}</td><td>${m.dlc}</td><td>${m.len}B</td><td>${(m.count||0).toLocaleString()}</td><td style="font-family:monospace;font-size:11px">${dat}</td></tr>`;
  });
  document.getElementById('msgBody').innerHTML=html;
}

function renderFiles(files){
  let card=document.getElementById('fileCard');
  card.style.display='block';
  let html='';
  files.sort((a,b)=>a.name>b.name?-1:1);
  files.forEach(f=>{
    let ext=f.name.split('.').pop().toLowerCase();
    html+=`<tr>
      <td><a href="/download?file=${f.name}" style="color:#58a6ff">${f.name}</a></td>
      <td>${fmtBytes(f.size)}</td>
      <td><input type="text" value="${f.comment||''}" placeholder="코멘트..." onchange="saveComment('${f.name}',this.value)" style="width:120px"></td>
      <td><button class="btn btn-red" onclick="delFile('${f.name}')" style="padding:2px 8px;font-size:11px">삭제</button></td>
    </tr>`;
  });
  document.getElementById('fileBody').innerHTML=html;
}

function startLog(){ws.send(JSON.stringify({cmd:'startLogging',format:document.getElementById('fmtSel').value}));}
function stopLog(){ws.send(JSON.stringify({cmd:'stopLogging'}));}
function getFiles(){ws.send(JSON.stringify({cmd:'getFiles'}));}
function hwReset(){if(confirm('장치를 재시작할까요?'))ws.send(JSON.stringify({cmd:'hwReset'}));}
function clearStats(){ws.send(JSON.stringify({cmd:'clearStats'}));}
function delFile(n){if(confirm(n+' 삭제?'))ws.send(JSON.stringify({cmd:'deleteFile',filename:n}));}
function saveComment(n,c){ws.send(JSON.stringify({cmd:'addComment',filename:n,comment:c}));}
function syncTime(){
  let now=new Date();
  ws.send(JSON.stringify({cmd:'syncTime',year:now.getFullYear(),month:now.getMonth()+1,
    day:now.getDate(),hour:now.getHours(),minute:now.getMinutes(),second:now.getSeconds()}));
}
connect();
</script>
</body></html>
)rawliteral";
 
#endif