Merge pull request #60 from jpirnay/fix-undue-delay

refactor: Change webserver layout due avoid undue startup delays
This commit is contained in:
jpirnay
2026-04-11 10:33:25 +02:00
committed by GitHub
18 changed files with 437 additions and 12 deletions
+6
View File
@@ -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"
+6
View File
@@ -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é"
+6
View File
@@ -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"
+6
View File
@@ -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"
+6
View File
@@ -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: "* = Защищена | + = Сохранена"
+95 -1
View File
@@ -1,10 +1,85 @@
import configparser
import os
import re
import gzip
import subprocess
import sys
SRC_DIR = "src"
CROSSPOINT_NAME = "CrossPoint Reader"
PLACEHOLDERS = {
"%%CROSSPOINT%%": CROSSPOINT_NAME,
}
def warn(msg: str) -> None:
print(f"WARNING [build_html.py]: {msg}", file=sys.stderr)
def get_base_version(project_dir: str) -> str:
ini_path = os.path.join(project_dir, "platformio.ini")
if not os.path.isfile(ini_path):
warn(f"platformio.ini not found at {ini_path}; using 0.0.0")
return "0.0.0"
config = configparser.ConfigParser()
config.read(ini_path)
if not config.has_option("crosspoint", "version"):
warn("No [crosspoint] section or version in platformio.ini; using 0.0.0")
return "0.0.0"
return config.get("crosspoint", "version")
def get_git_branch(project_dir: str) -> str:
try:
branch = subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
text=True,
stderr=subprocess.PIPE,
cwd=project_dir,
).strip()
if branch == "HEAD":
branch = subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
text=True,
stderr=subprocess.PIPE,
cwd=project_dir,
).strip()
return "".join(c for c in branch if c not in '"\\')
except FileNotFoundError:
warn('git not found on PATH; branch suffix will be "unknown"')
return "unknown"
except subprocess.CalledProcessError as e:
warn(
f'git command failed (exit {e.returncode}): {e.stderr.strip()}; branch suffix will be "unknown"'
)
return "unknown"
except Exception as e:
warn(
f'Unexpected error reading git branch: {e}; branch suffix will be "unknown"'
)
return "unknown"
def get_version_string(project_dir: str) -> str:
env_version = os.environ.get("CROSSPOINT_VERSION")
if env_version:
return env_version.strip('"')
base_version = get_base_version(project_dir)
pioenv = os.environ.get("PIOENV", "default")
if pioenv == "default":
branch = get_git_branch(project_dir)
return f"{base_version}-dev+{branch}"
return base_version
def replace_placeholders(html: str, replacements: dict) -> str:
for placeholder, replacement in replacements.items():
html = html.replace(placeholder, replacement)
return html
def strip_js_comments(js: str) -> str:
"""Remove JS comments while preserving string literals and URLs."""
result = []
@@ -159,6 +234,24 @@ def sanitize_identifier(name: str) -> str:
return sanitized
def get_project_dir() -> str:
try:
Import("env") # type: ignore[name-defined]
return env["PROJECT_DIR"]
except NameError:
if "__file__" in globals():
script_dir = os.path.dirname(os.path.abspath(__file__))
elif sys.argv and sys.argv[0]:
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
script_dir = os.getcwd()
return os.path.dirname(script_dir)
project_dir = get_project_dir()
version_string = get_version_string(project_dir)
PLACEHOLDERS["%%VERSION%%"] = version_string
for root, _, files in os.walk(SRC_DIR):
for file in files:
if file.endswith(".html") or file.endswith(".js"):
@@ -166,8 +259,9 @@ for root, _, files in os.walk(SRC_DIR):
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Only minify HTML files; JS files are typically pre-minified (e.g., jszip.min.js)
# Replace build-time placeholders only in HTML files
if file.endswith(".html"):
content = replace_placeholders(content, PLACEHOLDERS)
processed = minify_html(content)
else:
processed = content
+3
View File
@@ -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() {
@@ -8,6 +8,7 @@
#include "MappedInputManager.h"
#include "WifiSelectionActivity.h"
#include "activities/network/SignalStrengthWidget.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -29,6 +30,8 @@ void CalibreConnectActivity::onEnter() {
lastCompleteName.clear();
lastCompleteAt = 0;
lastProcessedCompleteAt = 0;
currentRssi = 0;
lastRssiUpdateTime = 0;
exitRequested = false;
if (WiFi.status() != WL_CONNECTED) {
@@ -126,6 +129,12 @@ void CalibreConnectActivity::loop() {
}
lastHandleClientTime = millis();
if (millis() - lastRssiUpdateTime > 5000) {
lastRssiUpdateTime = millis();
currentRssi = WiFi.RSSI();
requestUpdate();
}
const auto status = webServer->getWsUploadStatus();
bool changed = false;
if (status.inProgress) {
@@ -190,8 +199,15 @@ void CalibreConnectActivity::render(RenderLock&&) {
const int textX = contentRect.x + metrics.contentSidePadding;
const int textWidth = contentRect.width - metrics.contentSidePadding * 2;
int y = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 4;
int y = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2;
const auto heightText12 = renderer.getTextHeight(UI_12_FONT_ID);
const auto lineHeightSmall = renderer.getLineHeight(SMALL_FONT_ID);
const int signalHeight = 22;
drawWifiSignalStrength(renderer, textX, y, textWidth, signalHeight, currentRssi);
renderer.drawText(SMALL_FONT_ID, textX, y + signalHeight + 2, rssiLabel(currentRssi).c_str());
y += signalHeight + lineHeightSmall + metrics.verticalSpacing * 3;
renderer.drawText(UI_12_FONT_ID, textX, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD);
y += heightText12 + metrics.verticalSpacing * 2;
@@ -26,6 +26,8 @@ class CalibreConnectActivity final : public Activity {
std::string lastCompleteName;
unsigned long lastCompleteAt = 0;
unsigned long lastProcessedCompleteAt = 0; // Track which server value we've already processed
int currentRssi = 0;
unsigned long lastRssiUpdateTime = 0;
bool exitRequested = false;
void renderServerRunning() const;
@@ -14,6 +14,7 @@
#include "NetworkModeSelectionActivity.h"
#include "WifiSelectionActivity.h"
#include "activities/network/CalibreConnectActivity.h"
#include "activities/network/SignalStrengthWidget.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/QrUtils.h"
@@ -244,6 +245,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 +298,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 +377,8 @@ void CrossPointWebServerActivity::render(RenderLock&&) {
}
}
namespace {} // namespace
void CrossPointWebServerActivity::renderServerRunning() const {
const auto& metrics = UITheme::getInstance().getMetrics();
const Rect contentRect = UITheme::getContentRect(renderer, true, false);
@@ -412,6 +425,12 @@ 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;
drawWifiSignalStrength(renderer, contentRect.x + metrics.contentSidePadding, signalY, signalWidth, signalHeight, 0);
renderer.drawCenteredText(SMALL_FONT_ID, signalY + signalHeight + 2, tr(STR_HOTSPOT_MODE));
} else {
startY += metrics.verticalSpacing * 2;
@@ -435,8 +454,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;
drawWifiSignalStrength(renderer, contentRect.x + metrics.contentSidePadding, signalY, signalWidth, signalHeight,
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;
@@ -0,0 +1,51 @@
#pragma once
#include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm>
#include <string>
inline 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;
}
inline void drawWifiSignalStrength(const GfxRenderer& renderer, int x, int y, int width, int height, int rssi) {
const int barCount = 4;
const int gap = 6;
const int barWidth = std::max(1, 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 = std::max(1, height - 8);
const int bars = rssi == 0 ? 0 : (rssi <= -90 ? 1 : (rssi <= -75 ? 2 : (rssi <= -60 ? 3 : 4)));
const int baseY = y + height - 2;
for (int i = 0; i < barCount; ++i) {
const int barHeight = std::max(0, ((i + 1) * maxBarHeight) / barCount);
if (barHeight <= 0 || barWidth <= 0) {
continue;
}
const int barX = startX + i * (barWidth + gap);
const int barY = baseY - barHeight;
renderer.drawRect(barX, barY, barWidth, barHeight, true);
if (i < bars && barWidth > 2 && barHeight > 2) {
renderer.fillRect(barX + 1, barY + 1, barWidth - 2, barHeight - 2);
}
}
}
inline std::string rssiLabel(int rssi) {
if (rssi == 0) {
return std::string(tr(STR_NO_SIGNAL));
}
const char* quality = I18N.get(getRssiQualityStrId(rssi));
return std::string(tr(STR_RSSI)) + ": " + std::to_string(rssi) + " dBm (" + quality + ")";
}
+63 -5
View File
@@ -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) {
+3
View File
@@ -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;
+1 -1
View File
@@ -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">
+22 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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>
+116
View File
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%%CROSSPOINT%%</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%%</h1>
<div class="subtitle">Welcome to your reader</div>
<div class="version">Version %%VERSION%%</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>
</body>
</html>