Internal Documentation internal
TalkIDE internal documentation

Any authenticated user can view their AI credit balance and current quota consumption (5-hour and weekly rolling windows) in a single unified box in their profile — no special role required.

  • The unified box lives in BillingSection.vue in ProfileScreen.vuenot a new route.
  • The box contains three stacked elements: (1) AI credit balance (AiCreditPanel), (2) 5h window progress bar, (3) weekly window progress bar.
  • UC-08006 owns the entire unified box. Previously the credit panel was a separate pre-existing component; it is now considered part of this box’s scope.
  • Data is loaded on component mount and refreshed on demand via a single Refresh button for the whole box. No polling, no SSE.
  • Quota windows display percentage and time-to-reset only — no dollar amounts. The internal rate-limit threshold (e.g. limitCents) is intentionally hidden from the user.
  • AI credit balance (real-money credit) does show dollar amounts — that is legitimate billing information.
  • Backend reuses QuotaCheckService.getSnapshot(userId) (introduced in UC-08002). The endpoint adds computation of resetsAt timestamps per window derived from windowStartAt + TTL.
  • Data source is Redis (fast, in-window read via Lua snapshot script). PostgreSQL usage_events (UC-08005 SSoT) are not queried by this endpoint — Redis is the live quota state.
  • Window reset logic:
    • 5-hour window: windowStartAt + 5h (18 000 s). If windowStartAt is null (no usage yet), returns now + 5h as a conservative upper bound.
    • Weekly window: windowStartAt + 7d (604 800 s). Same null-guard applies.
  • Thresholds for UI colour coding (FE-only):
    • < 50 % → green (normal)
    • ≥ 50 % → amber (mild warning)
    • ≥ 85 % → amber dark (strong warning)
    • ≥ 95 % → red (critical)
  • Accessibility: progress bars carry aria-label with percentage only — no dollar amounts.
  • i18n: both Czech (cs) and English (en) translations required.
  • Related: UC-08002 Mara Fatigue & Quota Enforcement — defines the Redis quota model. UC-08005 Reconciliation Log — defines usage_events as SSoT post-MR1.
  • Cross-reference: UC-08004 admin capacity dashboard continues to display $ amounts for admins — the no-$ rule applies only to this user-facing UC-08006 view.
sequenceDiagram
    actor User

    User->>+FE: opens Profile → Billing & Usage section

    FE->>FE: onMounted — trigger quota load

    FE->>+BE: GET /api/v1/users/me/quota <br> Authorization: Bearer {accessToken}

    BE->>BE: extract userId from JWT

    alt JWT missing or invalid
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
        FE-->>User: show auth error / redirect to login
    end

    BE->>+QuotaCheckService: getSnapshot(userId)
    Note over QuotaCheckService: SNAPSHOT_LUA_SCRIPT — read-only <br> Redis keys: 5h_used, 5h_window_start, <br> weekly_used, weekly_window_start

    QuotaCheckService-->>-BE: UserFatigueSnapshot

    BE->>BE: compute resetsAt per window <br> (windowStartAt + TTL, null-guarded)

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

    FE->>FE: compute pct = usedCents / limitCents per window
    FE->>FE: render progress bars + % labels + time-to-reset countdown

    FE-->>-User: display unified AI credit & usage box

    User->>+FE: clicks Refresh button

    FE->>FE: set loading = true, disable Refresh button

    FE->>+BE: GET /api/v1/users/me/quota <br> Authorization: Bearer {accessToken}

    BE->>+QuotaCheckService: getSnapshot(userId)
    QuotaCheckService-->>-BE: UserFatigueSnapshot
    BE-->>-FE: 200 OK <br> QuotaSnapshotResponse

    FE->>FE: update reactive state, set loading = false

    FE-->>-User: updated usage values displayed

GET /api/v1/users/me/quota — no request body; authentication via JWT Bearer token.

200 OK QuotaSnapshotResponse:

{
  "fiveHourWindow": {
    "usedCents": 320,
    "limitCents": 500,
    "resetsAt": "2026-05-09T18:42:00Z"
  },
  "weeklyWindow": {
    "usedCents": 1850,
    "limitCents": 3000,
    "resetsAt": "2026-05-14T13:07:00Z"
  }
}

Field notes:

  • usedCents / limitCents — integer, USD micro-cents (1 cent = $0.01). BE returns real values; other clients (admin, internal tooling) may use them freely.
  • FE computes pct = usedCents / limitCents and renders only the percentage — dollar amounts from quota windows are never shown to the end user in UC-08006.
  • resetsAt — ISO 8601 UTC timestamp. FE computes a human-readable countdown from this value locally (e.g. “Resets in 2h 18m”).
  • When a window has never been used, usedCents = 0 and resetsAt = now + window TTL (conservative).

