Internal Documentation internal
TalkIDE internal documentation

Nový endpoint pro per-projekt AI breakdown a per-namespace hosting breakdown, plus rozšíření kredit panelů v Profile/Billing o reálná data (nahrazení mockupu USAGE konstanty).

  • Stávající GET /api/v1/users/me/usage/breakdown vrací AI + hosting součty bez per-projekt dimenze. Tato UC přidává nový endpoint GET /api/v1/users/me/usage/project-breakdown vracející obě sekce v jedné odpovědi.
  • Rozhodnutí o tvaru endpointu: jeden kombinovaný endpoint (ne dva). Důvod: FE potřebuje obě sekce (AI top-projekty + hosting per-namespace) najednou při otevření billing panelu, jediný network call je efektivnější a response je logicky celistvá (stejný from/to parametr platí pro obě sekce).
  • AI per-projekt: SUM(charged_cost_usd) + token součty z usage_events GROUP BY project_id, seřazeno DESC, limit N. Pro projectName se provede LEFT JOIN na projects tabulku (LEFT kvůli smazaným projektům — orphan project_id se vrátí s projectName = null).
  • Hosting per-namespace: SUM(cost_usd) z hosting_cost_events GROUP BY namespace. Tabulka nemá project_id — překlad na project name není možný bez lookup tabulky namespace → project, která neexistuje. Breakdown je tedy per-namespace, bez překladu. Marže se aplikuje read-side stejným způsobem jako v GetUsageBreakdownUseCase (přes PricingService).
  • Agregace nativním SQL dotazem (SUM v DB) — bez N+1 na PricingService. Marže se aplikuje jednou read-side na celkový součet per skupinu.
  • Endpoint je per-user (/users/me/), autentizovaný JWT. Žádná Liquibase migrace není potřeba — read-only feature nad existujícími tabulkami.
  • FE fe#10: AiCreditPanel.vue zobrazí „Top projekty” list (top-3), HostingBillingPanel.vue nahradí hardcoded USAGE mockup reálnými per-namespace daty. Přibývá per-period přepínač (this month / last month). Vizuální cue (červená) při budgetu < 10 % funguje již nyní přes accentColor v AiCreditPanelHostingBillingPanel dostane analogický mechanismus.
  • i18n klíče topProjects* v budget/i18n.ts již existují — nemusí se přidávat.
  • Related: UC-10008, UC-10012 F2.

A. Sekvence — načtení per-projekt breakdown

sequenceDiagram
    actor User

    User->>+FE: otevře Profile → Billing

    FE->>FE: BillingSection.onMounted()
    FE->>+BE: GET /api/v1/users/me/usage/project-breakdown<br/>?from=2026-05-01&to=2026-05-31&aiLimit=3

    BE->>BE: ověř JWT, extrahuj userId
    alt JWT chybí nebo neplatný
        BE-->>FE: 401 Unauthorized<br/>ErrorResponse
    end

    BE->>BE: validuj parametry (from ≤ to, aiLimit 1-10)
    alt parametry neplatné
        BE-->>FE: 400 Bad Request<br/>ErrorResponse
    end

    BE->>DB: SELECT project_id, p.name, SUM(charged_cost_usd),<br/>SUM(input_tokens), SUM(output_tokens), COUNT(*)<br/>FROM usage_events e LEFT JOIN projects p ON e.project_id = p.id<br/>WHERE e.user_id = :userId AND e.kind = 'AI'<br/>AND e.occurred_at >= :from AND e.occurred_at < :to<br/>GROUP BY e.project_id, p.name<br/>ORDER BY SUM(charged_cost_usd) DESC<br/>LIMIT :aiLimit
    DB-->>BE: List[AiProjectRow]

    BE->>DB: SELECT namespace, SUM(cost_usd), COUNT(*)<br/>FROM hosting_cost_events<br/>WHERE user_id = :userId<br/>AND window_start >= :from AND window_start < :to<br/>GROUP BY namespace<br/>ORDER BY SUM(cost_usd) DESC
    DB-->>BE: List[HostingNamespaceRow]

    BE->>BE: aplikuj markup read-side na hosting součty<br/>(PricingService)

    BE-->>-FE: 200 OK<br/>ProjectBreakdownResponse

    FE->>FE: usageStore.applyProjectBreakdown(response)
    FE->>-User: zobrazí AI top-projekty + hosting per-namespace

