Compare commits

2 Commits
Author SHA1 Message Date
oskarvitaliiandClaude Opus 4.8 f8294d4aed feat(web): scrollable Forecast log with fixed height
Cap the log panel at ~216px with a thin themed scrollbar so a long
forecast history does not stretch the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:08:21 +07:00
oskarvitaliiandClaude Opus 4.8 4b59b8936b fix: stream large API responses to stop heap-exhaustion truncation
Serving /api/history (and /api/forecasts) built a ~12 KB String +
JsonDocument every 15 s; over days this fragmented the ESP8266 heap until
the TCP stack could no longer send a full response, causing the web page
to fail with ERR_CONTENT_LENGTH_MISMATCH. Stream both array endpoints via
chunked transfer (small stack buffer, no big allocations). Add free-heap
to /api/current and Serial, plus a low-memory safety restart (history is
persisted first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 06:57:09 +07:00
3 changed files with 36 additions and 20 deletions
+9
View File
@@ -88,6 +88,15 @@ void loop() {
tSample = now; tSample = now;
sampleNow(); sampleNow();
displayRender(g_state); displayRender(g_state);
Serial.printf("heap=%u maxblock=%u\n", ESP.getFreeHeap(), ESP.getMaxFreeBlockSize());
// Safety net: if the largest contiguous block gets too small, the TCP stack
// can no longer send responses. Persist and reboot to defragment.
if (ESP.getMaxFreeBlockSize() < 6000) {
Serial.println(F("Low memory -> saving history and restarting"));
historySave();
delay(50);
ESP.restart();
}
} }
if (now - tHistory >= HISTORY_INTERVAL_MS) { if (now - tHistory >= HISTORY_INTERVAL_MS) {
+5 -1
View File
@@ -103,7 +103,11 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
.cap{color:var(--muted); font-size:.74rem; letter-spacing:.06em} .cap{color:var(--muted); font-size:.74rem; letter-spacing:.06em}
/* Forecast log */ /* Forecast log */
.log{list-style:none; margin:2px 0 0; padding:0} .log{list-style:none; margin:2px 0 0; padding:0 8px 0 0; max-height:216px; overflow-y:auto;
scrollbar-width:thin; scrollbar-color:var(--line) transparent}
.log::-webkit-scrollbar{width:8px}
.log::-webkit-scrollbar-thumb{background:var(--line); border-radius:8px}
.log::-webkit-scrollbar-track{background:transparent}
.log li{display:flex; align-items:center; gap:13px; padding:12px 0; border-top:1px solid var(--line)} .log li{display:flex; align-items:center; gap:13px; padding:12px 0; border-top:1px solid var(--line)}
.log li:first-child{border-top:0} .log li:first-child{border-top:0}
.log .dot{width:9px; height:9px; border-radius:50%; flex:0 0 auto} .log .dot{width:9px; height:9px; border-radius:50%; flex:0 0 auto}
+22 -19
View File
@@ -27,41 +27,44 @@ static void handleCurrent() {
doc["haveTrend"] = g_state.haveTrend; doc["haveTrend"] = g_state.haveTrend;
doc["forecast"] = g_state.haveTrend ? g_state.forecast.text : "Collecting data..."; doc["forecast"] = g_state.haveTrend ? g_state.forecast.text : "Collecting data...";
doc["category"] = g_state.haveTrend ? categoryShort(g_state.forecast.category) : "..."; doc["category"] = g_state.haveTrend ? categoryShort(g_state.forecast.category) : "...";
doc["heap"] = ESP.getFreeHeap(); // for monitoring memory health
String out; String out;
serializeJson(doc, out); serializeJson(doc, out);
server.send(200, "application/json", out); server.send(200, "application/json", out);
} }
// Streamed (chunked) so we never build a large String/JsonDocument on the heap.
static void handleHistory() { static void handleHistory() {
JsonDocument doc; server.setContentLength(CONTENT_LENGTH_UNKNOWN);
JsonArray arr = doc.to<JsonArray>(); server.send(200, "application/json", "");
server.sendContent("[");
char buf[80];
int n = historyCount(); int n = historyCount();
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
Sample s = historyGet(i); Sample s = historyGet(i);
JsonObject o = arr.add<JsonObject>(); snprintf(buf, sizeof(buf), "%s{\"t\":%lu,\"msl\":%.1f,\"temp\":%.1f}",
o["t"] = s.epoch; i ? "," : "", (unsigned long)s.epoch, s.mslHpa, s.tempC);
o["msl"] = s.mslHpa; server.sendContent(buf);
o["temp"] = s.tempC;
} }
String out; server.sendContent("]");
serializeJson(doc, out);
server.send(200, "application/json", out);
} }
// Streamed (chunked). Forecast phrases contain no quotes/backslashes, so they
// are safe to embed directly in JSON.
static void handleForecasts() { static void handleForecasts() {
JsonDocument doc; server.setContentLength(CONTENT_LENGTH_UNKNOWN);
JsonArray arr = doc.to<JsonArray>(); server.send(200, "application/json", "");
server.sendContent("[");
char buf[128];
int n = flogCount(); int n = flogCount();
for (int i = n - 1; i >= 0; i--) { // newest first for (int i = n - 1, first = 1; i >= 0; i--, first = 0) { // newest first
FcastEntry e = flogGet(i); FcastEntry e = flogGet(i);
JsonObject o = arr.add<JsonObject>(); snprintf(buf, sizeof(buf), "%s{\"t\":%lu,\"cat\":\"%s\",\"text\":\"%s\"}",
o["t"] = e.epoch; first ? "" : ",", (unsigned long)e.epoch,
o["cat"] = categoryShort((WxCategory)e.cat); categoryShort((WxCategory)e.cat), forecastTextForLetter(e.letter));
o["text"] = forecastTextForLetter(e.letter); server.sendContent(buf);
} }
String out; server.sendContent("]");
serializeJson(doc, out);
server.send(200, "application/json", out);
} }
static void handleGetSettings() { static void handleGetSettings() {