User (ze Workspace tabu) nebo Mara (z konverzace přes tool) nahlásí issue spojený s konkrétním projektem (target=PROJECT). Oba reporteři zapisují stejný datový shape — liší se zdrojem project_id a hodnotou reporter_agent. USER posílá project_id explicitně z FE route; Mara project_id nepředává vůbec — sidecar tool ho resolvuje z ctx automaticky.
- Phase 3 rozšiřuje Issue API o
target=PROJECT. Schema bylo připraveno již v Phase 1 (0019-create-issues-tables.xml), ale FK constraintissues.project_id → projects.idbyl omylem vynechán — doplňuje ho nová migrace0020-add-project-fk-to-issues.xml(immutable po deploymentu na prod). - Dva reporteři: USER (Studio uživatel kliká v UI) a MARA (tool call z konverzace).
reporter_agentv requestu rozlišuje oba. Phase 1 whitelistMARA onlyje v #91 rozšířen naMARA | USER. severity = CRITICALMara nikdy nenastaví — výhradně admin při triage (UC-09004). USER reportér taktéž nemůže nastavit CRITICAL.- Cascade delete: smazání projektu kaskádově smaže všechny jeho PROJECT issues přes DB FK
ON DELETE CASCADE.ProjectCascadeDeleteržádnou logiku nemění — FK zajistí vše. - Context enrichment: stejný mechanismus jako UC-09001 (K8s events, pod logy) — pro
target=PROJECTse dotazuje pouzetenant-<slug>namespace (netalkidenamespace). Detaily viz UC-09001 — Auto-context enrichment.
DB Migration — Phase 3 FK constraint
Nový soubor: src/main/resources/db/changelog/changes/0020-add-project-fk-to-issues.xml
NEEDITUJ existující soubor 0019. Toto je nový append-only changeset.
<changeSet id="0020-add-project-fk-to-issues" author="system">
<addForeignKeyConstraint
baseTableName="issues"
baseColumnNames="project_id"
constraintName="fk_issues_project"
referencedTableName="projects"
referencedColumnNames="id"
onDelete="CASCADE"/>
</changeSet>
project_id zůstává nullable (Phase 1 PLATFORM issues nemají projekt — migrace funguje pro obě prostředí).
Flow 1 — USER reportuje PROJECT issue z Workspace tabu
sequenceDiagram
actor User
User->>+FE: klikne "Nahlásit problém" ve Workspace tabu
FE->>FE: otevře modal s formulářem (kind, title, description, severity)
User->>FE: vyplní formulář a odešle
FE->>FE: validuje formulář (kind not null, title not blank max 80, description not blank)
alt formulář nevalidní
FE-->>User: zobrazí chybové hlášky u polí
end
FE->>+BE: POST /api/issues <br> IssueCreateRequest { target="PROJECT", project_id={current}, reporter_agent="USER", kind, title, description, severity? }
Note over FE,BE: Authorization: Bearer {userJWT}
BE->>BE: parseReporterAgent() — whitelist check: MARA | USER
BE->>BE: validuj request (target, kind, title max 80, severity != CRITICAL)
alt target=PROJECT a project_id null
BE-->>FE: 400 Bad Request (ISSUE_PROJECT_ID_REQUIRED)
end
BE->>DB: SELECT projects WHERE id=project_id AND tenant_id = <z JWT>
alt projekt neexistuje
BE-->>FE: 400 Bad Request (ISSUE_PROJECT_NOT_FOUND)
end
alt projekt patří jinému tenantu
BE-->>FE: 400 Bad Request (ISSUE_PROJECT_NOT_OWNED)
end
BE->>BE: server-side context enrichment (tenant-{slug} namespace)
BE->>DB: INSERT issues (target=PROJECT, project_id, reporter_user_id, reporter_agent=USER, ...)
BE-->>-FE: 201 Created { id, url, deduplicated: false }
FE->>-User: zobrazí toast "Issue nahlášen (#ID)" s odkazem
Flow 2 — MARA reportuje PROJECT issue z konverzace
sequenceDiagram
actor Mara
Mara->>Mara: detekuje bug v user-app (target=PROJECT dle decision tree krok 1a)
Mara->>Mara: dedup check: search_issues(target=PROJECT, query={symptom})
Note over Mara: Mara NESPECIFIKUJE project — sidecar tool<br/>resolvuje project_id z ctx (předaný do createIssueTrackingMcpServer(ctx))
Mara->>+BE: tool: report_issue(target="PROJECT", kind, title, description, severity?)
Note over BE: POST /api/issues<br/>Authorization: Bearer {agentJWT}<br/>project_id: number (doplnil sidecar z ctx)
BE->>BE: parseReporterAgent() — MARA whitelist OK
BE->>BE: validuj request (target=PROJECT → project_id required as int)
alt target=PROJECT a project_id null
BE-->>Mara: 400 Bad Request (ISSUE_PROJECT_ID_REQUIRED)
end
BE->>DB: SELECT projects WHERE id=project_id AND tenant_id=<z JWT>
alt projekt neexistuje
BE-->>Mara: 400 Bad Request (ISSUE_PROJECT_NOT_FOUND)
end
alt projekt patří jinému tenantu
BE-->>Mara: 400 Bad Request (ISSUE_PROJECT_NOT_OWNED)
end
BE->>BE: server-side context enrichment (tenant-{slug} namespace)
BE->>DB: INSERT issues (target=PROJECT, project_id=<z requestu>, reporter_agent=MARA, ...)
BE-->>-Mara: 201 Created { id, url, deduplicated: false }
Mara-->>User: "Narazila jsem na bug v naší aplikaci. Zaznamenala jsem ho jako TODO issue [#ID](url). Opravím ho v dalším kroku."
Tool API (Mara)
report_issue({
target: "PLATFORM" | "PROJECT",
kind: "BUG" | "FEATURE",
title: string, // krátký popis, max 80 chars
description: string, // markdown, full kontext
severity?: "LOW" | "MEDIUM" | "HIGH" // default MEDIUM; CRITICAL nikdy Mara
}): {
id: string, // UUID issue
url: string, // https://talkide.app link
deduplicated: boolean,
comment_id?: string
}
Žádný project_slug / project_id param. Sidecar (IssueTrackingTools.ts) zná aktivní projekt z ctx argumentu předaného do createIssueTrackingMcpServer(ctx). Při target=PROJECT sidecar resolvuje project_id: number z ctx a přidává ho do POST requestu na BE — Mara o tom neví.
Server enriched (nepředává Mara): tenant_slug, reporter_user_id, reporter_agent=MARA, session_id, context_jsonb. Sidecar enriched (doplňuje sidecar, ne Mara): project_id.
Mara vždy nejdříve volá search_issues(target=PROJECT) — teprve pokud nenajde duplikát, volá report_issue. Viz UC-09002 a UC-09001 — Mara decision tree.
REST API
POST /api/issues
Autentizace: User JWT nebo Agent JWT (Mara). reporter_agent v requestu musí být v whitelist MARA | USER — jiný agent → 403.
Request (USER flow):
{
"target": "PROJECT",
"project_id": 12345,
"reporter_agent": "USER",
"kind": "BUG",
"title": "Přihlašovací formulář nepřijímá speciální znaky",
"description": "Po zadání hesla se speciálními znaky (např. `!@#**User (ze Workspace tabu) nebo Mara (z konverzace přes tool) nahlásí issue spojený s konkrétním projektem (`target=PROJECT`). Oba reporteři zapisují stejný datový shape — liší se zdrojem `project_id` a hodnotou `reporter_agent`. USER posílá `project_id` explicitně z FE route; Mara `project_id` nepředává vůbec — sidecar tool ho resolvuje z ctx automaticky.**
* Phase 3 rozšiřuje Issue API o `target=PROJECT`. Schema bylo připraveno již v Phase 1 (`0019-create-issues-tables.xml`), ale FK constraint `issues.project_id → projects.id` byl omylem vynechán — doplňuje ho nová migrace `0020-add-project-fk-to-issues.xml` (immutable po deploymentu na prod).
* Dva reporteři: **USER** (Studio uživatel kliká v UI) a **MARA** (tool call z konverzace). `reporter_agent` v requestu rozlišuje oba. Phase 1 whitelist `MARA only` je v #91 rozšířen na `MARA | USER`.
* `severity = CRITICAL` Mara nikdy nenastaví — výhradně admin při triage (UC-09004). USER reportér taktéž nemůže nastavit CRITICAL.
* Cascade delete: smazání projektu kaskádově smaže všechny jeho PROJECT issues přes DB FK `ON DELETE CASCADE`. `ProjectCascadeDeleter` žádnou logiku nemění — FK zajistí vše.
* Context enrichment: stejný mechanismus jako UC-09001 (K8s events, pod logy) — pro `target=PROJECT` se dotazuje pouze `tenant-<slug>` namespace (ne `talkide` namespace). Detaily viz [UC-09001 — Auto-context enrichment](uc-09001_report-platform-issue#auto-context-enrichment-server-side).
---
## DB Migration — Phase 3 FK constraint
Nový soubor: `src/main/resources/db/changelog/changes/0020-add-project-fk-to-issues.xml`
**NEEDITUJ existující soubor 0019.** Toto je nový append-only changeset.
```xml
<changeSet id="0020-add-project-fk-to-issues" author="system">
<addForeignKeyConstraint
baseTableName="issues"
baseColumnNames="project_id"
constraintName="fk_issues_project"
referencedTableName="projects"
referencedColumnNames="id"
onDelete="CASCADE"/>
</changeSet>
project_id zůstává nullable (Phase 1 PLATFORM issues nemají projekt — migrace funguje pro obě prostředí).
Flow 1 — USER reportuje PROJECT issue z Workspace tabu
sequenceDiagram
actor User
User->>+FE: klikne "Nahlásit problém" ve Workspace tabu
FE->>FE: otevře modal s formulářem (kind, title, description, severity)
User->>FE: vyplní formulář a odešle
FE->>FE: validuje formulář (kind not null, title not blank max 80, description not blank)
alt formulář nevalidní
FE-->>User: zobrazí chybové hlášky u polí
end
FE->>+BE: POST /api/issues <br> IssueCreateRequest { target="PROJECT", project_id={current}, reporter_agent="USER", kind, title, description, severity? }
Note over FE,BE: Authorization: Bearer {userJWT}
BE->>BE: parseReporterAgent() — whitelist check: MARA | USER
BE->>BE: validuj request (target, kind, title max 80, severity != CRITICAL)
alt target=PROJECT a project_id null
BE-->>FE: 400 Bad Request (ISSUE_PROJECT_ID_REQUIRED)
end
BE->>DB: SELECT projects WHERE id=project_id AND tenant_id = <z JWT>
alt projekt neexistuje
BE-->>FE: 400 Bad Request (ISSUE_PROJECT_NOT_FOUND)
end
alt projekt patří jinému tenantu
BE-->>FE: 400 Bad Request (ISSUE_PROJECT_NOT_OWNED)
end
BE->>BE: server-side context enrichment (tenant-{slug} namespace)
BE->>DB: INSERT issues (target=PROJECT, project_id, reporter_user_id, reporter_agent=USER, ...)
BE-->>-FE: 201 Created { id, url, deduplicated: false }
FE->>-User: zobrazí toast "Issue nahlášen (#ID)" s odkazem
Flow 2 — MARA reportuje PROJECT issue z konverzace
sequenceDiagram
actor Mara
Mara->>Mara: detekuje bug v user-app (target=PROJECT dle decision tree krok 1a)
Mara->>Mara: dedup check: search_issues(target=PROJECT, query={symptom})
Note over Mara: Mara NESPECIFIKUJE project — sidecar tool<br/>resolvuje project_id z ctx (předaný do createIssueTrackingMcpServer(ctx))
Mara->>+BE: tool: report_issue(target="PROJECT", kind, title, description, severity?)
Note over BE: POST /api/issues<br/>Authorization: Bearer {agentJWT}<br/>project_id: number (doplnil sidecar z ctx)
BE->>BE: parseReporterAgent() — MARA whitelist OK
BE->>BE: validuj request (target=PROJECT → project_id required as int)
alt target=PROJECT a project_id null
BE-->>Mara: 400 Bad Request (ISSUE_PROJECT_ID_REQUIRED)
end
BE->>DB: SELECT projects WHERE id=project_id AND tenant_id=<z JWT>
alt projekt neexistuje
BE-->>Mara: 400 Bad Request (ISSUE_PROJECT_NOT_FOUND)
end
alt projekt patří jinému tenantu
BE-->>Mara: 400 Bad Request (ISSUE_PROJECT_NOT_OWNED)
end
BE->>BE: server-side context enrichment (tenant-{slug} namespace)
BE->>DB: INSERT issues (target=PROJECT, project_id=<z requestu>, reporter_agent=MARA, ...)
BE-->>-Mara: 201 Created { id, url, deduplicated: false }
Mara-->>User: "Narazila jsem na bug v naší aplikaci. Zaznamenala jsem ho jako TODO issue [#ID](url). Opravím ho v dalším kroku."
Tool API (Mara)
report_issue({
target: "PLATFORM" | "PROJECT",
kind: "BUG" | "FEATURE",
title: string, // krátký popis, max 80 chars
description: string, // markdown, full kontext
severity?: "LOW" | "MEDIUM" | "HIGH" // default MEDIUM; CRITICAL nikdy Mara
}): {
id: string, // UUID issue
url: string, // https://talkide.app link
deduplicated: boolean,
comment_id?: string
}
Žádný project_slug / project_id param. Sidecar (IssueTrackingTools.ts) zná aktivní projekt z ctx argumentu předaného do createIssueTrackingMcpServer(ctx). Při target=PROJECT sidecar resolvuje project_id: number z ctx a přidává ho do POST requestu na BE — Mara o tom neví.
Server enriched (nepředává Mara): tenant_slug, reporter_user_id, reporter_agent=MARA, session_id, context_jsonb. Sidecar enriched (doplňuje sidecar, ne Mara): project_id.
Mara vždy nejdříve volá search_issues(target=PROJECT) — teprve pokud nenajde duplikát, volá report_issue. Viz UC-09002 a UC-09001 — Mara decision tree.
REST API
POST /api/issues
Autentizace: User JWT nebo Agent JWT (Mara). reporter_agent v requestu musí být v whitelist MARA | USER — jiný agent → 403.
Request (USER flow): ) formulář zobrazí prázdnou error message bez textu.”, “severity”: “MEDIUM” }
**Request (MARA flow — sidecar doplní project_id z ctx, Mara ho nespecifikuje):**
```json
{
"target": "PROJECT",
"project_id": 42,
"reporter_agent": "MARA",
"kind": "BUG",
"title": "SQL query vrací duplicitní řádky při JOIN bez DISTINCT",
"description": "Metoda `UserRepository.findActiveUsers()` vrací duplicitní záznamy kvůli chybějícímu DISTINCT v JPA query.",
"severity": "HIGH"
}
project_idv MARA requestu je integer resolvovaný sidecarem (IssueTrackingTools.ts) z ctx — Mara ho nikdy nevyplňuje manuálně.
201 Created (nový issue):
{
"id": "660f9511-e3ac-42d5-b827-557766551234",
"url": "https://talkide.app/studio/issues/660f9511-e3ac-42d5-b827-557766551234",
"deduplicated": false
}
200 OK (dedup hit — stejný issue v posledních 10 minutách):
{
"id": "660f9511-e3ac-42d5-b827-557766551234",
"url": "https://talkide.app/studio/issues/660f9511-e3ac-42d5-b827-557766551234",
"deduplicated": true,
"comment_id": "771g0622-f4bd-53e6-c938-668877662345"
}
400 Bad Request — target=PROJECT bez project_id (sidecar ho vždy doplní pro MARA; USER flow musí poslat explicitně z FE route):
{
"code": "ISSUE_PROJECT_ID_REQUIRED",
"message": "project_id is required when target=PROJECT"
}
400 Bad Request — validace (title příliš dlouhý, neplatný kind, apod.):
{
"code": "VALIDATION",
"message": "title must not exceed 80 characters"
}
403 Forbidden — severity=CRITICAL nebo nepovolený reporter_agent:
{
"code": "FORBIDDEN",
"message": "CRITICAL severity can only be set by admin during triage"
}
400 Bad Request — projekt neexistuje nebo patří jinému tenantu:
{
"code": "ISSUE_PROJECT_NOT_FOUND",
"message": "Project not found"
}
{
"code": "ISSUE_PROJECT_NOT_OWNED",
"message": "Project does not belong to the current tenant"
}
Cross-tenant attack prevention: oba kódy (
ISSUE_PROJECT_NOT_FOUNDiISSUE_PROJECT_NOT_OWNED) vrací 400 Bad Request (ne 403/404), aby nešlo enumerovat existenci projektů cizích tenantů. BE záměrně nesděluje, zda projekt neexistuje nebo existuje, ale patří jinému tenantu.
Frontend
Workspace tab — “Nahlásit problém” modal
Trigger: Tlačítko “Nahlásit problém” v záhlaví Issues tabu v Workspace.
Formulář:
| Pole | Typ | Popis |
|---|---|---|
| Kind | select | BUG / FEATURE — required |
| Title | text input | Krátký popis, required |
| Description | textarea | Markdown, required |
| Severity | select | LOW / MEDIUM / HIGH — optional, default MEDIUM |
Po odeslání FE přidá do requestu target=PROJECT, project_id=<aktuální projekt z routy>, reporter_agent=USER.
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
kind | not_null | — | — | Výběr ze select; prázdná volba nepřípustná |
title | not_blank | 1 - 80 | — | Trim whitespace |
description | not_blank | — | — | Markdown; no size limit na FE |
severity | nullable | — | — | Default MEDIUM pokud nevybráno |
Backend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
target | not_blank, in(PLATFORM, PROJECT) | — | — | |
project_id | required pokud target=PROJECT | integer | — | USER: FE posílá z route; MARA: sidecar doplňuje z ctx; chybí → ISSUE_PROJECT_ID_REQUIRED |
| tenant ownership | projekt musí patřit tenant_id z JWT | — | — | ISSUE_PROJECT_NOT_OWNED při cross-tenant pokusu |
reporter_agent | not_blank, in(MARA, USER) | — | — | Phase 3 whitelist; jiný agent → 403 |
kind | not_blank, in(BUG, FEATURE) | — | — | INCIDENT → 400 |
title | not_blank, max 80 chars | 1 - 80 | — | Trim whitespace |
description | not_blank | — | — | |
severity | nullable, default MEDIUM, in(LOW, MEDIUM, HIGH) | — | — | CRITICAL → 403 |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| Přihlášený user, existující vlastní projekt | POST /api/issues s target=PROJECT, project_id=<vlastní int>, reporter_agent=USER, kind=BUG, title, description | 201 Created; issue vytvořen s reporter_agent=USER, status=OPEN, severity=MEDIUM |
| Mara má platný agent JWT, sidecar doplnil project_id z ctx | POST /api/issues s target=PROJECT, project_id=<int z ctx>, reporter_agent=MARA, kind=BUG, title, description | 201 Created; issue vytvořen s reporter_agent=MARA |
target=PROJECT ale chybí project_id (FE bug nebo sidecar selhal) | POST /api/issues | 400 Bad Request, code=ISSUE_PROJECT_ID_REQUIRED |
project_id odkazuje na projekt jiného tenantu | POST /api/issues s cizím project_id | 400 Bad Request, code=ISSUE_PROJECT_NOT_OWNED |
project_id odkazuje na neexistující projekt | POST /api/issues s neexistujícím project_id (integer, např. 99999) | 400 Bad Request, code=ISSUE_PROJECT_NOT_FOUND |
reporter_agent=KAI (není v whitelist) | POST /api/issues | 403 Forbidden |
severity=CRITICAL (jakýkoliv reporter) | POST /api/issues s severity=CRITICAL | 403 Forbidden, code=FORBIDDEN |
title delší než 80 znaků | POST /api/issues | 400 Bad Request, code=VALIDATION |
| Projekt smazán (CASCADE) | DELETE /api/projects/{id} → dotaz na issues WHERE project_id=id | DB vrátí prázdný výsledek; issues smazány kaskádou přes FK ON DELETE CASCADE |
Stejný (tenant_slug, PROJECT, title hash) v posledních 10 min | POST /api/issues se stejným titulem | 200 OK, deduplicated=true; přidán komentář; nový issue nevznikne |
References
- UC-09001 Report Platform Issue — PLATFORM varianta; dedup logika; Mara decision tree; context enrichment detail
- UC-09002 Search Issues — Mara dedup check před reportem;
target=PROJECTfiltr - UC-09006 View Project Issues — zobrazení nahlášených PROJECT issues
- UC-09007 Mara Injects Open Issues — OPEN PROJECT issues injektovány do CLAUDE.md
FEEDBACK
Zadání bylo výborně strukturované — dual reporter flow, error kódy i cascade behavior byly popsány konkrétně. Ambiguita project_slug="current" byla vyřešena: Mara žádný project_slug ani project_id nespecifikuje — sidecar (IssueTrackingTools.ts) resolvuje project_id: number z ctx argumentu předaného do createIssueTrackingMcpServer(ctx) a přidává ho do POST requestu transparentně. Tool schema proto neobsahuje project_slug ani project_id jako parametry pro Maru.
Thanks for the feedback.