Internal Documentation internal
TalkIDE internal documentation

Admin dashboard poskytující real-time přehled kapacity platformy a manuální emergency brake pro okamžité zastavení všech Mara API volání — přístupné výhradně uživatelům s rolí ADMIN.

  • Funkce vychází z ADR-020 Rozhodnutí 6 — “capacity recommender dashboard” pro founder rozhodování o invite grantech.
  • Kill-switch je MANUÁLNÍ override, nezávislý na automatickém EmergencyBrakeProperties (monthlyCentsCap/softThreshold/freezeThreshold), který zůstává aktivní souběžně.
  • Při paused=true gateway odmítne každé Mara volání s HTTP 503 PLATFORM_PAUSED — přeruší všechny konverzace, sidecar i CLI executor.
  • Kill-switch stav je uložen v DB tabulce platform_kill_switch (audit trail: kdo, kdy, proč) a cachován v Redis klíči platform:kill_switch:paused (TTL 10 s) pro low-latency gateway reads (~1 ms).
  • Admin identity se ověřuje stejným vzorem jako UC-08003: userRepository.findById(currentUserId)if (user.role != "ADMIN") throw ForbiddenException.
  • Capacity dashboard agreguje data ze tří zdrojů: users tabulka, api_usage_ledger tabulka, Redis per-user fatigue snapshots.
  • Všechna pole v API requestech/responsech jsou snake_case (konvence dle UC-08002/UC-08003).
  • Detailní DB schéma kill-switch tabulky viz sekce DB Schema níže.

Flow 1 — Admin čte stav kill-switch

sequenceDiagram
    actor Admin

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

    FE->>+BE: GET /api/v1/admin/platform/kill-switch <br> Authorization: Bearer {accessToken}

    BE->>BE: ověř roli ADMIN
    alt uživatel není ADMIN
        BE-->>FE: 403 Forbidden <br> ErrorResponse
    end

    BE->>Redis: GET platform:kill_switch:paused
    alt cache miss
        BE->>DB: SELECT * FROM platform_kill_switch LIMIT 1
        BE->>Redis: SET platform:kill_switch:paused TTL 10s
    end

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

    FE->>-Admin: zobraz aktuální stav kill-switch (toggle + audit info)

GET /api/v1/admin/platform/kill-switch — bez request body

200 OK KillSwitchResponse:

{
  "paused": false,
  "paused_at": null,
  "paused_by_user_id": null,
  "paused_by_email": null,
  "reason": null
}

200 OK KillSwitchResponse (paused stav):

{
  "paused": true,
  "paused_at": "2026-05-09T14:32:00Z",
  "paused_by_user_id": 1,
  "paused_by_email": "mirek@mddsummer.com",
  "reason": "Anthropic incident — spiking latency, pausing preventively"
}

403 Forbidden ErrorResponse:

{
  "code": "FORBIDDEN",
  "message": "Access denied"
}

401 Unauthorized ErrorResponse:

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

Flow 2 — Admin aktivuje emergency brake (pause)

sequenceDiagram
    actor Admin

    Admin->>+FE: klikne "Emergency Brake" → otevře confirm modal

    FE->>FE: validate reason field (min 10 znaků)
    alt reason je příliš krátký
        FE-->>Admin: show validation error
    end

    Admin->>+FE: vyplní reason, potvrdí

    FE->>+BE: POST /api/v1/admin/platform/kill-switch/pause <br> Authorization: Bearer {accessToken} <br> PauseRequest

    BE->>BE: ověř roli ADMIN
    alt uživatel není ADMIN
        BE-->>FE: 403 Forbidden <br> ErrorResponse
    end

    BE->>BE: validate request (reason not blank, min 10 znaků)
    alt request je invalid
        BE-->>FE: 400 Bad Request <br> ErrorResponse
    end

    BE->>DB: UPDATE platform_kill_switch SET paused=true, paused_at=NOW(), paused_by_user_id=?, reason=?, updated_at=NOW()

    BE->>Redis: SET platform:kill_switch:paused true (TTL 10s)

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

    FE->>-Admin: zobraz paused banner, toggle přepnut na PAUSED

POST /api/v1/admin/platform/kill-switch/pause PauseRequest:

{
  "reason": "Anthropic incident — spiking latency, pausing preventively"
}

200 OK KillSwitchResponse:

{
  "paused": true,
  "paused_at": "2026-05-09T14:32:00Z",
  "paused_by_user_id": 1,
  "paused_by_email": "mirek@mddsummer.com",
  "reason": "Anthropic incident — spiking latency, pausing preventively"
}

400 Bad Request (validation) ErrorResponse:

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

403 Forbidden ErrorResponse:

{
  "code": "FORBIDDEN",
  "message": "Access denied"
}

Flow 3 — Admin deaktivuje emergency brake (resume)

sequenceDiagram
    actor Admin

    Admin->>+FE: klikne "Resume" → potvrdí bez modalu (nebo s jednoduchým confirm)

    FE->>+BE: POST /api/v1/admin/platform/kill-switch/resume <br> Authorization: Bearer {accessToken}

    BE->>BE: ověř roli ADMIN
    alt uživatel není ADMIN
        BE-->>FE: 403 Forbidden <br> ErrorResponse
    end

    BE->>DB: UPDATE platform_kill_switch SET paused=false, paused_at=NULL, paused_by_user_id=NULL, reason=NULL, updated_at=NOW()

    BE->>Redis: SET platform:kill_switch:paused false (TTL 10s)

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

    FE->>-Admin: skryj paused banner, toggle přepnut na ACTIVE