401 Unauthorized ErrorResponse:

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

Frontend

Validations

Field / ControlConstraintsNote
Refresh buttondisabled while loading === truePrevents concurrent requests
Progress bar valuepct = usedCents / limitCents, clamped to [0, 100] % before renderGuards against usedCents > limitCents edge case (over-budget)
Quota window displayFE renders only pct (e.g. 12%) — does NOT render $ amounts from usedCents / limitCentslimitCents value is intentionally hidden; only ratio is shown
resetsAt countdowncomputed from resetsAt ISO timestamp at render time, not polledRe-evaluated on each refresh only
Colour threshold (≥ 50 %)amber warning class appliedAccessible: numeric % label always visible
Colour threshold (≥ 85 %)amber dark warning class appliedAccessible: numeric % label always visible
Colour threshold (≥ 95 %)red danger class appliedAccessible: numeric % label always visible

UX Guidelines

User Flow

Path 1 — On-mount automatický load
  1. User klikne na “Billing” v levém navigačním panelu ProfileScreen (?section=billing).
  2. BillingSection se mountuje — volá budgetStore.fetchBudget() (existující AI credit balance) a nově usageStore.fetchUsage().
  3. Během fetchu se zobrazí loading skeleton v celém unified boxu.
  4. Po úspěšném načtení — skeleton mizí, zobrazí se živá data:
    • AI credit balance: existující AiCreditPanel (zůstává nezměněn, zobrazuje $ balance).
    • 5h window progress bar: procento + time-to-reset countdown (bez $ částek).
    • Weekly window progress bar: procento + time-to-reset countdown (bez $ částek).
  5. Refresh button (top-right boxu) je aktivní ihned po první load.
Path 2 — Manual refresh
  1. User klikne Refresh button (ikona RefreshCw z lucide-vue-next + label “Refresh”).
  2. Tlačítko přejde do loading stavu: ikona se roztočí (animate-spin), label změní na “Refreshing…” — tlačítko zůstane viditelné ale disabled.
  3. usageStore.fetchUsage() se zavolá znovu.
  4. Existující data v progress barech zůstanou zobrazena (žádný skeleton — jen tiché update hodnot po fetch).
  5. Po dokončení fetch — button se vrátí do normálního stavu, hodnoty se aktualizují in-place s transition-all duration-300.
Path 3 — Error při fetch
  1. Fetch selže (network error nebo 401).
  2. Toast “Failed to load usage data. Please try again.” (rose, 5 s, dismissible) se zobrazí.
  3. Existující data (pokud byly jednou načteny) zůstanou v progress barech — uživatel vidí stale data s discreet indikátorem “Last updated: X min ago” ve fg-4 barvě.
  4. Pokud initial load selže (žádná předchozí data) — progress bar sekce zobrazí ”— Unable to load” s Retry tlačítkem.

Layout

Screen type: Rozšíření existující sekce (ne nová route) Umístění: BillingSection.vue — sjednocený box obsahující AI credit balance + oba quota progress bary Container: Sdílí existující main class="py-8 px-8 pb-16" z ProfileScreen

ASCII mockup — unified AI credit & usage box (desktop)
┌─ Profile → Billing & Usage ─────────────────────────────────────────────┐
│                                                                          │
│  BILLING & USAGE                                                         │
│  Manage your AI credits and monitor usage windows.                       │
│                                                                          │
│  ┌─ AI CREDIT & USAGE ─────────────────────────────────────── [↻] ───┐  │
│  │                                                                    │  │
│  │  AI CREDIT                                                         │  │
│  │  $48.20 remaining                     Spent: $51.80               │  │
│  │  ████████████░░░░░░░░░░░  52%         Initial: $100.00            │  │
│  │                                                                    │  │
│  │  ─────────────────────────────────────────────────────────        │  │
│  │                                                                    │  │
│  │  5H ROLLING WINDOW                                                 │  │
│  │  ████████░░░░░░░░░░░░░░░░░░░░  12%                                 │  │
│  │  Resets in 1h 18m                                                  │  │
│  │                                                                    │  │
│  │  WEEKLY ROLLING WINDOW                                             │  │
│  │  ████████████████░░░░░░░░░░░░  62%                                 │  │
│  │  Resets in 4d 7h                                                   │  │
│  │                                                                    │  │
│  └────────────────────────────────────────────────────────────────────┘  │
│                                                                          │
│  (existující HostingBillingPanel, Estimated costs, FieldRows, Invoices)   │
└──────────────────────────────────────────────────────────────────────────┘

