Fix: Magic-Link URL, Sidebar-Ladereihenfolge, Archiv-Standard + Geoparse-Button
- Magic-Link URL korrigiert: /auth/verify -> / (Login-Seite mit Token-Param)
- Sidebar: loadIncidents() vor NotificationCenter.init() verschoben
- NotificationCenter.init() in try/catch gewrappt
- Archiv-Sektion default geschlossen (display:none im HTML)
- Neuer Endpunkt POST /incidents/{id}/geoparse fuer bestehende Artikel
- "Orte erkennen"-Button in Karten-Kachel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -95,7 +95,7 @@ async def request_magic_link(
|
||||
await db.commit()
|
||||
|
||||
# E-Mail senden
|
||||
link = f"{MAGIC_LINK_BASE_URL}/auth/verify?token={token}"
|
||||
link = f"{MAGIC_LINK_BASE_URL}/?token={token}"
|
||||
subject, html = magic_link_login_email(user["username"], code, link)
|
||||
await send_email(email, subject, html)
|
||||
|
||||
|
||||
@@ -320,6 +320,50 @@ async def get_locations(
|
||||
return list(loc_map.values())
|
||||
|
||||
|
||||
@router.post("/{incident_id}/geoparse")
|
||||
async def trigger_geoparse(
|
||||
incident_id: int,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Geoparsing fuer alle Artikel einer Lage nachholen (bestehende Orte werden uebersprungen)."""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||
|
||||
# Artikel laden, die noch keine Locations haben
|
||||
cursor = await db.execute(
|
||||
"""SELECT a.* FROM articles a
|
||||
WHERE a.incident_id = ?
|
||||
AND a.id NOT IN (SELECT DISTINCT article_id FROM article_locations WHERE incident_id = ?)""",
|
||||
(incident_id, incident_id),
|
||||
)
|
||||
articles = [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
if not articles:
|
||||
return {"message": "Alle Artikel wurden bereits geoparsed", "new_locations": 0}
|
||||
|
||||
try:
|
||||
from agents.geoparsing import geoparse_articles
|
||||
geo_results = await geoparse_articles(articles)
|
||||
geo_count = 0
|
||||
for art_id, locations in geo_results.items():
|
||||
for loc in locations:
|
||||
await db.execute(
|
||||
"""INSERT INTO article_locations
|
||||
(article_id, incident_id, location_name, location_name_normalized,
|
||||
country_code, latitude, longitude, confidence, source_text, tenant_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(art_id, incident_id, loc["location_name"], loc["location_name_normalized"],
|
||||
loc["country_code"], loc["lat"], loc["lon"], loc["confidence"],
|
||||
loc.get("source_text", ""), tenant_id),
|
||||
)
|
||||
geo_count += 1
|
||||
await db.commit()
|
||||
return {"message": f"{geo_count} Orte aus {len(geo_results)} Artikeln extrahiert", "new_locations": geo_count}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Geoparsing fehlgeschlagen: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{incident_id}/refresh-log")
|
||||
async def get_refresh_log(
|
||||
incident_id: int,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=20260304c">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=20260304d">
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
|
||||
@@ -76,7 +76,7 @@
|
||||
Archiv
|
||||
<span class="sidebar-section-count" id="count-archived-incidents"></span>
|
||||
</h2>
|
||||
<div id="archived-incidents" aria-live="polite"></div>
|
||||
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="sidebar-sources-link">
|
||||
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()">Quellen verwalten</button>
|
||||
@@ -266,6 +266,7 @@
|
||||
<div class="card-header">
|
||||
<div class="card-title">Geografische Verteilung</div>
|
||||
<span class="map-stats" id="map-stats"></span>
|
||||
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln erkennen">Orte erkennen</button>
|
||||
</div>
|
||||
<div class="map-container" id="map-container">
|
||||
<div class="map-empty" id="map-empty">Keine Orte erkannt</div>
|
||||
@@ -559,10 +560,10 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack-all.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
||||
<script src="/static/js/api.js?v=20260304c"></script>
|
||||
<script src="/static/js/ws.js?v=20260304c"></script>
|
||||
<script src="/static/js/components.js?v=20260304c"></script>
|
||||
<script src="/static/js/layout.js?v=20260304c"></script>
|
||||
<script src="/static/js/app.js?v=20260304c"></script>
|
||||
<script src="/static/js/api.js?v=20260304d"></script>
|
||||
<script src="/static/js/ws.js?v=20260304d"></script>
|
||||
<script src="/static/js/components.js?v=20260304d"></script>
|
||||
<script src="/static/js/layout.js?v=20260304d"></script>
|
||||
<script src="/static/js/app.js?v=20260304d"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -106,6 +106,10 @@ const API = {
|
||||
return this._request('GET', `/incidents/${incidentId}/locations`);
|
||||
},
|
||||
|
||||
triggerGeoparse(incidentId) {
|
||||
return this._request('POST', `/incidents/${incidentId}/geoparse`);
|
||||
},
|
||||
|
||||
refreshIncident(id) {
|
||||
return this._request('POST', `/incidents/${id}/refresh`);
|
||||
},
|
||||
|
||||
@@ -461,8 +461,15 @@ const App = {
|
||||
document.getElementById('fb-char-count').textContent = e.target.value.length.toLocaleString('de-DE');
|
||||
});
|
||||
|
||||
// Sidebar-Chevrons initial auf offen setzen (Archiv geschlossen)
|
||||
document.querySelectorAll('.sidebar-chevron').forEach(c => c.classList.add('open'));
|
||||
document.getElementById('chevron-archived-incidents').classList.remove('open');
|
||||
|
||||
// Lagen laden (frueh, damit Sidebar sofort sichtbar)
|
||||
await this.loadIncidents();
|
||||
|
||||
// Notification-Center initialisieren
|
||||
await NotificationCenter.init();
|
||||
try { await NotificationCenter.init(); } catch (e) { console.warn('NotificationCenter:', e); }
|
||||
|
||||
// WebSocket
|
||||
WS.connect();
|
||||
@@ -472,14 +479,6 @@ const App = {
|
||||
WS.on('refresh_error', (msg) => this.handleRefreshError(msg));
|
||||
WS.on('refresh_cancelled', (msg) => this.handleRefreshCancelled(msg));
|
||||
|
||||
// Sidebar-Chevrons initial auf offen setzen (Archiv geschlossen)
|
||||
document.querySelectorAll('.sidebar-chevron').forEach(c => c.classList.add('open'));
|
||||
document.getElementById('archived-incidents').style.display = 'none';
|
||||
document.getElementById('chevron-archived-incidents').classList.remove('open');
|
||||
|
||||
// Lagen laden
|
||||
await this.loadIncidents();
|
||||
|
||||
// Laufende Refreshes wiederherstellen
|
||||
try {
|
||||
const data = await API.getRefreshingIncidents();
|
||||
@@ -1430,6 +1429,24 @@ const App = {
|
||||
}
|
||||
},
|
||||
|
||||
async triggerGeoparse() {
|
||||
if (!this.currentIncidentId) return;
|
||||
const btn = document.getElementById('geoparse-btn');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Erkennung...'; }
|
||||
try {
|
||||
const result = await API.triggerGeoparse(this.currentIncidentId);
|
||||
UI.showToast(result.message, result.new_locations > 0 ? 'success' : 'info');
|
||||
if (result.new_locations > 0) {
|
||||
const locations = await API.getLocations(this.currentIncidentId).catch(() => []);
|
||||
UI.renderMap(locations);
|
||||
}
|
||||
} catch (err) {
|
||||
UI.showToast('Geoparsing fehlgeschlagen: ' + err.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Orte erkennen'; }
|
||||
}
|
||||
},
|
||||
|
||||
_formatInterval(minutes) {
|
||||
if (minutes >= 10080 && minutes % 10080 === 0) {
|
||||
const w = minutes / 10080;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren