Internal Documentation internal
TalkIDE internal documentation

Drift detection a manuální rebuild Redis kvótových čítačů z PG usage_events — dva admin endpointy umožňují zobrazit aktuální odchylku mezi persistentní usage_events tabulkou (single source of truth pro náklady) a Redis rolling windows, a po incidentu atomicky přepočítat Redis stav pro konkrétního uživatele.

  • Navazuje na UC-08001 Anthropic Call Gateway (gateway emituje usage events) a UC-08002 Mara Fatigue & Quota Enforcement (Redis rolling windows, window semantics).
  • MR1 (2026-05) — single source of truth = usage_events: před MR1 se reconciliation počítal z api_usage_ledger, který trpěl ztrátou eventů kvůli pendingUsage map race v AnthropicGatewayService (orchestrator + subagent v jednom conversationId, druhý event přepsal první). usage_events je append-only, 1 NDJSON usage event = 1 row, takže suma je správná i pro paralelní orchestrator/subagent eventy. api_usage_ledger zůstává jako diagnostic source pro inFlightCalls / errors_24h / executor_breakdown_today (UC-08004) a v MR2 bude přejmenován na call_log.
  • Motivace: existuje 6 failure modes způsobujících drift — BE crash mezi PG INSERT a Redis INCRBY, swallowed log.warn při PG fail, Redis restart/flush, TTL expirace global:monthly_used_cents (po MR1: dotaženo, admin má manuální rebuild button přes POST /api/admin/reconciliation/rebuild-global), race dvou paralelních turnů, ztracené eventy v ledger pendingUsage map race. Žádná automatická detekce dnes neexistuje.
  • Window semantics (Anthropic-style): 5h okno začíná prvním eventem v daném okně, nikoli od now() - 5h. Při rebuild se bere MIN(occurred_at) v posledních 5h jako window_start. Totéž platí pro weekly okno (7 dní). Pokud v daném okně neexistuje žádný event → okno neexistuje, Redis klíče se smažou.
  • In-flight requesty: usage_events z definice neobsahuje crashed/in-flight rows (failed Anthropic call neemmituje usage event), takže filtrace request_completed_at IS NOT NULL z pre-MR1 ledgeru tu odpadá.
  • Žádná Liquibase migrace pro nové tabulky — oba endpointy čtou usage_events a Redis; MR1 jen přidává sloupec cost_cents do usage_events.
  • Žádný cron job — reconciliation je výhradně na vyžádání adminem.
  • Rebuild má dvě varianty: per-user a global. Per-user rebuild aktualizuje 5h+weekly Redis okna. Global rebuild aktualizuje global:monthly_used_cents. Oba jsou samostatné endpointy.
  • Rebuild je idempotentní — opakované volání konverguje do stejného stavu (PG SUM je deterministický).
  • Admin role se ověřuje vzorem silent probe: non-admin dostane 401 AUTHENTICATION_FAILED, nikoli 403 — endpoint nedává najevo svou existenci.
  • Přístupné výhradně přes /api/admin/... prefix (konzistentní s UC-08004 Admin Capacity Console).

Flow 1 — Admin čte reconciliation report

sequenceDiagram
    actor Admin

    Admin->>+FE: otevře /admin/reconciliation

    FE->>+BE: GET /api/admin/reconciliation/report <br> Authorization: Bearer {accessToken}

    BE->>BE: silent probe — ověř roli ADMIN
    alt uživatel není ADMIN nebo není autentizován
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>+ReconciliationReportUseCase: execute()

    ReconciliationReportUseCase->>DB: SELECT user_id, SUM(cost_cents) FROM usage_events <br> WHERE occurred_at >= now() - 7d <br> GROUP BY user_id

    ReconciliationReportUseCase->>Redis: KEYS user:*:5h_used_cents <br> KEYS user:*:weekly_used_cents

    ReconciliationReportUseCase->>Redis: GET global:monthly_used_cents

    ReconciliationReportUseCase->>DB: SELECT SUM(cost_cents) FROM usage_events <br> WHERE occurred_at >= billing_month_start

    ReconciliationReportUseCase->>ReconciliationReportUseCase: vypočítej delta pro každý user a global <br> has_drift = (delta_5h != 0) || (delta_weekly != 0)

    ReconciliationReportUseCase-->>-BE: ReconciliationReport

    BE->>-FE: 200 OK <br> ReconciliationReportResponse

    FE->>-Admin: zobraz tabulku uživatelů s delta hodnotami <br> zvýrazni řádky s has_drift=true

