VoteBroker Tech — 31. Mai 2026

in #votebroker15 hours ago

VoteBroker — Technischer Rückblick 31. Mai 2026

Entwurf — Vor der Veröffentlichung manuell überprüfen.
Keine Secrets, Keys, Infrastruktur-Details oder private Daten veröffentlichen.


Geänderte Bereiche

  • Root cause: In-memory state lost after container restart
  • Umsetzung: SQLite Persistence Layer
  • Hydration Race-Condition Fix
  • Smoke Test — Vollständiger Persistenz-Test
  • Ergebnis
  • Vote-DNA Tab: Erste Überarbeitung (Analyse → Onboarding-Tool)
  • Bugfix: Vote-DNA API nicht erreichbar (Failed to fetch)
  • Vote-DNA Strategy Editor: Editierbare Strategie-Tabelle

Architektur-Entscheidungen

Keine Architektur-Entscheidungen dokumentiert.


Root Causes & Fixes

Root cause: In-memory state lost after container restart

Problem

sessionStore.ts und consentStore.ts verwendeten Map<> im Prozessspeicher.
Bei jedem Container-Neustart wurden alle Sessions und Consents gelöscht:

  • Nutzer wurden automatisch ausgeloggt
  • Vote-Consent musste nach jedem Restart erneut erteilt werden
  • Strategie-Regeln existierten nur im Browser (localStorage)
  • Posting-Authority wurde bei jedem Vote erneut vom Steem-Node abgefragt

Symptome für den Nutzer: Generischer "network error" beim Voten nach Server-Neustart.


Hydration Race-Condition Fix

Problem: localStorage-Daten konnten API-Daten überschreiben wenn der Debounce-Timer feuerte bevor die API-Antwort ankam.

Lösung: strategyHydrated-Flag. Die Debounce-Persistenz startet erst nachdem getPersistedStrategy() abgeschlossen ist. Reihenfolge garantiert:

Login
  → getPersistedStrategy() [wartet]
  → setStrategyRules(apiData)
  → setStrategyHydrated(true)
  → ab jetzt: Änderungen → debounce → persistStrategy()

Bugfix: Vote-DNA API nicht erreichbar (Failed to fetch)

Problem

api.ts hatte const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:3000". Die .env-Datei mit VITE_API_BASE=/api lag im Monorepo-Root /opt/votebroker-modern/.env, aber Vite liest .env nur aus dem Workspace-Verzeichnis apps/web/. Damit fiel der Build auf http://localhost:3000 zurück – vom Browser aus nicht erreichbar. Der Fehler wurde zusätzlich durch


Tests & Verifikation

Smoke Test — Vollständiger Persistenz-Test

Datum: 2026-05-31
Durchgeführt: Programmatisch via API-Calls und SQLite-Direktabfragen

Testablauf

Vorbedingung: Clean-Slate für smoketest-user in allen Tabellen.

Schritt 1 — Login:

  • Session direkt in SQLite erstellt (simuliert SteemConnect OAuth)
  • GET /api/auth/me → 200 OK, {"username":"smoketest-user"}

Schritt 2 — Alle Consents erteilt:

  • POST /api/consents/grant für login, target_vote, fee_post_vote, auto_vote
  • Alle 4 in consents-Tabelle persistiert

Schritt 3+4 — Strategy generiert + manuell überschrieben:

  • 5 Regeln: steemchiller (lieblingsautor), gtg (bevorzugt), kingscrown (normal), jan-philippvieth (immer_voten, MANUAL), spammer99 (ignorieren)
  • POST /api/strategy{"ok":true,"savedRules":5}
  • manuallyModified: true für @jan-philippvieth gesetzt

Schritt 5 — Pre-Restart-Verifikation:

  • DB-Inhalt bestätigt: 1 Session, 4 Consents, 5 Strategy-Regeln

**Schritt 6 — Co

| Session State | ✓ PASS — Session überlebt Neustart |
| Consent State | ✓ PASS — Alle 4 Consents erhalten |
| Strategy State | ✓ PASS — Alle 5 Regeln erhalten |
| Manual Override | ✓ PASS — manuallyModified für @jan-philippvieth erhalten |
4/4 Tests bestanden. Persistence-Milestone abgeschlossen.


Technische Details

  • Nutzer wurden automatisch ausgeloggt
  • Vote-Consent musste nach jedem Restart erneut erteilt werden
  • Strategie-Regeln existierten nur im Browser (localStorage)
  • Posting-Authority wurde bei jedem Vote erneut vom Steem-Node abgefragt
  • getDb() — Singleton-Verbindung, lazy-initialized
  • WAL-Modus für bessere Concurrent-Reads
  • initSchema() — erstellt Tabellen beim ersten Start
  • pruneExpiredSessions() — bereinigt abgelaufene Sessions beim Start
  • DB-Pfad konfigurierbar via VOTEBROKER_DB_PATH (Standard: ./data/votebroker.db)
  • ESM-safe via createRequire für better-sqlite3 (CJS-natives Modul)
  • SQLite statt in-memory Map
  • API identisch zu vorher: createSession / getSession / deleteSession
  • SQLite statt in-memory Map
  • API identisch: grantConsent / revokeConsent / hasConsent / getConsentState
  • loadStrategy(username) — lädt JSON-Array aus DB

Nächste technische Schritte

  • fetchRecentPostsWithVotes(author, voter, limit) — ruft get_discussions_by_blog vom Steem-Node ab
  • Filtert Reblogs (nur eigene Posts des Autors)
  • Prüft active_votes auf Voter-Username → alreadyVoted
  • Berechnet Eligibility: nicht bereits gevoted, min. 5 Min. alt, max. 6.5 Tage alt (vor Payout-Lock)
  • Gibt PostOpportunity[] zurück
  • POST /api/curation/opportunities — nimmt { authors: string[], voterUsername: string }

Stack-Überblick

  • Runtime: Node.js 20 (Alpine Docker)
  • API: Fastify + TypeScript (ESM/NodeNext)
  • Persistence: SQLite via better-sqlite3 (WAL mode)
  • Frontend: React 18 + Vite + TypeScript
  • Deployment: Docker + Caddy reverse proxy
  • Auth: SteemConnect OAuth (in-house session management)

💬 Engagement für Tech-Leser:

  • Interessieren dich technische Deep-Dives zu SQLite, ESM-Interop oder Fastify-Architektur?
  • Hast du Feedback zur gewählten Persistence-Strategie (SQLite vs. Redis/Postgres)?
  • Welche Architektur-Entscheidungen möchtest du in einem eigenen Post erklärt haben?
  • Fragen oder Kritik zum Stack? Gerne in die Kommentare.

Dieser Beitrag ist ein technischer Einblick in die VoteBroker-Entwicklung.