Mara hledá existující issues před každým novým reportem — pokud najde podobný, přidá komentář místo duplikátu. Search je scoped: non-admin vidí pouze vlastní issues (reporter_user_id); admin (Mirek) vidí všechny.
- Primární use case: Mara volá
search_issuesjako krok 2a v decision tree (viz UC-09001) před volánímreport_issue. - Sekundárně: Mara může vyhledat issues na žádost usera (“jaké problémy jsme nahlásili?”).
- Admin (Mirek) má neomezený přístup — vidí issues napříč tenanty.
- Phase 1: pouze
target=PLATFORMissues reálně existují;target=PROJECTfilter se přijme ale vrátí prázdný výsledek.
Flow 1 — Mara hledá issue před reportem
sequenceDiagram
actor Mara
Mara->>Mara: detekuje problém, rozhoduje se reportovat PLATFORM issue
Mara->>+BE: tool: search_issues(target=PLATFORM, status=OPEN, query="registry push fail")
Note over Mara: Mara NESPECIFIKUJE projectId — sidecar resolvuje project scope z ctx automaticky při target=PROJECT
Note over BE: GET /api/issues?target=PLATFORM&status=OPEN&query=registry+push+fail<br/>Authorization: Bearer {agentJWT}
BE->>BE: ověř JWT, extrahuj reporter_user_id
BE->>DB: SELECT issues WHERE target=PLATFORM AND status=OPEN<br/>AND reporter_user_id = <extrahované z JWT><br/>AND (title ILIKE '%registry push fail%' OR description ILIKE '%registry push fail%')<br/>ORDER BY reported_at DESC LIMIT 20
BE-->>-Mara: 200 OK [ IssueSummary, ... ]
alt výsledky nalezeny
Mara->>Mara: vyhodnotí relevanci — je to stejný problém?
alt stejný problém
Mara->>BE: tool: comment_issue(id, "Stalo se znovu. Kontext: ...")
Mara-->>User: "Stejný problém jsem nahlásila dříve jako [#ID](url). Přidala jsem poznámku."
else jiný problém
Mara->>BE: tool: report_issue(...) — nový issue
end
else žádné výsledky
Mara->>BE: tool: report_issue(...) — nový issue
end
Flow 2 — Mara zobrazuje issues na žádost usera
sequenceDiagram
actor User
actor Mara
User->>Mara: "jaké problémy jsme nahlásili?"
Mara->>+BE: tool: search_issues(target=PLATFORM)
Note over BE: GET /api/issues?target=PLATFORM<br/>Authorization: Bearer {agentJWT}
BE->>DB: SELECT issues WHERE reporter_user_id = <z JWT><br/>ORDER BY reported_at DESC LIMIT 20
BE-->>-Mara: 200 OK [ IssueSummary[] ]
Mara-->>User: "Nahlásili jsme tyto problémy:\n- [#ID](url) HIGH BUG — registry push fail (OPEN)\n- ..."
Tool API (Mara)
search_issues({
target?: "PLATFORM" | "PROJECT", // filter; bez filtru vrátí vše (jen vlastní issues)
status?: "OPEN" | "TRIAGED" | "IN_PROGRESS" | "RESOLVED" | "WONTFIX" | "DUPLICATE",
query?: string // LIKE filtr nad title + description (case-insensitive)
}): IssueSummary[]
// Žádný projectId param — sidecar resolvuje project scope z ctx při target=PROJECT automaticky.
// Mara ho nikdy nespecifikuje manuálně.
// IssueSummary:
// {
// id: string,
// target: string,
// kind: string,
// severity: string,
// status: string,
// title: string,
// reported_at: string, // ISO 8601
// url: string,
// reporter_email: string | null, // vyplněno pouze v admin scope; non-admin → null
// tenant_slug: string // vždy vyplněno (i non-admin)
// }
REST API
GET /api/issues
Autentizace: Agent JWT (Mara) nebo Admin JWT (platform_admin role).
Query params:
| Param | Typ | Popis |
|---|---|---|
target / targetType | string | Filter: PLATFORM nebo PROJECT (oba aliasy přijaty; targetType preferováno v Phase 3) |
projectId | UUID | (Phase 3) Per-project filter — vrátí issues pouze pro daný projekt |
tenantScoped | boolean | (Phase 3) Pokud true, vrátí všechny PROJECT issues tenantu (bez reporter_user_id scope) |
status | string | Filter: OPEN, TRIAGED, IN_PROGRESS, RESOLVED, WONTFIX, DUPLICATE |
query | string | LIKE filtr (case-insensitive) nad title a description |
Scoping:
- Non-admin agent JWT: výsledky omezeny na
reporter_user_id = <z JWT>(pokud nenítenantScoped=true). tenantScoped=true: vrátí všechny PROJECT issues tenantu (bezreporter_user_idfiltru) — používá UC-09006.- Admin JWT (
platform_adminrole): vrátí issues napříč všemi tenanty a uživateli.
Phase 3 rozšíření (
projectId,tenantScoped) je detailně popsáno v UC-09006 View Project Issues.
Příklad requestu:
GET /api/issues?target=PLATFORM&status=OPEN&query=registry+push
Authorization: Bearer <agentJWT>
200 OK:
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"target": "PLATFORM",
"kind": "BUG",
"severity": "HIGH",
"status": "OPEN",
"title": "Kaniko push fail — registry write scope missing",
"reported_at": "2026-05-13T14:23:00Z",
"url": "https://talkide.app/admin/issues/550e8400-e29b-41d4-a716-446655440000",
"reporter_email": "user@example.com",
"tenant_slug": "my-tenant"
}
],
"total": 42,
"page": 1,
"pageSize": 20
}
Pagination metadata:
total= celkový počet záznamů vyhovujících filtru (bez stránkování). FE derivuje badge count ztotal— žádný separátní/countendpoint není potřeba.pageapageSizeumožňují stránkování (Phase 2 FE může ignorovat stránkování a zobrazit jen první stránku s notice pokudtotal > pageSize).
Admin scope:
reporter_emailje vyplněno ("user@example.com"). Non-admin scope:reporter_email: null,tenant_slugzůstává vyplněn.
200 OK (prázdný výsledek — ne 404):
{
"items": [],
"total": 0,
"page": 1,
"pageSize": 20
}
401 Unauthorized (chybějící nebo neplatný JWT):
{
"code": "UNAUTHORIZED",
"message": "Missing or invalid authentication token"
}
GET /api/issues/{id}
Autentizace: Agent JWT (Mara) nebo Admin JWT.
Scoping: Non-admin: 404 pokud reporter_user_id neodpovídá JWT subjektu (security-through-obscurity, neodhaluje existenci cizích issues).
200 OK:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"target": "PLATFORM",
"project_id": null,
"tenant_slug": "my-tenant",
"reporter_user_id": "user-uuid",
"reporter_agent": "MARA",
"session_id": "session-uuid",
"title": "Kaniko push fail — registry write scope missing",
"description": "Při pokusu o docker push...",
"kind": "BUG",
"severity": "HIGH",
"status": "OPEN",
"context_jsonb": {
"projectState": {
"tenant_slug": "my-tenant",
"deploy_url": "https://my-tenant.talkide.app",
"last_build_sha": "abc123"
},
"k8sEvents": [...],
"podLogSnippet": "...",
"conversationSnippet": [...]
},
"resolved_by_user_id": null,
"resolution_note": null,
"parent_issue_id": null,
"reported_at": "2026-05-13T14:23:00Z",
"triaged_at": null,
"resolved_at": null,
"comments": [
{
"id": "comment-uuid",
"author_agent": "MARA",
"body": "Stalo se znovu. Kontext: ...",
"created_at": "2026-05-13T15:10:00Z"
}
]
}
404 Not Found:
{
"code": "NOT_FOUND",
"message": "Issue not found"
}
Backend
Validace
| Param | Constraints | Poznámka |
|---|---|---|
target / targetType | nullable, in(PLATFORM, PROJECT) pokud předán | Neplatná hodnota → 400 |
projectId | nullable UUID | (Phase 3) Pokud předán: ověř project.tenant_id = tenant_id z JWT; neplatné UUID → 400 |
tenantScoped | boolean | (Phase 3) Pokud true: ignoruje reporter_user_id scope, filtruje celý tenant |
status | nullable, in(OPEN, TRIAGED, IN_PROGRESS, RESOLVED, WONTFIX, DUPLICATE) pokud předán | Neplatná hodnota → 400 |
query | nullable, max 200 chars | Trim před použitím v LIKE; ILIKE %query% nad title a description |
| Paging | default pageSize=20, max pageSize=100; page default 1 | Response vždy obsahuje total, page, pageSize — FE/Mara derivuje celkový count z total bez separátního COUNT requestu |
| Scoping | Non-admin: AND reporter_user_id = <z JWT> (pokud ne tenantScoped=true) | Admin JWT (role platform_admin): bez scope filtru |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| Existují 3 OPEN issues pro daného usera s “registry” v titulu | GET /api/issues?target=PLATFORM&status=OPEN&query=registry | 200 OK, 3 záznamy vráceny; každý obsahuje id, title, severity, status, url |
| Žádný issue nevyhovuje filtru | GET /api/issues?query=nonexistent | 200 OK, prázdné pole [] |
| Admin JWT, existují issues různých uživatelů | GET /api/issues | 200 OK; vrátí issues napříč tenanty (bez reporter_user_id scopingu) |
| Non-admin JWT, cizí issue ID | GET /api/issues/{cizi-id} | 404 Not Found (neodhalí existenci cizího issue) |
| Non-admin JWT, vlastní issue ID | GET /api/issues/{vlastni-id} | 200 OK s plným detailem včetně context_jsonb a comments[] |
| Chybějící JWT | GET /api/issues | 401 Unauthorized |
query delší než 200 znaků | GET /api/issues?query=... | 400 Bad Request |
Frontend (Phase 2 — #82)
Phase 1: Mara operuje výhradně přes tool API. Phase 2 přidává FE screen pro oba role typy.
Routing
| Role | Route | Screen |
|---|---|---|
| ADMIN | /platform-issues | PlatformIssuesScreen.vue (admin varianta) |
| USER | /platform-issues | PlatformIssuesScreen.vue (user varianta — filtrováno na reporter_user_id=currentUser) |
Stejná cesta, jiný scope. Varianta se detekuje ze store (JWT claim role=ADMIN).
Admin check pattern
Shodný s existujícím silent probe patternem (/admin/capacity, /admin/reconciliation):
onMountedvoláissuesStore.probeAdminRole()— GET/api/issues?target=PLATFORM&status=OPENs limit=1.- 200 OK + admin scope vrátil issues i jiných uživatelů →
isAdmin=true. - 403 → redirect na
/studio+ toastt('issues.accessDenied'). meta: { requiresAuth: true }na route, žádnýrequiresAdminflag (lazy check v onMounted).
Nav badge (ADMIN only)
- Top-level nav položka Platform issues zobrazuje badge s počtem OPEN issues.
- Badge = číslo z
GET /api/issues?target=PLATFORM&status=OPEN(vrácená délka pole, max LIMIT). - Refresh: při každé navigaci na
/platform-issues+ při route change (Vue RouterafterEach). - Pokud je počet 0, badge se skryje (žádný prázdný
0badge). - USER role: stejná nav položka bez badge.
List view layout
Oba role: stejný layout. Admin dostane všechny issues, USER dostane jen vlastní (server-side scope).
Toolbar:
| Prvek | Popis |
|---|---|
| Status filter dropdown | Výchozí = OPEN. Volby: OPEN, TRIAGED, IN_PROGRESS, RESOLVED, WONTFIX, DUPLICATE, ALL (bez filtru). |
| Search input | Textové pole → ?query= param. Debounce 300 ms. |
| Refresh button | Ručně znovu načte list. Ikona RefreshCw (lucide-vue-next — existující pattern). |
Tabulka — sloupce:
| Sloupec | Zdroj dat | Poznámka |
|---|---|---|
| Severity badge | severity | HIGH=červená, MEDIUM=žlutá, LOW=šedá, CRITICAL=tmavě červená |
| Kind | kind | BUG / FEATURE (plain text nebo pill) |
| Title | title | Kliknutelný link → detail view |
| Reporter | reporter_agent icon + reporterEmail | MARA=robot icon, USER=user icon, KAI=wrench icon, ELI=bar-chart icon (lucide-vue-next). Email: zobrazit reporterEmail pokud not null, jinak fallback #id.slice(0,8) (defenzivní pro legacy data). ADMIN scope only — USER varianta tento sloupec skryje (vždy = sám). |
| Tenant | tenantSlug | Plain text. Zobrazuje se v admin scope; USER varianta skryje (single-tenant kontextově zbytečné). |
| Reported at | reported_at | Relativní čas (vzor: 2h ago), na hover absolutní ISO timestamp |
| Status badge | status | OPEN=modrá, TRIAGED=cyan, IN_PROGRESS=oranžová, RESOLVED=zelená, WONTFIX=šedá, DUPLICATE=šedá |
Výchozí řazení: severity DESC, reported_at DESC (posíláno jako query param sort=severity,desc&sort=reported_at,desc; BE musí respektovat).
Empty state: Pokud [] vrátí API — zobrazit ilustraci + text t('issues.list.emptyState') (žádná issues pro vybraný filtr).
Paging: Bez explicitního stránkování v Phase 2. BE vrátí max 100 záznamů (LIMIT). Pokud vrátí 100 přesně, zobrazit notice t('issues.list.limitReached').
Severity badge classes (Tailwind)
| Severity | CSS |
|---|---|
| CRITICAL | bg-red-900 text-red-100 |
| HIGH | bg-red-500 text-white |
| MEDIUM | bg-yellow-400 text-yellow-900 |
| LOW | bg-[var(--bg-3)] text-[var(--fg-3)] |
Status badge classes (Tailwind)
| Status | CSS |
|---|---|
| OPEN | bg-blue-500 text-white |
| TRIAGED | bg-cyan-400 text-cyan-900 |
| IN_PROGRESS | bg-orange-400 text-white |
| RESOLVED | bg-green-500 text-white |
| WONTFIX | bg-[var(--bg-3)] text-[var(--fg-3)] |
| DUPLICATE | bg-[var(--bg-3)] text-[var(--fg-3)] |
i18n klíče (nová sekce v screens/admin/i18n.ts nebo nový screens/issues/i18n.ts)
issues.list.title = "Platform Issues" / "Platform Issues"
issues.list.emptyState = "No issues found" / "Žádné issues"
issues.list.limitReached = "Showing first 100 results — refine your filter" / "Zobrazeno prvních 100 výsledků — zpřesni filtr"
issues.filter.status.ALL = "All statuses" / "Všechny stavy"
issues.filter.status.OPEN = "Open" / "Otevřené"
issues.filter.status.TRIAGED = "Triaged" / "Protříděné"
... (ostatní statusy analogicky)
issues.col.severity = "Severity" / "Závažnost"
issues.col.kind = "Kind" / "Typ"
issues.col.title = "Title" / "Název"
issues.col.reporter = "Reporter" / "Reporter"
issues.col.reportedAt = "Reported" / "Nahlášeno"
issues.col.status = "Status" / "Stav"
issues.col.reporter = "Reporter" / "Reporter"
issues.col.tenant = "Tenant" / "Tenant"
issues.accessDenied = "Access denied" / "Přístup odepřen"
Pinia store (useIssuesStore)
state:
issues: IssueSummary[]
loading: boolean
error: string | null
statusFilter: string // výchozí 'OPEN'
query: string // výchozí ''
openCount: number // pro nav badge
actions:
fetchList(params?) // GET /api/issues s aktivními filtry → response.items
fetchOpenCount() // GET /api/issues?target=PLATFORM&status=OPEN&pageSize=1 → response.total
FE soubory (navrhovaná struktura)
src/screens/issues/
PlatformIssuesScreen.vue ← list screen
PlatformIssueDetailScreen.vue ← detail screen (viz UC-09004)
i18n.ts
stores/issues.ts
model/IssueSummaryDto.ts // reporterEmail: string | null; tenantSlug: string
model/IssueDetailDto.ts
components/
SeverityBadge.vue
StatusBadge.vue
ReporterCell.vue
IssueFiltersBar.vue
FEEDBACK
Zadání bylo jednoznačné a nechybělo nic zásadního. Drobná věc, která by editaci zrychlila: explicitní sdělení, zda tenantSlug má být viditelný i v USER variantě tabulky, nebo jen v admin — z kontextu jsem to musel odvodit (dorovnání s detailem a fráze “v Phase 1 user vidí jen vlastní tenant, ale do budoucna univerzální”). Totéž platí pro i18n klíč issues.col.reporter — v původní i18n sekci chyběl, přestože sloupec existoval; doplnil jsem ho při příležitosti.
References
- UC-09001 Report Platform Issue — search je prerekvizita před reportem
- UC-09003 Comment Issue — akce po nalezení existujícího issue
- UC-09004 Admin Triage Issue — admin vidí všechny issues bez scope
Thanks for the feedback.