GET /api/admin/reconciliation/report — bez request body, bez query params

200 OK ReconciliationReportResponse:

{
  "generated_at": "2026-05-09T14:23:45Z",
  "global": {
    "pg_monthly_cents": 12450,
    "redis_monthly_cents": 12380,
    "delta_monthly_cents": 70
  },
  "users": [
    {
      "user_id": 1234,
      "email": "test@talkide.app",
      "pg_5h_cents": 320,
      "redis_5h_cents": 320,
      "delta_5h_cents": 0,
      "pg_weekly_cents": 1450,
      "redis_weekly_cents": 1380,
      "delta_weekly_cents": 70,
      "has_drift": true
    },
    {
      "user_id": 5678,
      "email": "alice@example.com",
      "pg_5h_cents": 150,
      "redis_5h_cents": 150,
      "delta_5h_cents": 0,
      "pg_weekly_cents": 800,
      "redis_weekly_cents": 800,
      "delta_weekly_cents": 0,
      "has_drift": false
    }
  ]
}

401 Unauthorized ErrorResponse:

{
  "code": "AUTHENTICATION_FAILED",
  "message": "Authentication required"
}

Flow 2 — Admin provede rebuild Redis pro konkrétního uživatele

sequenceDiagram
    actor Admin

    Admin->>+FE: klikne "Rebuild" u uživatele s drift <br> (confirm modal)

    FE->>FE: UI guard — zobraz confirm modal <br> "Rebuild přepíše Redis state pro {email}. Potvrdit?"

    Admin->>+FE: potvrdí rebuild

    FE->>+BE: POST /api/admin/reconciliation/rebuild/{userId} <br> Authorization: Bearer {accessToken}

    BE->>BE: silent probe — ověř roli ADMIN
    alt uživatel není ADMIN nebo není autentizován
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>+ReconciliationRebuildUseCase: execute(userId)

    ReconciliationRebuildUseCase->>DB: SELECT id FROM users WHERE id = ?
    alt user neexistuje
        ReconciliationRebuildUseCase-->>BE: throws UserNotFoundException
        BE-->>FE: 404 Not Found <br> ErrorResponse
    end

    Note over ReconciliationRebuildUseCase: --- 5h window rebuild ---

    ReconciliationRebuildUseCase->>DB: SELECT MIN(occurred_at) FROM usage_events <br> WHERE user_id = ? AND occurred_at > now() - 5h

    alt žádný záznam v posledních 5h
        ReconciliationRebuildUseCase->>Redis: DEL user:{id}:5h_window_start_ts <br> DEL user:{id}:5h_used_cents
    else záznam nalezen → window_start_5h = MIN(occurred_at)
        ReconciliationRebuildUseCase->>DB: SELECT SUM(cost_cents) FROM usage_events <br> WHERE user_id = ? AND occurred_at >= window_start_5h
        ReconciliationRebuildUseCase->>Redis: Lua atomic write: <br> SET user:{id}:5h_window_start_ts = window_start_5h (TTL = 5h - elapsed) <br> SET user:{id}:5h_used_cents = sum (TTL = 5h - elapsed)
    end

    Note over ReconciliationRebuildUseCase: --- weekly window rebuild ---

    ReconciliationRebuildUseCase->>DB: SELECT MIN(occurred_at) FROM usage_events <br> WHERE user_id = ? AND occurred_at > now() - 7d

    alt žádný záznam v posledních 7d
        ReconciliationRebuildUseCase->>Redis: DEL user:{id}:weekly_window_start_ts <br> DEL user:{id}:weekly_used_cents
    else záznam nalezen → window_start_weekly = MIN(occurred_at)
        ReconciliationRebuildUseCase->>DB: SELECT SUM(cost_cents) FROM usage_events <br> WHERE user_id = ? AND occurred_at >= window_start_weekly
        ReconciliationRebuildUseCase->>Redis: Lua atomic write: <br> SET user:{id}:weekly_window_start_ts = window_start_weekly (TTL = 7d - elapsed) <br> SET user:{id}:weekly_used_cents = sum (TTL = 7d - elapsed)
    end

    ReconciliationRebuildUseCase->>DB: SELECT SUM(cost_cents) FROM usage_events <br> WHERE user_id = ? AND occurred_at >= billing_month_start

    ReconciliationRebuildUseCase-->>-BE: SingleUserReconciliationReport

    BE->>-FE: 200 OK <br> SingleUserReconciliationResponse

    FE->>-Admin: zobraz updated stav uživatele (delta by měla být 0)

