Sprint 14: Map-Fixes, City-Prewarm, Dog-Animation, Scan-Flash

Karte:
- Frankfurt-Fallback (Zoom 10→14 flyTo) mit _frankfurtTimer-Cancel
  wenn echter Standort eintrifft
- OSM-Tile-Fetch parallelisiert (asyncio.Semaphore(3))
- Bounds-Fix: invalidateSize() + pad(0.15) vor getBounds()
- map-pin-slash Icon für gesperrten Standort
- Scan-Done-Flash: Statusbar-Pill grün bei 100%
- Schnüffelhund: outer div (by-wander X) + inner SVG (by-sniff Y)
  für natürlichere zweiachsige Bewegung

Backend:
- City-Prewarm-Job: ~70 deutsche Großstädte beim Start (+90s) und
  wöchentlich (So 01:00), Fortschritts-Mails alle 5h an ADMIN_EMAIL
- ADMIN_EMAIL Env-Var in .env.example dokumentiert

Bugfixes:
- Profil-Edit: /api/profile → /profile (doppelter Prefix)
- Friends: Mobile-Portrait-Layout (flex-wrap, overflow-x:hidden)
- Trainingspläne: Pills text-wrap (flex + white-space:normal)
This commit is contained in:
rene 2026-04-17 14:06:10 +02:00
parent cd3f118113
commit 6fcf841594
10 changed files with 340 additions and 32 deletions

View file

