diff --git a/docs/plans/2026-05-20-six-features.md b/docs/plans/2026-05-20-six-features.md
index 463aa33..ec3f873 100644
--- a/docs/plans/2026-05-20-six-features.md
+++ b/docs/plans/2026-05-20-six-features.md
@@ -39,14 +39,16 @@
---
-## Feature 4: Building # → trailer number sync
+## Feature 4: Building # → trailer number sync ✅ DONE
**Current:** `building_number` and `trailer_number` are separate fields
**Target:** When building_number is entered, auto-populate trailer_number with same value
**Files:**
- `static/index.html` — manual form event listeners, auto-checkin logic
-**Change:** Add an `oninput` handler on the building number field that copies the value to trailer number. Also handle the OCR/barcode creation paths if they ever populate building number.
+**Change:** Added `oninput` handler on `manBuildingNumber` → copies to `manAddress` (address/trailer combo field) and `locFormBldgNum` → copies to `locFormTrailer`.
+
+**Commit:** 02b3ba6
---
diff --git a/server.py b/server.py
index 251d822..066d950 100644
--- a/server.py
+++ b/server.py
@@ -13,6 +13,8 @@ import os
import re
import secrets
import sqlite3
+import urllib.error
+import urllib.request
import uuid
from contextlib import asynccontextmanager
from pathlib import Path
diff --git a/static/index.html b/static/index.html
index 1b060b0..da57006 100644
--- a/static/index.html
+++ b/static/index.html
@@ -453,7 +453,8 @@
@keyframes fadeIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
.scan-result .sr-name { font-size: 17px; font-weight: 700; }
.scan-result .sr-meta { font-size: 12px; color: var(--text2); margin-top: 2px; }
- .scan-result .sr-actions { display: flex; gap: 8px; margin-top: 10px; }
+ .scan-result .sr-actions { display: flex; gap: 8px; margin-top: 10px; align-items: center; }
+ .scan-result .sr-checkin-done { font-size: 13px; color: #22c55e; font-weight: 600; white-space: nowrap; }
/* ═══════════════════════════════════════════════════════════════════════
MODE TOGGLES (Add Asset tab)
@@ -2717,12 +2718,10 @@
${asset.status || 'active'}
-
+ ✓ Auto checked in
`;
-
- document.getElementById('checkinCard').style.display = 'block';
}
function showNewAssetForm(machineId) {
@@ -3085,6 +3084,18 @@
name: name,
serial_number: document.getElementById('manSerialNumber').value.trim(),
description: document.getElementById('manDescription').value.trim(),
+
+ // F2: Auto-populate address from GPS (only if address field is empty)
+ if (AppState.gpsLat && !document.getElementById('manAddress').value.trim()) {
+ const geo = await reverseGeocode(AppState.gpsLat, AppState.gpsLng);
+ if (geo) {
+ if (!document.getElementById('manAddress').value.trim()) {
+ document.getElementById('manAddress').value = geo.address || geo.formatted || '';
+ document.getElementById('manBuildingNumber').value = geo.building_number || '';
+ document.getElementById('manBuildingName').value = geo.building_name || '';
+ }
+ }
+ }
category: document.getElementById('manCatSelect').value || 'Other',
make: makeName,
model: modelName,
@@ -3493,9 +3504,148 @@
if (!AppState.currentAssetId) return;
api('/api/assets/' + AppState.currentAssetId).then(a => {
if (a.map_link) window.open(a.map_link, '_blank');
+ else if (a.latitude && a.longitude) {
+ // Fallback: open Google Maps from current position
+ const origin = AppState.gpsLat ? `${AppState.gpsLat},${AppState.gpsLng}` : '';
+ const dest = `${a.latitude},${a.longitude}`;
+ const url = origin
+ ? `https://www.google.com/maps/dir/?api=1&origin=${origin}&destination=${dest}&travelmode=driving`
+ : `https://www.google.com/maps/search/?api=1&query=${dest}`;
+ window.open(url, '_blank');
+ } else {
+ showToast('No location data for this asset', true);
+ }
}).catch(() => {});
}
+ // F6: Navigate — open Google Maps with directions from user's current GPS
+ async function navigateToAsset() {
+ if (!AppState.currentAssetId) return;
+ if (!AppState.gpsLat) {
+ showToast('GPS not available — allow location access', true);
+ return;
+ }
+ try {
+ const a = await api('/api/assets/' + AppState.currentAssetId);
+ if (!a.latitude || !a.longitude) {
+ showToast('Asset has no location — check in first', true);
+ return;
+ }
+ const origin = `${AppState.gpsLat},${AppState.gpsLng}`;
+ const dest = `${a.latitude},${a.longitude}`;
+ const url = `https://www.google.com/maps/dir/?api=1&origin=${origin}&destination=${dest}&travelmode=driving`;
+ window.open(url, '_blank');
+ } catch (e) {
+ showToast('Navigation error: ' + e.message, true);
+ }
+ }
+
+ async function navigateToAsset() {
+ if (!AppState.currentAssetId) return;
+ if (!AppState.gpsLat || !AppState.gpsLng) {
+ showToast('GPS location not available. Tap ◎ My Location first.', true);
+ return;
+ }
+ try {
+ const nav = await api('/api/assets/' + AppState.currentAssetId +
+ '/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng);
+
+ // Switch to map tab
+ switchTab('tabMap');
+
+ // Initialize map if needed
+ if (!map) { initMap(); }
+
+ // Wait for map to be ready
+ setTimeout(() => {
+ if (!map) return;
+
+ // Clear previous route line
+ clearRouteLine();
+
+ // Draw route line from origin (user GPS) to destination (asset)
+ const routeCoords = [
+ [nav.origin_lat, nav.origin_lng],
+ [nav.asset_lat, nav.asset_lng]
+ ];
+
+ map._routeLine = L.polyline(routeCoords, {
+ color: '#5b6ef7',
+ weight: 4,
+ opacity: 0.8,
+ dashArray: '10, 6',
+ }).addTo(map);
+
+ // Add arrow markers at both ends
+ const originIcon = L.divIcon({
+ html: '',
+ className: '',
+ iconSize: [16, 16],
+ iconAnchor: [8, 8],
+ });
+
+ const destIcon = L.divIcon({
+ html: '📍
',
+ className: '',
+ iconSize: [20, 20],
+ iconAnchor: [10, 10],
+ });
+
+ if (map._routeOriginMarker) map.removeLayer(map._routeOriginMarker);
+ map._routeOriginMarker = L.marker([nav.origin_lat, nav.origin_lng], { icon: originIcon }).addTo(map);
+
+ if (map._routeDestMarker) map.removeLayer(map._routeDestMarker);
+ map._routeDestMarker = L.marker([nav.asset_lat, nav.asset_lng], { icon: destIcon }).addTo(map);
+
+ // Distance label at midpoint
+ const midLat = (nav.origin_lat + nav.asset_lat) / 2;
+ const midLng = (nav.origin_lng + nav.asset_lng) / 2;
+ const distText = nav.distance_km >= 1
+ ? nav.distance_km + ' km'
+ : nav.distance_meters + ' m';
+
+ if (map._routeDistLabel) map.removeLayer(map._routeDistLabel);
+ map._routeDistLabel = L.marker([midLat, midLng], {
+ icon: L.divIcon({
+ html: '' + distText + ' ' + nav.cardinal + '
',
+ className: '',
+ iconSize: null,
+ iconAnchor: null,
+ }),
+ }).addTo(map);
+
+ // Fit bounds to show the whole route
+ const bounds = L.latLngBounds(routeCoords);
+ map.fitBounds(bounds, { padding: [60, 60], maxZoom: 16 });
+
+ // Show info toast
+ showToast('📏 ' + distText + ' ' + nav.cardinal + ' to ' + nav.asset_name);
+
+ // If asset has address, add popup to destination marker
+ if (nav.asset_address) {
+ map._routeDestMarker.bindPopup(
+ '' +
+ '
' + esc(nav.asset_name) + '
' +
+ '
📍 ' + esc(nav.asset_address) + '
' +
+ '
🧭 Open in Google Maps' +
+ '
'
+ ).openPopup();
+ }
+ }, 300);
+
+ } catch (e) {
+ showToast(e.message, true);
+ }
+ }
+
+ function clearRouteLine() {
+ if (!map) return;
+ if (map._routeLine) { map.removeLayer(map._routeLine); map._routeLine = null; }
+ if (map._routeOriginMarker) { map.removeLayer(map._routeOriginMarker); map._routeOriginMarker = null; }
+ if (map._routeDestMarker) { map.removeLayer(map._routeDestMarker); map._routeDestMarker = null; }
+ if (map._routeDistLabel) { map.removeLayer(map._routeDistLabel); map._routeDistLabel = null; }
+ }
+
async function quickCheckin() {
if (!AppState.currentAssetId) return;
const payload = {