POST /api/admin/reconciliation/rebuild/{userId} — bez request body

200 OK SingleUserReconciliationResponse (stejný shape jako jeden users[] prvek z GET report):

{
  "generated_at": "2026-05-09T14:31:12Z",
  "global": {
    "pg_monthly_cents": 12450,
    "redis_monthly_cents": 12450,
    "delta_monthly_cents": 0
  },
  "users": [
    {
      "user_id": 1234,
      "email": "test@talkide.app",
      "pg_5h_cents": 320,
      "redis_5h_cents": 320,
      "delta_5h_cents": 0,
      "pg_weekly_cents": 1450,
      "redis_weekly_cents": 1450,
      "delta_weekly_cents": 0,
      "has_drift": false
    }
  ]
}

401 Unauthorized ErrorResponse:

{
  "code": "AUTHENTICATION_FAILED",
  "message": "Authentication required"
}

404 Not Found ErrorResponse:

{
  "code": "NOT_FOUND_USER",
  "message": "User not found"
}

Flow 3 — Admin provede rebuild globálního monthly Redis counteru

sequenceDiagram
    actor Admin

    Admin->>+FE: klikne "Rebuild Global" v GlobalDriftCard

    FE->>+BE: POST /api/admin/reconciliation/rebuild-global <br> Authorization: Bearer {accessToken}

    BE->>BE: silent probe — ověř roli ADMIN
    alt uživatel není ADMIN nebo není autentizován
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>+RebuildGlobalQuotaUseCase: invoke()

    RebuildGlobalQuotaUseCase->>DB: SELECT SUM(cost_cents) FROM usage_events <br> WHERE occurred_at >= billing_month_start (UTC)

    RebuildGlobalQuotaUseCase->>RebuildGlobalQuotaUseCase: secondsUntilEndOfMonth = seconds until <br> 1st day of next UTC month

    RebuildGlobalQuotaUseCase->>Redis: Lua atomic write: <br> SET global:monthly_used_cents = pgMonthlyCents (TTL = secondsUntilEndOfMonth)

    RebuildGlobalQuotaUseCase->>+GetReconciliationReportUseCase: invoke()
    GetReconciliationReportUseCase-->>-RebuildGlobalQuotaUseCase: ReconciliationReport

    RebuildGlobalQuotaUseCase-->>-BE: ReconciliationReport (delta_monthly_cents = 0)

    BE->>-FE: 200 OK <br> ReconciliationReportResponse

    FE->>-Admin: GlobalDriftCard aktualizuje delta na 0 (zelená)

POST /api/admin/reconciliation/rebuild-global — bez request body

200 OK ReconciliationReportResponse (stejný shape jako GET /report):

{
  "generated_at": "2026-05-09T14:35:00Z",
  "global": {
    "pg_monthly_cents": 12450,
    "redis_monthly_cents": 12450,
    "delta_monthly_cents": 0
  },
  "users": [ ... ]
}

401 Unauthorized ErrorResponse:

{
  "code": "AUTHENTICATION_FAILED",
  "message": "Authentication required"
}

Redis Keys Reference

Tento UC pracuje s Redis klíči definovanými v UC-08002:

KeyTypTTLPopis
user:{id}:5h_window_start_tsString (epoch ms)5 hodinTimestamp startu aktuálního 5h okna
user:{id}:5h_used_centsInteger5 hodinSpotřeba v aktuálním 5h okně (centy)
user:{id}:weekly_window_start_tsString (epoch ms)7 dníTimestamp startu aktuálního weekly okna
user:{id}:weekly_used_centsInteger7 dníSpotřeba v aktuálním weekly okně (centy)
global:monthly_used_centsIntegerdo konce billing měsíceGlobální měsíční spotřeba — rebuilds přes POST /api/admin/reconciliation/rebuild-global

Rebuild atomicky přepíše window_start_ts i used_cents pro dané okno v jediném Lua scriptu — race-condition free i při souběžně probíhajícím Mara turnu.

Frontend

Validations

PrvekPravidloPoznámka
Rebuild tlačítkoZobrazit pouze pokud has_drift = trueDisabled tlačítko pro uživatele bez driftu, aby se předešlo zbytečným API voláním
Confirm modal před rebuildPovinný confirm krokZobrazit email uživatele a aktuální delta hodnoty; bez potvrzení rebuild nespustit
Rebuild tlačítko (stav)Disabled po dobu inflight requestuZabránit dvojkliku — tlačítko disabled po kliknutí dokud POST /rebuild/{userId} nevrátí odpověď

