docs: client portal design + milestone plan (M1 live view → M4 full auth) #61
+10
-1
@@ -118,10 +118,19 @@ live data, all from cache.
|
||||
page or a script for now).
|
||||
|
||||
### M2 — Dashboard + alerts
|
||||
- Richer client dashboard (multi-location at-a-glance, map, status rollup).
|
||||
- Richer client dashboard (multi-location at-a-glance, status rollup).
|
||||
- **Live project map** — upgrade the overview's basic location pins into a real
|
||||
project map: pins colored by measuring/level, popups showing each location's
|
||||
current reading, centered/zoomed to the project. (M1 ships the plain pin map;
|
||||
this makes it a live status map.)
|
||||
- Surface each location's **threshold-alert status** (read-only) + an event/inbox
|
||||
view. Leans on the SLMM alert engine + dispatch.
|
||||
|
||||
### Notes carried from M1
|
||||
- Tile headline metric is **Leq** (energy-average, the sound-monitoring compliance
|
||||
metric) — chosen over the twitchy instantaneous Lp. If clients ever want a
|
||||
different headline (e.g. Lmax for peaks), make it a per-deployment setting.
|
||||
|
||||
### M3 — Reports
|
||||
- Client-facing list + download of the daily baseline-comparison reports.
|
||||
- Depends on the FTP report pipeline (`feat/ftp-report-pipeline`) landing and
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-0.5 truncate">{{ loc.address or loc.project_name or '' }}</div>
|
||||
<div class="mt-3 flex items-baseline gap-1">
|
||||
<span class="loc-lp text-3xl font-bold text-seismo-orange">--</span>
|
||||
<span class="text-sm text-gray-400">dB Lp</span>
|
||||
<span class="loc-leq text-3xl font-bold text-seismo-orange">--</span>
|
||||
<span class="text-sm text-gray-400">dB Leq</span>
|
||||
</div>
|
||||
<div class="loc-fresh text-xs text-gray-500 mt-1"> </div>
|
||||
</a>
|
||||
@@ -51,7 +51,7 @@ function fmtAgo(iso) {
|
||||
async function loadTile(loc) {
|
||||
const el = document.querySelector(`.loc-tile[data-loc="${loc.id}"]`);
|
||||
if (!el) return;
|
||||
const lp = el.querySelector('.loc-lp'), badge = el.querySelector('.loc-badge'),
|
||||
const leq = el.querySelector('.loc-leq'), badge = el.querySelector('.loc-badge'),
|
||||
fresh = el.querySelector('.loc-fresh');
|
||||
try {
|
||||
const j = await (await fetch(`/portal/api/location/${encodeURIComponent(loc.id)}/live`)).json();
|
||||
@@ -60,10 +60,10 @@ async function loadTile(loc) {
|
||||
if (!d) {
|
||||
badge.textContent = j.reason === 'no_device' ? 'No device' : 'Offline';
|
||||
badge.className = 'loc-badge shrink-0 px-2 py-0.5 text-xs rounded-full bg-slate-700 text-gray-300';
|
||||
lp.textContent = '--'; fresh.innerHTML = ' ';
|
||||
leq.textContent = '--'; fresh.innerHTML = ' ';
|
||||
return;
|
||||
}
|
||||
lp.textContent = (d.lp == null || d.lp === '') ? '--' : d.lp;
|
||||
leq.textContent = (d.leq == null || d.leq === '') ? '--' : d.leq;
|
||||
const measuring = d.measurement_state === 'Start' || d.measurement_state === 'Measure';
|
||||
badge.textContent = measuring ? '● Live' : 'Stopped';
|
||||
badge.className = 'loc-badge shrink-0 px-2 py-0.5 text-xs rounded-full ' +
|
||||
|
||||
Reference in New Issue
Block a user