Seitenaufrufe werden serverseitig pro (Tag, anonymisiertes Netz, Pfad)
gezaehlt -- nur auf den HTML-Seiten-Routen, nicht auf API/Assets. Die
Client-IP wird sofort maskiert (IPv4 /16, IPv6 /32); die vollstaendige
IP wird nie gespeichert oder weiterverwendet.
Optional loest eine lokale GeoLite2-ASN-DB (Env KVER_GEOIP_ASN) den
Netzbetreiber aus dem bereits anonymisierten Netz auf -- auch dafuer wird
nicht die volle IP herangezogen. Fehlt die DB, bleibt das Feld leer.
Neuer Endpunkt GET /stats/detail liefert die Aggregate; die Tabelle
impression speichert nur Zaehlwerte (kein Eventlog).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>
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>
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>
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>