UX Notes

  • Report se načítá při otevření /admin/reconciliation stránky a lze ho ručně obnovit tlačítkem “Refresh”.
  • Řádky s has_drift = true jsou zvýrazněny (např. žlutý nebo červený background), řádky bez driftu jsou neutrální.
  • Po úspěšném rebuild UI inline aktualizuje řádek daného uživatele s hodnotami z 200 OK response — nezobrazuje se celé přenačtení tabulky.
  • Pole generated_at je zobrazeno nad tabulkou jako timestamp posledního načtení reportu.

UX Guidelines

User Flow

Admin otevře /admin/reconciliation — stránka se okamžitě načte a zavolá GET /api/admin/reconciliation/report; během načítání se zobrazí skeleton ve GlobalDriftCard i v DriftTable. Po načtení jsou uživatelé s has_drift=true vypsáni nahoře (default sort) a filtr “Show only users with drift” je zapnutý. Admin vidí barevné DriftBadge buňky ve sloupcích delta a rozhodne, zda je drift podezřelý — pokud ano, klikne “Rebuild” u konkrétního uživatele. Systém zobrazí RebuildConfirmModal s emailem uživatele a aktuálními delta hodnotami; default focus je na tlačítku Cancel. Admin potvrdí tlačítkem Rebuild (červené) — button se přepne na spinner, zavolá se POST /api/admin/reconciliation/rebuild/{userId}, po úspěchu se řádek uživatele aktualizuje in-place a delta buňky přeskočí na 0. Admin může kdykoli kliknout RefreshButton pro nové načtení celého reportu.

Layout

Screen type: Dashboard (read-only + manuální akce Rebuild) Route: /admin/reconciliation Responsive: Desktop-first, single-column na mobilech (admin operační nástroj) Container: max-w-6xl mx-auto px-6 py-8 Admin layout: sdílený sidebar/header již přítomný — žádný vlastní sidebar na této obrazovce

┌─ /admin/reconciliation ────────────────────────────────────────────────┐
│                                                                         │
│  Reconciliation Log                    [Refresh]  Last refreshed: 14:23 │
│                                        (3 min ago)                      │
│                                                                         │
│  ┌─ GlobalDriftCard ────────────────────────────────────────────────┐   │
│  │  Monthly PG      Monthly Redis       Delta (monthly)             │   │
│  │  $124.50         $123.80             $0.70  ← žlutý badge        │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  [x] Show only users with drift                                         │
│                                                                         │
│  ┌─ DriftTable ─────────────────────────────────────────────────────┐   │
│  │ Email            PG 5h  Redis 5h  Δ 5h  PG wk  Redis wk  Δ wk  │   │
│  │─────────────────────────────────────────────────────────────────│   │
│  │ test@talkide.app  $3.20  $3.20   [OK]   $14.50  $13.80  [+$0.70]│  [Rebuild] │
│  │ alice@example.com $1.50  $1.50   [OK]   $8.00   $8.00   [OK]   │  (disabled) │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
┌─ RebuildConfirmModal ─────────────────────────────────────────────┐
│                                                                    │
│  Rebuild Redis quota for test@talkide.app?                         │
│                                                                    │
│  This will overwrite Redis quota counters for this user by         │
│  recalculating from PG ledger. Active Mara turn for this user      │
│  may have conflicting writes.                                      │
│                                                                    │
│  Current drift:                                                    │
│    5h delta:     $0.00                                             │
│    Weekly delta: +$0.70                                            │
│                                                                    │
│  [Cancel ← default focus]                    [Rebuild (red)]       │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Components

