v3: GPS tap-to-enable + server-side EXIF bypass for manual creation

GPS:
- initGPS() now shows tappable GPS badge instead of failing silently
- Badge pulses amber with '📍 Tap for GPS' — click triggers permission prompt
- fillGpsParking() auto-triggers badge click if GPS not yet available
- requestLocation() extracted as reusable Promise

EXIF bypass:
- submitManualAsset() now uses server-extracted GPS from raw upload bytes
- Server returns exif_gps in /api/upload/photo response (was already working)
- Client was ignoring it — now fills parking + map link fields automatically
- Bypasses Android content provider EXIF stripping since server reads raw bytes

Admin separation:
- admin.canteen.ourpad.casa now served by systemd service (canteen-admin.service)
- Main server also systemd (canteen-main.service) — both auto-restart on crash/boot
This commit is contained in:
2026-05-21 22:53:14 -04:00
parent bc3d27f3a2
commit e2025126a8
+67 -18
View File
@@ -89,11 +89,18 @@
.gps-badge {
display: inline-flex; align-items: center; gap: 3px;
font-size: 10px; font-weight: 600; padding: 4px 8px; border-radius: 20px;
white-space: nowrap;
white-space: nowrap; transition: 0.2s;
}
.gps-badge.ok { background: var(--green-bg); color: var(--green); }
.gps-badge.waiting{ background: var(--amber-bg); color: var(--amber); }
.gps-badge.err { background: var(--red-bg); color: var(--red); }
.gps-badge.waiting:hover,
.gps-badge.err:hover { filter: brightness(1.15); }
.gps-badge.waiting { animation: pulse-gps 2s ease-in-out infinite; }
@keyframes pulse-gps {
0%, 100% { opacity: 1; }
50% { opacity: 0.65; }
}
.user-badge {
width: 30px; height: 30px; border-radius: 50%;
background: var(--accent-bg); color: var(--accent2);
@@ -755,7 +762,7 @@
<h1>📦 Canteen Assets</h1>
<div class="header-badges">
<span id="roleBadge" class="role-badge guest" style="display:none;">guest</span>
<span id="gpsBadge" class="gps-badge waiting">📍 GPS...</span>
<span id="gpsBadge" class="gps-badge waiting" onclick="void(0)" style="cursor:pointer;">📍 Tap GPS</span>
<span id="queueIndicator" class="queue-indicator" onclick="processOfflineQueue()" title="Offline queue">⬇️ <span id="queueIndicatorCount">0</span></span>
<div class="user-badge" id="userBadge" onclick="openDrawer()" title="User">?</div>
</div>
@@ -1463,27 +1470,55 @@
// =========================================================================
// GPS
// =========================================================================
function requestLocation() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
pos => {
AppState.gpsLat = pos.coords.latitude;
AppState.gpsLng = pos.coords.longitude;
AppState.gpsAcc = pos.coords.accuracy;
const badge = document.getElementById('gpsBadge');
badge.className = 'gps-badge ok';
badge.textContent = `📍 ${AppState.gpsLat.toFixed(4)}, ${AppState.gpsLng.toFixed(4)}`;
resolve(pos);
},
err => {
const badge = document.getElementById('gpsBadge');
badge.className = 'gps-badge err';
if (err.code === 1) badge.textContent = '📍 Tap for GPS';
else badge.textContent = '📍 ' + err.message;
reject(err);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 300000 }
);
});
}
function initGPS() {
const badge = document.getElementById('gpsBadge');
if (!navigator.geolocation) {
badge.className = 'gps-badge err';
badge.textContent = '📍 Unavailable';
badge.style.cursor = 'default';
return;
}
navigator.geolocation.getCurrentPosition(
pos => {
AppState.gpsLat = pos.coords.latitude;
AppState.gpsLng = pos.coords.longitude;
AppState.gpsAcc = pos.coords.accuracy;
badge.className = 'gps-badge ok';
badge.textContent = `📍 ${AppState.gpsLat.toFixed(4)}, ${AppState.gpsLng.toFixed(4)}`;
},
err => {
badge.className = 'gps-badge err';
badge.textContent = '📍 ' + err.message;
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 300000 }
);
// Show a tappable hint so user knows to interact
badge.className = 'gps-badge waiting';
badge.textContent = '📍 Tap for GPS';
badge.style.cursor = 'pointer';
badge.onclick = function() {
badge.textContent = '📍 Locating...';
badge.onclick = null; // one-shot
requestLocation().catch(() => {
badge.onclick = function() { requestLocation().catch(() => {}); };
badge.style.cursor = 'pointer';
badge.textContent = '📍 Tap to retry';
});
};
// Also try silently once in case browser allows it without gesture
requestLocation().catch(() => {
// Silent fail is expected on mobile — badge click handler covers it
});
}
// =========================================================================
@@ -2637,6 +2672,17 @@
fd.append('file', manualPhotoFile, manualPhotoFile.name || 'photo.jpg');
const upData = await api('/api/upload/photo', { method: 'POST', body: fd });
photoPath = upData.path;
// Use server-extracted GPS from raw bytes (bypasses client-side EXIF stripping)
if (upData.exif_gps) {
const coords = upData.exif_gps.lat.toFixed(6) + ', ' + upData.exif_gps.lng.toFixed(6);
if (!document.getElementById('manParkingLocation').value.trim()) {
document.getElementById('manParkingLocation').value = coords;
}
if (!document.getElementById('manMapLink').value.trim()) {
document.getElementById('manMapLink').value = `https://www.openstreetmap.org/?mlat=${upData.exif_gps.lat}&mlon=${upData.exif_gps.lng}&zoom=17`;
}
showToast('📍 GPS extracted from photo!');
}
} catch (e) { /* photo upload failed, continue without */ }
}
@@ -2764,11 +2810,14 @@
function fillGpsParking(inputId) {
if (!AppState.gpsLat) {
showToast('GPS not available — allow location access', true);
showToast('📍 Tap the GPS badge in the header to enable location', true);
// Also trigger the GPS badge click if it exists
const badge = document.getElementById('gpsBadge');
if (badge && badge.onclick) badge.onclick();
return;
}
document.getElementById(inputId).value = AppState.gpsLat.toFixed(6) + ', ' + AppState.gpsLng.toFixed(6);
showToast('GPS coords filled!');
showToast('📍 GPS coords filled!');
}
// Settings are loaded after auth (in doLogin / initAuth)