428 lines
12 KiB
HTML
428 lines
12 KiB
HTML
<!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;
|
||
--title-color: #2c3e50;
|
||
--card-bg: #FFF;
|
||
--label-color: #7f8c8d;
|
||
--border-color: #eee;
|
||
--accent-color: rgb(110, 154, 130);
|
||
--accent-hover-color: #5a8c73;
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--font-color: #f5f5f5;
|
||
--bg: #333;
|
||
--title-color: #ecf0f1;
|
||
--card-bg: #444;
|
||
--label-color: #bdc3c7;
|
||
--border-color: #555;
|
||
color-scheme: dark;
|
||
}
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||
Oxygen, Ubuntu, sans-serif;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
background-color: var(--bg);
|
||
color: var(--font-color);
|
||
}
|
||
|
||
h1 {
|
||
color: var(--title-color);
|
||
border-bottom: 2px solid var(--accent-color);
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
h2 {
|
||
color: var(--title-color);
|
||
margin-top: 0;
|
||
}
|
||
|
||
.card {
|
||
background: var(--card-bg);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin: 15px 0;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.section {
|
||
margin-top: 18px;
|
||
}
|
||
|
||
.section:first-of-type {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 0.85em;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--label-color);
|
||
margin: 0 0 4px 0;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px solid var(--accent-color);
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.info-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.label {
|
||
font-weight: 600;
|
||
color: var(--label-color);
|
||
}
|
||
|
||
.value {
|
||
color: var(--title-color);
|
||
}
|
||
|
||
.status {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
background-color: #27ae60;
|
||
color: white;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.nav-links {
|
||
margin: 20px 0;
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.nav-links a {
|
||
padding: 10px 20px;
|
||
color: var(--font-color);
|
||
text-decoration: none;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.nav-links a.active {
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
}
|
||
|
||
.nav-links a:not(.active):hover {
|
||
background-color: var(--accent-hover-color);
|
||
color: white;
|
||
}
|
||
|
||
.action-row {
|
||
text-align: center;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.action-row button {
|
||
padding: 10px 18px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.action-row button:disabled {
|
||
opacity: 0.6;
|
||
cursor: default;
|
||
}
|
||
|
||
.debug-footer {
|
||
margin-top: 24px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed var(--border-color);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
font-size: 0.75em;
|
||
color: var(--label-color);
|
||
opacity: 0.7;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<h1>📚 CrossPoint Reader</h1>
|
||
|
||
<div class="nav-links">
|
||
<a href="/files">File Manager</a>
|
||
<a href="/settings">Settings</a>
|
||
<a href="/systeminfo" class="active">System Info</a>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Device Status</h2>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Version</div>
|
||
<div class="info-row">
|
||
<span class="label">Firmware</span>
|
||
<span class="value" id="version">Loading...</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">Device</span>
|
||
<span class="value" id="device">Loading...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Chip</div>
|
||
<div class="info-row">
|
||
<span class="label">Chip</span>
|
||
<span class="value" id="chip-version">Loading...</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">CPU</span>
|
||
<span class="value" id="cpu-mhz">Loading...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Network</div>
|
||
<div class="info-row">
|
||
<span class="label">WiFi Status</span>
|
||
<span class="status" id="wifi-status">Loading...</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">IP Address</span>
|
||
<span class="value" id="ip-address">Loading...</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">MAC Address</span>
|
||
<span class="value" id="mac-address">Loading...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Memory</div>
|
||
<div class="info-row">
|
||
<span class="label">Free / Min / Max</span>
|
||
<span class="value" id="heap-combined">Loading...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Flash</div>
|
||
<div class="info-row">
|
||
<span class="label">App partition</span>
|
||
<span class="value" id="app-partition-size">Loading...</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">Total flash</span>
|
||
<span class="value" id="flash-total">Loading...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Runtime</div>
|
||
<div class="info-row">
|
||
<span class="label">Uptime</span>
|
||
<span class="value" id="uptime">Loading...</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">Battery</span>
|
||
<span class="value" id="battery">Loading...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">Storage</div>
|
||
<div class="info-row">
|
||
<span class="label">SD Card</span>
|
||
<span class="value" id="sd-space">Not loaded</span>
|
||
</div>
|
||
<div class="action-row">
|
||
<button id="load-sd-button">Load SD info</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="debug-footer">
|
||
<span>Fast fetch: <span id="fast-status-duration">…</span></span>
|
||
<span>Full fetch: <span id="full-status-duration">—</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<p style="text-align: center; color: #95a5a6; margin: 0">
|
||
CrossPoint E-Reader • Open Source
|
||
</p>
|
||
</div>
|
||
<script>
|
||
function pickUnit(maxBytes) {
|
||
const units = ['B', 'KB', 'MB', 'GB'];
|
||
let div = 1, i = 0;
|
||
while (maxBytes >= 1024 && i < units.length - 1) { maxBytes /= 1024; div *= 1024; i++; }
|
||
return { unit: units[i], div };
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
if (bytes == null) return 'N/A';
|
||
const { unit, div } = pickUnit(bytes);
|
||
if (div === 1) return bytes.toLocaleString() + ' ' + unit;
|
||
return parseFloat((bytes / div).toFixed(2)).toLocaleString() + ' ' + unit;
|
||
}
|
||
|
||
function formatBytesTriple(a, b, c) {
|
||
if (a == null || b == null || c == null) return 'N/A';
|
||
const { unit, div } = pickUnit(Math.max(a, b, c));
|
||
const fmt = (v) => div === 1 ? v.toLocaleString() : parseFloat((v / div).toFixed(2)).toLocaleString();
|
||
return fmt(a) + ' / ' + fmt(b) + ' / ' + fmt(c) + ' ' + unit;
|
||
}
|
||
|
||
const SD_LABELS = {
|
||
NOT_LOADED: 'Not loaded',
|
||
LOAD_BUTTON: 'Load SD info',
|
||
LOADING: 'Loading...',
|
||
ERROR: 'Error loading'
|
||
};
|
||
|
||
let sdLoading = false;
|
||
|
||
function formatUptime(seconds) {
|
||
if (seconds == null) return 'N/A';
|
||
const h = Math.floor(seconds / 3600);
|
||
const m = Math.floor((seconds % 3600) / 60);
|
||
const s = seconds % 60;
|
||
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';
|
||
const deviceLabel = data.deviceType
|
||
? (data.displayWidth && data.displayHeight
|
||
? data.deviceType + ' (' + data.displayWidth + ' × ' + data.displayHeight + ' px)'
|
||
: data.deviceType)
|
||
: 'N/A';
|
||
document.getElementById('device').textContent = deviceLabel;
|
||
document.getElementById('chip-version').textContent = data.chipVersion || 'N/A';
|
||
document.getElementById('cpu-mhz').textContent = data.cpuMHz
|
||
? data.cpuMHz.toLocaleString() + ' MHz'
|
||
: 'N/A';
|
||
const wifiStatus = document.getElementById('wifi-status');
|
||
wifiStatus.textContent = data.mode || 'N/A';
|
||
document.getElementById('ip-address').textContent = data.ip || 'N/A';
|
||
document.getElementById('mac-address').textContent = data.macAddress || 'N/A';
|
||
document.getElementById('heap-combined').textContent =
|
||
formatBytesTriple(data.freeHeap, data.minFreeHeap, data.maxAllocHeap);
|
||
document.getElementById('app-partition-size').textContent = data.appPartitionSize != null
|
||
? formatBytes(data.appPartitionSize)
|
||
: 'N/A';
|
||
document.getElementById('flash-total').textContent = data.flashTotal != null
|
||
? formatBytes(data.flashTotal)
|
||
: 'N/A';
|
||
document.getElementById('battery').textContent = (data.batteryPercent != null)
|
||
? (data.batteryPercent + '%' + (data.charging ? ' (Charging)' : ''))
|
||
: 'N/A';
|
||
document.getElementById('uptime').textContent = formatUptime(data.uptime);
|
||
|
||
if (includeSd) {
|
||
document.getElementById('sd-space').textContent = data.sdTotal
|
||
? formatBytes(data.sdUsed) + ' / ' + formatBytes(data.sdTotal)
|
||
: 'N/A';
|
||
} else if (!sdLoading) {
|
||
document.getElementById('sd-space').textContent = SD_LABELS.NOT_LOADED;
|
||
}
|
||
}
|
||
|
||
async function fetchStatus() {
|
||
const start = performance.now();
|
||
try {
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function fetchSdStatus() {
|
||
if (sdLoading) {
|
||
return;
|
||
}
|
||
|
||
const button = document.getElementById('load-sd-button');
|
||
sdLoading = true;
|
||
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');
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch SD status: ' + response.status + ' ' + response.statusText);
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
|
||
function setSdButtonState(enabled) {
|
||
const button = document.getElementById('load-sd-button');
|
||
button.disabled = !enabled;
|
||
button.textContent = enabled ? SD_LABELS.LOAD_BUTTON : SD_LABELS.LOADING;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
setSdButtonState(false);
|
||
document.getElementById('load-sd-button').addEventListener('click', () => {
|
||
if (sdLoading) {
|
||
return;
|
||
}
|
||
fetchSdStatus();
|
||
});
|
||
fetchStatus();
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html> |