fix(pwa): Offline-Seite ausliefern, Fremdabhaengigkeiten der Karte entfernen

- Der Service-Worker-Cache wurde nie invalidiert: die letzte Runde hat
  offline.html in allen vier Zielen ersetzt, sw.js aber nicht angefasst.
  Ein Browser installiert einen Service Worker nur neu, wenn sich sw.js
  selbst aendert — bestehende Installationen haetten die alte, englische,
  ungestaltete Offline-Seite dauerhaft behalten. CACHE_NAME hochgezaehlt.
  Dabei faengt der Install jetzt einen fehlgeschlagenen Abruf ab; vorher
  wurde bei einem Fehler der ganze Service Worker nicht aktiv.
- Die Leaflet-Marker kamen von cdnjs.cloudflare.com — in einer PWA, die im
  Wald ohne Empfang benutzt wird, waren sie damit kaputt, und die IP jedes
  Besuchers ging an ein fremdes CDN. Verwiesen wurde ausserdem auf Leaflet
  1.7.1 bei installiertem 1.9.4. Die Bilder liegen im Paket und werden jetzt
  gebuendelt; Vite bettet sie als data:-URI ein (zusammen ~4 KB).
- Die Adress-Autovervollstaendigung im Benutzerformular rief Nominatim
  weiterhin direkt aus dem Browser auf, obwohl d9fecb6 dafuer einen Proxy
  eingefuehrt hat — nur die PLZ-Suche nutzte ihn. Neu: searchAddresses() in
  utils/geocode.js (teilt Wartezeit und Cache mit geocodeAddress) plus
  GET /api/public/geocode/search unter demselben geocodeLimiter. Die Antwort
  ist auf die Felder beschraenkt, die das Formular liest. Der Debounce steigt
  von 400 auf 600 ms, weil die Anfragen jetzt durch die serverseitige
  Mindestwartezeit laufen.

Geprueft: im Bundle kein cdnjs und kein nominatim mehr; der neue Endpunkt
liefert Treffer mit den erwarteten Feldern, zu kurze Begriffe 400.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
thomas 2026-08-30 20:47:26 +02:00
parent d0e48f3385
commit 87011eee3c
20 changed files with 280 additions and 55 deletions

View File

@ -1,5 +1,5 @@
const User = require('../models/User');
const { geocodeAddress } = require('../utils/geocode');
const { geocodeAddress, searchAddresses } = require('../utils/geocode');
const logger = require('../utils/logger');
const { escapeCell } = require('../utils/csv');
const config = require('../config/env');
@ -729,6 +729,32 @@ const getGeocodeByPostalCode = async (req, res) => {
}
};
/**
* GET /api/public/geocode/search?q=...
* Freitext-Adresssuche fuer die Autovervollstaendigung im Benutzerformular.
* Laeuft ueber den Server, damit Mindestwartezeit, Cache und der von Nominatim
* geforderte User-Agent greifen und keine Nutzer-IP beim Dienst landet.
*/
const searchAddressSuggestions = async (req, res) => {
const { q } = req.query;
const query = String(q || '').trim();
if (query.length < 3) {
return res.status(400).json({ success: false, message: 'Suchbegriff zu kurz (min. 3 Zeichen)' });
}
if (query.length > 200) {
return res.status(400).json({ success: false, message: 'Suchbegriff zu lang' });
}
try {
const results = await searchAddresses(query, req.query.limit);
res.json({ success: true, data: results });
} catch (error) {
logger.error('Fehler bei der Adresssuche:', error);
res.status(500).json({ success: false, message: 'Fehler bei der Adresssuche' });
}
};
module.exports = {
getAllUsers,
getUserById,
@ -746,5 +772,6 @@ module.exports = {
bulkDeleteUsers,
uploadUserPhoto,
deleteUserPhoto,
getGeocodeByPostalCode
getGeocodeByPostalCode,
searchAddressSuggestions
};

View File

@ -21,13 +21,15 @@ const {
bulkDeleteUsers,
uploadUserPhoto,
deleteUserPhoto,
getGeocodeByPostalCode
getGeocodeByPostalCode,
searchAddressSuggestions
} = require('../controllers/userController');
// Public routes
router.get('/public/users', getPublicUsers);
// Eigenes, engeres Limit: der Endpunkt loest ausgehende Nominatim-Anfragen aus.
router.get('/public/geocode', geocodeLimiter, getGeocodeByPostalCode);
router.get('/public/geocode/search', geocodeLimiter, searchAddressSuggestions);
// Protected routes (require authentication)
router.get('/users', authenticateToken, getAllUsers);

View File

@ -1,11 +1,13 @@
// Service Worker: nur Offline-Fallback, keine Asset-Caches
// Vite erzeugt content-addressierte Hashes, kein manuelles Caching nötig
const CACHE_NAME = 'drohnenfuehrer-offline-v1';
const CACHE_NAME = 'drohnenfuehrer-offline-v2';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.add('offline.html'))
caches.open(CACHE_NAME)
.then(cache => cache.add('offline.html'))
.catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err))
);
self.skipWaiting();
});

