app.py was ~1300 lines with every route, helper, and middleware mixed
together. Split into:
- app.py (~100 lines): FastAPI bootstrap, lifespan, /health, security
headers, Jinja filter registration, include_router calls
- common.py: shared helpers (templates, apply_client, base_context,
_is_htmx, client_ip, require_internal, time helpers, filter helpers,
apply-gate helpers, _kick_apply / _finish_apply_background,
_bg_tasks, _spawn, _mask_secret, _has_running_application, BERLIN_TZ)
- routes/auth.py: /login (GET+POST), /logout
- routes/wohnungen.py: /, /partials/wohnungen, /partials/wohnung/{id},
/flat-images/{slug}/{idx}, /actions/apply|reject|unreject|auto-apply|
submit-forms|reset-circuit|filters|enrich-all|enrich-flat; owns
_wohnungen_context + _wohnungen_partial_or_redirect
- routes/bewerbungen.py: /bewerbungen, /bewerbungen/{id}/report.zip
- routes/einstellungen.py: /einstellungen, /einstellungen/{section},
/actions/profile|notifications|account/password|partner/*; owns
VALID_SECTIONS
- routes/admin.py: /logs redirect, /admin, /admin/{section},
/logs/export.csv, /actions/users/*|secrets; owns ADMIN_SECTIONS,
_parse_date_range, _collect_events
- routes/internal.py: /internal/flats|heartbeat|error|secrets
Route-diff before/after is empty — all 41 routes + /static mount
preserved. No behavior changes, pure mechanical split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
"""Bewerbungen (application history) tab and forensics ZIP export."""
|
|
import base64
|
|
import io
|
|
import json
|
|
import zipfile
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
|
|
|
|
import db
|
|
from auth import current_user
|
|
from common import base_context, templates
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/bewerbungen", response_class=HTMLResponse)
|
|
def tab_bewerbungen(request: Request):
|
|
u = current_user(request)
|
|
if not u:
|
|
return RedirectResponse("/login", status_code=303)
|
|
ctx = base_context(request, u, "bewerbungen")
|
|
ctx["applications"] = db.recent_applications(u["id"], limit=100)
|
|
return templates.TemplateResponse("bewerbungen.html", ctx)
|
|
|
|
|
|
@router.get("/bewerbungen/{app_id}/report.zip")
|
|
def bewerbung_zip(request: Request, app_id: int):
|
|
u = current_user(request)
|
|
if not u:
|
|
raise HTTPException(401)
|
|
a = db.get_application(app_id)
|
|
if not a or (a["user_id"] != u["id"] and not u["is_admin"]):
|
|
raise HTTPException(404)
|
|
|
|
flat = db.get_flat(a["flat_id"])
|
|
forensics = json.loads(a["forensics_json"]) if a["forensics_json"] else {}
|
|
profile = json.loads(a["profile_snapshot_json"]) if a["profile_snapshot_json"] else {}
|
|
app_meta = {k: a[k] for k in a.keys() if k not in ("forensics_json", "profile_snapshot_json")}
|
|
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
zf.writestr(
|
|
"README.txt",
|
|
f"wohnungsdidi application report\n"
|
|
f"application_id={a['id']}\n"
|
|
f"flat_id={a['flat_id']}\n"
|
|
f"provider={a['provider']}\n"
|
|
f"success={a['success']}\n"
|
|
f"started_at_utc={a['started_at']}\n"
|
|
f"finished_at_utc={a['finished_at']}\n"
|
|
f"submit_forms_used={bool(a['submit_forms_used'])}\n"
|
|
f"\n"
|
|
f"Contents:\n"
|
|
f" application.json DB row + metadata\n"
|
|
f" flat.json Flat details at discovery time\n"
|
|
f" profile_snapshot.json Profile used for this attempt\n"
|
|
f" forensics.json Full captured forensics\n"
|
|
f" step_log.txt Human-readable step log\n"
|
|
f" page.html Final page HTML (if captured)\n"
|
|
f" console.json Browser console entries\n"
|
|
f" errors.json Browser pageerror events\n"
|
|
f" network.json Network requests + partial responses\n"
|
|
f" snapshots/NN_*.jpg Screenshot at each step (NN = order)\n"
|
|
f" snapshots/NN_*.html Page HTML at each step\n"
|
|
)
|
|
zf.writestr("application.json", json.dumps(app_meta, indent=2, default=str))
|
|
zf.writestr("flat.json", json.dumps(dict(flat) if flat else {}, indent=2, default=str))
|
|
zf.writestr("profile_snapshot.json", json.dumps(profile, indent=2, default=str))
|
|
zf.writestr("forensics.json", json.dumps(forensics, indent=2, default=str))
|
|
|
|
step_lines = []
|
|
for s in forensics.get("steps", []):
|
|
step_lines.append(f"[{s.get('ts', 0):7.2f}s] {s.get('step', '?'):<24} {s.get('status', ''):<5} {s.get('detail', '')}")
|
|
zf.writestr("step_log.txt", "\n".join(step_lines))
|
|
|
|
if forensics.get("final_html"):
|
|
zf.writestr("page.html", forensics["final_html"])
|
|
zf.writestr("console.json", json.dumps(forensics.get("console", []), indent=2))
|
|
zf.writestr("errors.json", json.dumps(forensics.get("errors", []), indent=2))
|
|
zf.writestr("network.json", json.dumps(forensics.get("network", []), indent=2))
|
|
|
|
for idx, s in enumerate(forensics.get("screenshots", []), start=1):
|
|
label = (s.get("label") or f"step{idx}").replace("/", "_").replace(" ", "_")
|
|
b64 = s.get("b64_jpeg", "")
|
|
if b64:
|
|
try:
|
|
data = base64.b64decode(b64)
|
|
zf.writestr(f"snapshots/{idx:02d}_{label}.jpg", data)
|
|
except Exception:
|
|
pass
|
|
html = s.get("html") or ""
|
|
if html:
|
|
zf.writestr(f"snapshots/{idx:02d}_{label}.html", html)
|
|
|
|
buf.seek(0)
|
|
filename = f"wohnungsdidi-report-{a['id']}.zip"
|
|
return StreamingResponse(
|
|
buf, media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|