Files
mongoose/tutorials/device-dashboard/web_root/index.html
T
2026-04-27 15:07:40 +01:00

325 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>My Dashboard</title>
<!-- Mongoose device dashboard -->
<script src="https://cdn.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js"></script>
<script>
Dashboard.init({
debug: true,
data: {
chart1: {data: []},
leds: { led1: true, led2: false, led3: false },
settings: { volume: 12, log_level: 3, name: 'Galway' },
metrics: { temperature: 1.2, cpu: 4, ram: 5 },
files: {data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]},
},
});
</script>
<script src="main.js" defer></script>
<link rel="stylesheet" href="style.css">
<!-- uPlot graph library -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.32/dist/uPlot.min.css">
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.32/dist/uPlot.iife.min.js" defer></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div class="header">
<div class="header-container">
<div class="header-left">
<nav class="nav-tabs" role="tablist" aria-label="Primary tabs">
<img src="logo.svg" alt="Logo" class="logo" />
<a href="#overview" id="tab-overview" class="nav-link nav-link-active" role="tab" aria-controls="overview" aria-selected="true">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 11.5 12 4l9 7.5" />
<path d="M5 10.5V20h14v-9.5" />
</svg>
Overview
</a>
<a href="#settings" id="tab-settings" class="nav-link" role="tab" aria-controls="settings" aria-selected="false" tabindex="-1">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-.4-1.1 1.7 1.7 0 0 0-1-.6 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H2.8a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.1-.4 1.7 1.7 0 0 0 .6-1 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6c.38-.16.72-.4 1-.7.26-.3.4-.69.4-1.1V2.8a2 2 0 1 1 4 0v.1c0 .41.14.8.4 1.1.28.3.62.54 1 .7a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.16.38.4.72.7 1 .3.26.69.4 1.1.4h.1a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.1.4 1.7 1.7 0 0 0-.7 1z" />
</svg>
Settings
</a>
<a href="#firmware" id="tab-firmware" class="nav-link" role="tab" aria-controls="firmware" aria-selected="false" tabindex="-1">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v10" />
<path d="m8 9 4 4 4-4" />
<rect x="4" y="17" width="16" height="4" rx="2" />
</svg>
Firmware Update
</a>
<a href="#files" id="tab-files" class="nav-link" role="tab" aria-controls="files" aria-selected="false" tabindex="-1">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v2H4Z" />
<path d="M4 11h16v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z" />
<path d="M8 15h8" />
</svg>
File Manager
</a>
</nav>
<div class="connection-badge connection-badge-${__status.online ? 'online' : 'offline'}" aria-label="connection active">
<span class="connection-label">${__status.online ? 'live' : 'offline'}</span>
<span class="connection-dot connection-dot-${__status.online ? 'online' : 'offline'}" aria-hidden="true"></span>
</div>
</div>
<div class="header-actions">
<nav class="nav-tabs">
<div class="nav-link">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
<span>admin</span>
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<path d="M16 17l5-5-5-5" />
<path d="M21 12H9" />
</svg>
</div>
</nav>
</div>
</div>
</div>
<main class="tab-stage">
<section id="overview" class="tab-panel tab-panel-active" role="tabpanel" aria-labelledby="tab-overview">
<div class="overview-grid" aria-label="Device metrics overview">
<article class="metric-tile">
<div class="metric-label">Temperature</div>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number" data-bind="metrics.temperature">24.8</span>
<span class="metric-unit">°C</span>
</div>
<span class="metric-status metric-status-good">ok</span>
</div>
</article>
<article class="metric-tile metric-tile-warning">
<div class="metric-label">CPU Load</div>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number" data-bind="metrics.cpu">37</span>
<span class="metric-unit">%</span>
</div>
<span class="metric-status metric-status-warn">moderate</span>
</div>
</article>
<article class="metric-tile metric-tile-alert">
<div class="metric-label">Free Memory</div>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number">${metrics.ram}</span>
<span class="metric-unit">KB</span>
</div>
<span class="metric-status metric-status-alert" style="${metrics.ram <= 30 ? '': 'display:none;'}">low</span>
<span class="metric-status metric-status-warn" style="${metrics.ram <= 50 && metrics.ram > 30 ? '': 'display:none;'}">ok-ish</span>
</div>
</article>
<article class="metric-tile">
<div class="metric-label">Volume</div>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number" data-bind="settings.volume">18.7</span>
<span class="metric-unit">dB</span>
</div>
<span class="metric-status metric-status-good">ok</span>
</div>
</article>
<article class="metric-tile note" style="min-width: 15rem;">
<div>
This dashboard source code is <a href="https://github.com/cesanta/mongoose/tree/master/tutorials/device-dashboard" target="_blank">here</a>.
To tailor it to your firmware, follow
<a target="_blank" href="https://mongoose.ws/docs/guides/web-dashboard-guide/#dashboard-example">this guide</a>
</div>
</article>
</div>
<div class="detail-row">
<!-- Sensor History Chart -->
<div class="panel chart-panel">
<div class="panel-title">
<span>Sensor History</span>
<div class="chart-legend">
<div class="legend-item"><span class="legend-dot legend-dot-color1"></span>Temperature</div>
<div class="legend-item"><span class="legend-dot legend-dot-color2"></span>Humidity</div>
</div>
</div>
<div class="panel-body">
<div id="chart"></div>
</div>
</div>
<!-- Controls Panel -->
<div class="panel">
<div class="panel-title">LED Control</div>
<div class="panel-body">
<div class="field">
<div class="field-label">LED1</div>
<input type="checkbox" data-bind="leds.led1" data-autosave="1" class="toggle" />
</div>
<div class="field">
<div class="field-label">LED2</div>
<input type="checkbox" data-bind="leds.led2" data-autosave="1" class="toggle" />
</div>
<div class="field">
<div class="field-label">LED3</div>
<input type="checkbox" data-bind="leds.led3" data-autosave="1" class="toggle" />
</div>
<div class="note">
To attach these LED controls to your hardware,
modify mongoose_dashboard.c :: sync_leds() function.
</div>
</div>
</div>
</div>
</section>
<section id="settings" class="tab-panel" role="tabpanel" aria-labelledby="tab-settings" hidden>
<div class="row">
<div class="panel">
<div class="panel-title">Device settings</div>
<div class="panel-body">
<div class="field">
<label class="field-label">Name</label>
<input class="field-input"type="text" data-bind="settings.name" data-autosave="1" />
</div>
<div class="field">
<label class="field-label">Volume</label>
<input class="field-input" type="text" data-bind="settings.volume" />
</div>
<div class="field">
<label class="field-label">Log Level</label>
<select class="field-input" data-bind="settings.log_level">
<option value="0">Disable logs</option>
<option value="1">Error</option>
<option value="2">Info</option>
<option value="3">Debug</option>
<option value="4">Verbose (very noisy)</option>
</select>
</div>
</div>
<div class="panel-footer">
<button class="btn btn-secondary" type="button" data-cancel="settings">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
Cancel
</button>
<button class="btn btn-primary" type="button" data-save="settings">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" />
<path d="M17 21v-8H7v8" />
<path d="M7 3v5h8" />
</svg>
Save
</button>
</div>
</div>
<div class="note" style="width: 44rem;">
<p>This settings panel is backed by the "settings" dashboard object
see Dashboard.init call at the top of index.html - and, consequently,
by the "settings" fieldset in mongoose_dashboard.c. </p>
<p>Fields are bound to the dashboard object via the data-bind attribute,
e.g. data-bind="setttings.name"</p>
<p>When you save changed fields, the changes are saved to the device.</p>
<p> The "NAME" field
has data-autosave="1", so it is autosaved when user types text.</p>
<p>The other fields are not auto-saved, so when they are changed,
dashboard.js automatically applies CSS class "edited" to them to
indicate they were changed but not saved.</p>
<p>The cancel and save buttons are attached to the settings object
via the data-cancel and data-save attributes. Those buttons
get automatically enabled or disabled based on editing status.</p>
</div>
</div>
</section>
<section id="firmware" class="tab-panel" role="tabpanel" aria-labelledby="tab-firmware" hidden></section>
<section id="files" class="tab-panel" role="tabpanel" aria-labelledby="tab-files" hidden>
<div class="row">
<div class="panel">
<div class="panel-title">
<span>
<span>File Manager</span>
<span class="metric-status">${files.data.length} Files</span>
</span>
<div class="header-actions">
<label class="btn btn-primary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v12" />
<path d="m7 8 5-5 5 5" />
<path d="M5 21h14" />
</svg>
Upload
<input type="file" hidden data-upload="1" />
</label>
</div>
</div>
<div class="panel-body">
<div class="file-list" data-repeat="files.data" data-iterator="f">
<div class="file-row">
<div class="file-info">
<span class="file-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7Z" />
<path d="M14 2v5h5" />
</svg>
</span>
<div class="file-name">${f.name}</div>
</div>
<div class="file-size">${f.size}</div>
<div class="file-actions">
<a href="files/${encodeURIComponent(f.name)}" target="_blank" class="btn btn-secondary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v12" />
<path d="m7 10 5 5 5-5" />
<path d="M5 21h14" />
</svg>
</a>
<button onclick="fetch('files/${encodeURIComponent(f.name)}?action=delete'); return false;" class="btn btn-secondary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18" />
<path d="M8 6V4h8v2" />
<path d="M19 6l-1 14H6L5 6" />
<path d="M10 11v5" />
<path d="M14 11v5" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="note">
<p>The two default files are fake - you can delete them</p>
<p>However the manager works - you can upload new files. They
are uploaded to /tmp/ directory on the device.
This requires stdio (fopen/fread/...) implemeneted.</p>
<p>The easiest way to implement it is to use a filesystem like
LittleFS on your internal or external flash
and retarget the standard library to use it.</p>
<p>See <a href="https://github.com/cpq/bare-metal-programming-guide/tree/main/templates/lfs/nucleo-h563zi">this example</a> on how LittleFS can be implemented on
Nucleo-H563ZI internal flash partition.</p>
</div>
</div>
</section>
</main>
</body>
</html>