diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 9aa114b0..eccddf66 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -17,6 +17,7 @@ #include "FontInstaller.h" #include "HttpFileStreamer.h" #include "OpdsServerStore.h" +#include "ReadingStats.h" #include "SdCardFontGlobals.h" #include "SdCardFontRegistry.h" #include "SettingsList.h" @@ -27,6 +28,7 @@ #include "html/FontsPageHtml.generated.h" #include "html/HomePageHtml.generated.h" #include "html/SettingsPageHtml.generated.h" +#include "html/StatsPageHtml.generated.h" #include "html/WelcomePageHtml.generated.h" #include "html/js/jszip_minJs.generated.h" #include "network/HttpDownloader.h" @@ -219,6 +221,10 @@ void CrossPointWebServer::begin() { server->on("/api/settings", HTTP_POST, [this] { handlePostSettings(); }); // Font management endpoints + server->on("/stats", HTTP_GET, [this] { handleStatsPage(); }); + server->on("/api/stats", HTTP_GET, [this] { handleStatsApi(); }); + server->on("/api/stats/export", HTTP_GET, [this] { handleStatsExport(); }); + server->on("/fonts", HTTP_GET, [this] { handleFontsPage(); }); server->on("/api/fonts", HTTP_GET, [this] { handleFontList(); }); server->on("/api/fonts/manifest", HTTP_GET, [this] { handleFontManifest(); }); @@ -419,6 +425,82 @@ void CrossPointWebServer::handleSystemInfoPage() const { LOG_DBG("WEB", "Served system info page in %d ms", t1 - t0); } +void CrossPointWebServer::handleStatsPage() const { + int32_t t0 = millis(); + sendHtmlContent(server.get(), StatsPageHtml, sizeof(StatsPageHtml)); + int32_t t1 = millis(); + LOG_DBG("WEB", "Served stats page in %d ms", t1 - t0); +} + +void CrossPointWebServer::handleStatsApi() const { + // Wire the same data the on-device screens use into a JSON payload the + // browser dashboard can consume. We pre-compute streaks and todayDayIndex + // here so the browser doesn't have to recreate the day-index math; the day + // arrays still go across untouched so the browser can render the sparkline. + const auto& store = READING_STATS; + const uint16_t today = currentLocalDayIndex(); + const bool haveStreak = today != 0 && !store.getGlobalDays().empty(); + + JsonDocument doc; + doc["totalSeconds"] = store.getGlobalTotalSeconds(); + doc["totalSessions"] = store.getGlobalTotalSessions(); + doc["totalPagesTurned"] = store.getGlobalTotalPagesTurned(); + doc["bookCount"] = static_cast(store.getBookCount()); + doc["todayDayIndex"] = today; + if (haveStreak) { + doc["currentStreak"] = store.computeCurrentStreak(today); + doc["longestStreak"] = store.computeLongestStreak(); + } + + // Day buckets as [[dayIndex, seconds], …] — same compact shape as on disk + // so the browser code can treat the export and the live API identically. + JsonArray globalDays = doc["globalDays"].to(); + for (const auto& d : store.getGlobalDays()) { + JsonArray pair = globalDays.add(); + pair.add(d.dayIndex); + pair.add(d.seconds); + } + + JsonArray booksArr = doc["books"].to(); + for (const auto& book : store.getBooks()) { + JsonObject obj = booksArr.add(); + obj["docId"] = book.docId; + obj["title"] = book.title; + obj["author"] = book.author; + obj["totalSeconds"] = book.totalSeconds; + obj["pagesTurned"] = book.pagesTurned; + obj["sessions"] = book.sessions; + obj["firstReadEpoch"] = static_cast(book.firstReadEpoch); + obj["lastReadEpoch"] = static_cast(book.lastReadEpoch); + obj["progress"] = book.progress; + obj["finished"] = book.finished; + JsonArray days = obj["days"].to(); + for (const auto& d : book.days) { + JsonArray pair = days.add(); + pair.add(d.dayIndex); + pair.add(d.seconds); + } + } + + String json; + serializeJson(doc, json); + server->send(200, "application/json", json); +} + +void CrossPointWebServer::handleStatsExport() const { + // Stream the raw stats file straight from SD — this is the same shape the + // device writes and reads, so it round-trips cleanly through external + // tooling without us having to maintain a second schema. + constexpr const char* kStatsFile = "/.crosspoint/reading-stats.json"; + if (!Storage.exists(kStatsFile)) { + server->send(404, "application/json", "{}"); + return; + } + String content = Storage.readFile(kStatsFile); + server->sendHeader("Content-Disposition", "attachment; filename=\"reading-stats.json\""); + server->send(200, "application/json", content); +} + void CrossPointWebServer::handleJszip() const { server->sendHeader("Content-Encoding", "gzip"); server->send_P(200, "application/javascript", jszip_minJs, jszip_minJsCompressedSize); diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index e73dce97..0987c61e 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -144,4 +144,9 @@ class CrossPointWebServer { void handleGetWifiNetworks() const; void handlePostWifiNetwork(); void handleDeleteWifiNetwork(); + + // Reading-stats handlers + void handleStatsPage() const; + void handleStatsApi() const; + void handleStatsExport() const; }; diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index 6f63fdda..104236a0 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -1821,6 +1821,7 @@ File Manager Settings Font Manager + Reading Stats System Info diff --git a/src/network/html/FontsPage.html b/src/network/html/FontsPage.html index 9147213e..0f6e7e58 100644 --- a/src/network/html/FontsPage.html +++ b/src/network/html/FontsPage.html @@ -197,6 +197,7 @@ File Manager Settings Font Manager + Reading Stats System Info diff --git a/src/network/html/HomePage.html b/src/network/html/HomePage.html index 2f1b6aba..1735bdd2 100644 --- a/src/network/html/HomePage.html +++ b/src/network/html/HomePage.html @@ -174,6 +174,7 @@ File Manager Settings Font Manager + Reading Stats System Info diff --git a/src/network/html/SettingsPage.html b/src/network/html/SettingsPage.html index 319d574e..f807de8b 100644 --- a/src/network/html/SettingsPage.html +++ b/src/network/html/SettingsPage.html @@ -301,6 +301,7 @@ File Manager Settings Font Manager + Reading Stats System Info diff --git a/src/network/html/StatsPage.html b/src/network/html/StatsPage.html new file mode 100644 index 00000000..caf41751 --- /dev/null +++ b/src/network/html/StatsPage.html @@ -0,0 +1,544 @@ + + + + + + + Reading Stats - %%CROSSPOINT%% + + + + +

Reading Stats

+ + + +
+
+
Loading…
+
+
+ + + + + diff --git a/src/network/html/WelcomePage.html b/src/network/html/WelcomePage.html index dab7f564..e2e274bd 100644 --- a/src/network/html/WelcomePage.html +++ b/src/network/html/WelcomePage.html @@ -108,6 +108,7 @@ Open File Manager Settings Font Manager + Reading Stats System Info