POST /api/v1/admin/platform/kill-switch/resume — bez request body

200 OK KillSwitchResponse:

{
  "paused": false,
  "paused_at": null,
  "paused_by_user_id": null,
  "paused_by_email": null,
  "reason": null
}

403 Forbidden ErrorResponse:

{
  "code": "FORBIDDEN",
  "message": "Access denied"
}

Flow 4 — Gateway pre-check (kill-switch integration)

sequenceDiagram
    participant Caller as ConversationUseCase / SidecarExecutor
    participant GW as AnthropicGatewayService
    participant Redis
    participant DB
    participant Anthropic

    Caller->>+GW: callMara(request)

    GW->>+Redis: GET platform:kill_switch:paused
    alt cache miss
        Redis-->>GW: (nil)
        GW->>+DB: SELECT paused FROM platform_kill_switch LIMIT 1
        DB-->>-GW: paused=true/false
        GW->>Redis: SET platform:kill_switch:paused {value} TTL 10s
    else cache hit
        Redis-->>-GW: "true" nebo "false"
    end

    alt paused = true
        GW-->>Caller: throw PlatformPausedException
        note over Caller: HTTP 503, error_code: PLATFORM_PAUSED
    end

    GW->>GW: check fatigue (UC-08002)
    GW->>+Anthropic: API call
    Anthropic-->>-GW: response
    GW->>DB: INSERT api_usage_ledger
    GW-->>-Caller: MaraResponse

503 Service Unavailable (platform paused) ErrorResponse:

{
  "code": "PLATFORM_PAUSED",
  "message": "Mara API calls are temporarily paused by the platform administrator"
}

Flow 5 — Admin čte capacity dashboard

sequenceDiagram
    actor Admin

    Admin->>+FE: otevře /admin/capacity (nebo refresh)

    FE->>+BE: GET /api/v1/admin/platform/capacity <br> Authorization: Bearer {accessToken}

    BE->>BE: ověř roli ADMIN
    alt uživatel není ADMIN
        BE-->>FE: 403 Forbidden <br> ErrorResponse
    end

    BE->>DB: SELECT count(*), role, invite_generation FROM users GROUP BY role, invite_generation
    BE->>DB: SELECT SUM(cost_cents), executor_type, date_trunc(...) FROM api_usage_ledger WHERE ...
    BE->>DB: COUNT(*) FROM api_usage_ledger WHERE request_completed_at IS NULL
    BE->>DB: COUNT(*) FROM api_usage_ledger WHERE error_message IS NOT NULL AND request_started_at > NOW()-24h
    BE->>Redis: GET global:monthly_used_cents
    BE->>Redis: pipeline — GET user:{id}:fatigue_state pro všechny user IDs

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

    FE->>-Admin: zobraz dashboard (cards + charts)

GET /api/v1/admin/platform/capacity — bez request body

200 OK CapacityResponse:

{
  "users": {
    "total": 42,
    "admin_count": 2,
    "user_count": 40,
    "by_generation": {
      "0": 2,
      "1": 15,
      "2": 25
    }
  },
  "fatigue_distribution": {
    "FRESH": 28,
    "WARMED_UP": 6,
    "TIRED": 4,
    "GROGGY": 2,
    "EXHAUSTED": 1,
    "ASLEEP": 1
  },
  "spend": {
    "today_cents": 1540,
    "yesterday_cents": 2310,
    "this_month_cents": 38700,
    "global_monthly_cap_cents": 500000,
    "monthly_burn_pct": 0.0774
  },
  "active_now": {
    "in_flight_calls": 3,
    "last_5min_unique_users": 5,
    "last_1h_unique_users": 12
  },
  "errors_last_24h": 7,
  "executor_breakdown_today": {
    "CLI": 48,
    "SIDECAR": 112
  }
}

403 Forbidden ErrorResponse:

{
  "code": "FORBIDDEN",
  "message": "Access denied"
}

401 Unauthorized ErrorResponse:

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

DB Schema

Tabulka platform_kill_switch

Single-row tabulka (seeded při prvním startu BE). Drží aktuální stav i audit trail posledního toggl.