View File

@ -2,14 +2,20 @@ import React, { useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Circle, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
import markerIcon from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
import './MapView.css';
// Fix für Standard-Marker-Icons in Leaflet
// Standard-Marker-Icons aus dem installierten Leaflet-Paket buendeln.
// Vorher kamen sie von cdnjs — in einer Offline-PWA fuer den Wald waren die
// Marker damit ohne Empfang kaputt, und die IP jedes Besuchers ging an ein
// fremdes CDN. Die Dateien liegen ohnehin in leaflet/dist/images.
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
iconRetinaUrl: markerIcon2x,
iconUrl: markerIcon,
shadowUrl: markerShadow,
});
// Komponente zum Aktualisieren der Kartenansicht

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { useConfigContext } from '../../contexts/ConfigContext';
import { searchAddresses } from '../../services/users';
import './UserForm.css';
const formatSuggestionAddress = (addr) => {
@ -125,17 +126,13 @@ const UserForm = ({ user, onSave, onCancel }) => {
searchTimer.current = setTimeout(async () => {
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&countrycodes=de&addressdetails=1&limit=5&q=${encodeURIComponent(value)}`,
{ headers: { 'User-Agent': 'drohnenfuehrer-app/1.0 (admin@kasimirat.de)' } }
);
const data = await res.json();
setSuggestions(Array.isArray(data) ? data : []);
const result = await searchAddresses(value);
setSuggestions(result.data);
setShowSuggestions(true);
} catch {
setSuggestions([]);
}
}, 400);
}, 600);
};
const selectSuggestion = (s) => {
@ -233,7 +230,8 @@ const UserForm = ({ user, onSave, onCancel }) => {
</div>
<div className="form-group">
<label>GPS-Koordinaten</label>
{/* Gruppenueberschrift: gehoert nicht zu einem einzelnen Feld */}
<span className="form-group-heading">GPS-Koordinaten</span>
{gpsAutoSet ? (
<p className="gps-hint gps-hint-success"> GPS automatisch aus Adresse übernommen</p>
) : (

View File

@ -214,6 +214,22 @@ export const uploadUserPhoto = async (id, photoDataUrl) => {
}
};
// Freitext-Adresssuche ueber das eigene Backend statt direkt bei Nominatim:
// dort greifen Mindestwartezeit, Cache und der vorgeschriebene User-Agent,
// und es wandert keine Nutzer-IP zu einem fremden Dienst.
export const searchAddresses = async (query) => {
try {
const response = await api.get('/public/geocode/search', { params: { q: query } });
return { success: true, data: response.data.data || [] };
} catch (error) {
return {
success: false,
data: [],
message: error.response?.data?.message || 'Fehler bei der Adresssuche'
};
}
};
// Einmal-Token, mit dem ein Fuehrer sein erstes Passwort setzt.
// Laeuft ueber die Admin-Session (Cookie), nicht ueber den Fuehrer-Token.
export const generateInviteToken = async (id) => {

View File

@ -1,5 +1,5 @@
const User = require('../models/User');
const { geocodeAddress } = require('../utils/geocode');
const { geocodeAddress, searchAddresses } = require('../utils/geocode');
const logger = require('../utils/logger');
const { escapeCell } = require('../utils/csv');
const config = require('../config/env');
@ -733,6 +733,32 @@ const getGeocodeByPostalCode = async (req, res) => {
}
};
/**
* GET /api/public/geocode/search?q=...
* Freitext-Adresssuche fuer die Autovervollstaendigung im Benutzerformular.
* Laeuft ueber den Server, damit Mindestwartezeit, Cache und der von Nominatim
* geforderte User-Agent greifen und keine Nutzer-IP beim Dienst landet.
*/
const searchAddressSuggestions = async (req, res) => {
const { q } = req.query;
const query = String(q || '').trim();
if (query.length < 3) {
return res.status(400).json({ success: false, message: 'Suchbegriff zu kurz (min. 3 Zeichen)' });
}
if (query.length > 200) {
return res.status(400).json({ success: false, message: 'Suchbegriff zu lang' });
}
try {
const results = await searchAddresses(query, req.query.limit);
res.json({ success: true, data: results });
} catch (error) {
logger.error('Fehler bei der Adresssuche:', error);
res.status(500).json({ success: false, message: 'Fehler bei der Adresssuche' });
}
};
module.exports = {
getAllUsers,
getUserById,
@ -750,5 +776,6 @@ module.exports = {
bulkDeleteUsers,
uploadUserPhoto,
deleteUserPhoto,
getGeocodeByPostalCode
getGeocodeByPostalCode,
searchAddressSuggestions
};

View File

@ -21,13 +21,15 @@ const {
bulkDeleteUsers,
uploadUserPhoto,
deleteUserPhoto,
getGeocodeByPostalCode
getGeocodeByPostalCode,
searchAddressSuggestions
} = require('../controllers/userController');
// Public routes
router.get('/public/users', getPublicUsers);
// Eigenes, engeres Limit: der Endpunkt loest ausgehende Nominatim-Anfragen aus.
router.get('/public/geocode', geocodeLimiter, getGeocodeByPostalCode);
router.get('/public/geocode/search', geocodeLimiter, searchAddressSuggestions);
// Protected routes (require authentication)
router.get('/users', authenticateToken, getAllUsers);

View File

@ -161,6 +161,76 @@ const geocodeAddress = async (address) => {
}
};
module.exports = {
geocodeAddress
// Nur die Felder, die das Adressformular im Frontend tatsächlich auswertet.
// Alles andere aus der Nominatim-Antwort wird verworfen.
const ADDRESS_PARTS = [
'road', 'house_number', 'postcode',
'city', 'town', 'village', 'municipality', 'hamlet',
'county', 'state'
];
const trimSuggestion = (hit) => {
const address = {};
for (const key of ADDRESS_PARTS) {
if (hit.address && hit.address[key]) address[key] = hit.address[key];
}
return {
lat: hit.lat,
lon: hit.lon,
display_name: hit.display_name,
address
};
};
/**
* Freitext-Adresssuche mit mehreren Treffern für die Autovervollständigung
* im Benutzerformular.
*
* Läuft bewusst über den Server: vorher hat der Browser Nominatim direkt
* angefragt, wodurch weder die Mindestwartezeit noch der vorgeschriebene
* User-Agent griffen und die IP jedes Admins beim Dienst landete.
*
* Teilt sich Wartezeit und Cache mit geocodeAddress; der Cache-Schlüssel ist
* mit einem Präfix versehen, damit die Einträge sich nicht überschneiden.
*/
const searchAddresses = async (query, limit = 5) => {
const normalized = (query || '').trim();
if (normalized.length < 3) {
return [];
}
const count = Math.min(Math.max(parseInt(limit, 10) || 5, 1), 10);
const cacheKey = `search:${count}:${normalized.toLowerCase()}`;
if (cache.has(cacheKey)) {
const hit = cache.get(cacheKey);
cache.delete(cacheKey);
cache.set(cacheKey, hit);
return hit || [];
}
const elapsed = Date.now() - lastRequestTime;
if (elapsed < config.geocodeMinDelayMs) {
await sleep(config.geocodeMinDelayMs - elapsed);
}
const url = `${config.geocodeUrl}?format=json&addressdetails=1&limit=${count}`
+ `&countrycodes=de&q=${encodeURIComponent(normalized)}`;
try {
const results = await fetchJson(url, { 'User-Agent': config.geocodeUserAgent });
lastRequestTime = Date.now();
const list = Array.isArray(results) ? results.map(trimSuggestion) : [];
rememberInCache(cacheKey, list);
return list;
} catch (error) {
logger.warn('Adresssuche fehlgeschlagen', { query: normalized, error: error.message });
lastRequestTime = Date.now();
return [];
}
};
module.exports = {
geocodeAddress,
searchAddresses
};

View File

@ -1,11 +1,13 @@
// Service Worker: nur Offline-Fallback, keine Asset-Caches
// Vite erzeugt content-addressierte Hashes, kein manuelles Caching nötig
const CACHE_NAME = 'nachsuchenfuehrer-offline-v2';
const CACHE_NAME = 'nachsuchenfuehrer-offline-v3';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.add('offline.html'))
caches.open(CACHE_NAME)
.then(cache => cache.add('offline.html'))
.catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err))
);
self.skipWaiting();
});

View File

@ -2,14 +2,20 @@ import React, { useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Circle, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
import markerIcon from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
import './MapView.css';
// Fix für Standard-Marker-Icons in Leaflet
// Standard-Marker-Icons aus dem installierten Leaflet-Paket buendeln.
// Vorher kamen sie von cdnjs — in einer Offline-PWA fuer den Wald waren die
// Marker damit ohne Empfang kaputt, und die IP jedes Besuchers ging an ein
// fremdes CDN. Die Dateien liegen ohnehin in leaflet/dist/images.
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
iconRetinaUrl: markerIcon2x,
iconUrl: markerIcon,
shadowUrl: markerShadow,
});
// Komponente zum Aktualisieren der Kartenansicht

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { useConfigContext } from '../../contexts/ConfigContext';
import { searchAddresses } from '../../services/users';
import './UserForm.css';
const formatSuggestionAddress = (addr) => {
@ -125,17 +126,13 @@ const UserForm = ({ user, onSave, onCancel }) => {
searchTimer.current = setTimeout(async () => {
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&countrycodes=de&addressdetails=1&limit=5&q=${encodeURIComponent(value)}`,
{ headers: { 'User-Agent': 'nachsuche-app/1.0 (admin@kasimirat.de)' } }
);
const data = await res.json();
setSuggestions(Array.isArray(data) ? data : []);
const result = await searchAddresses(value);
setSuggestions(result.data);
setShowSuggestions(true);
} catch {
setSuggestions([]);
}
}, 400);
}, 600);
};
const selectSuggestion = (s) => {
@ -233,7 +230,8 @@ const UserForm = ({ user, onSave, onCancel }) => {
</div>
<div className="form-group">
<label>GPS-Koordinaten</label>
{/* Gruppenueberschrift: gehoert nicht zu einem einzelnen Feld */}
<span className="form-group-heading">GPS-Koordinaten</span>
{gpsAutoSet ? (
<p className="gps-hint gps-hint-success"> GPS automatisch aus Adresse übernommen</p>
) : (

View File

@ -214,6 +214,22 @@ export const uploadUserPhoto = async (id, photoDataUrl) => {
}
};
// Freitext-Adresssuche ueber das eigene Backend statt direkt bei Nominatim:
// dort greifen Mindestwartezeit, Cache und der vorgeschriebene User-Agent,
// und es wandert keine Nutzer-IP zu einem fremden Dienst.
export const searchAddresses = async (query) => {
try {
const response = await api.get('/public/geocode/search', { params: { q: query } });
return { success: true, data: response.data.data || [] };
} catch (error) {
return {
success: false,
data: [],
message: error.response?.data?.message || 'Fehler bei der Adresssuche'
};
}
};
// Einmal-Token, mit dem ein Fuehrer sein erstes Passwort setzt.
// Laeuft ueber die Admin-Session (Cookie), nicht ueber den Fuehrer-Token.
export const generateInviteToken = async (id) => {

View File

@ -1,9 +1,11 @@
// Service Worker: Offline-Fallback für Portal-Selektor
const CACHE_NAME = 'portal-offline-v2';
const CACHE_NAME = 'portal-offline-v3';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.add('offline.html'))
caches.open(CACHE_NAME)
.then(cache => cache.add('offline.html'))
.catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err))
);
self.skipWaiting();
});