Klíčové: quota windows zobrazují pouze % a countdown — žádné “12% of $10.00” ani žádná jiná $ hodnota pro quota windows. AI credit balance nahoře ($ zůstatok) je výjimka — to je legitimní billing info.

ASCII mockup — 5h window detail
┌─ 5H ROLLING WINDOW ────────────────────────────────────────────────────┐
│  5H ROLLING WINDOW                                                     │
│                                                                        │
│  ┌── SpendProgressBar ──────────────────────────────────────────────┐  │
│  │  [████████░░░░░░░░░░░░░░░░░░░░]  bar track h-2.5                  │  │
│  │                                                      12%          │  │
│  └──────────────────────────────────────────────────────────────────┘  │
│                                                                        │
│  Resets in 1h 18m                                                      │
│  (text-xs fg-3, ikona Clock size-12 inline vlevo)                      │
└────────────────────────────────────────────────────────────────────────┘
Responsive chování
BreakpointLayout
>= md (768px)Dvě window progress bar sekce side-by-sidegrid grid-cols-2 gap-4
< md (< 768px)Dvě window progress bar sekce stackedgrid grid-cols-1 gap-4

Refresh button je v top-right celého unified boxu na všech breakpointech.


Komponenty

ElementKomponentaSouborMapping
Unified box wrappersekce v BillingSectionsrc/screens/profile/components/BillingSection.vueNahrazuje předchozí oddělený layout
AI credit panelAiCreditPanel (existující)src/screens/budget/components/AiCreditPanel.vueBeze změny, první řádek boxu
5h window progress barSpendProgressBar (reuse)src/screens/admin/components/SpendProgressBar.vuepct z usedCents/limitCents; bez $ popisků
Weekly window progress barSpendProgressBar (reuse)src/screens/admin/components/SpendProgressBar.vuepct z usedCents/limitCents; bez $ popisků
Time-to-reset display (oba windows)inline pod progress baremClock (lucide, size 12) + humanized string
Refresh buttonlucide RefreshCw + buttonanimate-spin při loading, aria-label povinný; jeden button pro celý box
Error toastexistující toast systémrose, 5 s, dismissible

Poznámka pro budoucí refactor: SpendProgressBar se importuje z src/screens/admin/ — uvažovat o přesunu do src/common/components/ až bude sdílena ve více než dvou místech.

SpendProgressBar props mapping

Existující komponenta (src/screens/admin/components/SpendProgressBar.vue) přijímá:

  • spent: number — použité centy (z API usedCents)
  • cap: number — limit v centech (z API limitCents)

Pozor: Pro UC-08006 quota windows se předávají usedCents a limitCents do SpendProgressBar pro výpočet %, ale komponenta nesmí renderovat $ hodnoty — buď se odstraní z template slotu, nebo se přidá prop hideAmounts: boolean. Doporučeno: přidat volitelný prop hideAmounts?: boolean (default false); při hideAmounts=true renderuje jen progress bar + % číslo.

Zároveň je nutné rozšíření prop earlyWarnAt?: number (default undefined) — pro UC-08006 volat s earlyWarnAt=50 (4stupňová škála 50/85/95).


Color Thresholds

Konzistentní s UC-08004 SpendProgressBar, rozšířeno o 50% early warning:

RozsahBarvaCSS varPopis
0–49 %Zelenávar(--green)Normální stav
50–84 %Žlutá/Ambervar(--amber)Mírné varování — přes polovinu okna
85–94 %Oranžová (amber dark)oklch(0.68 0.16 55)Silné varování — limit se blíží
95–100 %Červenávar(--rose)Kritické — téměř vyčerpáno

Barva NIKDY není jediný indikátor — procento je vždy textově zobrazeno. Dolarové limity quota oken se nezobrazují.


Time-to-reset Display

Formát: “Resets in Xd Yh Zm” — humanized, počítáno na FE z resetsAt ISO timestamp.

resetsAt: "2026-05-09T18:30:00Z"
now:       "2026-05-09T17:12:00Z"
delta:     1h 18m
display:   "Resets in 1h 18m"

Pravidla formátování:

DeltaFormátPříklad
> 1 denXd Yh”Resets in 4d 7h”
> 1 hodinaXh Ym”Resets in 1h 18m”
> 1 minutaXm”Resets in 42m”
< 1 minuta”Resets soon”
resetsAt v minulosti”Resetting…”
resetsAt nullskryj řádek

