Admin (Mirek, role platform_admin) třídí issue přes REST API — mění status, nastavuje severity=CRITICAL, přidává resolution_note a linkuje duplikáty. Mara ani jiní agenti k tomuto endpointu nemají přístup.
- Pouze
platform_adminrole může volatPATCH /api/issues/{id}. severity=CRITICALmůže nastavit výhradně admin — Mara nikdy (403 při pokusu viz UC-09001).- Status transitions jsou řízené — viz stavový diagram níže.
- Phase 1: admin operuje přes REST (curl / httpie / Mirekův vlastní script). Studio UI přijde v Phase 2 (#82).
Status state machine
┌─────────────────────────────────────┐
│ │
report_issue ▼ │
──────────► OPEN ──────────► TRIAGED ──────────► IN_PROGRESS
│ │
├────────────────────┤
│ │
▼ ▼
RESOLVED RESOLVED
WONTFIX WONTFIX
DUPLICATE DUPLICATE
| Z | Do | Podmínky |
|---|---|---|
OPEN | TRIAGED | Admin potvrdil a ohodnotil issue |
OPEN | WONTFIX | Admin rozhodl nepravdivý nebo nevhodný report |
OPEN | DUPLICATE | Admin linkoval parent_issue_id; resolution_note doporučen |
TRIAGED | IN_PROGRESS | Admin začal řešit |
TRIAGED | RESOLVED | Rychlé opravy bez explicitního IN_PROGRESS |
TRIAGED | WONTFIX | Přehodnocení po triage |
TRIAGED | DUPLICATE | Pozdní detekce duplikátu |
IN_PROGRESS | RESOLVED | Oprava dokončena; resolution_note povinný |
IN_PROGRESS | WONTFIX | Rozhodnutí during implementation |
RESOLVED | OPEN | Reopening — chyba se opakovala nebo fix selhala |
WONTFIX | OPEN | Přehodnocení — issue bude řešen |
DUPLICATE | OPEN | Parent issue zrušen; tento se stane samostatným |
Přechody RESOLVED → *, WONTFIX → *, DUPLICATE → * (kromě zpět na OPEN) jsou zakázány — uzavřený issue se jen reopenuje na OPEN.
Flow 1 — Admin třídí nový OPEN issue
sequenceDiagram
actor Admin as Admin (Mirek)
Admin->>+BE: PATCH /api/issues/{id}
Note over BE: Authorization: Bearer {adminJWT}<br/>{ status: "TRIAGED", severity: "CRITICAL" }
BE->>BE: ověř Admin JWT (platform_admin role)
alt chybí role platform_admin
BE-->>Admin: 403 Forbidden
end
BE->>DB: SELECT issues WHERE id = ? FOR UPDATE
alt issue neexistuje
BE-->>Admin: 404 Not Found
end
BE->>BE: validuj status transition (OPEN → TRIAGED je povoleno)
alt nepovolená transition
BE-->>Admin: 422 Unprocessable Entity (INVALID_TRANSITION)
end
BE->>DB: UPDATE issues SET status=TRIAGED, severity=CRITICAL, triaged_at=now()<br/>WHERE id = ?
BE-->>-Admin: 200 OK { id, status, severity, triaged_at }
Flow 2 — Admin označuje issue jako DUPLICATE
sequenceDiagram
actor Admin as Admin (Mirek)
Admin->>+BE: PATCH /api/issues/{id}
Note over BE: { status: "DUPLICATE", parent_issue_id: "uuid-parent",<br/> resolution_note: "Stejný jako #parent-id" }
BE->>BE: ověř Admin JWT
BE->>DB: SELECT issues WHERE id = ? FOR UPDATE
BE->>DB: SELECT issues WHERE id = parent_issue_id (ověř existenci parenta)
alt parent neexistuje
BE-->>Admin: 422 Unprocessable Entity (PARENT_ISSUE_NOT_FOUND)
end
alt parent je sám DUPLICATE (chain)
BE-->>Admin: 422 Unprocessable Entity (DUPLICATE_CHAIN_NOT_ALLOWED)
end
BE->>DB: UPDATE issues SET status=DUPLICATE, parent_issue_id=?,<br/>resolution_note=?, resolved_by_user_id={admin}, resolved_at=now()<br/>WHERE id = ?
BE-->>-Admin: 200 OK { id, status, parent_issue_id, resolution_note }
Flow 3 — Admin resolves issue
sequenceDiagram
actor Admin as Admin (Mirek)
Admin->>+BE: PATCH /api/issues/{id}
Note over BE: { status: "RESOLVED",<br/> resolution_note: "Opraveno v deployi 2026-05-14, registry scope fixed." }
BE->>BE: ověř Admin JWT
BE->>DB: SELECT issues WHERE id = ? FOR UPDATE
BE->>BE: validuj: status IN (TRIAGED, IN_PROGRESS) → RESOLVED povoleno
BE->>BE: validuj: resolution_note povinný pro RESOLVED
alt resolution_note chybí
BE-->>Admin: 400 Bad Request (RESOLUTION_NOTE_REQUIRED)
end
BE->>DB: UPDATE issues SET status=RESOLVED, resolution_note=?,<br/>resolved_by_user_id={admin}, resolved_at=now()<br/>WHERE id = ?
BE-->>-Admin: 200 OK { id, status, resolved_at, resolution_note }
REST API
PATCH /api/issues/{id}
Autentizace: Admin JWT s rolí platform_admin. Jakýkoli jiný JWT → 403 Forbidden.
Request (všechna pole optional — jen ta přítomná se uplatní):
{
"status": "TRIAGED",
"severity": "CRITICAL",
"resolution_note": "...",
"parent_issue_id": "uuid-of-parent"
}
200 OK:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "TRIAGED",
"severity": "CRITICAL",
"triaged_at": "2026-05-13T16:00:00Z",
"resolved_at": null,
"resolved_by_user_id": null,
"resolution_note": null,
"parent_issue_id": null
}
400 Bad Request (resolution_note chybí při RESOLVED/WONTFIX):
{
"code": "RESOLUTION_NOTE_REQUIRED",
"message": "resolution_note is required when resolving or closing an issue"
}
403 Forbidden (chybí platform_admin role):
{
"code": "FORBIDDEN",
"message": "Only platform_admin can triage issues"
}
404 Not Found:
{
"code": "NOT_FOUND",
"message": "Issue not found"
}
422 Unprocessable Entity (nepovolená status transition):
{
"code": "INVALID_TRANSITION",
"message": "Cannot transition from RESOLVED to IN_PROGRESS"
}
422 Unprocessable Entity (parent_issue_id neexistuje):
{
"code": "PARENT_ISSUE_NOT_FOUND",
"message": "Parent issue not found"
}
422 Unprocessable Entity (chain duplikátů):
{
"code": "DUPLICATE_CHAIN_NOT_ALLOWED",
"message": "Parent issue is already a DUPLICATE — use its parent instead"
}
Backend
Validace
| Pole | Constraints | Poznámka |
|---|---|---|
| JWT role | platform_admin | Jakýkoli jiný JWT → 403 |
status | nullable, in(OPEN, TRIAGED, IN_PROGRESS, RESOLVED, WONTFIX, DUPLICATE) | Validovat přechodovou tabulku (viz state machine) |
severity | nullable, in(LOW, MEDIUM, HIGH, CRITICAL) | CRITICAL povoleno pro admin — toto je záměrné; Mara blokována na 403 v UC-09001 |
resolution_note | povinný pokud status ∈ {RESOLVED, WONTFIX, DUPLICATE} | max 5000 chars |
parent_issue_id | UUID, povinný pokud status=DUPLICATE | Parent musí existovat a nesmí být sám DUPLICATE |
| Transition enforcement | viz state machine | Nepovolená transition → 422 |
| Timestamp fields | server-side auto | triaged_at=now() při přechodu na TRIAGED; resolved_at=now() při RESOLVED/WONTFIX/DUPLICATE |
resolved_by_user_id | automaticky z admin JWT subjektu | Nastavit při RESOLVED/WONTFIX/DUPLICATE |
Povinnost resolution_note
| Cílový status | resolution_note povinný? |
|---|---|
TRIAGED | Ne |
IN_PROGRESS | Ne |
RESOLVED | Ano |
WONTFIX | Ano |
DUPLICATE | Doporučen, ale nepovinný (parent_issue_id postačí) |
OPEN (reopen) | Ne |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| Admin JWT, issue ve stavu OPEN | PATCH /api/issues/{id} s { status: "TRIAGED", severity: "CRITICAL" } | 200 OK; status=TRIAGED, severity=CRITICAL, triaged_at nastaven; non-admin by dostal 403 |
| Admin JWT, issue ve stavu TRIAGED | PATCH /api/issues/{id} s { status: "IN_PROGRESS" } | 200 OK; status=IN_PROGRESS |
| Admin JWT, issue IN_PROGRESS, chybí resolution_note | PATCH /api/issues/{id} s { status: "RESOLVED" } | 400 Bad Request, code=RESOLUTION_NOTE_REQUIRED |
| Admin JWT, issue IN_PROGRESS, platná resolution_note | PATCH /api/issues/{id} s { status: "RESOLVED", resolution_note: "..." } | 200 OK; status=RESOLVED, resolved_at nastaven, resolved_by_user_id = admin ID |
| Admin JWT, pokus o RESOLVED → IN_PROGRESS | PATCH /api/issues/{id} s { status: "IN_PROGRESS" } | 422 Unprocessable Entity, code=INVALID_TRANSITION |
Admin JWT, status=DUPLICATE, platný parent_issue_id | PATCH /api/issues/{id} s { status: "DUPLICATE", parent_issue_id: "..." } | 200 OK; parent_issue_id nastaven; resolved_at nastaven |
Admin JWT, status=DUPLICATE, parent_issue_id je sám DUPLICATE | PATCH /api/issues/{id} | 422, code=DUPLICATE_CHAIN_NOT_ALLOWED |
Admin JWT, status=DUPLICATE, neexistující parent_issue_id | PATCH /api/issues/{id} | 422, code=PARENT_ISSUE_NOT_FOUND |
| Non-admin JWT (agent nebo user JWT) | PATCH /api/issues/{id} s libovolným payload | 403 Forbidden, code=FORBIDDEN |
| Issue neexistuje | PATCH /api/issues/{neexistujici-id} | 404 Not Found |
| Admin JWT, WONTFIX bez resolution_note | PATCH /api/issues/{id} s { status: "WONTFIX" } | 400 Bad Request, code=RESOLUTION_NOTE_REQUIRED |
| Admin JWT, pouze severity update bez status změny | PATCH /api/issues/{id} s { severity: "HIGH" } | 200 OK; pouze severity aktualizována; status beze změny |
Poznámky k implementaci
Partial update (PATCH sémantika)
PATCH /api/issues/{id} je partial update — přítomné fieldy se uplatní, chybějící se nezmění. BE musí rozlišit “field chybí v requestu” od “field má null hodnotu”. Doporučeno: explicitní nullable wrapper nebo přítomnost klíče v JSON body.
Timestamps server-side
Admin nenastavuje triaged_at, resolved_at explicitně — server je nastavuje při příslušném status přechodu:
triaged_at = now()při prvním přechodu doTRIAGEDresolved_at = now()při přechodu doRESOLVED,WONTFIX, neboDUPLICATEtriaged_atse nemění při dalších přechodech
Reopen (→ OPEN)
Při reopen se resolved_at a resolved_by_user_id nulují (SET NULL). triaged_at zůstane (audit trail). Admin musí znovu projít TRIAGED → IN_PROGRESS → RESOLVED cyklus.
Concurrent triage
PATCH používá SELECT ... FOR UPDATE — zabraňuje race condition při paralelních admin aktualizacích (i když v Phase 1 je admin jen Mirek, dobrá praxe).
Frontend (Phase 2 — #82)
Phase 1: admin operoval REST-only (curl / httpie). Phase 2 přidává kompletní FE detail view.
Routing
| Route | Screen | Přístup |
|---|---|---|
/platform-issues/:id | PlatformIssueDetailScreen.vue | ADMIN i USER (USER = read-only; triage panel skryt) |
Detail view — layout
Screen je rozdělen na tři zóny (vertikální stack):
┌─────────────────────────────────────────────────────┐
│ Header: Title + Severity badge + Status badge + Kind │
│ Meta row: Reporter icon + email + tenant | timestamps │
├─────────────────────────────────────────────────────┤
│ Description (markdown render, read-only) │
├─────────────────────────────────────────────────────┤
│ Collapsible context sekce (default collapsed) │
│ ▸ Conversation snippet │
│ ▸ K8s events │
│ ▸ Pod logs │
│ ▸ Project state │
├─────────────────────────────────────────────────────┤
│ Comments timeline (chronologicky ASC) │
├─────────────────────────────────────────────────────┤
│ Comment box (Markdown, max 10 000 chars) │
├─────────────────────────────────────────────────────┤
│ Triage akce panel (ADMIN only, skryt pro USER) │
└─────────────────────────────────────────────────────┘
Header a meta
- Title: h1, plný text, bez truncation.
- Severity badge: viz třídy v UC-09002 FE sekci.
- Status badge: viz třídy v UC-09002 FE sekci.
- Kind: plain text nebo malý pill
BUG/FEATURE. - Reporter meta row:
reporter_agentikona + user email +@ tenant_slug,reported_at(relativní čas, absolutní na hover). - Timestamps section (kompaktní, font-mono, fg-3):
reported_at— vždy přítomný.triaged_at— zobrazit pouze pokud není null.resolved_at— zobrazit pouze pokud není null.
Description
- Renderovat jako Markdown (použít existující Markdown renderer v projektu, případně
marked/markdown-it— zkontrolovat co workspace screen už používá). - Read-only pro oba role typy.
- Pokud description je prázdný → skrýt sekci (nemělo by nastat, BE vyžaduje not_blank).
Collapsible context sekce
Každá ze čtyř sekcí je samostatný <details> / collapse komponent, výchozí stav = closed.
| Sekce | Datový zdroj | Zobrazení |
|---|---|---|
| Conversation snippet | context_jsonb.conversationSnippet (array zpráv) | Chronologický list zpráv; role + content; monospace; max-height 300px se scrollem |
| K8s events | context_jsonb.k8sEvents (array) | Tabulka: type, reason, message, firstTime; WARN events zvýraznit žlutě, ERROR červeně |
| Pod logs | context_jsonb.podLogSnippet (string) | <pre> monospace, max-height 400px se scrollem, white-space: pre |
| Project state | context_jsonb.projectState (object) | Key-value tabulka: tenant_slug, deploy URL (kliknutelný link), last_build_sha |
Pokud je sekce v context_jsonb null nebo chybí (best-effort enrichment), zobrazit inline notice: t('issues.context.notAvailable') = “Not available — context enrichment may have failed”.
Žádná z sekcí se nenačítá lazy — data jsou součástí GET /api/issues/{id} response.
Comments timeline
- Chronologické řazení ASC (
created_at). - Každý komentář: avatar / ikona podle
author_agent(MARA=robot, USER=user, ADMIN=shield/wrench) + author email nebo “Mara” / “Admin” + relativní čas + markdown-rendered body. - Systémové auto-komentáře (dedup,
author_agent=MARA,author_user_id= reportující user) vizuálně odlišit lehčí barvou pozadí nebo dashed border. - Pokud žádné komentáře → zobrazit
t('issues.comments.empty')= “No comments yet”.
Comment box
- Dostupný pro oba role (ADMIN i USER).
- Prostý
<textarea>— žádný WYSIWYG editor. Placeholder:t('issues.comments.placeholder')= “Add a comment (Markdown supported)”. - Max 10 000 znaků (shodné s BE limitem). Counter zobrazit při > 9 000 znaků.
- Submit = tlačítko
t('issues.comments.submit')= “Comment”. - Po úspěšném
POST /api/issues/{id}/comments→ přidat nový komentář do timeline bez full page reload (optimistic update nebo refetch comments). - Loading state: tlačítko disabled + spinner.
- Error state: inline error pod textarea.
- Prázdné body → Submit tlačítko disabled (FE guard shodný s BE).
Triage akce panel (ADMIN only)
Panel je skryt pro USER roli. Pro ADMIN zobrazit jako kartu / panel oddělený od comment sekce.
Triage panel zobrazuje dostupné přechody dle aktuálního stavu issue (jen ty povolené dle state machine):
Přechod OPEN → TRIAGED:
- Tlačítko “Take ownership” (inline, bez extra inputu).
- Po kliknutí:
PATCH /api/issues/{id}s{ status: "TRIAGED" }. - Toast:
t('issues.triage.toastTriaged')= “Issue triaged”.
Přechod TRIAGED → IN_PROGRESS:
- Tlačítko “Start working on it” (inline, bez extra inputu).
- Po kliknutí:
PATCH /api/issues/{id}s{ status: "IN_PROGRESS" }.
Přechod IN_PROGRESS → RESOLVED nebo TRIAGED → RESOLVED:
- Tlačítko “Mark as resolved” → inline form rozbalí se pod tlačítkem (ne modal).
- Inline form:
<textarea>s labelemt('issues.triage.resolutionNote')= “Resolution note (required)”, min 10 znaků. - Submit:
PATCH /api/issues/{id}s{ status: "RESOLVED", resolution_note: "..." }. - Po úspěchu: inline form se skryje, status badge se aktualizuje.
Přechod * → WONTFIX:
- Tlačítko “Won’t fix” → inline form shodný s RESOLVED (resolution_note povinný).
- Submit:
PATCH /api/issues/{id}s{ status: "WONTFIX", resolution_note: "..." }.
Přechod * → DUPLICATE of #N:
- Tlačítko “Mark as duplicate” → inline form: number input
t('issues.triage.parentIssueId')= “Duplicate of issue ID (UUID)” + optional<textarea>pro poznámku. - Submit:
PATCH /api/issues/{id}s{ status: "DUPLICATE", parent_issue_id: "...", resolution_note?: "..." }. - Validace: UUID formát (BE vrátí 422 pokud neexistuje — zobrazit inline error).
Přechod RESOLVED/WONTFIX/DUPLICATE → OPEN (reopen):
- Tlačítko “Reopen” (červeně zvýrazněno) → confirmation inline: “Reopen this issue?” + tlačítka Confirm / Cancel.
- Submit:
PATCH /api/issues/{id}s{ status: "OPEN" }.
Severity upgrade (ADMIN only):
- Dropdown
Severitys volbami LOW / MEDIUM / HIGH / CRITICAL. - Změna → okamžitý
PATCH /api/issues/{id}s{ severity: "..." }(žádný extra submit krok). - Podsvítit CRITICAL volbu červeně jako upozornění.
Rozhodnutí: inline form místo modalu — triage akce s povinným resolution_note se realizují inline rozbalitelným formulářem pod tlačítkem, ne modálním dialogem. Důvody:
- Triage probíhá v kontextu detailu issue — user vidí description i history zároveň.
- Méně kliknutí než modal overlay.
- Shodné s UX pattern z
KillSwitchBanner.vue(inline confirm-before-action).
i18n klíče (přidat do issues sekce v i18n souboru)
issues.detail.reportedAt = "Reported" / "Nahlášeno"
issues.detail.triagedAt = "Triaged" / "Protříděno"
issues.detail.resolvedAt = "Resolved" / "Vyřešeno"
issues.context.conversation = "Conversation snippet" / "Snippet konverzace"
issues.context.k8sEvents = "K8s events" / "K8s události"
issues.context.podLogs = "Pod logs" / "Logy podu"
issues.context.projectState = "Project state" / "Stav projektu"
issues.context.notAvailable = "Not available — context enrichment may have failed" / "Nedostupné — obohacení kontextu mohlo selhat"
issues.comments.empty = "No comments yet" / "Zatím žádné komentáře"
issues.comments.placeholder = "Add a comment (Markdown supported)" / "Přidej komentář (Markdown podporován)"
issues.comments.submit = "Comment" / "Komentovat"
issues.triage.takeOwnership = "Take ownership" / "Vzít na sebe"
issues.triage.startWork = "Start working on it" / "Začít řešit"
issues.triage.markResolved = "Mark as resolved" / "Označit jako vyřešené"
issues.triage.wontFix = "Won't fix" / "Nebudu řešit"
issues.triage.markDuplicate = "Mark as duplicate" / "Označit jako duplikát"
issues.triage.reopen = "Reopen" / "Znovu otevřít"
issues.triage.resolutionNote = "Resolution note (required)" / "Poznámka k uzavření (povinná)"
issues.triage.parentIssueId = "Duplicate of issue (UUID)" / "Duplikát issue (UUID)"
issues.triage.confirmReopen = "Reopen this issue?" / "Znovu otevřít tento issue?"
issues.triage.toastTriaged = "Issue triaged" / "Issue přidán do triáže"
issues.triage.toastResolved = "Issue resolved" / "Issue vyřešen"
issues.triage.toastWontFix = "Marked as won't fix" / "Označeno jako nebudu řešit"
issues.triage.toastDuplicate = "Marked as duplicate" / "Označeno jako duplikát"
issues.triage.toastReopened = "Issue reopened" / "Issue znovu otevřen"
issues.triage.toastError = "Action failed — try again" / "Akce selhala — zkus znovu"
References
- UC-09001 Report Platform Issue — CRITICAL severity je výhradně admin; Mara dostane 403
- UC-09002 Search Issues — admin GET /api/issues vrátí issues napříč tenanty
- UC-09003 Comment Issue — admin může komentovat přes POST /api/issues/{id}/comments
- #82 — Phase 2: Studio UI pro admin triage (tento UC popisuje REST-only Phase 1)
Thanks for the feedback.