This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include "wifi_manager.h"
|
||||
#include "dns_server.h"
|
||||
#include "web_server.h"
|
||||
#include "serial_log.h"
|
||||
|
||||
static const char *TAG = "web";
|
||||
|
||||
@@ -67,6 +68,7 @@ static const char SPA_PAGE[] =
|
||||
"<div class=\"tabs\">"
|
||||
"<button id=\"tb-cards\" class=\"tab-btn active\" onclick=\"tab('cards')\">🃏 Card Pairing</button>"
|
||||
"<button id=\"tb-wifi\" class=\"tab-btn\" onclick=\"tab('wifi')\">📶 Wi-Fi Settings</button>"
|
||||
"<button id=\"tb-serial\" class=\"tab-btn\" onclick=\"tab('serial')\">💻 Serial Log</button>"
|
||||
"</div>"
|
||||
"<!-- Tab: Cards -->"
|
||||
"<div id=\"pane-cards\" class=\"tab-pane active\">"
|
||||
@@ -125,13 +127,49 @@ static const char SPA_PAGE[] =
|
||||
"</div>"
|
||||
"<div id=\"saved-box\"><p class=\"card-none\">Loading saved networks...</p></div>"
|
||||
"</div>"
|
||||
"<!-- Tab: Serial Log -->"
|
||||
"<div id=\"pane-serial\" class=\"tab-pane\">"
|
||||
"<div style=\"background:#1a1a2e;padding:1em;border-radius:8px;border:1px solid #2d2d48;margin-bottom:1.5em\">"
|
||||
"<div class=\"header-row\" style=\"margin-bottom:.8em;flex-wrap:wrap;gap:8px\">"
|
||||
"<div style=\"display:flex;align-items:center;gap:8px\">"
|
||||
"<span id=\"log-badge\" class=\"status-badge badge-ok\" style=\"margin-bottom:0\">● Live</span>"
|
||||
"<span id=\"log-info\" style=\"font-size:.85em;color:#aaa\">0 lines</span>"
|
||||
"</div>"
|
||||
"<div style=\"display:flex;gap:6px;flex-wrap:wrap\">"
|
||||
"<button class=\"btn\" id=\"btn-pause\" onclick=\"toggleLogPause()\" style=\"margin:0;font-size:.85em\">Pause</button>"
|
||||
"<button class=\"btn\" onclick=\"clearLogs()\" style=\"margin:0;font-size:.85em\">Clear</button>"
|
||||
"<button class=\"btn\" onclick=\"copyLogs()\" style=\"margin:0;font-size:.85em\">Copy</button>"
|
||||
"<button class=\"btn\" onclick=\"downloadLogs()\" style=\"margin:0;font-size:.85em\">Save</button>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
"<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:.6em;font-size:.85em;color:#ccc;flex-wrap:wrap;gap:8px\">"
|
||||
"<label style=\"display:flex;align-items:center;gap:6px;cursor:pointer\">"
|
||||
"<input type=\"checkbox\" id=\"log-scroll\" checked style=\"width:auto;margin:0\"> Auto-scroll"
|
||||
"</label>"
|
||||
"<div style=\"display:flex;align-items:center;gap:6px\">"
|
||||
"<span>Filter:</span>"
|
||||
"<select id=\"log-filter\" onchange=\"renderLogs()\" style=\"width:auto;padding:.3em .6em;font-size:.85em\">"
|
||||
"<option value=\"\">All levels</option>"
|
||||
"<option value=\"E\">Errors (E)</option>"
|
||||
"<option value=\"W\">Warnings & Errors (W/E)</option>"
|
||||
"<option value=\"I\">Info, Warn, Error (I/W/E)</option>"
|
||||
"</select>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
"<div id=\"term\" style=\"background:#0d0d17;border:1px solid #28283f;border-radius:6px;padding:.75em;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.45;height:420px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;color:#ddd\"></div>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
"<script>"
|
||||
"function tab(t){"
|
||||
"document.getElementById('tb-cards').className='tab-btn'+(t==='cards'?' active':'');"
|
||||
"document.getElementById('tb-wifi').className='tab-btn'+(t==='wifi'?' active':'');"
|
||||
"document.getElementById('tb-serial').className='tab-btn'+(t==='serial'?' active':'');"
|
||||
"document.getElementById('pane-cards').className='tab-pane'+(t==='cards'?' active':'');"
|
||||
"document.getElementById('pane-wifi').className='tab-pane'+(t==='wifi'?' active':'');"
|
||||
"document.getElementById('pane-serial').className='tab-pane'+(t==='serial'?' active':'');"
|
||||
"if(t==='wifi'){refreshWifiStatus();loadSavedWifi();}"
|
||||
"if(t==='serial'){if(!logPolling)startLogPoll();}"
|
||||
"else{stopLogPoll();}"
|
||||
"}"
|
||||
"var lastUid='';var suggestions=[];"
|
||||
"function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}"
|
||||
@@ -271,6 +309,73 @@ static const char SPA_PAGE[] =
|
||||
" else{alert('Failed to forget '+s);}"
|
||||
" }).catch(function(e){alert('Error: '+e);});"
|
||||
"}"
|
||||
"var logOffset=0;var logLines=[];var logPolling=false;var logPaused=false;var logTimer=null;"
|
||||
"function parseLogLine(str){"
|
||||
" var clean=str.replace(/\\x1b\\[[0-9;]*m/g,'');"
|
||||
" var lvl='I';var m=clean.match(/^([EWIDV])\\s+\\(/);"
|
||||
" if(m)lvl=m[1];"
|
||||
" var html=esc(clean);"
|
||||
" if(lvl==='E')html='<span style=\"color:#ff6b6b;font-weight:600\">'+html+'</span>';"
|
||||
" else if(lvl==='W')html='<span style=\"color:#ffd75a\">'+html+'</span>';"
|
||||
" else if(lvl==='I')html='<span style=\"color:#7fd77f\">'+html+'</span>';"
|
||||
" else if(lvl==='D')html='<span style=\"color:#6be5ff\">'+html+'</span>';"
|
||||
" else html='<span style=\"color:#aaa\">'+html+'</span>';"
|
||||
" return {raw:clean,lvl:lvl,html:html};"
|
||||
"}"
|
||||
"function renderLogs(){"
|
||||
" var filter=document.getElementById('log-filter').value;"
|
||||
" var term=document.getElementById('term');"
|
||||
" var auto=document.getElementById('log-scroll').checked;"
|
||||
" var filtered=logLines.filter(function(item){"
|
||||
" if(!filter)return true;"
|
||||
" if(filter==='E')return item.lvl==='E';"
|
||||
" if(filter==='W')return item.lvl==='W'||item.lvl==='E';"
|
||||
" if(filter==='I')return item.lvl==='I'||item.lvl==='W'||item.lvl==='E';"
|
||||
" return true;"
|
||||
" });"
|
||||
" term.innerHTML=filtered.map(function(item){return item.html;}).join('\\n');"
|
||||
" document.getElementById('log-info').textContent=logLines.length+' lines';"
|
||||
" if(auto)term.scrollTop=term.scrollHeight;"
|
||||
"}"
|
||||
"function fetchLogs(){"
|
||||
" if(logPaused)return;"
|
||||
" fetch('/api/logs?offset='+logOffset).then(function(r){"
|
||||
" var next=r.headers.get('X-Log-Offset');"
|
||||
" if(next)logOffset=parseInt(next,10);"
|
||||
" return r.text();"
|
||||
" }).then(function(txt){"
|
||||
" if(!txt||txt.length===0)return;"
|
||||
" var lines=txt.split('\\n');var added=false;"
|
||||
" for(var i=0;i<lines.length;i++){"
|
||||
" var l=lines[i];"
|
||||
" if(l.length>0||i<lines.length-1){logLines.push(parseLogLine(l));added=true;}"
|
||||
" }"
|
||||
" if(logLines.length>1200)logLines=logLines.slice(logLines.length-1000);"
|
||||
" if(added)renderLogs();"
|
||||
" }).catch(function(){});"
|
||||
"}"
|
||||
"function startLogPoll(){logPolling=true;fetchLogs();if(logTimer)clearInterval(logTimer);logTimer=setInterval(fetchLogs,1200);}"
|
||||
"function stopLogPoll(){logPolling=false;if(logTimer){clearInterval(logTimer);logTimer=null;}}"
|
||||
"function toggleLogPause(){"
|
||||
" logPaused=!logPaused;var btn=document.getElementById('btn-pause');var badge=document.getElementById('log-badge');"
|
||||
" if(logPaused){btn.textContent='Resume';badge.className='status-badge badge-bad';badge.textContent='⏸ Paused';}"
|
||||
" else{btn.textContent='Pause';badge.className='status-badge badge-ok';badge.textContent='● Live';fetchLogs();}"
|
||||
"}"
|
||||
"function clearLogs(){"
|
||||
" fetch('/api/logs/clear',{method:'POST'}).catch(function(){});"
|
||||
" logLines=[];document.getElementById('term').innerHTML='';"
|
||||
" document.getElementById('log-info').textContent='0 lines';"
|
||||
"}"
|
||||
"function copyLogs(){"
|
||||
" var t=logLines.map(function(item){return item.raw;}).join('\\n');"
|
||||
" navigator.clipboard.writeText(t).then(function(){alert('Logs copied to clipboard');}).catch(function(){alert('Copy failed');});"
|
||||
"}"
|
||||
"function downloadLogs(){"
|
||||
" var t=logLines.map(function(item){return item.raw;}).join('\\n');"
|
||||
" var b=new Blob([t],{type:'text/plain'});var u=URL.createObjectURL(b);"
|
||||
" var a=document.createElement('a');a.href=u;a.download='mtg-companion-serial.log';"
|
||||
" document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(u);"
|
||||
"}"
|
||||
"poll();loadCards();refreshWifiStatus();"
|
||||
"</script></body></html>";
|
||||
|
||||
@@ -288,6 +393,8 @@ static esp_err_t handle_wifi_scan(httpd_req_t *req);
|
||||
static esp_err_t handle_wifi_saved(httpd_req_t *req);
|
||||
static esp_err_t handle_wifi_connect(httpd_req_t *req);
|
||||
static esp_err_t handle_wifi_forget(httpd_req_t *req);
|
||||
static esp_err_t handle_logs(httpd_req_t *req);
|
||||
static esp_err_t handle_logs_clear(httpd_req_t *req);
|
||||
|
||||
static const httpd_uri_t URI_ROOT = {
|
||||
.uri = "/", .method = HTTP_GET, .handler = handle_root, .user_ctx = NULL
|
||||
@@ -325,6 +432,14 @@ static const httpd_uri_t URI_WIFI_FORGET = {
|
||||
.uri = "/api/wifi/forget", .method = HTTP_POST, .handler = handle_wifi_forget, .user_ctx = NULL
|
||||
};
|
||||
|
||||
/* Serial Log URIs */
|
||||
static const httpd_uri_t URI_LOGS = {
|
||||
.uri = "/api/logs", .method = HTTP_GET, .handler = handle_logs, .user_ctx = NULL
|
||||
};
|
||||
static const httpd_uri_t URI_LOGS_CLEAR = {
|
||||
.uri = "/api/logs/clear", .method = HTTP_POST, .handler = handle_logs_clear, .user_ctx = NULL
|
||||
};
|
||||
|
||||
/* Captive Portal Probe URIs */
|
||||
static const httpd_uri_t URI_CP_APPLE = {
|
||||
.uri = "/hotspot-detect.html", .method = HTTP_GET, .handler = handle_captive_portal, .user_ctx = NULL
|
||||
@@ -803,6 +918,50 @@ static esp_err_t handle_wifi_forget(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Serial Log endpoints */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static esp_err_t handle_logs(httpd_req_t *req)
|
||||
{
|
||||
uint32_t offset = 0;
|
||||
char query[64] = {0};
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
|
||||
char val[24] = {0};
|
||||
if (httpd_query_key_value(query, "offset", val, sizeof(val)) == ESP_OK) {
|
||||
offset = (uint32_t)strtoul(val, NULL, 10);
|
||||
}
|
||||
}
|
||||
|
||||
size_t chunk_size = 4096;
|
||||
char *buf = malloc(chunk_size);
|
||||
if (buf == NULL) {
|
||||
httpd_resp_send_500(req);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint32_t next_offset = offset;
|
||||
size_t bytes_read = serial_log_read(offset, buf, chunk_size, &next_offset);
|
||||
|
||||
char off_str[24];
|
||||
snprintf(off_str, sizeof(off_str), "%lu", (unsigned long)next_offset);
|
||||
|
||||
httpd_resp_set_type(req, "text/plain");
|
||||
httpd_resp_set_hdr(req, "X-Log-Offset", off_str);
|
||||
httpd_resp_set_hdr(req, "Access-Control-Expose-Headers", "X-Log-Offset");
|
||||
|
||||
esp_err_t ret = httpd_resp_send(req, buf, (ssize_t)bytes_read);
|
||||
free(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t handle_logs_clear(httpd_req_t *req)
|
||||
{
|
||||
serial_log_clear();
|
||||
send_json(req, 200, "{\"ok\":true}\n");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* SoftAP + server */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -841,7 +1000,7 @@ esp_err_t web_server_start(void)
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = 8 * 1024;
|
||||
config.max_uri_handlers = 24;
|
||||
config.max_uri_handlers = 32;
|
||||
config.lru_purge_enable = true;
|
||||
config.keep_alive_enable = true;
|
||||
config.max_open_sockets = 4;
|
||||
@@ -867,6 +1026,10 @@ esp_err_t web_server_start(void)
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_WIFI_CONNECT));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_WIFI_FORGET));
|
||||
|
||||
/* Serial Log APIs */
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_LOGS));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_LOGS_CLEAR));
|
||||
|
||||
/* Captive Portal OS Probes */
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CP_APPLE));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CP_204_1));
|
||||
|
||||
Reference in New Issue
Block a user