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=truegateway odmítne každé Mara volání s HTTP 503PLATFORM_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íčiplatform: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ů:
userstabulka,api_usage_ledgertabulka, 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íč | Typ | TTL | Hodnota | Popis |
|---|---|---|---|---|
platform:kill_switch:paused | String | 10 s | "true" / "false" | Hot-path cache pro gateway pre-check. Invalidován při každém toggle (pause/resume). |
global:monthly_used_cents | String | — | integer (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_state | String | TTL 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
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
reason (pause modal) | not_blank | 10 – 1000 | — | Povinné 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:
/profileby 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é
beforeEachguard pro admin check. - Vzor silent probe z UC-08003 (InvitesSection) zůstane zachován jako fallback: při prvním mountu
CapacityDashboardScreenzavolá GET probe a 403 → redirect na/profiles 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
- Admin se přihlásí (standardní flow UC-01002).
- V profilovém menu / navigation vidí odkaz “Capacity Console” (skrytý pro non-admin roli —
v-if="authStore.isAdmin"). - Naviguje na
/admin/capacity. - Router guard
requiresAdminprovede silent probeGET /api/v1/admin/platform/kill-switch:200 OK→ pokračuje.403 Forbidden→ redirect na/profile+ toast “Access denied” (viz Path 4).
CapacityDashboardScreense mountuje, zobrazí skeleton loader ve všech metric kartách.- Paralelně se volají dva endpointy:
GET /api/v1/admin/platform/kill-switch→ stav banneru.GET /api/v1/admin/platform/capacity→ metric data.
- 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).
- Dashboard je read-only v tomto stavu — admin sleduje metriky.
Path 2 — Emergency brake: pauza platformy
- Admin vidí
KillSwitchBannerve stavu NORMAL (zelený). - Klikne tlačítko “Pause Platform” (destructive outline button, ikona
AlertTriangle). - Otevře se
KillSwitchPauseModals 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).
- Admin vyplní reason, klikne “Confirm Pause”.
- Button přejde do loading stavu (spinner + “Pausing…”), textarea a Cancel disabled.
POST /api/v1/admin/platform/kill-switch/pauses{ reason }.- Po
200 OK:- Modal se zavře.
KillSwitchBannerse přepne na stav PAUSED (červený) spaused_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).
- 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
- Admin vidí
KillSwitchBannerve stavu PAUSED (červený banner s důvodem a časem pauzy). - Klikne “Resume Platform” (zelené tlačítko, ikona
Play). - 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.
- Admin klikne “Yes, Resume”.
- Button → loading stav.
POST /api/v1/admin/platform/kill-switch/resume.- Po
200 OK:- Banner se přepne na NORMAL (zelený).
- Toast “Platform resumed.” (green, 5 s).
- Při chybě: inline error v banneru, tlačítka zpět aktivní.
Path 4 — Non-admin přístup
- User (role USER) naviguje manuálně na
/admin/capacity. - Router guard provede silent probe →
403 Forbidden. - Router přesměruje na
/profile. - Toast “Access denied” (rose/error, 5 s) se zobrazí po přesměrování.
- 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)
- User odesílá zprávu Maře (např.
POST /api/v1/conversations/{id}/messages). - BE vrátí
503 SERVICE_UNAVAILABLEs{ "code": "PLATFORM_PAUSED", "message": "..." }. - Globální Axios response interceptor v
httpClient.tszachytí HTTP 503 +error.code === "PLATFORM_PAUSED"a:- Nastaví
platformStatusStore.paused = true. - Spustí
Mara503Toast(sticky toast / inline banner).
- Nastaví
Mara503Toastzobrazí text “Platform is temporarily paused. Please try again later.” (CS: “Platforma je dočasně pozastavena. Zkus to prosím za chvíli.”).- Chat input se přepne na disabled s placeholder “Platform paused — try again later.” (řízeno z
platformStatusStore.paused). - Žádný proactive polling — user nezná stav platformy při loadu page; zjistí ho až při prvním pokusu o Mara request.
- Při dalším úspěšném Mara requestu (HTTP 200) interceptor nastaví
platformStatusStore.paused = falsea toast zmizí, chat input se odemkne. - 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
| Komponenta | Soubor | Popis |
|---|---|---|
CapacityDashboardScreen | src/screens/admin/CapacityDashboardScreen.vue | Parent route /admin/capacity. Orchestruje data fetching, polling, předává props do child komponent. |
KillSwitchBanner | src/screens/admin/components/KillSwitchBanner.vue | Stavový 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. |
KillSwitchPauseModal | src/screens/admin/components/KillSwitchPauseModal.vue | Modal s reason textarea. Focus trap. Disabled stav tlačítka dokud reason.length >= 10. Loading state při submitu. Emituje @confirm(reason) a @cancel. |
MetricCard | src/screens/admin/components/MetricCard.vue | Generická 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. |
FatigueDistributionBar | src/screens/admin/components/FatigueDistributionBar.vue | Př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. |
SpendProgressBar | src/screens/admin/components/SpendProgressBar.vue | Progress bar s procentem. Barva: zelená < 85 %, amber 85–94 %, rose >= 95 %. Přijímá spent, cap (v centech) a label. |
Mara503Toast | src/common/components/Mara503Toast.vue | Reaktivní 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". |
useCapacityStore | src/screens/admin/stores/capacity.ts | Pinia 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(). |
usePlatformStatusStore | src/common/stores/platformStatus.ts | Lightweight 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)
| Stav | Vizuá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 error | Pod 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"naKillSwitchBannerpři přechodu NORMAL → PAUSED i PAUSED → NORMAL — screen reader okamžitě oznámí změnu stavu platformy.aria-live="assertive"naMara503Toast— kritická informace pro usery při 503.aria-labelna “Pause Platform” button:aria-label="Pause all Mara API calls platform-wide".aria-labelna “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-labelledbyna 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ý
MetricCardmárole="region"saria-labelodpoví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
MetricCardzobrazí skeleton (pulse animateddivsbg-[var(--bg-3)] animate-pulse rounded).KillSwitchBannerzobrazí 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
| Chyba | Zobrazení |
|---|---|
| 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 error | Inline v modalu pod tlačítky: “Failed to pause platform. Please try again.” (rose text, role="alert"). Modal zůstane otevřený. |
| Resume API error | Inline v banneru pod inline-confirm row. |
403 při vstupu na /admin/capacity | Router 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
| Stav | Pozadí | Border | Text | Ikona | CTA |
|---|---|---|---|---|---|
LOADING | var(--bg-3) skeleton | - | skeleton | - | skeleton |
NORMAL | var(--green-soft) | oklch(0.78 0.13 150 / 0.4) | ”PLATFORM NORMAL — All Mara calls operating.” | CheckCircle (green) | “Pause Platform” (destructive outline, rose border) |
PAUSED | var(--rose-soft) | var(--rose-line) | ”PLATFORM PAUSED” + audit info | AlertTriangle (rose) | “Resume Platform” (green solid) |
ERROR | var(--bg-3) | var(--line-2) | ”Status unavailable” | AlertCircle (amber) | “Retry” |
Fatigue Distribution — barvy
Konzistentní s UC-08002 visual states (barvy odvozeny od design tokenů):
| State | Bar color | CSS var |
|---|---|---|
FRESH | Zelená | var(--green) |
WARMED_UP | Amber | var(--amber) |
TIRED | Amber desaturovaný (~60 %) | oklch(0.72 0.08 70) |
GROGGY | Rose desaturovaný | oklch(0.68 0.10 25) |
EXHAUSTED | Rose plný | var(--rose) |
ASLEEP | Fg-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
| Rozsah | Barva | Dů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
SseEmitterbean 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č.
| Breakpoint | Chová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
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
reason (PauseRequest) | not_blank | 10 – 1000 | — | Pouze pro POST /pause; u POST /resume není request body |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| Uživatel s rolí ADMIN, kill-switch paused=false | GET /api/v1/admin/platform/kill-switch je zavolán | 200 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án | 200 OK, paused=true, paused_by_email a reason vyplněny |
| Uživatel s rolí USER | GET /api/v1/admin/platform/kill-switch je zavolán | 403 Forbidden, code=FORBIDDEN |
| Unauthenticated request | GET /api/v1/admin/platform/kill-switch je zavolán | 401 Unauthorized, code=AUTHENTICATION_FAILED |
| ADMIN, kill-switch paused=false, platný reason (≥10 znaků) | POST /api/v1/admin/platform/kill-switch/pause je zavolán | 200 OK, paused=true, paused_at a paused_by_user_id persisted do DB, Redis key aktualizován |
| ADMIN, kill-switch paused=false, reason je blank | POST /api/v1/admin/platform/kill-switch/pause je zavolán | 400 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án | 400 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 reasonem | 200 OK, stav přepsán novým reasonem (last-write-wins, no-op conflict) |
| ADMIN, kill-switch paused=true | POST /api/v1/admin/platform/kill-switch/resume je zavolán | 200 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án | 200 OK, paused=false (no-op) |
| USER volá pause | POST /api/v1/admin/platform/kill-switch/pause je zavolán | 403 Forbidden |
| platform_kill_switch.paused=true v DB | callMara() je zavolán v AnthropicGatewayService | PlatformPausedException je thrown, HTTP 503 s error_code=PLATFORM_PAUSED |
| platform_kill_switch.paused=false v DB | callMara() je zavolán v AnthropicGatewayService | normá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án | 200 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á capacity | GET /api/v1/admin/platform/capacity je zavolán | 403 Forbidden |
| Capacity dashboard response time s fixtures (≤100 záznamů) | GET /api/v1/admin/platform/capacity je zavolán | response 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án | active_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án | errors_last_24h=5 |
| Platform paused=true, běžný user posílá zprávu Maře | POST /api/v1/conversations/{id}/messages (Mara endpoint) je zavolán | 503 Service Unavailable, response body { "code": "PLATFORM_PAUSED", "message": "..." } |
| FE Axios interceptor — Mara endpoint vrátí 503 PLATFORM_PAUSED | response handler v httpClient.ts zpracuje chybu | platformStatusStore.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í 200 | response handler v httpClient.ts zpracuje úspěch | platformStatusStore.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 keyplatform:kill_switch:paused(TTL 10 s)- Cache miss →
SELECT paused FROM platform_kill_switch LIMIT 1→ naplní cache POST /pauseaPOST /resume→ UPDATE DB →SETRedis 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.tszachytí HTTP 503 +error.code === "PLATFORM_PAUSED". - Při zachycení →
platformStatusStore.setPausedFrom503()(paused=true, zobrazMara503Toast, 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ý
MaraPausedBannerjako standalone komponenta —Mara503Toast.vueje řízena výhradně reaktivně z global error handler v Pinia store.
Explicitně NE-zaváděné (zamítnuto):
GET /api/v1/platform/statusveřejný endpointMaraPausedBanners 60 s pollingemusePlatformStores background polling logikou
Tech debt a future work
usage_eventslegacy tabulka — UC-08004 cílí výhradně naapi_usage_ledger. Legacyusage_eventszůstává aktivní pro per-user view (jsou to záměrně oddělené věci, neslučovat).- Reconciliation log (drift PG ↔ Redis) — detekce drift mezi
api_usage_ledgersoučty a Redis rolling windows → navrhováno jako UC-08005. - Fatigue distribution Lua pipeline — optimalizace N Redis GET pro per-user fatigue při >100 users.
global:monthly_used_centsreset 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).- Per-user budget dashboard — oddělení per-user budget view od platformního capacity view plánováno jako samostatný cleanup task.
Thanks for the feedback.