"""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"lazyflat 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"lazyflat-report-{a['id']}.zip" return StreamingResponse( buf, media_type="application/zip", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, )