lazyflat: combined alert + apply behind authenticated web UI

Three isolated services (alert scraper, apply HTTP worker, web UI+DB)
with argon2 auth, signed cookies, CSRF, rate-limited login, kill switch,
apply circuit breaker, audit log, and strict CSP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Moritz 2026-04-21 09:51:35 +02:00
commit 69f2f1f635
46 changed files with 4183 additions and 0 deletions

219
web/db.py Normal file
View file

@ -0,0 +1,219 @@
import json
import sqlite3
import threading
from datetime import datetime, timezone
from typing import Any, Iterable
from settings import DB_PATH
_lock = threading.Lock()
def _connect() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH, isolation_level=None, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
return conn
_conn: sqlite3.Connection = _connect()
SCHEMA = """
CREATE TABLE IF NOT EXISTS flats (
id TEXT PRIMARY KEY,
link TEXT NOT NULL,
address TEXT,
rooms REAL,
size REAL,
total_rent REAL,
sqm_price REAL,
year_built TEXT,
wbs TEXT,
connectivity_morning_time REAL,
connectivity_night_time REAL,
address_link_gmaps TEXT,
payload_json TEXT NOT NULL,
matched_criteria INTEGER NOT NULL DEFAULT 0,
discovered_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_flats_discovered ON flats(discovered_at DESC);
CREATE INDEX IF NOT EXISTS idx_flats_matched ON flats(matched_criteria);
CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
flat_id TEXT NOT NULL,
url TEXT NOT NULL,
triggered_by TEXT NOT NULL, -- 'user' | 'auto'
started_at TEXT NOT NULL,
finished_at TEXT,
success INTEGER,
message TEXT,
FOREIGN KEY (flat_id) REFERENCES flats(id)
);
CREATE INDEX IF NOT EXISTS idx_applications_flat ON applications(flat_id);
CREATE INDEX IF NOT EXISTS idx_applications_started ON applications(started_at DESC);
CREATE TABLE IF NOT EXISTS state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
actor TEXT NOT NULL,
action TEXT NOT NULL,
details TEXT,
ip TEXT
);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp DESC);
"""
DEFAULTS = {
"mode": "manual", # 'manual' | 'auto'
"kill_switch": "0", # '1' = apply disabled
"apply_circuit_open": "0", # '1' = opened by circuit breaker
"apply_recent_failures": "0",
"last_alert_heartbeat": "",
"last_apply_heartbeat": "",
}
def init_db() -> None:
with _lock:
_conn.executescript(SCHEMA)
for k, v in DEFAULTS.items():
_conn.execute("INSERT OR IGNORE INTO state(key, value) VALUES (?, ?)", (k, v))
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def get_state(key: str) -> str | None:
row = _conn.execute("SELECT value FROM state WHERE key = ?", (key,)).fetchone()
return row["value"] if row else None
def set_state(key: str, value: str) -> None:
with _lock:
_conn.execute(
"INSERT INTO state(key, value) VALUES (?, ?) "
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
(key, value),
)
def upsert_flat(payload: dict, matched: bool) -> bool:
"""Returns True if this flat is new."""
flat_id = str(payload["id"])
conn_info = payload.get("connectivity") or {}
with _lock:
existing = _conn.execute("SELECT id FROM flats WHERE id = ?", (flat_id,)).fetchone()
if existing:
return False
_conn.execute(
"""
INSERT INTO flats(
id, link, address, rooms, size, total_rent, sqm_price, year_built, wbs,
connectivity_morning_time, connectivity_night_time, address_link_gmaps,
payload_json, matched_criteria, discovered_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
flat_id,
payload.get("link", ""),
payload.get("address", ""),
payload.get("rooms"),
payload.get("size"),
payload.get("total_rent"),
payload.get("sqm_price"),
str(payload.get("year_built", "")),
str(payload.get("wbs", "")),
conn_info.get("morning_time"),
conn_info.get("night_time"),
payload.get("address_link_gmaps"),
json.dumps(payload, default=str),
1 if matched else 0,
now_iso(),
),
)
return True
def recent_flats(limit: int = 50) -> list[sqlite3.Row]:
return list(
_conn.execute(
"""
SELECT f.*,
(SELECT success FROM applications a WHERE a.flat_id = f.id
ORDER BY a.started_at DESC LIMIT 1) AS last_application_success,
(SELECT message FROM applications a WHERE a.flat_id = f.id
ORDER BY a.started_at DESC LIMIT 1) AS last_application_message,
(SELECT started_at FROM applications a WHERE a.flat_id = f.id
ORDER BY a.started_at DESC LIMIT 1) AS last_application_at
FROM flats f
ORDER BY f.discovered_at DESC
LIMIT ?
""",
(limit,),
).fetchall()
)
def get_flat(flat_id: str) -> sqlite3.Row | None:
return _conn.execute("SELECT * FROM flats WHERE id = ?", (flat_id,)).fetchone()
def start_application(flat_id: str, url: str, triggered_by: str) -> int:
with _lock:
cur = _conn.execute(
"INSERT INTO applications(flat_id, url, triggered_by, started_at) VALUES (?, ?, ?, ?)",
(flat_id, url, triggered_by, now_iso()),
)
return cur.lastrowid
def finish_application(app_id: int, success: bool, message: str) -> None:
with _lock:
_conn.execute(
"UPDATE applications SET finished_at = ?, success = ?, message = ? WHERE id = ?",
(now_iso(), 1 if success else 0, message, app_id),
)
def recent_applications(limit: int = 20) -> list[sqlite3.Row]:
return list(
_conn.execute(
"""
SELECT a.*, f.address, f.link
FROM applications a
JOIN flats f ON f.id = a.flat_id
ORDER BY a.started_at DESC
LIMIT ?
""",
(limit,),
).fetchall()
)
def log_audit(actor: str, action: str, details: str = "", ip: str = "") -> None:
with _lock:
_conn.execute(
"INSERT INTO audit_log(timestamp, actor, action, details, ip) VALUES (?, ?, ?, ?, ?)",
(now_iso(), actor, action, details, ip),
)
def recent_audit(limit: int = 30) -> list[sqlite3.Row]:
return list(
_conn.execute(
"SELECT * FROM audit_log ORDER BY timestamp DESC LIMIT ?",
(limit,),
).fetchall()
)