rename to wohnungsdidi + didi logo + footer for all + seconds-only counter

- App is now called "wohnungsdidi" everywhere user-facing (page title,
  nav brand, login header, notification subjects, report filename,
  FastAPI titles, log messages)
- Brand dot replaced with an image of Didi (web/static/didi.webp),
  rendered as a round 2.25rem avatar in _layout + login
- "Programmiert für Annika ♥" footer now shows for every logged-in user,
  not only Annika
- Count-up shows only seconds ("vor 73 s") regardless of age — no
  rollover to minutes/hours
- Data continuity: DB file stays /data/lazyflat.sqlite and the Docker
  volume stays lazyflat_data so the rename doesn't strand existing data
- Session cookie renamed to wohnungsdidi_session (one-time logout on
  rollout)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-21 17:29:24 +02:00
parent da180bd7c7
commit 0c18f0870a
14 changed files with 34 additions and 32 deletions

View file

@ -1,4 +1,4 @@
# lazyflat
# wohnungsdidi
Combined deployment of **flat-alert** (reliable scraper) and **flat-apply**
(experimental autoapplier) behind a single authenticated web UI.
@ -51,7 +51,7 @@ Because `apply/` is still experimental, the system is hardened around it:
## Deployment on Coolify
1. **Create repo**: push this monorepo to `ssh://git@git.moritz.run:2222/moritz/lazyflat.git`.
1. **Create repo**: push this monorepo to `ssh://git@git.moritz.run:2222/moritz/wohnungsdidi.git`.
2. **New Coolify resource***Docker Compose* → point it at this repo. Coolify
will read `docker-compose.yml` and deploy all three services on one network.
3. **Domain**: set `flat.lab.moritz.run` on the `web` service only. Coolify

View file

@ -92,7 +92,7 @@ class FlatAlerter:
if __name__ == "__main__":
logger.info("starting lazyflat alert service")
logger.info("starting wohnungsdidi alert service")
alerter = FlatAlerter()
while True:
try:

View file

@ -81,7 +81,7 @@ async def lifespan(_app: FastAPI):
yield
app = FastAPI(lifespan=lifespan, title="lazyflat-apply", docs_url=None, redoc_url=None)
app = FastAPI(lifespan=lifespan, title="wohnungsdidi-apply", docs_url=None, redoc_url=None)
@app.get("/health")

View file

@ -1,7 +1,7 @@
services:
web:
build: ./web
container_name: lazyflat-web
container_name: wohnungsdidi-web
restart: unless-stopped
depends_on:
apply:
@ -26,18 +26,20 @@ services:
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_USERNAME=${SMTP_USERNAME:-}
- SMTP_PASSWORD=${SMTP_PASSWORD:-}
- SMTP_FROM=${SMTP_FROM:-lazyflat@localhost}
- SMTP_FROM=${SMTP_FROM:-wohnungsdidi@localhost}
- SMTP_STARTTLS=${SMTP_STARTTLS:-true}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- ANTHROPIC_MODEL=${ANTHROPIC_MODEL:-claude-haiku-4-5-20251001}
volumes:
# Legacy volume name — kept so Coolify reuses the existing data volume
# after the rename from lazyflat → wohnungsdidi.
- lazyflat_data:/data
expose:
- "8000"
apply:
build: ./apply
container_name: lazyflat-apply
container_name: wohnungsdidi-apply
restart: unless-stopped
expose:
- "8000"
@ -51,7 +53,7 @@ services:
alert:
build: ./alert
container_name: lazyflat-alert
container_name: wohnungsdidi-alert
restart: unless-stopped
depends_on:
web:

View file

