Files
CartoLite-Server/web/index.html
T
n30nex abfcebb5e8 Refine map layers and cross-view UI usability (#16)
* Refine layer controls and cross-view UI accessibility

* Complete cumulative labels and landscape layer usability checks

* Keep inactive layer labels readable on light scenes

* Check cumulative observation wording in route inspection
2026-09-13 10:09:11 -04:00

222 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0b151b" />
<meta name="description" content="A self-hosted, privacy-safe live MeshCore traffic map." />
<meta property="og:type" content="website" />
<meta property="og:title" content="CartoLite Server" />
<meta property="og:description" content="A self-hosted, privacy-safe live view of high-confidence MeshCore RF traffic." />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="manifest" href="/site.webmanifest" />
<title>CartoLite Server</title>
</head>
<body>
<main id="app" aria-label="CartoLite live map">
<div id="map" aria-label="Live MeshCore route map"></div>
<div id="map-grade" class="map-grade" aria-hidden="true"></div>
<canvas id="packet-canvas" aria-hidden="true"></canvas>
<div class="toolbar">
<section id="topbar" class="topbar glass" aria-label="CartoLite navigation and live status">
<button id="about-button" class="brand" type="button" aria-haspopup="dialog" title="About CartoLite">
<span class="brand-mark" aria-hidden="true"></span><span>CartoLite</span>
</button>
<a id="netgraph-link" class="netgraph-link" href="/netgraph/" aria-label="Open CartoLite Netgraph" title="CartoLite Netgraph">
<span aria-hidden="true">⌘</span><span>Netgraph</span>
</a>
<div id="status" class="status" data-state="loading" role="status">
<span class="status-dot" aria-hidden="true"></span><span id="status-text">Starting…</span>
<span id="traffic-meter" class="traffic-meter" data-level="0" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i>
</span>
</div>
</section>
<section class="controls glass" aria-label="Map controls">
<button id="follow-button" class="control-button" type="button" aria-label="Live Follow" aria-pressed="false" title="Follow live packets">
<span aria-hidden="true">◎</span><span>Live Follow</span>
</button>
<div id="find-control" class="find-control">
<button id="find-button" class="control-button" type="button" aria-label="Find a node" aria-expanded="false" aria-controls="find-panel" title="Find a node">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="10.5" cy="10.5" r="6.5"></circle><path d="m15.5 15.5 4 4"></path>
</svg>
<span>Find</span>
</button>
<section id="find-panel" class="find-panel glass" aria-label="Find a node" hidden>
<label for="node-search">Find a node</label>
<input id="node-search" type="search" autocomplete="off" spellcheck="false" placeholder="Search public labels" aria-controls="node-search-results" />
<div id="node-search-results" class="node-search-results" role="listbox" aria-live="polite"></div>
</section>
</div>
<div id="layers-disclosure" class="layers-disclosure">
<button id="layers-summary" class="control-button" type="button" aria-label="Map layers and appearance" aria-expanded="false" aria-controls="layers-panel" title="Map layers and appearance">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="m4 7 8-4 8 4-8 4z"></path><path d="m4 12 8 4 8-4M4 17l8 4 8-4"></path>
</svg>
<span>Layers</span>
</button>
<div id="layers-panel" class="layers-panel glass" aria-label="Map layers and appearance" hidden>
<header class="map-options-header"><div><strong>Map layers</strong><p id="layer-summary" class="map-options-subtitle" aria-live="polite"></p></div><button id="layers-close" class="control-button icon-button" type="button" aria-label="Close map options">×</button></header>
<section class="map-layer-group" aria-labelledby="traffic-layer-title" aria-describedby="traffic-layer-help">
<h2 id="traffic-layer-title">Traffic</h2>
<div class="layer-toggles">
<button id="routes-button" class="control-button" type="button" aria-label="Routes" aria-pressed="false" title="Show routes">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="6" cy="19" r="3"></circle>
<path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"></path>
<circle cx="18" cy="5" r="3"></circle>
</svg>
<span>Routes</span>
</button>
<button id="live-packets-button" class="control-button" type="button" aria-label="Live packets" aria-pressed="true"><span>Live packets</span></button>
<button id="heatmap-button" class="control-button selected" type="button" aria-label="Heatmap" aria-pressed="true" title="Hide heatmap">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="2.5"></circle>
<circle cx="12" cy="12" r="6"></circle>
<path d="M6.3 4.5A9.5 9.5 0 0 1 21.5 12M17.7 19.5A9.5 9.5 0 0 1 2.5 12"></path>
</svg>
<span>Heatmap</span>
</button>
<button id="clusters-button" class="control-button selected" type="button" aria-label="Clusters" aria-pressed="true" title="Hide clusters">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="7" cy="8" r="3"></circle><circle cx="16.5" cy="7" r="2.5"></circle><circle cx="12" cy="16" r="3.5"></circle>
</svg>
<span>Clusters</span>
</button>
</div>
<label class="route-window" for="route-window">
<span>Links heard within</span>
<select id="route-window" aria-label="Route age window">
<option value="auto">Auto · 15m</option>
<option value="15m">15m</option>
<option value="1h">1h</option>
<option value="6h">6h</option>
<option value="24h">24h</option>
</select>
</label>
<p id="traffic-layer-help" class="layer-hint">Routes show previously heard links. Live packets control new motion and sound.</p>
</section>
<section class="map-layer-group" aria-labelledby="detail-layer-title">
<h2 id="detail-layer-title">Map details</h2>
<div class="layer-toggles">
<button id="node-labels-button" class="control-button" type="button" aria-label="Node labels" aria-pressed="true"><span>Node labels</span></button>
<button id="map-labels-button" class="control-button" type="button" aria-label="Place labels" aria-pressed="true"><span>Place labels</span></button>
<button id="roads-button" class="control-button" type="button" aria-label="Roads" aria-pressed="true"><span>Roads</span></button>
</div>
</section>
<details id="terrain-options" class="map-layer-group">
<summary>Terrain &amp; buildings <span id="terrain-summary">2D</span></summary>
<div class="layer-group-body">
<div class="layer-toggles">
<button id="hillshade-button" class="control-button" type="button" aria-label="Topography" aria-pressed="false" title="Show topography">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="m3 18 6-9 3 4 3-7 6 12z"></path><path d="M6 18h12"></path>
</svg>
<span>Topo</span>
</button>
<button id="terrain-button" class="control-button" type="button" aria-label="3D terrain" aria-pressed="false" title="Enable 3D terrain">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="m4 15 5-7 3 4 3-6 5 9"></path><path d="m4 15 8 5 8-5"></path>
</svg>
<span>3D</span>
</button>
<button id="buildings-button" class="control-button" type="button" aria-label="Buildings" aria-pressed="false"><span>Buildings</span></button>
</div>
<p id="building-status" class="layer-hint" aria-live="polite">Buildings appear at neighborhood zoom.</p>
<label class="map-slider" for="terrain-relief"><span>Topo shading</span><output id="terrain-relief-output" for="terrain-relief">75%</output><input id="terrain-relief" type="range" min="0" max="100" step="5" value="75" /></label>
<p class="layer-hint">Topo adds shading. 3D adds height and enables Topo and Buildings.</p>
<details class="camera-settings"><summary>Desktop 3D camera</summary>
<p id="camera-status" class="layer-hint">Turn on 3D to adjust the camera and terrain height.</p><button id="north-button" class="control-button reset-appearance" type="button" aria-label="Reset north"><span class="compass-needle" aria-hidden="true">↑</span> Reset north <output id="camera-bearing" aria-label="Camera bearing">0°</output></button>
<label class="map-slider" for="camera-pitch"><span>Camera pitch</span><output id="camera-pitch-output" for="camera-pitch">50°</output><input id="camera-pitch" type="range" min="0" max="65" step="1" value="50" /></label>
<label class="map-slider" for="terrain-height"><span>Terrain height</span><output id="terrain-height-output" for="terrain-height">1×</output><input id="terrain-height" type="range" min="0.5" max="2" step="0.1" value="1" /></label>
<p class="display-note">Height changes the terrain’s visual scale. Buildings use approximate mapped heights, not antenna heights.</p>
</details>
</div>
</details>
<button id="reset-layers" class="control-button reset-appearance" type="button">Reset layers</button>
<details id="map-appearance" class="map-appearance">
<summary>Appearance <span id="appearance-summary">Night · Crisp</span></summary>
<div id="map-appearance-content" class="map-appearance-content">
<label class="map-option" for="basemap-style"><span>Map style</span><select id="basemap-style"><option value="dark">Night</option><option value="light">Daylight</option><option value="streets">Streets</option></select></label>
<label class="map-option" for="interface-theme"><span>Interface</span><select id="interface-theme"><option value="map">Match map</option><option value="dark">Dark</option><option value="light">Light</option></select></label>
<label class="map-slider" for="route-opacity"><span>Route strength</span><output id="route-opacity-output" for="route-opacity">80%</output><input id="route-opacity" type="range" min="20" max="100" step="5" value="80" /></label>
</div>
<button id="reset-appearance" class="control-button reset-appearance" type="button">Reset all display settings</button>
<p class="layer-hint">Resets layers and shared Map/Netgraph appearance. Sound settings stay unchanged.</p>
</details>
</div>
</div>
<div class="sound-control">
<button id="sound-button" class="control-button" type="button" aria-label="Sound" aria-pressed="false" aria-expanded="false" aria-controls="sound-panel" title="Sound off">
<svg class="control-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 10v4h3l4 3V7l-4 3z"></path>
<path d="M15 9.5a4 4 0 0 1 0 5M17.5 7a7.5 7.5 0 0 1 0 10"></path>
</svg>
<span class="sound-button-label">Sound</span><span id="sound-state" class="sound-state">Off</span>
</button>
<section id="sound-panel" class="sound-panel glass" aria-label="Sound settings" hidden>
<header><strong>Sound</strong><span id="sound-panel-state" aria-live="polite">Off</span></header>
<button id="sound-toggle" class="sound-toggle" type="button">Turn sound on</button>
<label class="scene-control" for="sound-scene">
<span>Voice</span>
<select id="sound-scene" aria-label="Sound voice" aria-describedby="sound-description"></select>
</label>
<p id="sound-description" class="sound-description"></p>
<label class="volume-control" for="sound-volume">
<span>Volume</span><output id="sound-volume-output" for="sound-volume">80%</output>
<input id="sound-volume" type="range" min="0" max="100" step="1" value="80" />
</label>
<p><span id="sound-activity" class="sound-activity" aria-hidden="true"></span>Visible live hops only</p>
</section>
</div>
<button id="reset-button" class="control-button icon-button" type="button" title="Reset map view" aria-label="Reset map view">⌂</button>
</section>
</div>
<section id="follow-card" class="follow-card glass" aria-label="Live Follow activity" hidden>
<header><span class="follow-badge">Live Follow</span><button id="follow-pause" class="control-button" type="button">Pause</button><button id="follow-close" class="control-button icon-button" type="button" aria-label="Close Live Follow">×</button></header>
<div class="follow-copy" role="status" aria-live="polite" aria-atomic="true"><strong id="follow-title">Waiting for activity</strong><span id="follow-detail">Nearby activity is shown first</span></div>
<footer><span id="follow-state">Waiting for a live packet</span><output id="follow-countdown" aria-label="Seconds until next activity"></output></footer>
<progress id="follow-progress" max="10" value="0" aria-label="Time remaining on this activity"></progress>
</section>
<section id="legend" class="legend glass" aria-label="Map legend" data-collapsed="true" data-focused="false">
<button id="legend-toggle" class="legend-toggle" type="button" aria-label="Show map legend" aria-controls="legend-items" aria-expanded="false">
<span class="legend-toggle-mark" aria-hidden="true">i</span>
</button>
<div id="legend-items" class="legend-items">
<span><i class="node repeater"></i>Repeater</span>
<span><i class="node companion"></i>Companion</span>
<span><i class="node room"></i>Room</span>
</div>
<div id="focus-chip" class="focus-chip" role="status" aria-live="polite" aria-atomic="true" hidden>
<i aria-hidden="true"></i><span id="focus-text"></span>
</div>
</section>
<section id="route-legend" class="route-legend glass" aria-label="Route colors show the latest packet type" hidden></section>
<div id="tooltip" class="tooltip glass" role="tooltip" hidden></div>
<section id="node-inspector-sheet" class="node-inspector-sheet glass" aria-label="Selected node details" hidden></section>
<div id="fatal" class="fatal glass" role="alert" hidden>
Unable to load the map. Check your connection, then try again.<br />
<a class="control-button" href="">Try again</a>
</div>
<dialog id="about-dialog" class="about-dialog glass" aria-labelledby="about-title">
<button id="about-close" class="about-close" type="button" aria-label="Close CartoLite information">×</button>
<h1 id="about-title">CartoLite</h1>
<p>A self-hosted, privacy-safe view of high-confidence MeshCore RF traffic worldwide.</p>
<dl>
<div><dt>Map</dt><dd>Shared display presets keep routes and packets readable on light and dark scenes. 3D enables Topo and Buildings. Desktop camera and terrain height are adjustable (1× terrain height by default). Terrain and buildings provide visual context, not antenna heights or radio coverage.</dd></div>
<div><dt>Colours</dt><dd>Route and packet colours show the latest sanitized packet kind, never message content.</dd></div>
<div><dt>Sound</dt><dd>Opt in to hear every visible live hop with one of 30 voices, from soft keys and bells to synths, pings, and percussion. Choose a Voice in Sound settings. Volume and your choice stay only in this browser; remembered sound always waits for a tap before resuming.</dd></div>
<div><dt>Find</dt><dd>Search public node labels locally, then inspect active neighbours without sending or saving your query.</dd></div>
<div><dt>Server</dt><dd>This instance reads one operator-configured MQTT feed and publishes only a sanitized live topology.</dd></div>
<div><dt>Privacy</dt><dd>No public keys, packet payloads, message text, raw paths, or visitor analytics are published.</dd></div>
<div><dt>Updated</dt><dd id="last-update">Waiting for live state…</dd></div>
</dl>
</dialog>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>