View File

@ -1,5 +1,5 @@
const User = require('../models/User');
const { geocodeAddress } = require('../utils/geocode');
const { geocodeAddress, searchAddresses } = require('../utils/geocode');
const logger = require('../utils/logger');
const { escapeCell } = require('../utils/csv');
const config = require('../config/env');
@ -729,6 +729,32 @@ const getGeocodeByPostalCode = async (req, res) => {
}
};
/**
* GET /api/public/geocode/search?q=...
* Freitext-Adresssuche fuer die Autovervollstaendigung im Benutzerformular.
* Laeuft ueber den Server, damit Mindestwartezeit, Cache und der von Nominatim
* geforderte User-Agent greifen und keine Nutzer-IP beim Dienst landet.
*/
const searchAddressSuggestions = async (req, res) => {
const { q } = req.query;
const query = String(q || '').trim();
if (query.length < 3) {
return res.status(400).json({ success: false, message: 'Suchbegriff zu kurz (min. 3 Zeichen)' });
}
if (query.length > 200) {
return res.status(400).json({ success: false, message: 'Suchbegriff zu lang' });
}
try {
const results = await searchAddresses(query, req.query.limit);
res.json({ success: true, data: results });
} catch (error) {
logger.error('Fehler bei der Adresssuche:', error);
res.status(500).json({ success: false, message: 'Fehler bei der Adresssuche' });
}
};
module.exports = {
getAllUsers,
getUserById,
@ -746,5 +772,6 @@ module.exports = {
bulkDeleteUsers,
uploadUserPhoto,
deleteUserPhoto,
getGeocodeByPostalCode
getGeocodeByPostalCode,
searchAddressSuggestions
};

View File

@ -21,13 +21,15 @@ const {
bulkDeleteUsers,
uploadUserPhoto,
deleteUserPhoto,
getGeocodeByPostalCode
getGeocodeByPostalCode,
searchAddressSuggestions
} = require('../controllers/userController');
// Public routes
router.get('/public/users', getPublicUsers);
// Eigenes, engeres Limit: der Endpunkt loest ausgehende Nominatim-Anfragen aus.
router.get('/public/geocode', geocodeLimiter, getGeocodeByPostalCode);
router.get('/public/geocode/search', geocodeLimiter, searchAddressSuggestions);
// Protected routes (require authentication)
router.get('/users', authenticateToken, getAllUsers);

View File

@ -1,11 +1,13 @@
// Service Worker: nur Offline-Fallback, keine Asset-Caches
// Vite erzeugt content-addressierte Hashes, kein manuelles Caching nötig
const CACHE_NAME = 'stoeberhunde-offline-v1';
const CACHE_NAME = 'stoeberhunde-offline-v2';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.add('offline.html'))
caches.open(CACHE_NAME)
.then(cache => cache.add('offline.html'))
.catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err))
);
self.skipWaiting();
});