Implementace: Pure computed z resetsAt ISO string, žádný live countdown (hodnota je statická po fetch, obnoví se jen při manuálním refresh). FE utility funkce humanizeDuration(resetsAt: string): string.

i18n: Klíč billing.resetsIn s interpolací — viz sekce i18n níže.


Loading States

On-mount (initial load)
  • Unified box zobrazí loading skeleton pro quota windows souběžně (usageStore.loading === true).
  • AiCreditPanel má vlastní skeleton (existující budgetStore.loading), není dotčen.
  • Nadpis boxu a Refresh button se zobrazí ihned (nejsou skeletovány).
Manual refresh
  • Refresh button: ikona RefreshCw přejde na animate-spin, label změní na t('billing.refreshing').
  • Button se stane disabled (opacity-60, cursor-not-allowed).
  • Obsah progress barů se neskeltonizuje — data zůstanou viditelná, jen tiché in-place update po fetch.
  • Po dokončení fetch: spinner → normální ikona, values se aktualizují s transition-all duration-300.

Error States

ChybaStav datZobrazení
Initial load — network errorŽádná předchozí dataQuota window sekce zobrazí “Unable to load” text + Retry button
Initial load — 401Žádná předchozí dataToast “Session expired” + redirect na login (existující auth interceptor)
Refresh — network errorStale data dostupnáToast (rose, 5 s): t('billing.errors.refreshFailed'). Progress bary zachovají poslední úspěšná data.
Refresh — 401Stale data dostupnáExistující auth interceptor → redirect na login
resetsAt null / malformedSkryj countdown řádek, nezobrazuj fallback datum

Toast behavior: Neblokující — zobrazí se v rohu (existující toast systém projektu), automaticky zmizí po 5 s, dismissible.


Accessibility

Progress bars

Každý SpendProgressBar pro quota windows musí mít:

  • role="progressbar" na track elementu
  • aria-valuemin="0" aria-valuemax="100" aria-valuenow="{pct}"
  • aria-label ve formátu obsahujícím pouze % — bez $ částek

Příklady aria-label:

  • "5h window: 12% used" (krátká forma)
  • "5h window: 12% — resets in 1h 18m" (doporučená forma s countdown)

Příklad implementace v slotu:

<SpendProgressBar
  :spent="usedCents"
  :cap="limitCents"
  :hide-amounts="true"
  :early-warn-at="50"
  :aria-label="`${t('billing.fiveHour.ariaLabel', { pct, resetIn: humanizeDuration(resetsAt) })}`"
/>

Pozor: aria-label nesmí obsahovat $ amount pro quota windows. I18n klíč billing.fiveHour.ariaLabel musí být upraven — viz sekce i18n.

Refresh button
<button
  :aria-label="t('billing.refresh.ariaLabel')"
  :aria-busy="usageStore.loading"
  :disabled="usageStore.loading"
  @click="usageStore.fetchUsage()">
  <RefreshCw :size="14" :class="{ 'animate-spin': usageStore.loading }" />
  <span>{{ usageStore.loading ? t('billing.refreshing') : t('billing.refresh') }}</span>
</button>
Obecná pravidla
  • Unified box dostane role="region" s aria-label="AI credit and usage".
  • AiCreditPanel má vlastní accessibility řešení (existující, nezměněno).
  • Time-to-reset text je plain <p> (žádný ARIA role), barva není podmíněná.
  • Keyboard navigace: Tab pořadí — Refresh button → AiCreditPanel → 5h progress bar → Weekly progress bar → HostingBillingPanel.
  • Focus visible na Refresh button — projekt používá standard browser focus ring, neodebrávat.
  • Skeleton stav: aria-live="polite" na wrapper quota sekce — screen reader oznámí update po loadu.
  • WCAG AA kontrast: amber/rose/green accenty na var(--bg-2) background jsou ≥ 4.5:1 (konzistentní s UC-08004 rozhodnutím).

i18n Keys

Czech (cs)
billing.usage.sectionTitle = "Využití AI"
billing.usage.sectionSubtitle = "Přehled spotřeby v klouzavých oknech"

billing.fiveHour.title = "5h okno"
billing.fiveHour.ariaLabel = "5h okno: {pct}% — resetuje se za {resetIn}"

billing.weekly.title = "Týdenní okno"
billing.weekly.ariaLabel = "Týdenní okno: {pct}% — resetuje se za {resetIn}"

billing.refresh = "Obnovit"
billing.refreshing = "Obnovuji..."
billing.refresh.ariaLabel = "Obnovit data využití"

