From 5478e92253773100ebc178472e8ec8ce7b6bc287 Mon Sep 17 00:00:00 2001 From: irrlicht Date: Sun, 7 Jun 2026 17:07:05 +0200 Subject: [PATCH] Migration, Schema-NOT-NULL & klickbare Karten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit migrate.sh erzeugt nicht-destruktiv eine frische kver.db aus original-kver.db mit dem aktuellen Go-Schema (NULL/'none'-filepath normalisiert, Soft-Deletes und Waisen verworfen). Das entry-Schema erzwingt jetzt NOT NULL DEFAULT '' auf content/filepath – NULL-filepath ließ scanEntries Zeilen still überspringen. Feed-Karten sind per Klick navigierbar (mit Hover-Highlight); DB-Artefakte landen im .gitignore. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 2 + db.go | 12 +-- migrate.sh | 188 ++++++++++++++++++++++++++++++++++++++++++++ notes/migrations.md | 16 ++++ web/css/app.css | 5 ++ web/js/feed.js | 6 ++ 6 files changed, 223 insertions(+), 6 deletions(-) create mode 100755 migrate.sh diff --git a/.gitignore b/.gitignore index 61106d2..df8c6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ venv/ kver.db +kver.db.bak +original-kver.db /kver *.env __pycache__/ diff --git a/db.go b/db.go index dcf8b88..bb3e0f1 100644 --- a/db.go +++ b/db.go @@ -29,14 +29,14 @@ CREATE TABLE IF NOT EXISTS session ( CREATE TABLE IF NOT EXISTS entry ( id INTEGER PRIMARY KEY AUTOINCREMENT, - pid INTEGER UNIQUE, - uid INTEGER, - created_at INTEGER, - content TEXT, - filepath TEXT, + pid INTEGER UNIQUE NOT NULL, + uid INTEGER NOT NULL, + created_at INTEGER NOT NULL, + content TEXT NOT NULL DEFAULT '', + filepath TEXT NOT NULL DEFAULT '', reply_to INTEGER NOT NULL DEFAULT 0, reply_count INTEGER NOT NULL DEFAULT 0, - last_activity INTEGER, + last_activity INTEGER NOT NULL DEFAULT 0, deleted INTEGER NOT NULL DEFAULT 0 ); diff --git a/migrate.sh b/migrate.sh new file mode 100755 index 0000000..d1b0109 --- /dev/null +++ b/migrate.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# +# migrate.sh — liest kver-original.db und erzeugt daraus eine frische kver.db +# mit dem aktuellen Go-Schema. Die Quelldatenbank wird nie verändert. +# Idempotent: kver.db wird bei jedem Lauf neu erstellt. +# +# Verwendung: +# ./migrate.sh [quelldatei [zieldatei]] # Standard: kver-original.db -> kver.db +# +# Schalter (als Umgebungsvariablen): +# REPLIES=keep|drop Antworten (reply_to != 0) übernehmen oder verwerfen (Default: keep) +# +set -euo pipefail + +SRC="${1:-original-kver.db}" +DST="${2:-kver.db}" +REPLIES="${REPLIES:-keep}" + +if [[ ! -f "$SRC" ]]; then + echo "FEHLER: Quelldatenbank '$SRC' nicht gefunden." >&2 + exit 1 +fi +if [[ "$REPLIES" != "keep" && "$REPLIES" != "drop" ]]; then + echo "FEHLER: REPLIES muss 'keep' oder 'drop' sein (war: '$REPLIES')." >&2 + exit 1 +fi +if [[ "$SRC" -ef "$DST" ]]; then + echo "FEHLER: Quelle und Ziel sind dieselbe Datei." >&2 + exit 1 +fi + +qs() { sqlite3 "$SRC" "$1"; } +qd() { sqlite3 "$DST" "$1"; } + +echo "== kver-Migration ==" +echo "Quelle: $SRC" +echo "Ziel: $DST" +echo "REPLIES: $REPLIES" +echo + +echo "Quelle:" +echo " Einträge gesamt: $(qs 'SELECT COUNT(*) FROM entry;')" +echo " davon deleted=1: $(qs 'SELECT COUNT(*) FROM entry WHERE deleted=1;')" +echo " davon Antworten: $(qs 'SELECT COUNT(*) FROM entry WHERE reply_to!=0;')" +echo " User: $(qs 'SELECT COUNT(*) FROM user;')" + +# Zieldatenbank immer frisch anlegen +rm -f "$DST" +echo +echo "Erstelle $DST mit aktuellem Schema..." + +sqlite3 "$DST" <<'SCHEMA' +.bail on +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid INTEGER UNIQUE, + username TEXT UNIQUE, + password BLOB, + created_at INTEGER, + last_login INTEGER +); +CREATE TABLE session ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid INTEGER, + value TEXT UNIQUE, + created_at INTEGER, + expires INTEGER, + description TEXT +); +CREATE TABLE entry ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pid INTEGER UNIQUE NOT NULL, + uid INTEGER NOT NULL, + created_at INTEGER NOT NULL, + content TEXT NOT NULL DEFAULT '', + filepath TEXT NOT NULL DEFAULT '', + reply_to INTEGER NOT NULL DEFAULT 0, + reply_count INTEGER NOT NULL DEFAULT 0, + last_activity INTEGER NOT NULL DEFAULT 0, + deleted INTEGER NOT NULL DEFAULT 0 +); +CREATE TABLE vote ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid INTEGER, + pid INTEGER, + mode TEXT, + UNIQUE(uid, pid) +); +SCHEMA + +# Prüfen, welche optionalen Spalten in der Quelle vorhanden sind. +# Ältere Python-DBs haben reply_count und last_activity noch nicht. +has_col() { + sqlite3 "$SRC" "SELECT COUNT(*) FROM pragma_table_info('entry') WHERE name='$1';" +} + +reply_count_expr="0" +[[ "$(has_col reply_count)" == "1" ]] && reply_count_expr="COALESCE(e.reply_count, 0)" + +last_activity_expr="e.created_at" +[[ "$(has_col last_activity)" == "1" ]] && last_activity_expr="COALESCE(e.last_activity, e.created_at)" + +replies_filter="" +[[ "$REPLIES" == "drop" ]] && replies_filter="AND e.reply_to = 0" + +has_vote_table=$(qs "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='vote';") + +# Quell-Pfad für SQL-String-Literal escapen (einfache Anführungszeichen verdoppeln) +src_sql="${SRC//\'/\'\'}" + +sqlite3 "$DST" < '' (Go-Zero-Value; scanEntries erwartet string, nicht *string) +-- content NULL -> '' +-- deleted: nur nicht-gelöschte Einträge werden übernommen +-- orphans: Einträge ohne bekannten User werden verworfen +INSERT INTO entry (pid, uid, created_at, content, filepath, reply_to, reply_count, last_activity, deleted) +SELECT + e.pid, + e.uid, + e.created_at, + COALESCE(e.content, ''), + COALESCE(NULLIF(e.filepath, 'none'), ''), + COALESCE(e.reply_to, 0), + $reply_count_expr, + $last_activity_expr, + 0 +FROM src.entry e +WHERE e.deleted = 0 + AND e.uid IN (SELECT uid FROM src.user) + $replies_filter; + +-- Sessions leer lassen: Alt-Tokens sind Integer (Python-Format), +-- Go erzeugt crypto/rand-Hex-Tokens -> alle Alt-Sessions sind wertlos. + +COMMIT; +DETACH src; +VACUUM; +SQL + +# Votes separat, da die Tabelle in alten DBs fehlen kann +if [[ "$has_vote_table" == "1" ]]; then + sqlite3 "$DST" < static/media/ aus der Produktion kopieren, sonst liefern diese Bilder 404." +fi + +echo +echo "Fertig." diff --git a/notes/migrations.md b/notes/migrations.md index 936eb16..401ac93 100644 --- a/notes/migrations.md +++ b/notes/migrations.md @@ -5,6 +5,22 @@ Eine frische DB (Schema aus `db.go`) braucht keine davon. --- +## `filepath` NULL → leerer String + +**Wann:** beim Umstieg auf die Go-Version, falls `entry.filepath` NULL-Werte enthält. + +**Hintergrund:** `scanEntries` scannt `filepath` in ein Go-`string`-Feld (kein Pointer). +SQLite NULL in ein `string` scannen schlägt fehl; `scanEntries` überspringt diese Zeilen +still mit `continue`. Ergebnis: Einträge ohne Bild fehlen im Feed komplett. +Das aktuelle Go-Schema definiert `filepath TEXT NOT NULL DEFAULT ''`. `migrate.sh` +normalisiert NULL → `''` beim Kopieren aus der Quelldatenbank. + +```sql +UPDATE entry SET filepath = '' WHERE filepath IS NULL; +``` + +--- + ## `filepath`-Sentinel `"none"` → leerer String **Wann:** beim Umstieg auf die Go-Version, falls eine alte `kver.db` aus der diff --git a/web/css/app.css b/web/css/app.css index 308d165..3c6aa86 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -133,6 +133,7 @@ a.button:hover, button:hover { /* --- Einträge --- */ .card { position: relative; + cursor: pointer; background-color: white; padding: 0.5rem; margin-bottom: 0; @@ -141,6 +142,10 @@ a.button:hover, button:hover { border-bottom: 1px solid #ccc; } +.card:hover { + background-color: #f7f7f5; +} + .card .content { padding: 0.5rem 0; line-height: 1.5; diff --git a/web/js/feed.js b/web/js/feed.js index 080311f..6811834 100644 --- a/web/js/feed.js +++ b/web/js/feed.js @@ -44,6 +44,9 @@ function renderEntry(e) {
[deleted]
`; + el.addEventListener("click", (ev) => { + if (!ev.target.closest("a, button")) location.href = `/e/${e.pid}`; + }); return el; } @@ -65,6 +68,9 @@ function renderEntry(e) { `; setupVoting(el, e.pid); + el.addEventListener("click", (ev) => { + if (!ev.target.closest("a, button")) location.href = `/e/${e.pid}`; + }); return el; }