KomponentaSouborOdpovědnost
ReconciliationDashboardScreensrc/screens/admin/ReconciliationDashboardScreen.vueParent route /admin/reconciliation. Orchestruje fetchReport() při mountu, předává data do child komponent, spravuje viditelnost RebuildConfirmModal.
GlobalDriftCardsrc/screens/admin/components/GlobalDriftCard.vueZobrazuje 3 čísla: PG monthly, Redis monthly, delta. Přijímá global objekt z response. Barva delta závisí na color/state matrix (viz níže). Skeleton prop při načítání.
DriftTablesrc/screens/admin/components/DriftTable.vue<table> s header cells <th scope="col">. Sloupce: Email, PG 5h, Redis 5h, Δ 5h, PG weekly, Redis weekly, Δ weekly, Action. Default sort: has_drift=true nahoře. Filter prop showOnlyDrift. Skeleton rows při fetchReport.
DriftBadgesrc/screens/admin/components/DriftBadge.vueBuňka delta hodnoty. Text “OK” nebo “Drift” + číselná hodnota (např. ”+$0.70”). Barva dle color/state matrix. Tooltip s vysvětlením driftu (např. “Redis underreports $0.70 vs ledger”). aria-label s plným popisem.
RebuildConfirmModalsrc/screens/admin/components/RebuildConfirmModal.vueConfirm dialog před POST /rebuild/{userId}. Zobrazuje email uživatele a aktuální delta hodnoty. Focus trap, Escape zavře (= Cancel), default focus na Cancel button. role="dialog" + aria-modal="true" + aria-labelledby. Loading spinner na Rebuild button při submitu.
RefreshButtonsrc/screens/admin/components/RefreshButton.vueManuální refresh reportu (žádný auto-poll). Zobrazuje timestamp “Last refreshed: HH:mm:ss (X min ago)”. Spinner při fetchReport.
useReconciliationStoresrc/screens/admin/stores/reconciliation.tsPinia store (feature-based scope). State: report, loading, error, lastRefreshedAt. Actions: fetchReport(), rebuildUser(userId). Po rebuildUser success → fetchReport() automaticky.

Color / State Matrix

GlobalDriftCard — delta barva

PodmínkaBarvaCSS varZdůvodnění
delta_monthly_cents == 0Zelenávar(--green)Plná shoda — vše OK
0 < &#124;delta&#124; < 50 (< $0.50)Žlutávar(--amber)Malá odchylka — pozorovat
&#124;delta&#124; >= 50 (>= $0.50)Červenávar(--rose)Významný drift — vyžaduje akci

DriftBadge — delta cell

PodmínkaBarvaTextTooltip
delta_cents == 0Zelená var(--green)”OK · $0.00""Redis and PG ledger are in sync”
0 < &#124;delta&#124; < 5 % sumy nebo &#124;delta&#124; < 50Žlutá var(--amber)”Drift · +$X.XX""Redis underreports $X.XX vs ledger (minor)“
&#124;delta&#124; >= 5 % sumy a &#124;delta&#124; >= 50Červená var(--rose)”Drift · +$X.XX""Redis underreports $X.XX vs ledger — rebuild recommended”

Pozn.: barva není jediný signál — vždy přítomen text “OK” nebo “Drift” i číselná hodnota.

DriftTable — řádek highlight

StavPozadí řádku
has_drift = falseNeutrální (default table row)
has_drift = truevar(--amber-soft) — jemný amber tint; přechod na červený tint pokud &#124;delta&#124; >= 50
Právě probíhá rebuild (loading)Skeleton overlay na řádku, ostatní řádky zachovány

Rebuild button — stav

StavVizuál
has_drift = true, idleAktivní červené destructive outline tlačítko
has_drift = falseDisabled, opacity-50, cursor-not-allowed, aria-disabled="true"
Rebuild in-flightSpinner + “Rebuilding…” text, disabled
Po úspěchuTlačítko přejde do disabled stavu (delta = 0, has_drift = false)

Validation Feedback

UdálostMechanismusZobrazení
Network error při fetchReportRed banner nad tabulkou”Failed to load reconciliation report. [Retry]” · role="alert" · aria-live="assertive"
401 UnauthorizedAxios interceptor → redirect na loginŽádný toast — interní guard
404 User not found po rebuildToast + auto-refresh”User not found, refreshing report.” · aria-live="polite" · 5 s
500 / network error při rebuildToast error”Rebuild failed. Please try again.” · aria-live="assertive" · 8 s
Úspěšný rebuildIn-place update řádku + toast”Redis quota rebuilt for user@example.com.” · aria-live="polite" · 5 s
Prázdná tabulka (žádná data)Empty state v DriftTable”No users tracked yet — no ledger entries and no active Redis quota keys.”

Toast vs Banner pravidlo:

  • Banner (persistentní, nad tabulkou): chyby načítání reportu — blokují celou funkci obrazovky.
  • Toast (dočasný): akce rebuild — úspěch, 404, rebuild failure. Neblokují pohled na tabulku.

Accessibility

Klávesové chování:

  • Tab naviguje po řádcích tabulky; Rebuild button je focusable standardně
  • Enter / Space na Rebuild button → otevře RebuildConfirmModal
  • Escape v RebuildConfirmModal → zavře modal (= Cancel)
  • Tab / Shift+Tab v modalu cykluje pouze mezi Cancel a Rebuild tlačítky (focus trap)
  • Default focus po otevření modalu → Cancel button
  • Po zavření modalu (Cancel nebo Escape) → focus vrácen na Rebuild button daného řádku
  • Po úspěšném rebuildu (modal se zavře) → focus vrácen na Rebuild button (nyní disabled) nebo na první aktivní prvek v řádku

