feat(deployments): mobile capture wizard + classify hopper + dashboard banner
UI for the pending-deployment workflow (commits 2 + 3 from the plan,
landed together since commit 1 already shipped the full backend).
New surfaces
- /deploy — mobile-first 3-step wizard. Pick unit → take photo (uses
<input capture="environment"> so it opens the phone camera) → add
optional note + submit. EXIF GPS auto-extracted on the server.
Success page shows the captured coords + links to either "Deploy
another" or "View pending hopper." Whole flow is meant to take
under 90 seconds on site.
- /tools/pending-deployments — the hopper. Filter pills: Awaiting /
Assigned / Cancelled. Each card shows photo thumbnail, unit serial
link, captured-at timestamp, coordinates, operator note, and
status-appropriate actions.
- Classify modal on the hopper: two modes — "Assign to existing
location" (project + location pickers, scoped to vibration_monitoring)
or "Create new location" (with new-or-existing project, plus a
"use captured coords" checkbox that writes the pending row's coords
onto the new location). Calls /pending/{id}/promote on submit.
- Cancel button uses prompt() for the optional reason → POSTs to
/pending/{id}/cancel.
Backend additions
- GET /api/deployments/seismograph-picker — JSON list of non-retired
seismograph units for the /deploy unit picker. Annotates each unit
with has_pending so the picker can flag units that already have a
pending capture waiting.
Discovery
- New "Field Deploy" + "Pending Deployments" cards on /tools.
- Dashboard banner: auto-shows when there are awaiting captures,
polled every 30s. Hides when count drops to 0. Click → /tools/
pending-deployments.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending-deployments banner — auto-shows when there are field captures
|
||||
awaiting classification. Hides itself when count is 0. Polled
|
||||
alongside the rest of the dashboard's 10-second refresh. -->
|
||||
<a id="pending-deploy-banner" href="/tools/pending-deployments"
|
||||
class="hidden mb-6 flex items-center justify-between gap-3 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 hover:bg-amber-100 dark:hover:bg-amber-900/30 transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-amber-100 dark:bg-amber-900/40 text-amber-600 dark:text-amber-400 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-amber-900 dark:text-amber-200">
|
||||
<span id="pending-deploy-count">0</span> field deployment<span id="pending-deploy-plural">s</span> awaiting classification
|
||||
</div>
|
||||
<div class="text-xs text-amber-700 dark:text-amber-300">Click to pick project / location for these captures</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-amber-600 dark:text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<script>
|
||||
async function _refreshPendingDeployBanner() {
|
||||
try {
|
||||
const r = await fetch('/api/deployments/pending?status=awaiting');
|
||||
if (!r.ok) return;
|
||||
const data = await r.json();
|
||||
const banner = document.getElementById('pending-deploy-banner');
|
||||
const countEl = document.getElementById('pending-deploy-count');
|
||||
const pluralEl = document.getElementById('pending-deploy-plural');
|
||||
if (data.count > 0) {
|
||||
countEl.textContent = data.count;
|
||||
pluralEl.textContent = data.count === 1 ? '' : 's';
|
||||
banner.classList.remove('hidden');
|
||||
} else {
|
||||
banner.classList.add('hidden');
|
||||
}
|
||||
} catch (e) {
|
||||
/* silent — banner just stays hidden */
|
||||
}
|
||||
}
|
||||
_refreshPendingDeployBanner();
|
||||
setInterval(_refreshPendingDeployBanner, 30000);
|
||||
</script>
|
||||
|
||||
<!-- Dashboard cards with auto-refresh -->
|
||||
<div hx-get="/api/status-snapshot"
|
||||
hx-trigger="load, every 10s"
|
||||
|
||||
Reference in New Issue
Block a user