B. Sekvence — per-period přepínač (this month / last month)

sequenceDiagram
    actor User

    User->>+FE: klikne na přepínač "Last month"

    FE->>FE: selectedPeriod = 'lastMonth'<br/>from = 1. den minulého měsíce<br/>to = poslední den minulého měsíce

    FE->>+BE: GET /api/v1/users/me/usage/project-breakdown<br/>?from=2026-04-01&to=2026-04-30&aiLimit=3

    BE->>DB: (stejné dotazy jako sekvence A, jiné datum)
    DB-->>BE: výsledky za duben

    BE-->>-FE: 200 OK<br/>ProjectBreakdownResponse

    FE->>-User: aktualizuje panely pro minulý měsíc

API kontrakt

GET /api/v1/users/me/usage/project-breakdown

Query parametry:

ParametrTypPovinnýDefaultOmezeníPopis
fromISO date YYYY-MM-DDanotoZačátek období (inclusive, UTC)
toISO date YYYY-MM-DDanofrom, ≤ today+1Konec období (inclusive, UTC)
aiLimitintegerne31–10Max počet AI projektů v response

200 OK ProjectBreakdownResponse:

{
  "period": {
    "from": "2026-05-01",
    "to": "2026-05-31"
  },
  "ai": {
    "topProjects": [
      {
        "projectId": 12,
        "projectName": "todo-list",
        "totalChargedCostUsd": 0.84,
        "totalInputTokens": 142000,
        "totalOutputTokens": 38000,
        "eventCount": 47
      },
      {
        "projectId": 7,
        "projectName": null,
        "totalChargedCostUsd": 0.21,
        "totalInputTokens": 31000,
        "totalOutputTokens": 9000,
        "eventCount": 12
      }
    ],
    "totalChargedCostUsd": 1.05,
    "totalEventCount": 59
  },
  "hosting": {
    "namespaces": [
      {
        "namespace": "popelkam-talkide",
        "environmentId": 1,
        "totalCostUsd": 0.42,
        "eventCount": 18
      },
      {
        "namespace": "h-talkide",
        "environmentId": 2,
        "totalCostUsd": 0.11,
        "eventCount": 7
      }
    ],
    "totalCostUsd": 0.53,
    "totalEventCount": 25
  }
}

Pozn. k projectName = null: projekt byl smazán, ale usage events zůstávají (append-only). FE zobrazí fallback text (např. (deleted project)). environmentId v hosting sekci je nullable — null pro namespace bez environment_id attribution (orphan namespace, ADR-026 §6).

Pozn. k ai.totalChargedCostUsd a ai.totalEventCount: jde o součet všech AI usage events uživatele za dané období (přes všechny projekty), ne jen součet hodnot zobrazených v topProjects. topProjects je pouze top-N contributors — podmnožina. Celkové součty slouží FE pro zobrazení celkové útraty a porovnání s budgetem, nezávisle na hodnotě aiLimit.

400 Bad Request (validace parametrů) ErrorResponse:

{
  "code": "VALIDATION",
  "message": "Bad request"
}

401 Unauthorized ErrorResponse:

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

Datový model — čtené tabulky (jen přehled, žádná migrace)

erDiagram
    USAGE_EVENTS {
        bigint id
        bigint user_id
        bigint project_id
        string kind
        bigint input_tokens
        bigint output_tokens
        numeric charged_cost_usd
        timestamp occurred_at
    }
    PROJECTS {
        bigint id
        string name
        string slug
    }
    HOSTING_COST_EVENTS {
        bigint id
        bigint user_id
        string namespace
        bigint environment_id
        numeric cost_usd
        timestamp recorded_at
    }
    USAGE_EVENTS }o--o| PROJECTS : "LEFT JOIN project_id → id"

Žádná nová tabulka ani sloupec. Endpoint je čistě read-only nad existujícím schématem. Poslední Liquibase changeset je 0045 (create-hosting-enforcement-log) — tento UC nevyžaduje 0046 ani vyšší.


