All checks were successful
Build & Deploy / deploy (push) Successful in 1m24s
Auslesen im Hintergrund: /api/scan antwortet, sobald die Karten freigestellt und gespeichert sind, und stößt die Extraktion als Task an. Ein Zwanzigerstapel blockierte vorher den Upload für die ganze Dauer aller Modellaufrufe. Die Oberfläche zeigt "wird gelesen" und lädt nach, solange etwas offen ist. Zwei Folgen davon sind mitbehandelt: Beim Schreiben der Ergebnisse steht COALESCE, damit ein Handeintrag während des Lesens nicht überschrieben wird, und offene Karten werden beim Start nachgeholt, statt dauerhaft in der Warteschlange zu hängen. Bildaufbereitung des Zuschnitts: - Der Einzug zieht die erkannten Ecken um 1,5 % zur Mitte, damit kein Untergrund im Zuschnitt bleibt. - Die Beleuchtung wird ausgeglichen (Division durch eine weichgezeichnete Fassung), damit das Papier weiß wird statt grau. Dunkle Karten bleiben unangetastet - bei ihnen ist das Dunkle das Papier, kein Schatten. - Das Modell meldet die nötige Drehung im Schema; das gespeicherte Bild wird entsprechend gedreht. Geometrisch ist die Lage nicht bestimmbar. Sammelexport entfernt: Der Button "Alle als vCard" ist weg, mit ihm der Endpunkt /vcf sowie vcard.build_many und db.execute_many, die dadurch keinen Aufrufer mehr hatten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
2.5 KiB
Python
94 lines
2.5 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,
|
|
source TEXT NOT NULL DEFAULT 'batch',
|
|
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)
|
|
_conn.commit()
|
|
return _conn
|
|
|
|
|
|
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()
|
|
|