Redesign web server to avoid lengthy system stats retrieval
This commit is contained in:
@@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network"
|
||||
STR_OPEN_URL_HINT: "Open this URL in your browser"
|
||||
STR_OR_HTTP_PREFIX: "or http://"
|
||||
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
|
||||
STR_RSSI: "RSSI"
|
||||
STR_NO_SIGNAL: "No signal"
|
||||
STR_SIGNAL_QUALITY_POOR: "Poor"
|
||||
STR_SIGNAL_QUALITY_WEAK: "Weak"
|
||||
STR_SIGNAL_QUALITY_GOOD: "Good"
|
||||
STR_SIGNAL_QUALITY_EXCELLENT: "Excellent"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
||||
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
||||
|
||||
@@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce WiFi"
|
||||
STR_OPEN_URL_HINT: "Ouvrez cette URL dans un navigateur"
|
||||
STR_OR_HTTP_PREFIX: "ou http://"
|
||||
STR_SCAN_QR_HINT: "ou scannez le QR code :"
|
||||
STR_RSSI: "RSSI"
|
||||
STR_NO_SIGNAL: "Pas de signal"
|
||||
STR_SIGNAL_QUALITY_POOR: "Faible"
|
||||
STR_SIGNAL_QUALITY_WEAK: "Très faible"
|
||||
STR_SIGNAL_QUALITY_GOOD: "Bon"
|
||||
STR_SIGNAL_QUALITY_EXCELLENT: "Excellent"
|
||||
STR_CALIBRE_WIRELESS: "Connexion Calibre sans fil"
|
||||
STR_CALIBRE_WEB_URL: "URL Web Calibre"
|
||||
STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
|
||||
|
||||
@@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Gerät mit diesem WLAN verbinden"
|
||||
STR_OPEN_URL_HINT: "Diese URL im Browser öffnen"
|
||||
STR_OR_HTTP_PREFIX: "oder http://"
|
||||
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
|
||||
STR_RSSI: "RSSI"
|
||||
STR_NO_SIGNAL: "Kein Signal"
|
||||
STR_SIGNAL_QUALITY_POOR: "Schlecht"
|
||||
STR_SIGNAL_QUALITY_WEAK: "Schwach"
|
||||
STR_SIGNAL_QUALITY_GOOD: "Gut"
|
||||
STR_SIGNAL_QUALITY_EXCELLENT: "Ausgezeichnet"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "Calibre-Web-URL"
|
||||
STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert"
|
||||
|
||||
@@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Connettere il dispositivo a questa rete WiFi"
|
||||
STR_OPEN_URL_HINT: "Aprire questo URL nel browser"
|
||||
STR_OR_HTTP_PREFIX: "o http://"
|
||||
STR_SCAN_QR_HINT: "oppure scansionare il codice QR col telefono:"
|
||||
STR_RSSI: "RSSI"
|
||||
STR_NO_SIGNAL: "Nessun segnale"
|
||||
STR_SIGNAL_QUALITY_POOR: "Scarso"
|
||||
STR_SIGNAL_QUALITY_WEAK: "Debole"
|
||||
STR_SIGNAL_QUALITY_GOOD: "Buono"
|
||||
STR_SIGNAL_QUALITY_EXCELLENT: "Eccellente"
|
||||
STR_CALIBRE_WIRELESS: "Calibre wireless"
|
||||
STR_CALIBRE_WEB_URL: "URL Web Calibre"
|
||||
STR_NETWORK_LEGEND: "* = Protetta | + = Salvata"
|
||||
|
||||
@@ -44,6 +44,12 @@ STR_CONNECT_WIFI_HINT: "Подключите устройство к этой с
|
||||
STR_OPEN_URL_HINT: "Откройте этот адрес в браузере"
|
||||
STR_OR_HTTP_PREFIX: "или http://"
|
||||
STR_SCAN_QR_HINT: "или отсканируйте QR-код:"
|
||||
STR_RSSI: "RSSI"
|
||||
STR_NO_SIGNAL: "Нет сигнала"
|
||||
STR_SIGNAL_QUALITY_POOR: "Плохо"
|
||||
STR_SIGNAL_QUALITY_WEAK: "Слабо"
|
||||
STR_SIGNAL_QUALITY_GOOD: "Хорошо"
|
||||
STR_SIGNAL_QUALITY_EXCELLENT: "Отлично"
|
||||
STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi"
|
||||
STR_CALIBRE_WEB_URL: "Web-адрес Calibre"
|
||||
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
@@ -72,9 +73,11 @@ struct SystemStatus {
|
||||
}
|
||||
|
||||
static void fillSdStatus(SystemStatus& s) {
|
||||
uint32_t t0 = millis();
|
||||
s.sdTotalBytes = Storage.sdTotalBytes();
|
||||
s.sdUsedBytes = Storage.sdUsedBytes();
|
||||
s.sdFreeBytes = Storage.sdFreeBytes();
|
||||
LOG_DBG("SYSINFO", "Filled SD status in %u ms", millis() - t0);
|
||||
}
|
||||
|
||||
static SystemStatus collect() {
|
||||
|
||||
@@ -244,6 +244,10 @@ void CrossPointWebServerActivity::startWebServer() {
|
||||
|
||||
if (webServer->isRunning()) {
|
||||
state = WebServerActivityState::SERVER_RUNNING;
|
||||
if (!isApMode) {
|
||||
currentRssi = WiFi.RSSI();
|
||||
lastRssiUpdateTime = millis();
|
||||
}
|
||||
LOG_DBG("WEBACT", "Web server started successfully");
|
||||
|
||||
// Force an immediate render since we're transitioning from a subactivity
|
||||
@@ -293,6 +297,12 @@ void CrossPointWebServerActivity::loop() {
|
||||
LOG_DBG("WEBACT", "Warning: Weak WiFi signal: %d dBm", rssi);
|
||||
}
|
||||
}
|
||||
|
||||
if (millis() - lastRssiUpdateTime > 5000) { // Refresh signal indicator every 5 seconds
|
||||
lastRssiUpdateTime = millis();
|
||||
currentRssi = WiFi.RSSI();
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle web server requests - maximize throughput with watchdog safety
|
||||
@@ -366,6 +376,69 @@ void CrossPointWebServerActivity::render(RenderLock&&) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
StrId getRssiQualityStrId(int rssi) {
|
||||
if (rssi <= -90) {
|
||||
return StrId::STR_SIGNAL_QUALITY_POOR;
|
||||
}
|
||||
if (rssi <= -75) {
|
||||
return StrId::STR_SIGNAL_QUALITY_WEAK;
|
||||
}
|
||||
if (rssi <= -60) {
|
||||
return StrId::STR_SIGNAL_QUALITY_GOOD;
|
||||
}
|
||||
return StrId::STR_SIGNAL_QUALITY_EXCELLENT;
|
||||
}
|
||||
|
||||
int rssiToSignalBars(int rssi) {
|
||||
if (rssi <= -90) {
|
||||
return 1;
|
||||
}
|
||||
if (rssi <= -75) {
|
||||
return 2;
|
||||
}
|
||||
if (rssi <= -60) {
|
||||
return 3;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
void drawSignalStrength(const GfxRenderer& renderer, const Rect& bounds, int rssi) {
|
||||
const int x = bounds.x;
|
||||
const int y = bounds.y;
|
||||
const int width = bounds.width;
|
||||
const int height = bounds.height;
|
||||
const int barCount = 4;
|
||||
const int gap = 6;
|
||||
const int barWidth = std::min(10, (width - (barCount - 1) * gap) / barCount);
|
||||
const int totalWidth = barCount * barWidth + (barCount - 1) * gap;
|
||||
const int startX = x + (width - totalWidth) / 2;
|
||||
const int maxBarHeight = height - 8;
|
||||
const int bars = rssi == 0 ? 0 : rssiToSignalBars(rssi);
|
||||
const int baseY = y + height - 2;
|
||||
for (int i = 0; i < barCount; ++i) {
|
||||
const int barHeight = ((i + 1) * maxBarHeight) / barCount;
|
||||
const int barX = startX + i * (barWidth + gap);
|
||||
const int barY = baseY - barHeight;
|
||||
renderer.drawRect(barX, barY, barWidth, barHeight, true);
|
||||
if (i < bars) {
|
||||
renderer.fillRect(barX + 1, barY + 1, barWidth - 2, barHeight - 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string rssiLabel(int rssi) {
|
||||
if (rssi == 0) {
|
||||
return std::string(tr(STR_NO_SIGNAL));
|
||||
}
|
||||
const char* quality = I18N.get(getRssiQualityStrId(rssi));
|
||||
std::string label = std::string(tr(STR_RSSI)) + ": " + std::to_string(rssi) + " dBm (" + quality + ")";
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const Rect contentRect = UITheme::getContentRect(renderer, true, false);
|
||||
@@ -412,6 +485,13 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
hostnameUrl.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100,
|
||||
ipUrl.c_str());
|
||||
|
||||
const int signalHeight = 22;
|
||||
const int signalWidth = contentRect.width - metrics.contentSidePadding * 2;
|
||||
const int signalY = startY + 120;
|
||||
const Rect signalBounds(contentRect.x + metrics.contentSidePadding, signalY, signalWidth, signalHeight);
|
||||
drawSignalStrength(renderer, signalBounds, 0);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, signalY + signalHeight + 2, tr(STR_HOTSPOT_MODE));
|
||||
} else {
|
||||
startY += metrics.verticalSpacing * 2;
|
||||
|
||||
@@ -435,8 +515,19 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
// Also show hostname URL
|
||||
std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/";
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, startY, hostnameUrl.c_str(), true);
|
||||
|
||||
// AP mode: no external RSSI metric available, but keep UI spacing consistent.
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
|
||||
if (!isApMode) {
|
||||
const int signalHeight = 22;
|
||||
const int signalWidth = contentRect.width - metrics.contentSidePadding * 2;
|
||||
const int signalY = startY + height10 + metrics.verticalSpacing * 2;
|
||||
const Rect signalBounds(contentRect.x + metrics.contentSidePadding, signalY, signalWidth, signalHeight);
|
||||
drawSignalStrength(renderer, signalBounds, currentRssi);
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, signalY + signalHeight + 2, rssiLabel(currentRssi).c_str(), true);
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ class CrossPointWebServerActivity final : public Activity {
|
||||
// Server status
|
||||
std::string connectedIP;
|
||||
std::string connectedSSID; // For STA mode: network name, For AP mode: AP name
|
||||
int currentRssi = 0;
|
||||
unsigned long lastRssiUpdateTime = 0;
|
||||
|
||||
// Performance monitoring
|
||||
unsigned long lastHandleClientTime = 0;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "html/FilesPageHtml.generated.h"
|
||||
#include "html/HomePageHtml.generated.h"
|
||||
#include "html/SettingsPageHtml.generated.h"
|
||||
#include "html/WelcomePageHtml.generated.h"
|
||||
#include "html/js/jszip_minJs.generated.h"
|
||||
|
||||
namespace {
|
||||
@@ -162,11 +163,13 @@ void CrossPointWebServer::begin() {
|
||||
|
||||
// Setup routes
|
||||
LOG_DBG("WEB", "Setting up routes...");
|
||||
server->on("/", HTTP_GET, [this] { handleRoot(); });
|
||||
server->on("/files", HTTP_GET, [this] { handleFileList(); });
|
||||
server->on("/", HTTP_GET, [this] { handleWelcomePage(); });
|
||||
server->on("/systeminfo", HTTP_GET, [this] { handleSystemInfoPage(); });
|
||||
server->on("/files", HTTP_GET, [this] { handleRoot(); });
|
||||
server->on("/js/jszip.min.js", HTTP_GET, [this] { handleJszip(); });
|
||||
|
||||
server->on("/api/status", HTTP_GET, [this] { handleStatus(); });
|
||||
server->on("/api/status/fast", HTTP_GET, [this] { handleStatusFast(); });
|
||||
server->on("/api/files", HTTP_GET, [this] { handleFileListData(); });
|
||||
server->on("/download", HTTP_GET, [this] { handleDownload(); });
|
||||
|
||||
@@ -353,8 +356,24 @@ static void sendHtmlContent(WebServer* server, const char* data, size_t len) {
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleRoot() const {
|
||||
int32_t t0 = millis();
|
||||
sendHtmlContent(server.get(), FilesPageHtml, sizeof(FilesPageHtml));
|
||||
int32_t t1 = millis();
|
||||
LOG_DBG("WEB", "Served file manager page in %d ms", t1 - t0);
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleWelcomePage() const {
|
||||
int32_t t0 = millis();
|
||||
sendHtmlContent(server.get(), WelcomePageHtml, sizeof(WelcomePageHtml));
|
||||
int32_t t1 = millis();
|
||||
LOG_DBG("WEB", "Served welcome page in %d ms", t1 - t0);
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleSystemInfoPage() const {
|
||||
int32_t t0 = millis();
|
||||
sendHtmlContent(server.get(), HomePageHtml, sizeof(HomePageHtml));
|
||||
LOG_DBG("WEB", "Served root page");
|
||||
int32_t t1 = millis();
|
||||
LOG_DBG("WEB", "Served system info page in %d ms", t1 - t0);
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleJszip() const {
|
||||
@@ -370,10 +389,14 @@ void CrossPointWebServer::handleNotFound() const {
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleStatus() const {
|
||||
const bool fastOnly = server->hasArg("phase") && server->arg("phase") == "fast";
|
||||
|
||||
const bool fastOnly = server->hasArg("phase") && server->arg("phase").equalsIgnoreCase("fast");
|
||||
LOG_DBG("SYSINFO", "handleStatus request received (fastOnly=%d)", fastOnly);
|
||||
int32_t t0 = millis();
|
||||
SystemStatus status = SystemStatus::collectFast();
|
||||
int32_t t1 = millis();
|
||||
LOG_DBG("SYSINFO", "Collected fast status in %d ms (fastOnly=%d)", t1 - t0, fastOnly);
|
||||
if (!fastOnly) {
|
||||
LOG_DBG("SYSINFO", "handleStatus will collect SD stats");
|
||||
SystemStatus::fillSdStatus(status);
|
||||
}
|
||||
|
||||
@@ -404,6 +427,41 @@ void CrossPointWebServer::handleStatus() const {
|
||||
server->send(200, "application/json", json);
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleStatusFast() const {
|
||||
LOG_DBG("SYSINFO", "handleStatusFast request received");
|
||||
int32_t t0 = millis();
|
||||
SystemStatus status = SystemStatus::collectFast();
|
||||
int32_t t1 = millis();
|
||||
LOG_DBG("SYSINFO", "Collected fast-only status in %d ms", t1 - t0);
|
||||
|
||||
JsonDocument doc;
|
||||
doc["version"] = status.version;
|
||||
doc["chipVersion"] = status.chipVersion;
|
||||
doc["cpuMHz"] = status.cpuFreqMHz;
|
||||
doc["ip"] = status.ip;
|
||||
doc["mode"] = status.wifiMode;
|
||||
doc["rssi"] = status.rssi;
|
||||
doc["macAddress"] = status.macAddress;
|
||||
doc["freeHeap"] = status.freeHeapBytes;
|
||||
doc["minFreeHeap"] = status.minFreeHeapBytes;
|
||||
doc["maxAllocHeap"] = status.maxAllocHeapBytes;
|
||||
doc["flashTotal"] = status.flashBytes;
|
||||
doc["flashAppUsed"] = status.flashAppUsedBytes;
|
||||
doc["flashAppFree"] = status.flashAppFreeBytes;
|
||||
doc["batteryPercent"] = status.batteryPercent;
|
||||
doc["charging"] = status.charging;
|
||||
doc["uptime"] = status.uptimeSeconds;
|
||||
doc["sdReady"] = false;
|
||||
// Still include SD stats in response, but client should ignore them when sdReady=false
|
||||
doc["sdTotal"] = status.sdTotalBytes;
|
||||
doc["sdUsed"] = status.sdUsedBytes;
|
||||
doc["sdFree"] = status.sdFreeBytes;
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
server->send(200, "application/json", json);
|
||||
}
|
||||
|
||||
void CrossPointWebServer::scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const {
|
||||
FsFile root = Storage.open(path);
|
||||
if (!root) {
|
||||
|
||||
@@ -89,9 +89,12 @@ class CrossPointWebServer {
|
||||
|
||||
// Request handlers
|
||||
void handleRoot() const;
|
||||
void handleWelcomePage() const;
|
||||
void handleSystemInfoPage() const;
|
||||
void handleJszip() const;
|
||||
void handleNotFound() const;
|
||||
void handleStatus() const;
|
||||
void handleStatusFast() const;
|
||||
void handleFileList() const;
|
||||
void handleFileListData() const;
|
||||
void handleDownload() const;
|
||||
|
||||
@@ -1742,9 +1742,9 @@
|
||||
|
||||
<body>
|
||||
<div class="nav-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/files" class="active">File Manager</a>
|
||||
<a href="/settings">Settings</a>
|
||||
<a href="/systeminfo">System Info</a>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
|
||||
@@ -137,9 +137,9 @@
|
||||
<h1>📚 CrossPoint Reader</h1>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="active">Home</a>
|
||||
<a href="/files">File Manager</a>
|
||||
<a href="/settings">Settings</a>
|
||||
<a href="/systeminfo" class="active">System Info</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -192,6 +192,14 @@
|
||||
<span class="label">Uptime</span>
|
||||
<span class="value" id="uptime">Loading...</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Fast status fetch</span>
|
||||
<span class="value" id="fast-status-duration">Loading...</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Full status fetch</span>
|
||||
<span class="value" id="full-status-duration">Not loaded</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">SD Card</span>
|
||||
<span class="value" id="sd-space">Not loaded</span>
|
||||
@@ -232,6 +240,10 @@
|
||||
return h + 'h ' + String(m).padStart(2, '0') + 'm ' + String(s).padStart(2, '0') + 's';
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
return ms == null ? 'N/A' : ms + ' ms';
|
||||
}
|
||||
|
||||
function applyStatus(data, includeSd) {
|
||||
document.getElementById('version').textContent = data.version || 'N/A';
|
||||
document.getElementById('chip-version').textContent = data.chipVersion || 'N/A';
|
||||
@@ -269,16 +281,19 @@
|
||||
}
|
||||
|
||||
async function fetchStatus() {
|
||||
const start = performance.now();
|
||||
try {
|
||||
const fastResponse = await fetch('/api/status?phase=fast');
|
||||
const fastResponse = await fetch('/api/status/fast');
|
||||
if (!fastResponse.ok) {
|
||||
throw new Error('Failed to fetch fast status: ' + fastResponse.status + ' ' + fastResponse.statusText);
|
||||
}
|
||||
const fastData = await fastResponse.json();
|
||||
applyStatus(fastData, false);
|
||||
document.getElementById('fast-status-duration').textContent = formatDuration(Math.round(performance.now() - start));
|
||||
} catch (error) {
|
||||
console.error('Error fetching status:', error);
|
||||
document.getElementById('sd-space').textContent = SD_LABELS.ERROR;
|
||||
document.getElementById('fast-status-duration').textContent = 'Error';
|
||||
} finally {
|
||||
if (!sdLoading) {
|
||||
setSdButtonState(true);
|
||||
@@ -296,6 +311,9 @@
|
||||
button.disabled = true;
|
||||
button.textContent = SD_LABELS.LOADING;
|
||||
document.getElementById('sd-space').textContent = SD_LABELS.LOADING;
|
||||
document.getElementById('full-status-duration').textContent = 'Loading...';
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
@@ -305,11 +323,13 @@
|
||||
const data = await response.json();
|
||||
applyStatus(data, true);
|
||||
button.style.display = 'none';
|
||||
document.getElementById('full-status-duration').textContent = formatDuration(Math.round(performance.now() - start));
|
||||
} catch (error) {
|
||||
console.error('Error fetching SD status:', error);
|
||||
button.disabled = false;
|
||||
button.textContent = SD_LABELS.LOAD_BUTTON;
|
||||
document.getElementById('sd-space').textContent = SD_LABELS.ERROR;
|
||||
document.getElementById('full-status-duration').textContent = 'Error';
|
||||
} finally {
|
||||
sdLoading = false;
|
||||
}
|
||||
|
||||
@@ -240,9 +240,9 @@
|
||||
<h1>⚙️ Settings</h1>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/files">File Manager</a>
|
||||
<a href="/settings" class="active">Settings</a>
|
||||
<a href="/systeminfo">System Info</a>
|
||||
</div>
|
||||
|
||||
<div id="message" class="message"></div>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CrossPoint Reader</title>
|
||||
<style>
|
||||
:root {
|
||||
--font-color: #333;
|
||||
--bg: #f5f5f5;
|
||||
--card-bg: #fff;
|
||||
--accent-color: rgb(110, 154, 130);
|
||||
--accent-color-dark: #5a8c73;
|
||||
--subtitle-color: #7f8c8d;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--font-color: #f5f5f5;
|
||||
--bg: #222;
|
||||
--card-bg: #333;
|
||||
--accent-color: #6fae83;
|
||||
--accent-color-dark: #5a8c73;
|
||||
--subtitle-color: #b0bfc7;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: radial-gradient(circle at top, rgba(110, 154, 130, 0.15), transparent 35%), var(--bg);
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(100%, 420px);
|
||||
background: var(--card-bg);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.09);
|
||||
padding: 36px 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
letter-spacing: -0.04em;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 10px 0 24px;
|
||||
color: var(--subtitle-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin: 14px 0 24px;
|
||||
font-size: 0.95rem;
|
||||
color: var(--accent-color-dark);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
display: block;
|
||||
padding: 14px 18px;
|
||||
border-radius: 12px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
background: var(--accent-color-dark);
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 28px;
|
||||
color: var(--subtitle-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="logo">📚</div>
|
||||
<h1>CrossPoint Reader</h1>
|
||||
<div class="subtitle">Welcome to your reader</div>
|
||||
<div class="version">Version <span id="version">Loading...</span></div>
|
||||
<div class="nav-links">
|
||||
<a href="/files">Open File Manager</a>
|
||||
<a href="/settings">Settings</a>
|
||||
<a href="/systeminfo">System Info</a>
|
||||
</div>
|
||||
<div class="footer">Fast start page designed for weak connections</div>
|
||||
</div>
|
||||
<script>
|
||||
async function loadVersion() {
|
||||
try {
|
||||
const response = await fetch('/api/status/fast');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch version');
|
||||
}
|
||||
const data = await response.json();
|
||||
document.getElementById('version').textContent = data.version || 'N/A';
|
||||
} catch (error) {
|
||||
document.getElementById('version').textContent = 'Unavailable';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadVersion);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user