billing.resetsIn = "Resetuje se za {duration}"
billing.resetsSoon = "Resetuje se brzy"
billing.resetting = "Resetování..."

billing.errors.loadFailed = "Nepodařilo se načíst data využití. Zkus to znovu."
billing.errors.refreshFailed = "Obnovení selhalo. Data jsou z {time}."

billing.retry = "Zkusit znovu"
billing.unableToLoad = "Nelze načíst data"
billing.lastUpdated = "Naposledy aktualizováno: {time}"
English (en)
billing.usage.sectionTitle = "AI Usage"
billing.usage.sectionSubtitle = "Consumption overview across rolling windows"

billing.fiveHour.title = "5H Rolling Window"
billing.fiveHour.ariaLabel = "5h window: {pct}% — resets in {resetIn}"

billing.weekly.title = "Weekly Rolling Window"
billing.weekly.ariaLabel = "Weekly window: {pct}% — resets in {resetIn}"

billing.refresh = "Refresh"
billing.refreshing = "Refreshing..."
billing.refresh.ariaLabel = "Refresh usage data"

billing.resetsIn = "Resets in {duration}"
billing.resetsSoon = "Resets soon"
billing.resetting = "Resetting..."

billing.errors.loadFailed = "Failed to load usage data. Please try again."
billing.errors.refreshFailed = "Refresh failed. Data is from {time}."

billing.retry = "Retry"
billing.unableToLoad = "Unable to load"
billing.lastUpdated = "Last updated: {time}"

Mobile Responsiveness

Primární layout: desktop (ProfileScreen je desktop-first, min-width 1240px grid).

BreakpointLayout quota windowsRefresh buttonProgress bary
>= md (768px)grid grid-cols-2 gap-4 — side-by-sideInline top-right unified boxuStandardní výška
< md (768px)grid grid-cols-1 gap-4 — stackedInline top-right unified boxuPlná šířka

Minimální šířka progress barů: 280px (zajistí SpendProgressBar čitelnost).


Vazby na existující komponenty

SpendProgressBar (src/screens/admin/components/SpendProgressBar.vue):

  • Existující props: spent: number, cap: number
  • Nový prop: hideAmounts?: boolean (default false) — při true renderuje jen bar + % bez $ hodnot
  • Nový prop: earlyWarnAt?: number (default undefined) — pro 4stupňovou color škálu; UC-08006 volá s earlyWarnAt=50

BillingSection (src/screens/profile/components/BillingSection.vue):

  • Přidat import SpendProgressBar z admin komponent
  • Přidat import nového useUsageStore (pinia store pro UC-08006 API)
  • Refaktorovat layout do unified boxu: AiCreditPanel + quota windows sekce v jednom containeru s jedním Refresh buttonem

Backend

Validations

FieldConstraintsNote
JWT Authorization headernot_blank, valid signature, not expiredSpring Security filter — returns 401 if missing or invalid
userIdextracted from JWT principal; must resolve to an existing userAuthorizationService.getCurrentUserId()
Redis snapshotread-only; no writes triggered by this endpointSNAPSHOT_LUA_SCRIPT

Test Cases

GIVENWHENTHEN
Authenticated user with active 5h and weekly usage in Redis (used5h=320, limit5h=500, usedWk=1850, limitWk=3000)GET /api/v1/users/me/quota is called200 OK with correct usedCents and limitCents for both windows; resetsAt is a future UTC timestamp; FE renders 5h as 64% and weekly as 62% with countdown — no $ amounts in quota window UI
Authenticated user with 5h window at 86 % of limit (approaching threshold)GET /api/v1/users/me/quota is called200 OK; FE computes pct > 0.85; FE applies amber dark colour class to 5h progress bar; % label visible
Authenticated user with weekly window at 97 % of limit (danger zone)GET /api/v1/users/me/quota is called200 OK; FE computes pct > 0.95; FE applies red colour class to weekly progress bar; % label visible
Request sent without Authorization headerGET /api/v1/users/me/quota is called401 Unauthorized with code: AUTHENTICATION_FAILED
Authenticated user with no prior usage (all Redis keys absent / zero)GET /api/v1/users/me/quota is called200 OK; usedCents = 0 for both windows; FE renders 0%; resetsAt = approximately now + TTL
User is on Billing & Usage panel and clicks the Refresh buttonsecond GET /api/v1/users/me/quota is called200 OK; FE updates reactive state with new values; Refresh button re-enabled after response
Quota window cards rendered after successful loadUI text content is inspectedString "$" does NOT appear inside quota window progress bar elements or their labels; "$" MAY appear inside AI credit balance section

Was this page helpful?

Thanks for the feedback.