secrets tab, drop commute filter, favicon, robust error reports
1. Admin → Geheimnisse sub-tab lets you edit ANTHROPIC_API_KEY + BERLIN_WOHNEN_USERNAME/PASSWORD at runtime. Migration v7 adds a secrets(key,value,updated_at) table; startup seeds missing keys from env (idempotent). web reads secrets DB-first (env fallback) via llm._api_key(); alert fetches them from web /internal/secrets on each scan, passes them into Scraper(). Rotating creds no longer needs a redeploy. Masked display: 6 leading + 4 trailing chars, "…" in the middle. Blank form fields leave the stored value untouched. 2. Drop the max_morning_commute filter from UI + server + FILTER_KEYS + filter summary (the underlying Maps.calculate_score code stays for potential future re-enable). 3. /static/didi.webp wired as favicon via <link rel="icon"> in base.html. 4. apply.open_page wraps page.goto in try/except so a failed load still produces a "goto.failed" step + screenshot instead of returning an empty forensics blob. networkidle + post-submission sleep are also made best-effort. The error ZIP export already writes screenshot+HTML per step and final_html — with this change every apply run leaves a reconstructable trail even when the listing is already offline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9fbe1ce728
commit
3bb04210c4
12 changed files with 211 additions and 27 deletions
45
web/app.py
45
web/app.py
|
|
@ -78,6 +78,7 @@ apply_client = ApplyClient()
|
|||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
db.init_db()
|
||||
db.seed_secrets_from_env()
|
||||
bootstrap_admin()
|
||||
retention.start()
|
||||
logger.info("web service ready")
|
||||
|
|
@ -220,7 +221,7 @@ def _last_scrape_utc() -> str:
|
|||
|
||||
|
||||
FILTER_KEYS = ("rooms_min", "rooms_max", "max_rent", "min_size",
|
||||
"max_morning_commute", "wbs_required", "max_age_hours")
|
||||
"wbs_required", "max_age_hours")
|
||||
|
||||
|
||||
def _has_filters(f) -> bool:
|
||||
|
|
@ -263,8 +264,6 @@ def _filter_summary(f) -> str:
|
|||
parts.append(f"≤ {int(f['max_rent'])} €")
|
||||
if f["min_size"]:
|
||||
parts.append(f"≥ {int(f['min_size'])} m²")
|
||||
if f["max_morning_commute"]:
|
||||
parts.append(f"≤ {int(f['max_morning_commute'])} min")
|
||||
if f["wbs_required"] == "yes":
|
||||
parts.append("WBS")
|
||||
elif f["wbs_required"] == "no":
|
||||
|
|
@ -552,7 +551,6 @@ async def action_save_filters(
|
|||
rooms_max: str = Form(""),
|
||||
max_rent: str = Form(""),
|
||||
min_size: str = Form(""),
|
||||
max_morning_commute: str = Form(""),
|
||||
wbs_required: str = Form(""),
|
||||
max_age_hours: str = Form(""),
|
||||
user=Depends(require_user),
|
||||
|
|
@ -575,7 +573,6 @@ async def action_save_filters(
|
|||
"rooms_max": _f(rooms_max),
|
||||
"max_rent": _f(max_rent),
|
||||
"min_size": _f(min_size),
|
||||
"max_morning_commute": _f(max_morning_commute),
|
||||
"wbs_required": (wbs_required or "").strip(),
|
||||
"max_age_hours": _i(max_age_hours),
|
||||
})
|
||||
|
|
@ -825,7 +822,15 @@ def tab_logs_legacy():
|
|||
return RedirectResponse("/admin/protokoll", status_code=301)
|
||||
|
||||
|
||||
ADMIN_SECTIONS = ("protokoll", "benutzer")
|
||||
ADMIN_SECTIONS = ("protokoll", "benutzer", "geheimnisse")
|
||||
|
||||
|
||||
def _mask_secret(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
if len(value) <= 10:
|
||||
return "•" * len(value)
|
||||
return value[:6] + "…" + value[-4:]
|
||||
|
||||
|
||||
@app.get("/admin", response_class=HTMLResponse)
|
||||
|
|
@ -857,6 +862,10 @@ def tab_admin(request: Request, section: str):
|
|||
})
|
||||
elif section == "benutzer":
|
||||
ctx["users"] = db.list_users()
|
||||
elif section == "geheimnisse":
|
||||
secrets = db.all_secrets()
|
||||
ctx["secrets_masked"] = {k: _mask_secret(secrets.get(k, "")) for k in db.SECRET_KEYS}
|
||||
ctx["secret_flash"] = request.query_params.get("ok")
|
||||
return templates.TemplateResponse("admin.html", ctx)
|
||||
|
||||
|
||||
|
|
@ -1073,6 +1082,23 @@ async def action_users_disable(
|
|||
return RedirectResponse("/admin/benutzer", status_code=303)
|
||||
|
||||
|
||||
@app.post("/actions/secrets")
|
||||
async def action_secrets(request: Request, admin=Depends(require_admin)):
|
||||
form = await request.form()
|
||||
require_csrf(admin["id"], form.get("csrf", ""))
|
||||
changed = []
|
||||
for key in db.SECRET_KEYS:
|
||||
raw = (form.get(key) or "").strip()
|
||||
if not raw:
|
||||
continue
|
||||
db.set_secret(key, raw)
|
||||
changed.append(key)
|
||||
db.log_audit(admin["username"], "secrets.updated",
|
||||
",".join(changed) or "no-op",
|
||||
user_id=admin["id"], ip=client_ip(request))
|
||||
return RedirectResponse("/admin/geheimnisse?ok=1", status_code=303)
|
||||
|
||||
|
||||
@app.post("/actions/enrich-all")
|
||||
async def action_enrich_all(
|
||||
request: Request,
|
||||
|
|
@ -1181,3 +1207,10 @@ async def internal_report_error(
|
|||
context=payload.get("context"),
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/internal/secrets")
|
||||
async def internal_secrets(_g: None = Depends(require_internal)):
|
||||
"""Give sibling services (alert) the current runtime creds that the admin
|
||||
may have edited via the UI, so no redeploy is needed when rotating."""
|
||||
return db.all_secrets()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue