Commit Graph

66 Commits

Author SHA1 Message Date
irrlicht bbd4bc27ba Compose: Button "Posten" -> "Eintrag erstellen"
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:23:09 +02:00
irrlicht def6a13782 Compose: Bild aus Zwischenablage per Strg+V einfügen
paste-Listener am Formular übernimmt ein Clipboard-Bild in den File-Input
(via DataTransfer), benennt es zu zwischenablage.<ext> und zeigt einen
grauen Hinweis über dem Datei-Input. Kein Bild -> normaler Text-Paste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:22:14 +02:00
irrlicht e7a7ef8d20 Tagline: Body-Textgröße, schwarz, enger an den Titel
font-size auf 1rem, Farbe schwarz, margin 0 (entfernt den 0.2rem-Abstand
zum Titel; verbleibender Mini-Abstand stammt aus dem h1-Margin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:48:40 +02:00
irrlicht d9028a8400 Header: Statuszeile "Ohne Datenverlust seit n Tagen" + Kennzahlen zurückholen
Schlanke Wiederbelebung des alten Uptime-Schriftzugs aus der Python-Version,
jetzt als kleine Zeile direkt unter dem Titel: Tage seit fixem Startpunkt
(13.12.2025, Kontinuität) plus aktuelle Profil- und Beitragszahlen.

- /stats: neuer öffentlicher Endpunkt (COUNT user / COUNT entry WHERE deleted=0)
- app.js: renderStats() rechnet die Tage und füllt #stats per textContent
- index.html: <p id="stats" class="tagline"> unter der h1
- app.css: kleine graue .tagline-Zeile

Kein Inline-JS/CSS -> strikte CSP bleibt unangetastet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:46:05 +02:00
irrlicht 21911e84e0 auth: Unicode-Usernames in sanitizeUsername erhalten
reNonWord nutzte \w, das in Go-RE2 ASCII-only ist und Nicht-ASCII-Buchstaben
(z. B. den migrierten Namen "佳紫达尔") restlos entfernte. Da sanitizeUsername
auch auf die Login-Eingabe angewandt wird, konnten sich solche -- aus der
Python-Prod-DB migrierten -- Konten gar nicht mehr anmelden und nicht umbenannt
werden. Ersetzt durch \p{L}\p{N} (Buchstaben/Ziffern aller Schriften).

Zusätzlich Längenkürzung auf Runen- statt Byte-Basis, damit der 32er-Schnitt
nicht mitten in eine Multibyte-Rune fällt und kaputtes UTF-8 erzeugt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:21:14 +02:00
irrlicht 0089bc5909 feed.js: HTML-Injection in linkify schließen (escapeHtml deckt nun Quotes ab)
escapeHtml nutzte den textContent->innerHTML-Trick, der laut HTML-Spec nur
& < > escaped, aber keine Anführungszeichen (in Textknoten harmlos). linkify
steckt den Wert anschließend in ein href="..."-Attribut, wo ein rohes " das
Attribut vorzeitig schließt und beliebige Attribute (on...=, style=) am <a>
einschleusbar macht -- HTML-Injection. Die CSP (default-src 'self', kein
'unsafe-inline') verhinderte nur die Ausführung, nicht die Injektion.

Ersetzt durch einen spec-korrekten Entity-Escaper, der auch " und ' abdeckt,
sodass der Wert in Text- UND Attributkontext sicher ist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 07:52:18 +02:00
irrlicht 85c283404d Legal-Docs: Header an Hauptseite angleichen, app.css statt main.css
- Header-Markup wie web/index.html (nav#legal + zentrierte h1), gleiche
  Schrift/Beton-Hintergrund, weil jetzt dasselbe /css/app.css geladen wird
- Inhalt in <main><div class="doc"> -> weiße Spalte mit 5px Padding
- verwaiste static/css/{main.css,main.scss,normalize.css} entfernt (wurden
  nur von den Docs referenziert)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:18:50 +02:00
irrlicht 642fe9466a security.txt (RFC 9116) + aktualisierte Rechtsdokumente
- security.txt unter /.well-known/ mit Contact, Expires, Canonical (kver.xyz)
- Impressum + Beitragsordnung aus aktueller Prod übernommen (sauberes HTML
  ohne Jinja/htmx/jQuery-Reste, § 5 DDG statt TMG)
- fehlende normalize.css ergänzt (wurde von allen Docs referenziert -> 404)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:05:32 +02:00
irrlicht 7488b849db migrate.sh: /srv/kver/-Pfade aus alter Python-Prod-Umgebung normalisieren
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 20:49:47 +02:00
irrlicht 8d9bcd08ea deploy.md: Caddy-Config, Port 7777, Upload-Anleitung überarbeitet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 20:11:38 +02:00
irrlicht 99cc1f0ebb deploy.md: Healthcheck in Quadlet-Unit statt Containerfile (OCI kennt kein HEALTHCHECK)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:54:58 +02:00
irrlicht 50be342899 Deployment-Härtung + Podman-Container
- http.Server mit Timeouts (Slowloris) und Graceful Shutdown (SIGTERM)
- Adresse und DB-Pfad per Env (KVER_ADDR, KVER_DB)
- Security-Header global: nosniff, X-Frame-Options, Referrer-Policy, CSP
- Upload-Requests hart auf 17 MB gedeckelt (MaxBytesReader statt nur
  Multipart-Speichergrenze)
- Indizes für Feed-, Thread- und Vote-Queries
- middleware.RealIP für Logging/Rate-Limit hinter dem Reverse-Proxy
- Containerfile (Multi-Stage, Alpine, non-root) + Deploy-Doku in notes/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 07:00:09 +02:00
irrlicht 925b00edf5 Bild-Upload: animierte GIFs erhalten statt aufs erste Frame reduzieren
Animierte GIFs (mehr als 1 Frame) werden jetzt unverändert durchgereicht
statt via Decode+Re-Encode auf das erste Frame reduziert. Da die Bytes
dabei nicht mehr sanitisiert werden, zwei Guardrails:

- gif.DecodeAll läuft über einen io.LimitReader (16 MB) und prüft danach
  ein Frame-Budget (1000) sowie ein Gesamtpixel-Budget (100 MP) -> bremst
  den DoS-Vektor (GIF-Bombe mit vielen Frames). copyUpload schreibt die
  Bytes unter serverseitig erzeugtem Namen und deckelt hart bei 16 MB.
- noSniff-Middleware setzt X-Content-Type-Options: nosniff auf /static/*,
  damit der Browser eine durchgereichte Datei nicht als HTML/JS deutet
  (Polyglot/Stored-XSS).

Standbilder (JPEG/PNG/Einzelbild-GIF) behalten den skalierenden
Re-Encode-Pfad und damit die Sanitisierung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 07:06:54 +02:00
irrlicht ad2c08d93a Thread-Layout: Strich als Flex-Geschwister, mittig unter dem Avatar
Strich neben dem Content wird über ein eigenes Flex-Element mit
align-self: stretch gezeichnet statt über eine Border am Content –
dadurch exakt so hoch wie der Inhalt, kein Über-/Unterstehen mehr.
Vorfahren und Antworten teilen denselben Mechanismus (Antworten ohne
Border). Strichposition und -dicke über CSS-Variablen an die halbe
Avatargröße gekoppelt, sodass die Linie mittig unter dem Byline-Avatar
sitzt. Antworten-Label in die Byline verschoben, vertikaler Abstand
unter den .body-Container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 06:49:56 +02:00
irrlicht f97a44452f Add prod_backup to gitignore 2026-06-10 06:12:21 +02:00
irrlicht 15ef7db5f7 Profilbilder: Avatar-Upload, Anzeige auf Profil & Feed
- user.avatar-Spalte (Schema, migrate.sh, Migrationsdoku)
- POST /user/avatar (geschützt): Upload via storeImage, ersetzt/löscht altes Bild
- avatar in /u/{name}/info, /user/info und im entrySelect-Join (Feed-Karten)
- Account-Löschung entfernt auch das Avatar-File
- Frontend: Avatar im Profilkopf + Byline der Karten (50px), Upload-Form,
  Platzhalterbild no_profile_pic.jpg als Fallback
- .card img per :not(.avatar) von Beitragsbildern getrennt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:20:14 +02:00
irrlicht cad1fa5c5c Infinite Scroll: nachladen, bis Sentinel aus dem Viewport wandert
Der IntersectionObserver feuert nur bei Zustandswechseln. Füllte die erste
Feed-Seite den Viewport nicht über den Sentinel hinaus (Hauptfeed mit 198
Roots), kam es nie zu einem Übergang und damit nie zum Nachladen. loadMore
prüft jetzt nach jedem Fetch, ob der Sentinel noch sichtbar ist, und lädt
aktiv weiter; rootMargin 300px lädt zudem vorausschauend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:05:24 +02:00
irrlicht 466049ee9d User-Page: Profil & Posts in einem weißen Container
Profilname, "Mitglied seit", Einstellungen und Feed liegen jetzt in einer
gemeinsamen weißen section statt als getrennte Kästen mit Textur-Lücken.
"Beiträge"-Überschrift entfernt (samt JS-Logik für Deine/Beiträge von …).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 17:32:08 +02:00
irrlicht b4d03f8741 Frontend: Kommentarbereich, Ladezeit & Interaktionsleiste
- "Antworten"-Überschrift entfernt; direkte Antworten sind eine flache Liste
- Thread-Strich nur noch bei der Ahnenkette (#ancestors), Antworten nur eingerückt
- font-display: swap + korrekte format()-Hinweise -> kein FOIT mehr beim Laden
- Interaktionsleiste als Grid: Vote-Buttons zentriert, "n Antworten" links daneben

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 17:21:28 +02:00
irrlicht 1313e9613c Sicherheit & Robustheit härten
- SQLite-DSN: busy_timeout(5000) + WAL + foreign_keys gegen SQLITE_BUSY
- uid ist nie 0 mehr (0 = Sentinel für gelöscht/anonym), Retry bei Kollision
- scanEntries gibt Scan-Fehler zurück statt Zeilen still zu überspringen
- Session-timeout serverseitig auf 30 Tage gedeckelt (clientgesteuert)
- Bild-Upload: Dimensionen vor Decode prüfen (Decompression-Bomb-Schutz)
- Secure-Cookie via TLS-Erkennung (r.TLS / X-Forwarded-Proto)
- Rate-Limit (httprate, 20/min/IP) auf Login & Registrierung
- Vote nur auf existierende Beiträge (keine Waisen-Votes)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 17:07:17 +02:00
irrlicht 5478e92253 Migration, Schema-NOT-NULL & klickbare Karten
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 <noreply@anthropic.com>
2026-06-07 17:07:05 +02:00
irrlicht 720d5d64d5 Header: Doku-Links, Profil-Button & Titel "Kontrollverlust"
Impressum/Datenschutz/Beitragsordnung als Links ueber der Ueberschrift,
Ueberschrift in "Kontrollverlust" umbenannt (verlinkt auf den Feed),
"mein Profil" und "zurueck zum Feed" im Button-Layout. datenschutz.html
aus bb6b966 wiederhergestellt, damit der Link nicht ins Leere fuehrt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:23:41 +02:00
irrlicht 2160920595 Design: Textur, EB-Garamond/Cheltenham-Fonts & Designsprache aus Python-Branch
Concrete-Textur als Hintergrund, EB Garamond als Fließtext und Cheltenham
Classic für die Überschriften, weiße zentrierte Inhaltsspalte, umrandete
Buttons und Threadline-Optik fuer Ahnen/Antworten -- uebernommen aus der
verwaisten Python-Linie bb6b966.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:23:41 +02:00
irrlicht ad469ab366 Bearbeiten: POST /entry/{pid}/edit (Inhalt ändern)
- handleEditEntry: Eigentümerprüfung, nicht-leer, content-only Update
- Bild und last_activity bleiben unverändert; gelöschte Posts nicht editierbar
- Frontend: Bearbeiten-Button + Inline-Formular auf der Focus-Seite

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:33:22 +02:00
irrlicht cb3da3bb24 Soft-Delete: Beiträge als [deleted] erhalten statt löschen
- entry.deleted; entrySelect LEFT JOIN + COALESCE(username)
- POST /entry/{pid}/delete: deleted=1, Inhalt/Bild/Autor geleert, Zeile bleibt
- Konto-Löschung soft-deletet die eigenen Beiträge (statt hartem DELETE),
  damit fremde Antworten nicht verwaisen
- Frontend: [deleted]-Platzhalter im Feed, Löschen-Button auf Focus-Seite
- notes/migrations.md: ALTER TABLE deleted

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:21:42 +02:00
irrlicht ba085eff4e Threading: Antworten (reply_to) + Focus-View
- entry: reply_to/reply_count/last_activity; reply_count bubbelt bis Root
- Hauptfeed nur Roots nach last_activity, Profil-Feed inkl. Antworten
- GET /entry/{pid}/thread (Ahnen+Antworten), GET /e/{pid} Focus-Seite
- Frontend: Antworten-Link im Feed, entry.html/entry.js Focus-Seite
- notes/migrations.md: ALTER TABLE fuer Prod

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:21:55 +02:00
irrlicht 844bbb44e7 Frontend+API: Namen ändern (POST /user/rename)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:47:44 +02:00
irrlicht 7ffc35808a User 2026-06-02 16:35:47 +02:00
irrlicht 2dcba21250 Frontend: roher Oldschool-Look, CSS auf 6 Zeilen
- Dark-Theme, Farbvariablen, Border-Radius, custom Form-Controls und
  Layout-Zentrierung entfernt -> natives Browser-Default-Rendering
- nur noch: 1px-Rahmen um Beitraege (eckig), graue Nebentexte, rote
  Fehler, fett fuer gewaehlte Stimme
- Textarea-Hoehe via rows-Attribut, <details> nativ (Danger-Klasse weg)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 10:47:02 +02:00
irrlicht f442138043 Frontend: CSS auf allgemeine Klassen verschlankt (82 Zeilen)
- ID-Selektoren (#account-info, #delete-form button) und verschachtelte
  Selektoren (.entry .meta, .interactions .vote) entfernt
- wiederverwendbare Klassen: .card, .muted, .row, .ghost
- globale a/h1/h2-Regeln, Farben als CSS-Variablen (--border, --danger)
- HTML/JS synchron umgestellt; counts-Span via querySelector("span")
- Cross-Check: keine toten CSS-Klassen, keine unbekannten Referenzen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 10:29:40 +02:00
irrlicht d1a269db2f Frontend: Account-Bereich (GET /user/info, POST /user/delete)
- Account-Section nur sichtbar wenn eingeloggt; zeigt Nutzername und
  Mitglied-seit-Datum aus /user/info
- Konto loeschen in aufklappbarer Danger-Zone, Passwortbestaetigung
  plus zusaetzliche confirm()-Rueckfrage (unwiderruflich)
- nach Loeschung zurueck zur Login-Ansicht via refreshHeader

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:59:53 +02:00
irrlicht a47f41bbfa Frontend: Registrierung angebunden (POST /auth/newuser)
- Umschalter Login <-> Registrieren im Auth-Bereich
- Fehlerlisten-Anzeige ({"errors": [...]}) fuer newuser
- nach erfolgreicher Registrierung automatischer Login
- Logout setzt Ansicht auf Login zurueck

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:47:00 +02:00
irrlicht 2ab08ebf9a Alten Flask/Python-Code und Jinja-Templates entfernt
Go-Reimplementierung hat Feature-Paritaet (inkl. URL-Linkify im
JS-Frontend via linkify()). static/ bleibt (Go liefert /static/* aus
und legt Uploads unter static/media/ ab); alte Flask-Assets dort
unberuehrt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:37:47 +02:00
irrlicht e210df3e2e Code-Cleanup: toter Check, none-Sentinel, Login-Jitter raus
- entry.go: redundantes stored != "" entfernt
- filepath-Sentinel "none" -> leerer String (Go-Zero-Value),
  Checks in entry.go/user.go und web/js/app.js vereinfacht
- auth.go: zufaelligen Login-Jitter samt loginJitter-Variable und
  Test-Seam entfernt (war nur Spielerei, kein echter Schutz)
- notes/migrations.md: einmalige "none"->"" Migration dokumentiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:33:41 +02:00
irrlicht bee1305a2b Add endpoint tests with httptest
Two small refactors make the handlers testable:
- extract route registration into routes() http.Handler (was inline in main)
- initDB(dsn) takes a DSN so tests use an isolated temp-file DB

loginJitter var lets tests disable the anti-timing sleep for speed.

endpoints_test.go covers the main flows via httptest + cookie jar:
register/login/headerbar, empty-then-populated feed, create requires auth,
voting (new/toggle/switch, per-user tallies), vote auth + invalid mode,
and account deletion (wrong/correct password, login fails after).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:16:02 +02:00
irrlicht 38da498754 Split voting into clean read/write endpoints
Replaces the GET-mutates-state interactions route with a proper split:
- GET  /entry/{pid}/votes  reads the tally (public, read-only)
- POST /entry/{pid}/vote   casts/toggles the vote (auth, mode in body)

Shared voteTally/writeTally helpers back both handlers. Invalid mode now
returns 400. Frontend updated to read via GET and vote via POST.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:03:33 +02:00
irrlicht be8dbb4902 Implement left/right voting on entries
GET /entry/{pid}/interactions/{mode} now reads the vote tally and, for
mode=left/right (auth required), toggles the user's vote: new vote, repeat
same mode removes it, different mode switches. Returns {left, right,
selected}. A UNIQUE(uid, pid) constraint on the vote table prevents
duplicate votes. The JS frontend renders Links/Rechts buttons with live
counts under each entry.

This completes the voting feature that was left commented-out and broken
in the Flask version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:58:58 +02:00
irrlicht c904e3e2db Implement user account deletion flow
POST /user/delete (auth required) verifies the password via pass1 and
transactionally removes the user's sessions, votes, entries and the user
row, then clears the session cookie. Associated media files are removed
best effort after commit. This was only a stub in the Flask original.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:19:29 +02:00
irrlicht cb237c248e Rewrite backend in Go with JSON API and JS frontend
Reimplements the Flask app as a Go HTTP API (chi + modernc sqlite) with a
minimal vanilla-JS frontend in web/. Endpoints mirror the original Flask
routes but return JSON instead of HTML.

- auth: login/logout/register/sessioninfo/headerbar with crypto/rand tokens
- entry: paginated feed (single JOIN), create with image scaling
- user: profile, userinfo; delete still a stub
- requireAuth middleware passes uid via context
- notes/api.md documents the API and schema

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:17:02 +02:00
irrlicht c2a8199a84 Moved user creation to user_controller.new() 2025-12-23 01:15:15 +01:00
irrlicht 6f6d05fe31 Added Semicolon to URL-Regex, so Youtube-Links with List-Index are properly shown 2025-12-22 23:53:35 +01:00
irrlicht 3d9eae89de Removed old stuff 2025-12-22 23:33:39 +01:00
irrlicht 782e608db4 Revert "Removed old stuff"
This reverts commit 45e66ca7e6.
2025-12-20 05:27:10 +01:00
irrlicht 45e66ca7e6 Removed old stuff 2025-12-20 03:50:00 +01:00
irrlicht a3afb0726f ? 2025-12-20 03:46:45 +01:00
irrlicht 8c2429732e Margin of .content 2025-12-20 03:46:25 +01:00
irrlicht a180fa6a74 Added fetchone Option to database.fetch 2025-12-20 03:45:42 +01:00
irrlicht 37c3c8dac8 Added content filter for max len and URLs 2025-12-17 22:14:30 +01:00
irrlicht 01e6e92c78 Impressum 2025-12-17 04:34:31 +01:00
irrlicht 08008b5c70 Beitragsordnung 2025-12-17 04:26:55 +01:00