Files
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

131 lines
3.9 KiB
Python

"""SQLite-Zugriff. Eine Verbindung, Schreibzugriffe unter einem Lock."""
import sqlite3
import threading
from typing import Any, Iterable
from . import config
_lock = threading.Lock()
_conn: sqlite3.Connection | None = None
SCHEMA = """
CREATE TABLE IF NOT EXISTS cards (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
captured_at TEXT,
batch_id TEXT,
image_file TEXT,
back_image_file TEXT,
source TEXT NOT NULL DEFAULT 'batch',
occasion TEXT,
full_name TEXT,
first_name TEXT,
last_name TEXT,
job_title TEXT,
company TEXT,
department TEXT,
email TEXT,
phone TEXT,
mobile TEXT,
fax TEXT,
website TEXT,
street TEXT,
postal_code TEXT,
city TEXT,
country TEXT,
notes TEXT NOT NULL DEFAULT '',
lat REAL,
lon REAL,
place TEXT,
geo_source TEXT,
extract_status TEXT NOT NULL DEFAULT 'ok',
extract_error TEXT,
exported_at TEXT
);
CREATE TABLE IF NOT EXISTS reminders (
id TEXT PRIMARY KEY,
card_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
text TEXT NOT NULL,
due_at TEXT NOT NULL,
created_at TEXT NOT NULL,
sent_at TEXT,
send_error TEXT
);
CREATE INDEX IF NOT EXISTS idx_cards_created ON cards(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_reminders_due ON reminders(due_at) WHERE sent_at IS NULL;
CREATE TABLE IF NOT EXISTS geocache (
key TEXT PRIMARY KEY,
place TEXT,
fetched_at TEXT NOT NULL
);
"""
def connect() -> sqlite3.Connection:
global _conn
if _conn is None:
config.ensure_dirs()
_conn = sqlite3.connect(config.DB_PATH, check_same_thread=False)
_conn.row_factory = sqlite3.Row
_conn.execute("PRAGMA journal_mode=WAL")
_conn.execute("PRAGMA foreign_keys=ON")
_conn.executescript(SCHEMA)
_migrate(_conn)
_conn.commit()
return _conn
# Spalten, die nach dem ersten Deploy dazugekommen sind. CREATE TABLE IF NOT
# EXISTS ruehrt eine bestehende Tabelle nicht an - ohne das hier liefe die
# App auf dem Server mit dem alten Schema weiter.
LATER_COLUMNS = {
"back_image_file": "TEXT",
"occasion": "TEXT",
}
def _migrate(conn: sqlite3.Connection) -> None:
present = {row["name"] for row in conn.execute("PRAGMA table_info(cards)")}
for name, kind in LATER_COLUMNS.items():
if name not in present:
conn.execute(f"ALTER TABLE cards ADD COLUMN {name} {kind}")
_split_legacy_names(conn)
def _split_legacy_names(conn: sqlite3.Connection) -> None:
"""full_name ist als Feld entfallen. Aeltere Datensaetze haben nur diesen
einen Namen - am letzten Leerzeichen trennen, damit sie nicht namenlos
dastehen. Die Spalte bleibt: SQLite baut zum Loeschen die Tabelle neu,
und der Gewinn waere ein paar Byte."""
rows = conn.execute(
"SELECT id, full_name FROM cards "
"WHERE full_name IS NOT NULL AND first_name IS NULL AND last_name IS NULL"
).fetchall()
for row in rows:
first, _, last = row["full_name"].strip().rpartition(" ")
conn.execute(
"UPDATE cards SET first_name = ?, last_name = ? WHERE id = ?",
(first or None, last or None, row["id"]),
)
def query(sql: str, params: Iterable[Any] = ()) -> list[sqlite3.Row]:
with _lock:
return connect().execute(sql, tuple(params)).fetchall()
def query_one(sql: str, params: Iterable[Any] = ()) -> sqlite3.Row | None:
rows = query(sql, params)
return rows[0] if rows else None
def execute(sql: str, params: Iterable[Any] = ()) -> None:
with _lock:
conn = connect()
conn.execute(sql, tuple(params))
conn.commit()