@ -1,5 +1,5 @@
"""
lazyflat web app.
wohnungsdidi web app.
Tabs:
- / Wohnungen
@ -84,7 +84,7 @@ async def lifespan(_app: FastAPI):
yield
app = FastAPI(lifespan=lifespan, title="lazyflat", docs_url=None, redoc_url=None, openapi_url=None)
app = FastAPI(lifespan=lifespan, title="wohnungsdidi", docs_url=None, redoc_url=None, openapi_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@ -712,7 +712,7 @@ def bewerbung_zip(request: Request, app_id: int):
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr(
"README.txt",
f"lazyflat application report\n"
f"wohnungsdidi application report\n"
f"application_id={a['id']}\n"
f"flat_id={a['flat_id']}\n"
f"provider={a['provider']}\n"
@ -764,7 +764,7 @@ def bewerbung_zip(request: Request, app_id: int):
zf.writestr(f"snapshots/{idx:02d}_{label}.html", html)
buf.seek(0)
filename = f"lazyflat-report-{a['id']}.zip"
filename = f"wohnungsdidi-report-{a['id']}.zip"
return StreamingResponse(
buf, media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
@ -889,7 +889,7 @@ def tab_logs_export(request: Request):
e["ip"],
])
body = buf.getvalue().encode("utf-8")
filename = "lazyflat-protokoll"
filename = "wohnungsdidi-protokoll"
if q.get("from"): filename += f"-{q['from']}"
if q.get("to"): filename += f"-bis-{q['to']}"
filename += ".csv"

View file

@ -1,5 +1,5 @@
"""
SQLite data layer for lazyflat.
SQLite data layer for wohnungsdidi.
Multi-user: users, per-user profiles/filters/notifications/preferences.
All per-user rows are 1:1 with users. Errors and forensics are retained

View file

@ -127,7 +127,7 @@ def _download_images(flat_id: str, urls: list[str], referer: str) -> int:
r = requests.get(
raw_url,
headers={"Referer": referer,
"User-Agent": "Mozilla/5.0 (lazyflat enricher)"},
"User-Agent": "Mozilla/5.0 (wohnungsdidi enricher)"},
timeout=IMAGE_TIMEOUT,
stream=True,
)

View file