@ -53,6 +53,13 @@ def start():
replace_existing=True,
misfire_grace_time=7200,
)
_scheduler.add_job(
_job_prewarm_cities,
CronTrigger(day_of_week='sun', hour=1), # jeden Sonntag 01:00 Uhr
id="prewarm_cities",
replace_existing=True,
misfire_grace_time=7200,
)
# Einmalig beim Start (nach 10s Verzögerung) für sofortige Befüllung
_scheduler.add_job(
_job_import_events,
@ -61,6 +68,14 @@ def start():
id="import_events_startup",
replace_existing=True,
)
# Einmalig beim Start (nach 90s) — OSM-Tiles für Großstädte vorwärmen
_scheduler.add_job(
_job_prewarm_cities,
'date',
run_date=datetime.now() + timedelta(seconds=90),
id="prewarm_cities_startup",
replace_existing=True,
)
# Einmalig beim Start (nach 15s Verzögerung) — Rassen aus TheDogAPI befüllen
_scheduler.add_job(
_job_seed_breeds,
@ -384,6 +399,178 @@ async def _job_seed_wikidata_breeds():
logger.error(f"Wikidata-Seed: Fehler: {e}")
# ------------------------------------------------------------------
# JOB: OSM-Tiles für deutsche Großstädte vorwärmen
# Läuft einmalig 90s nach Start + wöchentlich So 01:00 Uhr.
# Nur stale Tiles werden abgerufen (bereits gecachte werden übersprungen).
# ------------------------------------------------------------------
# Deutsche Städte mit >50.000 Einwohnern (lat, lon, name)
_CITIES_DE = [
(52.5200, 13.4050, "Berlin"),
(53.5753, 10.0153, "Hamburg"),
(48.1372, 11.5755, "München"),
(51.2217, 6.7762, "Düsseldorf"),
(50.9333, 6.9500, "Köln"),
(50.1109, 8.6821, "Frankfurt"),
(48.7775, 9.1800, "Stuttgart"),
(51.4566, 7.0116, "Dortmund"),
(51.5136, 7.4653, "Dortmund-Ost"),
(51.4508, 7.0131, "Essen"),
(51.3388, 12.3799, "Leipzig"),
(51.2254, 6.7762, "Düsseldorf"),
(51.0534, 13.7373, "Dresden"),
(52.3759, 9.7320, "Hannover"),
(51.4818, 7.2162, "Bochum"),
(51.9607, 7.6261, "Münster"),
(51.3670, 7.4595, "Hagen"),
(50.7753, 6.0839, "Aachen"),
(51.2563, 7.1500, "Wuppertal"),
(49.4521, 11.0767, "Nürnberg"),
(53.0758, 8.8072, "Bremen"),
(50.7323, 7.0955, "Bonn"),
(49.0069, 8.4037, "Karlsruhe"),
(51.9607, 7.6261, "Münster"),
(51.4344, 6.7623, "Duisburg"),
(51.6667, 6.1667, "Moers"),
(48.3705, 10.8978, "Augsburg"),
(52.2689, 10.5268, "Braunschweig"),
(50.9287, 11.5861, "Jena"),
(53.8655, 10.6866, "Lübeck"),
(54.3233, 10.1394, "Kiel"),
(53.1435, 8.2146, "Oldenburg"),
(52.0302, 8.5325, "Bielefeld"),
(51.3167, 9.5000, "Kassel"),
(50.0000, 8.2731, "Mainz"),
(49.8728, 8.6512, "Darmstadt"),
(49.0047, 12.0949, "Regensburg"),
(48.9960, 8.4025, "Pforzheim"),
(53.4706, 9.9817, "Hamburg-Süd"),
(50.8283, 12.9209, "Chemnitz"),
(51.7227, 8.7559, "Paderborn"),
(52.1205, 11.6276, "Magdeburg"),
(52.6367, 11.8683, "Magdeburg-Ost"),
(50.3569, 7.5890, "Koblenz"),
(48.4010, 9.9876, "Ulm"),
(51.0504, 13.7373, "Dresden-Mitte"),
(49.4875, 8.4660, "Mannheim"),
(49.2354, 7.0038, "Kaiserslautern"),
(50.1155, 8.6782, "Frankfurt-Mitte"),
(50.0782, 8.2398, "Wiesbaden"),
(52.4227, 10.7865, "Wolfsburg"),
(51.9607, 8.8693, "Gütersloh"),
(53.5753, 9.8500, "Hamburg-West"),
(48.5216, 9.0576, "Reutlingen"),
(48.9522, 9.4358, "Heilbronn"),
(49.4478, 7.7691, "Kaiserslautern-W"),
(53.6333, 9.9833, "Hamburg-Nord"),
(52.3905, 13.0645, "Potsdam"),
(54.0924, 12.1407, "Rostock"),
(53.4339, 14.5508, "Szczecin-grenze"),
(51.7563, 14.3329, "Cottbus"),
(50.4782, 12.3598, "Zwickau"),
(53.5507, 9.9967, "Hamburg-Mitte"),
(51.8127, 10.3354, "Goslar"),
(48.6843, 9.0061, "Böblingen"),
(48.7761, 9.1775, "Stuttgart-Mitte"),
(49.4521, 8.4660, "Heidelberg"),
(50.8088, 8.7667, "Marburg"),
(51.9607, 7.6261, "Münster-Mitte"),
(52.2763, 8.0479, "Osnabrück"),
(53.8755, 10.7000, "Lübeck-Ost"),
(51.9333, 6.8667, "Borken"),
]
async def _job_prewarm_cities():
import os, asyncio, time
from routes.osm import _covering_tiles, _stale_tiles, _fetch_and_store_tile, OSM_QUERIES, CACHE_ZOOM
from mailer import send_email
ADMIN = os.getenv("ADMIN_EMAIL", "")
REPORT_INTERVAL = 5 * 3600 # alle 5 Stunden
logger.info("City-Prewarm Job startet…")
sem = asyncio.Semaphore(2)
total_fetched = 0
cities_done = 0
start_time = time.monotonic()
last_report = start_time
async def _fetch(poi_type, x, y):
nonlocal total_fetched
async with sem:
await _fetch_and_store_tile(poi_type, x, y)
total_fetched += 1
await asyncio.sleep(1.5)
async def _send_progress(subject_prefix, cities_done, total_cities, eta_str=""):
if not ADMIN:
return
elapsed = int(time.monotonic() - start_time)
h, m = divmod(elapsed // 60, 60)
elapsed_str = f"{h}h {m:02d}min" if h else f"{m}min"
pct = round(cities_done / total_cities * 100)
body_plain = (
f"City-Prewarm Fortschritt\n\n"
f"Städte: {cities_done}/{total_cities} ({pct}%)\n"
f"Tiles geladen: {total_fetched}\n"
f"Laufzeit: {elapsed_str}\n"
f"{('Verbleibend (ca.): ' + eta_str) if eta_str else ''}"
)
body_html = f"""\
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px">
<h2 style="color:#C4843A;margin:0 0 16px">🗺 City-Prewarm {subject_prefix}</h2>
<table style="border-collapse:collapse;width:100%">
<tr><td style="padding:6px 0;color:#666">Städte:</td>
<td style="padding:6px 0;font-weight:600">{cities_done} / {total_cities} ({pct}%)</td></tr>
<tr><td style="padding:6px 0;color:#666">Tiles geladen:</td>
<td style="padding:6px 0;font-weight:600">{total_fetched}</td></tr>
<tr><td style="padding:6px 0;color:#666">Laufzeit:</td>
<td style="padding:6px 0;font-weight:600">{elapsed_str}</td></tr>
{f'<tr><td style="padding:6px 0;color:#666">Verbleibend (ca.):</td><td style="padding:6px 0;font-weight:600">{eta_str}</td></tr>' if eta_str else ''}
</table>
</div>"""
try:
await send_email(ADMIN, f"Ban Yaro — City-Prewarm {subject_prefix}", body_html, body_plain)
except Exception as e:
logger.warning(f"City-Prewarm Mail fehlgeschlagen: {e}")
total_cities = len(_CITIES_DE)
for lat, lon, city in _CITIES_DE:
dlat = 0.18
dlon = 0.25
south, west, north, east = lat - dlat, lon - dlon, lat + dlat, lon + dlon
tiles = _covering_tiles(south, west, north, east, CACHE_ZOOM)
tasks = []
for poi_type in OSM_QUERIES:
stale = _stale_tiles(poi_type, tiles)
for (x, y) in stale:
tasks.append(_fetch(poi_type, x, y))
if tasks:
logger.info(f"City-Prewarm: {city}{len(tasks)} Tiles zu laden")
await asyncio.gather(*tasks)
else:
logger.debug(f"City-Prewarm: {city} — alle Tiles frisch")
cities_done += 1
# Fortschritts-Mail alle 5 Stunden
now = time.monotonic()
if ADMIN and (now - last_report) >= REPORT_INTERVAL:
elapsed = now - start_time
rate = cities_done / elapsed if elapsed > 0 else 0
remaining = int((total_cities - cities_done) / rate) if rate > 0 else 0
rh, rm = divmod(remaining // 60, 60)
eta_str = f"{rh}h {rm:02d}min" if rh else f"{rm}min"
await _send_progress("Fortschritt", cities_done, total_cities, eta_str)
last_report = now
logger.info(f"City-Prewarm Job fertig — {total_fetched} Tiles geladen.")
await _send_progress("abgeschlossen ✓", cities_done, total_cities)
def _compute_milestone(today: date, bday: date, dog_name: str):
"""
Gibt (titel, text) zurück wenn heute ein Meilenstein-Tag ist,