# Közös minták — platform-szintű alapréteg (v0.4, munkapéldány)

**Dokumentum-típus:** platform-szintű referencia (nem feature-spec)
**Státusz:** munkadokumentum — mind a nyolc szakasz átnézésre kész
**Verzió:** v0.4
**Dátum:** 2026.05.19
**Cél olvasó:** senior fejlesztő + Claude Code
**Épít:** `project_backend` CLAUDE.md, `project_backend_client_angular`
CLAUDE.md, `02_architekturalis_kerdeslista.md` (fejlesztői válaszok),
`kanonikus_donek.md` (K-008, K-024, K-036)

> **Mire való ez a fájl.** Ez a Specifikációs projekt **alaprétege**: a
> platform-szintű minták, amelyekre minden feature-spec hivatkozik, hogy ne
> kelljen ismételni. Egy feature-spec nem írja le újra a tenant-resolution
> működését vagy az API-konvenciókat — **ide hivatkozik** (pl. "lásd
> `01_kozos_mintak.md` 1.3 — tenant-resolution").
>
> **Mi NEM ez.** Nem feature-spec, és nem írja újra a `CLAUDE.md` mintákat —
> azokra **hivatkozik**. Ahol a platform-modell eltér a `CLAUDE.md`-től, azt
> explicit **"Eltérés a `CLAUDE.md`-től:"** jelölés kíséri.
>
> **A döntések jelölése.** Ez a fájl több platform-szintű döntést rögzít. Ezek
> **specifikációs döntések (SD-xxx)** — elkülönítve a funkcionális projekt
> kanonikus állításaitól (K-xxx). Az SD-döntések teljes szövege a
> `99_donesnaplo.md`-ben; itt a döntés és a rövid indok áll.

---

## 0. Olvasási térkép

| Szakasz | Tárgy | Státusz |
|---|---|---|
| 1 | Multi-tenancy — a kétszintű DB-modell | ✓ kész, átnézésre |
| 2 | Tenant-resolution a kérésfolyamatban | ✓ kész, átnézésre |
| 3 | Autentikáció és jogosultság (3.5: a jogosultság-érvényesítés platform-mintája) | ✓ kész, átnézésre |
| 4 | Kategória-katalógus — core-default és tenant-másolat | ✓ kész, átnézésre |
| 5 | Időzóna és lokalizáció | ✓ kész, átnézésre |
| 6 | API-konvenciók | ✓ kész, átnézésre |
| 7 | Audit és naplózás | ✓ kész, átnézésre |
| 8 | A specifikációs döntésnapló | ✓ kész, átnézésre |

Mind a nyolc szakasz kidolgozva. A kísérő `99_donesnaplo.md` ezzel
párhuzamosan jött létre — a teljes SD-1 — SD-10 készlettel.

---

## 1. Multi-tenancy — a kétszintű DB-modell

### 1.1 A modell egy mondatban

> **SD-1 — Kétszintű, fizikailag elkülönített multi-tenancy: egy Core DB és
> tenantonként egy-egy Tenant DB. A tenant-azonosítás a `Tenant` HTTP request
> headerből történik. A tenant DB minden működési lekérdezéshez önellátó; a
> Core DB-ből csak egy szűk, csak-olvasható adatkör replikálódik be.**

A pilot egyetlen tenanttal indul (Balatonalmádi), de a modell az első naptól
multi-tenant — K-008 ("multi-tenant fegyelem az első naptól").

### 1.2 Eltérés a `CLAUDE.md`-től — kimondva

**Eltérés a `project_backend` CLAUDE.md-től:** a `CLAUDE.md` jelenleg egyetlen
`urbino_core` adatbázist ír le, és kijelenti, hogy "there are no tenant
subdomains". Ez a leírás **elavult**. Az `02_architekturalis_kerdeslista.md`
fejlesztői válaszai a **DB-per-tenant** modellt erősítik meg ("sok db, ahol
indokolt külön db szerver"; "tenant szinten" backup; per-request scope
tenant-aware factory). A `01_kozos_mintak.md` jelen szakasza ezt a felülírást
formálisan rögzíti.

**Amit a `CLAUDE.md`-ből megtartunk** (továbbra is érvényes): a `Tenant` HTTP
request header mint a tenant-azonosítás csatornája; a route-alapú jogosultság
`authorization.json`-ban; a `tenant_`-prefixes szerepkör-névkonvenció; a
snake_case DB / camelCase JSON.

> **Teendő (fejlesztői):** a `project_backend` CLAUDE.md "Tenant context" és
> "Key Configuration" szakaszát frissíteni kell, hogy ne az egy-`urbino_core`
> modellt írja le. Ez nem a specifikáció dolga — visszajelzés a kódbázis
> gazdájának. *(Döntésnapló: SD-1.)*

### 1.3 A két DB-szint — mi hol él

**Core DB** — tenant-független, központi adatok. Egyetlen példány.

| Adatkör | Tartalom | Megjegyzés |
|---|---|---|
| `User` | A globális felhasználói fiók: név, e-mail, jelszó-hash (a Zitadel kezeli), globális fiók-állapot | A teljes identitás itt él |
| `Tenant` | A tenant-regisztrum: minden tenant metaadata és DB-kapcsolata | Lásd 1.6 |
| `UserTenantRole` | A `User` ↔ `Tenant` kötés, tenant-szintű szerepkörrel | Egy user több tenanthoz, tenantonként saját szerepkör-halmazzal |
| `DefaultCategoryCatalog` | Az Urbino által termékesített default kategória-fa | Lásd 4. szakasz |

**Tenant DB** — tenantonként egy fizikai adatbázis, a tenant teljes operatív
adata. A pilotra egy ilyen DB (Balatonalmádi).

| Adatkör | Tartalom | Megjegyzés |
|---|---|---|
| `Ticket` | A bejelentések — a modul gerince | A `Ticket`-spec fejti ki |
| `Category` | A tenant kategória-fája (a default katalógus másolata + tenant-egyedi) | Lásd 4. szakasz |
| `Group` | Terepi munkacsoportok | `50_konfig` 3. |
| `TenantUser` | A Core `User` szűk, csak-olvasható projekciója | Lásd 1.4 — ez a modell kulcsa |
| `ActivityLog`, `Attachment`, `Notification`-célok, riport-adatok | A bejelentés-gerinc kísérő-entitásai | A vonatkozó feature-spec fejti ki |

A pontos entitás-mezők a `00_domain_model.md`-be kerülnek; itt csak a
**DB-szint hovatartozás** rögzül.

### 1.4 A core ↔ tenant híd — a `TenantUser`-projekció

A nehéz pont a két DB **metszete**: amikor egy tenant-művelet Core-adatra
hivatkozik — tipikusan a `User`-re (ki a felelős, ki triage-elt, ki a
bejelentő). Ha ezt cross-DB join oldaná meg, minden bejelentés-lista két
adatbázist kérdezne.

**A megoldás — replikáció a tenant DB-be, nem cross-DB olvasás.**

> **SD-2 — A Core DB-ből a tenant DB-be egy szűk, csak-olvasható `TenantUser`
> projekció replikálódik. A tenant-oldali entitások (felelős, naplózó, stb.)
> erre a `TenantUser`-re mutatnak FK-val, nem a Core `User`-re. A tenant DB így
> minden működési lekérdezéshez önellátó — cross-DB join a működési útvonalon
> nincs.**

A `TenantUser` **nem** a teljes `User` másolata — csak annyi, amennyit a
tenant-oldali működés igényel:

```
TenantUser
  coreUserId    – a Core User.id (a forrás-hivatkozás)
  displayName   – megjelenítendő név (triage felelős-választó, activity-log)
  email         – kapcsolatfelvételhez, azonosításhoz
  roles[]       – a tenant-szintű szerepkörök ezen a tenanton
  isActive      – aktív-e ezen a tenanton
```

A pontos mezők a `00_domain_model.md`-ben véglegesülnek. A lényeg: a triage
felelős-választójához (`10` 3.3), a tevékenység-naplóhoz és a Dashboard
csapat-táblájához (`40` 3.2) egy **név + szerepkör** kell — nem a teljes fiók.

**Miért ez a jó kompromisszum.** A cross-DB join elkerülése a működési
útvonalon (bejelentés-lista, adatlap, Dashboard) jelentős egyszerűsítés: a
`BaseController` AutoMapper-projekciói és az `[ManyToManyConnection]`-kötések
a tenant DB-n belül maradnak, a referenciális integritás a tenant DB-n belül
érvényesül. Az ár egy szinkron-mechanizmus — de az egyirányú és konfliktusmentes
(lásd lent), így olcsó.

### 1.5 Mi syncelődik és mi nem — a szinkron-elv

A fejlesztői iránymutatás: a `User`-eket azonnal syncelni kell, de lesznek
adatok, amelyeket nem. Ebből az elv:

> **SD-3 — A Core → Tenant szinkron egyirányú, és a Core a "source of truth".
> A `User`/`UserTenantRole` változás **azonnal**, a Core-oldali mentés
> tranzakciójához kötve propagál a `TenantUser`-be. A default kategória-katalógus
> ezzel szemben csak **tenant-provisioning-időben** másolódik (lásd 4.). Más
> Core-adat nem replikálódik — a tenant DB azt cross-DB módon olvassa, ha
> egyáltalán kell.**

A "mi azonnal, mi nem" eldöntésének elve — **a változás gyakorisága és az
elvárt konzisztencia dönt**:

| Core-adat | Szinkron | Indok |
|---|---|---|
| `User` alapadat (név, e-mail) | **Azonnal**, a mentés tranzakciójában | Béla felvesz egy felhasználót, és az **rögtön** kiosztható kell legyen a triage-ben — késleltetett sync zavaró lenne |
| `UserTenantRole` (szerepkör-hozzárendelés) | **Azonnal**, a mentés tranzakciójában | A jogosultság nem lehet "késésben" — egy frissen kiosztott szerepkörnek azonnal érvényesülnie kell |
| Default kategória-katalógus | **Provisioning-időben**, egyszer | A katalógus ritkán változik; egy új tenant felvételekor másolódik be. A *futás közbeni* frissítés-propagálás elhalasztva (lásd 4.4) |
| Egyéb Core-adat | Nem replikálódik | Ha egy tenant-művelet kivételesen Core-adatra szorul, azt eseti cross-DB olvasással éri el — nem a működési útvonalon |

**A szinkron egyirányúsága fontos.** A `TenantUser` a tenant-oldalon
**soha nem szerkeszthető**. A felhasználó-CRUD (`/beallitasok/felhasznalok`,
`50_konfig` 4.) a Core `User`-t és a `UserTenantRole`-t módosítja, és onnan
**propagál** a tenant DB(k) `TenantUser` táblájába. Mivel a tenant-oldal csak
olvas, **nincs konfliktus-feloldás** (last-write-wins, divergencia) — a
`TenantUser` mindig a Core hű másolata.

**A szinkron technikai megvalósítása** (event-driven outbox, közvetlen
`AfterSaveAsync`-propagálás, vagy reconciliation-job) tisztán fejlesztői
döntés. A specifikáció annyit köt ki, hogy a `User`/`UserTenantRole`-sync a
Core-oldali mentés **tranzakciójához kötött** legyen — azaz vagy a mentés és a
propagálás is sikerül, vagy egyik sem. Ezzel a tenant DB sosem lát "félig
mentett" felhasználót.

> **Tervezési döntés — nyitva hagyott technikai részlet.** A
> tranzakció-határon átnyúló sync (a Core DB és a tenant DB két külön fizikai
> adatbázis — egy elosztott tranzakció nem triviális) konkrét mintája a
> fejlesztői műhely döntése, a `99_donesnaplo.md`-be kerül. A specifikáció
> elvárása: a `TenantUser` **eventuális, de rövid késleltetésű** konzisztenciája
> elfogadható, ha a sync hibája naplózott és újrapróbálható — de a felhasználó
> *létrehozásának* visszajelzése (a `/beallitasok/felhasznalok` mentés sikere)
> ne függjön a propagálás befejeződésétől, nehogy egy tenant DB lassúsága a
> Core-műveletet blokkolja. Ez egy **valódi fejlesztői döntés** — itt jelezzük,
> a műhely zárja le.

### 1.6 A `Tenant`-regisztrum séma

A Core DB `Tenant` táblája a regisztrum. A pilot-minimum séma:

> **SD-4 — A `Tenant`-regisztrum pilot-sémája.**

```
Tenant
  id              – belső azonosító (PK)
  code            – a tenant rövid kódja, pl. "almadi" — ez megy a Tenant headerben
  name            – megjelenítendő név, pl. "Balatonalmádi Városgondnokság"
  dbConnectionRef – a tenant DB elérése (kapcsolat-azonosító, nem nyers
                    connection string a táblában — lásd biztonsági megjegyzés)
  status          – a tenant életciklus-állapota (lásd lent)
  timeZone        – a tenant megjelenítési időzónája, alap: "Europe/Budapest"
  createdAt / createdBy / updatedAt / updatedBy – AuditableEntity
```

A `code` a kulcs: ez érkezik a `Tenant` HTTP headerben (pl. `Tenant: almadi`),
és ez a `tenant_`-prefixes Zitadel-szerepkör suffixe is (lásd 3.).

**A `dbConnectionRef`-ről.** A nyers connection string (jelszóval) **nem** a
`Tenant` táblában tárolódik — a tábla csak egy hivatkozást tart (pl. egy
konfig-kulcs vagy egy secret-store referencia), és a tényleges connection
string a futtatókörnyezet titok-kezelőjéből jön. Ezt a `00_domain_model.md`
nem modellezi mezőként; itt a biztonsági elv rögzül. *(A pontos minta —
appsettings, environment, secret manager — fejlesztői döntés.)*

**A `status` értékkészlete:**

> **SD-5 — A `Tenant.status` négy értéket vesz fel: `Setup`, `Active`,
> `Suspended`, `Archived`.**

| Status | Jelentés | Hatás a működésre |
|---|---|---|
| `Setup` | A tenant DB létrejött, de a tenant még nem éles (provisioning, alapadat-betöltés zajlik) | Az API a `Setup`-állapotú tenant felé **nem szolgál ki** üzleti kérést — csak az Urbino-admin férhet hozzá. A pilot-tenant a T0 előtt ebben az állapotban van |
| `Active` | Éles, normál működés | Teljes kiszolgálás |
| `Suspended` | Ideiglenesen felfüggesztve (pl. fizetési ok) | Az API 403-at ad a tenant-kérésekre; az adat megmarad. A felfüggesztés az Urbino-admin hatásköre (K-024) |
| `Archived` | Lezárt tenant (a szerződés véget ért) | Csak olvasható / nem elérhető — az adatmegőrzési politika szerint. Pilotra nem releváns, de a modell elbírja |

A pilotra gyakorlatilag a `Setup` → `Active` átmenet él; a `Suspended` és
`Archived` az Urbino-admin későbbi modulja, de a `status`-mező az első naptól
mind a négy értéket ismeri, hogy ne kelljen később migrálni.

**Tenant-provisioning.** A kérdéslista 2. pontjának fejlesztői válasza:
**kézi DBA-folyamat**, nincs self-service provisioning a pilotra. Vagyis új
tenant felvétele = a DBA létrehozza a tenant DB-t, lefuttatja a migrációkat,
betölti az alapadatot (köztük a default kategória-katalógust — lásd 4.), és
felveszi a `Tenant`-rekordot. A specifikáció ezt **nem automatizálja**; a
pilotnak egy tenant kell, és az kézzel áll fel.

### 1.7 Cross-tenant lekérdezés — hibatípus

A K-008 multi-tenant fegyelem technikai következménye: **egy tenant
felhasználója soha nem láthat más tenant adatát.** A modell ezt
**szerkezetileg** garantálja — a tenant DB-k fizikailag külön vannak, és egy
kérés a tenant-resolution (2. szakasz) után **egyetlen** tenant DB-hez
kapcsolódik. Egy `Ticket`-lekérdezés szó szerint nem tud másik tenant
bejelentését visszaadni, mert az másik adatbázisban van.

**Ami mégis hibalehetőség:** ha a tenant-resolution elromlik, vagy a felhasználó
olyan tenant `code`-ot küld a headerben, amelyhez nincs jogosultsága. Ezt a
3. szakasz (jogosultság) zárja le: a JWT a felhasználó tenant-jogosultságait
hordozza, és a resolution ellenőrzi, hogy a kért tenant `code` szerepel-e
köztük. Ha nem — **403, és a kísérlet naplózott**.

> **Tervezési döntés — a cross-tenant kísérlet mint naplózandó esemény.** A
> "rossz tenant headert küldött egy user, akinek nincs joga hozzá" eset nem
> csendben elnyelt 403, hanem **naplózott biztonsági esemény** — mert vagy
> hibás kliens, vagy támadási kísérlet. A naplózás részletei a 7. szakaszba
> (audit) kerülnek; itt az elv rögzül.

---

## 2. Tenant-resolution a kérésfolyamatban

### 2.1 A resolution helye — middleware

A kérdéslista 1. pontjának fejlesztői válaszai egyértelműek, és ez a szakasz
ezeket formalizálja:

> **SD-6 — A tenant-resolution ASP.NET Core **middleware**-ben történik, minden
> bejövő HTTP-kérésnél. A DI per-request scope-ban ad ki tenant-specifikus
> `DbContext`-et egy tenant-aware factory-n keresztül. A `BaseController` a
> tenant-resolutionről **nem tud** — transzparens.**

A kérés-útvonal:

```
HTTP kérés
   │  Tenant: almadi   (header)
   │  Authorization: Bearer <JWT>
   ▼
[1] Auth middleware        – a JWT validálása, a claim-ek kinyerése
   ▼
[2] Tenant-resolution middleware
       – kiolvassa a Tenant headert ("almadi")
       – ellenőrzi: a JWT tenant-jogosultságai közt van-e "almadi"
       – ha nincs → 403 + naplózás (1.7)
       – ha van → a tenant code-ot a request-scope-ba teszi
       – feloldja a Tenant-regisztrumból a tenant DB-kapcsolatot
       – ellenőrzi a Tenant.status-t (Setup/Suspended → elutasítás)
   ▼
[3] DI: tenant-aware DbContext factory
       – a request-scope tenant code alapján a megfelelő
         tenant DB-re mutató DbContext-et ad ki
   ▼
[4] Controller (BaseController<...>)
       – a befecskendezett DbContext már a helyes tenant DB-re mutat
       – a controller-kód nem tartalmaz tenant-logikát
```

### 2.2 A `BaseController` transzparenciája — eltérés nincs

Fontos, hogy ez **nem eltérés a `CLAUDE.md` mintától**: a `BaseController`
ugyanúgy működik, mint az "education" domain-ben — kap egy `DbContext`-et,
és dolgozik vele. A különbség annyi, hogy a befecskendezett `DbContext` a
tenant-aware factory-tól jön, és a helyes tenant DB-re mutat. A
feature-fejlesztő (és a Claude Code) a tenant-resolutionnel **nem foglalkozik**
— a controllerek és a `IFeatureEntityConfig`-ok úgy íródnak, mintha egyetlen
adatbázis lenne.

**Következmény a feature-specekre:** egy feature-spec a "Multi-tenancy
érvényesítés" szakaszában (a sablon 3. szakasza) tipikusan ennyit ír:
*"Standard tenant-resolution a `01_kozos_mintak.md` 2. szerint — a `Ticket`
entitás a tenant DB-ben él, a `DbContext` a resolution után a helyes
tenant DB-re mutat. Eltérés nincs."* Csak az **eltérést** kell részletezni —
pl. ha egy végpont kivételesen Core-adatot is olvas (cross-DB), azt explicit
jelezni kell.

### 2.3 A polgári mobilapp tenant-resolutionje — jelzés

A polgári mobilapp tenant-felismerése **eltér** az admin felületétől: a
mobilapp előbb a Core API-t hívja egy város-listáért (`GET /cities` vagy
hasonló), majd a választott tenant felé fordul. A kérdéslista 8. pontja
DNS-szintű feloldást említ (`api-<ident>.urbino.io`).

**Ezt itt nem fejtjük ki**, mert a polgári mobilapp funkcionális tervezése
még nem zárult le (a manager felület átadása után következik), és a
specifikáció nem pótolja a funkcionális tervezést. Amikor a polgári
adatigény-specre kerül a sor, a tenant-resolution polgári vetülete ott
záródik. **Nyitott kérdésként jelölve** — a `01_kozos_mintak.md` jelen
verziója a **manager felület és a szerver** tenant-resolutionjét rögzíti
teljesen; a polgári oldal a polgári adatigény-spec dolga.

> A lényeg, ami már most biztos: a *szerver-oldali* tenant-resolution (a
> 2.1 middleware-lánc) **közös** — a `Tenant` header és a JWT-jogosultság
> ellenőrzése ugyanaz, akárhonnan jön a kérés. Csak az különbözik, hogyan
> jut el a kliens a helyes `Tenant` `code`-hoz: az admin felület fix
> aldomain + tenant-választó UI-ból (kérdéslista 3.), a polgári app a
> város-listából.

---

## 3. Autentikáció és jogosultság

### 3.1 Autentikáció — Zitadel JWT, eltérés nincs

Az autentikáció a meglévő minta: **Zitadel OIDC**, JWT bearer token. A
`CLAUDE.md`-k leírják (szerver: `RoleClaimsTransformation`; admin felület:
`angular-oauth2-oidc`, `AuthService.authState` signal). A specifikáció ezt
**nem írja újra** — hivatkozik rá.

A polgári mobilapp autentikációs mintája (e-mail+jelszó, magic link, social
login — a kérdéslista 4. pontja "Később eldöntött"-et ír) **nyitott**, és a
polgári adatigény-spec dolga. Itt csak az admin felület + szerver auth rögzül,
ami a meglévő Zitadel-minta.

### 3.2 A tenant-szintű szerepkör-modell

A `05_jogosultsagok_v2.md` négy szerepkört rögzít funkcionálisan: diszpécser,
vezető, tartalomkezelő, terepi dolgozó (átmeneti). A specifikáció ehhez a
**kódbeli leképezést** adja:

> **SD-7 — A tenant-szerepkörök kódbeli nevei: `dispatcher`, `manager`,
> `content_manager`, `field_worker`. A teljes Zitadel-szerepkör a `CLAUDE.md`
> `tenant_<role>_<code>` mintáját követi — pl. `tenant_dispatcher_almadi`,
> `tenant_manager_almadi`.**

| Funkcionális név (`05_jogosultsagok`) | Kódbeli szerepkör | Teljes Zitadel-szerepkör (példa) |
|---|---|---|
| Diszpécser | `dispatcher` | `tenant_dispatcher_almadi` |
| Vezető | `manager` | `tenant_manager_almadi` |
| Tartalomkezelő | `content_manager` | `tenant_content_manager_almadi` |
| Terepi dolgozó (átmeneti) | `field_worker` | `tenant_field_worker_almadi` |

**Megjegyzés a "manager" névhez.** A `CLAUDE.md` példája `tenant_manager_praga`
— a `manager` szó ott a tenant-adminisztrátor értelmében áll. Nálunk a
`manager` a **vezető** szerepkör (Béla), ami a `05_jogosultsagok` szerint a
beolvadt tenant-admin jogkört is hordozza — tehát a névválasztás konzisztens
a `CLAUDE.md`-vel. Ha a fejlesztők a `manager` helyett `tenant_admin`-t vagy
mást preferálnak, ez egy egy soros csere; jelezzétek.

**A szerepkörök a `UserTenantRole`-ban és a JWT-ben is.** A Core
`UserTenantRole` tárolja, hogy egy `User` melyik tenanton milyen
szerepkör(öke)t birtokol. A bejelentkezéskor ezek a Zitadel JWT
`urn:zitadel:iam:org:project:roles` claim-jébe kerülnek — a felhasználó
**összes** tenant-jogosultsága a tokenben. A K-025 implicit unió-modell így
természetesen működik: ha egy felhasználó `tenant_dispatcher_almadi` **és**
`tenant_content_manager_almadi` is, mindkettő a tokenben van, és a felület a
kettő unióját mutatja.

> **Eltérés a `CLAUDE.md`-től — pontosítás, nem ellentmondás.** A `CLAUDE.md`
> a tenant-szerepkört a JWT-ben feltételezi. A `02_kerdeslista` 3. pontja
> kétféle tárolást is említ ("Core DB-ben kötési táblában ÉS minden tenant
> DB-jében redundánsan"). Ezt a `01_kozos_mintak.md` így oldja fel: az
> **igazság forrása** a Core `UserTenantRole`; a JWT ennek a leképezése
> bejelentkezéskor; a tenant DB-beli `TenantUser.roles[]` ennek a replikája
> (SD-2/SD-3 szerint, a tenant-oldali lekérdezésekhez — pl. a Dashboard
> csapat-táblájához, ami szerepkör szerint szűr). Három helyen jelenik meg
> ugyanaz az adat, de **egy forrásból** — a Core `UserTenantRole`-ból.

### 3.3 Az `authorization.json` és a tenant-prefix

A route-alapú jogosultság a meglévő minta: az `authorization.json` HTTP-method
+ route-mintát köt szerepkörhöz. A `CLAUDE.md` szerint a tenant-szerepköröknél
**csak a prefixet** írjuk a szabályba (pl. `tenant_dispatcher`), és a
middleware futásidőben fűzi hozzá a `Tenant` header értékét
(`tenant_dispatcher_almadi`), majd ellenőrzi.

A specifikáció ezt **megtartja, eltérés nincs**. Egy feature-spec a
jogosultság-szakaszában az `authorization.json`-bejegyzéseket prefix-formában
adja meg, pl.:

```jsonc
// példa-bejegyzés egy bejelentés-végponthoz (a Ticket-spec véglegesíti)
{ "method": "POST", "path": "/v1/tickets/{id}/assign",
  "roles": ["tenant_dispatcher", "tenant_manager"] }
```

A `field_worker` itt szándékosan nincs a listán — a terepi dolgozó nem
triage-el (`05_jogosultsagok` 2.2). A pontos permission-mátrixot
végpontonként a feature-specek adják; a `01_kozos_mintak.md` csak a
**konvenciót** rögzíti: prefix-forma, a middleware oldja fel.

### 3.4 Permission-kulcs vs. szerepkör — a pilot durva szemcséje

A `05_jogosultsagok` és a K-016 explicit: a pilotra **szerepkör-szintű** a
jogosultság, nincs mezőnkénti vagy finomszemcsés permission. Ezért a pilotra
**nincs külön permission-kulcs réteg** — a szerepkör maga a jogosultság, és az
`authorization.json` közvetlenül szerepkörre szabályoz.

A modell viszont úgy épül, hogy egy **új szerepkör** később egy új sor a
mátrixban / egy új érték az `authorization.json`-ban (a `05_jogosultsagok` 5.
"a kibővítés helye megmarad" elve). Finomszemcsés permission-réteg a 6-12.
hónap dolga — a pilot-specifikáció nem vezeti be.

> **Tervezési döntés.** Megfontoltuk egy explicit permission-kulcs absztrakció
> bevezetését (szerepkör → permission-kulcsok → route) már a pilotra, hogy a
> későbbi finomítás olcsóbb legyen. **Elvetve:** a K-016 a fölösleges
> absztrakció ellen szól, és a route-alapú `authorization.json` szerepkör-szinten
> is teljesen működik. Az új szerepkör hozzáadása enélkül is csak egy oszlop a
> mátrixban. Ha a finomszemcsés jogosultság a 6-12. hónapban valóban kell, az
> akkor egy önálló, jól körülírt refaktor — nem kell a pilotot terhelni vele.
> *(Döntésnapló: SD — permission-réteg elhalasztva.)*

### 3.5 A jogosultság-érvényesítés platform-mintája

A `05_jogosultsagok_v2.md` akció-szintű mátrixa **mit szabad kinek** kérdésre
felel — funkcionális szinten. Ez az alszakasz a **hogyan érvényesül**
kérdést rögzíti platform-szinten: minden feature-spec erre hivatkozik a saját
jogosultság-szakaszában, ahelyett hogy a mechanikát újra leírná. A
felhasználókezelés feature-spec
(`20_admin_felulet/20_felhasznalokezeles.md`) emelte ide ezt a réteget.

**A két érvényesítési réteg.** A jogosultság a pilotban két szinten dől el:

1. **Route-szintű érvényesítés (durva szűrő).** Az `authorization.json`
   HTTP-method + route-mintát köt szerepkörhöz (3.3). Ez dönti el, hogy egy
   adott szerepkör egy végpontot *egyáltalán* hívhat-e. A legtöbb akció itt
   eldől teljesen.
2. **Objektum-szintű szűrés (finom szűrő).** Ahol a `05_jogosultsagok` mátrixa
   `(saját)` jelölést használ — tipikusan a terepi dolgozó „csak a hozzá
   rendelt bejelentést látja" —, ott a route-engedély önmagában nem elég: a
   végpont a visszaadott adathalmazt a kérő felhasználóra szűri. Ezt nem az
   `authorization.json` adja, hanem a feature-spec által leírt
   lekérdezés-feltétel (a bejelentés-feature ezt a maga területén már
   rögzítette; a minta általánosítható).

**A szerepkör-prefix és a tenant.** Az `authorization.json` a
tenant-szerepköröknél csak a **prefixet** írja a szabályba (`tenant_dispatcher`,
`tenant_manager`, `tenant_content_manager`, `tenant_field_worker`), és a
middleware futásidőben fűzi hozzá az aktív `Tenant` header kódját
(`tenant_manager_almadi`), majd ellenőrzi a JWT szerepkör-claimje ellen (3.3).
Egy adott tenant JWT-jével érkező kérés egy *másik* tenant `Tenant`
headerével a hibakód-keret (6.3) szerint elutasításra kerül (`403`/`404`), és
a cross-tenant kísérlet naplózódik (7.3).

**A `vezető ⊇ diszpécser ÉS ⊇ tartalomkezelő` reláció — felsorolással.** A
`05_jogosultsagok` szerint a `manager` mindent elér, amit a `dispatcher` és a
`content_manager`. Az `authorization.json` ezt **felsorolással** képezi le:
minden diszpécser- és tartalomkezelő-route engedélyezett szerepkör-listájában a
`manager` prefix is szerepel. Nincs szerepkör-öröklődés a route-szabályban — a
meglévő `authorization.json`-minta route→szerepkör-listát ismer, öröklődést
nem; a felsorolás illeszkedik a mintába, és a `05_jogosultsagok` mátrixa amúgy
is explicit, így a felsorolás gépiesen generálható belőle.

> **Tervezési döntés — felsorolás, nem öröklődés.** Megfontolható lenne egy
> szerepkör-hierarchia bevezetése (`manager` örökli a `dispatcher` jogait),
> hogy a `manager` ne ismétlődjön sok route-on. **Elvetve:** a meglévő
> `authorization.json`-minta nem ismer hierarchiát, és a bevezetése a
> route-kiértékelő logika átírását kívánná. A felsorolás ára — a `manager`
> sok helyen ismétlődik — elhanyagolható, mert a teljes lista a
> `05_jogosultsagok` mátrixából determinisztikusan származtatható. *(A
> felhasználókezelés feature-spec rögzíti — lásd ott.)*

**A `TenantRole` enum.** A négy tenant-szerepkör kódbeli neve `dispatcher`,
`manager`, `content_manager`, `field_worker` (SD-7); a `00_domain_model.md`
`TenantRole` enumként rögzíti. Egy új szerepkör (a 6-12. hónapra tervezett
moderátor, polgármester, önálló tenant-admin) egy új enum-érték és új
`authorization.json`-bejegyzések — architektúra-újratervezés nélkül, a
`05_jogosultsagok` 5. „a kibővítés helye megmarad" elve szerint.

**Token-érvényesség és szerepkör-változás.** A JWT a `UserTenantRole`
*bejelentkezéskori* leképezése (3.2). Ha egy szerepkört a token élettartama
alatt visszavonnak, a token a lejártáig még a régi szerepköröket hordozza — a
pilotra ezt **eventual consistency**-ként elfogadjuk: rövid token-élettartammal
a rés percekben mérhető. Aktív token-invalidálás (a token azonnali
érvénytelenítése szerepkör-változáskor) iterációs elem, nem a pilot része.

---

## 4. Kategória-katalógus — core-default és tenant-másolat

### 4.1 A probléma

A kategória **modulközi és kétszintű DB-érintettségű** objektum: a triage
egyik döntése ráépül (K-031), a polgári app bejelentés-flow-ja is ezt használja
(`50_konfig` 2.), és kétszintű fa (K-036 — gyökér=terméktulajdon,
alkategória=tenant-tulajdon). A kérdés: hol él, és hogyan viszonyul a Core-beli
default-katalógus a tenant kategória-fájához.

### 4.2 A modell — provisioning-időben másolt, követhető hivatkozással

> **SD-8 — A default kategória-katalógus a Core DB-ben él
> (`DefaultCategoryCatalog`). Tenant-provisioning-időben **lemásolódik** a
> tenant DB `Category` táblájába. Minden másolt rekord megtartja a
> `sourceCatalogId` hivatkozást a Core-beli forrásra. A tenant-oldali
> működés (triage, polgári flow) **kizárólag a tenant DB `Category` tábláját**
> kérdezi — cross-DB nélkül.**

Ez a kérdéslista 5. pontjának három vázolt opciója közül a **hibrid** —
és konzisztens az SD-2/SD-3 elvvel (a tenant DB önellátó):

| Opció | Miért nem | / Miért igen |
|---|---|---|
| Tenant DB üres, a Core-ból olvas | Minden kategória-olvasás cross-DB — a triage-ben és a polgári listában is. Ellentmond az SD-2 önellátó-elvnek | ✗ |
| Provisioning-időben másol, független élet | Tiszta lekérdezés, de elveszik a "melyik a rendszer-default" információ | részben |
| **Hibrid: másol + `sourceCatalogId`** | Tiszta lekérdezés **és** követhető marad, melyik kategória termékesített és melyik tenant-egyedi | ✓ |

A `sourceCatalogId` azért kell, mert a K-036 termék/tenant tulajdon-határa
**futásidőben** érvényesül: a vezető a `/beallitasok/kategoriak` oldalon
**átnevezheti és deaktiválhatja** a gyökér-kategóriákat és kezelheti az
alkategóriákat, de **szabad szöveges új gyökeret nem hozhat létre** — új
gyökér csak a Core-default készletből jöhet. Ahhoz, hogy a felület tudja,
melyik gyökér "termékesített" (és így védett a szabad szerkesztéstől) és
melyik tenant-egyedi, a `Category` rekordnak ismernie kell a forrását — ezt
adja a `sourceCatalogId`. (A `sourceCatalogId` `null` egy esetleges
tenant-egyedi alkategórián, és kitöltött egy default-ból másolt rekordon.)

A `Category` entitás pontos mezői (a kétszintű fa szülő-hivatkozása, az
`isActive`, a sorrend, az ikon stb.) a `00_domain_model.md`-be kerülnek; itt
a **DB-szint és a forrás-hivatkozás elve** rögzül.

### 4.3 Kétszintű fa — a duplikáció-heurisztika feltétele

A kategória-fa **kétszintű**: gyökér + alkategória (K-036). Ez nem
ízlés-kérdés: a duplikáció-szűrés (K-032) **gyökér-szintű kategória-egyezésre**
épül (`20` 2.2). Ha a kategória-modell lapos lenne, a K-032 heurisztika
gyökér-egyezés-szabálya értelmezhetetlen volna. A két döntés
összekapcsolódik — a kétszintű fa a duplikáció-szűrés előfeltétele.

> Az `02_architekturalis_kerdeslista.md` 5. pontja "most csak lapos" listát
> említ — ez a dokumentum a kanonikus érlelés (K-036, 2026.05.14) **előtti**,
> és elavult. A kétszintű fa az érvényes — megerősítve a felmérő körben.

### 4.4 Mit halaszt el ez a modell

A katalógus **futás közbeni frissítésének propagálása** élő tenantokba — azaz
amikor az Urbino-csapat egy új default gyökér-kategóriát termékesít, és az
megjelenik a már működő tenantokban — **nincs a pilot-scope-ban**. A
kérdéslista 5. pontja maga is "kitalálandó még"-et ír erre.

A pilotra a provisioning-időben másolt katalógus elég (egy tenant, a T0 előtt
felállítva). A "core-default változott, frissítsük a tenantokat" mechanizmus a
6-12. hónap és az Urbino-admin modul dolga.

> **Nyitott kérdés — átadva.** A default-katalógus frissítés-propagálása az
> **Urbino-admin** funkcionális tervezésének kérdése (K-024 — az Urbino-csapat
> super-admin felülete). A `01_kozos_mintak.md` itt csak rögzíti, hogy a
> pilot-modell ezt nem fedi, és a `sourceCatalogId` hivatkozás **megléte** a
> későbbi propagálást technikailag lehetővé teszi (tudható, melyik tenant-rekord
> melyik default-ból származik) — de a propagálás *folyamatát* nem építjük meg
> most.

---

## 5. Időzóna és lokalizáció

### 5.1 Tárolás — UTC mindig

> **SD-9 — Minden időpont-adat a DB-ben UTC-ben tárolódik, PostgreSQL
> `timestamp with time zone` (`timestamptz`) oszloptípussal. A szerver minden
> belső számítást és összehasonlítást UTC-ben végez. A tenant-időzónára
> konvertálás kizárólag megjelenítéskor történik.**

A kérdéslista 6. pontjának fejlesztői válasza explicit: "with timezone". Ez
mind a Core, mind a tenant DB-kre érvényes. A `CLAUDE.md` az EFCore
snake_case névkonvenciót már rögzíti — a `timestamptz` ezzel kompatibilis.

**Következmény a feature-specekre.** Egy entitás dátum-mezője (pl.
`Ticket.createdAt`, `Ticket.dueDate`) a domain-modellben `DateTime`/`DateTimeOffset`
típusú, és a leírás mindig hozzáteszi: "UTC-ben tárolva". A
megjelenítés-konverzió **nem** az entitás dolga.

### 5.2 Megjelenítés — tenant-időzóna szerint

> **SD-10 — A megjelenítési időzóna **tenant-szintű**, a `Tenant.timeZone`
> mezőből (SD-4), alapérték `Europe/Budapest`. Nem a felhasználó eszközének
> időzónáját használjuk.**

A kérdéslista 6. pontja itt is egyértelmű: "Tenant szintű" megjelenítés. Az
indok terméktani: a városgazdálkodás egy földrajzi helyhez kötött — Anna, Béla
és a terepi csapat ugyanabban a városban dolgozik. Egy bejelentés "06:12-kor
érkezett" — ez a *város* ideje, nem a böngészőt épp külföldről nyitó
felhasználóé. A tenant-időzóna konzisztens élményt ad: mindenki ugyanazt az
órát látja.

**Hol történik a konverzió.** Két út lehetséges, és ezt **fejlesztői
döntésként** hagyom nyitva, mert tisztán technikai (nincs üzleti vetülete):
vagy a szerver konvertál a válasz összeállításakor (a DTO már tenant-helyi
időt ad), vagy a szerver UTC-t ad és az admin felület konvertál. A `40_riport`
(Dashboard "ma"/"ez a hét" definíció, `40` 3.5) **megköveteli**, hogy a
"naptári nap" és a "hét" határa egységes legyen — ezt a határ-számítást a
**szerver** végzi tenant-időzónában (mert a Dashboard-aggregátum és a heti
PDF ugyanazt a hét-definíciót kell használja, `40` 3.5). Az egyedi időpont-mezők
megjelenítés-konverziójának helye viszont fejlesztői döntés.

> **Tervezési döntés — a hét/nap határ a szerveren.** A "ma" és "ez a hét"
> nem megjelenítési formázás, hanem **lekérdezési határ** (mely ügyek esnek a
> mai napra). Ezért a szerver számítja, tenant-időzónában: a "ma" a tenant
> `Europe/Budapest` szerinti 00:00–24:00, UTC-re visszaszámolva a lekérdezéshez.
> A magyar hét hétfővel kezdődik (`40` 3.5). Ezt a `Ticket`-aggregációs spec
> és a riport-spec részletezi; itt az elv rögzül. *(Döntésnapló: SD-10
> hivatkozza.)*

### 5.3 i18n — kulcs-alapú, magyar, eltérés nincs

Az i18n a meglévő minta: `@ngx-translate`, statikus loader, minden szöveg a
`src/i18n/hu.json`-ban, a `TableStateConfig.translationPrefix` auto-generálja
a lista-fejléceket. A specifikáció ezt **megtartja, eltérés nincs**.

Az elv, amit minden feature-spec követ: **UI-szöveg sosem hardkódolt** — a
spec a mező-szintű részletezésnél megadja az i18n-kulcsot (a mezősablon
"i18n kulcs" sora), és a kulcs a `hu.json`-ba kerül. A feature-spec 4. szakasza
("Admin felület") tartalmaz egy "i18n kulcsok" alszakaszt.

**Többnyelvűség — pilot után.** A kérdéslista 6. pontja: "pilot után lesz csak
többnyelvű". A pilotra **csak magyar** (`hu.json`). De a kulcs-alapú szerkezet
az első naptól adott — a többnyelvűség később egy új fordításfájl
(`en.json`) hozzáadása, nem refaktor. A specifikáció nem tervez nyelvváltó
UI-t a pilotra.

> **Eltérés nincs, de egy elhatárolás.** A nemzetközi expanzió (nyelv/valuta
> tenant-szinten — kérdéslista 6.) kifejezetten pilot-scope-on kívül. A
> `Tenant.timeZone` mező (SD-4) már most tenant-szintű, tehát a modell a
> tenant-szintű lokalizációt elbírja — de a pilotra egyetlen időzóna
> (`Europe/Budapest`) és egyetlen nyelv él.

### 5.4 Hangvétel — tegezés, globális elv

> **A teljes Urbino-felület — az admin felület és a polgári mobilapp egyaránt —
> tegez.** Minden felhasználónak szóló szöveg (`hu.json`, gomb-feliratok,
> hibaüzenetek, üres-állapotok, értesítések) tegező formában íródik.

Indok: a polgári Flutter-app a meglévő képernyőkön végig tegez (a felhasználói
élmény közvetlen, barátságos), és az admin felületnek konzisztensnek kell
lennie vele — a kettő ugyanannak a terméknek a két felülete. A városgazdálkodási
szakember tegezése sem szokatlan egy modern munkaeszközben (Linear, Asana, Jira
mind tegez magyar lokalizációban). A feature-specek a `hu.json`-kulcsok
szövegezésénél ezt az elvet követik — nem feature-szintű döntés.

### 5.5 Hibaüzenet-szöveg — a szerver kulcsot ad, nem kész szöveget

> **A nem-mező hibák (`409`, `422`) hibatestjében a szerver kódot/`reason`-t
> ad, nem kész magyar szöveget — a felhasználónak szóló üzenetet az admin
> felület i18n-rétege fogalmazza a `hu.json`-ból.**

Indok: a hibaüzenet ugyanúgy UI-szöveg, mint bármi más — a kulcs-alapú elv
(5.3) rá is vonatkozik. Ha a szerver kész magyar szöveget adna, az (a)
hardkódolt UI-szöveg lenne a szerveren, (b) megakadályozná a későbbi
többnyelvűséget, (c) kettős igazságforrást teremtene. A szerver tehát stabil,
nyelvfüggetlen hibakódot/`reason`-t ad; a kliens ezt fordítja.

**Kivétel — a mezőszintű `fieldErrors`.** A `400`-as validációs hiba a meglévő
`fieldErrors` mintát követi (6.3): a `[validationForm]` + `<form-field>`
infrastruktúra a `fieldErrors` mezőnkénti tartalmát automatikusan megjeleníti.
A `fieldErrors` hibakulcsot hordoz, amit a `hu.json` fordít — tehát itt is
kulcs-alapú, csak a megjelenítés-mechanizmus a meglévő minta.

> Ez a két elv (5.4 tegezés, 5.5 szerver-kulcs) az első feature-spec
> (`20_admin_felulet/10_bejelentes_lista_es_adatlap.md`) validációs köréből
> emelkedett platform-szintre — minden további feature-spec követi.

---

## 6. API-konvenciók

### 6.1 Route, formátum, verziózás — a meglévő minta

A meglévő szerver-minta, amit a specifikáció **megtart**:

- **Route-prefix:** `/v1/...` — minden végpont verziózott. REST-stílus.
- **JSON:** camelCase a kérésben és válaszban; a DB snake_case (EFCore
  névkonvenció). A szerializáció Newtonsoft.Json, strict number handling
  (a `CLAUDE.md` szerint).
- **Verziózás:** a `/v1/` major-verzió. Új major (`/v2/`) csak **breaking
  change**-nél; visszafelé kompatibilis bővítés (új mező, új végpont) a
  `/v1/`-en belül marad. A pilot teljes egészében `/v1/`.

A feature-specek a végpontokat `/v1/`-prefixszel adják meg, és csak az
eltérést jelölik (lásd 6.4).

### 6.2 Paginálás — offset-alapú

> **SD-11 — A lista-végpontok **offset-alapú** paginálást használnak, a
> meglévő `ListRequest` mintán keresztül. Nincs cursor-alapú paginálás a
> pilotra.**

A kérdéslista 7. pontjának fejlesztői válasza: "Offset based". Az indok a
pilot-skálán működik: egy 10.000 fős város bejelentés-volumene (a brief
sikermutatója szerint ~100 bejelentés/hó) **nem** nagy adathalmaz — az offset
lassulása csak több százezres nagyságrendnél kezd számítani. A `ListRequest`
+ `FilterQuery` minta (`CLAUDE.md`) ezt készen adja: a `BaseController`
`ListAsync()` offset-lapozott, szűrhető, rendezhető listát ad.

A `TableStateConfig` admin-oldali párja (`CLAUDE.md`) az URL query-paramba
kódolja a lapozást, szűrést, rendezést — a feature-specek erre a mintára
hivatkoznak, nem írják le újra.

> 6-12. hó: ha egy nagyobb tenant ügy-volumene az offsetet érezhetően
> lassítja, a cursor-paginálás akkor egy lokalizált optimalizáció a
> lista-végpontokon — nem érinti az API-szerződést kívülről. A pilotot nem
> terheljük vele. *(Nem önálló SD — pilot-megfigyelési pont.)*

### 6.3 Hibakezelés — egyedi séma, státuszkód + JSON

> **SD-12 — A hibaválasz egyedi sémát követ: HTTP státuszkód + JSON
> hibatest. **Nem** RFC 7807 (Problem Details). A 400-as validációs hiba a
> meglévő `fieldErrors` szerkezetet adja, amit az admin felület
> `ValidationInterceptor` → `FormErrorService` lánca automatikusan megjelenít.**

A kérdéslista 7. pontjának fejlesztői válasza: "egyedi. státuszkód + json
válasz". A meglévő minta egy pontját a `CLAUDE.md` már rögzíti: a HTTP 400
`fieldErrors` mezőt ad, és az admin felület `[validationForm]` +
`<form-field>` infrastruktúrája ezt **feature-kód nélkül** megjeleníti
mező-szinten.

A hiba-státuszkódok használatának kerete, amit a feature-specek követnek:

| Státusz | Mikor | A feature-spec ezt így jelöli |
|---|---|---|
| `400` | Validációs hiba — a `fieldErrors` szerkezettel | A FluentValidation-szabályok adják; a spec a validáció-szakaszban felsorolja a szabályokat |
| `401` | Hiányzó/érvénytelen JWT | Platform-szintű, a feature-spec nem foglalkozik vele |
| `403` | Hiányzó jogosultság — szerepkör vagy rossz tenant (1.7) | Az `authorization.json`-bejegyzés adja; a spec jogosultság-szakasza hivatkozza |
| `404` | Nem létező erőforrás (vagy: másik tenant erőforrása — lásd lent) | Standard `BaseController`-viselkedés |
| `409` | Állapot-konfliktus — pl. tiltott státuszátmenet, vagy optimistic-update ütközés | A `Ticket`-spec részletezi (a státusz-állapotgép és az inline-szerkesztés) |
| `422` | Üzleti szabály sérül (a kérés formailag jó, de a művelet nem megengedett) | A workflow-szabályoknál — a feature-spec jelöli, ha használja |

> **Tervezési döntés — 404 vs. 403 a tenant-határon.** Ha egy felhasználó
> egy *másik tenant* erőforrásának ID-jét kéri, a fizikai DB-szeparáció miatt
> az adat egyszerűen **nincs** abban a tenant DB-ben, amelyhez a kérés
> kapcsolódott — tehát a természetes válasz `404`. Ez **biztonságilag is
> helyes**: nem áruljuk el, hogy az ID egy másik tenantnál létezik. A `403`
> a *rossz tenant header* esetére van fenntartva (a user olyan tenantot
> kért, amihez nincs joga — 1.7). A két eset szétválik: rossz tenant header
> → `403` + naplózás; jó tenant header, de idegen ID → `404`. *(Döntésnapló:
> SD-12 hivatkozza.)*

A `409` és `422` közti határ a pilotra: a **státusz-állapotgép** tiltott
átmenete és az **optimistic-update ütközés** `409` (a `Ticket`-spec
véglegesíti); az egyéb üzleti szabály-sértés `422`. Ezt a `Ticket`-spec és a
státusz-állapotgép-spec élesíti — itt a keret rögzül.

### 6.4 A "csak az eltérést specifikáld" elv — formalizálva

A meglévő `BaseController` és `TableStateConfig` minták teljes CRUD-ot és
teljes lista/űrlap-infrastruktúrát adnak. A feature-spec ezt **nem írja le
újra**. A formális szabály:

**Szerver-oldal.** Ha egy entitás a `BaseController` mintát követi, a
feature-spec API-szakasza ennyit ír: *"Standard CRUD a `BaseController`
szerint."* Részletezni **csak** ezt kell:
- egyedi `BeforeSaveAsync` / `AfterSaveAsync` logika;
- nem-standard végpontok (route, method, request, response, hibakódok);
- workflow- és business-szabályok.

Minden eltérés explicit **"Eltérés a mintától:"** jelölést kap, indoklással.
Jelölés hiányában a Claude Code joggal feltételezi, hogy a standard minta
érvényes.

**Admin felület.** Ha egy lista-oldal a `TableStateConfig` mintát követi, a
spec a **`TableStateConfig` vázlatot** adja (oszlopok, szűrők, akciók) — nem
írja le a pagináció vagy az URL-kódolás működését. Ha egy űrlap a
`[validationForm]` mintát követi, a mezőket a mezősablon szerint adja meg —
de a hibamegjelenítés mechanikáját nem.

> Ez nem új döntés — az instrukció "csak az eltérést specifikáld" elvének a
> formalizálása, hogy a feature-specek konzisztensen alkalmazzák. A
> `01_kozos_mintak.md` itt **referencia-pontot** ad: a feature-spec
> hivatkozhat ide ahelyett, hogy az elvet újra leírná.

---

## 7. Audit és naplózás

### 7.1 Az `AuditableEntity` — a meglévő minta

A meglévő minta: az `AuditableEntity` bázisosztály a `CreatedAt`, `CreatedBy`,
`UpdatedAt`, `UpdatedBy` mezőket adja, és az `AppDbContext` automatikusan
tölti a JWT subject claim alapján (`CLAUDE.md`). A specifikáció ezt
**megtartja**: minden tenant-entitás, amelynek érdemi életciklusa van
(`Ticket`, `Category`, `Group`, hír, program, városi információ) az
`AuditableEntity`-ből származik.

**Egy pontosítás a `CreatedBy`-hoz a multi-tenant kontextusban.** A
`CreatedBy` a JWT subject — ez a **Core `User`** azonosítója. A tenant DB-ben
viszont a `TenantUser`-re hivatkozunk (SD-2). A megfeleltetés: a `CreatedBy`
a Core `User.id`-t tárolja (ahogy a `CLAUDE.md` leírja), és a tenant-oldali
megjelenítéskor a `TenantUser.coreUserId` kötésen keresztül oldódik fel névre.
A kérdéslista 1. pontja ezt "Nem eldöntött"-ként hagyta — a `01_kozos_mintak.md`
így zárja le: a `CreatedBy`/`UpdatedBy` a Core `User.id`, a `TenantUser`
adja hozzá a megjelenítendő nevet. A `00_domain_model.md` ezt mezőszinten
rögzíti.

### 7.2 Mi az, amit az `AuditableEntity` NEM fed

Az `AuditableEntity` csak a **"ki és mikor hozta létre / módosította utoljára"**
kérdést válaszolja meg — egy rekord aktuális állapotát. Nem fedi:

- **A változás-történetet** — mit, miről mire módosítottak (pl. a `Ticket`
  státusz-átmenetei, a felelős-váltások sora).
- **Az esemény-jellegű naplót** — pl. "ekkor kiosztották", "ekkor lezárták".

A `10_triage_flow` (4.x, 6.1) explicit megköveteli a **tevékenység-naplót**
minden státuszváltásról és felelős-váltásról, és a `90_sitemap` a
bejelentés-adatlap egyik elemeként rögzíti.

> **Tervezési döntés — a tevékenység-napló külön entitás.** A `Ticket`
> tevékenység-naplója (`ActivityLog`) **nem** az `AuditableEntity`-re épül —
> az csak az utolsó módosítást tudja. A tevékenység-napló egy önálló,
> append-only entitás a tenant DB-ben: minden státuszváltás, felelős-váltás,
> elutasítás, visszanyitás, összevonás egy-egy `ActivityLog`-rekord. A
> **pontos szerkezetét a `Ticket`-spec / a státusz-állapotgép-spec fejti ki**
> — itt csak az elv rögzül: a `Ticket` életciklus-eseményei append-only
> naplóba kerülnek, a `00_domain_model.md`-ben modellezve. *(Döntésnapló:
> SD — tevékenység-napló mint külön entitás, a `Ticket`-spec részletezi.)*

A `00_architektura` 6.2 és a `10_triage_flow` 6.2 egyaránt kimondja: a
**rendszerszintű audit-modul** (minden entitás minden változása, kereshető
audit-felület) a pilotra **nincs** — a `Ticket`-szintű tevékenység-napló
elég. Ezt a specifikáció tartja: nincs globális audit-trail a pilotra.

### 7.3 Biztonsági naplózás — a cross-tenant kísérlet

Az 1.7 rögzítette: ha egy felhasználó olyan `Tenant` headert küld, amelyhez
nincs jogosultsága, az **naplózandó biztonsági esemény**, nem csendben
elnyelt 403.

> **SD-13 — A cross-tenant hozzáférési kísérlet (rossz/jogosulatlan `Tenant`
> header) naplózott biztonsági esemény: a kérés időpontja, a felhasználó
> Core `User.id`-ja, a kért tenant `code`, és a tényleges jogosultságok
> rögzülnek. A naplózás nem blokkolja a választ — a kérés `403`-at kap (1.7),
> a naplózás mellette történik.**

A naplózás **technikai csatornája** (alkalmazás-log, dedikált biztonsági
log-tábla, külső log-aggregátor) fejlesztői döntés — a kérdéslista 11. pontja
("Application logging: milyen szintű") ezt nyitva hagyta. A specifikáció
annyit köt ki, hogy az esemény **azonosítható és visszakereshető** legyen.

> **Nyitott kérdés.** Az általános application-logging stratégia (Serilog
> vs. built-in, hová gyűlik, tenant-id minden eseménynél) a kérdéslista 11.
> pontjában nyitott, és **nem feltétlenül a specifikáció dolga** — inkább
> üzemeltetési/fejlesztői döntés. A `01_kozos_mintak.md` csak azt köti ki,
> hogy a tenant-id (a `Tenant` `code`) **minden naplóeseményben** szerepeljen
> — mert egy multi-tenant rendszerben a tenant-szűrhető log alapkövetelmény.
> A részletes log-stratégia a fejlesztői műhely / üzemeltetés hatásköre.

### 7.4 Event-driven gondolkodás — keret, nem kötelezettség

A projekt-instrukció "event-driven gondolkodást" említ az auditálhatósághoz.
A pilotra ezt **elvként** tartjuk, nem infrastruktúraként: a `Ticket`
életciklus-eseményei (státuszváltás, kiosztás, lezárás) **fogalmilag
események**, és a tevékenység-napló ezeket rögzíti. De a pilotra **nincs**
event-bus, message-queue, event-sourcing — az állapot az entitásban él, a
napló append-only kísérő.

> A `00_architektura` és a `10_triage_flow` is jelzi, hogy a státuszváltások
> **értesítéseket** váltanak ki (push a polgárnak, értesítés a felelősnek).
> Ezek kiváltása a pilotra lehet közvetlen (a `Ticket`-művelet
> `AfterSaveAsync`-je indítja a notification-t) — nem igényel event-bus
> infrastruktúrát. A `Ticket`-spec és a polgári adatigény-spec részletezi az
> értesítés-mechanizmust; a `01_kozos_mintak.md` itt csak elhatárol: a
> pilot event-driven *gondolkodású*, de nem event-driven *infrastruktúrájú*.

---

## 8. A specifikációs döntésnapló

### 8.1 A `99_donesnaplo.md` szerepe és szerkezete

A kísérő `99_donesnaplo.md` a Specifikációs projekt **döntés-emlékezete**:
minden lényeges termék- vagy technikai döntés rögzítve, hogy a Claude Code és
a fejlesztő egy helyen lássa, mi miért dőlt el. A `01_kozos_mintak.md`-vel
párhuzamosan jött létre, és tartalmazza a felmérő kör Ü-1—Ü-6 döntéseit és a
jelen dokumentum SD-1—SD-13 döntéseit.

Egy döntésnapló-bejegyzés szerkezete: **azonosító** (SD-xxx), **dátum**, a
**döntés** szövege, az **indok**, a **mérlegelt alternatíva**, és **ki
döntötte el** (az alapító vagy a specifikáció). Ez illeszkedik az instrukció
"99_donesnaplo.md" előírásához.

### 8.2 SD vs. K — a két döntés-réteg elhatárolása

Fontos, hogy a két azonosító-rendszer **ne keveredjen**:

| | **K-xxx — kanonikus döntés** | **SD-xxx — specifikációs döntés** |
|---|---|---|
| **Hol születik** | A stratégiai / funkcionális projektben | A Specifikációs projektben |
| **Mit rögzít** | Termék- és funkció-szintű, "nem vitatható újra" állítás | Implementációs / platform-szintű döntés |
| **Hol él** | `kanonikus_donek.md` | `99_donesnaplo.md` |
| **Módosítás** | Csak a stratégiai/funkcionális projekt nyithatja újra | A Specifikációs projekt kezeli |

Ha egy specifikációs döntésnek **kanonikus súlya** lenne (terméket érintő,
örök érvényű), az **nem** lesz magától K-xxx — a Specifikációs projekt
**visszacsorgatja** a funkcionális projektnek, és ott emelik kanonikussá. Erre
a felmérő körben volt példa: az Ü-3 (a "Triage kész — Kiosztás" gomb modellje)
megerősített **specifikációs döntés** (SD-ként rögzítve), és ha a funkcionális
projekt kanonikussá emeli, ott kap K-számot. A specifikáció nem oszt ki
K-számot magának.

### 8.3 A felmérő kör döntései a naplóban

A `99_donesnaplo.md` első blokkja a felmérő kör hat tisztázó döntése
(Ü-1—Ü-6) — köztük a multi-tenancy modell (Ü-1, ami az SD-1 alapja), a
`VarosApp_Projekt_Brief.md` elavult-státusza (Ü-2), a "Triage kész" gomb
megerősítése (Ü-3), a téves összevonás kézi korrekciója (Ü-4), a kétszintű
kategória-fa megerősítése (Ü-5), és a meghívó-flow / nincs-offline döntés
(Ü-6). Ezek után következnek a jelen dokumentum SD-1—SD-13 döntései.

---

## Specifikációs döntések ebben a dokumentumban (SD-1 — SD-13)

A teljes szöveg a `99_donesnaplo.md`-ben; itt az összefoglaló:

| SD | Döntés | Szakasz |
|---|---|---|
| **SD-1** | Kétszintű multi-tenancy: Core DB + DB-per-tenant, `Tenant` header-resolution. Felülírja a `project_backend` CLAUDE.md egy-`urbino_core` modelljét | 1.1–1.2 |
| **SD-2** | Szűk, csak-olvasható `TenantUser`-projekció a tenant DB-ben; a tenant-entitások erre mutatnak FK-val; nincs cross-DB join a működési útvonalon | 1.4 |
| **SD-3** | A Core→Tenant szinkron egyirányú, a Core a source of truth; `User`/`UserTenantRole` azonnal (tranzakcióhoz kötve), a kategória-katalógus provisioning-időben | 1.5 |
| **SD-4** | A `Tenant`-regisztrum pilot-sémája (`code`, `name`, `dbConnectionRef`, `status`, `timeZone`, audit) | 1.6 |
| **SD-5** | A `Tenant.status` négy értéke: `Setup`, `Active`, `Suspended`, `Archived` | 1.6 |
| **SD-6** | Tenant-resolution middleware-ben; per-request tenant-aware `DbContext`-factory; a `BaseController` transzparens | 2.1 |
| **SD-7** | A tenant-szerepkörök kódbeli nevei: `dispatcher`, `manager`, `content_manager`, `field_worker`; teljes Zitadel-szerepkör `tenant_<role>_<code>` | 3.2 |
| **SD-8** | A default kategória-katalógus provisioning-időben másolódik a tenant DB-be, `sourceCatalogId` forrás-hivatkozással (hibrid modell) | 4.2 |
| **SD-9** | Minden időpont UTC-ben tárolva, `timestamptz` típussal | 5.1 |
| **SD-10** | A megjelenítési időzóna tenant-szintű (`Tenant.timeZone`, alap `Europe/Budapest`); a nap/hét-határt a szerver számítja tenant-időzónában | 5.2 |
| **SD-11** | Offset-alapú paginálás a `ListRequest` mintán; nincs cursor-paginálás a pilotra | 6.2 |
| **SD-12** | Egyedi hibasséma (státuszkód + JSON, nem RFC 7807); a státuszkód-keret; 404 idegen tenant-ID-ra, 403 rossz tenant headerre | 6.3 |
| **SD-13** | A cross-tenant hozzáférési kísérlet naplózott biztonsági esemény | 7.3 |

Két további döntés, szöveg nélkül, csak jelölve (a `99_donesnaplo.md`-ben
teljes szöveggel): **a finomszemcsés permission-réteg elhalasztva** a 6-12.
hónapra (3.4); **a `Ticket` tevékenység-naplója külön, append-only entitás**,
a `Ticket`-spec részletezi (7.2).

---

## Nyitott kérdések — a dokumentum végén

1. **A Core→Tenant sync technikai mintája** (event-driven outbox / közvetlen
   propagálás / reconciliation-job) — fejlesztői műhely-döntés. A specifikáció
   elvárása rögzítve (1.5): a sync tranzakcióhoz kötött, a Core-művelet nem
   blokkolódik a tenant DB-re.
2. **A megjelenítési időzóna-konverzió helye** (szerver-oldali DTO vs.
   admin felület) — fejlesztői döntés; a nap/hét-határ viszont biztosan a
   szerveren (5.2).
3. **A polgári mobilapp tenant-resolutionje** (város-lista → tenant API) — a
   polgári adatigény-spec dolga, a polgári mobilapp funkcionális tervezése
   után (2.3).
4. **A `manager` szerepkör-név** megfelel-e, vagy `tenant_admin`/más preferált
   — egy soros csere, fejlesztői visszajelzést kér (3.2).
5. **A default-katalógus frissítés-propagálása** élő tenantokba — az
   Urbino-admin modul kérdése, pilot-scope-on kívül (4.4).
6. **Az általános application-logging stratégia** (Serilog vs. built-in, hová
   gyűlik) — üzemeltetési/fejlesztői döntés; a specifikáció csak a tenant-id
   kötelező jelenlétét írja elő a logban (7.3).

---

## Hivatkozott dokumentumok

- `project_backend` CLAUDE.md — a szerver meglévő mintái (a tenant-szakasz
  felülírva, lásd 1.2)
- `project_backend_client_angular` CLAUDE.md — az admin felület meglévő mintái
- `02_architekturalis_kerdeslista.md` — a fejlesztői műhely válaszai
- `kanonikus_donek.md` — K-008 (multi-tenant fegyelem), K-016 (pilot-scope),
  K-024 (Urbino-admin), K-025 (implicit unió-modell), K-031 (triage négy
  döntése), K-032 (duplikáció-heurisztika), K-036 (kétszintű kategória-fa)
- `05_jogosultsagok_v2.md` — a négy szerepkör funkcionális definíciója
- `10_triage_flow_v1.md` — a tevékenység-napló követelménye (7.2)
- `40_riport_v1.md` — a nap/hét-definíció követelménye (5.2)
- `99_donesnaplo.md` — a teljes SD-döntéslista és a felmérő kör döntései

---

## Verziónapló

- **v0.1 (2026.05.18)** — Első munkapéldány. Az 1–4. szakasz (multi-tenancy,
  tenant-resolution, auth/jogosultság, kategória-katalógus) teljes mélységben;
  nyolc specifikációs döntés (SD-1 — SD-8). Az 5–8. szakasz vázlat.
- **v0.2 (2026.05.18)** — Az 5–8. szakasz kidolgozva: időzóna/lokalizáció
  (SD-9, SD-10), API-konvenciók (SD-11, SD-12), audit/naplózás (SD-13), és a
  döntésnapló szerkezete (SD vs. K elhatárolás). Összesen tizenhárom
  specifikációs döntés (SD-1 — SD-13) plusz két jelölt döntés. A
  `99_donesnaplo.md` ezzel párhuzamosan létrejött.
- **v0.3 (2026.05.18)** — Az 5. szakasz (időzóna és lokalizáció) két új
  alszakasszal bővült, az első feature-spec validációs köréből platform-szintre
  emelve: 5.4 — a teljes Urbino-felület **tegez** (globális hangvétel-elv);
  5.5 — a hibaüzenet-szöveg kulcs-alapú, a szerver kódot/`reason`-t ad, nem
  kész magyar szöveget. Mindkettő minden további feature-spec számára kötelező
  elv. Új SD-számot nem hoz — a két elv a meglévő i18n-elv (5.3) kiterjesztése.
- **v0.4 (2026.05.19)** — A 3. szakasz új 3.5 alszakasszal bővült: a
  jogosultság-érvényesítés platform-mintája — a kétrétegű érvényesítés
  (route-szintű `authorization.json` + objektum-szintű `(saját)`-szűrés), a
  szerepkör-prefix és a tenant viszonya, a `vezető ⊇ diszpécser ÉS
  tartalomkezelő` reláció felsorolásos leképezése, a `TenantRole` enum és a
  token-érvényesség kérdése. A felhasználókezelés feature-spec
  (`20_admin_felulet/20_felhasznalokezeles.md` v1.0) emelte ide ezt a
  feature-független réteget. Új SD-számot nem hoz — a meglévő 3.2–3.4
  szerepkör-modell általánosítása; a kapcsolódó döntések (SD-7, SD-36, SD-38)
  a `99_donesnaplo.md`-ben élnek.
