Compare commits

..

No commits in common. "9d8a73e2a96ad940f8c107513fe314d6b560577a" and "aa9e5699f045ef7a10e7adc52246640dfb5533a5" have entirely different histories.

103 changed files with 64 additions and 2214 deletions

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""Pydantic response models for the API.""" """Pydantic response models for the API."""
from datetime import date, datetime from datetime import date, datetime
@ -57,13 +56,6 @@ class KiZusammenfassung(BaseModel):
partei: str | list[str] | None = None partei: str | list[str] | None = None
class UmsetzungsBewertung(BaseModel):
score: float | None = None
begruendung: str | None = None
quelle_vorlage_id: int | None = None
quelle_aktenzeichen: str | None = None
class VorlageDetail(BaseModel): class VorlageDetail(BaseModel):
id: int id: int
aktenzeichen: str | None = None aktenzeichen: str | None = None
@ -83,7 +75,6 @@ class VorlageDetail(BaseModel):
referenzen_ausgehend: list[ReferenzOut] = [] referenzen_ausgehend: list[ReferenzOut] = []
referenzen_eingehend: list[ReferenzOut] = [] referenzen_eingehend: list[ReferenzOut] = []
kette_id: int | None = None kette_id: int | None = None
umsetzungsbewertungen: list[UmsetzungsBewertung] = []
class KettenGliedOut(BaseModel): class KettenGliedOut(BaseModel):
@ -113,7 +104,6 @@ class KetteDetail(BaseModel):
status_seit: date | None = None status_seit: date | None = None
letzte_aktivitaet: date | None = None letzte_aktivitaet: date | None = None
vertagungen_count: int = 0 vertagungen_count: int = 0
begruendung: str | None = None
glieder: list[KettenGliedOut] = [] glieder: list[KettenGliedOut] = []
antragsteller: list[ParteiOut] = [] antragsteller: list[ParteiOut] = []
graph: dict | None = None graph: dict | None = None

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""API routes for Abstimmungen und Stimmverhalten-Analysen.""" """API routes for Abstimmungen und Stimmverhalten-Analysen."""
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query

View File