ARIA:

  • DriftTable: <table>, <th scope="col"> na všech header cells
  • DriftBadge: aria-label s plným popisem (např. aria-label="Weekly delta: Redis underreports $0.70 vs PG ledger")
  • RebuildConfirmModal: role="dialog", aria-modal="true", aria-labelledby odkazující na nadpis modalu
  • Rebuild button (disabled): aria-disabled="true" + disabled atribut
  • Error banner: role="alert", aria-live="assertive"
  • Success / info toast: aria-live="polite"
  • Error toast (rebuild failure): aria-live="assertive"
  • RefreshButton: aria-label="Refresh reconciliation report", aria-busy="true" při načítání
  • GlobalDriftCard: role="region", aria-label="Global monthly drift summary"

Barevný kontrast: Všechny rose/amber/green akcenty na var(--bg-2) pozadí splňují WCAG AA (min 4.5:1) v dark i light tématu. Barva není jediný signál — textové popisky vždy přítomny.

Loading States

  • Initial load (fetchReport): GlobalDriftCard zobrazí skeleton (3 pulse boxy). DriftTable zobrazí 3–5 skeleton řádků (pulse animated). RefreshButton disabled + spinner.
  • Manual refresh: identický skeleton jako initial load — celá tabulka se překreslí.
  • Rebuild in-flight: pouze Rebuild button daného řádku → spinner + “Rebuilding…” text + disabled. Ostatní řádky a tabulka zůstanou interaktivní.
  • Skeleton styl: bg-[var(--bg-3)] animate-pulse rounded (konzistentní s UC-08004 MetricCard skeleton).

i18n Klíče (namespace reconciliation.*)

reconciliation.title
reconciliation.lastRefreshed
reconciliation.lastRefreshedAgo

reconciliation.global.title
reconciliation.global.pgMonthly
reconciliation.global.redisMonthly
reconciliation.global.deltaMonthly

reconciliation.filter.showOnlyDrift

reconciliation.table.headerEmail
reconciliation.table.headerPg5h
reconciliation.table.headerRedis5h
reconciliation.table.headerDelta5h
reconciliation.table.headerPgWeekly
reconciliation.table.headerRedisWeekly
reconciliation.table.headerDeltaWeekly
reconciliation.table.headerAction
reconciliation.table.emptyState

reconciliation.badge.ok
reconciliation.badge.drift
reconciliation.badge.tooltipSync
reconciliation.badge.tooltipMinor
reconciliation.badge.tooltipMajor

reconciliation.rebuild.button
reconciliation.rebuild.buttonLoading
reconciliation.rebuild.modal.title
reconciliation.rebuild.modal.description
reconciliation.rebuild.modal.currentDrift
reconciliation.rebuild.modal.delta5h
reconciliation.rebuild.modal.deltaWeekly
reconciliation.rebuild.modal.cancel
reconciliation.rebuild.modal.confirm
reconciliation.rebuild.modal.warningActiveConversation

reconciliation.toast.rebuildSuccess
reconciliation.toast.rebuildFailure
reconciliation.toast.userNotFound

reconciliation.error.loadFailed
reconciliation.error.retry

Backend

Validations

Pole / PodmínkaConstraintsPoznámka
Admin role (oba endpointy)user.role == ADMINSilent probe — non-admin dostane 401 AUTHENTICATION_FAILED, ne 403
userId (path param, POST rebuild)not_null, validní Long (BIGINT), existuje v users tabulce404 NOT_FOUND_USER pokud user neexistuje. users.id je bigint, nikoli UUID — konzistentní s UC-08001/02/03/04.
window_start_5h výpočetMIN(occurred_at WHERE > now()-5h)Anthropic-style: okno začíná prvním requestem, ne od now()-5h
window_start_weekly výpočetMIN(occurred_at WHERE > now()-7d)Stejná logika pro weekly okno
Redis TTL při rebuildTTL = window_duration - (now - window_start)TTL musí být kladné; pokud by vyšlo ≤ 0, okno již expiruje → smaž klíče
global:monthly_used_cents TTL při global rebuildTTL = secondsUntilEndOfMonth()Seconds to 1st day of next UTC month; pokud = 0 (extrémní případ na měsíční hranici) → DEL klíče
Rebuild idempotenceOpakované volání → stejný Redis stavPG SUM je deterministický; Lua SET přepíše předchozí hodnotu
MR1 invariant: usage_events neobsahuje crashed/in-flight rowsŽádná query-level filtrace není potřebaStrukturální invariant: crashed/in-flight call nikdy neemittuje NDJSON usage event → žádný řádek nevznikne. usage_events.occurred_at je NOT NULL sloupec. Filtrace occurred_at IS NULL by byla sémanticky nesmyslná. Invariant je vynucen schématem, ne runtime filtrem.