Backend

Nativní SQL dotazy (v novém ProjectBreakdownRepository)

AI per-projekt dotaz:

SELECT
    e.project_id                        AS projectId,
    p.name                              AS projectName,
    SUM(e.charged_cost_usd)             AS totalChargedCostUsd,
    SUM(e.input_tokens)                 AS totalInputTokens,
    SUM(e.output_tokens)                AS totalOutputTokens,
    COUNT(*)                            AS eventCount
FROM usage_events e
LEFT JOIN projects p ON e.project_id = p.id
WHERE e.user_id = :userId
  AND e.kind = 'AI'
  AND e.occurred_at >= :from
  AND e.occurred_at < :to
GROUP BY e.project_id, p.name
ORDER BY SUM(e.charged_cost_usd) DESC
LIMIT :aiLimit

Hosting per-namespace dotaz:

SELECT
    h.namespace                         AS namespace,
    h.environment_id                    AS environmentId,
    SUM(h.cost_usd)                     AS totalCostUsd,
    COUNT(*)                            AS eventCount
FROM hosting_cost_events h
WHERE h.user_id = :userId
  AND h.window_start >= :from
  AND h.window_start < :to
GROUP BY h.namespace, h.environment_id
ORDER BY SUM(h.cost_usd) DESC

environment_id je ve GROUP BY přes namespace — pro daný namespace je vždy jeden environment_id (nebo NULL pro orphan). Pokud by z historických dat existoval namespace se dvěma různými environment_id, GROUP BY produkuje více řádků; FE je oba zobrazí. V praxi je tento edge-case nepravděpodobný.

Nové Kotlin třídy

features/usage/
  api/
    dto/
      ProjectBreakdownResponse.kt      (data class, API kontrakt)
  domain/
    GetProjectBreakdownUseCase.kt      (@Service — orchestrace, markup read-side)
  data/
    ProjectBreakdownRepository.kt      (Spring Data / native query interface)

UsageController rozšířen o:

@GetMapping("/project-breakdown")
fun getProjectBreakdown(
    @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) from: LocalDate,
    @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) to: LocalDate,
    @RequestParam(defaultValue = "3") aiLimit: Int,
): ResponseEntity<ProjectBreakdownResponse>

Markup aplikace (hosting sekce)

Marže se aplikuje read-side na totalCostUsd každého namespace záznamu stejným způsobem jako v GetUsageBreakdownUseCase — přes PricingService (injektovat PricingService, nikoliv PricingMarkupConfigRepository přímo):

val markupFactor = pricingService.getEffectiveMarkup()
hostingRow.totalCostUsd = pricingService.applyMarkup(hostingRow.totalCostUsd, markupFactor)

AI sekce čte charged_cost_usd přímo z DB (marže je již aplikovaná při zápisu, immutable).

Validations

ParametrConstraintsNote
fromnot_null, valid ISO date400 VALIDATION
tonot_null, valid ISO date, ≥ from400 VALIDATION
aiLimit1–10 (inclusive)400 VALIDATION při < 1 nebo > 10; default 3 bez validační chyby
JWTnot_null, valid, not_expired401 AUTHENTICATION_FAILED

Frontend

Nový Pinia store / composable useProjectBreakdown

Store (nebo composable) src/screens/budget/composables/useProjectBreakdown.ts:

interface AiProjectItem {
  projectId: number
  projectName: string | null
  totalChargedCostUsd: number
  totalInputTokens: number
  totalOutputTokens: number
  eventCount: number
}

interface HostingNamespaceItem {
  namespace: string
  environmentId: number | null
  totalCostUsd: number
  eventCount: number
}

interface ProjectBreakdown {
  period: { from: string; to: string }
  ai: {
    topProjects: AiProjectItem[]
    totalChargedCostUsd: number
    totalEventCount: number
  }
  hosting: {
    namespaces: HostingNamespaceItem[]
    totalCostUsd: number
    totalEventCount: number
  }
}

Endpoint: GET /api/v1/users/me/usage/project-breakdown?from=&to=&aiLimit=3

Per-period přepínač

