All checks were successful
Build & Deploy / deploy (push) Successful in 1m29s
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>
95 lines
2.8 KiB
Python
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)
|