CREATE TABLE platform_kill_switch (
    id            BIGSERIAL PRIMARY KEY,
    paused        BOOLEAN NOT NULL DEFAULT FALSE,
    paused_at     TIMESTAMPTZ,
    paused_by_user_id BIGINT REFERENCES users(id),
    reason        TEXT,
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Seed: vždy právě 1 řádek
INSERT INTO platform_kill_switch (paused) VALUES (FALSE);

Liquibase changeset hint:

<changeSet id="XXXX-create-platform-kill-switch" author="dev">
    <createTable tableName="platform_kill_switch">
        <column name="id" type="BIGSERIAL"><constraints primaryKey="true"/></column>
        <column name="paused" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
        <column name="paused_at" type="TIMESTAMPTZ"/>
        <column name="paused_by_user_id" type="BIGINT">
            <constraints foreignKeyName="fk_kill_switch_user" references="users(id)"/>
        </column>
        <column name="reason" type="TEXT"/>
        <column name="updated_at" type="TIMESTAMPTZ" defaultValueComputed="NOW()"><constraints nullable="false"/></column>
    </createTable>
    <insert tableName="platform_kill_switch">
        <column name="paused" valueBoolean="false"/>
    </insert>
</changeSet>

Redis Keys

KlíčTypTTLHodnotaPopis
platform:kill_switch:pausedString10 s"true" / "false"Hot-path cache pro gateway pre-check. Invalidován při každém toggle (pause/resume).
global:monthly_used_centsStringinteger (cents)Globální měsíční spend counter. Spravován UC-08002 automatickým brake systémem. Reset job (ShedLock) dle UC-08002 spec.
user:{id}:fatigue_stateStringTTL dle UC-08002"FRESH" / "WARMED_UP" / "TIRED" / "GROGGY" / "EXHAUSTED" / "ASLEEP"Per-user fatigue stav. Dashboard čte pipeline přes všechny user IDs. Hodnoty konzistentní s UC-08002 fatigue state machine.

Frontend

Validations

FieldConstraintsSizePatternNote
reason (pause modal)not_blank10 – 1000Povinné při pause; zobrazuj live counter “X/10 min. znaků”

UX Guidelines

Routing

Kapacitní konzole dostane vlastní route /admin/capacity s meta: { requiresAuth: true, requiresAdmin: true }.

Důvody:

  • /profile by se stal přetíženým — admin konzole je operační nástroj, ne nastavení profilu.
  • Vlastní URL umožňuje bookmarkování, přímé sdílení linku mezi adminy a čisté beforeEach guard pro admin check.
  • Vzor silent probe z UC-08003 (InvitesSection) zůstane zachován jako fallback: při prvním mountu CapacityDashboardScreen zavolá GET probe a 403 → redirect na /profile s error toastem.

Nav link “Admin — Capacity” se zobrazí v profilu/menu jen pokud store.isAdmin === true (stejný pattern jako admin sekce v InvitesSection).

User Flow

Path 1 — Admin dashboard view
  1. Admin se přihlásí (standardní flow UC-01002).
  2. V profilovém menu / navigation vidí odkaz “Capacity Console” (skrytý pro non-admin roli — v-if="authStore.isAdmin").
  3. Naviguje na /admin/capacity.
  4. Router guard requiresAdmin provede silent probe GET /api/v1/admin/platform/kill-switch:
    • 200 OK → pokračuje.
    • 403 Forbidden → redirect na /profile + toast “Access denied” (viz Path 4).
  5. CapacityDashboardScreen se mountuje, zobrazí skeleton loader ve všech metric kartách.
  6. Paralelně se volají dva endpointy:
    • GET /api/v1/admin/platform/kill-switch → stav banneru.
    • GET /api/v1/admin/platform/capacity → metric data.
  7. Po načtení: skeleton → živá data. Začne polling každých 30 sekund (interval začíná po prvním úspěšném loadu; clearInterval při unmount).
  8. Dashboard je read-only v tomto stavu — admin sleduje metriky.
Path 2 — Emergency brake: pauza platformy
  1. Admin vidí KillSwitchBanner ve stavu NORMAL (zelený).
  2. Klikne tlačítko “Pause Platform” (destructive outline button, ikona AlertTriangle).
  3. Otevře se KillSwitchPauseModal s focus trapem:
    • Nadpis: “Pause Mara for all users?”
    • Popis: “This will immediately block all Mara API calls platform-wide. Active in-flight calls will finish; new calls will receive 503.”
    • Textarea “Reason” (required, min 10 znaků) s real-time char counter “X / 500”.
    • Primární akce: “Confirm Pause” (červená, disabled dokud reason.length >= 10).
    • Sekundární akce: “Cancel” (zavře modal bez akce).
  4. Admin vyplní reason, klikne “Confirm Pause”.
  5. Button přejde do loading stavu (spinner + “Pausing…”), textarea a Cancel disabled.
  6. POST /api/v1/admin/platform/kill-switch/pause s { reason }.
  7. Po 200 OK:
    • Modal se zavře.
    • KillSwitchBanner se přepne na stav PAUSED (červený) s paused_by_email, paused_at (relativní čas), reason.
    • Toast “Platform paused.” (amber/warning, 5 s).
    • Polling se okamžitě spustí znovu (refresh bez čekání na 30s interval).
  8. Při chybě API:
    • Modal zůstane otevřený.
    • Pod tlačítkem se zobrazí inline error “Failed to pause — try again.” (červený text).
    • Tlačítko se vrátí do aktivního stavu.
Path 3 — Resume platformy
  1. Admin vidí KillSwitchBanner ve stavu PAUSED (červený banner s důvodem a časem pauzy).
  2. Klikne “Resume Platform” (zelené tlačítko, ikona Play).
  3. Jednoduchý inline confirm — žádný modal, místo toho inline confirm row přímo v banneru:
    • Text: “Resume and allow all Mara calls?”
    • Tlačítka: “Yes, Resume” (zelená) + “Cancel”.
    • Tento pattern (méně friction než modal) je záměrný: resume je méně rizikový než pauza.
  4. Admin klikne “Yes, Resume”.
  5. Button → loading stav.
  6. POST /api/v1/admin/platform/kill-switch/resume.
  7. Po 200 OK:
    • Banner se přepne na NORMAL (zelený).
    • Toast “Platform resumed.” (green, 5 s).
  8. Při chybě: inline error v banneru, tlačítka zpět aktivní.
Path 4 — Non-admin přístup
  1. User (role USER) naviguje manuálně na /admin/capacity.
  2. Router guard provede silent probe → 403 Forbidden.
  3. Router přesměruje na /profile.
  4. Toast “Access denied” (rose/error, 5 s) se zobrazí po přesměrování.
  5. Nav link “Capacity Console” se vůbec nezobrazí (podmíněný render v-if="authStore.isAdmin").
Path 5 — Běžný user při pauzované platformě (reaktivně přes 503)
  1. User odesílá zprávu Maře (např. POST /api/v1/conversations/{id}/messages).
  2. BE vrátí 503 SERVICE_UNAVAILABLE s { "code": "PLATFORM_PAUSED", "message": "..." }.
  3. Globální Axios response interceptor v httpClient.ts zachytí HTTP 503 + error.code === "PLATFORM_PAUSED" a:
    • Nastaví platformStatusStore.paused = true.
    • Spustí Mara503Toast (sticky toast / inline banner).
  4. Mara503Toast zobrazí text “Platform is temporarily paused. Please try again later.” (CS: “Platforma je dočasně pozastavena. Zkus to prosím za chvíli.”).
  5. Chat input se přepne na disabled s placeholder “Platform paused — try again later.” (řízeno z platformStatusStore.paused).
  6. Žádný proactive polling — user nezná stav platformy při loadu page; zjistí ho až při prvním pokusu o Mara request.
  7. Při dalším úspěšném Mara requestu (HTTP 200) interceptor nastaví platformStatusStore.paused = false a toast zmizí, chat input se odemkne.
  8. User může zkusit znovu odeslat zprávu (manuálně) — interceptor opět vyhodnotí response.

Layout

Screen type: Dashboard (read-only s jednou operační akcí) Route: /admin/capacity Responsive: Desktop-first. Na mobilních zařízeních je dashboard read-only — kill switch akce (Pause modal) jsou dostupné, ale layout přejde na single-column. Důvod: admin operace na platformě by se neměly provádět z mobilního telefonu, ale v nouzi být schopen zkontrolovat stav a zavolat Resume je kriticky důležité. Container: Full-width s max-w-6xl mx-auto, padding px-6 py-8.

Text-based mockup — NORMAL state
┌─ /admin/capacity ─────────────────────────────────────────────────────┐
│                                                                        │
│  Admin — Capacity Console                    [auto-refresh: 28s ●]    │
│                                                                        │
│  ┌─ STATUS ─────────────────────────────────────────────────────────┐  │
│  │  ● PLATFORM NORMAL   All Mara calls operating.   [Pause Platform]│  │
│  └──────────────────────────────────────────────────────────────────┘  │
│                                                                        │
│  ┌─ Users ──────────┐  ┌─ Fatigue Distribution ───────────────────┐   │
│  │  Total      42   │  │  FRESH       ████████████░░░  28 (67 %) │   │
│  │  Admins      2   │  │  WARMED_UP   ███░░░░░░░░░░░░   6 (14 %) │   │
│  │  Users      40   │  │  TIRED       ██░░░░░░░░░░░░░   4  (9 %) │   │
│  │  Gen 0/1/2+ 2/15/23│  │  GROGGY      █░░░░░░░░░░░░░░   2  (5 %) │   │
│  │                  │  │  EXHAUSTED   ░░░░░░░░░░░░░░░   1  (2 %) │   │
│  │                  │  │  ASLEEP      ░░░░░░░░░░░░░░░   1  (2 %) │   │
│  └──────────────────┘  └──────────────────────────────────────────┘   │
│                                                                        │
│  ┌─ Today Spend ────┐  ┌─ Active Now ──────────────────────────────┐   │
│  │  Today   $12.40  │  │  In-flight        3                       │   │
│  │  Yest    $18.20  │  │  Last 5 min       8                       │   │
│  │  Month budget:   │  │  Last 1 h        15                       │   │
│  │  $312 / $1 000   │  └──────────────────────────────────────────┘   │
│  │  ████████░░  31% │                                                  │
│  └──────────────────┘                                                  │
│                                                                        │
│  ┌─ Errors 24h ─────┐  ┌─ Executor Today ─────────────────────────┐   │
│  │  API errors    2 │  │  CLI       87                             │   │
│  │  Timeouts      0 │  │  SIDECAR   43                             │   │
│  └──────────────────┘  └──────────────────────────────────────────┘   │
│                                                                        │
│  Last updated: just now                                                │
└────────────────────────────────────────────────────────────────────────┘
Text-based mockup — PAUSED state (KillSwitchBanner)
┌─ STATUS ──────────────────────────────────────────────────────────────┐
│  ▲ PLATFORM PAUSED                                                    │
│    Paused by: mirek@talkide.app   ·   3 minutes ago                  │
│    Reason: "Investigating Anthropic API outage"                       │
│                                                                       │
│    Are you sure you want to resume?  [Yes, Resume]  [Cancel]          │
│    (inline confirm row — shown only after "Resume Platform" click)    │
│                                                                [Resume Platform] │
└───────────────────────────────────────────────────────────────────────┘
Pause Modal
┌─ Pause Platform ──────────────────────────────────────┐
│                                                        │
│  ▲  This will pause ALL Mara API calls for all users.  │
│     Active calls will finish; new calls → 503.         │
│                                                        │
│  Reason *                                              │
│  ┌──────────────────────────────────────────────────┐  │
│  │                                                  │  │
│  │                                                  │  │
│  └──────────────────────────────────────────────────┘  │
│  Min 10 characters.                        0 / 500     │
│                                                        │
│  [Cancel]                        [Confirm Pause ▲]     │
│                                                        │
└────────────────────────────────────────────────────────┘
Mara503Toast (běžný user, reaktivně po 503)
┌─ Mara503Toast (sticky top, řízeno reactive z httpClient interceptor) ─┐
│  ▲  Platform is temporarily paused. Please try again later.            │
└────────────────────────────────────────────────────────────────────────┘

Components

KomponentaSouborPopis
CapacityDashboardScreensrc/screens/admin/CapacityDashboardScreen.vueParent route /admin/capacity. Orchestruje data fetching, polling, předává props do child komponent.
KillSwitchBannersrc/screens/admin/components/KillSwitchBanner.vueStavový banner NORMAL / PAUSED. V NORMAL zobrazí zelený strip + “Pause Platform” button. V PAUSED zobrazí červený strip s audit info + inline confirm pro Resume. Emituje @pause-click a @resume-confirm.
KillSwitchPauseModalsrc/screens/admin/components/KillSwitchPauseModal.vueModal s reason textarea. Focus trap. Disabled stav tlačítka dokud reason.length >= 10. Loading state při submitu. Emituje @confirm(reason) a @cancel.
MetricCardsrc/screens/admin/components/MetricCard.vueGenerická karta s title prop a default slotem pro obsah. Slot umožňuje libovolný vnitřní layout. Skeleton loading prop. Sdílené bg-[var(--bg-2)] rounded-[var(--r-lg)] p-5 border border-[var(--line-2)] styly.
FatigueDistributionBarsrc/screens/admin/components/FatigueDistributionBar.vuePřijímá { FRESH, WARMED_UP, TIRED, GROGGY, EXHAUSTED, ASLEEP } counts. Renduje 6 horizontálních mini-barů s popiskem a číslem. Barvy viz tabulka níže.
SpendProgressBarsrc/screens/admin/components/SpendProgressBar.vueProgress bar s procentem. Barva: zelená < 85 %, amber 85–94 %, rose >= 95 %. Přijímá spent, cap (v centech) a label.
Mara503Toastsrc/common/components/Mara503Toast.vueReaktivní toast/banner pro běžné usery, řízený výhradně z global Axios response interceptor (žádný polling). Zobrazí se když interceptor zachytí HTTP 503 + code: PLATFORM_PAUSED. Skryje se při dalším úspěšném Mara requestu (HTTP 200). aria-live="assertive".
useCapacityStoresrc/screens/admin/stores/capacity.tsPinia store (feature-based scope — admin doména, ne shared common). State: killSwitchStatus, capacity, loading, error, lastUpdatedAt, isAdmin. Actions: probeAdmin(), fetchKillSwitchStatus(), fetchCapacity(), loadAll(), pausePlatform(reason), resumePlatform(), startPolling(), stopPolling().
usePlatformStatusStoresrc/common/stores/platformStatus.tsLightweight reactive store pro běžné usery. State: paused: boolean (default false). Setter volaný pouze z Axios interceptoru — setPausedFrom503() a clearPausedOnSuccess(). Žádný polling, žádný API call.

Validation Feedback

Reason textarea (KillSwitchPauseModal)
StavVizuální feedback
Prázdné pole (initial)Neutrální border var(--line-2), char counter “0 / 500” šedý. “Confirm Pause” disabled (opacity-50, cursor-not-allowed).
Psaní, < 10 znakůBorder zůstane neutrální. Char counter červený (var(--rose)). “Confirm Pause” stále disabled.
Psaní, >= 10 znakůBorder zezelená (var(--green-line) = oklch(0.78 0.13 150 / 0.4)). Char counter šedý. “Confirm Pause” se aktivuje.
Přes 450 znakůChar counter amber (“450 / 500”).
Přes 500 znakůNepůjde — textarea má maxlength="500".
Submit errorPod tlačítkem se zobrazí role="alert" inline error text v rose barvě.
Obecná pravidla dashboardu
  • Polling interval countdown (číslo v headeru “auto-refresh: Xs”) vizuálně odpočítává — jemný ring progress nebo jen číslo. Nesmí být rušivý.
  • Při load error metriky: MetricCard zobrazí ”— Unable to load” místo dat + retry tlačítko.
  • Stale data (polling selhal > 3× po sobě): badge “Data may be stale” v šedé barvě vedle timestamp.

Accessibility

  • aria-live="assertive" na KillSwitchBanner při přechodu NORMAL → PAUSED i PAUSED → NORMAL — screen reader okamžitě oznámí změnu stavu platformy.
  • aria-live="assertive" na Mara503Toast — kritická informace pro usery při 503.
  • aria-label na “Pause Platform” button: aria-label="Pause all Mara API calls platform-wide".
  • aria-label na “Resume Platform” button: aria-label="Resume Mara API calls".
  • Focus trap v KillSwitchPauseModal — Tab/Shift+Tab cykluje pouze mezi textarea a dvěma tlačítky. Escape zavře modal (= Cancel).
  • Focus management: po otevření modalu focus přesune na textarea. Po zavření modalu focus vrátit na “Pause Platform” button.
  • role="dialog" + aria-modal="true" + aria-labelledby na modal element.
  • Keyboard shortcut pro emergency brake: záměrně nevyužívat (Cmd+Shift+P nebo podobné). Důvod: emergency brake je destruktivní akce, nechceme accidental trigger. Uživatel musí vědomě kliknout a projít confirm flow.
  • Barevné cues nikdy nejsou jedinou informací — NORMAL/PAUSED stav je vždy i textově oznámen.
  • Metric cards: každý MetricCardrole="region" s aria-label odpovídajícím title prop.
  • Tab order: Banner → Metric cards (top-left → right, bottom-left → right) → Last updated timestamp.
  • WCAG AA kontrast: rose/green/amber accenty na var(--bg-2) background jsou v dark i light theme ≥ 4.5:1.

Loading States

  • Initial load: všechny MetricCard zobrazí skeleton (pulse animated div s bg-[var(--bg-3)] animate-pulse rounded). KillSwitchBanner zobrazí skeleton strip.
  • Polling refresh: data se aktualizují in-place bez skeleton — živé číslo tiše přeskočí na novou hodnotu. Flashování zamezit transition-all duration-300.
  • Pause/Resume action: tlačítko → spinner + disabled stav. Ostatní UI není blokováno.
  • Modal submit: “Confirm Pause” button → spinner + “Pausing…” text, textarea + Cancel disabled.

Error States

ChybaZobrazení
Initial load selhání (network error)Celý dashboard: role="alert" error banner “Failed to load capacity data.” + “Retry” button. MetricCards zůstanou prázdné (no skeleton po erroru).
Polling selhání (background)Tichá degradace — timestamp “Last updated: X min ago” zšedne. Po 3 selhání přibude badge “Data may be stale”. Žádný disruptivní error overlay.
Pause API errorInline v modalu pod tlačítky: “Failed to pause platform. Please try again.” (rose text, role="alert"). Modal zůstane otevřený.
Resume API errorInline v banneru pod inline-confirm row.
403 při vstupu na /admin/capacityRouter redirect na /profile + toast “Access denied” (rose, 5 s).
503 PLATFORM_PAUSED u běžného usera (Mara endpoint)Axios interceptor → Mara503Toast se zobrazí, chat input disabled. Žádný polling, zmizí až po dalším úspěšném 200.

State Variants — KillSwitchBanner

StavPozadíBorderTextIkonaCTA
LOADINGvar(--bg-3) skeleton-skeleton-skeleton
NORMALvar(--green-soft)oklch(0.78 0.13 150 / 0.4)”PLATFORM NORMAL — All Mara calls operating.”CheckCircle (green)“Pause Platform” (destructive outline, rose border)
PAUSEDvar(--rose-soft)var(--rose-line)”PLATFORM PAUSED” + audit infoAlertTriangle (rose)“Resume Platform” (green solid)
ERRORvar(--bg-3)var(--line-2)”Status unavailable”AlertCircle (amber)“Retry”

Fatigue Distribution — barvy

Konzistentní s UC-08002 visual states (barvy odvozeny od design tokenů):

StateBar colorCSS var
FRESHZelenávar(--green)
WARMED_UPAmbervar(--amber)
TIREDAmber desaturovaný (~60 %)oklch(0.72 0.08 70)
GROGGYRose desaturovanýoklch(0.68 0.10 25)
EXHAUSTEDRose plnývar(--rose)
ASLEEPFg-4 (šedá)var(--fg-4)

(ASLEEP = nejméně výrazná barva — aktivních userů ASLEEP má být co nejméně a nechceme alarmovat přemírou barvy.)

SpendProgressBar — color thresholds

RozsahBarvaDůvod
0–84 %var(--green)V pohodě
85–94 %var(--amber)Warning — global brake +1 stupeň se blíží
>= 95 %var(--rose)Kritické — global brake freeze nových konverzací

Refresh Strategy (admin dashboard only)

Doporučení: polling 30 sekund pro MVP (ne SSE). Polling se vztahuje pouze na admin dashboard — běžní uživatelé NEPOLLUJÍ stav platformy (viz Path 5).

Důvody:

  • Admin dashboard je low-urgency monitoring — 30 s latence je přijatelná.
  • SSE by vyžadoval server-side SseEmitter bean na BE pro admin endpoint + connection management → větší BE effort.
  • Polling je jednodušší na implementaci, testování i debugování.
  • 30 s je rozumný kompromis: dost čerstvé pro monitoring, nepřetěžuje BE (2 requesty/min × admin počet ≈ nulová zátěž).
  • Při destructive akci (Pause/Resume) polling spustit okamžitě (bez čekání na interval) pro okamžité potvrzení.

Implementace: setInterval v onMounted s clearInterval v onUnmounted. Interval se restartuje po každé akci (Pause/Resume) pro čerstvá data.

Mobile / Responsive

Desktop-first — admin konzole je operační nástroj pro desktopový prohlížeč.

BreakpointChování
>= lg (1024px)Plný 2-sloupcový grid pro MetricCards (2×3 nebo 2×2+1). KillSwitchBanner full-width.
md (768–1023px)2-sloupcový grid se stisnuto. Vše čitelné.
< md (< 768px)Single-column. Metric cards pod sebou. Kill switch akce jsou dostupné (admin může v nouzi pausovat z telefonu), ale modální dialog je adaptován pro mobile (full-screen drawer pattern).

Na mobilním zařízení KillSwitchPauseModal přechází na bottom sheet (position: fixed; bottom: 0; left: 0; right: 0; border-radius: var(--r-xl) var(--r-xl) 0 0) místo centrovaného overlaye.

i18n klíče (orientační)

admin.capacity.title
admin.capacity.lastUpdated
admin.capacity.autoRefresh

admin.killSwitch.statusNormal
admin.killSwitch.statusPaused
admin.killSwitch.pausedBy
admin.killSwitch.pausedAgo
admin.killSwitch.reason
admin.killSwitch.btnPause
admin.killSwitch.btnResume
admin.killSwitch.btnConfirmPause
admin.killSwitch.btnYesResume
admin.killSwitch.confirmResumeText

admin.killSwitch.modal.title
admin.killSwitch.modal.description
admin.killSwitch.modal.reasonLabel
admin.killSwitch.modal.reasonPlaceholder
admin.killSwitch.modal.reasonHint
admin.killSwitch.modal.errorSubmit

admin.metrics.users.title
admin.metrics.users.total
admin.metrics.users.admins
admin.metrics.users.users
admin.metrics.users.byGeneration

admin.metrics.fatigue.title
admin.metrics.spend.title
admin.metrics.spend.today
admin.metrics.spend.yesterday
admin.metrics.spend.monthProgress

admin.metrics.active.title
admin.metrics.active.inflight
admin.metrics.active.last5min
admin.metrics.active.last1h

admin.metrics.errors.title
admin.metrics.executor.title

platform.paused.userToast
platform.paused.chatPlaceholder

common.accessDenied
common.dataStale
common.retry
common.loadFailed

Backend

Validations

FieldConstraintsSizePatternNote
reason (PauseRequest)not_blank10 – 1000Pouze pro POST /pause; u POST /resume není request body

Test Cases

GIVENWHENTHEN
Uživatel s rolí ADMIN, kill-switch paused=falseGET /api/v1/admin/platform/kill-switch je zavolán200 OK, paused=false, všechna audit pole null
Uživatel s rolí ADMIN, kill-switch paused=true (audit data přítomna)GET /api/v1/admin/platform/kill-switch je zavolán200 OK, paused=true, paused_by_email a reason vyplněny
Uživatel s rolí USERGET /api/v1/admin/platform/kill-switch je zavolán403 Forbidden, code=FORBIDDEN
Unauthenticated requestGET /api/v1/admin/platform/kill-switch je zavolán401 Unauthorized, code=AUTHENTICATION_FAILED
ADMIN, kill-switch paused=false, platný reason (≥10 znaků)POST /api/v1/admin/platform/kill-switch/pause je zavolán200 OK, paused=true, paused_at a paused_by_user_id persisted do DB, Redis key aktualizován
ADMIN, kill-switch paused=false, reason je blankPOST /api/v1/admin/platform/kill-switch/pause je zavolán400 Bad Request, code=VALIDATION_ERROR
ADMIN, kill-switch paused=false, reason má 9 znaků (< 10 min)POST /api/v1/admin/platform/kill-switch/pause je zavolán400 Bad Request, code=VALIDATION_ERROR
ADMIN, kill-switch paused=true (již pauzováno — idempotent)POST /api/v1/admin/platform/kill-switch/pause je zavolán s novým reasonem200 OK, stav přepsán novým reasonem (last-write-wins, no-op conflict)
ADMIN, kill-switch paused=truePOST /api/v1/admin/platform/kill-switch/resume je zavolán200 OK, paused=false, audit pole null, Redis key aktualizován
ADMIN, kill-switch paused=false (již resumováno — idempotent)POST /api/v1/admin/platform/kill-switch/resume je zavolán200 OK, paused=false (no-op)
USER volá pausePOST /api/v1/admin/platform/kill-switch/pause je zavolán403 Forbidden
platform_kill_switch.paused=true v DBcallMara() je zavolán v AnthropicGatewayServicePlatformPausedException je thrown, HTTP 503 s error_code=PLATFORM_PAUSED
platform_kill_switch.paused=false v DBcallMara() je zavolán v AnthropicGatewayServicenormální flow pokračuje (fatigue check, API call)
ADMIN, přítomny test fixtures (3 users různých generací, 2 admins, fatigue stavy pokrývají všech 6 hodnot FRESH/WARMED_UP/TIRED/GROGGY/EXHAUSTED/ASLEEP, api_usage_ledger záznamy)GET /api/v1/admin/platform/capacity je zavolán200 OK, správně agregovaná data: users.total=5, fatigue_distribution obsahuje všech 6 klíčů s odpovídajícími counts dle Redis dat, spend odpovídá DB součtům
USER volá capacityGET /api/v1/admin/platform/capacity je zavolán403 Forbidden
Capacity dashboard response time s fixtures (≤100 záznamů)GET /api/v1/admin/platform/capacity je zavolánresponse vrácena do 500 ms
api_usage_ledger obsahuje záznamy s request_completed_at IS NULL (3 in-flight)GET /api/v1/admin/platform/capacity je zavolánactive_now.in_flight_calls=3
api_usage_ledger obsahuje záznamy s error_message IS NOT NULL v posledních 24 h (5 chyb)GET /api/v1/admin/platform/capacity je zavolánerrors_last_24h=5
Platform paused=true, běžný user posílá zprávu MařePOST /api/v1/conversations/{id}/messages (Mara endpoint) je zavolán503 Service Unavailable, response body { "code": "PLATFORM_PAUSED", "message": "..." }
FE Axios interceptor — Mara endpoint vrátí 503 PLATFORM_PAUSEDresponse handler v httpClient.ts zpracuje chybuplatformStatusStore.paused = true, Mara503Toast se zobrazí, chat input disabled, žádný polling spuštěn
FE — platformStatusStore.paused=true, user pošle další Mara request který vrátí 200response handler v httpClient.ts zpracuje úspěchplatformStatusStore.paused = false, Mara503Toast zmizí, chat input odemčen

Implementation Notes

Kill-switch cache strategie

BE developer vybere jednu ze dvou implementací — obě jsou akceptovatelné:

Varianta A — DB + Redis hybrid (doporučeno):

  • KillSwitchService.isPaused() čte Redis key platform:kill_switch:paused (TTL 10 s)
  • Cache miss → SELECT paused FROM platform_kill_switch LIMIT 1 → naplní cache
  • POST /pause a POST /resume → UPDATE DB → SET Redis key (invalidace + nová hodnota, TTL reset na 10 s)
  • Konzistence: max 10 s lag při výpadku Redis → fallback na DB (fail-open nebo fail-closed dle bezpečnostní politiky)

Varianta B — pure DB s krátkým polling window:

  • Gateway čte přímo z DB, ale s JVM-local in-memory cache (ConcurrentHashMap, TTL 5 s)
  • Jednodušší, bez Redis dependency pro tento feature, ale vyšší DB load při vysokém concurrency

Fail behavior při výpadku Redis

Rozhodnutí: pokud Redis nedostupný a cache miss → gateway volá DB. Pokud ani DB nedostupná → fail-closed (throw PlatformPausedException) pro bezpečnost.

Fatigue distribution — N Redis round-trips

Pro >100 userů bude GET /capacity provádět N SCAN + GET round-tripů pro per-user fatigue stav. Optimalizace přes Lua pipeline (atomický MULTI/EXEC přes všechny user klíče) je plánována jako future work při překročení prahu výkonu.

Notifikace pause stavu pro běžné usery — REAKTIVNĚ přes 503 (FINAL DECISION)

Rozhodnutí (Mirek, 2026-05-09): Žádný public status endpoint pro běžné usery. Status je pouze reaktivní přes HTTP 503 PLATFORM_PAUSED z Mara endpoints.

Trade-off: User vidí pause stav až při prvním pokusu o Mara call, ne proactively při loadu page. Pokud user otevře aplikaci, ale neudělá žádný Mara request, neuvidí žádný banner — což je akceptovatelné, protože pause stav je transient (admin ho rozpustí typicky během minut/hodin).

Důvod: Minimální scope MVP, žádný extra endpoint, žádný polling overhead, jednodušší implementace.

Implementace:

  • Globální Axios response interceptor v httpClient.ts zachytí HTTP 503 + error.code === "PLATFORM_PAUSED".
  • Při zachycení → platformStatusStore.setPausedFrom503() (paused=true, zobraz Mara503Toast, chat input disabled).
  • Při dalším úspěšném 200 z Mara endpointu → platformStatusStore.clearPausedOnSuccess() (paused=false, skryj toast, odemkni chat).
  • Žádný proactive polling veřejného endpointu.
  • Žádný MaraPausedBanner jako standalone komponenta — Mara503Toast.vue je řízena výhradně reaktivně z global error handler v Pinia store.

Explicitně NE-zaváděné (zamítnuto):

  • GET /api/v1/platform/status veřejný endpoint
  • MaraPausedBanner s 60 s pollingem
  • usePlatformStore s background polling logikou

Tech debt a future work

  1. usage_events legacy tabulka — UC-08004 cílí výhradně na api_usage_ledger. Legacy usage_events zůstává aktivní pro per-user view (jsou to záměrně oddělené věci, neslučovat).
  2. Reconciliation log (drift PG ↔ Redis) — detekce drift mezi api_usage_ledger součty a Redis rolling windows → navrhováno jako UC-08005.
  3. Fatigue distribution Lua pipeline — optimalizace N Redis GET pro per-user fatigue při >100 users.
  4. global:monthly_used_cents reset job — UC-08002 spec zmiňuje ShedLock reset job; BE developer ověří existenci v kódu před implementací UC-08004 (jinak capacity dashboard zobrazí chybný this_month_cents).
  5. Per-user budget dashboard — oddělení per-user budget view od platformního capacity view plánováno jako samostatný cleanup task.

Was this page helpful?

Thanks for the feedback.