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:
+67
-18
@@ -89,11 +89,18 @@
|
|||||||
.gps-badge {
|
.gps-badge {
|
||||||
display: inline-flex; align-items: center; gap: 3px;
|
display: inline-flex; align-items: center; gap: 3px;
|
||||||
font-size: 10px; font-weight: 600; padding: 4px 8px; border-radius: 20px;
|
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.ok { background: var(--green-bg); color: var(--green); }
|
||||||
.gps-badge.waiting{ background: var(--amber-bg); color: var(--amber); }
|
.gps-badge.waiting{ background: var(--amber-bg); color: var(--amber); }
|
||||||
.gps-badge.err { background: var(--red-bg); color: var(--red); }
|
.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 {
|
.user-badge {
|
||||||
width: 30px; height: 30px; border-radius: 50%;
|
width: 30px; height: 30px; border-radius: 50%;
|
||||||
background: var(--accent-bg); color: var(--accent2);
|
background: var(--accent-bg); color: var(--accent2);
|
||||||
@@ -755,7 +762,7 @@
|
|||||||
<h1>📦 Canteen Assets</h1>
|
<h1>📦 Canteen Assets</h1>
|
||||||
<div class="header-badges">
|
<div class="header-badges">
|
||||||
<span id="roleBadge" class="role-badge guest" style="display:none;">guest</span>
|
<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>
|
<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 class="user-badge" id="userBadge" onclick="openDrawer()" title="User">?</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1463,27 +1470,55 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
// GPS
|
// 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() {
|
function initGPS() {
|
||||||
const badge = document.getElementById('gpsBadge');
|
const badge = document.getElementById('gpsBadge');
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
badge.className = 'gps-badge err';
|
badge.className = 'gps-badge err';
|
||||||
badge.textContent = '📍 Unavailable';
|
badge.textContent = '📍 Unavailable';
|
||||||
|
badge.style.cursor = 'default';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigator.geolocation.getCurrentPosition(
|
// Show a tappable hint so user knows to interact
|
||||||
pos => {
|
badge.className = 'gps-badge waiting';
|
||||||
AppState.gpsLat = pos.coords.latitude;
|
badge.textContent = '📍 Tap for GPS';
|
||||||
AppState.gpsLng = pos.coords.longitude;
|
badge.style.cursor = 'pointer';
|
||||||
AppState.gpsAcc = pos.coords.accuracy;
|
badge.onclick = function() {
|
||||||
badge.className = 'gps-badge ok';
|
badge.textContent = '📍 Locating...';
|
||||||
badge.textContent = `📍 ${AppState.gpsLat.toFixed(4)}, ${AppState.gpsLng.toFixed(4)}`;
|
badge.onclick = null; // one-shot
|
||||||
},
|
requestLocation().catch(() => {
|
||||||
err => {
|
badge.onclick = function() { requestLocation().catch(() => {}); };
|
||||||
badge.className = 'gps-badge err';
|
badge.style.cursor = 'pointer';
|
||||||
badge.textContent = '📍 ' + err.message;
|
badge.textContent = '📍 Tap to retry';
|
||||||
},
|
});
|
||||||
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 300000 }
|
};
|
||||||
);
|
// 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');
|
fd.append('file', manualPhotoFile, manualPhotoFile.name || 'photo.jpg');
|
||||||
const upData = await api('/api/upload/photo', { method: 'POST', body: fd });
|
const upData = await api('/api/upload/photo', { method: 'POST', body: fd });
|
||||||
photoPath = upData.path;
|
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 */ }
|
} catch (e) { /* photo upload failed, continue without */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2764,11 +2810,14 @@
|
|||||||
|
|
||||||
function fillGpsParking(inputId) {
|
function fillGpsParking(inputId) {
|
||||||
if (!AppState.gpsLat) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
document.getElementById(inputId).value = AppState.gpsLat.toFixed(6) + ', ' + AppState.gpsLng.toFixed(6);
|
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)
|
// Settings are loaded after auth (in doLogin / initAuth)
|
||||||
|
|||||||
Reference in New Issue
Block a user