View File

@ -2,14 +2,20 @@ import React, { useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Circle, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
import markerIcon from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
import './MapView.css';
// Fix für Standard-Marker-Icons in Leaflet
// Standard-Marker-Icons aus dem installierten Leaflet-Paket buendeln.
// Vorher kamen sie von cdnjs — in einer Offline-PWA fuer den Wald waren die
// Marker damit ohne Empfang kaputt, und die IP jedes Besuchers ging an ein
// fremdes CDN. Die Dateien liegen ohnehin in leaflet/dist/images.
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
iconRetinaUrl: markerIcon2x,
iconUrl: markerIcon,
shadowUrl: markerShadow,
});
// Komponente zum Aktualisieren der Kartenansicht

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { useConfigContext } from '../../contexts/ConfigContext';
import { searchAddresses } from '../../services/users';
import './UserForm.css';
const formatSuggestionAddress = (addr) => {
@ -125,17 +126,13 @@ const UserForm = ({ user, onSave, onCancel }) => {
searchTimer.current = setTimeout(async () => {
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&countrycodes=de&addressdetails=1&limit=5&q=${encodeURIComponent(value)}`,
{ headers: { 'User-Agent': 'stoeberhunde-app/1.0 (admin@kasimirat.de)' } }
);
const data = await res.json();
setSuggestions(Array.isArray(data) ? data : []);
const result = await searchAddresses(value);
setSuggestions(result.data);
setShowSuggestions(true);
} catch {
setSuggestions([]);
}
}, 400);
}, 600);
};
const selectSuggestion = (s) => {
@ -233,7 +230,8 @@ const UserForm = ({ user, onSave, onCancel }) => {
</div>
<div className="form-group">
<label>GPS-Koordinaten</label>
{/* Gruppenueberschrift: gehoert nicht zu einem einzelnen Feld */}
<span className="form-group-heading">GPS-Koordinaten</span>
{gpsAutoSet ? (
<p className="gps-hint gps-hint-success"> GPS automatisch aus Adresse übernommen</p>
) : (

View File

@ -214,6 +214,22 @@ export const uploadUserPhoto = async (id, photoDataUrl) => {
}
};
// Freitext-Adresssuche ueber das eigene Backend statt direkt bei Nominatim:
// dort greifen Mindestwartezeit, Cache und der vorgeschriebene User-Agent,
// und es wandert keine Nutzer-IP zu einem fremden Dienst.
export const searchAddresses = async (query) => {
try {
const response = await api.get('/public/geocode/search', { params: { q: query } });
return { success: true, data: response.data.data || [] };
} catch (error) {
return {
success: false,
data: [],
message: error.response?.data?.message || 'Fehler bei der Adresssuche'
};
}
};
// Einmal-Token, mit dem ein Fuehrer sein erstes Passwort setzt.
// Laeuft ueber die Admin-Session (Cookie), nicht ueber den Fuehrer-Token.
export const generateInviteToken = async (id) => {