Files
business-card-scanner/app/reminders.py
Lucas Orth 1e8918e0cd
All checks were successful
Build & Deploy / deploy (push) Successful in 1m29s
Bildeditor, klickbare Felder, Einstellungen; Kopfzeile und Namensfeld raus
Bildbearbeitung im Nachgang: Drehen in 90-Grad-Schritten, Ausrichten per
Schieberegler, Zuschneiden per Rahmen mit anfassbaren Ecken. Der Server
rechnet jede Korrektur vom unbearbeiteten Original, das beim ersten
Eingriff daneben abgelegt wird - sonst summieren sich die Verluste über
mehrere Korrekturen, und Zurücksetzen wäre nicht möglich. Zwei
Zuschnitte hintereinander werden ineinander verrechnet.

Der Ort ließ sich nicht übernehmen, weil beide Quellen versagen können:
iOS entfernt beim Weitergeben an eine Webseite oft die GPS-Daten aus dem
Foto, und der Browserstandort scheiterte bisher stumm. Jetzt meldet er
seinen Grund ("Standortfreigabe fehlt"), die Detailansicht zeigt die
Herkunft des Ortes, und der Scan-Ort ist von Hand nachtragbar.

E-Mail, Web, Telefon und Mobil sind in der Tabelle jetzt Links; Straße,
PLZ, Ort und Land stehen als eine Adresszeile, die die eingestellte
Navigations-App öffnet.

Einstellungen als vierter Reiter: Navigations-App (Apple Karten, Google
Maps, OpenStreetMap), Statusübersicht und Abmelden - der Knopf saß
vorher in der Kopfzeile.

Die Kopfzeile ist weg. Sie kostete auf dem Telefon eine Bildschirmzeile
und wiederholte nur, was die Tableiste schon sagt.

Kein full_name mehr, nur Vor- und Nachname; akademische Titel gehören in
den Vornamen. Bestehende Datensätze werden bei der Migration am letzten
Leerzeichen aufgeteilt. Die Spalte bleibt ungenutzt stehen: SQLite baut
zum Löschen die ganze Tabelle neu, der Gewinn wären ein paar Byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 15:19:10 +02:00

95 lines
2.8 KiB
Python

"""Faellige Erinnerungen als Mail verschicken.
Der Scheduler fragt die Datenbank ab, statt Timer im Speicher zu halten:
laeuft der Server zum Zeitpunkt einer Erinnerung nicht, wird sie beim
naechsten Start nachgeholt.
"""
import asyncio
import logging
from datetime import datetime, timezone
from . import config, db, mailer
log = logging.getLogger(__name__)
INTERVAL_SECONDS = 60
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _name(row) -> str:
return " ".join(filter(None, (row["first_name"], row["last_name"])))
def _body(row) -> str:
who = _name(row) or row["company"] or "Unbekannter Kontakt"
lines = [row["text"], "", "-" * 40, f"Kontakt: {who}"]
if row["company"] and _name(row):
lines.append(f"Firma: {row['company']}")
if row["email"]:
lines.append(f"E-Mail: {row['email']}")
if row["phone"]:
lines.append(f"Telefon: {row['phone']}")
if row["mobile"]:
lines.append(f"Mobil: {row['mobile']}")
if row["place"]:
lines.append(f"Kennengelernt in: {row['place']}")
if config.PUBLIC_BASE_URL:
lines += ["", f"{config.PUBLIC_BASE_URL}/#/karte/{row['card_id']}"]
return "\n".join(lines)
def due_reminders() -> list:
return db.query(
"""
SELECT r.id, r.card_id, r.text, r.due_at,
c.first_name, c.last_name, c.company, c.email, c.phone, c.mobile, c.place
FROM reminders r
JOIN cards c ON c.id = r.card_id
WHERE r.sent_at IS NULL AND r.due_at <= ?
ORDER BY r.due_at
""",
(_now(),),
)
def process_due() -> int:
"""Alle faelligen Erinnerungen abarbeiten, Anzahl der Versendeten zurueck."""
rows = due_reminders()
if not rows:
return 0
if not mailer.configured():
log.warning("%d faellige Erinnerung(en), aber kein SMTP konfiguriert", len(rows))
return 0
sent = 0
for row in rows:
who = _name(row) or row["company"] or "Kontakt"
try:
mailer.send(f"Erinnerung: {who}", _body(row))
except Exception as exc:
log.exception("Erinnerung %s konnte nicht verschickt werden", row["id"])
db.execute(
"UPDATE reminders SET send_error = ? WHERE id = ?",
(f"{type(exc).__name__}: {exc}", row["id"]),
)
continue
db.execute(
"UPDATE reminders SET sent_at = ?, send_error = NULL WHERE id = ?",
(_now(), row["id"]),
)
sent += 1
return sent
async def run_forever() -> None:
log.info("Erinnerungs-Scheduler gestartet (Intervall %ds)", INTERVAL_SECONDS)
while True:
try:
await asyncio.to_thread(process_due)
except Exception:
log.exception("Scheduler-Durchlauf fehlgeschlagen")
await asyncio.sleep(INTERVAL_SECONDS)