docs: client portal design + milestone plan (M1 live view → M4 full auth) #61

Merged
serversdown merged 27 commits from feat/client-portal into dev 2026-06-11 23:21:53 -04:00
2 changed files with 15 additions and 6 deletions
Showing only changes of commit b971d19068 - Show all commits
+10 -1
View File
@@ -118,10 +118,19 @@ live data, all from cache.
page or a script for now). page or a script for now).
### M2 — Dashboard + alerts ### 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 - Surface each location's **threshold-alert status** (read-only) + an event/inbox
view. Leans on the SLMM alert engine + dispatch. 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 ### M3 — Reports
- Client-facing list + download of the daily baseline-comparison reports. - Client-facing list + download of the daily baseline-comparison reports.
- Depends on the FTP report pipeline (`feat/ftp-report-pipeline`) landing and - Depends on the FTP report pipeline (`feat/ftp-report-pipeline`) landing and
+5 -5
View File
@@ -20,8 +20,8 @@
</div> </div>
<div class="text-xs text-gray-400 mt-0.5 truncate">{{ loc.address or loc.project_name or '' }}</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"> <div class="mt-3 flex items-baseline gap-1">
<span class="loc-lp text-3xl font-bold text-seismo-orange">--</span> <span class="loc-leq text-3xl font-bold text-seismo-orange">--</span>
<span class="text-sm text-gray-400">dB Lp</span> <span class="text-sm text-gray-400">dB Leq</span>
</div> </div>
<div class="loc-fresh text-xs text-gray-500 mt-1">&nbsp;</div> <div class="loc-fresh text-xs text-gray-500 mt-1">&nbsp;</div>
</a> </a>
@@ -51,7 +51,7 @@ function fmtAgo(iso) {
async function loadTile(loc) { async function loadTile(loc) {
const el = document.querySelector(`.loc-tile[data-loc="${loc.id}"]`); const el = document.querySelector(`.loc-tile[data-loc="${loc.id}"]`);
if (!el) return; 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'); fresh = el.querySelector('.loc-fresh');
try { try {
const j = await (await fetch(`/portal/api/location/${encodeURIComponent(loc.id)}/live`)).json(); const j = await (await fetch(`/portal/api/location/${encodeURIComponent(loc.id)}/live`)).json();
@@ -60,10 +60,10 @@ async function loadTile(loc) {
if (!d) { if (!d) {
badge.textContent = j.reason === 'no_device' ? 'No device' : 'Offline'; 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'; 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 = '&nbsp;'; leq.textContent = '--'; fresh.innerHTML = '&nbsp;';
return; 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'; const measuring = d.measurement_state === 'Start' || d.measurement_state === 'Measure';
badge.textContent = measuring ? '● Live' : 'Stopped'; badge.textContent = measuring ? '● Live' : 'Stopped';
badge.className = 'loc-badge shrink-0 px-2 py-0.5 text-xs rounded-full ' + badge.className = 'loc-badge shrink-0 px-2 py-0.5 text-xs rounded-full ' +