Alle nennenswerten Änderungen an diesem Projekt. Format lose nach Keep a Changelog; Versionierung nach Semantic Versioning.
Der Schwerpunkt dieser Version ist Richtigkeit: zwei Kennzahlen auf der Oberfläche waren nachweislich falsch, und zwar nicht knapp.
- Kosten waren rund 8,5 % zu niedrig.
usage.cache_creationwurde nie gelesen, also lief jeder Cache-Schreibvorgang zum 5-Minuten-Satz (1,25× Eingabepreis). Laut Anthropic-Preisdoku kostet ein 1-Stunden-Write 2× Eingabe, und 90,5 % aller Cache-Write-Tokens im Datenbestand sind 1-Stunden-Writes. Über das vorhandene JSONL-Fenster nachgerechnet: $32.246,90 → $34.996,18. Parser und Sync-Agent lesen die Aufteilung jetzt aus, zwei additive Spalten halten sie fest,calculateCostrechnet je Stufe. Zeilen ohne erfasste Aufteilung behalten den 5-Minuten-Satz — nachträglich zu verteuern, was niemand mehr belegen kann, wäre schlechter; Oberfläche und Report weisen die Abdeckung aus - „Gesamtzeit" im Projekt-Dialog war keine Zeit. Angezeigt wurde die Summe
der Sitzungs-Spannen: Leerlauf zählte als Arbeit, parallele Sitzungen doppelt,
und eine Sitzung, die den Zeitraum nur überlappte, brachte ihre gesamte
Historie mit. Gemessen 2.344 h in einem 2.062-h-Fenster und 659 h in einem
10-Tage-Filter — mehr Zeit, als überhaupt vergangen war. Die aktive Zeit
kommt jetzt aus einer projektweiten Zeitachse (
computeActiveMinutes), also aus derselben Korrektur, die die Übersicht längst hatte claude-opus-5fehlte in der Offline-Preistabelle — 38.911 Nachrichten und 19,5 Mrd. Tokens, das meistgenutzte Modell, wären ohne LiteLLM zu Sonnet-Preisen abgerechnet worden- Sonnet-5-Preisepoche behauptete eine abgesagte Erhöhung — $2/$10 ist der Standardpreis; die Epoche ist entfernt, der Fallback korrigiert
- Der Projekt-Dialog ignorierte den Cache-Umschalter (13,0 Mrd. neben 26,7 Mio. für dasselbe Projekt) und zählte Sitzungen anders als die Tabelle
- Acht Achievements waren unerreichbar, nicht schwer —
output_ratio_60/70/80verlangten 60–80 % Output-Anteil bei real 0,204 %,model_haiku_majorityeine Haiku-Mehrheit bei 2,9 % Ist-Anteil, dazu vier Streaks über vier bis zehn Jahre. Schlüssel bleiben, Schwellen sind korrigiert und gegen Rückbau gepinnt - Der Backup-Suchpfad übersprang
/root/apps(Betriebs-Skript) — drei Datenbanken lagen in keiner Off-Site-Sicherung, darunter die des Trackers. Zusätzlich vergab das Skript bei gleichnamigen Datenbanken denselben Zieldateinamen und überschrieb die größere stillschweigend
- 500 neue Achievements (700 → 1.200). Schwellen sind abgeleitet, nicht
geschätzt:
heutiger Wert + gemessene Rate × Horizontgegen einen Messstand der echten Historie. Die nächsten liegen etwa drei Wochen Arbeit entfernt, die fernsten rund zwanzig Monate; alle 500 waren bei Auslieferung verschlossen. Drei Bauformen: 414 Leitern mit thematischer Namensfolge, 60 Kombinationen mit zwei gleichzeitigen Bedingungen, 26 Wegmarken - Neue Kennzahlen dafür: echte Arbeitszeit (
totalActiveHours,deepSessions_*,deepDays_*), MCP-Server und Sub-Agenten,cacheSavingsUsd(was die Cache-Reads zum vollen Eingabepreis gekostet hätten, minus was sie kosteten), Modelle je Generation, Projekt-Tiefe nach Aufwand und Rhythmus-Muster - Projekt-Report als HTML und PDF (
GET /api/project-report) — eigenständig, druckoptimiert, ohne CDN und ohne Chart-Bibliothek; Diagramme sind Inline-SVG. PDF über den Druckdialog des Browsers - Rechenweg-Dialog — Info-Symbol an jeder Kennzahl, dazu ein Dialog mit Formeln, dem 5-Minuten-Deckel, der Preisquelle und dem, was bewusst nicht erfasst wird
- Vier Referenzdokumente:
docs/API.md,docs/ARCHITECTURE.md,docs/METRICS.md,docs/CONFIGURATION.md, dazuCONTRIBUTING.mdunddocs/metrik-audit-2026-08-30.mdmit den Messungen hinter den Korrekturen - Doku-Synchronitätstests — jede Route steht in der Referenz, jede
Umgebungsvariable in der Konfiguration und in
.env.example, jede Achievement-Zahl stimmt, kein Link zeigt ins Leere, und die deutsche Langfassung darf der englischen nicht mehr davonlaufen (genau das war passiert: sie stand noch bei 700 Achievements) - Tests für drei bislang ungetestete Module —
lib/cache.js,lib/github.js(inklusive Stale-while-revalidate) undlib/plan-usage.js, dort samt Pin gegen das Auseinanderdriften der Normalisierer in Server und Sync-Agent - Badges werden generiert statt gepflegt — Testzahl, Codezeilen, Achievements, Routen, Tabellen, i18n-Schlüssel und Abhängigkeiten kommen aus dem Code
- Der „Rückdatiert neu berechnen"-Knopf ist entfernt. Der Backfill läuft ohnehin bei frischer Installation und bei jedem Versionssprung des Migrationsmerkers; der Endpunkt bleibt als Wartungsweg
ACH_BACKFILL_FLAGv2 → v3, damit die korrigierten Schwellen ihre Freischaltdaten historisch bekommen- Der öffentliche Share-Endpunkt liefert
span_min;total_duration_minist ein Alias der aktiven Zeit, damit bestehende Abnehmer keine unmögliche Zahl mehr bekommen
0.1.0 taucht unten dreimal mit verschiedenen Daten auf, und die als 0.0.4
und 0.0.5 geführten Einträge stammen von nach dem ersten 0.1.0. Es gab
in diesem Zeitraum auch keine GitHub-Releases. Verlässlich ist deshalb allein
das Datum der jeweiligen Abschnitte; die Überschriften bleiben unverändert,
weil das der tatsächliche Verlauf ist. Ab 0.2.0 gilt Semantic Versioning mit
Tag und Release.
Die neuen Tests (Share-API, Watcher, Export, Backup) haben vier echte Fehler aufgedeckt:
- Wildcard-CORS auf allen API-Antworten —
sendJSON()setzte pauschalAccess-Control-Allow-Origin: *. Damit konnte jede Webseite, die der Browser des Nutzers besucht, die (im Single-User-Modus komplett ungeschützte) Dashboard-API auflocalhost:5010auslesen. Schlimmer: weilwriteHead()die persetHeader()gesetzten Header überschreibt, hat der Wildcard auch die bewusste Origin-Allowlist des öffentlichen Share-Endpunkts stillschweigend ausgehebelt — die dokumentierte CORS-Beschränkung aufops.celox.io/tracker.celox.iowar faktisch wirkungslos.sendJSON()setzt jetzt keinen CORS-Header mehr, reicht aber einen zuvor gesetzten durch;/api/live(SSE) und/api/export(voller Datenexport!) verlieren ihren Wildcard ebenfalls. Server-zu-Server-Konsumenten (OPS-Backend, Sync-Agent, curl) sind nicht betroffen — CORS ist reine Browser-Policy /api/shares/projectsliefertelast_activity: undefined— das Feld wurde ausp.lastTsgemappt, dasgetProjects()gar nicht kennt. Der Aggregator führt jetzt pro Projekt ein Aktivitätsfenster (firstTs/lastTs) und gibt es in beidengetProjects()-Zweigen aus- HTML-Export war aus Projektnamen heraus manipulierbar — die eingebettete JSON-Nutzlast wurde unescaped in einen Inline-
<script>-Block geschrieben; ein Ordnername wie</script><script>…(Projektnamen kommen direkt von der Platte) konnte damit aus dem Script-Block ausbrechen.<wird jetzt als<serialisiert (bleibt gültiges JSON, identisches Parse-Ergebnis) - Zwei Backups innerhalb derselben Sekunde scheiterten mit dem nackten SQLite-Fehler „output file already exists" (der Dateiname hat Sekundenauflösung) — jetzt wird ein Zähler-Suffix angehängt
- 17 weitere Tests (296 → 333), zwei neue Testdateien:
test/share-api.test.js(12) — die einzige öffentlich erreichbare Fläche des Trackers: Token-Format (48 Hex, Fehlformat wird vor dem DB-Zugriff abgewiesen), Ablauf, sofortige Sperrung beim Löschen, Admin-Key ist kein Ersatz für ein gültiges Token, sanitisierte Nutzlast (nurlabel/period/summary/daily/sessions— der interne Projektpfad darf nicht durchsickern), Zeitraumfilter, CORS-Allowlist (erlaubter Origin wird gespiegelt, fremder nicht, Preflight) und das Rate-Limit von 30 Requests/Minute pro IP (ein anderer Client bleibt unbehelligt)test/watcher.test.js(11) — bisher ungetestet: dasignored-Prädikat (ein Dotfile-Regex hätte.claudeim Pfad getroffen und alle Events verschluckt), inkrementelles Nachlesen nur der angehängten Bytes, Ignorieren von Nicht-JSONL und Sub-Agent-Transkripten, Überleben einer kaputten Datei, Rate-Limit-Events ohne Token-Nachrichten, sowie die SSE-Verteilung (Frames, Nutzer-Filterung, Auswerfen toter Clients, Abmeldung beiclose). Events werden synthetisch ausgelöst (emit) statt auf echte Dateisystem-Benachrichtigungen zu warten — dadurch deterministisch und schnell in CI- API: statische Auslieferung mit ETag +
304bei Revalidierung (und200bei veraltetem Validator), Dashboard-Shell unter/,/api/config,/api/rebuildbehält die DB-History (Regressionstest für geprunte JSONL),/api/rate-limits,/api/tool-cost-daily, Sessions inkl. aktiver Zeit, Wochentagsverteilung - Export: Escaping von Projekt-/Session-/Modellnamen, Rate-Limit-Sektion, Selbstgenügsamkeit (keine lokalen Asset-Referenzen)
- Backup: 50-%-Schrumpf-Schutz greift, gewachsene Backups werden akzeptiert, kein Überschreiben in derselben Sekunde
- Testdaten-Fixture gehärtet: die Nachrichten des letzten Tages liegen jetzt wenige Minuten vor „jetzt" statt auf festen Uhrzeiten — sonst lagen sie bei einem Lauf früh am Morgen (oder in einer UTC+14-Zeitzone) in der Zukunft, und
getTrends()ignoriert Zukunftsdaten korrekterweise, wodurch „heute" still leer war. Suite läuft jetzt in UTC, Berlin, Los Angeles und Kolkata identisch durch
- CI-Fehlschlag
POST /api/achievements/recompute(expected 0 to be greater than 0) — die API-Tests starteten den Server gegen die echtedata/tracker.dbund das echte~/.claude. Lokal lief das (zufällig) durch, in CI gibt es beides nicht → 0 Nachrichten → 0 Achievements. Nebenwirkungen: die Suite brauchte ~33 s und der Recompute-Test überschrieb die reale Achievements-Tabelle des Entwicklers.DATA_DIR/DB_PATHsind jetzt per Env überschreibbar; die API-Tests booten gegen eine Wegwerf-DB inmkdtemp()mit leeremCLAUDE_DIRund seeden vorher eine deterministische 45-Tage-History (test/fixtures/history.js). Suite: 33 s → 0,6 s, und sie ist reproduzierbar statt vom Rechner abhängig
- 41 neue Tests (255 → 296), überall dort, wo bisher nur die Response-Form geprüft wurde:
- Parser: Rate-Limit-Events (inkl. stabiler, aus Session+Timestamp abgeleiteter ID → Re-Parse dupliziert nicht), Sub-Agent-Erkennung über den
/subagents/-Pfad, Zeilenzählung ausEdit/Write-Tool-Input, Offset-Fortschreibung beim inkrementellen Parsen - DB: Geräte-CRUD (eindeutige API-Keys, Key-Regenerierung invalidiert den alten Key, Löschen eines Geräts verwaist die Nachrichten statt sie zu löschen), Rate-Limit-Events (Duplikate werden ignoriert, leerer Batch), Projekt-Shares (48-stelliges Token, abgelaufene Shares sind öffentlich unsichtbar, bleiben aber verwaltbar, Löschen)
- Aggregator: Vormonats-Cutoff-Clamping (31.03. → 28.02.), Montag als Wochenstart (Sonntag zählt zur abgelaufenen Woche), Zukunftsdaten werden ignoriert, leerer Aggregator liefert ein Null-Payload, Momentum-Einträge ohne Volumen fallen raus,
daily90sind 90 lückenlose aufeinanderfolgende Lokaltage - Achievements: Backfill ist deterministisch (zwei Läufe = identische Daten), nutzt die atomare
replaceAchievementsForUser-API wenn vorhanden, macht bei leerer History nichts (löscht insbesondere nichts) und die Sample-Gates greifen stufenweise (4 aktive Tage: kein Gold; 10: Gold, aber kein Platin/Diamant; 31: alle) - API: echte Zahlen statt Formen — Overview zählt die geseedeten 270 Nachrichten/90 Sessions/45 aktiven Tage,
/api/dailydeckt alle 45 Tage ab und summiert sich zum Overview,daily90endet auf heute, Heatmap-Summe == Overview, Tool-Kosten-Attribution inkl. MCP-Typisierung, MCP-Server-Gruppierung, Sub-Agent-Anteil, öffentliches/api/pricing, Zeitraumfilter verengt das Ergebnis, unbekannte/api/*-Routen liefern 404 - Config:
DATA_DIR/DB_PATH-Auflösung inkl. relativer Pfade
- Parser: Rate-Limit-Events (inkl. stabiler, aus Session+Timestamp abgeleiteter ID → Re-Parse dupliziert nicht), Sub-Agent-Erkennung über den
- Automatisch aktualisierte Badges ganz oben in allen drei READMEs: Tests (aus dem echten Vitest-JSON-Report, nicht geschätzt) und Lines of Code (aus
git ls-filesüber*.js/*.css/*.html).scripts/update-badges.jsschreibt den Block zwischen<!-- BADGES:START/END -->,npm run badgeslokal, und der neue Workflow.github/workflows/badges.ymlrechnet sie bei jedem Push auf main neu und committet sie zurück ([skip ci]). Die handgepflegten Zahlen waren chronisch veraltet (Badge: 238 Tests, Suite: 255; „LOC 25k+" geraten statt gemessen)
- Fünf Vergleichs-Charts unter den Nutzungs-Trends — die vier Trend-Karten beantworten „mehr oder weniger als zuletzt?", die Charts jetzt auch „in welche Richtung, wo und womit?". Alle fünf hängen am selben
/api/trends-Payload (kein zusätzlicher Request, kein zweiter Message-Scan) und folgen Cache- und Token↔Kosten-Toggle:- 90-Tage-Verlauf mit gleitendem Durchschnitt — Tagesbalken + 7-Tage- und 30-Tage-Schnitt: die 7er-Linie glättet das Wochentags-Rauschen, die 30er zeigt die eigentliche Richtung (unvollständige Fenster bleiben leer statt verzerrt)
- Monatsverlauf (kumuliert) — laufender Monat gegen den kompletten Vormonat, plus gestrichelte Hochrechnung aufs Monatsende; kumuliert nur bis heute, damit kommende Tage die Linie nicht flach auslaufen lassen
- Wochenvergleich (Mo–So) — diese vs. letzte Woche je Wochentag; noch kommende Tage bleiben leer statt 0, sonst liest sich der Rest der Woche wie ein Einbruch
- Projekt-Momentum — divergierende Balken (letzte 7 Tage minus die 7 Tage davor) zeigen, welche Projekte zu- bzw. abgenommen haben; Labels tragen die letzten zwei Pfadsegmente (der Leaf allein ist mehrdeutig)
- Modell-Mix-Verschiebung — 100 %-gestapelte Anteile letzte 7 Tage vs. Vor-7-Tage; deckt eine Verschiebung zwischen Modellen auch dann auf, wenn das Gesamtvolumen gleich bleibt
- Backend:
getTrends()liefert aus demselben Scan zusätzlichdaily90(90 lokale Tage, per Datums-String indiziert —ms/86400000würde bei DST einen Tag verschmieren) undmomentum({windowDays, projects[], models[]}mitcur/prevje Eintrag). i18n DE/EN, Tooltips, Demo-Daten, Tests (Aggregator + API-Shape)
Beim Neuaufnehmen der README-Screenshots (alle 10 Desktop- + 5 Mobile-Ansichten, jetzt inkl. der Trend-Ansichten) sind fünf UI-Fehler aufgefallen und behoben worden:
- „All Time"-Kopfzeile zeigte „– Wed 07/22/2026" — der Zeitraum-Header prüfte
!from && !to, aber „Gesamt" hat nur keinfrom, sehr wohl aberto(heute) - Deutsche Zeiteinheiten in der englischen UI — „Active Work Time" rendete
751 Std. 49 Min.statt751h 49m;_formatActiveTime()ist jetzt sprachabhängig (wie in der Doku ohnehin beschrieben) - Projekt-Chart ignorierte den Cache-Toggle —
createProjectBarChart(..., false)war hart verdrahtet, wodurch die Balken (nur Input+Output, 18 M) den Summen der Tabelle direkt darunter (7,9 Mrd.) widersprachen; jetztstate.includeCachewie überall sonst. Der Dataset-Titel „Total Tokens" wird zusätzlich übersetzt - Achse der Tool-Kosten-Attribution unlesbar — schräg gestellte
$0.00 $2000.00 $4000.00 …-Labels; jetzt kompakt ($5k,$10k) mitmaxTicksLimitund ohne Rotation, volle Präzision bleibt im Tooltip - Aktive-Sessions-Karten auf dem Handy zerquetscht — die
min-width: 0-Regel unter 480 px ließ fünf parallele Sessions in einer Flex-Zeile auf je ~50 px schrumpfen (ein Wort pro Zeile); die Karten stapeln jetzt (flex: 1 1 100%)
- Achievements-Timeline: Balken-Klick zeigt Tages-Detail — ein Klick auf einen Balken im „Achievements im Zeitverlauf"-Chart öffnet einen Dialog mit allen an diesem Tag freigeschalteten Achievements (Icon, Name, Beschreibung, Stufe mit Tier-Farbe, Punkte + Tagessumme), sortiert nach Stufe; Cursor wird über Balken zum Pointer, Schließen per ×/Escape/Overlay
- Rückdatierte Achievement-Freischaltung — bisher wurden bei App-Initialisierung auf bestehender Claude-History hunderte Achievements mit „jetzt" als
unlocked_atgestempelt (verzerrte Timeline-Spikes am Init-Tag). Neu:backfillAchievements()replayt die Message-History Tag für Tag (frischer Aggregator wird tageweise gefüttert, nach jedem Tag werden noch offene Achievements geprüft) —unlocked_atlandet auf dem Tag, an dem die Bedingung historisch erstmals erfüllt war. Läuft automatisch bei frischer Initialisierung (0 Unlocks + vorhandene History) statt des Bulk-Unlocks; bestehende (verzerrte) Daten lassen sich per Button „🔄 Rückdatiert neu berechnen" im Achievements-Tab bzw.POST /api/achievements/recomputekorrigieren (atomarer Rewrite in einer Transaktion, kollisionssicher gegen parallele Watcher-Checks). Auto-Migration: bestehende Nutzer mit Vor-Backfill-Daten werden zusätzlich automatisch einmalig migriert (Metadata-Flagach_backfill_v1_<userId>; Single-User beim Start, Multi-User beim ersten/api/achievements-Aufruf) — der 23.02.-Init-Spike verschwindet ohne Nutzeraktion. Sample-Gates für Quoten-Achievements (v2): Durchschnitts-/Quoten-Achievements (avg_*,cache_rate_*,deletion_ratio_*,output_ratio_*,tokens_per_msg_*,tokens_per_dollar_*,msgs_per_session_*,sessions_per_day_*, Modell-Mehrheit/-Treue) brauchen jetzt eine stufenskalierte Mindest-Datenbasis (Bronze 3 / Silber 5 / Gold 7 / Platin 14 / Diamant 30 aktive Tage) — vorher vergab z. B. eine 90%-Cache-Rate über einen einzigen Tag sofort Diamant, wodurch sich ~44 Artefakt-Unlocks auf Tag 1 der History stapelten (95→50). Flag-Bump aufach_backfill_v2_re-migriert bestehende Daten automatisch - 📈 Nutzungs-Trends im Overview — vier live-Karten (Heute · Diese Woche · Dieser Monat · Letzte 7 Tage) zeigen, ob die Nutzung steigt oder fällt. Faire Vergleiche zum gleichen Stand: heute-bis-jetzt vs. gestern bis zur gleichen Uhrzeit, Kalenderwoche (Mo-Start) vs. letzte Woche bis zum gleichen Wochentag+Uhrzeit, Monat vs. Vormonat bis zum gleichen Tag+Uhrzeit (geclamped auf Monatsende, 31.→28.) — der Gesamtwert der Vorperiode steht als Kontext dabei; die rollierende 7-Tage-Karte vergleicht immer zwei volle Fenster. Jede Karte: ▲/▼-Delta-Badge (±3 % = „≈"), Overlay-Sparkline (aktuelle Periode farbig, Vorperiode gestrichelt; die laufende Periode endet sichtbar dort, wo sie steht), Monatsend-Hochrechnung, Tooltip mit Nachrichten + aktiver Arbeitszeit beider Perioden. Folgt den bestehenden Token↔Kosten- und Cache-Toggles, unabhängig vom Zeitraumfilter, i18n DE/EN, Demo-Daten. Backend:
aggregator.getTrends(now)(ein Message-Scan, ~60 ms bei 176k Messages,nowinjizierbar für Tests) +GET /api/trends
/api/rebuildverlor geprunte History — Claude Code löscht alte JSONL-Session-Files (auf diesem Rechner bereits ~2.300 von ~4.000); der Rebuild resettete den Aggregator und las nur noch JSONL neu ein, wodurch alles jenseits des Retention-Fensters (inkl. Rate-Limit-Events) bis zum nächsten Neustart aus dem Dashboard verschwand. Der Rebuild lädt jetzt zuerst die DB-History (Streaming) + Rate-Limit-Events und re-parst JSONL obendrauf (Dedup per Message-ID)
- Restore-Skript
scripts/restore-from-server.sh: stellt nach Neuinstallation/Reset die volle lokale History aus dem Hosted-Server wieder her (konsistenterVACUUM INTO-Snapshot via SSH, LaunchAgent-Stop/Swap/Start; lokale JSONL werden per ID-Dedup drübergeparst, Achievements rekonstruieren sich über die Backfill-Migration) - Lokale Auto-Backups aktiviert:
BACKUP_PATH/BACKUP_INTERVAL_HOURS=24in den LaunchAgent-EnvironmentVariables (die Plist lädt kein.env) — täglicheVACUUM INTO-Snapshots nachdata/backups/, 10 Kopien Rotation - README-Abschnitt „Data Continuity & Restore" (JSONL = rollierendes Fenster, DB = Langzeitspeicher, Hosted-Sync = geräteübergreifende Kontinuität)
- ~75% weniger RAM (509 MB → ~130 MB nach Start, gemessen mit 176k Messages). Vier Ursachen behoben:
- SQLite mmap entfernt (
mmap_size 256MB→ 0): die inzwischen 100+ MB große DB wurde komplett in den Prozess gemappt und zählte voll ins RSS (~160 MB scheinbarer Verbrauch), obwohl alle Analytics aus dem In-Memory-Aggregator kommen. Der 16-MB-Default-Page-Cache reicht für Bootstrap-Scan und inkrementelle Writes - Streaming-Bootstrap (
streamAllMessages()/streamMessagesForUser()viastmt.iterate()): der Start materialisierte zwei volle ~175k-Objekt-Arrays (Rows + gemappte Messages) gleichzeitig — dieser transiente Peak ließ den V8-Heap um hunderte MB wachsen, die nie ans OS zurückgingen. Messages fließen jetzt einzeln in den Aggregator; das Dedup-Set für neue JSONL-Messages nutzt die vorhandene Aggregator-ID-Map (hasMessage()) statt eines zweiten 175k-Sets - String-Interning im Aggregator (
_addMessage-Choke-Point): project/model/sessionId/stopReason/Tool-Namen/_dateteilen sich eine Kopie pro Wert statt einer frischen Allokation pro Message (−17 MB Live-Heap) - Einmaliger Voll-GC 10s nach dem Listen (via
v8.setFlagsFromString('--expose-gc')): kompaktiert den Startup-Churn und gibt die Pages ans OS zurück — auf einem idle Server läuft die memory-reducing Major-GC sonst lange nicht
- SQLite mmap entfernt (
- Interne Query-Loops iterieren
_messageById.values()direkt — dermessages-Getter allozierte pro API-Request ein frisches ~175k-Element-Array (~1,4 MB Garbage pro Request) - WAL-Checkpoint (TRUNCATE) beim Start: die WAL war unter den Dauer-Writes des Watchers auf 57 MB angewachsen (jeder Read konsultiert den WAL-Index); jetzt beim Start auf die Hauptdatei zurückgefaltet (57 MB → <3 MB)
- Suite auf 242 erweitert (streamAllMessages ≡ getAllMessages,
hasMessage/messageCount, Generator-Input füraddMessages, Werterhaltung durchs Interning)
- Projects table fits without horizontal scrolling — long project paths are shortened from the left (
…/customers/celox/portal— the distinguishing tail is kept, the full name shows as a tooltip and remains the sort key), the name column is width-capped, and cell padding tightens in steps below 1100/1000/800px so all 9 columns stay visible at once (verified overflow-free at 768–1440px)
- Card entrance animations replayed after every live refresh — the root cause of the remaining card flicker.
body.motion-quietsetsanimation: noneon cards during a refresh; removing the class re-appliedmd-drop, and CSS restarts a re-applied animation from the beginning (22 elements — 15 KPI cards, 6 chart boxes, active-sessions — replayed their entrance after every SSE refresh, blanking during their stagger delay). Fix:body.motion-settledis added once, 1.6s after load (when the first-paint choreography has finished), and never removed — entrances are permanently disarmed, so nothing is ever re-applied/restarted. The tab-panel swing and the KPI value pop remain the only recurring motion. Verified with real SSE traffic: 0 entrance restarts in 25s (previously 22 per refresh), value pops still firing - Local dev: stale PM2 process fought the LaunchAgent — a leftover
token-trackerentry in the local PM2 daemon crash-looped against the LaunchAgent for port 5010 (dozens of restarts), killing SSE/API requests mid-flight. Removed from PM2 (pm2 delete token-tracker && pm2 save); local runs via LaunchAgent only - Live refreshes no longer re-render/flicker the whole GUI — only the KPI numbers animate now. Three sources eliminated: (1) charts were destroyed + recreated on every refresh (blank-canvas flash) —
renderChart()now updates existing instances in place (chart.data/chart.optionsswap +update('none'), all 40 chart creators converted; legend-visibility restore made idempotent for doughnuts so in-place updates don't un-hide slices); (2) the usage heatmap rebuilt all its DOM cells per refresh — same-shape renders now update cell colours/titles in place (the entrance wave still plays on first paint and single-day ↔ multi-day shape changes); (3)loadActiveSessions/loadPlanUsage/loadGlobalComparisonwere fire-and-forget, so their DOM updates landed after the SSE handler removedmotion-quietand replayed entrance animations —loadOverviewnow awaits them (still parallel)
- 4–10× faster API endpoints (measured with 144k messages): per-message derived values (
_date,_ms,_hour,_day,_cost,_pricing) are now computed once in_applyDelta(the single choke point) and cached on the message object — period-filtered queries no longer allocate aDateand re-resolve pricing per message per request. Overview all-time 140ms → 15ms, productivity 186ms → 32ms (30d), chart endpoints 45–66ms → 5–17ms computeActiveMinutesworks on numeric epoch-ms timestamps (sessions store_timestampsas numbers) — eliminates twoDateallocations per gap (~288k per overview request before)getOverviewderives the per-type cost breakdown from the precomputed per-day sums instead of its own full message scan- ETag +
Cache-Control: must-revalidatefor static assets — browser reloads revalidate (304, 0 bytes) instead of re-downloading ~640KB of JS/CSS
- Token ↔ Cost toggle for the overview charts — a pill toggle above the charts switches the daily chart (stacked by input/output/cache-read/cache-create), model doughnut, hourly chart, and usage heatmap between token counts and dollars. Persisted in
localStorage(metricMode), survives reloads, works for single-day and multi-day ranges, respects the cache toggle, localized DE/EN, covered by demo data. New backend fields: per-day cost breakdown on/api/daily,costNoCache+maxCost/maxCostNoCacheon/api/hourly-weekday - Time-aware pricing (
PRICING_EPOCHS) —calculateCost(model, usage, timestamp)resolves time-windowed prices per model, so past messages permanently keep the price that was in effect when they were sent (the live LiteLLM feed only knows the current price). First real epoch: Sonnet 5 introductory pricing ($2/$10 per MTok through 2026-08-31, then $3/$15). Aggregator call sites pass the message object, whose owntimestampmakes every cost calculation time-aware automatically. Epochs are exposed inGET /api/pricing
- Per-component cost breakdowns (hourly chart, insights, project detail) used the hard-coded
PRICINGtable with a Sonnet-price fallback, ignoring live LiteLLM overrides — now resolved viagetPricing(model, timestamp)everywhere
- Test suite expanded to 238 (9 new: pricing-epoch resolution incl. override precedence and timestamp fallback, per-day cost-breakdown sum, heatmap cost cells/maxima, time-aware heatmap cost)
- New-model label derivation —
_deriveLabelnow handles the newest ID shapes: single-digit versions with no minor (claude-sonnet-5→ "Sonnet 5"), a brand-new family (claude-fable-5→ "Fable 5"), and dated base IDs whose release-date suffix was being misread as a minor version (claude-opus-4-20250514→ "Opus 4", was "Opus 4.20250514"). Model costs were already auto-detected correctly from LiteLLM — this fixes only the display label. The family set is now open/extensible (opus|sonnet|haiku|fable) - Stale offline pricing fallback — the hard-coded
PRICINGsafety net (used only when LiteLLM is unreachable at boot with an empty cache) is refreshed to the current generation (Opus 4.8/4.7, Sonnet 5, Fable 5, plus bare-ID variants). Previously a fresh offline boot would undercount e.g. Opus 4.8 as Sonnet pricing viaDEFAULT_PRICING
- README badge stack expanded (all three variants) with rows for auto-synced LiteLLM pricing, per-model support (Opus 4.8 / Sonnet 5 / Fable 5 / Haiku 4.5), single/multi-user + multi-device modes, and the MD3-Expressive/heatmap/accessibility feature set
- Test suite expanded to 229 (13 new): new-generation label derivation, trailing-alias/two-digit-minor labels, unrecognized-family fallback, Fable pricing, offline-fallback pricing for Opus 4.8 / Sonnet 5 / Fable 5, Opus 4.8 full-formula + bare-ID pricing, hard-coded-label precedence over overrides, and
getPricingMetaorigin tagging
- Usage heatmap — weekday × hour grid in the overview that visualizes token-usage intensity. Multi-day ranges render a 7×24 grid (rows Mon→Sun); a single day renders a 24-hour strip. Cache-toggle aware, with a per-cell tooltip (tokens · messages · cost) and a colour legend. Lightweight CSS grid (no extra dependency). New
Aggregator.getHourlyWeekday()+GET /api/hourly-weekday, plus demo-data coverage - Weekday labels on dates — chart axis labels now carry the weekday (
Sat 06-27), and a new period-range header in the status bar shows the selected window with weekdays (Thu 05/28/2026 – Sat 06/27/2026).formatPeriodLabel(comparison labels) updated too - Material 3 Expressive motion for the new UI — the heatmap reveals with a diagonal spring wave (per-cell
--d = row+colstagger), cells spring on hover with an accent glow, and the period-range header spring-swaps when the range changes. Motion uses authentic MD3 motion-physics springs rendered as CSSlinear()easings (--ease-spatial-expressive,--ease-spatial-expressive-fast,--ease-effects-expressive) derived from the official spring tokens. Gated to real navigations (calm on live/SSE refreshes) with aprefers-reduced-motionguard
- Heatmap CSS classes use a dedicated
uheat-prefix to avoid colliding with the GitHub contribution graph's.heatmap-grid(grid-auto-flow: column), which otherwise scrambled the weekday rows into a horizontal zigzag
- Test suite expanded to 216 (added 4
getHourlyWeekdaycases: grid shape, totals, local-time bucketing, from/to filter)
- Project search — pill-shaped live substring filter above the Projects table with an
aria-liveresult count, clear button, and Escape-to-reset; filters the table only (the top-15 chart stays a stable overview) and persists across period changes. Newdata-i18n-placeholdersupport inapplyTranslations() - Project merge — fold projects that are the same codebase (renamed/moved, or synced from another device under a different path) into one canonical name. Non-destructive (originals untouched in the DB) and applied at the aggregator's single
_addMessagechoke-point, so the merge folds existing and future messages across all views and survives re-parses/syncs; un-merge restores the original split. Newproject_aliasestable,GET /api/project-aliases,POST /api/project-merge,DELETE /api/project-aliases, a merge dialog with checkbox sources + target select + active-merges list, and a "+n merged" badge on combined projects - Merge suggestions — 🪄 button (auto-surfaced when candidates exist) that detects likely-duplicate projects by bucketing on an identity key = path minus its device/tool root segment (
claude/mrxdown≡WebstormProjects/mrxdown); discrete buckets, capped at 2–6, no false-merging of names that only share a leaf word - Material 3 Expressive motion system for the dashboard frontend (spring/emphasized easings, staggered card entrance, directional tab transitions, cursor-reactive KPI tilt, value-pop, progressive-enhancement + reduced-motion guards)
- Project-merge aliases are strictly per-user scoped; the cross-user share aggregator applies no alias map, preventing cross-tenant project-name poisoning. The merge endpoint validates that every source/target is a project the requesting user actually owns
- CI test matrix and
enginesfloor bumped to Node ≥ 20.12 (vitest 4 importsutil.styleText, unavailable on Node 18)
- Project merge was destructive after a re-parse — the aggregator mutated the shared message object's
projectto the canonical name, and the watcher//api/rebuildadd to the aggregator before they insert into the DB, so every new/re-parsed message persisted the canonical name and un-merge could no longer restore the split. The aggregator now clones on fold instead of mutating, keeping the original name in the DB - Merge dialog:
_mergeKeycould throw on an empty//-only project name (crashing suggestions); ownership validation now also accepts already-merged source names (so an existing alias can be redirected); merged-project badge reads the live alias list (count + tooltip no longer desync after a re-sort); merge/un-merge surface a refresh error instead of silently leaving a stale table
- Test suite expanded to 212 (added
project-merge,export-html,anthropic-apicrypto,configcoverage, and a non-destructive-fold regression test)
- Period Navigation Buttons — prev/next arrow buttons beside the date picker, jump by the currently selected period duration (1 day, 7 days, 30 days)
- Achievements Points System — tier-based point values (Bronze: 10, Silver: 25, Gold: 50, Platinum: 100, Diamond: 250), displayed on each achievement card and as a total score
- Achievements Timeline Chart — bar+line chart showing achievements unlocked per day with cumulative points curve
- Achievements Stats — total points counter and average achievements per day metric in the achievements header
- 5 new i18n keys in both EN and DE (achievementsPoints, achievementsAvgPerDay, achievementsTimeline, achievementsUnlockedCount, achievementsCumulativePoints)
- Period-over-Period Comparison in the Productivity tab — compare any two time periods side-by-side
- "Compare Periods" toggle button activates the comparison section
- Period B selector: Previous (auto-computed same-length preceding window), 7d, 30d, 90d, or Custom date range
- 8 comparison metrics: Tokens/Min, Lines/Hour, Cost/Line, Tokens/Line, Lines/Turn, Tools/Turn, I/O Ratio, Coding Hours
- Visual comparison cards with dual bar charts (Period A blue, Period B grey)
- Delta percentage with color-coded indicators (green = improvement, red = regression)
- Respects "lower is better" semantics for Cost/Line and Tokens/Line
- Hint text below each metric shows whether higher or lower is better
- Period labels show exact date ranges for both periods
- State persisted in localStorage (active period B selection, custom date range)
- Automatically recalculates when global period selector changes
- Reuses existing
/api/productivityendpoint (no backend changes needed)
- 11 new i18n keys in both EN and DE (periodComparison, compareToggle, compareTo, previousPeriod, periodA, periodB, improvement, regression, noChange, betterLower, betterHigher)
- 2 new aggregator tests for period-isolated productivity data
- ~100 lines of CSS for comparison section, Period B selector, delta indicators
- Test count: 146 → 148 (2 new aggregator tests)
- Multi-user mode with GitHub OAuth login (
MULTI_USER=true) - Per-user data isolation (user_id on messages, user-scoped aggregator cache)
usersanduser_sessionsdatabase tableslib/auth.js— GitHub OAuth flow, session management, cookie-based authAggregatorCacheclass — per-user lazy loading with 30min eviction- Sync Agent CLI (
sync-agent/) — standalone tool to upload token data from client to server POST /api/syncendpoint with API key authenticationGET/POST /api/sync-keyfor API key managementGET /api/configendpoint (tells frontend about multi-user mode)- Login overlay with GitHub sign-in button
- User avatar and name in header with logout button
- Sync Agent setup section in Info tab (API key display, installation instructions)
scripts/deploy.shfor VPS deployment (tracker.celox.io)- SSE broadcast filtering by userId in multi-user mode
- 25 new tests: auth (13), sync (4), multi-user isolation (3), aggregator cache (5)
- New i18n keys for login, logout, sync setup (DE + EN)
- Server routes refactored: auth gate on
/api/*in multi-user mode - Static files served before auth check (login page needs CSS/JS)
- File watcher disabled in multi-user mode (data comes via sync agent)
- Stats-cache endpoint returns 404 in multi-user mode
- Initial release
- Token usage dashboard with 5 tabs (Overview, Sessions, Projects, Tools, Models)
- Real-time updates via SSE
- Bilingual UI (German/English)
- 7 chart types (daily tokens, daily cost, model distribution, hourly activity, project bar, tool bar, model area)
- API-equivalent cost calculation for all Claude models
- Incremental JSONL parsing with file watcher
- SQLite database for persistent storage
- Automatic backup system with configurable schedule
- Insights tab with 6 additional charts
- CSS-only tooltips with bilingual explanations
- CI/CD pipeline with GitHub Actions
- Comprehensive test suite (vitest + supertest)