Implementation Notes

  • ReconciliationReportUseCase a ReconciliationRebuildUseCase jsou Spring @Service / UseCase beany. Controller je AdminReconciliationController na /api/admin/reconciliation.
  • GET report inkluduje všechny uživatele s ledger záznamy v posledních 7 dnech, UNION s uživateli s aktivními Redis klíči. Uživatelé bez jakékoli aktivity se nezobrazují.
  • pg_5h_cents pro report = SUM(cost_cents WHERE user_id=? AND occurred_at >= user:{id}:5h_window_start_ts z Redis). Pokud Redis klíč neexistuje → redis_5h_cents = 0, pg_5h_cents se počítá za posledních 5h od now() jako best-effort.
  • OffsetDateTime pro všechny timestamp sloupce (TIMESTAMPTZ v PG), nikoli LocalDateTime.
  • Rebuild Lua script: atomicky nastaví oba klíče (window_start_ts + used_cents) pro dané okno s identickým TTL — obě hodnoty jsou vždy konzistentní nebo obě chybí.

Test Cases

GIVENWHENTHEN
Admin je přihlášen; 2 uživatelé mají ledger záznamy; Redis a PG jsou v syncGET /api/admin/reconciliation/report je zavolán200 OK; oba uživatelé v users[]; has_drift = false pro oba; delta_*_cents = 0
Admin je přihlášen; user A má redis_weekly_cents = 1380, PG weekly sum = 1450GET /api/admin/reconciliation/report je zavolán200 OK; user A má delta_weekly_cents = 70, has_drift = true
Admin je přihlášen; Redis a PG jsou v sync; delta_5h = 0, delta_weekly = 0GET /api/admin/reconciliation/report je zavolánhas_drift = false pro daného uživatele
Žádné ledger záznamy v posledních 7d; žádné Redis kvóta klíčeGET /api/admin/reconciliation/report je zavolán200 OK; users[] je prázdné pole; global delta = 0
Non-admin uživatel (ROLE_USER)GET /api/admin/reconciliation/report je zavolán401 AUTHENTICATION_FAILED; endpoint nedává najevo svou existenci
Neautentizovaný request (žádný Bearer token)GET /api/admin/reconciliation/report je zavolán401 AUTHENTICATION_FAILED
Admin je přihlášen; user existuje; Redis weekly counter odchýlen od PG o 70 centůPOST /api/admin/reconciliation/rebuild/{userId} je zavolán200 OK; Redis weekly_used_cents přepsán na PG SUM; delta_weekly_cents = 0 v response; has_drift = false
Admin je přihlášen; user nemá žádné záznamy v posledních 5h (5h window neaktivní)POST /api/admin/reconciliation/rebuild/{userId} je zavolánRedis klíče user:{id}:5h_window_start_ts a user:{id}:5h_used_cents smazány; pg_5h_cents = 0, redis_5h_cents = 0
Admin je přihlášen; user nemá žádné záznamy v posledních 7d (weekly window neaktivní)POST /api/admin/reconciliation/rebuild/{userId} je zavolánRedis klíče user:{id}:weekly_window_start_ts a user:{id}:weekly_used_cents smazány
Admin je přihlášen; userId neexistuje v tabulce usersPOST /api/admin/reconciliation/rebuild/{userId} je zavolán404 NOT_FOUND_USER; Redis ani PG se nemění
Non-admin uživatel (ROLE_USER)POST /api/admin/reconciliation/rebuild/{userId} je zavolán401 AUTHENTICATION_FAILED
Admin je přihlášen; rebuild byl úspěšně dokončenPOST /api/admin/reconciliation/rebuild/{userId} je zavolán podruhé (identická data)200 OK; Redis state identický jako po prvním rebuildu; žádná chyba — idempotentní
Admin je přihlášen; global:monthly_used_cents je v Redis s hodnotou XPOST /api/admin/reconciliation/rebuild/{userId} je zavolánglobal:monthly_used_cents v Redis zůstane beze změny; hodnota X je zachována
Admin je přihlášen; MR1 invariant: usage_events neobsahuje crashed/in-flight rows z definicePOST /api/admin/reconciliation/rebuild/{userId} je zavolánStrukturální invariant místo runtime filtru: crashed call nikdy neemittuje NDJSON usage event → žádný usage_events řádek nevznikne. occurred_at je NOT NULL — query-level filtrace IS NULL není potřeba ani možná.
Admin je přihlášen; user má ledger záznamy pouze od 2h zpět (okno začíná za 2h)POST /api/admin/reconciliation/rebuild/{userId} je zavolánwindow_start_5h = MIN(occurred_at) z 2h zpět; TTL Redis klíčů = 3h (zbývající čas); pg_5h_cents = SUM od toho timestampu
Admin je přihlášen; global:monthly_used_cents v Redis underreportuje o 200 centů; PG SUM usage_events = 1500POST /api/admin/reconciliation/rebuild-global je zavolán200 OK; Redis global:monthly_used_cents přepsán na 1500; delta_monthly_cents = 0 v response
Admin je přihlášen; žádné usage_events v aktuálním billing měsíci; Redis global key má stale hodnotuPOST /api/admin/reconciliation/rebuild-global je zavolán200 OK; Redis global:monthly_used_cents nastaven na 0 (nebo smazán na měsíční hranici); delta_monthly_cents = 0
Non-admin uživatel (ROLE_USER)POST /api/admin/reconciliation/rebuild-global je zavolán401 AUTHENTICATION_FAILED; endpoint nedává najevo svou existenci