@ -72,14 +72,14 @@ def on_match(user_id: int, flat: dict) -> None:
body = f"Neue passende Wohnung: {addr}\nMiete: {rent}\nZimmer: {rooms}\n{link}"
md = (f"*Neue passende Wohnung*\n[{addr}]({link})\n"
f"Miete: {rent} € · Zimmer: {rooms}\n{PUBLIC_URL}")
notify_user(user_id, "match", subject="[lazyflat] passende Wohnung", body_plain=body, body_markdown=md)
notify_user(user_id, "match", subject="[wohnungsdidi] passende Wohnung", body_plain=body, body_markdown=md)
def on_apply_ok(user_id: int, flat: dict, message: str) -> None:
addr = flat.get("address") or flat.get("link")
body = f"Bewerbung erfolgreich: {addr}\n{message}"
md = f"*Bewerbung erfolgreich*\n{addr}\n{message}"
notify_user(user_id, "apply_ok", subject="[lazyflat] Bewerbung OK", body_plain=body, body_markdown=md)
notify_user(user_id, "apply_ok", subject="[wohnungsdidi] Bewerbung OK", body_plain=body, body_markdown=md)
def on_apply_fail(user_id: int, flat: dict, message: str) -> None:
@ -87,5 +87,5 @@ def on_apply_fail(user_id: int, flat: dict, message: str) -> None:
body = f"Bewerbung fehlgeschlagen: {addr}\n{message}\n{PUBLIC_URL}/fehler"
md = (f"*Bewerbung fehlgeschlagen*\n{addr}\n{message}\n"
f"[Fehler ansehen]({PUBLIC_URL}/fehler)")
notify_user(user_id, "apply_fail", subject="[lazyflat] Bewerbung fehlgeschlagen",
notify_user(user_id, "apply_fail", subject="[wohnungsdidi] Bewerbung fehlgeschlagen",
body_plain=body, body_markdown=md)

View file

@ -25,7 +25,7 @@ AUTH_PASSWORD_HASH: str = _required("AUTH_PASSWORD_HASH")
# --- Session cookie -----------------------------------------------------------
SESSION_SECRET: str = getenv("SESSION_SECRET") or secrets.token_urlsafe(48)
SESSION_COOKIE_NAME: str = "lazyflat_session"
SESSION_COOKIE_NAME: str = "wohnungsdidi_session"
SESSION_MAX_AGE_SECONDS: int = int(getenv("SESSION_MAX_AGE_SECONDS", str(60 * 60 * 24 * 7)))
COOKIE_SECURE: bool = getenv("COOKIE_SECURE", "true").lower() in ("true", "1", "yes", "on")
@ -43,6 +43,8 @@ ALERT_SCRAPE_INTERVAL_SECONDS: int = int(getenv("ALERT_SCRAPE_INTERVAL_SECONDS",
# --- Storage ------------------------------------------------------------------
DATA_DIR: Path = Path(getenv("DATA_DIR", "/data"))
DATA_DIR.mkdir(parents=True, exist_ok=True)
# Legacy filename — kept so existing data under /data/lazyflat.sqlite stays
# reachable across the rename to wohnungsdidi. Not user-facing.
DB_PATH: Path = DATA_DIR / "lazyflat.sqlite"
# Retention (errors / audit / application forensics). Default 14 days.
@ -58,7 +60,7 @@ SMTP_HOST: str = getenv("SMTP_HOST", "")
SMTP_PORT: int = int(getenv("SMTP_PORT", "587"))
SMTP_USERNAME: str = getenv("SMTP_USERNAME", "")
SMTP_PASSWORD: str = getenv("SMTP_PASSWORD", "")
SMTP_FROM: str = getenv("SMTP_FROM", "lazyflat@localhost")
SMTP_FROM: str = getenv("SMTP_FROM", "wohnungsdidi@localhost")
SMTP_STARTTLS: bool = getenv("SMTP_STARTTLS", "true").lower() in ("true", "1", "yes", "on")
# --- App URL (used to build links in notifications) ---------------------------

View file

@ -19,9 +19,7 @@ function fmtCountUp(iso) {
const ts = Date.parse(iso);
if (!iso || Number.isNaN(ts)) return "—";
const diff = Math.max(0, Math.floor((Date.now() - ts) / 1000));
if (diff < 60) return `vor ${diff} s`;
if (diff < 3600) return `vor ${Math.floor(diff / 60)} min`;
return `vor ${Math.floor(diff / 3600)} h`;
return `vor ${diff} s`;
}
function updateRelativeTimes() {

BIN
web/static/didi.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View file

@ -7,8 +7,8 @@
<header class="border-b border-soft bg-white/70 backdrop-blur sticky top-0 z-10">
<div class="max-w-6xl mx-auto px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="brand-dot"></div>
<h1 class="text-xl font-semibold">lazyflat</h1>
<img src="/static/didi.webp" alt="" class="brand-dot">
<h1 class="text-xl font-semibold">wohnungsdidi</h1>
</div>
<div class="flex items-center gap-4 text-sm">
<span class="text-slate-500">{{ user.username }}{% if is_admin %} · <span class="chip chip-info">Administrator</span>{% endif %}</span>
@ -29,9 +29,7 @@
<main class="max-w-6xl mx-auto px-6 py-6 space-y-6">
{% block content %}{% endblock %}
</main>
{% if user.username == 'annika' %}
<footer class="text-center text-xs text-slate-500 py-6">
Programmiert für Annika ♥
</footer>
{% endif %}
{% endblock %}

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>{% block title %}lazyflat{% endblock %}</title>
<title>{% block title %}wohnungsdidi{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
@ -123,9 +123,11 @@
.map-popup-actions .btn { padding: 0.35rem 0.7rem; font-size: 12px; }
.map-popup-actions form { margin: 0; }
.brand-dot {
width: 2rem; height: 2rem; border-radius: 10px;
background: linear-gradient(135deg, #66b7f2 0%, #2f8ae0 60%, #fbd76b 100%);
width: 2.25rem; height: 2.25rem; border-radius: 9999px;
object-fit: cover; display: block;
border: 2px solid #fff;
box-shadow: 0 1px 4px rgba(47, 138, 224, 0.35);
background: #fff;
}
a { color: var(--primary); }
a:hover { text-decoration: underline; }

View file

@ -3,9 +3,9 @@
<main class="flex min-h-screen items-center justify-center p-4">
<div class="card w-full max-w-sm p-8">
<div class="flex items-center gap-3 mb-6">
<div class="brand-dot"></div>
<img src="/static/didi.webp" alt="" class="brand-dot">
<div>
<h1 class="text-2xl font-semibold leading-tight">lazyflat</h1>
<h1 class="text-2xl font-semibold leading-tight">wohnungsdidi</h1>
<p class="text-sm text-slate-500">Anmeldung erforderlich</p>
</div>
</div>