BillingSection.vue (nebo nová helper komponenta) vystaví přepínač:

Volbafromto
This month (default)1. den aktuálního měsícednes
Last month1. den minulého měsíceposlední den minulého měsíce

Přepnutí triggernuje nový fetch s příslušnými daty. Přepínač je sdílený pro obě sekce (AI + hosting).

Přechod přes rok-hranici (prosinec → leden): pro „Last month” v lednu je from = 1. 12. předchozího roku a to = 31. 12. předchozího roku. Výpočet musí přecházet rok správně — použijte date-fns (startOfMonth + subMonths + endOfMonth), nikoli ruční month - 1 aritmetiku, která by v lednu vrátila month = 0.

Rozšíření AiCreditPanel.vue

Pod progress barem přibyde sekce “Top projekty” (pod stávající BudgetWarningBanner):

[ Top projects this period ]
  todo-list          $0.84
  (deleted project)  $0.21
  my-app             $0.09

  [ loading skeleton / empty state / error state ]
  • Napojeno na useProjectBreakdown — data načítá BillingSection a předává prop (nebo composable je sdílené). Preferovaný přístup: BillingSection volá fetch a předává topProjects jako prop do AiCreditPanel.
  • i18n klíče: topProjectsTitle, topProjectsLoading, topProjectsEmpty, topProjectsError (vše již existuje v budget/i18n.ts).
  • projectName = null → zobrazit (deleted project) — nový i18n klíč topProjectsDeleted.
  • Vizuální cue: AiCreditPanel již má accentColor (červená při < 10 % nebo exhausted) — tato logika se nemění.

Rozšíření HostingBillingPanel.vue

Pod progress barem přibyde per-namespace breakdown (nahrazuje mockup v BillingSection):

