Internal Documentation internal
TalkIDE internal documentation

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_issues jako krok 2a v decision tree (viz UC-09001) před voláním report_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=PLATFORM issues reálně existují; target=PROJECT filter 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:

ParamTypPopis
target / targetTypestringFilter: PLATFORM nebo PROJECT (oba aliasy přijaty; targetType preferováno v Phase 3)
projectIdUUID(Phase 3) Per-project filter — vrátí issues pouze pro daný projekt
tenantScopedboolean(Phase 3) Pokud true, vrátí všechny PROJECT issues tenantu (bez reporter_user_id scope)
statusstringFilter: OPEN, TRIAGED, IN_PROGRESS, RESOLVED, WONTFIX, DUPLICATE
querystringLIKE 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 (bez reporter_user_id filtru) — používá UC-09006.
  • Admin JWT (platform_admin role): 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 z total — žádný separátní /count endpoint není potřeba. page a pageSize umožňují stránkování (Phase 2 FE může ignorovat stránkování a zobrazit jen první stránku s notice pokud total > pageSize).

Admin scope: reporter_email je vyplněno ("user@example.com"). Non-admin scope: reporter_email: null, tenant_slug zů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

ParamConstraintsPoznámka
target / targetTypenullable, in(PLATFORM, PROJECT) pokud předánNeplatná hodnota → 400
projectIdnullable UUID(Phase 3) Pokud předán: ověř project.tenant_id = tenant_id z JWT; neplatné UUID → 400
tenantScopedboolean(Phase 3) Pokud true: ignoruje reporter_user_id scope, filtruje celý tenant
statusnullable, in(OPEN, TRIAGED, IN_PROGRESS, RESOLVED, WONTFIX, DUPLICATE) pokud předánNeplatná hodnota → 400
querynullable, max 200 charsTrim před použitím v LIKE; ILIKE %query% nad title a description
Pagingdefault pageSize=20, max pageSize=100; page default 1Response vždy obsahuje total, page, pageSize — FE/Mara derivuje celkový count z total bez separátního COUNT requestu
ScopingNon-admin: AND reporter_user_id = <z JWT> (pokud ne tenantScoped=true)Admin JWT (role platform_admin): bez scope filtru

Test Cases

GIVENWHENTHEN
Existují 3 OPEN issues pro daného usera s “registry” v tituluGET /api/issues?target=PLATFORM&status=OPEN&query=registry200 OK, 3 záznamy vráceny; každý obsahuje id, title, severity, status, url
Žádný issue nevyhovuje filtruGET /api/issues?query=nonexistent200 OK, prázdné pole []
Admin JWT, existují issues různých uživatelůGET /api/issues200 OK; vrátí issues napříč tenanty (bez reporter_user_id scopingu)
Non-admin JWT, cizí issue IDGET /api/issues/{cizi-id}404 Not Found (neodhalí existenci cizího issue)
Non-admin JWT, vlastní issue IDGET /api/issues/{vlastni-id}200 OK s plným detailem včetně context_jsonb a comments[]
Chybějící JWTGET /api/issues401 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

RoleRouteScreen
ADMIN/platform-issuesPlatformIssuesScreen.vue (admin varianta)
USER/platform-issuesPlatformIssuesScreen.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):

  • onMounted volá issuesStore.probeAdminRole() — GET /api/issues?target=PLATFORM&status=OPEN s limit=1.
  • 200 OK + admin scope vrátil issues i jiných uživatelů → isAdmin=true.
  • 403 → redirect na /studio + toast t('issues.accessDenied').
  • meta: { requiresAuth: true } na route, žádný requiresAdmin flag (lazy check v onMounted).
  • 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 Router afterEach).
  • Pokud je počet 0, badge se skryje (žádný prázdný 0 badge).
  • 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:

PrvekPopis
Status filter dropdownVýchozí = OPEN. Volby: OPEN, TRIAGED, IN_PROGRESS, RESOLVED, WONTFIX, DUPLICATE, ALL (bez filtru).
Search inputTextové pole → ?query= param. Debounce 300 ms.
Refresh buttonRučně znovu načte list. Ikona RefreshCw (lucide-vue-next — existující pattern).

Tabulka — sloupce:

SloupecZdroj datPoznámka
Severity badgeseverityHIGH=červená, MEDIUM=žlutá, LOW=šedá, CRITICAL=tmavě červená
KindkindBUG / FEATURE (plain text nebo pill)
TitletitleKliknutelný link → detail view
Reporterreporter_agent icon + reporterEmailMARA=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).
TenanttenantSlugPlain text. Zobrazuje se v admin scope; USER varianta skryje (single-tenant kontextově zbytečné).
Reported atreported_atRelativní čas (vzor: 2h ago), na hover absolutní ISO timestamp
Status badgestatusOPEN=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)

SeverityCSS
CRITICALbg-red-900 text-red-100
HIGHbg-red-500 text-white
MEDIUMbg-yellow-400 text-yellow-900
LOWbg-[var(--bg-3)] text-[var(--fg-3)]

Status badge classes (Tailwind)

StatusCSS
OPENbg-blue-500 text-white
TRIAGEDbg-cyan-400 text-cyan-900
IN_PROGRESSbg-orange-400 text-white
RESOLVEDbg-green-500 text-white
WONTFIXbg-[var(--bg-3)] text-[var(--fg-3)]
DUPLICATEbg-[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


Was this page helpful?

Thanks for the feedback.