## Summary
* **What is the goal of this PR?** Fix a reproducible crash: opening the
web UI's Settings page reliably crashes the device (silent reboot to
Home) under real-world conditions.
* **What changes are included?** `SettingsPage.html` fired three
unawaited `fetch()` calls on page load (`/api/settings`, `/api/wifi`,
`/api/opds`), opening simultaneous connections to the device's web
server. That server (Arduino's `WebServer`) handles one client at a
time; a genuinely concurrent second/third connection can stall a
response mid-stream for multiple seconds — long enough to trip the
firmware's watchdogs (300ms interrupt WDT / 5s task WDT, both
panic-on-timeout in this project's config). This PR makes the three
initial loads run sequentially via a small async IIFE instead of firing
them concurrently. Same end state for the page, no visible behavior
change.
## Scope Check
CrossPoint is intentionally narrow. See
[SCOPE.md](../blob/master/SCOPE.md) and
[ROADMAP.md](../blob/master/ROADMAP.md).
- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme.
- [x] This PR is **not** a new external network connector.
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] This is a bug fix for a reproducible crash, not new functionality.
- [x] Does not touch `freeink-sdk/`, `lib/hal/`, the bootloader, OTA, or
recovery code.
## Testing performed
Verified live on an X4 (driven via the `CMD:WIFI` serial debug command
from the companion PR, since I didn't have physical access to the device
for part of this investigation):
- Root-caused via a temporary per-entry heap/timing log in
`handleGetSettings()` (not included in this diff): under concurrent
load, individual `sendContent()` calls stalled 1-3+ seconds between
simple, static settings entries that should serialize in well under a
millisecond each.
- Reproduced the crash directly: firing `/api/settings`, `/api/wifi`,
and `/api/opds` concurrently (mirroring the page's old JS behavior)
crashed the device in 2/2 attempts. The companion PR (watchdog reset
detection) let the second crash actually get logged: reset reason
`ESP_RST_TASK_WDT`, panic confirmed.
- After the fix: same three endpoints, hit sequentially (matching the
new JS), succeeded cleanly across dozens of repeats (single-shot and a
60-request hammer loop) with no crashes and stable free-heap.
- Confirmed the live device served the updated, gzip-compressed
generated header after a normal `pio run` (source HTML →
`SettingsPageHtml.generated.h` via the existing `scripts/build_html.py`
build step).
## Additional Context
Memory/flash impact: none — same JS logic, just sequenced instead of
concurrent. No new allocations, no change to the generated header's size
beyond the few added bytes of source text.
Risk: low. This only changes the *order* three existing fetches happen
in on page load; no new endpoints, no changed request/response shapes.
---
### AI Usage
YES — diagnosed and fixed by Claude (Sonnet 5), including live
reproduction and verification against real hardware.
Ties bookmarks to the actual text on the page rather than to a page number. Page numbers shift whenever the book gets re-laid-out — changing font size, margins, line spacing, or rotating the screen — which could previously land you on the wrong spot.
This is a followup of #2805, making bookmarks use the same mechanism. Several minor improvments ride along.
needsIndex() opens .idx and .qidx and re-reads the sidecar header on every
call, and DictionaryWordSelectActivity called it once per tapped word rather
than once per dictionary open. A lookup therefore cost 4 SD opens and 2
std::string path temporaries, not the 2 and 0 the previous change set out to
achieve.
Ask needsIndex() once, inside the same guarded block that opens the
dictionary, and feed the build result back so a successful build stops
re-triggering while a failed one still retries on the next word.
Reimplement needsIndex() on openSession() so the "is the sidecar usable?"
rule lives in one place rather than two that have to be kept in step. This
is behaviour-preserving for everything buildIndex() can produce: it always
writes at least one sample (entry 0) and removes the file on failure, so
sampleCount == 0 means absent, stale or corrupt, all of which rebuild. The
one divergence is an improvement -- a corrupt sidecar with a valid magic
previously reported fresh and then full-scanned every lookup forever, and
now rebuilds once.
Also make the open()-time length bound a compile-time constant instead of
two runtime strlen() calls, and correct the buildPath() comment, which
claimed a lookup allocates nothing at all -- cleanWord(), stemVariants() and
the matched headword all still do.