mirror of
https://github.com/n30nex/CartoLite-Server.git
synced 2026-10-16 00:49:55 +00:00
* 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
222 lines
17 KiB
HTML
222 lines
17 KiB
HTML
<!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 & 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>
|