[ Usage by namespace ]
  popelkam-talkide   $0.42
  h-talkide          $0.11
  • USAGE konstanta + TODO Stopa C.4 blok v BillingSection.vue (řádky 48–63 a 136–194) se nahradí reálnými daty z useProjectBreakdown.
  • Vizuální cue: hosting budget < 10 % → accentColor červená (analogicky jako AI panel). HostingBillingPanel (přejmenováno z HostingBillingPanel v fe#38 — postpaid framing) má zatím statické accentColor = 'var(--teal)' — přidá se computed stejně jako v AiCreditPanel.

Hostingový low-budget práh

StavPodmínkaVizuální efekt
NormalhostingSpentPercent < 90teal barva
LowhostingSpentPercent >= 90červená (var(--rose))
Exhausted (zero)hostingAccruedPercent >= 100 (hosting je postpaid — žádný hosting credit balance)červená

i18n klíče (nové)

V budget/i18n.ts přidat:

KlíčENCS
topProjectsDeleted(deleted project)(smazaný projekt)
hostingNamespacesTitleUsage by namespaceVyužití dle namespace
hostingNamespacesLoadingLoading namespace usage...Načítám využití namespace...
hostingNamespacesEmptyNo hosting usage yet.Zatím žádné využití hostingu.
hostingNamespacesErrorCould not load namespace usage.Nepodařilo se načíst využití namespace.
periodThisMonthThis monthTento měsíc
periodLastMonthLast monthMinulý měsíc

Validations (FE)

ParametrConstraintsNote
periodpřepínač enum (thisMonth / lastMonth)výpočet from/to na FE straně
aiLimitfixní hodnota 3nekonfiguruje uživatel

Business pravidla

  1. Agregace charged, ne raw. AI sekce čte charged_cost_usd (po marži, immutable při zápisu). Hosting sekce čte cost_usd (raw) a marži aplikuje read-side.
  2. Smazaný projekt. project_id bez záznamu v projects (smazán) → projectName = null. FE zobrazí fallback. BE nefiltruje tyto záznamy ven — usage zůstává auditovatelné.
  3. Orphan namespace. hosting_cost_events.environment_id = null → namespace bez environment attribution (ADR-026 §6, konzervativní non-charge). V response je environmentId = null.
  4. Prázdné období. Bez usage events za dané období → topProjects: [], namespaces: [], totalChargedCostUsd: 0.
  5. aiLimit max 10. Ochrana před příliš velkými response; limit není pro uživatele viditelný (FE volá s fixním 3).
  6. Žádná Liquibase migrace. Read-only endpoint — žádná změna schématu. Poslední changeset 0045 (create-hosting-enforcement-log) zůstává posledním.
  7. Marže 0 = no-op. Markup = 0 → hosting totalCostUsd se nezmění (multiply by 1.0).
  8. Maximální rozsah období. Shodně s existujícím endpointem /breakdown — max 366 dní. BE vrátí 400, pokud to - from > 366 dní.
  9. ai.totalChargedCostUsd a ai.totalEventCount = součet VŠECH projektů, ne jen top-N. Tyto agregáty se počítají samostatným SQL dotazem (nebo subquery) přes celý dataset uživatele za období — bez LIMIT. topProjects je jen seřazená podmnožina top-N contributors. Garantuje, že FE vždy vidí skutečnou celkovou útratu bez ohledu na hodnotu aiLimit.

Test Cases

Backend

IDGIVENWHENTHEN
TC-16-BE-1User má 5 AI usage events pro 3 projekty (project 1: $0.84, project 2: $0.21, project 3: $0.05) ve zvoleném obdobíGET /project-breakdown?from=2026-05-01&to=2026-05-31&aiLimit=3200 OK; ai.topProjects má 3 záznamy seřazeny DESC dle totalChargedCostUsd; ai.totalChargedCostUsd = 1.10
TC-16-BE-2User má 0 AI usage events v daném obdobíGET /project-breakdown?from=2026-05-01&to=2026-05-31200 OK; ai.topProjects = [], ai.totalChargedCostUsd = 0, ai.totalEventCount = 0
TC-16-BE-3User má AI usage event s project_id = 999 (projekt neexistuje v projects tabulce — smazán)GET /project-breakdown?from=...200 OK; odpovídající item má projectName = null, totalChargedCostUsd správně agregováno
TC-16-BE-4User má 2 hosting cost events pro namespace popelkam-talkide (cost $0.30 + $0.12) a markup je 20 %GET /project-breakdown?from=...200 OK; hosting.namespaces[0].totalCostUsd = 0.504 (0.42 × 1.20)
TC-16-BE-5User má 0 hosting cost events v daném obdobíGET /project-breakdown?from=...200 OK; hosting.namespaces = [], hosting.totalCostUsd = 0
TC-16-BE-6aiLimit = 2, user má 4 AI projektyGET /project-breakdown?from=...&aiLimit=2200 OK; ai.topProjects má právě 2 záznamy (top-2 dle charged cost)
TC-16-BE-7aiLimit = 11 (over max)GET /project-breakdown?from=...&aiLimit=11400 VALIDATION
TC-16-BE-8aiLimit = 0 (under min)GET /project-breakdown?from=...&aiLimit=0400 VALIDATION
TC-16-BE-9from = 2026-05-31, to = 2026-05-01 (from > to)GET /project-breakdown?...400 VALIDATION
TC-16-BE-10Chybí parametr fromGET /project-breakdown?to=2026-05-31400 VALIDATION
TC-16-BE-11Neplatný JWTGET /project-breakdown?from=...&to=...401 AUTHENTICATION_FAILED
TC-16-BE-12User A nemá přístup k datům User BGET s JWT user A, user B má usage data200 OK; response obsahuje pouze data user A (WHERE user_id = :userId)
TC-16-BE-13Hosting cost events pro namespace orphan-ns s environment_id = nullGET /project-breakdown?from=...200 OK; namespace item má environmentId = null; totalCostUsd správně agregováno
TC-16-BE-14Markup = 0 (žádná marže)GET /project-breakdown?from=...200 OK; hosting totalCostUsd = raw SUM(cost_usd) (no-op)
TC-16-BE-15from = 2025-01-01, to = 2026-05-01 (rozsah 485 dní — přesahuje 366 dní)GET /project-breakdown?from=2025-01-01&to=2026-05-01400 VALIDATION (business pravidlo 8)
TC-16-BE-16to = today+2 (např. to = 2026-05-23 při dnešním datu 2026-05-21)GET /project-breakdown?from=2026-05-01&to=2026-05-23400 VALIDATION (to nesmí přesahovat today+1)

Frontend

IDGIVENWHENTHEN
TC-16-FE-1BE vrátí 2 AI projekty (project 1: $0.84, project 2: $0.21)AiCreditPanel se vyrendrujeZobrazeny 2 řádky v “Top projects” sekci; todo-list $0.84, druhý (deleted project) $0.21 pokud je projectName = null
TC-16-FE-2BE vrátí prázdné ai.topProjects = []AiCreditPanel se vyrendrujeZobrazí se empty state text topProjectsEmpty
TC-16-FE-3Fetch selže (BE vrátí 500)AiCreditPanel po chyběZobrazí se error state text topProjectsError; zbytek panelu (balance, progress bar) funguje normálně
TC-16-FE-4BE vrátí 2 hosting namespace záznamyHostingBillingPanel se vyrendrujeMockup USAGE konstanta není viditelná; zobrazeny reálné namespace řádky s totalCostUsd
TC-16-FE-5hostingSpentPercent >= 90HostingBillingPanel se vyrendrujeaccentColor = červená (var(--rose)); progress bar a velké číslo jsou červené
TC-16-FE-6User klikne “Last month”BillingSection period switcherNový fetch s from/to pro minulý měsíc; oba panely se aktualizují
TC-16-FE-7User klikne “This month” po “Last month”Period switcherFetch s aktuálním měsícem; data se obnoví
TC-16-FE-8Fetching (loading) stavPanel při prvním načteníZobrazí se loading skeleton pro “Top projects” sekci; neukazuje prázdný stav předčasně

UX guidelines

  • Top projekty v AI panelu: seznam max 3 řádků pod progress barem a BudgetWarningBanner. Každý řádek: projectName (nebo fallback (deleted project)) vlevo, $X.XX vpravo (font-mono). Stejná vizuální tíha jako stávající billing řádky.
  • Hosting per-namespace: nahrazuje celý estimated blok v BillingSection (project hours bars, storage bars, bandwidth bars). Nový blok: nadpis Usage by namespace, seznam namespace řádků (namespace vlevo, $X.XX vpravo). Velké číslo $USAGE.estimated se nahradí součtem hosting.totalCostUsd z real dat.
  • Period switcher: diskrétní přepínač (pill tabs) This month | Last month umístěný nad oběma credit panely nebo ve hlavičce hosting boxu. Nezabírá zbytečný prostor — může být malý, font-mono, muted barva.
  • Červené skóre < 10 %: jak AI, tak hosting panel — velké číslo (remaining balance) a progress bar zčervenají. Threshold = 10 % zbývajícího kreditu z počátečního.
  • Loading state: skeleton animace pro seznam projektů / namespaců (2-3 řádky obdélníků), nikoliv spinner. Konzistentní se stávajícím skeletonem v AiCreditPanel (řádky 54–58).


FEEDBACK

Chybělo mi přesné schema projects tabulky (konkrétně sloupce name vs slug a nullable stav) — rozhodl jsem se na základě kontextu z jiných UC souborů, kde se název projektu konzistentně mapuje na project.name. Dále by se hodilo vědět, zda BillingSection.vue má přijímat breakdown data jako prop od rodiče nebo si je fetching composable spravuje interně — zvolil jsem prop přístup jako konzervativnější (rodiče orchestruje fetching), ale toto je design-level rozhodnutí, které by měl potvrdit FE developer. Nakonec chyběl přesný tvar existujícího UsageBreakdownResponse DTO pro ověření konzistence pojmenování s novou ProjectBreakdownResponse.

Opravy z implementačního review (2026-05-21):

  • Hosting filtrace opravena z recorded_at na window_start (sémanticky správný sloupec — kdy náklad nastal, ne kdy byl zapsán do DB; konzistentní s existujícím /usage/breakdown).
  • ai.totalChargedCostUsd a ai.totalEventCount upřesněny jako součet VŠECH projektů uživatele za období (bez LIMIT), nikoliv součet top-N zobrazených v topProjects.
Was this page helpful?

Thanks for the feedback.