@ -1,312 +0,0 @@
from __future__ import annotations
"""API routes for KI re-evaluation."""
import json
import os
import sqlite3
import threading
from datetime import datetime
import httpx
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from tracker.db.session import get_connection
# Note: Re-evaluation should re-scrape ALLRIS data before KI evaluation
# to exclude transfer errors. See Gitea issue for full spec.
# IMPORTANT: Destructive changes (deleting old bewertungen etc.) only after
# explicit user request with explanation. Keep old data as backup where possible.
router = APIRouter(prefix="/bewertung", tags=["bewertung"])
def _db():
conn = get_connection()
try:
yield conn
finally:
conn.close()
# API config
DASHSCOPE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions"
GEMINI_URL_TEMPLATE = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={key}"
# Job tracking
_jobs: dict[str, dict] = {}
def _get_key(name: str) -> str:
"""Get API key from env or macOS keychain."""
val = os.environ.get(name)
if val:
return val
import subprocess
keychain_map = {"QWEN_API_KEY": "qwen-api", "GEMINI_API_KEY": "gemini-api"}
svc = keychain_map.get(name)
if svc:
try:
return subprocess.check_output(
["security", "find-generic-password", "-s", svc, "-w"],
stderr=subprocess.DEVNULL,
).decode().strip()
except Exception:
pass
return ""
ZUSAMMENFASSUNG_PROMPT = """Analysiere diesen kommunalpolitischen Antrag aus Hagen.
ZUSÄTZLICHE HINWEISE DES NUTZERS:
{anmerkung}
DOKUMENT:
{volltext}
---
Erstelle eine strukturierte Zusammenfassung im JSON-Format:
{{
"zusammenfassung": "2-3 Sätze, was gefordert wird",
"kernforderung": "Die zentrale Forderung in einem Satz",
"begruendung": "Warum wird das gefordert? (kurz)",
"thema": "Hauptthema (z.B. Verkehr, Soziales, Umwelt)",
"partei": "Antragstellende Fraktion falls erkennbar",
"orte": []
}}
NUR JSON ausgeben, keine Erklärungen."""
KETTEN_MATCH_PROMPT = """Du bist ein Analyst für kommunalpolitische Vorgänge in Hagen.
ZUSÄTZLICHE HINWEISE DES NUTZERS:
{anmerkung}
Vergleiche den URSPRÜNGLICHEN ANTRAG/ANFRAGE mit der ANTWORT/BESCHLUSS.
Bewerte ob die ursprüngliche Forderung tatsächlich erfüllt wurde.
=== URSPRÜNGLICHE FORDERUNG ===
Aktenzeichen: {az_ursprung}
{ki_zusammenfassung}
Volltext:
{volltext_ursprung}
=== BERATUNGEN & BESCHLÜSSE ===
{beratungen_text}
---
Bewerte NUR als JSON:
{{
"score": <0.0-1.0>,
"bewertung": "erfuellt|teilweise|abgewiegelt|nebelkerze|vertagt|unklar",
"begruendung": "1-2 Sätze warum",
"kernpunkt_erfuellt": true/false,
"details": "Was konkret beschlossen/abgelehnt wurde"
}}
Bewertungsskala:
- 1.0: Forderung vollständig erfüllt, konkreter Beschluss
- 0.7-0.9: Weitgehend erfüllt, kleine Abweichungen
- 0.4-0.6: Teilweise erfüllt oder auf den Weg gebracht
- 0.2-0.3: Abgewiegelt Verwaltung weicht aus, kündigt nur "Prüfung" an
- 0.0-0.1: Nebelkerze Thema gewechselt oder komplett ignoriert"""
class BewertungRequest(BaseModel):
anmerkung: str = ""
def _call_qwen(prompt: str) -> dict | None:
key = _get_key("QWEN_API_KEY")
if not key:
return None
try:
resp = httpx.post(
DASHSCOPE_URL,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
json={"model": "qwen-plus-latest", "messages": [{"role": "user", "content": prompt}], "temperature": 0.3},
timeout=180,
)
resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"]
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
return json.loads(content.strip())
except Exception as e:
return {"error": str(e)}
def _run_zusammenfassung(vorlage_id: int, anmerkung: str, job_id: str):
"""Background task: re-evaluate a Vorlage."""
try:
conn = get_connection()
row = conn.execute("SELECT volltext_clean, aktenzeichen FROM vorlagen WHERE id = ?", (vorlage_id,)).fetchone()
if not row or not row["volltext_clean"]:
_jobs[job_id] = {"status": "error", "error": "Kein Volltext vorhanden"}
return
volltext = row["volltext_clean"]
if len(volltext) > 12000:
volltext = volltext[:12000] + "\n[...gekürzt...]"
prompt = ZUSAMMENFASSUNG_PROMPT.format(anmerkung=anmerkung or "(keine)", volltext=volltext)
result = _call_qwen(prompt)
if not result or "error" in result:
_jobs[job_id] = {"status": "error", "error": str(result)}
return
# Delete old, insert new
conn.execute("DELETE FROM ki_bewertungen WHERE vorlage_id = ? AND typ = 'zusammenfassung'", (vorlage_id,))
conn.execute(
"""INSERT INTO ki_bewertungen (vorlage_id, typ, begruendung, anmerkungen, modell, prompt_version)
VALUES (?, 'zusammenfassung', ?, ?, 'qwen-plus-latest', 'v2-reeval')""",
(vorlage_id, result.get("zusammenfassung"), json.dumps(result, ensure_ascii=False)),
)
if result.get("kernforderung"):
conn.execute("UPDATE vorlagen SET thema_kurz = ? WHERE id = ?", (result["kernforderung"][:200], vorlage_id))
conn.commit()
conn.close()
_jobs[job_id] = {"status": "done", "result": result}
except Exception as e:
_jobs[job_id] = {"status": "error", "error": str(e)}
def _run_ketten_bewertung(kette_id: int, anmerkung: str, job_id: str):
"""Background task: re-evaluate a Kette."""
try:
conn = get_connection()
# Get kette + ursprung
kette = conn.execute(
"SELECT k.*, v.aktenzeichen, v.volltext_clean FROM ketten k JOIN vorlagen v ON k.ursprung_id = v.id WHERE k.id = ?",
(kette_id,),
).fetchone()
if not kette:
_jobs[job_id] = {"status": "error", "error": "Kette nicht gefunden"}
return
# Get KI summary of ursprung
ki_row = conn.execute(
"SELECT anmerkungen FROM ki_bewertungen WHERE vorlage_id = ? AND typ = 'zusammenfassung' LIMIT 1",
(kette["ursprung_id"],),
).fetchone()
ki_text = ""
if ki_row and ki_row["anmerkungen"]:
try:
ki = json.loads(ki_row["anmerkungen"])
ki_text = f"Zusammenfassung: {ki.get('zusammenfassung', '')}\nKernforderung: {ki.get('kernforderung', '')}"
except Exception:
pass
# Collect beratungen
members = conn.execute(
"SELECT vorlage_id FROM ketten_glieder WHERE kette_id = ?", (kette_id,)
).fetchall()
member_ids = [m["vorlage_id"] for m in members]
placeholders = ",".join("?" * len(member_ids))
beratungen = conn.execute(
f"""SELECT b.sitzung_datum, b.rolle, b.ergebnis, b.beschlusstext, b.wortprotokoll,
v.aktenzeichen
FROM beratungen b JOIN vorlagen v ON b.vorlage_id = v.id
WHERE b.vorlage_id IN ({placeholders})
ORDER BY b.sitzung_datum""",
member_ids,
).fetchall()
beratungen_text = ""
for b in beratungen:
beratungen_text += f"\n--- {b['aktenzeichen']} | {b['sitzung_datum']} | {b['rolle']} ---\n"
if b["ergebnis"]:
beratungen_text += f"Ergebnis: {b['ergebnis']}\n"
if b["beschlusstext"]:
beratungen_text += f"Beschlusstext: {b['beschlusstext'][:500]}\n"
if b["wortprotokoll"]:
beratungen_text += f"Wortprotokoll: {b['wortprotokoll'][:1000]}\n"
volltext = kette["volltext_clean"] or ""
if len(volltext) > 8000:
volltext = volltext[:8000] + "\n[...gekürzt...]"
prompt = KETTEN_MATCH_PROMPT.format(
anmerkung=anmerkung or "(keine)",
az_ursprung=kette["aktenzeichen"],
ki_zusammenfassung=ki_text,
volltext_ursprung=volltext,
beratungen_text=beratungen_text or "(keine Beratungen vorhanden)",
)
result = _call_qwen(prompt)
if not result or "error" in result:
_jobs[job_id] = {"status": "error", "error": str(result)}
return
# Delete old umsetzung_match, insert new
conn.execute(
"DELETE FROM ki_bewertungen WHERE vorlage_id = ? AND typ = 'umsetzung_match'",
(kette["ursprung_id"],),
)
conn.execute(
"""INSERT INTO ki_bewertungen (vorlage_id, typ, score, begruendung, anmerkungen, modell, prompt_version)
VALUES (?, 'umsetzung_match', ?, ?, ?, 'qwen-plus-latest', 'v2-reeval')""",
(
kette["ursprung_id"],
result.get("score"),
result.get("begruendung"),
json.dumps(result, ensure_ascii=False),
),
)
# Rebuild chain status
from tracker.core.chains import build_single_chain
build_single_chain(conn, kette["ursprung_id"])
conn.commit()
conn.close()
_jobs[job_id] = {"status": "done", "result": result}
except Exception as e:
_jobs[job_id] = {"status": "error", "error": str(e)}
@router.post("/vorlagen/{vorlage_id}")
def reeval_vorlage(vorlage_id: int, req: BewertungRequest, conn=Depends(_db)):
"""Trigger KI re-evaluation for a Vorlage."""
row = conn.execute("SELECT id FROM vorlagen WHERE id = ?", (vorlage_id,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Vorlage nicht gefunden")
job_id = f"vorlage-{vorlage_id}-{int(datetime.now().timestamp())}"
_jobs[job_id] = {"status": "running"}
t = threading.Thread(target=_run_zusammenfassung, args=(vorlage_id, req.anmerkung, job_id), daemon=True)
t.start()
return {"job_id": job_id, "status": "running"}
@router.post("/ketten/{kette_id}")
def reeval_kette(kette_id: int, req: BewertungRequest, conn=Depends(_db)):
"""Trigger KI re-evaluation for a Kette."""
row = conn.execute("SELECT id FROM ketten WHERE id = ?", (kette_id,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Kette nicht gefunden")
job_id = f"kette-{kette_id}-{int(datetime.now().timestamp())}"
_jobs[job_id] = {"status": "running"}
t = threading.Thread(target=_run_ketten_bewertung, args=(kette_id, req.anmerkung, job_id), daemon=True)
t.start()
return {"job_id": job_id, "status": "running"}
@router.get("/status/{job_id}")
def get_job_status(job_id: str):
"""Check status of a re-evaluation job."""
if job_id not in _jobs:
raise HTTPException(status_code=404, detail="Job nicht gefunden")
return _jobs[job_id]

View File

@ -1,128 +0,0 @@
from __future__ import annotations
"""API routes for Fraktions-Dashboard."""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from tracker.core.kategorien import BEWERTUNGSKATEGORIEN
from tracker.db.session import get_connection
router = APIRouter(prefix="/fraktionen", tags=["Fraktionen"])
def _db():
conn = get_connection()
try:
yield conn
finally:
conn.close()
@router.get("/kategorien")
def get_kategorien():
"""Return category definitions for frontend tooltips."""
return {
key: {
"label": val["label"],
"farbe": val["farbe"],
"icon": val["icon"],
"beschreibung": val["beschreibung"],
"beispiel": val["beispiel"],
"score_range": val["score_range"],
}
for key, val in BEWERTUNGSKATEGORIEN.items()
}
@router.get("")
def list_fraktionen(conn=Depends(_db)):
"""List all parties with Antrag counts."""
rows = conn.execute("""
SELECT p.id, p.kuerzel, p.name, p.farbe, COUNT(a.vorlage_id) as anzahl
FROM parteien p
LEFT JOIN antragsteller a ON p.id = a.partei_id
GROUP BY p.id
HAVING anzahl > 0
ORDER BY anzahl DESC
""").fetchall()
return [dict(r) for r in rows]
@router.get("/{kuerzel}/dashboard")
def fraktion_dashboard(
kuerzel: str,
jahr: Optional[int] = None,
conn=Depends(_db),
):
"""Dashboard for a single Fraktion with Umsetzungsanalyse."""
# Find party
partei = conn.execute(
"SELECT id, kuerzel, name, farbe FROM parteien WHERE kuerzel = ?", (kuerzel,)
).fetchone()
if not partei:
raise HTTPException(404, f"Fraktion '{kuerzel}' nicht gefunden")
# Base query: all Vorlagen by this party
jahr_filter = ""
params = [partei["id"]]
if jahr:
jahr_filter = "AND strftime('%Y', v.datum_eingang) = ?"
params.append(str(jahr))
# Total Anträge
total = conn.execute(f"""
SELECT COUNT(DISTINCT v.id) as c
FROM vorlagen v
JOIN antragsteller a ON a.vorlage_id = v.id
WHERE a.partei_id = ? {jahr_filter}
""", params).fetchone()["c"]
# With Ketten-Match results
umsetzung_rows = conn.execute(f"""
SELECT
json_extract(kb.anmerkungen, '$.bewertung') as bewertung,
COUNT(*) as anzahl,
ROUND(AVG(kb.score), 2) as avg_score
FROM vorlagen v
JOIN antragsteller a ON a.vorlage_id = v.id
JOIN ki_bewertungen kb ON kb.vorlage_id = v.id AND kb.typ = 'umsetzung_match'
WHERE a.partei_id = ? {jahr_filter}
GROUP BY bewertung
ORDER BY anzahl DESC
""", params).fetchall()
umsetzung = [dict(r) for r in umsetzung_rows]
bewertet = sum(r["anzahl"] for r in umsetzung_rows)
# Top Anträge with scores
antraege = conn.execute(f"""
SELECT v.id, v.aktenzeichen, v.betreff, v.typ, v.datum_eingang,
kb.score as umsetzung_score,
json_extract(kb.anmerkungen, '$.bewertung') as umsetzung_bewertung,
kb.begruendung as umsetzung_begruendung
FROM vorlagen v
JOIN antragsteller a ON a.vorlage_id = v.id
LEFT JOIN ki_bewertungen kb ON kb.vorlage_id = v.id AND kb.typ = 'umsetzung_match'
WHERE a.partei_id = ? {jahr_filter}
ORDER BY v.datum_eingang DESC
LIMIT 200
""", params).fetchall()
# Jahre für Filter
jahre = conn.execute("""
SELECT DISTINCT strftime('%Y', v.datum_eingang) as j
FROM vorlagen v
JOIN antragsteller a ON a.vorlage_id = v.id
WHERE a.partei_id = ? AND v.datum_eingang IS NOT NULL
ORDER BY j DESC
""", [partei["id"]]).fetchall()
return {
"partei": dict(partei),
"total_antraege": total,
"bewertet": bewertet,
"umsetzung": umsetzung,
"antraege": [dict(r) for r in antraege],
"jahre": [r["j"] for r in jahre],
}

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""API routes for Ketten (chains).""" """API routes for Ketten (chains)."""
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
@ -32,7 +31,6 @@ def list_ketten(
status: str | None = None, status: str | None = None,
typ: str | None = None, typ: str | None = None,
suche: str | None = None, suche: str | None = None,
partei: str | None = None,
conn=Depends(_db), conn=Depends(_db),
): ):
"""List Ketten with optional filters.""" """List Ketten with optional filters."""
@ -47,13 +45,6 @@ def list_ketten(
where_clauses.append("k.typ = ?") where_clauses.append("k.typ = ?")
params.append(typ) params.append(typ)
if partei:
where_clauses.append(
"k.ursprung_id IN (SELECT a.vorlage_id FROM antragsteller a "
"JOIN parteien p ON a.partei_id = p.id WHERE p.kuerzel = ?)"
)
params.append(partei)
if suche: if suche:
where_clauses.append("k.thema LIKE ?") where_clauses.append("k.thema LIKE ?")
params.append(f"%{suche}%") params.append(f"%{suche}%")
@ -109,7 +100,7 @@ def get_kette(kette_id: int, conn=Depends(_db)):
"""Get a single Kette with all Glieder.""" """Get a single Kette with all Glieder."""
row = conn.execute( row = conn.execute(
"""SELECT k.id, k.typ, k.thema, k.status, k.status_seit, """SELECT k.id, k.typ, k.thema, k.status, k.status_seit,
k.letzte_aktivitaet, k.vertagungen_count, k.begruendung, k.ursprung_id, k.letzte_aktivitaet, k.vertagungen_count, k.ursprung_id,
v.aktenzeichen, v.typ as v_typ, v.betreff, v.datum_eingang, v.aktenzeichen, v.typ as v_typ, v.betreff, v.datum_eingang,
v.ist_verwaltungsvorlage v.ist_verwaltungsvorlage
FROM ketten k FROM ketten k
@ -179,7 +170,6 @@ def get_kette(kette_id: int, conn=Depends(_db)):
status_seit=row["status_seit"], status_seit=row["status_seit"],
letzte_aktivitaet=row["letzte_aktivitaet"], letzte_aktivitaet=row["letzte_aktivitaet"],
vertagungen_count=row["vertagungen_count"], vertagungen_count=row["vertagungen_count"],
begruendung=row["begruendung"],
glieder=glieder, glieder=glieder,
antragsteller=antragsteller, antragsteller=antragsteller,
graph=graph, graph=graph,

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""API routes for Orte und Karten-Daten.""" """API routes for Orte und Karten-Daten."""
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""API routes for Dashboard statistics.""" """API routes for Dashboard statistics."""
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""API routes for Vorlagen.""" """API routes for Vorlagen."""
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
@ -29,13 +28,12 @@ def _db():
conn.close() conn.close()
@router.get("") @router.get("", response_model=PaginatedVorlagen)
def list_vorlagen( def list_vorlagen(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200), page_size: int = Query(50, ge=1, le=200),
typ: str | None = None, typ: str | None = None,
suche: str | None = None, suche: str | None = None,
partei: str | None = None,
conn=Depends(_db), conn=Depends(_db),
): ):
"""List Vorlagen with optional filters.""" """List Vorlagen with optional filters."""
@ -46,24 +44,9 @@ def list_vorlagen(
where_clauses.append("v.typ = ?") where_clauses.append("v.typ = ?")
params.append(typ) params.append(typ)
if partei:
where_clauses.append(
"v.id IN (SELECT a.vorlage_id FROM antragsteller a "
"JOIN parteien p ON a.partei_id = p.id WHERE p.kuerzel = ?)"
)
params.append(partei)
if suche: if suche:
where_clauses.append( where_clauses.append("(v.betreff LIKE ? OR v.aktenzeichen LIKE ?)")
"(v.betreff LIKE ? OR v.aktenzeichen LIKE ?" params.extend([f"%{suche}%", f"%{suche}%"])
" OR v.volltext_clean LIKE ?"
" OR v.id IN ("
" SELECT kb.vorlage_id FROM ki_bewertungen kb"
" WHERE kb.typ = 'zusammenfassung' AND kb.begruendung LIKE ?"
"))"
)
like = f"%{suche}%"
params.extend([like, like, like, like])
where_sql = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else "" where_sql = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else ""
@ -81,36 +64,19 @@ def list_vorlagen(
params + [page_size, offset], params + [page_size, offset],
).fetchall() ).fetchall()
# Fetch antragsteller for all vorlagen in one query
vorlage_ids = [r["id"] for r in rows]
antragsteller_map: dict = {}
if vorlage_ids:
placeholders = ",".join("?" * len(vorlage_ids))
ast_rows = conn.execute(
f"""SELECT a.vorlage_id, p.kuerzel, p.name, p.farbe
FROM antragsteller a JOIN parteien p ON a.partei_id = p.id
WHERE a.vorlage_id IN ({placeholders})""",
vorlage_ids,
).fetchall()
for a in ast_rows:
antragsteller_map.setdefault(a["vorlage_id"], []).append(
{"kuerzel": a["kuerzel"], "name": a["name"], "farbe": a["farbe"]}
)
items = [ items = [
{ VorlageKurz(
"id": r["id"], id=r["id"],
"aktenzeichen": r["aktenzeichen"], aktenzeichen=r["aktenzeichen"],
"typ": r["typ"], typ=r["typ"],
"betreff": r["betreff"], betreff=r["betreff"],
"datum_eingang": r["datum_eingang"], datum_eingang=r["datum_eingang"],
"ist_verwaltungsvorlage": bool(r["ist_verwaltungsvorlage"]), ist_verwaltungsvorlage=bool(r["ist_verwaltungsvorlage"]),
"antragsteller": antragsteller_map.get(r["id"], []), )
}
for r in rows for r in rows
] ]
return {"items": items, "total": total, "page": page, "page_size": page_size} return PaginatedVorlagen(items=items, total=total, page=page, page_size=page_size)
@router.get("/{vorlage_id}", response_model=VorlageDetail) @router.get("/{vorlage_id}", response_model=VorlageDetail)
@ -183,26 +149,6 @@ def get_vorlage(vorlage_id: int, conn=Depends(_db)):
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
pass pass
# Umsetzungsbewertungen
from tracker.api.models import UmsetzungsBewertung
umsetzungs_rows = conn.execute(
"""SELECT kb.score, kb.begruendung, kb.vorlage_id as quelle_id, v2.aktenzeichen as quelle_az
FROM ki_bewertungen kb
LEFT JOIN vorlagen v2 ON v2.id = kb.vorlage_id
WHERE kb.vorlage_id = ? AND kb.typ = 'umsetzung_match'
ORDER BY kb.score DESC""",
(vorlage_id,),
).fetchall()
umsetzungsbewertungen = [
UmsetzungsBewertung(
score=u["score"],
begruendung=u["begruendung"],
quelle_vorlage_id=u["quelle_id"],
quelle_aktenzeichen=u["quelle_az"],
)
for u in umsetzungs_rows
]
return VorlageDetail( return VorlageDetail(
id=row["id"], id=row["id"],
aktenzeichen=row["aktenzeichen"], aktenzeichen=row["aktenzeichen"],
@ -222,5 +168,4 @@ def get_vorlage(vorlage_id: int, conn=Depends(_db)):
referenzen_eingehend=[ReferenzOut(**r) for r in refs["eingehend"]], referenzen_eingehend=[ReferenzOut(**r) for r in refs["eingehend"]],
kette_id=kette_row["kette_id"] if kette_row else None, kette_id=kette_row["kette_id"] if kette_row else None,
ki_zusammenfassung=ki_zusammenfassung, ki_zusammenfassung=ki_zusammenfassung,
umsetzungsbewertungen=umsetzungsbewertungen,
) )

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""Ketten-Builder: groups Vorlagen into chains based on Aktenzeichen-Suffix references.""" """Ketten-Builder: groups Vorlagen into chains based on Aktenzeichen-Suffix references."""
import sqlite3 import sqlite3
@ -98,7 +97,7 @@ def build_chains(conn: sqlite3.Connection) -> int:
conn.execute(""" conn.execute("""
UPDATE ketten UPDATE ketten
SET typ = ?, thema = ?, status = ?, status_seit = ?, SET typ = ?, thema = ?, status = ?, status_seit = ?,
letzte_aktivitaet = ?, vertagungen_count = ?, begruendung = ? letzte_aktivitaet = ?, vertagungen_count = ?
WHERE id = ? WHERE id = ?
""", ( """, (
chain_typ, chain_typ,
@ -107,15 +106,14 @@ def build_chains(conn: sqlite3.Connection) -> int:
status_info.get("status_seit"), status_info.get("status_seit"),
letzte_aktivitaet, letzte_aktivitaet,
status_info.get("vertagungen_count", 0), status_info.get("vertagungen_count", 0),
status_info.get("begruendung"),
kette_id, kette_id,
)) ))
conn.execute("DELETE FROM ketten_glieder WHERE kette_id = ?", (kette_id,)) conn.execute("DELETE FROM ketten_glieder WHERE kette_id = ?", (kette_id,))
else: else:
cursor = conn.execute(""" cursor = conn.execute("""
INSERT INTO ketten (ursprung_id, typ, thema, status, status_seit, INSERT INTO ketten (ursprung_id, typ, thema, status, status_seit,
letzte_aktivitaet, vertagungen_count, begruendung) letzte_aktivitaet, vertagungen_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", ( """, (
ursprung["id"], ursprung["id"],
chain_typ, chain_typ,
@ -124,7 +122,6 @@ def build_chains(conn: sqlite3.Connection) -> int:
status_info.get("status_seit"), status_info.get("status_seit"),
letzte_aktivitaet, letzte_aktivitaet,
status_info.get("vertagungen_count", 0), status_info.get("vertagungen_count", 0),
status_info.get("begruendung"),
)) ))
kette_id = cursor.lastrowid kette_id = cursor.lastrowid
@ -142,47 +139,6 @@ def build_chains(conn: sqlite3.Connection) -> int:
return count return count
def build_single_chain(conn: sqlite3.Connection, ursprung_id: int) -> None:
"""Rebuild a single chain by its ursprung vorlage ID."""
row = conn.execute(
"SELECT aktenzeichen_basis FROM vorlagen WHERE id = ?", (ursprung_id,)
).fetchone()
if not row or not row["aktenzeichen_basis"]:
return
basis = row["aktenzeichen_basis"]
members = conn.execute("""
SELECT id, aktenzeichen, aktenzeichen_suffix, typ, datum_eingang, betreff
FROM vorlagen WHERE aktenzeichen_basis = ?
ORDER BY CASE WHEN aktenzeichen_suffix IS NULL THEN 0
ELSE CAST(REPLACE(aktenzeichen_suffix, '-', '') AS INTEGER) END
""", (basis,)).fetchall()
if not members:
return
ursprung = members[0]
chain_typ = ursprung["typ"]
if chain_typ not in ("antrag", "anfrage"):
return
status_info = compute_status(conn, ursprung["id"], chain_typ, members)
dates = [m["datum_eingang"] for m in members if m["datum_eingang"]]
letzte_aktivitaet = max(dates) if dates else ursprung["datum_eingang"]
existing = conn.execute("SELECT id FROM ketten WHERE ursprung_id = ?", (ursprung["id"],)).fetchone()
if existing:
conn.execute("""
UPDATE ketten SET status = ?, status_seit = ?, letzte_aktivitaet = ?,
vertagungen_count = ?, begruendung = ?
WHERE id = ?
""", (
status_info["status"], status_info.get("status_seit"), letzte_aktivitaet,
status_info.get("vertagungen_count", 0), status_info.get("begruendung"),
existing["id"],
))
def _determine_rolle(member: sqlite3.Row, position: int) -> str: def _determine_rolle(member: sqlite3.Row, position: int) -> str:
if position == 0: if position == 0:
return "ursprung" return "ursprung"

View File

@ -1,83 +0,0 @@
from __future__ import annotations
"""Bewertungskategorien für Ketten-Match Umsetzungsanalyse."""
from typing import Dict
# Canonical categories with metadata
BEWERTUNGSKATEGORIEN: Dict[str, dict] = {
"erfuellt": {
"label": "Erfüllt",
"score_range": [0.8, 1.0],
"farbe": "#22c55e", # green-500
"icon": "",
"beschreibung": "Forderung vollständig oder weitgehend umgesetzt. Konkreter Beschluss liegt vor, der die Kernpunkte des Antrags aufgreift.",
"beispiel": "Antrag auf Zuschuss → Zuschuss in beantragter Höhe einstimmig bewilligt.",
},
"teilweise": {
"label": "Teilweise",
"score_range": [0.5, 0.7],
"farbe": "#eab308", # yellow-500
"icon": "⚠️",
"beschreibung": "Kernpunkt wurde adressiert, aber mit Abstrichen. Verwaltung greift Teile auf, lässt andere fallen oder verwässert die Forderung.",
"beispiel": "Antrag auf Radweg → Prüfauftrag für Machbarkeitsstudie statt direktem Baubeschluss.",
},
"abgewiegelt": {
"label": "Abgewiegelt",
"score_range": [0.2, 0.4],
"farbe": "#f97316", # orange-500
"icon": "🚫",
"beschreibung": "Verwaltung weicht aus. Kündigt Prüfung an, verweist auf Zuständigkeiten oder beantwortet die Frage nicht substantiell.",
"beispiel": "Anfrage zu Missständen → 'Wird geprüft' ohne Zeitrahmen oder konkrete Zusagen.",
},
"nebelkerze": {
"label": "Nebelkerze",
"score_range": [0.0, 0.1],
"farbe": "#ef4444", # red-500
"icon": "💨",
"beschreibung": "Thema komplett ignoriert, Diskussion auf Nebenschauplatz verlagert oder Antrag ohne Behandlung von der Tagesordnung genommen.",
"beispiel": "Antrag zu Sauberkeit → 'Ohne Beschlussfassung' oder keine Wortmeldung.",
},
"vertagt": {
"label": "Vertagt",
"score_range": None, # Variable score
"farbe": "#8b5cf6", # violet-500
"icon": "",
"beschreibung": "Antrag explizit verschoben, ruhend gestellt oder in einen anderen Ausschuss verwiesen. Kein inhaltliches Ergebnis, aber formal nicht abgelehnt.",
"beispiel": "Antrag wird zur weiteren Beratung in den Fachausschuss überwiesen.",
},
"unklar": {
"label": "Unklar",
"score_range": None,
"farbe": "#6b7280", # gray-500
"icon": "",
"beschreibung": "Beschlusslage nicht eindeutig zuordenbar. Möglicherweise fehlen Dokumente oder der Beschlusstext ist nicht aussagekräftig genug.",
"beispiel": "Nur 'Kenntnisnahme' ohne weitere Erläuterung.",
},
}
def score_to_kategorie(score: float) -> str:
"""Map a numeric score to the canonical category."""
if score >= 0.8:
return "erfuellt"
if score >= 0.5:
return "teilweise"
if score >= 0.2:
return "abgewiegelt"
return "nebelkerze"
def normalize_kategorie(raw: str) -> str:
"""Normalize variant spellings to canonical category."""
ALIASES = {
"erfüllt": "erfuellt",
"weitgehend erfüllt": "erfuellt",
"weitgehend erfuellt": "erfuellt",
"weitgehend_erfuellt": "erfuellt",
"weitestgehend_erfuellt": "erfuellt",
"weitgehend": "erfuellt",
"ohne beschlussfassung": "nebelkerze",
"ohne_beschlussfassung": "nebelkerze",
}
normalized = raw.strip().lower()
return ALIASES.get(normalized, normalized if normalized in BEWERTUNGSKATEGORIEN else "unklar")

View File

@ -23,7 +23,7 @@ def compute_status(
return _status_anfrage(conn, ursprung_id, members) return _status_anfrage(conn, ursprung_id, members)
elif chain_typ == "antrag": elif chain_typ == "antrag":
return _status_antrag(conn, ursprung_id, members) return _status_antrag(conn, ursprung_id, members)
return {"status": "unbekannt", "status_seit": None, "vertagungen_count": 0, "begruendung": "Unbekannter Kettentyp"} return {"status": "unbekannt", "status_seit": None, "vertagungen_count": 0}
def _status_anfrage( def _status_anfrage(
@ -47,21 +47,16 @@ def _status_anfrage(
stellungnahmen = [m for m in members if m["typ"] == "stellungnahme"] stellungnahmen = [m for m in members if m["typ"] == "stellungnahme"]
has_stellungnahme = len(stellungnahmen) > 0 has_stellungnahme = len(stellungnahmen) > 0
# Check for Kenntnisnahme in Beratungen (alle Kettenglieder) # Check for Kenntnisnahme in Beratungen
member_ids = [m["id"] for m in members] beratungen = conn.execute("""
placeholders = ",".join("?" * len(member_ids)) SELECT rolle, ergebnis, sitzung_datum
beratungen = conn.execute(f"""
SELECT rolle, ergebnis, sitzung_datum, beschlusstext
FROM beratungen FROM beratungen
WHERE vorlage_id IN ({placeholders}) WHERE vorlage_id = ?
ORDER BY sitzung_datum DESC ORDER BY sitzung_datum DESC
""", member_ids).fetchall() """, (ursprung_id,)).fetchall()
has_kenntnisnahme = any( has_kenntnisnahme = any(
(b["rolle"] and "kenntnisnahme" in b["rolle"].lower()) b["rolle"] and "kenntnisnahme" in b["rolle"].lower()
or (b["beschlusstext"] and "kenntnis" in b["beschlusstext"].lower())
if "beschlusstext" in b.keys() else
(b["rolle"] and "kenntnisnahme" in b["rolle"].lower())
for b in beratungen for b in beratungen
) )
@ -70,28 +65,20 @@ def _status_anfrage(
# Check zurückgezogen # Check zurückgezogen
if _is_zurueckgezogen(beratungen): if _is_zurueckgezogen(beratungen):
return {"status": "zurückgezogen", "status_seit": _latest_date(beratungen), "vertagungen_count": 0, return {"status": "zurückgezogen", "status_seit": _latest_date(beratungen), "vertagungen_count": 0}
"begruendung": "In einer Beratung explizit zurückgezogen."}
if has_stellungnahme: if has_stellungnahme:
if ki_score is not None and ki_score < 0.5: if ki_score is not None and ki_score < 0.5:
return {"status": "abgewiegelt", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0, return {"status": "abgewiegelt", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0}
"begruendung": f"Stellungnahme vorhanden, aber KI-Bewertung nur {ki_score:.0%} — die Antwort geht nicht ausreichend auf die Anfrage ein."}
if has_kenntnisnahme and (ki_score is None or ki_score >= 0.7): if has_kenntnisnahme and (ki_score is None or ki_score >= 0.7):
score_text = f" (KI-Match: {ki_score:.0%})" if ki_score is not None else "" return {"status": "beantwortet", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0}
return {"status": "beantwortet", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0, return {"status": "offen", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0}
"begruendung": f"Stellungnahme liegt vor{score_text}, Kenntnisnahme im Gremium erfolgt."}
return {"status": "offen", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0,
"begruendung": "Stellungnahme vorhanden, aber noch keine Kenntnisnahme im Gremium."}
# No Stellungnahme # No Stellungnahme
if ursprung_datum and (heute - ursprung_datum).days > VERSANDET_TAGE: if ursprung_datum and (heute - ursprung_datum).days > VERSANDET_TAGE:
tage = (heute - ursprung_datum).days return {"status": "versandet", "status_seit": ursprung_datum, "vertagungen_count": 0}
return {"status": "versandet", "status_seit": ursprung_datum, "vertagungen_count": 0,
"begruendung": f"Keine Stellungnahme nach {tage} Tagen."}
return {"status": "angefragt", "status_seit": ursprung_datum, "vertagungen_count": 0, return {"status": "angefragt", "status_seit": ursprung_datum, "vertagungen_count": 0}
"begruendung": "Anfrage gestellt, Stellungnahme steht noch aus."}
def _status_antrag( def _status_antrag(
@ -116,23 +103,19 @@ def _status_antrag(
heute = date.today() heute = date.today()
ursprung_datum = _parse_date(members[0]["datum_eingang"]) ursprung_datum = _parse_date(members[0]["datum_eingang"])
# Beratungen ALLER Kettenglieder sammeln (nicht nur Ursprung) beratungen = conn.execute("""
member_ids = [m["id"] for m in members] SELECT rolle, ergebnis, sitzung_datum
placeholders = ",".join("?" * len(member_ids))
beratungen = conn.execute(f"""
SELECT rolle, ergebnis, sitzung_datum, beschlusstext
FROM beratungen FROM beratungen
WHERE vorlage_id IN ({placeholders}) WHERE vorlage_id = ?
ORDER BY sitzung_datum DESC NULLS LAST ORDER BY sitzung_datum DESC NULLS LAST
""", member_ids).fetchall() """, (ursprung_id,)).fetchall()
# Count Vertagungen # Count Vertagungen
vertagungen = sum(1 for b in beratungen if b["ergebnis"] and "vertagt" in b["ergebnis"].lower()) vertagungen = sum(1 for b in beratungen if b["ergebnis"] and "vertagt" in b["ergebnis"].lower())
# Check zurückgezogen # Check zurückgezogen
if _is_zurueckgezogen(beratungen): if _is_zurueckgezogen(beratungen):
return {"status": "zurückgezogen", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen, return {"status": "zurückgezogen", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen}
"begruendung": "In einer Beratung explizit zurückgezogen."}
# Check for Berichte in chain # Check for Berichte in chain
berichte = [m for m in members if m["typ"] == "bericht"] berichte = [m for m in members if m["typ"] == "bericht"]
@ -140,11 +123,9 @@ def _status_antrag(
# Determine beschluss from beratungen # Determine beschluss from beratungen
beschluss = _get_beschluss(beratungen) beschluss = _get_beschluss(beratungen)
beschluss_details = _get_beschluss_details(beratungen)
if beschluss == "abgelehnt": if beschluss == "abgelehnt":
return {"status": "abgelehnt", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen, return {"status": "abgelehnt", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen}
"begruendung": f"In Beratung abgelehnt. {beschluss_details}"}
if beschluss == "angenommen": if beschluss == "angenommen":
beschluss_datum = _latest_date(beratungen) beschluss_datum = _latest_date(beratungen)
@ -155,43 +136,31 @@ def _status_antrag(
if ki_score is not None: if ki_score is not None:
if ki_score >= 0.7: if ki_score >= 0.7:
return {"status": "umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen, return {"status": "umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen}
"begruendung": f"Beschlossen ({beschluss_details}), Umsetzungsbericht liegt vor. KI-Bewertung: {ki_score:.0%} Übereinstimmung."}
elif ki_score >= 0.4: elif ki_score >= 0.4:
return {"status": "teilweise_umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen, return {"status": "teilweise_umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen}
"begruendung": f"Beschlossen ({beschluss_details}), aber Umsetzungsbericht zeigt nur teilweise Umsetzung. KI-Bewertung: {ki_score:.0%}."}
else: else:
return {"status": "abgewiegelt", "status_seit": bericht_datum, "vertagungen_count": vertagungen, return {"status": "abgewiegelt", "status_seit": bericht_datum, "vertagungen_count": vertagungen}
"begruendung": f"Beschlossen ({beschluss_details}), aber Umsetzungsbericht weicht stark vom Beschluss ab. KI-Bewertung: nur {ki_score:.0%}."} return {"status": "umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen}
return {"status": "umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen,
"begruendung": f"Beschlossen ({beschluss_details}), Umsetzungsbericht liegt vor (keine KI-Bewertung)."}
# Angenommen but no Bericht # Angenommen but no Bericht
if beschluss_datum and (heute - beschluss_datum).days > VERSANDET_TAGE: if beschluss_datum and (heute - beschluss_datum).days > VERSANDET_TAGE:
tage = (heute - beschluss_datum).days return {"status": "versandet", "status_seit": beschluss_datum, "vertagungen_count": vertagungen}
return {"status": "versandet", "status_seit": beschluss_datum, "vertagungen_count": vertagungen,
"begruendung": f"Beschlossen ({beschluss_details}), aber seit {tage} Tagen kein Umsetzungsbericht."}
return {"status": "beschlossen", "status_seit": beschluss_datum, "vertagungen_count": vertagungen, return {"status": "beschlossen", "status_seit": beschluss_datum, "vertagungen_count": vertagungen}
"begruendung": f"Antrag angenommen. {beschluss_details}"}
if beschluss == "verwiesen": if beschluss == "verwiesen":
return {"status": "verwiesen", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen, return {"status": "verwiesen", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen}
"begruendung": f"An anderen Ausschuss überwiesen. {beschluss_details}"}
# No final decision yet # No final decision yet
if beratungen: if beratungen:
last = beratungen[0] last = beratungen[0]
if last["ergebnis"] and "vertagt" in last["ergebnis"].lower(): if last["ergebnis"] and "vertagt" in last["ergebnis"].lower():
return {"status": "vertagt", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen, return {"status": "vertagt", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen}
"begruendung": f"Zuletzt vertagt ({vertagungen}x insgesamt). Letzte Beratung: {last['sitzung_datum'] or '?'}."} return {"status": "in_beratung", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen}
gremium_text = f"Letzte Beratung: {last['sitzung_datum'] or '?'}, Ergebnis: {last['ergebnis'] or 'k.A.'}."
return {"status": "in_beratung", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen,
"begruendung": f"In Beratung, noch kein Endbeschluss. {gremium_text}"}
# No beratungen at all # No beratungen at all
return {"status": "eingereicht", "status_seit": ursprung_datum, "vertagungen_count": vertagungen, return {"status": "eingereicht", "status_seit": ursprung_datum, "vertagungen_count": vertagungen}
"begruendung": "Eingereicht, noch keine Beratung erfolgt."}
# --- Helpers --- # --- Helpers ---
@ -231,65 +200,23 @@ def _is_zurueckgezogen(beratungen: list[sqlite3.Row]) -> bool:
) )
def _get_beschluss_details(beratungen: list[sqlite3.Row]) -> str:
"""Build a human-readable summary of the decisive Beratung."""
for b in beratungen:
ergebnis = (b["ergebnis"] or "").lower()
rolle = (b["rolle"] or "")
beschlusstext = (b["beschlusstext"] or "") if "beschlusstext" in b.keys() else ""
combined = f"{ergebnis} {beschlusstext}".lower()
datum = b["sitzung_datum"] or "?"
if any(kw in combined for kw in ("angenommen", "empfohlen", "beschlossen", "zugestimmt",
"einstimmig", "abgelehnt", "kenntnis", "ohne beschlussfassung")):
ergebnis_display = b["ergebnis"] or "behandelt"
parts = [f'Ergebnis: \u201e{ergebnis_display}\u201c ({datum})']
if beschlusstext and len(beschlusstext) > 5:
snippet = beschlusstext[:150]
ellipsis = "\u2026" if len(beschlusstext) > 150 else ""
parts.append(f'Beschlusstext: \u201e{snippet}{ellipsis}\u201c')
return " ".join(parts)
if "entscheidung" in rolle.lower() and ergebnis:
return f'Ergebnis: \u201e{b["ergebnis"]}\u201c ({datum})'
if beschlusstext and len(beschlusstext) > 5 and beschlusstext.lower() not in ("0", "-", ""):
snippet = beschlusstext[:150]
ellipsis = "\u2026" if len(beschlusstext) > 150 else ""
return f'Beschlusstext vorhanden ({datum}): \u201e{snippet}{ellipsis}\u201c.'
return ""
def _get_beschluss(beratungen: list[sqlite3.Row]) -> str | None: def _get_beschluss(beratungen: list[sqlite3.Row]) -> str | None:
"""Determine the final decision from Beratungen. """Determine the final decision from Beratungen.
Checks both ergebnis (OParl) and beschlusstext (scraped) fields.
Looks for Entscheidung-role beratungen with a result. Looks for Entscheidung-role beratungen with a result.
""" """
for b in beratungen: for b in beratungen:
ergebnis = (b["ergebnis"] or "").lower() ergebnis = (b["ergebnis"] or "").lower()
rolle = (b["rolle"] or "").lower() rolle = (b["rolle"] or "").lower()
beschlusstext = (b["beschlusstext"] or "").lower() if "beschlusstext" in b.keys() else ""
# Combine ergebnis and beschlusstext for keyword search if "abgelehnt" in ergebnis:
combined = f"{ergebnis} {beschlusstext}"
if "abgelehnt" in combined:
return "abgelehnt" return "abgelehnt"
if "verwiesen" in combined: if "verwiesen" in ergebnis:
return "verwiesen" return "verwiesen"
if any(kw in combined for kw in ("angenommen", "empfohlen", "beschlossen", "zugestimmt", "einstimmig")): if any(kw in ergebnis for kw in ("angenommen", "empfohlen", "beschlossen", "zugestimmt")):
return "angenommen" return "angenommen"
if any(kw in combined for kw in ("zur kenntnis genommen", "kenntnisnahme", "kenntnis genommen")):
return "angenommen" # Kenntnisnahme = Vorgang abgeschlossen
if "ohne beschlussfassung" in combined:
return "angenommen" # Behandelt ohne formalen Beschluss = erledigt
# If rolle is Entscheidung and there's any ergebnis, it's likely a decision # If rolle is Entscheidung and there's any ergebnis, it's likely a decision
if "entscheidung" in rolle and ergebnis and "vertagt" not in ergebnis: if "entscheidung" in rolle and ergebnis and "vertagt" not in ergebnis:
return "angenommen" return "angenommen"
# beschlusstext vorhanden und nicht leer/0/kurz = es gab eine Behandlung
if beschlusstext and len(beschlusstext) > 5 and beschlusstext not in ("0", "-", ""):
# Es wurde beraten und es gibt einen Text → vermutlich erledigt
return "angenommen"
return None return None

View File

@ -1,4 +1,3 @@
from __future__ import annotations
"""SQLite database connection management.""" """SQLite database connection management."""
import os import os
@ -10,7 +9,7 @@ from pathlib import Path
DB_PATH = Path(os.environ.get("DATABASE_PATH", Path(__file__).resolve().parents[4] / "data" / "tracker.db")) DB_PATH = Path(os.environ.get("DATABASE_PATH", Path(__file__).resolve().parents[4] / "data" / "tracker.db"))
def get_connection(db_path=None) -> sqlite3.Connection: def get_connection(db_path: Path | str | None = None) -> sqlite3.Connection:
path = str(db_path or DB_PATH) path = str(db_path or DB_PATH)
conn = sqlite3.connect(path, detect_types=0) conn = sqlite3.connect(path, detect_types=0)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row

View File

@ -1,15 +1,13 @@
from __future__ import annotations
"""FastAPI application for Antragstracker Hagen.""" """FastAPI application for Antragstracker Hagen."""
import os import os
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, Request from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from tracker.api.routes import abstimmungen, bewertung, fraktionen, ketten, orte, stats, vorlagen from tracker.api.routes import abstimmungen, ketten, orte, stats, vorlagen
app = FastAPI( app = FastAPI(
title="Antragstracker Hagen", title="Antragstracker Hagen",
@ -20,7 +18,7 @@ app = FastAPI(
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
allow_methods=["GET", "POST"], allow_methods=["GET"],
allow_headers=["*"], allow_headers=["*"],
) )
@ -29,8 +27,6 @@ app.include_router(ketten.router, prefix="/api")
app.include_router(stats.router, prefix="/api") app.include_router(stats.router, prefix="/api")
app.include_router(abstimmungen.router, prefix="/api") app.include_router(abstimmungen.router, prefix="/api")
app.include_router(orte.router, prefix="/api") app.include_router(orte.router, prefix="/api")
app.include_router(fraktionen.router, prefix="/api")
app.include_router(bewertung.router, prefix="/api")
@app.get("/api/health") @app.get("/api/health")
@ -40,23 +36,8 @@ def health():
# Serve static frontend files in production # Serve static frontend files in production
# Try multiple paths (Docker vs local dev) # Try multiple paths (Docker vs local dev)
_static_dir: Path | None = None
for static_path in ["/app/static", Path(__file__).parent.parent.parent.parent / "static"]: for static_path in ["/app/static", Path(__file__).parent.parent.parent.parent / "static"]:
static_dir = Path(static_path) static_dir = Path(static_path)
if static_dir.exists() and (static_dir / "index.html").exists(): if static_dir.exists() and (static_dir / "index.html").exists():
_static_dir = static_dir app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
app.mount("/_app", StaticFiles(directory=str(static_dir / "_app")), name="static-app")
break break
# SPA catch-all: serve index.html for any non-API, non-asset route
@app.get("/{full_path:path}")
async def spa_fallback(request: Request, full_path: str):
if _static_dir is None:
return {"error": "static files not found"}
# Try to serve static file first
file_path = _static_dir / full_path
if file_path.is_file():
return FileResponse(file_path)
# Fallback to index.html for SPA routing
return FileResponse(_static_dir / "index.html")

View File

@ -95,7 +95,6 @@ export interface KetteDetail {
status_seit: string | null; status_seit: string | null;
letzte_aktivitaet: string | null; letzte_aktivitaet: string | null;
vertagungen_count: number; vertagungen_count: number;
begruendung: string | null;
glieder: KettenGliedOut[]; glieder: KettenGliedOut[];
antragsteller: ParteiOut[]; antragsteller: ParteiOut[];
graph: { graph: {
@ -168,44 +167,3 @@ export const fetchKetten = (params: Record<string, string>) => {
}; };
export const fetchKette = (id: number) => get<KetteDetail>(`/ketten/${id}`); export const fetchKette = (id: number) => get<KetteDetail>(`/ketten/${id}`);
// Fraktionen
export interface FraktionDashboard {
partei: { id: number; kuerzel: string; name: string; farbe: string | null };
total_antraege: number;
bewertet: number;
umsetzung: { bewertung: string; anzahl: number; avg_score: number }[];
antraege: {
id: number; aktenzeichen: string | null; betreff: string | null;
typ: string | null; datum_eingang: string | null;
umsetzung_score: number | null; umsetzung_bewertung: string | null;
umsetzung_begruendung: string | null;
}[];
jahre: string[];
}
// Re-evaluation
async function post<T>(path: string, body: object): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export const reevalVorlage = (id: number, anmerkung: string) =>
post<{ job_id: string; status: string }>(`/bewertung/vorlagen/${id}`, { anmerkung });
export const reevalKette = (id: number, anmerkung: string) =>
post<{ job_id: string; status: string }>(`/bewertung/ketten/${id}`, { anmerkung });
export const fetchJobStatus = (jobId: string) =>
get<{ status: string; result?: object; error?: string }>(`/bewertung/status/${jobId}`);
export const fetchFraktionen = () => get<{ id: number; kuerzel: string; name: string; farbe: string | null; anzahl: number }[]>('/fraktionen');
export const fetchFraktionDashboard = (kuerzel: string, jahr?: string) => {
const params = jahr ? `?jahr=${jahr}` : '';
return get<FraktionDashboard>(`/fraktionen/${kuerzel}/dashboard${params}`);
};

View File

@ -1,37 +0,0 @@
<script lang="ts">
import { umsetzungInfo, type UmsetzungKategorie } from '$lib/umsetzung';
let { kategorie, score = null, showTooltip = true }: {
kategorie: string | null;
score?: number | null;
showTooltip?: boolean;
} = $props();
const info = $derived(umsetzungInfo(kategorie as UmsetzungKategorie));
let tooltipVisible = $state(false);
</script>
<span
class="relative inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium cursor-help"
style="background-color: {info.farbe}20; color: {info.farbe}; border: 1px solid {info.farbe}40;"
onmouseenter={() => tooltipVisible = true}
onmouseleave={() => tooltipVisible = false}
>
<span class="mr-1">{info.icon}</span>
{info.label}
{#if score !== null && score !== undefined}
<span class="ml-1 opacity-60">({(score * 100).toFixed(0)}%)</span>
{/if}
{#if showTooltip && tooltipVisible}
<div class="absolute z-50 bottom-full left-1/2 -translate-x-1/2 mb-2 w-72 p-3 bg-gray-900 text-white text-xs rounded-lg shadow-lg pointer-events-none">
<div class="font-bold mb-1">{info.icon} {info.label}</div>
<div class="mb-2 leading-relaxed">{info.beschreibung}</div>
<div class="text-gray-400 italic">Beispiel: {info.beispiel}</div>
{#if info.score_range}
<div class="mt-1 text-gray-400">Score: {info.score_range[0]}{info.score_range[1]}</div>
{/if}
<div class="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-2 h-2 bg-gray-900 rotate-45"></div>
</div>
{/if}
</span>

View File

@ -1,84 +0,0 @@
export type UmsetzungKategorie = 'erfuellt' | 'teilweise' | 'abgewiegelt' | 'nebelkerze' | 'vertagt' | 'unklar';
interface KategorieInfo {
label: string;
farbe: string;
icon: string;
beschreibung: string;
beispiel: string;
score_range: [number, number] | null;
}
const KATEGORIEN: Record<UmsetzungKategorie, KategorieInfo> = {
erfuellt: {
label: 'Erfüllt',
farbe: '#22c55e',
icon: '✅',
beschreibung: 'Forderung vollständig oder weitgehend umgesetzt. Konkreter Beschluss liegt vor, der die Kernpunkte des Antrags aufgreift.',
beispiel: 'Antrag auf Zuschuss → Zuschuss in beantragter Höhe einstimmig bewilligt.',
score_range: [0.8, 1.0],
},
teilweise: {
label: 'Teilweise',
farbe: '#eab308',
icon: '⚠️',
beschreibung: 'Kernpunkt wurde adressiert, aber mit Abstrichen. Verwaltung greift Teile auf, lässt andere fallen oder verwässert die Forderung.',
beispiel: 'Antrag auf Radweg → Prüfauftrag für Machbarkeitsstudie statt direktem Baubeschluss.',
score_range: [0.5, 0.7],
},
abgewiegelt: {
label: 'Abgewiegelt',
farbe: '#f97316',
icon: '🚫',
beschreibung: 'Verwaltung weicht aus. Kündigt Prüfung an, verweist auf Zuständigkeiten oder beantwortet die Frage nicht substantiell.',
beispiel: 'Anfrage zu Missständen → „Wird geprüft" ohne Zeitrahmen oder konkrete Zusagen.',
score_range: [0.2, 0.4],
},
nebelkerze: {
label: 'Nebelkerze',
farbe: '#ef4444',
icon: '💨',
beschreibung: 'Thema komplett ignoriert, Diskussion auf Nebenschauplatz verlagert oder Antrag ohne Behandlung von der Tagesordnung genommen.',
beispiel: 'Antrag zu Sauberkeit → „Ohne Beschlussfassung" oder keine Wortmeldung.',
score_range: [0.0, 0.1],
},
vertagt: {
label: 'Vertagt',
farbe: '#8b5cf6',
icon: '⏳',
beschreibung: 'Antrag explizit verschoben, ruhend gestellt oder in einen anderen Ausschuss verwiesen. Kein inhaltliches Ergebnis, aber formal nicht abgelehnt.',
beispiel: 'Antrag wird zur weiteren Beratung in den Fachausschuss überwiesen.',
score_range: null,
},
unklar: {
label: 'Unklar',
farbe: '#6b7280',
icon: '❓',
beschreibung: 'Beschlusslage nicht eindeutig zuordenbar. Möglicherweise fehlen Dokumente oder der Beschlusstext ist nicht aussagekräftig genug.',
beispiel: 'Nur „Kenntnisnahme" ohne weitere Erläuterung.',
score_range: null,
},
};
const FALLBACK: KategorieInfo = {
label: 'Unbekannt',
farbe: '#9ca3af',
icon: '❓',
beschreibung: 'Keine Bewertung vorhanden.',
beispiel: '',
score_range: null,
};
export function umsetzungInfo(kategorie: UmsetzungKategorie | string | null): KategorieInfo {
if (!kategorie) return FALLBACK;
return KATEGORIEN[kategorie as UmsetzungKategorie] ?? FALLBACK;
}
export function scoreToKategorie(score: number): UmsetzungKategorie {
if (score >= 0.8) return 'erfuellt';
if (score >= 0.5) return 'teilweise';
if (score >= 0.2) return 'abgewiegelt';
return 'nebelkerze';
}
export { KATEGORIEN };

View File

@ -18,7 +18,6 @@
<a href="/vorlagen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Vorlagen</a> <a href="/vorlagen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Vorlagen</a>
<a href="/abstimmungen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Abstimmungen</a> <a href="/abstimmungen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Abstimmungen</a>
<a href="/karte" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Karte</a> <a href="/karte" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Karte</a>
<a href="/fraktionen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Fraktionen</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,37 +0,0 @@
<script lang="ts">
import { fetchFraktionen } from '$lib/api';
import { onMount } from 'svelte';
let fraktionen = $state<{ id: number; kuerzel: string; name: string; farbe: string | null; anzahl: number }[]>([]);
let loading = $state(true);
onMount(async () => {
fraktionen = await fetchFraktionen();
loading = false;
});
</script>
<svelte:head>
<title>Fraktionen — Antragstracker Hagen</title>
</svelte:head>
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-6">Fraktionen</h1>
{#if loading}
<div class="text-gray-500">Laden...</div>
{:else}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{#each fraktionen as f}
<a href="/fraktionen/{f.kuerzel}"
class="block p-4 rounded-lg border hover:shadow-md transition-shadow"
style="border-left: 4px solid {f.farbe || '#6b7280'}">
<div class="font-bold text-lg" style="color: {f.farbe || '#374151'}">{f.kuerzel}</div>
<div class="text-sm text-gray-600">{f.name}</div>
<div class="mt-2 text-2xl font-semibold">{f.anzahl}</div>
<div class="text-xs text-gray-500">Anträge & Anfragen</div>
</a>
{/each}
</div>
{/if}
</div>

View File

@ -1,180 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { fetchFraktionDashboard, type FraktionDashboard } from '$lib/api';
import UmsetzungBadge from '$lib/components/UmsetzungBadge.svelte';
import { KATEGORIEN } from '$lib/umsetzung';
import { formatDate } from '$lib/status';
import { onMount } from 'svelte';
let data = $state<FraktionDashboard | null>(null);
let loading = $state(true);
let error = $state<string | null>(null);
let selectedJahr = $state<string>('');
let filterKategorie = $state<string>('');
let kuerzel = $derived($page.params.kuerzel);
async function loadData() {
loading = true;
error = null;
try {
data = await fetchFraktionDashboard(kuerzel, selectedJahr || undefined);
} catch (e) {
error = (e as Error).message;
}
loading = false;
}
onMount(loadData);
// Reload when filters change
$effect(() => {
if (kuerzel) loadData();
});
let filteredAntraege = $derived(
data?.antraege.filter(a => !filterKategorie || a.umsetzung_bewertung === filterKategorie) ?? []
);
// Calculate percentages for bar chart
let umsetzungPct = $derived(() => {
if (!data || data.bewertet === 0) return [];
return data.umsetzung.map(u => ({
...u,
pct: ((u.anzahl / data!.bewertet) * 100).toFixed(1),
}));
});
</script>
<svelte:head>
<title>{data?.partei?.name ?? kuerzel} — Antragstracker Hagen</title>
</svelte:head>
<div class="max-w-6xl mx-auto p-6">
{#if loading && !data}
<div class="text-gray-500">Laden...</div>
{:else if error}
<div class="text-red-600">Fehler: {error}</div>
{:else if data}
<!-- Header -->
<div class="flex items-center gap-4 mb-6">
<div class="w-3 h-12 rounded" style="background-color: {data.partei.farbe || '#6b7280'}"></div>
<div>
<h1 class="text-2xl font-bold">{data.partei.name}</h1>
<span class="text-sm text-gray-500">{data.partei.kuerzel}</span>
</div>
<a href="/fraktionen" class="ml-auto text-sm text-blue-600 hover:underline">← Alle Fraktionen</a>
</div>
<!-- KPIs -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-lg border p-4">
<div class="text-3xl font-bold">{data.total_antraege}</div>
<div class="text-sm text-gray-500">Anträge gesamt</div>
</div>
<div class="bg-white rounded-lg border p-4">
<div class="text-3xl font-bold">{data.bewertet}</div>
<div class="text-sm text-gray-500">Mit Umsetzungsbewertung</div>
</div>
{#each data.umsetzung.filter(u => u.bewertung === 'erfuellt') as u}
<div class="bg-green-50 rounded-lg border border-green-200 p-4">
<div class="text-3xl font-bold text-green-700">{u.anzahl}</div>
<div class="text-sm text-green-600">Erfüllt</div>
</div>
{/each}
{#each data.umsetzung.filter(u => u.bewertung === 'nebelkerze') as u}
<div class="bg-red-50 rounded-lg border border-red-200 p-4">
<div class="text-3xl font-bold text-red-700">{u.anzahl}</div>
<div class="text-sm text-red-600">Nebelkerzen</div>
</div>
{/each}
</div>
<!-- Umsetzungs-Übersicht (Horizontal Bar) -->
{#if data.bewertet > 0}
<div class="bg-white rounded-lg border p-6 mb-8">
<h2 class="font-bold mb-4">Umsetzungsquote</h2>
<div class="flex rounded-full overflow-hidden h-8 mb-4">
{#each data.umsetzung as u}
{@const info = KATEGORIEN[u.bewertung as keyof typeof KATEGORIEN]}
{@const pct = (u.anzahl / data.bewertet) * 100}
{#if info && pct > 0}
<div
class="flex items-center justify-center text-xs font-medium text-white transition-all"
style="width: {pct}%; background-color: {info.farbe};"
title="{info.label}: {u.anzahl} ({pct.toFixed(1)}%)"
>
{#if pct > 8}{info.label} {pct.toFixed(0)}%{/if}
</div>
{/if}
{/each}
</div>
<!-- Legend -->
<div class="flex flex-wrap gap-4 text-sm">
{#each data.umsetzung as u}
{@const info = KATEGORIEN[u.bewertung as keyof typeof KATEGORIEN]}
{#if info}
<button
class="flex items-center gap-1.5 hover:opacity-70 transition-opacity"
class:opacity-40={filterKategorie && filterKategorie !== u.bewertung}
onclick={() => filterKategorie = filterKategorie === u.bewertung ? '' : u.bewertung}
>
<span class="w-3 h-3 rounded-full inline-block" style="background-color: {info.farbe}"></span>
{info.label}: {u.anzahl}
</button>
{/if}
{/each}
</div>
</div>
{/if}
<!-- Filters -->
<div class="flex gap-4 mb-4">
<select bind:value={selectedJahr} onchange={loadData} class="border rounded px-3 py-1.5 text-sm">
<option value="">Alle Jahre</option>
{#each data.jahre as j}
<option value={j}>{j}</option>
{/each}
</select>
{#if filterKategorie}
<button onclick={() => filterKategorie = ''} class="text-sm text-blue-600 hover:underline">
Filter zurücksetzen
</button>
{/if}
<span class="text-sm text-gray-500 ml-auto">{filteredAntraege.length} Anträge</span>
</div>
<!-- Anträge-Liste -->
<div class="bg-white rounded-lg border overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-left">
<tr>
<th class="px-4 py-3 font-medium">Aktenzeichen</th>
<th class="px-4 py-3 font-medium">Betreff</th>
<th class="px-4 py-3 font-medium">Datum</th>
<th class="px-4 py-3 font-medium">Umsetzung</th>
</tr>
</thead>
<tbody class="divide-y">
{#each filteredAntraege as a}
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">
<a href="/vorlagen/{a.id}" class="text-blue-600 hover:underline font-mono text-xs">
{a.aktenzeichen}
</a>
</td>
<td class="px-4 py-3 max-w-md truncate">{a.betreff}</td>
<td class="px-4 py-3 text-gray-500 whitespace-nowrap">{formatDate(a.datum_eingang)}</td>
<td class="px-4 py-3">
{#if a.umsetzung_bewertung}
<UmsetzungBadge kategorie={a.umsetzung_bewertung} score={a.umsetzung_score} />
{:else}
<span class="text-gray-400 text-xs"></span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>

View File

@ -2,7 +2,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fetchKetten, fetchFraktionen, type KetteKurz, type Paginated } from '$lib/api'; import { fetchKetten, type KetteKurz, type Paginated } from '$lib/api';
import { formatDate } from '$lib/status'; import { formatDate } from '$lib/status';
import StatusBadge from '$lib/components/StatusBadge.svelte'; import StatusBadge from '$lib/components/StatusBadge.svelte';
@ -14,16 +14,13 @@
let filterStatus = $state(''); let filterStatus = $state('');
let filterTyp = $state(''); let filterTyp = $state('');
let filterSuche = $state(''); let filterSuche = $state('');
let filterPartei = $state('');
let currentPage = $state(1); let currentPage = $state(1);
let parteien = $state<{ kuerzel: string; name: string; farbe: string | null; anzahl: number }[]>([]);
function syncFromUrl() { function syncFromUrl() {
const p = new URL(window.location.href).searchParams; const p = new URL(window.location.href).searchParams;
filterStatus = p.get('status') || ''; filterStatus = p.get('status') || '';
filterTyp = p.get('typ') || ''; filterTyp = p.get('typ') || '';
filterSuche = p.get('suche') || ''; filterSuche = p.get('suche') || '';
filterPartei = p.get('partei') || '';
currentPage = parseInt(p.get('page') || '1'); currentPage = parseInt(p.get('page') || '1');
} }
@ -34,7 +31,6 @@
if (filterStatus) params.status = filterStatus; if (filterStatus) params.status = filterStatus;
if (filterTyp) params.typ = filterTyp; if (filterTyp) params.typ = filterTyp;
if (filterSuche) params.suche = filterSuche; if (filterSuche) params.suche = filterSuche;
if (filterPartei) params.partei = filterPartei;
data = await fetchKetten(params); data = await fetchKetten(params);
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Fehler'; error = e instanceof Error ? e.message : 'Fehler';
@ -48,7 +44,6 @@
if (filterStatus) params.set('status', filterStatus); if (filterStatus) params.set('status', filterStatus);
if (filterTyp) params.set('typ', filterTyp); if (filterTyp) params.set('typ', filterTyp);
if (filterSuche) params.set('suche', filterSuche); if (filterSuche) params.set('suche', filterSuche);
if (filterPartei) params.set('partei', filterPartei);
currentPage = 1; currentPage = 1;
params.set('page', '1'); params.set('page', '1');
goto(`/ketten?${params.toString()}`, { replaceState: true }); goto(`/ketten?${params.toString()}`, { replaceState: true });
@ -63,8 +58,7 @@
load(); load();
} }
onMount(async () => { onMount(() => {
parteien = await fetchFraktionen();
syncFromUrl(); syncFromUrl();
load(); load();
}); });
@ -115,16 +109,6 @@
<option value="anfrage">Anfrage</option> <option value="anfrage">Anfrage</option>
</select> </select>
</div> </div>
<div>
<label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label>
<select id="partei" bind:value={filterPartei} onchange={applyFilters}
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500">
<option value="">Alle</option>
{#each parteien as p}
<option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option>
{/each}
</select>
</div>
<button onclick={applyFilters} <button onclick={applyFilters}
class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors"> class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
Filtern Filtern

View File

@ -1,47 +1,13 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { fetchKette, reevalKette, fetchJobStatus, type KetteDetail } from '$lib/api'; import { fetchKette, type KetteDetail } from '$lib/api';
import { statusInfo, typLabel, formatDate } from '$lib/status'; import { statusInfo, typLabel, formatDate } from '$lib/status';
import StatusBadge from '$lib/components/StatusBadge.svelte'; import StatusBadge from '$lib/components/StatusBadge.svelte';
import Perlenschnur from '$lib/components/Perlenschnur.svelte'; import Perlenschnur from '$lib/components/Perlenschnur.svelte';
let kette: KetteDetail | null = $state(null); let kette: KetteDetail | null = $state(null);
let error: string | null = $state(null); let error: string | null = $state(null);
let showReeval = $state(false);
let reevalAnmerkung = $state('');
let reevalStatus = $state<'idle' | 'running' | 'done' | 'error'>('idle');
let reevalError = $state('');
async function triggerReeval() {
if (!kette) return;
reevalStatus = 'running';
reevalError = '';
try {
const { job_id } = await reevalKette(kette.id, reevalAnmerkung);
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 3000));
const status = await fetchJobStatus(job_id);
if (status.status === 'done') {
reevalStatus = 'done';
kette = await fetchKette(kette!.id);
showReeval = false;
reevalAnmerkung = '';
return;
}
if (status.status === 'error') {
reevalStatus = 'error';
reevalError = status.error || 'Unbekannter Fehler';
return;
}
}
reevalStatus = 'error';
reevalError = 'Timeout nach 3 Minuten';
} catch (e) {
reevalStatus = 'error';
reevalError = e instanceof Error ? e.message : 'Fehler';
}
}
onMount(async () => { onMount(async () => {
try { try {
@ -109,53 +75,6 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<!-- Begründung -->
{#if kette.begruendung}
<div class="mt-4 p-3 rounded-lg bg-gray-50 border border-gray-200">
<span class="text-xs font-medium text-gray-500 uppercase">Klassifikation</span>
<p class="text-sm text-gray-700 mt-1">{kette.begruendung}</p>
</div>
{/if}
<!-- Neu bewerten -->
<div class="mt-4">
{#if !showReeval}
<button onclick={() => showReeval = true}
class="text-sm text-green-600 hover:text-green-800 font-medium flex items-center gap-1.5">
<span>🔄</span> Kette neu bewerten lassen
</button>
{:else}
<div class="p-4 rounded-lg border border-green-200 bg-green-50">
<h3 class="text-sm font-semibold text-gray-900 mb-2">KI-Neubewertung der Kette</h3>
<textarea bind:value={reevalAnmerkung} placeholder="Anmerkungen für die KI (optional) z.B. 'Der Antrag wurde mündlich im Ausschuss behandelt' oder 'Bitte Wortprotokoll stärker gewichten'"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm mb-3 h-20 resize-y focus:ring-2 focus:ring-green-500 focus:border-green-500"
disabled={reevalStatus === 'running'}></textarea>
<div class="flex gap-2 items-center">
<button onclick={triggerReeval} disabled={reevalStatus === 'running'}
class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-wait transition-colors">
{#if reevalStatus === 'running'}
<span class="inline-flex items-center gap-2">
<span class="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></span>
KI bewertet…
</span>
{:else}
Bewertung starten
{/if}
</button>
{#if reevalStatus !== 'running'}
<button onclick={() => { showReeval = false; reevalStatus = 'idle'; }}
class="text-sm text-gray-500 hover:text-gray-700">Abbrechen</button>
{/if}
</div>
{#if reevalStatus === 'done'}
<p class="mt-2 text-sm text-green-700 font-medium">✅ Bewertung aktualisiert!</p>
{/if}
{#if reevalStatus === 'error'}
<p class="mt-2 text-sm text-red-600">{reevalError}</p>
{/if}
</div>
{/if}
</div>
</div> </div>
<!-- Perlenschnur Timeline --> <!-- Perlenschnur Timeline -->

View File

@ -1,30 +1,21 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fetchVorlagen, fetchFraktionen, type VorlageKurz, type Paginated } from '$lib/api'; import { fetchVorlagen, type VorlageKurz, type Paginated } from '$lib/api';
import { formatDate } from '$lib/status'; import { formatDate } from '$lib/status';
let data: Paginated<VorlageKurz & { antragsteller?: { kuerzel: string; name: string; farbe: string | null }[] }> | null = $state(null); let data: Paginated<VorlageKurz> | null = $state(null);
let parteien = $state<{ kuerzel: string; name: string; farbe: string | null; anzahl: number }[]>([]);
function highlight(text: string | null, query: string): string {
if (!text || !query) return text || '-';
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return text.replace(new RegExp(`(${escaped})`, 'gi'), '<mark class="bg-yellow-200 rounded px-0.5">$1</mark>');
}
let error: string | null = $state(null); let error: string | null = $state(null);
let loading = $state(false); let loading = $state(false);
let filterTyp = $state(''); let filterTyp = $state('');
let filterSuche = $state(''); let filterSuche = $state('');
let filterPartei = $state('');
let currentPage = $state(1); let currentPage = $state(1);
function syncFromUrl() { function syncFromUrl() {
const p = new URL(window.location.href).searchParams; const p = new URL(window.location.href).searchParams;
filterTyp = p.get('typ') || ''; filterTyp = p.get('typ') || '';
filterSuche = p.get('suche') || ''; filterSuche = p.get('suche') || '';
filterPartei = p.get('partei') || '';
currentPage = parseInt(p.get('page') || '1'); currentPage = parseInt(p.get('page') || '1');
} }
@ -34,7 +25,6 @@
const params: Record<string, string> = { page: String(currentPage), page_size: '50' }; const params: Record<string, string> = { page: String(currentPage), page_size: '50' };
if (filterTyp) params.typ = filterTyp; if (filterTyp) params.typ = filterTyp;
if (filterSuche) params.suche = filterSuche; if (filterSuche) params.suche = filterSuche;
if (filterPartei) params.partei = filterPartei;
data = await fetchVorlagen(params); data = await fetchVorlagen(params);
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Fehler'; error = e instanceof Error ? e.message : 'Fehler';
@ -47,7 +37,6 @@
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filterTyp) params.set('typ', filterTyp); if (filterTyp) params.set('typ', filterTyp);
if (filterSuche) params.set('suche', filterSuche); if (filterSuche) params.set('suche', filterSuche);
if (filterPartei) params.set('partei', filterPartei);
currentPage = 1; currentPage = 1;
params.set('page', '1'); params.set('page', '1');
goto(`/vorlagen?${params.toString()}`, { replaceState: true }); goto(`/vorlagen?${params.toString()}`, { replaceState: true });
@ -62,8 +51,7 @@
load(); load();
} }
onMount(async () => { onMount(() => {
parteien = await fetchFraktionen();
syncFromUrl(); syncFromUrl();
load(); load();
}); });
@ -83,7 +71,7 @@
<div class="flex flex-wrap gap-3 items-end"> <div class="flex flex-wrap gap-3 items-end">
<div> <div>
<label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label> <label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label>
<input id="suche" type="text" bind:value={filterSuche} placeholder="Volltextsuche..." <input id="suche" type="text" bind:value={filterSuche} placeholder="Betreff oder Aktenzeichen..."
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
onkeydown={(e) => { if (e.key === 'Enter') applyFilters(); }} /> onkeydown={(e) => { if (e.key === 'Enter') applyFilters(); }} />
</div> </div>
@ -98,16 +86,6 @@
<option value="bericht">Bericht</option> <option value="bericht">Bericht</option>
</select> </select>
</div> </div>
<div>
<label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label>
<select id="partei" bind:value={filterPartei} onchange={applyFilters}
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500">
<option value="">Alle</option>
{#each parteien as p}
<option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option>
{/each}
</select>
</div>
<button onclick={applyFilters} <button onclick={applyFilters}
class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors"> class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
Filtern Filtern
@ -130,7 +108,6 @@
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Aktenzeichen</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Aktenzeichen</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Betreff</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Betreff</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Partei</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Typ</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Typ</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Datum</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Datum</th>
</tr> </tr>
@ -140,36 +117,10 @@
<tr class="hover:bg-gray-50 transition-colors cursor-pointer" onclick={() => goto(`/vorlagen/${v.id}`)}> <tr class="hover:bg-gray-50 transition-colors cursor-pointer" onclick={() => goto(`/vorlagen/${v.id}`)}>
<td class="px-4 py-3"> <td class="px-4 py-3">
<a href="/vorlagen/{v.id}" class="font-mono text-sm font-medium text-green-700 hover:underline"> <a href="/vorlagen/{v.id}" class="font-mono text-sm font-medium text-green-700 hover:underline">
{#if filterSuche}
{@html highlight(v.aktenzeichen || `#${v.id}`, filterSuche)}
{:else}
{v.aktenzeichen || `#${v.id}`} {v.aktenzeichen || `#${v.id}`}
{/if}
</a> </a>
</td> </td>
<td class="px-4 py-3 text-sm text-gray-700 max-w-lg truncate"> <td class="px-4 py-3 text-sm text-gray-700 max-w-lg truncate">{v.betreff || '-'}</td>
{#if filterSuche}
{@html highlight(v.betreff, filterSuche)}
{:else}
{v.betreff || '-'}
{/if}
</td>
<td class="px-4 py-3">
{#if v.antragsteller?.length}
<div class="flex flex-wrap gap-1">
{#each v.antragsteller as a}
<a href="/vorlagen?partei={a.kuerzel}"
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium hover:opacity-80"
style="background-color: {a.farbe || '#6b7280'}20; color: {a.farbe || '#6b7280'}; border: 1px solid {a.farbe || '#6b7280'}40;"
onclick={(e) => e.stopPropagation()}>
{a.kuerzel}
</a>
{/each}
</div>
{:else}
<span class="text-gray-400 text-xs"></span>
{/if}
</td>
<td class="px-4 py-3 text-sm text-gray-600 capitalize">{v.typ || '-'}</td> <td class="px-4 py-3 text-sm text-gray-600 capitalize">{v.typ || '-'}</td>
<td class="px-4 py-3 text-sm text-gray-500">{formatDate(v.datum_eingang)}</td> <td class="px-4 py-3 text-sm text-gray-500">{formatDate(v.datum_eingang)}</td>
</tr> </tr>

View File

@ -1,58 +1,17 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { fetchVorlage, fetchKette, reevalVorlage, fetchJobStatus, type VorlageDetail } from '$lib/api'; import { fetchVorlage, type VorlageDetail } from '$lib/api';
import { typLabel, formatDate } from '$lib/status'; import { typLabel, formatDate } from '$lib/status';
let vorlage: VorlageDetail | null = $state(null); let vorlage: VorlageDetail | null = $state(null);
let ketteInfo: { status: string; begruendung: string | null; id: number } | null = $state(null);
let error: string | null = $state(null); let error: string | null = $state(null);
let showVolltext = $state(false); let showVolltext = $state(false);
let showReeval = $state(false);
let reevalAnmerkung = $state('');
let reevalStatus = $state<'idle' | 'running' | 'done' | 'error'>('idle');
let reevalError = $state('');
async function triggerReeval() {
if (!vorlage) return;
reevalStatus = 'running';
reevalError = '';
try {
const { job_id } = await reevalVorlage(vorlage.id, reevalAnmerkung);
// Poll for completion
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 3000));
const status = await fetchJobStatus(job_id);
if (status.status === 'done') {
reevalStatus = 'done';
// Reload vorlage
vorlage = await fetchVorlage(vorlage!.id);
showReeval = false;
reevalAnmerkung = '';
return;
}
if (status.status === 'error') {
reevalStatus = 'error';
reevalError = status.error || 'Unbekannter Fehler';
return;
}
}
reevalStatus = 'error';
reevalError = 'Timeout nach 3 Minuten';
} catch (e) {
reevalStatus = 'error';
reevalError = e instanceof Error ? e.message : 'Fehler';
}
}
onMount(async () => { onMount(async () => {
try { try {
const id = parseInt($page.params.id); const id = parseInt($page.params.id);
vorlage = await fetchVorlage(id); vorlage = await fetchVorlage(id);
if (vorlage?.kette_id) {
const kette = await fetchKette(vorlage.kette_id);
ketteInfo = { status: kette.status, begruendung: kette.begruendung, id: kette.id };
}
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Fehler'; error = e instanceof Error ? e.message : 'Fehler';
} }
@ -176,103 +135,6 @@
</div> </div>
{/if} {/if}
<!-- Ketten-Status & Klassifikation -->
{#if ketteInfo}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
<span>🏷️</span> Klassifikation
</h2>
<div class="flex items-center gap-3 mb-3">
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-semibold
{ketteInfo.status === 'umgesetzt' ? 'bg-green-100 text-green-800' :
ketteInfo.status === 'beschlossen' ? 'bg-blue-100 text-blue-800' :
ketteInfo.status === 'teilweise_umgesetzt' ? 'bg-amber-100 text-amber-800' :
ketteInfo.status === 'abgelehnt' || ketteInfo.status === 'abgewiegelt' ? 'bg-red-100 text-red-800' :
ketteInfo.status === 'versandet' ? 'bg-gray-100 text-gray-800' :
ketteInfo.status === 'in_beratung' ? 'bg-purple-100 text-purple-800' :
'bg-gray-100 text-gray-700'}">
{ketteInfo.status.replace(/_/g, ' ')}
</span>
<a href="/ketten/{ketteInfo.id}" class="text-sm text-green-600 hover:underline">→ Kette anzeigen</a>
</div>
{#if ketteInfo.begruendung}
<p class="text-sm text-gray-700 leading-relaxed">{ketteInfo.begruendung}</p>
{/if}
</div>
{/if}
<!-- Umsetzungsbewertung -->
{#if vorlage.umsetzungsbewertungen?.length}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2">
<span>📊</span> Umsetzungsbewertung
</h2>
<div class="space-y-4">
{#each vorlage.umsetzungsbewertungen as ub, i}
<div class="p-4 rounded-lg border {ub.score >= 0.7 ? 'border-green-200 bg-green-50' : ub.score >= 0.4 ? 'border-amber-200 bg-amber-50' : 'border-red-200 bg-red-50'}">
<div class="flex items-center gap-3 mb-2">
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-full flex items-center justify-center text-lg font-bold
{ub.score >= 0.7 ? 'bg-green-200 text-green-800' : ub.score >= 0.4 ? 'bg-amber-200 text-amber-800' : 'bg-red-200 text-red-800'}">
{Math.round((ub.score || 0) * 100)}%
</div>
</div>
<div>
<span class="text-sm font-medium {ub.score >= 0.7 ? 'text-green-800' : ub.score >= 0.4 ? 'text-amber-800' : 'text-red-800'}">
{ub.score >= 0.7 ? 'Weitgehend umgesetzt' : ub.score >= 0.4 ? 'Teilweise umgesetzt' : 'Kaum umgesetzt'}
</span>
{#if vorlage.umsetzungsbewertungen.length > 1}
<span class="text-xs text-gray-500 ml-2">Bewertung {i + 1}/{vorlage.umsetzungsbewertungen.length}</span>
{/if}
</div>
</div>
{#if ub.begruendung}
<p class="text-sm text-gray-700 leading-relaxed">{ub.begruendung}</p>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- Neu bewerten -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
{#if !showReeval}
<button onclick={() => showReeval = true}
class="text-sm text-green-600 hover:text-green-800 font-medium flex items-center gap-1.5">
<span>🔄</span> Neu bewerten lassen
</button>
{:else}
<h3 class="text-sm font-semibold text-gray-900 mb-2">KI-Neubewertung anstoßen</h3>
<textarea bind:value={reevalAnmerkung} placeholder="Anmerkungen für die KI (optional) z.B. 'Bitte den Beschlusstext genauer auswerten' oder 'Antrag wurde im Ausschuss XY besprochen'"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm mb-3 h-20 resize-y focus:ring-2 focus:ring-green-500 focus:border-green-500"
disabled={reevalStatus === 'running'}></textarea>
<div class="flex gap-2 items-center">
<button onclick={triggerReeval} disabled={reevalStatus === 'running'}
class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-wait transition-colors">
{#if reevalStatus === 'running'}
<span class="inline-flex items-center gap-2">
<span class="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></span>
KI bewertet…
</span>
{:else}
Bewertung starten
{/if}
</button>
{#if reevalStatus !== 'running'}
<button onclick={() => { showReeval = false; reevalStatus = 'idle'; }}
class="text-sm text-gray-500 hover:text-gray-700">Abbrechen</button>
{/if}
</div>
{#if reevalStatus === 'done'}
<p class="mt-2 text-sm text-green-700 font-medium">✅ Bewertung aktualisiert!</p>
{/if}
{#if reevalStatus === 'error'}
<p class="mt-2 text-sm text-red-600">{reevalError}</p>
{/if}
{/if}
</div>
<!-- Volltext --> <!-- Volltext -->
{#if vorlage.volltext_clean} {#if vorlage.volltext_clean}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">

View File

@ -1,78 +0,0 @@
#!/bin/bash
# Deploy DB zum VServer
# Usage: ./scripts/deploy-db.sh [--dry-run]
set -e
LOCAL_DB="data/tracker_remote.db"
REMOTE_DB="/opt/antragstracker/data/tracker.db"
REMOTE_HOST="vserver"
DRY_RUN=false
if [ "$1" = "--dry-run" ]; then
DRY_RUN=true
echo "🔍 DRY RUN — keine Änderungen"
echo ""
fi
cd "$(dirname "$0")/.."
echo "=== DB Deploy ==="
echo "Lokale DB: $(du -sh $LOCAL_DB | cut -f1)"
# 0. Schema-Validierung
echo "0. Schema prüfen..."
EXPECTED_TABLES="vorlagen beratungen ketten ketten_glieder ki_bewertungen parteien antragsteller gremien orte referenzen"
MISSING=""
for tbl in $EXPECTED_TABLES; do
EXISTS=$(sqlite3 "$LOCAL_DB" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$tbl'" 2>/dev/null)
if [ "$EXISTS" != "1" ]; then
MISSING="$MISSING $tbl"
fi
done
if [ -n "$MISSING" ]; then
echo "❌ FEHLER: Fehlende Tabellen:$MISSING"
exit 1
fi
echo " ✓ Alle ${#EXPECTED_TABLES[@]} Tabellen vorhanden"
# Stats
echo ""
echo " Vorlagen: $(sqlite3 "$LOCAL_DB" 'SELECT COUNT(*) FROM vorlagen')"
echo " Ketten: $(sqlite3 "$LOCAL_DB" 'SELECT COUNT(*) FROM ketten')"
echo " KI-Bew.: $(sqlite3 "$LOCAL_DB" 'SELECT COUNT(*) FROM ki_bewertungen')"
echo " Matches: $(sqlite3 "$LOCAL_DB" "SELECT COUNT(*) FROM ki_bewertungen WHERE typ='umsetzung_match'")"
echo " Orte: $(sqlite3 "$LOCAL_DB" 'SELECT COUNT(*) FROM orte WHERE lat IS NOT NULL')"
echo ""
# Flush WAL journal
echo " Flushing WAL..."
sqlite3 "$LOCAL_DB" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null
echo ""
if $DRY_RUN; then
echo "🔍 Würde hochladen: $(du -sh $LOCAL_DB | cut -f1) nach $REMOTE_HOST:$REMOTE_DB"
echo "🔍 Prod-DB Größe: $(ssh $REMOTE_HOST "du -sh $REMOTE_DB 2>/dev/null | cut -f1" || echo "N/A")"
echo ""
echo "DRY RUN fertig. Ohne --dry-run ausführen zum Deployen."
exit 0
fi
# 1. Backup auf VServer
BACKUP_NAME="${REMOTE_DB}.bak-$(date +%Y%m%d-%H%M)"
echo "1. Backup Prod-DB..."
ssh $REMOTE_HOST "cp $REMOTE_DB $BACKUP_NAME"
echo "$BACKUP_NAME"
# 2. Upload
echo "2. Upload..."
scp "$LOCAL_DB" "$REMOTE_HOST:$REMOTE_DB"
# 3. Restart
echo "3. Restart Backend..."
ssh $REMOTE_HOST 'cd /opt/antragstracker && docker compose restart antragstracker' 2>&1 | grep -v "level=warning"
echo ""
echo "✅ Deploy fertig"
echo "Backup: $BACKUP_NAME"
echo "Rollback: ssh $REMOTE_HOST 'cp $BACKUP_NAME $REMOTE_DB && cd /opt/antragstracker && docker compose restart antragstracker'"

View File

@ -1,55 +0,0 @@
#!/usr/bin/env python3
"""Fix fehlende KI-Zusammenfassungen mit Claude Haiku."""
import json
import os
import sqlite3
import time
from pathlib import Path
import httpx
DB = Path(__file__).resolve().parent.parent / "data" / "tracker_remote.db"
API_KEY = os.popen("security find-generic-password -s anthropic-api -w 2>/dev/null").read().strip()
PROMPT = """Fasse diese kommunalpolitische Vorlage aus Hagen in 2-3 Sätzen zusammen.
Fokus: Was wird gefordert/vorgeschlagen? Von wem? Was ist das Ziel?
Vorlage {az}:
{text}"""
def summarize(az, text):
resp = httpx.post("https://api.anthropic.com/v1/messages",
headers={"x-api-key": API_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json"},
json={"model": "claude-haiku-4-5-20241022", "max_tokens": 300, "messages": [
{"role": "user", "content": PROMPT.format(az=az, text=text[:8000])}
]}, timeout=30)
if resp.status_code == 200:
return resp.json()["content"][0]["text"]
print(f" ERROR {resp.status_code}: {resp.text[:200]}")
return None
conn = sqlite3.connect(str(DB))
conn.row_factory = sqlite3.Row
missing = conn.execute("""
SELECT v.id, v.aktenzeichen, v.volltext_clean
FROM vorlagen v
WHERE v.volltext_clean IS NOT NULL AND v.volltext_clean != ''
AND v.id NOT IN (SELECT vorlage_id FROM ki_bewertungen WHERE typ='zusammenfassung')
""").fetchall()
print(f"Fehlende Zusammenfassungen: {len(missing)}")
for row in missing:
print(f" {row['aktenzeichen']}...", end=" ")
summary = summarize(row["aktenzeichen"], row["volltext_clean"])
if summary:
conn.execute("""INSERT INTO ki_bewertungen (vorlage_id, typ, score, begruendung, modell, prompt_version)
VALUES (?, 'zusammenfassung', 0, ?, 'claude-haiku-4-5', 'v2-fix')""",
(row["id"], summary))
conn.commit()
print(f"{summary[:60]}")
else:
print("")
time.sleep(0.5)
print(f"\nFertig.")
conn.close()

View File

@ -38,7 +38,7 @@ def get_db():
return conn return conn
def geocode_ort(client: httpx.Client, name: str) -> "tuple[float, float] | None": def geocode_ort(client: httpx.Client, name: str) -> tuple[float, float] | None:
"""Geocodiert einen Ort in Hagen.""" """Geocodiert einen Ort in Hagen."""
# Verschiedene Suchvarianten # Verschiedene Suchvarianten
queries = [ queries = [

View File

@ -1,93 +0,0 @@
#!/usr/bin/env python3
"""Geocodiert pending Orte via Nominatim (1 req/s)."""
import argparse
import sqlite3
import time
from pathlib import Path
from typing import Optional, Tuple
import httpx
DB = Path(__file__).resolve().parent.parent / "data" / "tracker_remote.db"
NOMINATIM = "https://nominatim.openstreetmap.org/search"
UA = "Antragstracker-Hagen/1.0 (tobias.roedel@econgood.org)"
HAGEN_BBOX = "7.35,51.30,7.65,51.45"
def geocode(client: httpx.Client, name: str) -> Optional[Tuple[float, float]]:
# Clean name: remove trailing "Hagen" to avoid duplication
clean = name.strip().rstrip(",").strip()
if clean.lower().endswith(" hagen"):
clean = clean[:-6].strip().rstrip(",").strip()
# Try multiple query variants, progressively less strict
queries = [
(f"{clean}, Hagen, Germany", True), # bounded to Hagen
(f"{clean}, Hagen, NRW", False), # unbounded fallback
(f"{name}, Germany", False), # original name
]
for q, bounded in queries:
try:
params = {"q": q, "format": "json", "limit": 1}
if bounded:
params["viewbox"] = HAGEN_BBOX
params["bounded"] = 1
r = client.get(NOMINATIM, params=params,
headers={"User-Agent": UA}, timeout=10)
if r.status_code == 200 and r.json():
d = r.json()[0]
lat, lon = float(d["lat"]), float(d["lon"])
# Sanity check: roughly in Hagen area
if 51.25 <= lat <= 51.50 and 7.30 <= lon <= 7.70:
return lat, lon
except Exception:
pass
time.sleep(3.0) # Conservative: Nominatim blocks aggressively
return None
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--limit", type=int, default=500)
args = parser.parse_args()
conn = sqlite3.connect(str(DB))
conn.row_factory = sqlite3.Row
pending = conn.execute(
"SELECT id, name FROM orte WHERE geocode_status='pending' ORDER BY vorlage_count DESC LIMIT ?",
(args.limit,)
).fetchall()
total_pending = conn.execute("SELECT COUNT(*) FROM orte WHERE geocode_status='pending'").fetchone()[0]
print(f"Pending: {len(pending)} (von {total_pending} total)")
success = 0
failed = 0
client = httpx.Client()
for i, row in enumerate(pending):
coords = geocode(client, row["name"])
if coords:
conn.execute("UPDATE orte SET lat=?, lon=?, geocode_status='success' WHERE id=?",
(coords[0], coords[1], row["id"]))
success += 1
if success % 20 == 0:
print(f" [{i+1}/{len(pending)}] ✓ {success} geocoded, ✗ {failed} failed")
else:
conn.execute("UPDATE orte SET geocode_status='failed' WHERE id=?", (row["id"],))
failed += 1
if (i + 1) % 50 == 0:
conn.commit()
conn.commit()
conn.close()
client.close()
total_geo = success # just this run
print(f"\n{success} | ✗ {failed} | Gesamt: {success + failed}")
if __name__ == "__main__":
main()

View File

@ -1,281 +0,0 @@
#!/usr/bin/env python3
"""
Ketten-Match: Vergleicht Ursprungsforderung mit Blatt-Antwort via Gemini Flash.
Bewertet ob Anträge tatsächlich erfüllt wurden.
Überwacht Free Tier Limits (5h-Fenster, pausiert bei 85%).
"""
import argparse
import json
import os
import re
import sqlite3
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from threading import Lock
from typing import Optional
import httpx
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DB_PATH = PROJECT_ROOT / "data" / "tracker_remote.db"
LOG_FILE = PROJECT_ROOT / "data" / "ketten_match.log"
# Gemini API
GEMINI_KEY = os.environ.get("GEMINI_API_KEY") or os.popen("security find-generic-password -s gemini-api -w 2>/dev/null").read().strip()
GEMINI_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={GEMINI_KEY}"
MODEL = "gemini-2.5-flash"
# Rate Limiting
MAX_REQUESTS_PER_5H = 1500
PAUSE_THRESHOLD = 0.85
WINDOW_SECONDS = 5 * 3600
PROMPT = """Du bist ein Analyst für kommunalpolitische Vorgänge in Hagen.
Vergleiche den URSPRÜNGLICHEN ANTRAG/ANFRAGE mit der ANTWORT/BESCHLUSS.
Bewerte ob die ursprüngliche Forderung tatsächlich erfüllt wurde.
=== URSPRÜNGLICHE FORDERUNG ===
Aktenzeichen: {az_ursprung}
Typ: {typ_ursprung}
{ki_zusammenfassung}
Volltext (Auszug):
{volltext_ursprung}
=== ANTWORT / BESCHLUSS ===
Aktenzeichen: {az_blatt}
Typ: {typ_blatt}
Beschlussart: {beschlussart}
Beschlusstext:
{beschlusstext}
Wortprotokoll:
{wortprotokoll}
---
Bewerte NUR als JSON:
{{
"score": <0.0-1.0>,
"bewertung": "erfuellt|teilweise|abgewiegelt|nebelkerze|vertagt|unklar",
"begruendung": "1-2 Sätze warum",
"kernpunkt_erfuellt": true/false,
"details": "Was konkret beschlossen/abgelehnt wurde"
}}
Bewertungsskala:
- 1.0: Forderung vollständig erfüllt, konkreter Beschluss
- 0.7-0.9: Weitgehend erfüllt, kleine Abweichungen
- 0.4-0.6: Teilweise erfüllt oder auf den Weg gebracht
- 0.2-0.3: Abgewiegelt Verwaltung weicht aus, kündigt nur "Prüfung" an
- 0.0-0.1: Nebelkerze Thema gewechselt oder komplett ignoriert"""
def _score_to_bewertung(score: float) -> str:
if score >= 0.8: return "erfuellt"
if score >= 0.5: return "teilweise"
if score >= 0.2: return "abgewiegelt"
return "nebelkerze"
db_lock = Lock()
log_lock = Lock()
stats = {"success": 0, "failed": 0, "throttled": 0, "requests": 0, "window_start": time.time()}
def log(msg: str):
ts = time.strftime("%H:%M:%S")
line = f"[{ts}] {msg}"
print(line)
with log_lock:
with open(LOG_FILE, "a") as f:
f.write(line + "\n")
def get_db():
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def check_rate_limit():
elapsed = time.time() - stats["window_start"]
if elapsed > WINDOW_SECONDS:
stats["requests"] = 0
stats["window_start"] = time.time()
return
usage_pct = stats["requests"] / MAX_REQUESTS_PER_5H
if usage_pct >= PAUSE_THRESHOLD:
remaining = WINDOW_SECONDS - elapsed
log(f"⏸️ 85% Limit ({stats['requests']}/{MAX_REQUESTS_PER_5H}). Pause {remaining/60:.0f} Min.")
time.sleep(remaining + 10)
stats["requests"] = 0
stats["window_start"] = time.time()
log("▶️ Weiter")
def call_gemini(prompt: str, max_retries: int = 5) -> Optional[dict]:
for attempt in range(max_retries):
check_rate_limit()
stats["requests"] += 1
try:
resp = httpx.post(GEMINI_URL, json={
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {
"temperature": 0.1,
"responseMimeType": "application/json",
"maxOutputTokens": 2048,
"thinkingConfig": {"thinkingBudget": 0},
}
}, timeout=60)
if resp.status_code == 429:
stats["throttled"] += 1
time.sleep(min(10, attempt + 2))
continue
if resp.status_code != 200:
time.sleep(1)
continue
text = resp.json()["candidates"][0]["content"]["parts"][0]["text"].strip()
# Clean markdown code fences
if text.startswith("```"):
text = re.sub(r'^```\w*\n?', '', text)
text = re.sub(r'\n?```$', '', text)
# Try full JSON parse (multiple attempts)
for parse_text in [text, text.replace('\n', ' '), re.sub(r'[\x00-\x1f]', ' ', text)]:
try:
parsed = json.loads(parse_text)
if isinstance(parsed, dict) and "score" in parsed:
parsed["score"] = float(parsed["score"])
if not parsed.get("bewertung"):
parsed["bewertung"] = _score_to_bewertung(parsed["score"])
if not parsed.get("begruendung"):
parsed["begruendung"] = parsed.get("details", "Keine Begründung")
return parsed
except (json.JSONDecodeError, ValueError, TypeError):
continue
# Regex fallback — extract all fields
score_m = re.search(r'"score"\s*:\s*([\d.]+)', text)
bew_m = re.search(r'"bewertung"\s*:\s*"([^"]+)"', text)
grund_m = re.search(r'"begruendung"\s*:\s*"([^"]*(?:\\.[^"]*)*)"', text)
detail_m = re.search(r'"details"\s*:\s*"([^"]*(?:\\.[^"]*)*)"', text)
kern_m = re.search(r'"kernpunkt_erfuellt"\s*:\s*(true|false)', text)
if score_m:
score = float(score_m.group(1))
return {
"score": score,
"bewertung": bew_m.group(1) if bew_m else _score_to_bewertung(score),
"begruendung": grund_m.group(1) if grund_m else (detail_m.group(1) if detail_m else "Regex-Fallback"),
"kernpunkt_erfuellt": kern_m.group(1) == "true" if kern_m else score >= 0.5,
"details": detail_m.group(1) if detail_m else "",
}
time.sleep(0.5)
except Exception:
time.sleep(0.5)
return None
def process_kette(kette_id: int, data: dict) -> bool:
prompt = PROMPT.format(**data)
result = call_gemini(prompt)
if not result:
stats["failed"] += 1
return False
score = result.get("score", 0)
bewertung = result.get("bewertung", "unklar")
with db_lock:
conn = get_db()
conn.execute("""
INSERT INTO ki_bewertungen (vorlage_id, typ, score, begruendung, anmerkungen, modell, prompt_version)
VALUES (?, 'umsetzung_match', ?, ?, ?, ?, 'v1-ketten-match')
""", (data["ursprung_id"], score, result.get("begruendung", ""), json.dumps(result, ensure_ascii=False), MODEL))
conn.execute("UPDATE vorlagen SET ki_status = 'matched' WHERE id = ?", (data["ursprung_id"],))
conn.commit()
conn.close()
stats["success"] += 1
log(f"{data['az_ursprung']}{bewertung} ({score:.1f}): {result.get('begruendung', '')[:60]}")
return True
def get_pending(conn, limit):
return conn.execute("""
SELECT k.id as kette_id, v_u.id as ursprung_id,
v_u.aktenzeichen as az_ursprung, v_u.typ as typ_ursprung,
v_u.volltext_clean as volltext_ursprung,
v_b.aktenzeichen as az_blatt, v_b.typ as typ_blatt,
b.beschlussart, b.beschlusstext, b.wortprotokoll,
ki.begruendung as ki_zusammenfassung
FROM ketten k
JOIN ketten_glieder kg_u ON kg_u.kette_id = k.id AND kg_u.position = 0
JOIN vorlagen v_u ON kg_u.vorlage_id = v_u.id
JOIN ketten_glieder kg_b ON kg_b.kette_id = k.id
AND kg_b.position = (SELECT MAX(position) FROM ketten_glieder WHERE kette_id = k.id)
JOIN vorlagen v_b ON kg_b.vorlage_id = v_b.id
LEFT JOIN beratungen b ON b.vorlage_id = v_b.id AND b.beschlusstext IS NOT NULL
LEFT JOIN ki_bewertungen ki ON ki.vorlage_id = v_u.id AND ki.typ = 'zusammenfassung'
LEFT JOIN ki_bewertungen ki_match ON ki_match.vorlage_id = v_u.id AND ki_match.typ = 'umsetzung_match'
WHERE b.beschlusstext IS NOT NULL
AND v_u.volltext_clean IS NOT NULL AND v_u.volltext_clean != ''
AND ki_match.id IS NULL
GROUP BY k.id
ORDER BY v_u.datum_eingang DESC
LIMIT ?
""", (limit,)).fetchall()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--batch-size", type=int, default=100)
parser.add_argument("--workers", type=int, default=3)
args = parser.parse_args()
log(f"=== Ketten-Match ({MODEL}) ===")
log(f"Workers: {args.workers}, Batch: {args.batch_size}")
if not GEMINI_KEY:
log("FEHLER: Kein API-Key!")
return 1
conn = get_db()
rows = get_pending(conn, args.batch_size)
conn.close()
pending = [{
"kette_id": r["kette_id"], "ursprung_id": r["ursprung_id"],
"az_ursprung": r["az_ursprung"], "typ_ursprung": r["typ_ursprung"] or "",
"volltext_ursprung": (r["volltext_ursprung"] or "")[:5000],
"az_blatt": r["az_blatt"] or "", "typ_blatt": r["typ_blatt"] or "",
"beschlussart": r["beschlussart"] or "",
"beschlusstext": (r["beschlusstext"] or "")[:2000],
"wortprotokoll": (r["wortprotokoll"] or "")[:2000],
"ki_zusammenfassung": (r["ki_zusammenfassung"] or "")[:500],
} for r in rows]
log(f"Zu verarbeiten: {len(pending)}")
if not pending:
log("Alle fertig!")
return 0
start = time.time()
with ThreadPoolExecutor(max_workers=args.workers) as executor:
futures = {executor.submit(process_kette, p["kette_id"], p): p["az_ursprung"] for p in pending}
for f in as_completed(futures):
try: f.result()
except Exception as e: log(f"{futures[f]}: {e}")
elapsed = time.time() - start
log(f"\n=== Batch fertig ===")
log(f"{stats['success']} | ✗ {stats['failed']} | ⏳ {stats['throttled']} throttled")
log(f"Dauer: {elapsed:.0f}s | {stats['success']/max(elapsed,1):.2f} docs/sec")
log(f"5h-Fenster: {stats['requests']}/{MAX_REQUESTS_PER_5H} ({stats['requests']*100/MAX_REQUESTS_PER_5H:.0f}%)")
return 0 if not pending or stats["success"] >= len(pending) else 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""
Repariert kaputte ketten_match Einträge:
- begruendung enthält rohes JSON statt Klartext
- bewertung ist 'unklar' obwohl Score eindeutig ist
Löscht kaputte Einträge run_ketten_match.sh matched sie erneut mit gefixtem Script.
"""
import sqlite3
from pathlib import Path
DB = Path(__file__).resolve().parent.parent / "data" / "tracker_remote.db"
conn = sqlite3.connect(str(DB))
conn.row_factory = sqlite3.Row
# Zähle kaputte
broken = conn.execute("""
SELECT COUNT(*) as n FROM ki_bewertungen
WHERE typ='umsetzung_match' AND begruendung LIKE '{%'
""").fetchone()["n"]
print(f"Kaputte Einträge: {broken}")
if broken > 0:
# Lösche alle kaputten → werden vom Runner erneut bewertet
conn.execute("""
DELETE FROM ki_bewertungen
WHERE typ='umsetzung_match' AND begruendung LIKE '{%'
""")
# Setze ki_status zurück für betroffene Vorlagen
conn.execute("""
UPDATE vorlagen SET ki_status = NULL
WHERE id IN (
SELECT vorlage_id FROM ki_bewertungen WHERE typ='umsetzung_match'
) OR (ki_status = 'matched' AND id NOT IN (
SELECT vorlage_id FROM ki_bewertungen WHERE typ='umsetzung_match'
))
""")
conn.commit()
remaining = conn.execute("""
SELECT COUNT(*) as n FROM ki_bewertungen WHERE typ='umsetzung_match'
""").fetchone()["n"]
print(f"Gelöscht: {broken}")
print(f"Verbleibend (intakt): {remaining}")
print(f"→ Runner wird {broken} erneut matchen")
conn.close()

View File

@ -1,56 +0,0 @@
#!/bin/bash
# Chunked Ketten-Match Runner
# Läuft in Batches à 200, pausiert 30s zwischen Batches
# Gemini Free Tier: 1500 RPM / 1M TPM — wir bleiben drunter
# set -e # removed: single batch failure should not kill runner
cd "$(dirname "$0")/.."
BATCH_SIZE=200
WORKERS=3
PAUSE=30
MAX_BATCHES=50 # Safety cap
echo "=== Chunked Ketten-Match Runner ==="
echo "Batch: $BATCH_SIZE | Workers: $WORKERS | Pause: ${PAUSE}s"
for i in $(seq 1 $MAX_BATCHES); do
echo ""
echo "--- Batch $i/$MAX_BATCHES ($(date +%H:%M:%S)) ---"
# Check ob noch was offen ist
REMAINING=$(sqlite3 data/tracker_remote.db "
SELECT COUNT(*) FROM ketten k
JOIN ketten_glieder kg_u ON kg_u.kette_id = k.id AND kg_u.position = 0
JOIN vorlagen v_u ON kg_u.vorlage_id = v_u.id
JOIN ketten_glieder kg_b ON kg_b.kette_id = k.id
AND kg_b.position = (SELECT MAX(position) FROM ketten_glieder WHERE kette_id = k.id)
JOIN vorlagen v_b ON kg_b.vorlage_id = v_b.id
LEFT JOIN beratungen b ON b.vorlage_id = v_b.id AND b.beschlusstext IS NOT NULL
LEFT JOIN ki_bewertungen ki_match ON ki_match.vorlage_id = v_u.id AND ki_match.typ = 'umsetzung_match'
WHERE b.beschlusstext IS NOT NULL
AND v_u.volltext_clean IS NOT NULL AND v_u.volltext_clean != ''
AND ki_match.id IS NULL
")
echo "Noch offen: $REMAINING"
if [ "$REMAINING" -eq 0 ]; then
echo "✅ Alle Ketten bewertet!"
break
fi
python3 scripts/ketten_match.py --batch-size $BATCH_SIZE --workers $WORKERS
DONE=$(sqlite3 data/tracker_remote.db "SELECT COUNT(*) FROM ki_bewertungen WHERE typ='umsetzung_match'")
echo "Gesamt bewertet: $DONE"
if [ "$i" -lt "$MAX_BATCHES" ] && [ "$REMAINING" -gt "$BATCH_SIZE" ]; then
echo "Pause ${PAUSE}s..."
sleep $PAUSE
fi
done
echo ""
echo "=== Runner beendet ==="
TOTAL=$(sqlite3 data/tracker_remote.db "SELECT COUNT(*) FROM ki_bewertungen WHERE typ='umsetzung_match'")
echo "Gesamt bewertet: $TOTAL"

View File

@ -1 +0,0 @@
export const env={}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{a5 as l,a6 as o,a7 as u,a8 as n,a9 as d,B as m,F as p,a4 as _,aa as v,ab as b}from"./Cjw4vZKn.js";class w{anchor;#t=new Map;#s=new Map;#e=new Map;#i=new Set;#a=!0;constructor(t,s=!0){this.anchor=t,this.#a=s}#f=t=>{if(this.#t.has(t)){var s=this.#t.get(t),e=this.#s.get(s);if(e)l(e),this.#i.delete(s);else{var a=this.#e.get(s);a&&(this.#s.set(s,a.effect),this.#e.delete(s),a.fragment.lastChild.remove(),this.anchor.before(a.fragment),e=a.effect)}for(const[i,f]of this.#t){if(this.#t.delete(i),i===t)break;const r=this.#e.get(f);r&&(o(r.effect),this.#e.delete(f))}for(const[i,f]of this.#s){if(i===s||this.#i.has(i))continue;const r=()=>{if(Array.from(this.#t.values()).includes(i)){var c=document.createDocumentFragment();v(f,c),c.append(n()),this.#e.set(i,{effect:f,fragment:c})}else o(f);this.#i.delete(i),this.#s.delete(i)};this.#a||!e?(this.#i.add(i),u(f,r,!1)):r()}}};#r=t=>{this.#t.delete(t);const s=Array.from(this.#t.values());for(const[e,a]of this.#e)s.includes(e)||(o(a.effect),this.#e.delete(e))};ensure(t,s){var e=m,a=b();if(s&&!this.#s.has(t)&&!this.#e.has(t))if(a){var i=document.createDocumentFragment(),f=n();i.append(f),this.#e.set(t,{effect:d(()=>s(f)),fragment:i})}else this.#s.set(t,d(()=>s(this.anchor)));if(this.#t.set(e,t),a){for(const[r,h]of this.#s)r===t?e.unskip_effect(h):e.skip_effect(h);for(const[r,h]of this.#e)r===t?e.unskip_effect(h.effect):e.skip_effect(h.effect);e.oncommit(this.#f),e.ondiscard(this.#r)}else p&&(this.anchor=_),this.#f(e)}}export{w as B};

View File

@ -1 +0,0 @@
import{i as A,j as L,P as D,g as P,p as T,a as b,k as B,l as Y,D as x,m as M,o as N,q as U,v as h,w as q,x as w,y as z,z as $,S as j,L as y}from"./DAfY0XTB.js";import{c as C}from"./splFp8Bu.js";function F(r,a,t,s){var f=!U||(t&h)!==0,v=(t&M)!==0,E=(t&$)!==0,n=s,S=!0,g=()=>(S&&(S=!1,n=E?N(s):s),n);let u;if(v){var O=j in r||y in r;u=A(r,a)?.set??(O&&a in r?e=>r[a]=e:void 0)}var _,I=!1;v?[_,I]=C(()=>r[a]):_=r[a],_===void 0&&s!==void 0&&(_=g(),u&&(f&&L(),u(_)));var i;if(f?i=()=>{var e=r[a];return e===void 0?g():(S=!0,e)}:i=()=>{var e=r[a];return e!==void 0&&(n=void 0),e===void 0?n:e},f&&(t&D)===0)return i;if(u){var R=r.$$legacy;return(function(e,l){return arguments.length>0?((!f||!l||R||I)&&u(l?i():e),e):i()})}var c=!1,d=((t&q)!==0?w:z)(()=>(c=!1,i()));v&&P(d);var m=Y;return(function(e,l){if(arguments.length>0){const o=l?P(d):f&&v?T(e):e;return b(d,o),c=!0,n!==void 0&&(n=o),e}return B&&c||(m.f&x)!==0?d.v:P(d)})}export{F as p};

View File

@ -1,2 +0,0 @@
const e=[...`
\r\f \v\uFEFF`];function o(t,f,u){var n=t==null?"":""+t;if(f&&(n=n?n+" "+f:f),u){for(var s of Object.keys(u))if(u[s])n=n?n+" "+s:s;else if(n.length)for(var i=s.length,l=0;(l=n.indexOf(s,l))>=0;){var r=l+i;(l===0||e.includes(n[l-1]))&&(r===n.length||e.includes(n[r]))?n=(l===0?"":n.substring(0,l))+n.substring(r+1):l=r}}return n===""?null:n}function c(t,f){return t==null?null:String(t)}export{o as a,c as t};

View File

@ -1 +0,0 @@
import{A as m,B as v,C as _,o as b,E as i,F as y}from"./DAfY0XTB.js";function k(e,l,u=l){var s=new WeakSet;m(e,"input",async r=>{var a=r?e.defaultValue:e.value;if(a=o(e)?t(a):a,u(a),v!==null&&s.add(v),await _(),a!==(a=l())){var d=e.selectionStart,f=e.selectionEnd,n=e.value.length;if(e.value=a??"",f!==null){var c=e.value.length;d===f&&f===n&&c>n?(e.selectionStart=c,e.selectionEnd=c):(e.selectionStart=d,e.selectionEnd=Math.min(f,c))}}}),(y&&e.defaultValue!==e.value||b(l)==null&&e.value)&&(u(o(e)?t(e.value):e.value),v!==null&&s.add(v)),i(()=>{var r=l();if(e===document.activeElement){var a=v;if(s.has(a))return}o(e)&&r===t(e.value)||e.type==="date"&&!r&&!e.value||r!==e.value&&(e.value=r??"")})}function o(e){var l=e.type;return l==="number"||l==="range"}function t(e){return e===""?null:+e}export{k as b};

View File

@ -1 +0,0 @@
import{ai as v,a8 as d,ac as u,aF as T,l,aG as p,aH as h,F as i,a4 as s,aI as E,Z as y,aJ as g,a2 as w,aK as N}from"./Cjw4vZKn.js";const M=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function x(t){return M?.createHTML(t)??t}function A(t){var r=v("template");return r.innerHTML=x(t.replaceAll("<!>","<!---->")),r.content}function a(t,r){var e=l;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function I(t,r){var e=(r&p)!==0,f=(r&h)!==0,n,_=!t.startsWith("<!>");return()=>{if(i)return a(s,null),s;n===void 0&&(n=A(_?t:"<!>"+t),e||(n=u(n)));var o=f||T?document.importNode(n,!0):n.cloneNode(!0);if(e){var c=u(o),m=o.lastChild;a(c,m)}else a(o,o);return o}}function O(t=""){if(!i){var r=d(t+"");return a(r,r),r}var e=s;return e.nodeType!==g?(e.before(e=d()),w(e)):N(e),a(e,e),e}function P(){if(i)return a(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=d();return t.append(r,e),a(r,e),t}function R(t,r){if(i){var e=l;((e.f&E)===0||e.nodes.end===null)&&(e.nodes.end=s),y();return}t!==null&&t.before(r)}const L="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(L);export{R as a,a as b,P as c,I as f,O as t};

View File

@ -1 +0,0 @@
import{Q as c,F as o,R as l,T as b,U as p,V as v,W as g,X as f,Y as m}from"./DAfY0XTB.js";import{B as y}from"./Duumi1XQ.js";function F(_,d,h=!1){var n;o&&(n=m,l());var s=new y(_),u=h?b:0;function t(a,r){if(o){var e=p(n);if(a!==parseInt(e.substring(1))){var i=v();g(i),s.anchor=i,f(!1),s.ensure(a,r),f(!0);return}}s.ensure(a,r)}c(()=>{var a=!1;d((r,e=0)=>{a=!0,t(e,r)}),a||t(-1,null)},u)}export{F as i};

View File

@ -1 +0,0 @@
import{a as y}from"./DVOkFnep.js";import{F as r}from"./DAfY0XTB.js";function a(t,e,f,i){var l=t.__style;if(r||l!==e){var s=y(e);(!r||s!==t.getAttribute("style"))&&(s==null?t.removeAttribute("style"):t.style.cssText=s),t.__style=e}return i}export{a as s};

View File

@ -1 +0,0 @@
import{a1 as z,Q as Z,ao as G,F as A,W as M,a5 as J,R as re,g as U,U as ae,ap as fe,V as X,X as O,Y as H,a7 as K,aq as ie,ar as P,B as le,as as C,a2 as L,at as se,a4 as ue,y as oe,H as te,au as q,av as ve,aw as de,O as ce,ax as Q,ay as pe,D as ge,Z as $,a0 as j,az as B,af as _e,aA as Ee,aB as he,aC as me,a3 as Te,_ as Ce,a8 as V,aD as we,aE as Ae}from"./DAfY0XTB.js";function Ie(e,a){return a}function Se(e,a,s){for(var u=[],t=a.length,r,i=a.length,p=0;p<t;p++){let E=a[p];j(E,()=>{if(r){if(r.pending.delete(E),r.done.add(E),r.pending.size===0){var v=e.outrogroups;Y(e,q(r.done)),v.delete(r),v.size===0&&(e.outrogroups=null)}}else i-=1},!1)}if(i===0){var l=u.length===0&&s!==null;if(l){var c=s,f=c.parentNode;me(f),f.append(c),e.items.clear()}Y(e,a,!l)}else r={pending:new Set(a),done:new Set},(e.outrogroups??=new Set).add(r)}function Y(e,a,s=!0){var u;if(e.pending.size>0){u=new Set;for(const i of e.pending.values())for(const p of i)u.add(e.items.get(p).e)}for(var t=0;t<a.length;t++){var r=a[t];if(u?.has(r)){r.f|=C;const i=document.createDocumentFragment();Te(r,i)}else Ce(a[t],s)}}var W;function Ne(e,a,s,u,t,r=null){var i=e,p=new Map,l=(a&G)!==0;if(l){var c=e;i=A?M(J(c)):c.appendChild(z())}A&&re();var f=null,E=oe(()=>{var d=s();return te(d)?d:d==null?[]:q(d)}),v,h=new Map,m=!0;function I(d){(S.effect.f&ge)===0&&(S.pending.delete(d),S.fallback=f,De(S,v,i,a,u),f!==null&&(v.length===0?(f.f&C)===0?$(f):(f.f^=C,k(f,null,i)):j(f,()=>{f=null})))}function n(d){S.pending.delete(d)}var o=Z(()=>{v=U(E);var d=v.length;let _=!1;if(A){var b=ae(i)===fe;b!==(d===0)&&(i=X(),M(i),O(!1),_=!0)}for(var D=new Set,g=le,N=ue(),R=0;R<d;R+=1){A&&H.nodeType===K&&H.data===ie&&(i=H,_=!0,O(!1));var T=v[R],y=u(T,R),w=m?null:p.get(y);w?(w.v&&P(w.v,T),w.i&&P(w.i,R),N&&g.unskip_effect(w.e)):(w=Re(p,m?i:W??=z(),T,y,R,t,a,s),m||(w.e.f|=C),p.set(y,w)),D.add(y)}if(d===0&&r&&!f&&(m?f=L(()=>r(i)):(f=L(()=>r(W??=z())),f.f|=C)),d>D.size&&se(),A&&d>0&&M(X()),!m)if(h.set(g,D),N){for(const[ee,ne]of p)D.has(ee)||g.skip_effect(ne.e);g.oncommit(I),g.ondiscard(n)}else I(g);_&&O(!0),U(E)}),S={effect:o,items:p,pending:h,outrogroups:null,fallback:f};m=!1,A&&(i=H)}function x(e){for(;e!==null&&(e.f&Ee)===0;)e=e.next;return e}function De(e,a,s,u,t){var r=(u&he)!==0,i=a.length,p=e.items,l=x(e.effect.first),c,f=null,E,v=[],h=[],m,I,n,o;if(r)for(o=0;o<i;o+=1)m=a[o],I=t(m,o),n=p.get(I).e,(n.f&C)===0&&(n.nodes?.a?.measure(),(E??=new Set).add(n));for(o=0;o<i;o+=1){if(m=a[o],I=t(m,o),n=p.get(I).e,e.outrogroups!==null)for(const T of e.outrogroups)T.pending.delete(n),T.done.delete(n);if((n.f&B)!==0&&($(n),r&&(n.nodes?.a?.unfix(),(E??=new Set).delete(n))),(n.f&C)!==0)if(n.f^=C,n===l)k(n,null,s);else{var S=f?f.next:l;n===e.effect.last&&(e.effect.last=n.prev),n.prev&&(n.prev.next=n.next),n.next&&(n.next.prev=n.prev),F(e,f,n),F(e,n,S),k(n,S,s),f=n,v=[],h=[],l=x(f.next);continue}if(n!==l){if(c!==void 0&&c.has(n)){if(v.length<h.length){var d=h[0],_;f=d.prev;var b=v[0],D=v[v.length-1];for(_=0;_<v.length;_+=1)k(v[_],d,s);for(_=0;_<h.length;_+=1)c.delete(h[_]);F(e,b.prev,D.next),F(e,f,b),F(e,D,d),l=d,f=D,o-=1,v=[],h=[]}else c.delete(n),k(n,l,s),F(e,n.prev,n.next),F(e,n,f===null?e.effect.first:f.next),F(e,f,n),f=n;continue}for(v=[],h=[];l!==null&&l!==n;)(c??=new Set).add(l),h.push(l),l=x(l.next);if(l===null)continue}(n.f&C)===0&&v.push(n),f=n,l=x(n.next)}if(e.outrogroups!==null){for(const T of e.outrogroups)T.pending.size===0&&(Y(e,q(T.done)),e.outrogroups?.delete(T));e.outrogroups.size===0&&(e.outrogroups=null)}if(l!==null||c!==void 0){var g=[];if(c!==void 0)for(n of c)(n.f&B)===0&&g.push(n);for(;l!==null;)(l.f&B)===0&&l!==e.fallback&&g.push(l),l=x(l.next);var N=g.length;if(N>0){var R=(u&G)!==0&&i===0?s:null;if(r){for(o=0;o<N;o+=1)g[o].nodes?.a?.measure();for(o=0;o<N;o+=1)g[o].nodes?.a?.fix()}Se(e,g,R)}}r&&_e(()=>{if(E!==void 0)for(n of E)n.nodes?.a?.apply()})}function Re(e,a,s,u,t,r,i,p){var l=(i&ve)!==0?(i&de)===0?ce(s,!1,!1):Q(s):null,c=(i&pe)!==0?Q(t):null;return{v:l,i:c,e:L(()=>(r(a,l??s,c??t,p),()=>{e.delete(u)}))}}function k(e,a,s){if(e.nodes)for(var u=e.nodes.start,t=e.nodes.end,r=a&&(a.f&C)===0?a.nodes.start:s;u!==null;){var i=V(u);if(r.before(u),u===t)return;u=i}}function F(e,a,s){a===null?e.effect.first=s:a.next=s,s===null?e.effect.last=a:s.prev=a}function xe(e,a){let s=null,u=A;var t;if(A){s=H;for(var r=J(document.head);r!==null&&(r.nodeType!==K||r.data!==e);)r=V(r);if(r===null)O(!1);else{var i=V(r);r.remove(),M(i)}}A||(t=document.head.appendChild(z()));try{Z(()=>a(t),we|Ae)}finally{u&&(O(!0),M(s))}}export{Ne as e,xe as h,Ie as i};

View File

@ -1 +0,0 @@
import{c as y,a as p,f as _}from"./Bkzsmr9I.js";import{d as j,f as S,h as B,c as i,r as n,b as c,t as v,g as r,u as I}from"./Cjw4vZKn.js";import{s as m}from"./DfJQ0EIT.js";import{i as q}from"./kjB3f-xG.js";import{s as w}from"./RVjQLo13.js";import{s as d}from"./CWOupeSg.js";import{p as z}from"./fSdafo1a.js";import{s as A}from"./utcFFRIM.js";var C=_('<a><span class="mr-1"> </span> </a>'),D=_('<span><span class="mr-1"> </span> </span>');function N(x,o){j(o,!0);let h=z(o,"linked",3,!1);const s=I(()=>A(o.status));var u=y(),b=S(u);{var g=e=>{var t=C(),a=i(t),f=i(a,!0);n(a);var l=c(a);n(t),v(()=>{w(t,"href",`/ketten?status=${o.status??""}`),d(t,1,`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${r(s).color??""} hover:opacity-80 transition-opacity`),m(f,r(s).emoji),m(l,` ${r(s).label??""}`)}),p(e,t)},k=e=>{var t=D(),a=i(t),f=i(a,!0);n(a);var l=c(a);n(t),v(()=>{d(t,1,`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${r(s).color??""}`),m(f,r(s).emoji),m(l,` ${r(s).label??""}`)}),p(e,t)};q(b,e=>{h()&&o.status?e(g):e(k,-1)})}p(x,u),B()}export{N as S};

View File

@ -1 +0,0 @@
import{A as m,B as v,C as _,o as b,E as i,F as y}from"./Cjw4vZKn.js";function k(e,l,u=l){var s=new WeakSet;m(e,"input",async r=>{var a=r?e.defaultValue:e.value;if(a=o(e)?t(a):a,u(a),v!==null&&s.add(v),await _(),a!==(a=l())){var d=e.selectionStart,f=e.selectionEnd,n=e.value.length;if(e.value=a??"",f!==null){var c=e.value.length;d===f&&f===n&&c>n?(e.selectionStart=c,e.selectionEnd=c):(e.selectionStart=d,e.selectionEnd=Math.min(f,c))}}}),(y&&e.defaultValue!==e.value||b(l)==null&&e.value)&&(u(o(e)?t(e.value):e.value),v!==null&&s.add(v)),i(()=>{var r=l();if(e===document.activeElement){var a=v;if(s.has(a))return}o(e)&&r===t(e.value)||e.type==="date"&&!r&&!e.value||r!==e.value&&(e.value=r??"")})}function o(e){var l=e.type;return l==="number"||l==="range"}function t(e){return e===""?null:+e}export{k as b};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{a as b}from"./B08B5jt4.js";import{F as g}from"./Cjw4vZKn.js";function A(i,h,f,N,t,r){var o=i.__className;if(g||o!==f||o===void 0){var a=b(f,N,r);(!g||a!==i.getAttribute("class"))&&(a==null?i.removeAttribute("class"):i.className=a),i.__className=f}else if(r&&t!==r)for(var l in r){var u=!!r[l];(t==null||u!==!!t[l])&&i.classList.toggle(l,u)}return r}export{A as s};

View File

@ -1 +0,0 @@
const a=typeof window<"u"&&window.location.port==="5173"?`http://${window.location.hostname}:8099/api`:"/api";async function e(t){const n=await fetch(`${a}${t}`);if(!n.ok)throw new Error(`API error: ${n.status}`);return n.json()}const r=t=>{const n=new URLSearchParams(t).toString();return e(`/vorlagen?${n}`)},s=t=>e(`/vorlagen/${t}`),c=t=>{const n=new URLSearchParams(t).toString();return e(`/ketten?${n}`)},i=t=>e(`/ketten/${t}`),f=()=>e("/fraktionen"),h=(t,n)=>{const o=n?`?jahr=${n}`:"";return e(`/fraktionen/${t}/dashboard${o}`)};export{c as a,s as b,i as c,r as d,h as e,f};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{t as b}from"./DVOkFnep.js";import{F as g}from"./DAfY0XTB.js";function A(i,h,t,N,f,r){var o=i.__className;if(g||o!==t||o===void 0){var a=b(t,N,r);(!g||a!==i.getAttribute("class"))&&(a==null?i.removeAttribute("class"):i.className=a),i.__className=t}else if(r&&f!==r)for(var l in r){var u=!!r[l];(f==null||u!==!!f[l])&&i.classList.toggle(l,u)}return r}export{A as s};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{ab as v,a1 as d,a5 as u,aF as T,l,aG as p,aH as h,F as i,Y as s,aI as E,R as y,aJ as g,W as w,aK as N}from"./DAfY0XTB.js";const M=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function b(t){return M?.createHTML(t)??t}function x(t){var r=v("template");return r.innerHTML=b(t.replaceAll("<!>","<!---->")),r.content}function a(t,r){var e=l;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function R(t,r){var e=(r&p)!==0,f=(r&h)!==0,n,_=!t.startsWith("<!>");return()=>{if(i)return a(s,null),s;n===void 0&&(n=x(_?t:"<!>"+t),e||(n=u(n)));var o=f||T?document.importNode(n,!0):n.cloneNode(!0);if(e){var c=u(o),m=o.lastChild;a(c,m)}else a(o,o);return o}}function I(t=""){if(!i){var r=d(t+"");return a(r,r),r}var e=s;return e.nodeType!==g?(e.before(e=d()),w(e)):N(e),a(e,e),e}function O(){if(i)return a(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=d();return t.append(r,e),a(r,e),t}function P(t,r){if(i){var e=l;((e.f&E)===0||e.nodes.end===null)&&(e.nodes.end=s),y();return}t!==null&&t.before(r)}const A="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(A);export{P as a,a as b,O as c,R as f,I as t};

View File

@ -1 +0,0 @@
import{am as o,an as t,q as c,o as l}from"./DAfY0XTB.js";function a(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(e){t===null&&a(),c&&t.l!==null?u(t).m.push(e):o(()=>{const n=l(e);if(typeof n=="function")return n})}function u(e){var n=e.l;return n.u??={a:[],b:[],m:[]}}export{r as o};

View File

@ -1 +0,0 @@
import{N as o,o as a,ae as d}from"./DAfY0XTB.js";function p(s,u,n){if(s==null)return u(void 0),o;const t=a(()=>s.subscribe(u,n));return t.unsubscribe?()=>t.unsubscribe():t}const i=[];function _(s,u=o){let n=null;const t=new Set;function c(r){if(d(s,r)&&(s=r,n)){const b=!i.length;for(const e of t)e[1](),i.push(e,s);if(b){for(let e=0;e<i.length;e+=2)i[e][0](i[e+1]);i.length=0}}}function f(r){c(r(s))}function l(r,b=o){const e=[r,b];return t.add(e),t.size===1&&(n=u(c,f)||o),r(s),()=>{t.delete(e),t.size===0&&n&&(n(),n=null)}}return{set:c,update:f,subscribe:l}}function h(s){let u;return p(s,n=>u=n)(),u}export{h as g,p as s,_ as w};

View File

@ -1,2 +0,0 @@
import{F as l,af as u,ag as d,ah as g,ai as v,aj as h,ak as A,al as m}from"./DAfY0XTB.js";const i=[...`
\r\f \v\uFEFF`];function I(r,s,t){var e=r==null?"":""+r;if(s&&(e=e?e+" "+s:s),t){for(var n of Object.keys(t))if(t[n])e=e?e+" "+n:n;else if(e.length)for(var f=n.length,o=0;(o=e.indexOf(n,o))>=0;){var a=o+f;(o===0||i.includes(e[o-1]))&&(a===e.length||i.includes(e[a]))?e=(o===0?"":e.substring(0,o))+e.substring(a+1):o=a}}return e===""?null:e}function y(r,s){return r==null?null:String(r)}const p=Symbol("is custom element"),S=Symbol("is html"),L=h?"link":"LINK";function E(r){if(l){var s=!1,t=()=>{if(!s){if(s=!0,r.hasAttribute("value")){var e=r.value;c(r,"value",null),r.value=e}if(r.hasAttribute("checked")){var n=r.checked;c(r,"checked",null),r.checked=n}}};r.__on_r=t,u(t),d()}}function c(r,s,t,e){var n=M(r);l&&(n[s]=r.getAttribute(s),s==="src"||s==="srcset"||s==="href"&&r.nodeName===L)||n[s]!==(n[s]=t)&&(s==="loading"&&(r[g]=t),t==null?r.removeAttribute(s):typeof t!="string"&&N(r).includes(s)?r[s]=t:r.setAttribute(s,t))}function M(r){return r.__attributes??={[p]:r.nodeName.includes("-"),[S]:r.namespaceURI===v}}var _=new Map;function N(r){var s=r.getAttribute("is")||r.nodeName,t=_.get(s);if(t)return t;_.set(s,t=[]);for(var e,n=r,f=Element.prototype;f!==n;){e=m(n);for(var o in e)e[o].set&&t.push(o);n=A(n)}return t}export{y as a,E as r,c as s,I as t};

View File

@ -1 +0,0 @@
import{a8 as z,Y as G,F as A,a2 as M,ac as J,Z as ae,g as P,a0 as re,ao as fe,a1 as Q,a3 as O,a4 as H,ae as K,ap as ie,aq as U,B as le,ar as C,a9 as L,as as se,ab as ue,y as oe,H as te,at as V,au as ve,av as de,O as ce,aw as X,ax as pe,D as ge,a5 as W,a7 as $,ay as B,az as _e,aA as Ee,aa as he,a6 as me,af as Y,aB as j,Q as Te,aC as Ce,aD as we,aE as Ae}from"./Cjw4vZKn.js";function Ne(e,r){return r}function Se(e,r,s){for(var u=[],t=r.length,a,i=r.length,p=0;p<t;p++){let E=r[p];$(E,()=>{if(a){if(a.pending.delete(E),a.done.add(E),a.pending.size===0){var v=e.outrogroups;q(e,V(a.done)),v.delete(a),v.size===0&&(e.outrogroups=null)}}else i-=1},!1)}if(i===0){var l=u.length===0&&s!==null;if(l){var c=s,f=c.parentNode;Ee(f),f.append(c),e.items.clear()}q(e,r,!l)}else a={pending:new Set(r),done:new Set},(e.outrogroups??=new Set).add(a)}function q(e,r,s=!0){var u;if(e.pending.size>0){u=new Set;for(const i of e.pending.values())for(const p of i)u.add(e.items.get(p).e)}for(var t=0;t<r.length;t++){var a=r[t];if(u?.has(a)){a.f|=C;const i=document.createDocumentFragment();he(a,i)}else me(r[t],s)}}var Z;function Re(e,r,s,u,t,a=null){var i=e,p=new Map,l=(r&j)!==0;if(l){var c=e;i=A?M(J(c)):c.appendChild(z())}A&&ae();var f=null,E=oe(()=>{var d=s();return te(d)?d:d==null?[]:V(d)}),v,h=new Map,m=!0;function N(d){(S.effect.f&ge)===0&&(S.pending.delete(d),S.fallback=f,De(S,v,i,r,u),f!==null&&(v.length===0?(f.f&C)===0?W(f):(f.f^=C,k(f,null,i)):$(f,()=>{f=null})))}function n(d){S.pending.delete(d)}var o=G(()=>{v=P(E);var d=v.length;let _=!1;if(A){var b=re(i)===fe;b!==(d===0)&&(i=Q(),M(i),O(!1),_=!0)}for(var D=new Set,g=le,R=ue(),F=0;F<d;F+=1){A&&H.nodeType===K&&H.data===ie&&(i=H,_=!0,O(!1));var T=v[F],y=u(T,F),w=m?null:p.get(y);w?(w.v&&U(w.v,T),w.i&&U(w.i,F),R&&g.unskip_effect(w.e)):(w=Fe(p,m?i:Z??=z(),T,y,F,t,r,s),m||(w.e.f|=C),p.set(y,w)),D.add(y)}if(d===0&&a&&!f&&(m?f=L(()=>a(i)):(f=L(()=>a(Z??=z())),f.f|=C)),d>D.size&&se(),A&&d>0&&M(Q()),!m)if(h.set(g,D),R){for(const[ee,ne]of p)D.has(ee)||g.skip_effect(ne.e);g.oncommit(N),g.ondiscard(n)}else N(g);_&&O(!0),P(E)}),S={effect:o,items:p,pending:h,outrogroups:null,fallback:f};m=!1,A&&(i=H)}function x(e){for(;e!==null&&(e.f&_e)===0;)e=e.next;return e}function De(e,r,s,u,t){var a=(u&Ce)!==0,i=r.length,p=e.items,l=x(e.effect.first),c,f=null,E,v=[],h=[],m,N,n,o;if(a)for(o=0;o<i;o+=1)m=r[o],N=t(m,o),n=p.get(N).e,(n.f&C)===0&&(n.nodes?.a?.measure(),(E??=new Set).add(n));for(o=0;o<i;o+=1){if(m=r[o],N=t(m,o),n=p.get(N).e,e.outrogroups!==null)for(const T of e.outrogroups)T.pending.delete(n),T.done.delete(n);if((n.f&B)!==0&&(W(n),a&&(n.nodes?.a?.unfix(),(E??=new Set).delete(n))),(n.f&C)!==0)if(n.f^=C,n===l)k(n,null,s);else{var S=f?f.next:l;n===e.effect.last&&(e.effect.last=n.prev),n.prev&&(n.prev.next=n.next),n.next&&(n.next.prev=n.prev),I(e,f,n),I(e,n,S),k(n,S,s),f=n,v=[],h=[],l=x(f.next);continue}if(n!==l){if(c!==void 0&&c.has(n)){if(v.length<h.length){var d=h[0],_;f=d.prev;var b=v[0],D=v[v.length-1];for(_=0;_<v.length;_+=1)k(v[_],d,s);for(_=0;_<h.length;_+=1)c.delete(h[_]);I(e,b.prev,D.next),I(e,f,b),I(e,D,d),l=d,f=D,o-=1,v=[],h=[]}else c.delete(n),k(n,l,s),I(e,n.prev,n.next),I(e,n,f===null?e.effect.first:f.next),I(e,f,n),f=n;continue}for(v=[],h=[];l!==null&&l!==n;)(c??=new Set).add(l),h.push(l),l=x(l.next);if(l===null)continue}(n.f&C)===0&&v.push(n),f=n,l=x(n.next)}if(e.outrogroups!==null){for(const T of e.outrogroups)T.pending.size===0&&(q(e,V(T.done)),e.outrogroups?.delete(T));e.outrogroups.size===0&&(e.outrogroups=null)}if(l!==null||c!==void 0){var g=[];if(c!==void 0)for(n of c)(n.f&B)===0&&g.push(n);for(;l!==null;)(l.f&B)===0&&l!==e.fallback&&g.push(l),l=x(l.next);var R=g.length;if(R>0){var F=(u&j)!==0&&i===0?s:null;if(a){for(o=0;o<R;o+=1)g[o].nodes?.a?.measure();for(o=0;o<R;o+=1)g[o].nodes?.a?.fix()}Se(e,g,F)}}a&&Te(()=>{if(E!==void 0)for(n of E)n.nodes?.a?.apply()})}function Fe(e,r,s,u,t,a,i,p){var l=(i&ve)!==0?(i&de)===0?ce(s,!1,!1):X(s):null,c=(i&pe)!==0?X(t):null;return{v:l,i:c,e:L(()=>(a(r,l??s,c??t,p),()=>{e.delete(u)}))}}function k(e,r,s){if(e.nodes)for(var u=e.nodes.start,t=e.nodes.end,a=r&&(r.f&C)===0?r.nodes.start:s;u!==null;){var i=Y(u);if(a.before(u),u===t)return;u=i}}function I(e,r,s){r===null?e.effect.first=s:r.next=s,s===null?e.effect.last=r:s.prev=r}function xe(e,r){let s=null,u=A;var t;if(A){s=H;for(var a=J(document.head);a!==null&&(a.nodeType!==K||a.data!==e);)a=Y(a);if(a===null)O(!1);else{var i=Y(a);a.remove(),M(i)}}A||(t=document.head.appendChild(z()));try{G(()=>r(t),we|Ae)}finally{u&&(O(!0),M(s))}}export{Re as e,xe as h,Ne as i};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{s as e}from"./CTIvq_GE.js";const r=()=>{const s=e;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},b={subscribe(s){return r().page.subscribe(s)}};export{b as p};

View File

@ -1 +0,0 @@
import{N as o,o as a,al as d}from"./Cjw4vZKn.js";function p(s,u,e){if(s==null)return u(void 0),o;const t=a(()=>s.subscribe(u,e));return t.unsubscribe?()=>t.unsubscribe():t}const i=[];function _(s,u=o){let e=null;const t=new Set;function c(r){if(d(s,r)&&(s=r,e)){const b=!i.length;for(const n of t)n[1](),i.push(n,s);if(b){for(let n=0;n<i.length;n+=2)i[n][0](i[n+1]);i.length=0}}}function f(r){c(r(s))}function l(r,b=o){const n=[r,b];return t.add(n),t.size===1&&(e=u(c,f)||o),r(s),()=>{t.delete(n),t.size===0&&e&&(e(),e=null)}}return{set:c,update:f,subscribe:l}}function h(s){let u;return p(s,e=>u=e)(),u}export{h as g,p as s,_ as w};

View File

@ -1 +0,0 @@
import{s as f,g as c}from"./DiIboHMF.js";import{G as l,M as b,N as a,O as _,g as p,a as d}from"./Cjw4vZKn.js";let u=!1,t=Symbol();function v(e,r,n){const s=n[r]??={store:null,source:_(void 0),unsubscribe:a};if(s.store!==e&&!(t in n))if(s.unsubscribe(),s.store=e??null,e==null)s.source.v=void 0,s.unsubscribe=a;else{var i=!0;s.unsubscribe=f(e,o=>{i?s.source.v=o:d(s.source,o)}),i=!1}return e&&t in n?c(e):p(s.source)}function y(){const e={};function r(){l(()=>{for(var n in e)e[n].unsubscribe();b(e,t,{enumerable:!1,value:!0})})}return[e,r]}function N(e){var r=u;try{return u=!1,[e(),u]}finally{u=r}}export{v as a,N as c,y as s};

View File

@ -1 +0,0 @@
import{Z as l,_ as o,a0 as u,a1 as n,a2 as d,B as m,F as p,Y as _,a3 as v,a4 as k}from"./DAfY0XTB.js";class w{anchor;#t=new Map;#s=new Map;#e=new Map;#i=new Set;#a=!0;constructor(t,s=!0){this.anchor=t,this.#a=s}#f=t=>{if(this.#t.has(t)){var s=this.#t.get(t),e=this.#s.get(s);if(e)l(e),this.#i.delete(s);else{var a=this.#e.get(s);a&&(this.#s.set(s,a.effect),this.#e.delete(s),a.fragment.lastChild.remove(),this.anchor.before(a.fragment),e=a.effect)}for(const[i,f]of this.#t){if(this.#t.delete(i),i===t)break;const r=this.#e.get(f);r&&(o(r.effect),this.#e.delete(f))}for(const[i,f]of this.#s){if(i===s||this.#i.has(i))continue;const r=()=>{if(Array.from(this.#t.values()).includes(i)){var c=document.createDocumentFragment();v(f,c),c.append(n()),this.#e.set(i,{effect:f,fragment:c})}else o(f);this.#i.delete(i),this.#s.delete(i)};this.#a||!e?(this.#i.add(i),u(f,r,!1)):r()}}};#r=t=>{this.#t.delete(t);const s=Array.from(this.#t.values());for(const[e,a]of this.#e)s.includes(e)||(o(a.effect),this.#e.delete(e))};ensure(t,s){var e=m,a=k();if(s&&!this.#s.has(t)&&!this.#e.has(t))if(a){var i=document.createDocumentFragment(),f=n();i.append(f),this.#e.set(t,{effect:d(()=>s(f)),fragment:i})}else this.#s.set(t,d(()=>s(this.anchor)));if(this.#t.set(e,t),a){for(const[r,h]of this.#s)r===t?e.unskip_effect(h):e.skip_effect(h);for(const[r,h]of this.#e)r===t?e.unskip_effect(h.effect):e.skip_effect(h.effect);e.oncommit(this.#f),e.ondiscard(this.#r)}else p&&(this.anchor=_),this.#f(e)}}export{w as B};

View File

@ -1 +0,0 @@
import{c as y,a as p,f as _}from"./DCPIP6Ym.js";import{d as j,f as S,h as B,c as i,r as n,b as c,t as v,g as r,u as I}from"./DAfY0XTB.js";import{s as m}from"./D2u1A_4g.js";import{i as q}from"./Br6sCvve.js";import{s as w}from"./DVOkFnep.js";import{s as d}from"./D6E-zrqv.js";import{p as z}from"./B-uV6-Xr.js";import{s as A}from"./utcFFRIM.js";var C=_('<a><span class="mr-1"> </span> </a>'),D=_('<span><span class="mr-1"> </span> </span>');function N(x,o){j(o,!0);let h=z(o,"linked",3,!1);const s=I(()=>A(o.status));var u=y(),b=S(u);{var g=e=>{var t=C(),a=i(t),f=i(a,!0);n(a);var l=c(a);n(t),v(()=>{w(t,"href",`/ketten?status=${o.status??""}`),d(t,1,`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${r(s).color??""} hover:opacity-80 transition-opacity`),m(f,r(s).emoji),m(l,` ${r(s).label??""}`)}),p(e,t)},k=e=>{var t=D(),a=i(t),f=i(a,!0);n(a);var l=c(a);n(t),v(()=>{d(t,1,`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${r(s).color??""}`),m(f,r(s).emoji),m(l,` ${r(s).label??""}`)}),p(e,t)};q(b,e=>{h()&&o.status?e(g):e(k,-1)})}p(x,u),B()}export{N as S};

View File

@ -1 +0,0 @@
import{s as e}from"./3I_XkZiy.js";const r=()=>{const s=e;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},b={subscribe(s){return r().page.subscribe(s)}};export{b as p};

View File

@ -1 +0,0 @@
const p="modulepreload",y=function(u,i){return new URL(u,i).href},v={},w=function(i,a,f){let d=Promise.resolve();if(a&&a.length>0){let E=function(e){return Promise.all(e.map(o=>Promise.resolve(o).then(s=>({status:"fulfilled",value:s}),s=>({status:"rejected",reason:s}))))};const r=document.getElementsByTagName("link"),t=document.querySelector("meta[property=csp-nonce]"),m=t?.nonce||t?.getAttribute("nonce");d=E(a.map(e=>{if(e=y(e,f),e in v)return;v[e]=!0;const o=e.endsWith(".css"),s=o?'[rel="stylesheet"]':"";if(f)for(let c=r.length-1;c>=0;c--){const l=r[c];if(l.href===e&&(!o||l.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${e}"]${s}`))return;const n=document.createElement("link");if(n.rel=o?"stylesheet":p,o||(n.as="script"),n.crossOrigin="",n.href=e,m&&n.setAttribute("nonce",m),document.head.appendChild(n),o)return new Promise((c,l)=>{n.addEventListener("load",c),n.addEventListener("error",()=>l(new Error(`Unable to preload CSS for ${e}`)))})}))}function h(r){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=r,window.dispatchEvent(t),!t.defaultPrevented)throw r}return d.then(r=>{for(const t of r||[])t.status==="rejected"&&h(t.reason);return i().catch(h)})};export{w as _};

View File

@ -1 +0,0 @@
import{t as y}from"./B08B5jt4.js";import{F as r}from"./Cjw4vZKn.js";function n(t,e,f,i){var l=t.__style;if(r||l!==e){var s=y(e);(!r||s!==t.getAttribute("style"))&&(s==null?t.removeAttribute("style"):t.style.cssText=s),t.__style=e}return i}export{n as s};

View File

@ -1 +0,0 @@
import{F as i,Q as n,R as d,T as v,U as u,V as h,W as l,X as g}from"./Cjw4vZKn.js";const A=Symbol("is custom element"),T=Symbol("is html"),m=h?"link":"LINK";function N(r){if(i){var s=!1,e=()=>{if(!s){if(s=!0,r.hasAttribute("value")){var t=r.value;_(r,"value",null),r.value=t}if(r.hasAttribute("checked")){var o=r.checked;_(r,"checked",null),r.checked=o}}};r.__on_r=e,n(e),d()}}function _(r,s,e,t){var o=p(r);i&&(o[s]=r.getAttribute(s),s==="src"||s==="srcset"||s==="href"&&r.nodeName===m)||o[s]!==(o[s]=e)&&(s==="loading"&&(r[v]=e),e==null?r.removeAttribute(s):typeof e!="string"&&L(r).includes(s)?r[s]=e:r.setAttribute(s,e))}function p(r){return r.__attributes??={[A]:r.nodeName.includes("-"),[T]:r.namespaceURI===u}}var c=new Map;function L(r){var s=r.getAttribute("is")||r.nodeName,e=c.get(s);if(e)return e;c.set(s,e=[]);for(var t,o=r,f=Element.prototype;f!==o;){t=g(o);for(var a in t)t[a].set&&e.push(a);o=l(o)}return e}export{N as r,_ as s};

View File

@ -1 +0,0 @@
import{i as A,j as L,P as D,g as P,p as T,a as b,k as B,l as Y,D as x,m as M,o as N,q as U,v as h,w as q,x as w,y as z,z as $,S as j,L as y}from"./Cjw4vZKn.js";import{c as C}from"./DjXdyWBG.js";function F(r,a,t,s){var f=!U||(t&h)!==0,v=(t&M)!==0,E=(t&$)!==0,n=s,S=!0,g=()=>(S&&(S=!1,n=E?N(s):s),n);let u;if(v){var O=j in r||y in r;u=A(r,a)?.set??(O&&a in r?e=>r[a]=e:void 0)}var _,I=!1;v?[_,I]=C(()=>r[a]):_=r[a],_===void 0&&s!==void 0&&(_=g(),u&&(f&&L(),u(_)));var i;if(f?i=()=>{var e=r[a];return e===void 0?g():(S=!0,e)}:i=()=>{var e=r[a];return e!==void 0&&(n=void 0),e===void 0?n:e},f&&(t&D)===0)return i;if(u){var R=r.$$legacy;return(function(e,l){return arguments.length>0?((!f||!l||R||I)&&u(l?i():e),e):i()})}var c=!1,d=((t&q)!==0?w:z)(()=>(c=!1,i()));v&&P(d);var m=Y;return(function(e,l){if(arguments.length>0){const o=l?P(d):f&&v?T(e):e;return b(d,o),c=!0,n!==void 0&&(n=o),e}return B&&c||(m.f&x)!==0?d.v:P(d)})}export{F as p};

View File

@ -1 +0,0 @@
import{Y as c,F as o,Z as l,_ as b,a0 as p,a1 as v,a2 as g,a3 as _,a4 as m}from"./Cjw4vZKn.js";import{B as y}from"./6IKeDOr0.js";function T(f,d,h=!1){var n;o&&(n=m,l());var s=new y(f),u=h?b:0;function t(a,r){if(o){var e=p(n);if(a!==parseInt(e.substring(1))){var i=v();g(i),s.anchor=i,_(!1),s.ensure(a,r),_(!0);return}}s.ensure(a,r)}c(()=>{var a=!1;d((r,e=0)=>{a=!0,t(e,r)}),a||t(-1,null)},u)}export{T as i};

View File

@ -1 +0,0 @@
import{A as s,B as v,e as o,G as c,H as b,I as m,J as h}from"./Cjw4vZKn.js";function d(e,r,f=!1){if(e.multiple){if(r==null)return;if(!b(r))return m();for(var a of e.options)a.selected=r.includes(i(a));return}for(a of e.options){var t=i(a);if(h(t,r)){a.selected=!0;return}}(!f||r!==void 0)&&(e.selectedIndex=-1)}function y(e){var r=new MutationObserver(()=>{d(e,e.__value)});r.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),c(()=>{r.disconnect()})}function S(e,r,f=r){var a=new WeakSet,t=!0;s(e,"change",u=>{var l=u?"[selected]":":checked",n;if(e.multiple)n=[].map.call(e.querySelectorAll(l),i);else{var _=e.querySelector(l)??e.querySelector("option:not([disabled])");n=_&&i(_)}f(n),e.__value=n,v!==null&&a.add(v)}),o(()=>{var u=r();if(e===document.activeElement){var l=v;if(a.has(l))return}if(d(e,u,t),t&&u===void 0){var n=e.querySelector(":checked");n!==null&&(u=i(n),f(u))}e.__value=u,t=!1}),y(e)}function i(e){return"__value"in e?e.__value:e.value}export{S as b};

View File

@ -1 +0,0 @@
import{s as f,g as c}from"./DIGUPa-Q.js";import{G as l,M as b,N as a,O as _,g as p,a as d}from"./DAfY0XTB.js";let u=!1,t=Symbol();function v(e,r,n){const s=n[r]??={store:null,source:_(void 0),unsubscribe:a};if(s.store!==e&&!(t in n))if(s.unsubscribe(),s.store=e??null,e==null)s.source.v=void 0,s.unsubscribe=a;else{var i=!0;s.unsubscribe=f(e,o=>{i?s.source.v=o:d(s.source,o)}),i=!1}return e&&t in n?c(e):p(s.source)}function y(){const e={};function r(){l(()=>{for(var n in e)e[n].unsubscribe();b(e,t,{enumerable:!1,value:!0})})}return[e,r]}function N(e){var r=u;try{return u=!1,[e(),u]}finally{u=r}}export{v as a,N as c,y as s};

View File

@ -1 +0,0 @@
import{am as o,an as t,q as c,o as l}from"./Cjw4vZKn.js";function a(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(e){t===null&&a(),c&&t.l!==null?u(t).m.push(e):o(()=>{const n=l(e);if(typeof n=="function")return n})}function u(e){var n=e.l;return n.u??={a:[],b:[],m:[]}}export{r as o};

View File

@ -1 +0,0 @@
const t={angefragt:{emoji:"📝",label:"Angefragt",color:"bg-blue-100 text-blue-800"},beantwortet:{emoji:"✅",label:"Beantwortet",color:"bg-green-100 text-green-800"},offen:{emoji:"⏳",label:"Offen",color:"bg-yellow-100 text-yellow-800"},abgewiegelt:{emoji:"⚠️",label:"Abgewiegelt",color:"bg-orange-100 text-orange-800"},versandet:{emoji:"💀",label:"Versandet",color:"bg-red-100 text-red-800"},zurückgezogen:{emoji:"🔙",label:"Zurückgezogen",color:"bg-gray-100 text-gray-800"},eingereicht:{emoji:"📝",label:"Eingereicht",color:"bg-blue-100 text-blue-800"},in_beratung:{emoji:"🔄",label:"In Beratung",color:"bg-indigo-100 text-indigo-800"},vertagt:{emoji:"⏸️",label:"Vertagt",color:"bg-amber-100 text-amber-800"},verwiesen:{emoji:"↪️",label:"Verwiesen",color:"bg-purple-100 text-purple-800"},beschlossen:{emoji:"📋",label:"Beschlossen",color:"bg-teal-100 text-teal-800"},umgesetzt:{emoji:"✅",label:"Umgesetzt",color:"bg-green-100 text-green-800"},teilweise_umgesetzt:{emoji:"🔶",label:"Teilw. umgesetzt",color:"bg-lime-100 text-lime-800"},abgelehnt:{emoji:"❌",label:"Abgelehnt",color:"bg-red-100 text-red-800"},still_uebernommen:{emoji:"🔄✨",label:"Still übernommen",color:"bg-pink-100 text-pink-800"}};function r(e){return e?t[e]??{emoji:"❓",label:e,color:"bg-gray-100 text-gray-600"}:{emoji:"❓",label:"Unbekannt",color:"bg-gray-100 text-gray-600"}}const l={antrag:"Antrag",anfrage:"Anfrage",stellungnahme:"Stellungnahme",bericht:"Bericht"};function o(e){return e?l[e]??e:"Unbekannt"}function n(e){if(!e)return"";try{return new Date(e).toLocaleDateString("de-DE",{day:"2-digit",month:"2-digit",year:"numeric"})}catch{return e}}export{n as f,r as s,o as t};

View File

@ -1 +0,0 @@
import{A as s,B as v,e as o,G as c,H as b,I as m,J as h}from"./DAfY0XTB.js";function d(e,r,f=!1){if(e.multiple){if(r==null)return;if(!b(r))return m();for(var a of e.options)a.selected=r.includes(i(a));return}for(a of e.options){var t=i(a);if(h(t,r)){a.selected=!0;return}}(!f||r!==void 0)&&(e.selectedIndex=-1)}function y(e){var r=new MutationObserver(()=>{d(e,e.__value)});r.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),c(()=>{r.disconnect()})}function S(e,r,f=r){var a=new WeakSet,t=!0;s(e,"change",u=>{var l=u?"[selected]":":checked",n;if(e.multiple)n=[].map.call(e.querySelectorAll(l),i);else{var _=e.querySelector(l)??e.querySelector("option:not([disabled])");n=_&&i(_)}f(n),e.__value=n,v!==null&&a.add(v)}),o(()=>{var u=r();if(e===document.activeElement){var l=v;if(a.has(l))return}if(d(e,u,t),t&&u===void 0){var n=e.querySelector(":checked");n!==null&&(u=i(n),f(u))}e.__value=u,t=!1}),y(e)}function i(e){return"__value"in e?e.__value:e.value}export{S as b};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{l as o,a as r}from"../chunks/CTIvq_GE.js";export{o as load_css,r as start};

View File

@ -1 +0,0 @@
import{l as o,a as r}from"../chunks/3I_XkZiy.js";export{o as load_css,r as start};

View File

@ -1 +0,0 @@
import{a as d,f as x}from"../chunks/Bkzsmr9I.js";import{Y as i,_ as l,b as p,c as n,r as o}from"../chunks/Cjw4vZKn.js";import{B as c}from"../chunks/6IKeDOr0.js";/* empty css */function f(r,s,...e){var t=new c(r);i(()=>{const a=s()??null;t.ensure(a,a&&(m=>a(m,...e)))},l)}const g=!1,u=!1,k=Object.freeze(Object.defineProperty({__proto__:null,prerender:g,ssr:u},Symbol.toStringTag,{value:"Module"}));var v=x('<div class="min-h-screen bg-gray-50"><nav class="bg-white border-b border-gray-200 shadow-sm"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"><div class="flex justify-between h-16"><div class="flex items-center space-x-8"><a href="/" class="text-xl font-bold text-gray-900">Antragstracker <span class="text-green-600">Hagen</span></a> <div class="hidden sm:flex space-x-4"><a href="/" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Dashboard</a> <a href="/ketten" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Ketten</a> <a href="/vorlagen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Vorlagen</a> <a href="/abstimmungen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Abstimmungen</a> <a href="/karte" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Karte</a> <a href="/fraktionen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Fraktionen</a></div></div></div></div></nav> <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"><!></main></div>');function w(r,s){var e=v(),t=p(n(e),2),a=n(t);f(a,()=>s.children),o(t),o(e),d(r,e)}export{w as component,k as universal};

View File

@ -1 +0,0 @@
import{a as d,f as x}from"../chunks/DCPIP6Ym.js";import{Q as i,T as l,b as p,c as n,r as o}from"../chunks/DAfY0XTB.js";import{B as c}from"../chunks/Duumi1XQ.js";/* empty css */function f(r,s,...e){var t=new c(r);i(()=>{const a=s()??null;t.ensure(a,a&&(m=>a(m,...e)))},l)}const g=!1,u=!1,k=Object.freeze(Object.defineProperty({__proto__:null,prerender:g,ssr:u},Symbol.toStringTag,{value:"Module"}));var v=x('<div class="min-h-screen bg-gray-50"><nav class="bg-white border-b border-gray-200 shadow-sm"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"><div class="flex justify-between h-16"><div class="flex items-center space-x-8"><a href="/" class="text-xl font-bold text-gray-900">Antragstracker <span class="text-green-600">Hagen</span></a> <div class="hidden sm:flex space-x-4"><a href="/" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Dashboard</a> <a href="/ketten" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Ketten</a> <a href="/vorlagen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Vorlagen</a> <a href="/abstimmungen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Abstimmungen</a> <a href="/karte" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Karte</a> <a href="/fraktionen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Fraktionen</a></div></div></div></div></nav> <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"><!></main></div>');function w(r,s){var e=v(),t=p(n(e),2),a=n(t);f(a,()=>s.children),o(t),o(e),d(r,e)}export{w as component,k as universal};

View File

@ -1 +0,0 @@
import{a as b,f as x}from"../chunks/DCPIP6Ym.js";import{an as k,aL as y,am as i,o as $,aM as l,aN as E,g as v,aO as L,x as M,aP as N,d as O,f as P,t as j,h as q,c as u,r as m,b as w}from"../chunks/DAfY0XTB.js";import{s as g}from"../chunks/D2u1A_4g.js";import{s as z,p as _}from"../chunks/CTIvq_GE.js";function A(r=!1){const t=k,e=t.l.u;if(!e)return;let a=()=>L(t.s);if(r){let o=0,s={};const f=M(()=>{let p=!1;const c=t.s;for(const n in c)c[n]!==s[n]&&(s[n]=c[n],p=!0);return p&&o++,o});a=()=>v(f)}e.b.length&&y(()=>{d(t,a),l(e.b)}),i(()=>{const o=$(()=>e.m.map(E));return()=>{for(const s of o)typeof s=="function"&&s()}}),e.a.length&&i(()=>{d(t,a),l(e.a)})}function d(r,t){if(r.l.s)for(const e of r.l.s)v(e);t()}N();const B={get error(){return _.error},get status(){return _.status}};z.updated.check;const h=B;var C=x("<h1> </h1> <p> </p>",1);function I(r,t){O(t,!1),A();var e=C(),a=P(e),o=u(a,!0);m(a);var s=w(a,2),f=u(s,!0);m(s),j(()=>{g(o,h.status),g(f,h.error?.message)}),b(r,e),q()}export{I as component};

View File

@ -1 +0,0 @@
import{a as b,f as x}from"../chunks/Bkzsmr9I.js";import{an as k,aL as y,am as i,o as $,aM as l,aN as E,g as v,aO as L,x as M,aP as N,d as O,f as P,t as j,h as q,c as u,r as m,b as w}from"../chunks/Cjw4vZKn.js";import{s as g}from"../chunks/DfJQ0EIT.js";import{s as z,p as _}from"../chunks/3I_XkZiy.js";function A(r=!1){const t=k,e=t.l.u;if(!e)return;let a=()=>L(t.s);if(r){let o=0,s={};const f=M(()=>{let p=!1;const c=t.s;for(const n in c)c[n]!==s[n]&&(s[n]=c[n],p=!0);return p&&o++,o});a=()=>v(f)}e.b.length&&y(()=>{d(t,a),l(e.b)}),i(()=>{const o=$(()=>e.m.map(E));return()=>{for(const s of o)typeof s=="function"&&s()}}),e.a.length&&i(()=>{d(t,a),l(e.a)})}function d(r,t){if(r.l.s)for(const e of r.l.s)v(e);t()}N();const B={get error(){return _.error},get status(){return _.status}};z.updated.check;const h=B;var C=x("<h1> </h1> <p> </p>",1);function I(r,t){O(t,!1),A();var e=C(),a=P(e),o=u(a,!0);m(a);var s=w(a,2),f=u(s,!0);m(s),j(()=>{g(o,h.status),g(f,h.error?.message)}),b(r,e),q()}export{I as component};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{a as l,f as v}from"../chunks/DCPIP6Ym.js";import{p as S,s as $,t as m,a as u,g as o,e as Q,$ as T,b as d,c as s,r as a,n as _}from"../chunks/DAfY0XTB.js";import{s as n}from"../chunks/D2u1A_4g.js";import{i as U}from"../chunks/Br6sCvve.js";import{h as W,e as X,i as Y}from"../chunks/CBOKTDOo.js";/* empty css */var ee=v('<div class="p-6 text-center text-red-500"> </div>'),te=v('<div class="p-6 text-center text-gray-500"> </div>'),ae=v('<div class="p-6 text-center text-gray-500">Keine Anträge gefunden</div>'),se=v('<li class="px-6 py-4 hover:bg-gray-50 cursor-pointer"><div class="flex justify-between items-start"><div class="flex-1"><div class="flex items-center gap-2"><span class="font-mono text-sm text-green-700 bg-green-50 px-2 py-0.5 rounded"> </span> <span class="text-xs text-gray-400"> </span></div> <p class="mt-1 text-gray-700 line-clamp-2"> </p></div> <span class="text-xs px-2 py-1 rounded-full bg-yellow-100 text-yellow-800">⏳ offen</span></div></li>'),re=v('<ul class="divide-y divide-gray-100"></ul>'),oe=v('<main class="min-h-screen bg-gray-50"><header class="bg-green-700 text-white py-6 shadow-lg"><div class="max-w-6xl mx-auto px-4"><h1 class="text-3xl font-bold">🏛️ Antragstracker Hagen</h1> <p class="text-green-100 mt-1">Kommunale Anträge & Anfragen nachverfolgen</p></div></header> <div class="max-w-6xl mx-auto px-4 py-8"><div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"><div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-green-600"> </div> <div class="text-gray-500 text-sm">Vorlagen</div></div> <div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-blue-600"> </div> <div class="text-gray-500 text-sm">Ketten</div></div> <div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-purple-600">41</div> <div class="text-gray-500 text-sm">Gremien</div></div> <div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-orange-600">20042026</div> <div class="text-gray-500 text-sm">Zeitraum</div></div></div> <section class="bg-white rounded-lg shadow"><div class="px-6 py-4 border-b border-gray-200"><h2 class="text-xl font-semibold text-gray-800">📋 Aktuelle Anträge</h2></div> <!></section></div></main>');function ge(K){let g=S({vorlagen:0,beratungen:0,ketten:0,gremien:0}),x=$(S([])),R=$(!0),p=$("");const i=typeof window<"u"&&window.location.port==="5173"?`http://${window.location.hostname}:8099/api`:"/api";async function B(){console.log("API_BASE:",i);try{console.log("Fetching health...");const e=await fetch(`${i}/health`);if(console.log("Health response:",e.status),e.ok){console.log("Fetching vorlagen...");const r=await fetch(`${i}/vorlagen?page_size=1`);console.log("Vorlagen response:",r.status);const c=await r.json();g.vorlagen=c.total;const f=await(await fetch(`${i}/ketten?page_size=1`)).json();g.ketten=f.total}else u(p,`Health check failed: ${e.status}`);const t=await fetch(`${i}/vorlagen?typ=antrag&page_size=10`);if(console.log("Antraege response:",t.status),t.ok){const r=await t.json();console.log("Antraege data:",r.items.length),u(x,r.items,!0)}}catch(e){console.error("API Fehler:",e),u(p,`Fehler: ${e}`)}finally{u(R,!1),console.log("Loading done, antraege:",o(x).length)}}B();var w=oe();W("1uha8ag",e=>{Q(()=>{T.title="Antragstracker Hagen"})});var j=d(s(w),2),y=s(j),b=s(y),z=s(b),E=s(z,!0);a(z),_(2),a(b);var D=d(b,2),F=s(D),V=s(F,!0);a(F),_(2),a(D),_(4),a(y);var H=d(y,2),G=d(s(H),2);{var Z=e=>{var t=ee(),r=s(t,!0);a(t),m(()=>n(r,o(p))),l(e,t)},q=e=>{var t=te(),r=s(t);a(t),m(()=>n(r,`Lade Daten... (API: ${i})`)),l(e,t)},C=e=>{var t=ae();l(e,t)},J=e=>{var t=re();X(t,21,()=>o(x),Y,(r,c)=>{var h=se(),f=s(h),I=s(f),k=s(I),A=s(k),M=s(A,!0);a(A);var L=d(A,2),N=s(L,!0);a(L),a(k);var P=d(k,2),O=s(P,!0);a(P),a(I),_(2),a(f),a(h),m(()=>{n(M,o(c).aktenzeichen),n(N,o(c).datum_eingang),n(O,o(c).betreff)}),l(r,h)}),a(t),l(e,t)};U(G,e=>{o(p)?e(Z):o(R)?e(q,1):o(x).length===0?e(C,2):e(J,-1)})}a(H),a(j),a(w),m((e,t)=>{n(E,e),n(V,t)},[()=>g.vorlagen.toLocaleString(),()=>g.ketten.toLocaleString()]),l(K,w)}export{ge as component};

View File

@ -1 +0,0 @@
import{a as l,f as v}from"../chunks/Bkzsmr9I.js";import{p as S,s as $,t as m,a as u,g as o,e as Q,$ as T,b as d,c as s,r as a,n as _}from"../chunks/Cjw4vZKn.js";import{s as n}from"../chunks/DfJQ0EIT.js";import{i as U}from"../chunks/kjB3f-xG.js";import{h as W,e as X,i as Y}from"../chunks/DaCWmHjB.js";/* empty css */var ee=v('<div class="p-6 text-center text-red-500"> </div>'),te=v('<div class="p-6 text-center text-gray-500"> </div>'),ae=v('<div class="p-6 text-center text-gray-500">Keine Anträge gefunden</div>'),se=v('<li class="px-6 py-4 hover:bg-gray-50 cursor-pointer"><div class="flex justify-between items-start"><div class="flex-1"><div class="flex items-center gap-2"><span class="font-mono text-sm text-green-700 bg-green-50 px-2 py-0.5 rounded"> </span> <span class="text-xs text-gray-400"> </span></div> <p class="mt-1 text-gray-700 line-clamp-2"> </p></div> <span class="text-xs px-2 py-1 rounded-full bg-yellow-100 text-yellow-800">⏳ offen</span></div></li>'),re=v('<ul class="divide-y divide-gray-100"></ul>'),oe=v('<main class="min-h-screen bg-gray-50"><header class="bg-green-700 text-white py-6 shadow-lg"><div class="max-w-6xl mx-auto px-4"><h1 class="text-3xl font-bold">🏛️ Antragstracker Hagen</h1> <p class="text-green-100 mt-1">Kommunale Anträge & Anfragen nachverfolgen</p></div></header> <div class="max-w-6xl mx-auto px-4 py-8"><div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"><div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-green-600"> </div> <div class="text-gray-500 text-sm">Vorlagen</div></div> <div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-blue-600"> </div> <div class="text-gray-500 text-sm">Ketten</div></div> <div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-purple-600">41</div> <div class="text-gray-500 text-sm">Gremien</div></div> <div class="bg-white rounded-lg shadow p-4 text-center"><div class="text-3xl font-bold text-orange-600">20042026</div> <div class="text-gray-500 text-sm">Zeitraum</div></div></div> <section class="bg-white rounded-lg shadow"><div class="px-6 py-4 border-b border-gray-200"><h2 class="text-xl font-semibold text-gray-800">📋 Aktuelle Anträge</h2></div> <!></section></div></main>');function ge(K){let g=S({vorlagen:0,beratungen:0,ketten:0,gremien:0}),x=$(S([])),R=$(!0),p=$("");const i=typeof window<"u"&&window.location.port==="5173"?`http://${window.location.hostname}:8099/api`:"/api";async function B(){console.log("API_BASE:",i);try{console.log("Fetching health...");const e=await fetch(`${i}/health`);if(console.log("Health response:",e.status),e.ok){console.log("Fetching vorlagen...");const r=await fetch(`${i}/vorlagen?page_size=1`);console.log("Vorlagen response:",r.status);const c=await r.json();g.vorlagen=c.total;const f=await(await fetch(`${i}/ketten?page_size=1`)).json();g.ketten=f.total}else u(p,`Health check failed: ${e.status}`);const t=await fetch(`${i}/vorlagen?typ=antrag&page_size=10`);if(console.log("Antraege response:",t.status),t.ok){const r=await t.json();console.log("Antraege data:",r.items.length),u(x,r.items,!0)}}catch(e){console.error("API Fehler:",e),u(p,`Fehler: ${e}`)}finally{u(R,!1),console.log("Loading done, antraege:",o(x).length)}}B();var w=oe();W("1uha8ag",e=>{Q(()=>{T.title="Antragstracker Hagen"})});var j=d(s(w),2),y=s(j),b=s(y),z=s(b),E=s(z,!0);a(z),_(2),a(b);var D=d(b,2),F=s(D),V=s(F,!0);a(F),_(2),a(D),_(4),a(y);var H=d(y,2),G=d(s(H),2);{var Z=e=>{var t=ee(),r=s(t,!0);a(t),m(()=>n(r,o(p))),l(e,t)},q=e=>{var t=te(),r=s(t);a(t),m(()=>n(r,`Lade Daten... (API: ${i})`)),l(e,t)},C=e=>{var t=ae();l(e,t)},J=e=>{var t=re();X(t,21,()=>o(x),Y,(r,c)=>{var h=se(),f=s(h),I=s(f),k=s(I),A=s(k),M=s(A,!0);a(A);var L=d(A,2),N=s(L,!0);a(L),a(k);var P=d(k,2),O=s(P,!0);a(P),a(I),_(2),a(f),a(h),m(()=>{n(M,o(c).aktenzeichen),n(N,o(c).datum_eingang),n(O,o(c).betreff)}),l(r,h)}),a(t),l(e,t)};U(G,e=>{o(p)?e(Z):o(R)?e(q,1):o(x).length===0?e(C,2):e(J,-1)})}a(H),a(j),a(w),m((e,t)=>{n(E,e),n(V,t)},[()=>g.vorlagen.toLocaleString(),()=>g.ketten.toLocaleString()]),l(K,w)}export{ge as component};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{a as l,f as n}from"../chunks/Bkzsmr9I.js";import{o as L}from"../chunks/trpXq522.js";import{d as M,s as _,p as j,h as q,e as B,b as c,c as i,a as h,$ as C,g as a,r as e,n as D,t as E}from"../chunks/Cjw4vZKn.js";import{s as f}from"../chunks/DfJQ0EIT.js";import{i as G}from"../chunks/kjB3f-xG.js";import{h as I,e as J,i as K}from"../chunks/DaCWmHjB.js";import{s as N}from"../chunks/RVjQLo13.js";import{s as u}from"../chunks/QfvBL-nR.js";import{f as O}from"../chunks/Cgke0YGN.js";var P=n('<div class="text-gray-500">Laden...</div>'),Q=n('<a class="block p-4 rounded-lg border hover:shadow-md transition-shadow"><div class="font-bold text-lg"> </div> <div class="text-sm text-gray-600"> </div> <div class="mt-2 text-2xl font-semibold"> </div> <div class="text-xs text-gray-500">Anträge & Anfragen</div></a>'),R=n('<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>'),S=n('<div class="max-w-4xl mx-auto p-6"><h1 class="text-2xl font-bold mb-6">Fraktionen</h1> <!></div>');function ra(b,k){M(k,!0);let p=_(j([])),x=_(!0);L(async()=>{h(p,await O(),!0),h(x,!1)});var v=S();I("caopi2",t=>{B(()=>{C.title="Fraktionen — Antragstracker Hagen"})});var y=c(i(v),2);{var $=t=>{var s=P();l(t,s)},w=t=>{var s=R();J(s,21,()=>a(p),K,(z,r)=>{var o=Q(),d=i(o),A=i(d,!0);e(d);var m=c(d,2),F=i(m,!0);e(m);var g=c(m,2),H=i(g,!0);e(g),D(2),e(o),E(()=>{N(o,"href",`/fraktionen/${a(r).kuerzel??""}`),u(o,`border-left: 4px solid ${(a(r).farbe||"#6b7280")??""}`),u(d,`color: ${(a(r).farbe||"#374151")??""}`),f(A,a(r).kuerzel),f(F,a(r).name),f(H,a(r).anzahl)}),l(z,o)}),e(s),l(t,s)};G(y,t=>{a(x)?t($):t(w,-1)})}e(v),l(b,v),q()}export{ra as component};

View File

@ -1 +0,0 @@
import{a as l,f as n}from"../chunks/DCPIP6Ym.js";import{o as L}from"../chunks/DDErvS7v.js";import{d as M,s as _,p as j,h as q,e as B,b as c,c as i,a as h,$ as C,g as a,r as e,n as D,t as E}from"../chunks/DAfY0XTB.js";import{s as f}from"../chunks/D2u1A_4g.js";import{i as G}from"../chunks/Br6sCvve.js";import{h as I,e as J,i as K}from"../chunks/CBOKTDOo.js";import{s as N}from"../chunks/DVOkFnep.js";import{s as u}from"../chunks/C-x9yHfs.js";import{f as O}from"../chunks/Cgke0YGN.js";var P=n('<div class="text-gray-500">Laden...</div>'),Q=n('<a class="block p-4 rounded-lg border hover:shadow-md transition-shadow"><div class="font-bold text-lg"> </div> <div class="text-sm text-gray-600"> </div> <div class="mt-2 text-2xl font-semibold"> </div> <div class="text-xs text-gray-500">Anträge & Anfragen</div></a>'),R=n('<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>'),S=n('<div class="max-w-4xl mx-auto p-6"><h1 class="text-2xl font-bold mb-6">Fraktionen</h1> <!></div>');function ra(b,k){M(k,!0);let p=_(j([])),x=_(!0);L(async()=>{h(p,await O(),!0),h(x,!1)});var v=S();I("caopi2",t=>{B(()=>{C.title="Fraktionen — Antragstracker Hagen"})});var y=c(i(v),2);{var $=t=>{var s=P();l(t,s)},w=t=>{var s=R();J(s,21,()=>a(p),K,(z,r)=>{var o=Q(),d=i(o),A=i(d,!0);e(d);var m=c(d,2),F=i(m,!0);e(m);var g=c(m,2),H=i(g,!0);e(g),D(2),e(o),E(()=>{N(o,"href",`/fraktionen/${a(r).kuerzel??""}`),u(o,`border-left: 4px solid ${(a(r).farbe||"#6b7280")??""}`),u(d,`color: ${(a(r).farbe||"#374151")??""}`),f(A,a(r).kuerzel),f(F,a(r).name),f(H,a(r).anzahl)}),l(z,o)}),e(s),l(t,s)};G(y,t=>{a(x)?t($):t(w,-1)})}e(v),l(b,v),q()}export{ra as component};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../assets/leaflet.CIGW-MKW.css"])))=>i.map(i=>d[i]);
import{_ as S}from"../chunks/PPVm8Dsz.js";import{a as l,f as n}from"../chunks/DCPIP6Ym.js";import{o as de}from"../chunks/DDErvS7v.js";import{d as ce,s as u,p as B,b as i,f as ve,t as _,h as pe,e as ge,c as r,g as o,$ as me,r as a,a as h}from"../chunks/DAfY0XTB.js";import{d as fe,s as d,a as ue}from"../chunks/D2u1A_4g.js";import{i as L}from"../chunks/Br6sCvve.js";import{h as _e,e as Z,i as q}from"../chunks/CBOKTDOo.js";import{s as he}from"../chunks/DVOkFnep.js";import{s as xe}from"../chunks/D6E-zrqv.js";var be=n('<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>'),ye=n('<div class="h-[500px] flex items-center justify-center"><div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600"></div></div>'),we=n('<div id="map" class="h-[500px]"></div>'),ke=n('<li><a class="block p-2 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors"><div class="flex items-center gap-2"><span class="font-mono text-xs font-medium text-green-700"> </span> <span class="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-500"> </span></div> <p class="text-xs text-gray-600 mt-1 line-clamp-2"> </p></a></li>'),Oe=n('<ul class="space-y-2"></ul>'),Ae=n('<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"><h2 class="text-lg font-semibold text-gray-900 mb-2"> </h2> <p class="text-sm text-gray-500 mb-4"> </p> <!></div>'),Ee=n('<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"><p class="text-gray-500 text-sm">Klicke auf einen Marker um die zugehörigen Vorlagen zu sehen.</p></div>'),$e=n('<li><button><span class="font-medium"> </span> <span class="text-gray-400 ml-2"> </span></button></li>'),Ve=n('<div class="mb-6"><h1 class="text-2xl font-bold text-gray-900">📍 Anträge auf der Karte</h1> <p class="text-gray-500 text-sm mt-1">Orte aus Anträgen und Anfragen in Hagen</p></div> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"><div class="lg:col-span-2"><div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"><!></div> <div class="mt-4 text-sm text-gray-500"> </div></div> <div class="space-y-6"><!> <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"><h2 class="text-lg font-semibold text-gray-900 mb-4">🗺️ Alle Orte</h2> <ul class="space-y-1 max-h-80 overflow-y-auto"></ul></div></div></div>',1);function Ke(J,Q){ce(Q,!0);let g=u(B([])),p=u(null),x=u(B([])),P=u(!0),b=null;const T=typeof window<"u"&&window.location.port==="5173"?`http://${window.location.hostname}:8099/api`:"/api",U=[51.361,7.476],W=12;async function X(){try{const t=await fetch(`${T}/orte`);t.ok&&h(g,await t.json(),!0)}catch(t){console.error("Fehler beim Laden der Orte:",t)}finally{h(P,!1)}}async function H(t){h(p,t,!0);try{const e=await fetch(`${T}/orte/${t.id}/vorlagen`);e.ok&&h(x,await e.json(),!0)}catch(e){console.error("Fehler:",e)}}de(async()=>{await X();const t=await S(()=>import("../chunks/CjPBq9Bq.js").then(e=>e.l),[],import.meta.url);await S(()=>Promise.resolve({}),__vite__mapDeps([0]),import.meta.url),b=t.map("map").setView(U,W),t.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'}).addTo(b);for(const e of o(g)){const s=t.circleMarker([e.lat,e.lon],{radius:Math.min(8+e.vorlage_count*2,20),fillColor:"#16a34a",color:"#166534",weight:2,opacity:1,fillOpacity:.7}).addTo(b);s.bindPopup(`
<strong>${e.name}</strong><br>
${e.vorlage_count} Vorlage(n)
`),s.on("click",()=>H(e))}});var j=Ve();_e("hbihfm",t=>{var e=be();ge(()=>{me.title="Karte - Antragstracker Hagen"}),l(t,e)});var G=i(ve(j),2),y=r(G),w=r(y),Y=r(w);{var ee=t=>{var e=ye();l(t,e)},te=t=>{var e=we();l(t,e)};L(Y,t=>{o(P)?t(ee):t(te,-1)})}a(w);var I=i(w,2),ae=r(I);a(I),a(y);var K=i(y,2),N=r(K);{var re=t=>{var e=Ae(),s=r(e),v=r(s,!0);a(s);var c=i(s,2),k=r(c);a(c);var m=i(c,2);{var O=A=>{var E=Oe();Z(E,21,()=>o(x),q,(se,f)=>{var $=ke(),V=r($),M=r(V),z=r(M),ie=r(z,!0);a(z);var D=i(z,2),le=r(D,!0);a(D),a(M);var F=i(M,2),ne=r(F,!0);a(F),a(V),a($),_(()=>{he(V,"href",`/vorlagen/${o(f).id??""}`),d(ie,o(f).aktenzeichen),d(le,o(f).typ),d(ne,o(f).betreff)}),l(se,$)}),a(E),l(A,E)};L(m,A=>{o(x).length>0&&A(O)})}a(e),_(()=>{d(v,o(p).name),d(k,`${o(p).vorlage_count??""} Vorlage(n) betreffen diesen Ort`)}),l(t,e)},oe=t=>{var e=Ee();l(t,e)};L(N,t=>{o(p)?t(re):t(oe,-1)})}var R=i(N,2),C=i(r(R),2);Z(C,21,()=>o(g),q,(t,e)=>{var s=$e(),v=r(s),c=r(v),k=r(c,!0);a(c);var m=i(c,2),O=r(m);a(m),a(v),a(s),_(()=>{xe(v,1,`w-full text-left px-3 py-2 rounded-lg hover:bg-gray-50 transition-colors text-sm
${o(p)?.id===o(e).id?"bg-green-50 text-green-700":"text-gray-700"}`),d(k,o(e).name),d(O,`(${o(e).vorlage_count??""})`)}),ue("click",v,()=>H(o(e))),l(t,s)}),a(C),a(R),a(K),a(G),_(()=>d(ae,`${o(g).length??""} Orte geocodiert Marker-Größe = Anzahl Vorlagen`)),l(J,j),pe()}fe(["click"]);export{Ke as component};

View File

@ -1,6 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../assets/leaflet.CIGW-MKW.css"])))=>i.map(i=>d[i]);
import{_ as S}from"../chunks/PPVm8Dsz.js";import{a as l,f as n}from"../chunks/Bkzsmr9I.js";import{o as de}from"../chunks/trpXq522.js";import{d as ce,s as u,p as B,b as i,f as ve,t as _,h as pe,e as ge,c as r,g as o,$ as me,r as a,a as h}from"../chunks/Cjw4vZKn.js";import{d as fe,s as d,a as ue}from"../chunks/DfJQ0EIT.js";import{i as L}from"../chunks/kjB3f-xG.js";import{h as _e,e as Z,i as q}from"../chunks/DaCWmHjB.js";import{s as he}from"../chunks/RVjQLo13.js";import{s as xe}from"../chunks/CWOupeSg.js";var be=n('<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>'),ye=n('<div class="h-[500px] flex items-center justify-center"><div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600"></div></div>'),we=n('<div id="map" class="h-[500px]"></div>'),ke=n('<li><a class="block p-2 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors"><div class="flex items-center gap-2"><span class="font-mono text-xs font-medium text-green-700"> </span> <span class="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-500"> </span></div> <p class="text-xs text-gray-600 mt-1 line-clamp-2"> </p></a></li>'),Oe=n('<ul class="space-y-2"></ul>'),Ae=n('<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"><h2 class="text-lg font-semibold text-gray-900 mb-2"> </h2> <p class="text-sm text-gray-500 mb-4"> </p> <!></div>'),Ee=n('<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"><p class="text-gray-500 text-sm">Klicke auf einen Marker um die zugehörigen Vorlagen zu sehen.</p></div>'),$e=n('<li><button><span class="font-medium"> </span> <span class="text-gray-400 ml-2"> </span></button></li>'),Ve=n('<div class="mb-6"><h1 class="text-2xl font-bold text-gray-900">📍 Anträge auf der Karte</h1> <p class="text-gray-500 text-sm mt-1">Orte aus Anträgen und Anfragen in Hagen</p></div> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"><div class="lg:col-span-2"><div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"><!></div> <div class="mt-4 text-sm text-gray-500"> </div></div> <div class="space-y-6"><!> <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"><h2 class="text-lg font-semibold text-gray-900 mb-4">🗺️ Alle Orte</h2> <ul class="space-y-1 max-h-80 overflow-y-auto"></ul></div></div></div>',1);function Ke(J,Q){ce(Q,!0);let g=u(B([])),p=u(null),x=u(B([])),P=u(!0),b=null;const T=typeof window<"u"&&window.location.port==="5173"?`http://${window.location.hostname}:8099/api`:"/api",U=[51.361,7.476],W=12;async function X(){try{const t=await fetch(`${T}/orte`);t.ok&&h(g,await t.json(),!0)}catch(t){console.error("Fehler beim Laden der Orte:",t)}finally{h(P,!1)}}async function H(t){h(p,t,!0);try{const e=await fetch(`${T}/orte/${t.id}/vorlagen`);e.ok&&h(x,await e.json(),!0)}catch(e){console.error("Fehler:",e)}}de(async()=>{await X();const t=await S(()=>import("../chunks/CjPBq9Bq.js").then(e=>e.l),[],import.meta.url);await S(()=>Promise.resolve({}),__vite__mapDeps([0]),import.meta.url),b=t.map("map").setView(U,W),t.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'}).addTo(b);for(const e of o(g)){const s=t.circleMarker([e.lat,e.lon],{radius:Math.min(8+e.vorlage_count*2,20),fillColor:"#16a34a",color:"#166534",weight:2,opacity:1,fillOpacity:.7}).addTo(b);s.bindPopup(`
<strong>${e.name}</strong><br>
${e.vorlage_count} Vorlage(n)
`),s.on("click",()=>H(e))}});var j=Ve();_e("hbihfm",t=>{var e=be();ge(()=>{me.title="Karte - Antragstracker Hagen"}),l(t,e)});var G=i(ve(j),2),y=r(G),w=r(y),Y=r(w);{var ee=t=>{var e=ye();l(t,e)},te=t=>{var e=we();l(t,e)};L(Y,t=>{o(P)?t(ee):t(te,-1)})}a(w);var I=i(w,2),ae=r(I);a(I),a(y);var K=i(y,2),N=r(K);{var re=t=>{var e=Ae(),s=r(e),v=r(s,!0);a(s);var c=i(s,2),k=r(c);a(c);var m=i(c,2);{var O=A=>{var E=Oe();Z(E,21,()=>o(x),q,(se,f)=>{var $=ke(),V=r($),M=r(V),z=r(M),ie=r(z,!0);a(z);var D=i(z,2),le=r(D,!0);a(D),a(M);var F=i(M,2),ne=r(F,!0);a(F),a(V),a($),_(()=>{he(V,"href",`/vorlagen/${o(f).id??""}`),d(ie,o(f).aktenzeichen),d(le,o(f).typ),d(ne,o(f).betreff)}),l(se,$)}),a(E),l(A,E)};L(m,A=>{o(x).length>0&&A(O)})}a(e),_(()=>{d(v,o(p).name),d(k,`${o(p).vorlage_count??""} Vorlage(n) betreffen diesen Ort`)}),l(t,e)},oe=t=>{var e=Ee();l(t,e)};L(N,t=>{o(p)?t(re):t(oe,-1)})}var R=i(N,2),C=i(r(R),2);Z(C,21,()=>o(g),q,(t,e)=>{var s=$e(),v=r(s),c=r(v),k=r(c,!0);a(c);var m=i(c,2),O=r(m);a(m),a(v),a(s),_(()=>{xe(v,1,`w-full text-left px-3 py-2 rounded-lg hover:bg-gray-50 transition-colors text-sm
${o(p)?.id===o(e).id?"bg-green-50 text-green-700":"text-gray-700"}`),d(k,o(e).name),d(O,`(${o(e).vorlage_count??""})`)}),ue("click",v,()=>H(o(e))),l(t,s)}),a(C),a(R),a(K),a(G),_(()=>d(ae,`${o(g).length??""} Orte geocodiert Marker-Größe = Anzahl Vorlagen`)),l(J,j),pe()}fe(["click"]);export{Ke as component};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More