Known Test Limitations (alpha)

  • TC-06 E2E (Rebuild fixes drift) je v e2e/tests/.../UC-08005_reconciliation-log.spec.ts označený jako test.skip(). Důvod: E2E helper setRedisWeeklyCounter nastaví Redis-only stav pro admin usera, ale BE report endpoint v drop-first DB neenumeruje usera bez PG ledger entries (nebo helper nepokrývá všechny 4 Redis klíče: 5h_window_start_ts, 5h_used_cents, weekly_window_start_ts, weekly_used_cents). BE JUnit TC-16: Rebuild Redis-only user v ReconciliationTestCases.kt pokrývá tuto logiku přímou Redis manipulací a prochází. UI flow lze ověřit v alpha manuálně — admin user nejdřív udělá pár requestů přes UI (UC-08001 Mara conversation), vznikne PG ledger entry, drift se zobrazí, rebuild funguje.

  • TC-API-04 E2E (MR1 invariant — no in-flight rows) je test.skip() — MR1 invariant je vynucen schématem (occurred_at NOT NULL, usage_events je append-only z NDJSON eventu), E2E framework nemá potřebu ověřovat strukturální DB constraint. Backend JUnit TC-14: usage_events MR1 invariant a TC-17: in-flight record nesmí být v pg_monthly_cents v ReconciliationTestCases.kt to pokrývají.


FEEDBACK

Při psaní UC-08005 mi chybělo jasné dokumentování přesného tvaru userId napříč UC-08 sérií — UC-08001 až UC-08004 používají v DB BIGINT user_id, zatímco zadání pro UC-08005 popisuje user_id jako UUID v API responsu. Bylo by užitečné mít v model/README.md explicitně zdokumentováno, zda users.id je BIGINT nebo UUID, aby reference UCs nebyly v rozporu. Dále by pomohlo, kdyby UC-08002 obsahoval explicitní příklad KEYS user:*:5h_used_cents scan patternů nebo preferovaný přístup (SCAN vs KEYS) — pro report endpoint je to relevantní implementační detail, který ovlivňuje výkon v produkci.

FEEDBACK — UI/UX Designer

Pro definici UX Guidelines mi chyběl odkaz na projektový design token soubor (CSS custom properties var(--green), var(--amber), var(--rose) apod.) — musel jsem je odvodit z UC-08004, ne ze zdrojového CSS. Bylo by užitečné mít v design SKILL nebo v projektové spec odkaz na src/assets/tokens.css (nebo ekvivalent), aby designerský agent mohl ověřit skutečné hodnoty tokenů místo opisování z jiného UC. Dále: zadání popisovalo threshold pro DriftBadge červené barvy jako |delta| >= 50¢ (absolutní) i >= 5 % (relativní), ale chybělo, zda jsou obě podmínky splněny zároveň (AND) nebo stačí jedna (OR) — pro implementaci DriftBadge je to klíčové; bylo by dobré, aby UX zadání explicitně uvádělo booleovský operátor.


Was this page helpful?

Thanks for the feedback.