# 60_tartalom — Tartalomkezelés (Hírek, Programok, Városi információk)

**Modul:** Admin felület → a polgári app "puha" tartalmának kezelése
**Fájl:** `20_admin_felulet/60_tartalom.md`
**Verzió:** v1.0
**Dátum:** 2026-05-20
**Státusz:** implementálható
**Cél olvasó:** senior fejlesztő + Claude Code
**Épít:** `60_tartalom_v2.md` (funkcionális), `00_architektura_v4.md`,
`05_jogosultsagok_v2.md`, `00_domain_model.md` v1.7, `01_kozos_mintak.md`,
`30_beallitasok.md` (VF-K1 forrása), a két `CLAUDE.md`

---

## 0. Funkcionális alap

### 0.1 A feature azonosítása

A `60_tartalom` feature **három admin-aloldalt** fed le, közös szerepkörrel
(tartalomkezelő + vezető) és közös életciklussal (`Draft` / `Published` /
`Archived`):

| Aloldal | Route | Entitás |
|---|---|---|
| Hírek | `/tartalom/hirek` | `News` |
| Városi programok | `/tartalom/varosi_programok` | `Event` |
| Városi információk | `/tartalom/varosi_informaciok` | `CityInfo` |

> **A route-mintázat eltérése a funkcionális dokumentumtól.** A
> `60_tartalom_v2.md` 3.1 a programok route-ját `/tartalom/programok`-ként,
> a városi információkat `/tartalom/varosi-informaciok`-ként rögzítette. A
> spec az aláhúzás-mintázatot teszi kanonikussá
> (`/tartalom/varosi_programok`, `/tartalom/varosi_informaciok`), és a kis
> eltérést **VF-T1 visszacsorgó jelzésként** dokumentálja a funkcionális
> projektnek (lásd 8.).

### 0.2 Funkcionális forrás-dokumentumok

**Elsődleges forrás:**
- `60_tartalom_v2.md` (2026.05.14) — a tartalomkezelő szerepkör (2.), a
  három tartalomtípus mezőkészlete (3.2-3.4), kétállapotú életciklus
  funkcionálisan (3.5), tartalom-szövegmező igénye (3.6), pilot-scope (5.).

**Másodlagos kapcsolódó funkcionális dokumentumok:**
- `00_architektura_v4.md` — 2.1 tartalomkezelő szerepkör; 3.2 navigáció
  (Tartalom lenyíló almenü); 3.3 szerepkör-érzékeny kezdő-nézet (K-027).
- `05_jogosultsagok_v2.md` — 2.9 (Tartalomkezelés) akció-szintű mátrix.
- `90_sitemap_v3.md` — modul-sitemap.

### 0.3 Mit döntött már el a funkcionális réteg (rövid hivatkozás)

- **Három tartalomtípus, három aloldal, lenyíló Tartalom almenü.**
- **Szerepkör:** `content_manager`; `manager ⊇ content_manager` (K-038).
- **A három tartalom tenant-tulajdon**; a GYIK Urbino-hatáskör (K-039).
- **Kétállapotú életciklus funkcionálisan** (Vázlat / Publikált), kézi
  publikálás; nincs ütemezett auto-publikálás, jóváhagyási workflow vagy
  verziótörténet a pilotra (K-016).
- **Hír-mezők:** cím, tartalom (formázott), borítókép, publikálás
  dátuma, push-jelölőnégyzet hírenként, állapot.
- **Program-mezők:** cím, leírás (formázott), kezdő (és opcionálisan
  záró) időpont, helyszín szöveg, borítókép, állapot.
- **Városi információ-mezők:** megnevezés, rövid leírás, kötelező külső
  URL, ikon/kategória, sorrend, állapot.
- **A polgári oldal nem itt** — az admin oldal a fókusz; a polgári adatigény-
  spec definiálja a polgári-oldali olvasói szerződést.

### 0.4 Mit tölt ki ez a spec (a fejlesztői mélység)

A funkcionális dokumentum kétállapotú életciklust ír; a domain-modell
viszont **háromállapotú** `ContentStatus` enumot rögzít (SD-22). A spec ezt
következetesen átvezeti, plusz a következőket tölti ki:

1. A `ContentStatus` formális állapotgépe (3.4)
2. Mezőtáblák FluentValidation-szinten (4.)
3. Borítókép-kezelés mintája (D-T1, SD-68)
4. Három `BaseController`-leszármazott eltérései (4.)
5. `TableStateConfig`-vázlatok három admin-listára (5.)
6. Mezősablonok a három űrlapra (5.)
7. `authorization.json` route-leképezés (MH-J1 lezárása, 4.1.1)
8. i18n-kulcsok (5.5)
9. 75 acceptance criterion (7.)

### 0.5 Érintett kanonikus és tervezési döntések

**Kanonikus döntések:** K-007, K-008, K-016, K-024, K-025, K-027, K-028,
K-038, K-039.

**Korábbi specifikációs döntések (épít, nem nyitja újra):** SD-1, SD-9,
SD-10, SD-15, SD-21, SD-22, SD-23 (megerősítendő), SD-32, SD-60, SD-61,
SD-63, SD-64, SD-66.

**A spec által lezárt / megerősített:**
- MH-J1 (a `60_tartalom` route-stringjei) — **LEZÁRVA** (4.1.1)
- MH-2 (tartalmi entitások törölhetősége) — **LEZÁRVA** erre a modulra (4.2.6)
- NY-6 (`News`/`Event` `body` formátum) — **LEZÁRVA** (SD-69)
- NY-7 (`CityInfo.groupLabel` lookup vs. szabad szöveg) — **LEZÁRVA** (SD-71)
- SD-23 — **MEGERŐSÍTVE** (3.6.4)
- VF-K1 (`30_beallitasok.md` visszacsorgó jelzés) — befogadva (SD-68)

### 0.6 Hatókör — explicit határok

**Nincs benne:**
- Polgári mobilapp UX-tervezése (csak az írási oldal és az olvasási
  igény-jelölés).
- A push-értesítés kiküldési mechanikája (polgári mobilapp specifikációja,
  NY-T1).
- GYIK-szerkesztő (Urbino-admin hatáskör, K-039).
- Ütemezett publikálás, jóváhagyási workflow, verziótörténet, teljes
  WYSIWYG, kategorizált hírek, kiemelt hír, beágyazott média (K-016).
- Ötletláda, közvélemény (koncepció-roadmap).
- `DefaultCategoryCatalog` mezőtáblája (NY-K1, Urbino-admin hatáskör).

---

## 1. Cél és hatókör

A `60_tartalom` modul lehetővé teszi a városgazdálkodás tartalomkezelő
munkatársa (`content_manager`) számára, hogy a polgári mobilapp "puha"
tartalmait — hírek, városi programok, városi információk — saját maga
feltöltse, publikálja, archiválja a saját tenantján belül.

**Üzleti érték:** a város kommunikációs kontrollja a saját polgári
csatornája felett. A K-008 multi-tenant fegyelem és a Péter-pálya (a hír
mint politikai csatorna) értelmében ez **nem** Urbino-csapat hatáskör.

**Hatókörön kívül ebben a feature-ben:** lásd 0.6.

---

## 2. Domain-modell

A három tartalmi entitás (`News`, `Event`, `CityInfo`) adatszerkezeti váza
a `00_domain_model.md` 4. blokkjában rögzített. Ez a feature-spec **nem
vezet be új entitást**, mezőszintű módosításokat eszközöl, és formalizálja
a `ContentStatus` állapotgépet.

### 2.1 Új mező — `News.pushOnPublish`

| Mező | Típus | Köt. | Megjegyzés |
|---|---|---|---|
| `pushOnPublish` | `bool` | K | Hírenkénti push-jelzés a polgári mobilapp felé. Default `false`. A polgári app a `Draft → Published` átmenet idején (vagy azt követően egy feed-szinkronizációval) olvassa; a push tényleges kiküldése a polgári mobilapp értesítési rendszerének dolga (NY-T1). |

**Domain-modell-érintés:** a `00_domain_model.md` 4.2 (News) mezőtáblája
egy új sorral bővül.

### 2.2 Mezőtisztázások — `News.body` és `Event.body` (SD-69)

A `00_domain_model.md` 4.2/4.3 a `body` mezőt `string (hosszú)` típusúnak
veszi fel, NY-6-tal a méret/formátum nyitva. Az SD-69 lezárja:

| Mező | Tisztázás |
|---|---|
| `News.body` | HTML-tárolás, max 10 000 karakter (HTML-tartalommal együtt). Megengedett tag-whitelist (lásd 4.1.4). Üres tartalom (`<p></p>` vagy csak whitespace) érvénytelen. Szanitizáció FluentValidation-hoz csatolva. |
| `Event.body` | Ua. szabályok, ua. limitek. |

**A `CityInfo.description` nem érintett** — plain text marad, max 500
karakter.

### 2.3 Borítókép — `News.coverImageRef` és `Event.coverImageRef` (SD-68)

A két mező a `00_domain_model.md` 4.2/4.3-ban már rögzített, `string`
(opcionális) típussal. Az SD-68 a *kezelés mintáját* rögzíti — az
adatszerkezet maga nem változik:

- A mező S3-objektum-hivatkozást tárol.
- A mező a standard `PUT /v1/news/{id}` / `PUT /v1/events/{id}`-en **nem
  szerkeszthető** (`400 fieldErrors.coverImageRef = "field_not_editable"`).
- Csak a dedikált `POST /v1/news/{id}/cover-image` és
  `DELETE /v1/news/{id}/cover-image` végpontok módosíthatják.

### 2.4 `Event.startsAt` és `Event.endsAt` — formális reláció

| Szabály | FluentValidation-kifejezés |
|---|---|
| `endsAt` opcionális | `When(x => x.endsAt != null, ...)` |
| Ha kitöltött: `endsAt ≥ startsAt` | `GreaterThanOrEqualTo(x => x.startsAt)` |
| `startsAt` UTC-tárolt | SD-9/SD-10 közös platform-elv |
| Múltbeli `startsAt` megengedett | A tartalomkezelő utólag is rögzíthet eseményt. |

### 2.5 `CityInfo.groupLabel` — NY-7 lezárása (SD-71)

| Aspektus | Döntés |
|---|---|
| Adat-típus | `string` (opcionális), **max 100 karakter** |
| Lookup-entitás | **Nincs** — nincs `CityInfoGroup` entitás |
| Beírási élmény | Az admin-űrlap a meglévő distinct értékeket egy `GET /v1/city-info-groups`-végpontról kínálja autocomplete-tel |
| Polgári lista szekcionálás | A polgári app a `groupLabel`-szerint szekcionál |

### 2.6 `CityInfo.url` — formátum-validáció

| Szabály | Részletezés |
|---|---|
| Szigorúan `https://` előtagú | Csak HTTPS; `http://` → `400 must_be_https` |
| Max 2000 karakter | URL-hossz reális felső korlát |
| URL-formátum-validáció | `Uri.TryCreate(value, UriKind.Absolute, out var uri) && uri.Scheme == "https"` |

### 2.7 `CityInfo.iconRef` — közös ikonkészlet kulcs

A `Category.iconRef` és a `CityInfo.iconRef` **ugyanazt a közös
ikonkészletet** használja. Az ikon-whitelist a `30_beallitasok.md`-ben
rögzített ikon-katalógussal egyező. A `News`-nak és `Event`-nek **nincs
`iconRef`-mezeje**.

### 2.8 Tartalmi szerző — SD-23 megerősítése

A tartalmi entitásokon a szerző-megjelenítés a `createdBy` (Core
`User.id`)-ra joinolt `TenantUser`-ből megy
(`TenantUser.userId == entity.createdBy`). **Nincs** dedikált `authorId`
mező — a három admin-lista (Hír, Program, Városi információ) oszlopkészlete
sehol nem nevesít szerző-oszlopot; az adatlap-szintű szerző-megjelenítés
egy +1 LEFT JOIN ugyanazon a Tenant DB-n belül (nem cross-DB).

### 2.9 `ContentStatus` állapotgép (SD-70)

A három tartalmi entitás (`News`, `Event`, `CityInfo`) **azonos
állapotgépet** használ a `ContentStatus` enumon (`Draft` / `Published` /
`Archived`).

**Állapot-átmenet tábla:**

| # | Forrás | Cél | Kiváltó akció | Jogosultság | Feltétel | Mellékhatás |
|---|---|---|---|---|---|---|
| T1 | `Draft` | `Published` | "Publikálás" | `content_manager` vagy `manager` | — | `publishedAt = now()` |
| T2 | `Published` | `Draft` | "Visszavonás vázlatba" | ua. | — | `publishedAt = null` |
| T3 | `Published` | `Archived` | "Archiválás" | ua. | — | `publishedAt` változatlan |
| T4 | `Archived` | `Published` | "Újrapublikálás" | ua. | — | `publishedAt = now()` |
| — | `Draft` | `Archived` | (tilos) | — | — | `409 invalid_transition` |
| — | `Archived` | `Draft` | (tilos) | — | — | `409 invalid_transition` |

**Diagram (Mermaid):**

```mermaid
stateDiagram-v2
    [*] --> Draft : létrehozás
    Draft --> Published : T1 — Publikálás
    Published --> Draft : T2 — Visszavonás vázlatba
    Published --> Archived : T3 — Archiválás
    Archived --> Published : T4 — Újrapublikálás
```

**Tiltott átmenetek (`Draft → Archived`, `Archived → Draft`)** —
`409 invalid_transition`, `details.from`, `details.to`.

**A `DELETE` állapot-érzékeny:**

| Állapot | `DELETE` engedélyezett? |
|---|---|
| `Draft` | igen |
| `Published` | **nem** — `409 cannot_delete_published` |
| `Archived` | igen |

**A polgári app csak `Published` állapotú tartalmat lát** — a polgári
olvasói végpont `WHERE contentStatus = 'Published'` szűrőt alkalmaz.

A `02_globalis_allapotgep.md` új blokkal bővül a `ContentStatus`
állapotgépre — a fenti szakaszok átírható formában kerülnek át.

### 2.10 Lookup-ok, default-katalógus érintettség

Nincs új lookup-entitás. A `groupLabel` autocomplete nem entitás-alapú,
hanem distinct-query. A `DefaultCategoryCatalog` **nem érintett** —
NY-K1 marad megtartott hiány.

---

## 3. Szerver — API és logika

### 3.1 Három controller — áttekintés

| Controller | Bázis-route | Standard CRUD | Eltérések / dedikált végpontok |
|---|---|---|---|
| `NewsController` | `/v1/news` | igen | `cover-image` (POST/DELETE); `transition` (POST) |
| `EventsController` | `/v1/events` | igen | `cover-image` (POST/DELETE); `transition` (POST) |
| `CityInfoController` | `/v1/city-infos` | igen | `transition` (POST); `reorder` (POST); plusz `/v1/city-info-groups` lookup |

**Mindhárom `BaseController`-leszármazott.** A standard CRUD-ot a meglévő
minta adja; az eltéréseket alább rögzítjük.

### 3.1.1 A teljes route-leképezés (MH-J1 lezárása)

A `05_jogosultsag_es_authorization.md` 3.2 G-blokk **Szabály-R3** szerint
minden tartalom-route `roles`-listája `["tenant_content_manager",
"tenant_manager"]`. A spec **27 új route-ot** ad az `authorization.json`-höz:

| # | Route | Method | Szerep |
|---|---|---|---|
| **News** | | | |
| T-1 | `/v1/news` | `GET` | Hír-lista |
| T-2 | `/v1/news/{id}` | `GET` | Hír-adatlap |
| T-3 | `/v1/news` | `POST` | Új hír (Draft) |
| T-4 | `/v1/news/{id}` | `PUT` | Hír szerkesztése |
| T-5 | `/v1/news/{id}` | `DELETE` | Hír törlése (guard mögött) |
| T-6 | `/v1/news/bulk` | `DELETE` | Bulk törlés (guard mögött) |
| T-7 | `/v1/news/{id}/cover-image` | `POST` | Borítókép feltöltése |
| T-8 | `/v1/news/{id}/cover-image` | `DELETE` | Borítókép eltávolítása |
| T-9 | `/v1/news/{id}/transition` | `POST` | Állapot-átmenet |
| **Events** | | | |
| T-10 | `/v1/events` | `GET` | Program-lista |
| T-11 | `/v1/events/{id}` | `GET` | Program-adatlap |
| T-12 | `/v1/events` | `POST` | Új program |
| T-13 | `/v1/events/{id}` | `PUT` | Program szerkesztése |
| T-14 | `/v1/events/{id}` | `DELETE` | Program törlése (guard mögött) |
| T-15 | `/v1/events/bulk` | `DELETE` | Bulk törlés (guard mögött) |
| T-16 | `/v1/events/{id}/cover-image` | `POST` | Borítókép feltöltése |
| T-17 | `/v1/events/{id}/cover-image` | `DELETE` | Borítókép eltávolítása |
| T-18 | `/v1/events/{id}/transition` | `POST` | Állapot-átmenet |
| **CityInfo** | | | |
| T-19 | `/v1/city-infos` | `GET` | Lista |
| T-20 | `/v1/city-infos/{id}` | `GET` | Adatlap |
| T-21 | `/v1/city-infos` | `POST` | Új városi információ |
| T-22 | `/v1/city-infos/{id}` | `PUT` | Szerkesztés |
| T-23 | `/v1/city-infos/{id}` | `DELETE` | Törlés (guard mögött) |
| T-24 | `/v1/city-infos/bulk` | `DELETE` | Bulk törlés (guard mögött) |
| T-25 | `/v1/city-infos/{id}/transition` | `POST` | Állapot-átmenet |
| T-26 | `/v1/city-infos/reorder` | `POST` | Sorrendezés |
| T-27 | `/v1/city-info-groups` | `GET` | `groupLabel` autocomplete |

> **Az MH-J1 ezzel lezárva.** A `05_jogosultsag_es_authorization.md` 3.2
> G-blokk hézag-sorai (Hír CRUD, Városi program CRUD, Városi információ
> CRUD, Publikálás/visszavonás) most kanonikus route-stringekkel vannak
> kitöltve. **Mind a 27 route** `roles`-listája pontosan
> `["tenant_content_manager", "tenant_manager"]`. A Szabály-R3
> érvényesítve.

### 3.1.2 Multi-tenancy

Mindhárom entitás a Tenant DB-ben él. A `Tenant`-header-resolution, a
JWT-kiértékelés és a cross-tenant védelem a platform-minta
(`01_kozos_mintak.md` 1.-3.). Cross-DB olvasás csak a
`createdBy → TenantUser`-join esetén, ami **ugyanazon Tenant DB-n belül**
marad.

### 3.1.3 SD-69 — A HTML-szanitizáció whitelistje (formális)

A `News.body` és `Event.body` HTML-tárolású. A FluentValidation-hoz
csatolt szanitizátor (pl. HtmlSanitizer NuGet) az alábbi whitelistet
érvényesíti **a szerver oldalon**:

**Megengedett elemek:**

| Tag | Cél | Megengedett attribútum |
|---|---|---|
| `<p>` | Bekezdés | — |
| `<br>` | Sortörés | — |
| `<strong>` | Félkövér | — |
| `<em>` | Dőlt | — |
| `<ul>`, `<ol>`, `<li>` | Felsorolás | — |
| `<a>` | Hivatkozás | `href` (csak `https://`); `target="_blank"` és `rel="noopener noreferrer"` automatikusan hozzáadva |

**Tiltott elemek (explicit):** `<script>`, `<iframe>`, `<img>`, `<video>`,
`<audio>`, `<embed>`, `<object>`, `<style>`, `<link>`, `<meta>`, `<form>`,
`<input>`, `<button>`, `<table>` és minden más nem-whitelist tag —
**eltávolítva**, a tartalom (text content) megmarad.

**Tiltott attribútumok:** `style`, `onclick`, `onerror`, `onload`, és
minden `on*` event-handler.

**Az `<a href>` URL-szigorítása:** `javascript:`, `data:`, `file:`,
`http://` — kiszűrve, a `<a>` tag tartalma plain szövegként megmarad.

**Maximum-karakter számítás:** 10 000 karakter a **teljes HTML-tartalomra**
(tag-ekkel együtt). FluentValidation: `MaximumLength(10000)`.

**Üresség-ellenőrzés:** szanitizáció után a sima text content trim után
nem lehet üres. `<p></p>` egyedüli tartalomként → `400 fieldErrors.body =
"invalid_html_or_empty_after_sanitization"`.

**Kliens-oldali tükör:** a rich-text-szerkesztő (PrimeNG Editor, 4.6)
ugyanezt a whitelistet alkalmazza. A szerver-oldali szanitizáció az
igazság; a kliens a szerver válaszát fogadja el.

### 3.2 `NewsController` — News API-szerződés

#### 3.2.1 DTO-mezőkészlet

A `NewsDto` (lista- és adatlap-válasz):

| Mező | Típus | Forrás |
|---|---|---|
| `id` | `long` | `News.id` |
| `title` | `string` | `News.title` |
| `body` | `string` (HTML, szanitizált) | `News.body` |
| `coverImageUrl` | `string?` | a `coverImageRef`-ből feloldott publikus URL |
| `pushOnPublish` | `bool` | `News.pushOnPublish` |
| `contentStatus` | `enum` | `News.contentStatus` |
| `publishedAt` | `DateTime?` | `News.publishedAt`, UTC |
| `createdAt`, `createdBy`, `createdByName` | — | audit (a `createdByName` a `TenantUser`-join eredménye) |
| `updatedAt`, `updatedBy`, `updatedByName` | — | audit |

A `NewsListDto` szűkebb — a `body` nincs benne.

#### 3.2.2 `GET /v1/news` — lista (T-1)

**Standard `BaseController` list** a `ListRequest` + `FilterQuery`
mintával.

**Kliens-oldali default:** `contentStatus IN (Draft, Published)` — az
`Archived` rejtett alapból. Az "Archív megjelenítése" checkbox hozzáadja
az `Archived`-et a szűrőhöz. URL query: `?contentStatus.in=Draft,Published`.

**Támogatott szűrők:**
- `contentStatus` — `eq`, `in`
- `pushOnPublish` — `eq`
- `publishedAt` — `gte`, `lte`, `eq`
- `createdAt` — `gte`, `lte`
- `title` — `contains` (case-insensitive)

**Rendezés:** `createdAt` (default desc), `publishedAt`, `title`,
`updatedAt`.

**Eltérés a mintától:** nincs.

#### 3.2.3 `GET /v1/news/{id}` — adatlap (T-2)

**Standard `BaseController` get.** A teljes `NewsDto`-t adja vissza.

#### 3.2.4 `POST /v1/news` — létrehozás (T-3)

**Eltérés a mintától:** az új entitás `contentStatus`-a a szerverben
hardkódolt `Draft` (a `BeforeSaveAsync`-ben); a `coverImageRef` a POST-on
tiltott.

**Request — `CreateNewsRequest`:**

| Mező | Típus | Köt. | Validáció |
|---|---|---|---|
| `title` | `string` | K | Trim után nem üres; max 200 |
| `body` | `string` | K | HTML, szanitizált; max 10 000 |
| `pushOnPublish` | `bool` | K | Default `false` |

**Tiltott mezők (`400 field_not_editable`):** `contentStatus`,
`publishedAt`, `coverImageRef`.

**Mellékhatás:** `contentStatus = Draft`, `publishedAt = null`,
`coverImageRef = null`.

**Response:** `201` + `NewsDto`.

#### 3.2.5 `PUT /v1/news/{id}` — szerkesztés (T-4)

**Eltérés a mintától:** a szerkeszthető mezők szűkítettek.

**Request — `UpdateNewsRequest`:**

| Mező | Típus | Köt. | Validáció |
|---|---|---|---|
| `title` | `string` | K | Mint POST |
| `body` | `string` | K | Mint POST |
| `pushOnPublish` | `bool` | K | — |
| `expectedUpdatedAt` | `DateTime` | K | Optimista konkurrencia (SD-32) |

**Nem szerkeszthető (`400 field_not_editable`):** `contentStatus`,
`publishedAt`, `coverImageRef`, `createdBy`, `createdAt`.

**A szerkesztés bármely állapotban megengedett** — `Draft`, `Published`,
`Archived` mind szerkeszthető. A `Published` szerkesztése gyors korrekciót
enged; érdemi újragondoláshoz a tartalomkezelő a `transition`-on
visszadrafftol.

**Hibakódok:** `400`, `400 field_not_editable`, `404`, `409 stale`.

**Response:** `200` + `NewsDto`.

#### 3.2.6 `DELETE /v1/news/{id}` — törlés (T-5)

**Eltérés a mintától — `BeforeDeleteAsync` állapot-guard.**

**Workflow:**
1. `News.contentStatus` lekérve.
2. Ha `Published` → `409 cannot_delete_published`,
   `details.currentStatus = "Published"`.
3. Ha `Draft` vagy `Archived` → engedélyezett.
4. A `coverImageRef`-hez tartozó S3-objektum törlése
   `AfterSaveAsync`-ben (NY-K3 takarítás meghiúsulás esetén).

**Hibakódok:** `404`, `409 cannot_delete_published`, `409 stale`.

**Response:** `204`.

#### 3.2.7 `DELETE /v1/news/bulk` — bulk törlés (T-6)

**Eltérés a mintától — atomi guard.** Ha a batch egy tagja `Published`,
a teljes batch `409 cannot_delete_published`-szel zár,
`details.publishedIds` listával; egyetlen tétel sem törlődik.

**Request:** `{ ids: long[] }`.

#### 3.2.8 `POST /v1/news/{id}/cover-image` — borítókép feltöltése (T-7)

**Eltérés a mintától — SD-68 "egyedi-fájl-mező".** Dedikált
multipart-feltöltő, nem standard CRUD.

**Request:** `multipart/form-data`, egyetlen `file` rész.

**Validáció:**

| Szabály | Érték |
|---|---|
| MIME | `image/jpeg`, `image/png`, `image/webp` |
| Max méret | 5 MB |
| Min méret | 1 KB |

**Workflow:**
1. `News.id` ellenőrzés (`404` ha nincs).
2. Fájl validáció (MIME, méret).
3. S3-feltöltés (kulcs-struktúra a backend dolga, pl.
   `tenant-{code}/news/{id}/cover-{guid}.jpg`).
4. Régi `coverImageRef` esetén az S3-objektum törlése
   `AfterSaveAsync`-ben.
5. `News.coverImageRef = új kulcs`; `updatedAt`, `updatedBy` frissül.

**Hibakódok:** `400 invalid_mime_type`, `400 file_too_large`,
`400 file_too_small`, `404`.

**Response:** `200` + `NewsDto`.

#### 3.2.9 `DELETE /v1/news/{id}/cover-image` — borítókép eltávolítása (T-8)

**Eltérés a mintától — SD-68.** Idempotens — `coverImageRef = null`
állapotban is `204`-gyel zár.

**Response:** `204`.

#### 3.2.10 `POST /v1/news/{id}/transition` — állapot-átmenet (T-9)

**Eltérés a mintától — dedikált akció-végpont az állapotgéphez.**

**Request — `TransitionRequest`:**

| Mező | Típus | Köt. |
|---|---|---|
| `targetStatus` | `enum` (`Draft`, `Published`, `Archived`) | K |
| `expectedUpdatedAt` | `DateTime` | K |

**Workflow:**
1. Jelenlegi `contentStatus` lekérve.
2. A `(current, target)` páros ellenőrzése a 2.9 átmenet-tábla ellen.
3. Ha a páros nem T1-T4 között → `409 invalid_transition`.
4. `expectedUpdatedAt`-ellenőrzés; ha eltér → `409 stale`.
5. Mellékhatások a 2.9 szerint (`publishedAt` írása/törlése, `updatedAt`).
6. Ha T1 (publikálás) és `pushOnPublish == true`, a polgári oldal felé
   kiküldött push **a polgári adatigény-spec dolga** (NY-T1).

**Hibakódok:** `400` (érvénytelen enum), `404`, `409 invalid_transition`
(`details.from`, `details.to`), `409 stale`.

**Response:** `200` + `NewsDto`.

#### 3.2.11 FluentValidation — News (vázlat)

```csharp
public class CreateNewsRequestValidator : AbstractValidator<CreateNewsRequest>
{
    public CreateNewsRequestValidator(IHtmlSanitizer sanitizer)
    {
        RuleFor(x => x.title).NotEmpty().MaximumLength(200);
        RuleFor(x => x.body)
            .NotEmpty()
            .MaximumLength(10000)
            .Must(b => !string.IsNullOrWhiteSpace(sanitizer.Sanitize(b).StripTags()))
            .WithMessage("invalid_html_or_empty_after_sanitization");
        RuleFor(x => x.pushOnPublish).NotNull();
    }
}
```

### 3.3 `EventsController` — Event API-szerződés

A `News`-szal **94%-ban azonos** mintát követ. Az alábbiak az eltérések.

#### 3.3.1 DTO-mezőkészlet — eltérések

**Plusz mezők a `News`-hoz képest:** `startsAt`, `endsAt`, `locationText`,
`latitude`, `longitude`.

**Hiányzik:** `pushOnPublish` (a programok nem küldenek pusht).

#### 3.3.2 `GET /v1/events` — lista (T-10)

**Plusz szűrők:** `startsAt` (`gte`, `lte`), `endsAt` (`gte`, `lte`).

#### 3.3.3 `POST /v1/events` — létrehozás (T-12)

**Request — `CreateEventRequest`:**

| Mező | Típus | Köt. | Validáció |
|---|---|---|---|
| `title` | `string` | K | Trim, max 200 |
| `body` | `string` | K | HTML, szanitizált, max 10 000 |
| `startsAt` | `DateTime` | K | UTC; múltbeli is megengedett |
| `endsAt` | `DateTime?` | O | Ha kitöltött: `≥ startsAt` |
| `locationText` | `string?` | O | Max 300 |
| `latitude` | `decimal?` | F | -90..90; páros a `longitude`-zel |
| `longitude` | `decimal?` | F | -180..180; páros a `latitude`-tel |

**Tiltott mezők:** `contentStatus`, `publishedAt`, `coverImageRef`.

**FluentValidation — Event-specifikus szabályok:**

```csharp
RuleFor(x => x.endsAt)
    .GreaterThanOrEqualTo(x => x.startsAt)
    .When(x => x.endsAt.HasValue)
    .WithMessage("ends_at_before_starts_at");

RuleFor(x => x.locationText).MaximumLength(300).When(x => x.locationText != null);
RuleFor(x => x.latitude).InclusiveBetween(-90m, 90m).When(x => x.latitude.HasValue);
RuleFor(x => x.longitude).InclusiveBetween(-180m, 180m).When(x => x.longitude.HasValue);

RuleFor(x => x)
    .Must(x => (x.latitude.HasValue && x.longitude.HasValue) ||
               (!x.latitude.HasValue && !x.longitude.HasValue))
    .WithMessage("coordinates_must_be_both_or_neither");
```

#### 3.3.4 A többi végpont

`PUT`, `DELETE`, `cover-image`, `transition` — ua. mint a News-spec, az
eltérő mezőkkel.

### 3.4 `CityInfoController` — CityInfo API-szerződés

#### 3.4.1 DTO-mezőkészlet

A `CityInfoDto`:

| Mező | Típus | Forrás |
|---|---|---|
| `id` | `long` | `CityInfo.id` |
| `title` | `string` | `CityInfo.title` |
| `description` | `string?` | plain text, max 500 |
| `url` | `string` | `CityInfo.url` |
| `iconRef` | `string?` | közös ikonkészlet kulcs |
| `groupLabel` | `string?` | max 100 |
| `sortOrder` | `int` | `CityInfo.sortOrder` |
| `contentStatus`, `publishedAt` | — | közös váz |
| audit mezők | — | mint News |

**A `coverImageRef` nincs** a `CityInfo`-n.

#### 3.4.2 `POST /v1/city-infos` — létrehozás (T-21)

**Request — `CreateCityInfoRequest`:**

| Mező | Típus | Köt. | Validáció |
|---|---|---|---|
| `title` | `string` | K | Trim, max 200 |
| `description` | `string?` | O | Plain text, max 500 |
| `url` | `string` | K | `https://` előtag; URI-formátum; max 2000 |
| `iconRef` | `string?` | O | Whitelist; max 50 |
| `groupLabel` | `string?` | O | Max 100; trim |

**Tiltott mezők:** `contentStatus`, `publishedAt`, `sortOrder` (a szerver
állítja a végén: `max(sortOrder) + 1`).

**FluentValidation — CityInfo-specifikus:**

```csharp
RuleFor(x => x.url)
    .NotEmpty()
    .MaximumLength(2000)
    .Must(url => Uri.TryCreate(url, UriKind.Absolute, out var uri)
                 && uri.Scheme == "https")
    .WithMessage("must_be_https");

RuleFor(x => x.iconRef)
    .MaximumLength(50)
    .Must(key => iconCatalog.Contains(key))
    .When(x => !string.IsNullOrEmpty(x.iconRef))
    .WithMessage("invalid_icon_ref");

RuleFor(x => x.groupLabel).MaximumLength(100).When(x => x.groupLabel != null);
RuleFor(x => x.description).MaximumLength(500).When(x => x.description != null);
```

#### 3.4.3 `PUT /v1/city-infos/{id}` — szerkesztés (T-22)

**Szerkeszthető:** `title`, `description`, `url`, `iconRef`, `groupLabel`,
`expectedUpdatedAt`.

**Nem szerkeszthető:** `contentStatus`, `publishedAt`, `sortOrder`.

#### 3.4.4 `POST /v1/city-infos/{id}/transition` — állapot-átmenet (T-25)

Ua. mint a News/Event — négy átmenet, hibakódok azonos.

#### 3.4.5 `POST /v1/city-infos/reorder` — sorrendezés (T-26)

**Standard `BaseController` reorder-mintája.**

**Request:** `{ ids: long[] }`.

**Workflow:** a szerver `sortOrder = 0, 1, 2, ...` értékeket ír a tömb
sorrendjében.

**Eltérés a `Category`-tól (SD-64):** nincs `parentId`-hatókör — a
CityInfo lapos lista.

#### 3.4.6 `GET /v1/city-info-groups` — `groupLabel` autocomplete (T-27)

**Eltérés a mintától — új lookup-jellegű végpont, nem entitás.**

**Request:** opcionális `query` query-paraméter (case-insensitive
contains).

**Response:** `200` + `string[]` (distinct `groupLabel` értékek, `null`
és üres kihagyva, alphabet-sort).

**Implementáció:** `SELECT DISTINCT groupLabel FROM CityInfo WHERE
groupLabel IS NOT NULL AND groupLabel ILIKE '%...%' ORDER BY groupLabel`.

### 3.5 Hibakód-tábla (összefoglaló)

| Status | `reason` | Forrás |
|---|---|---|
| `400` | — | Mezőszintű FluentValidation; `fieldErrors` mező |
| `400` | `field_not_editable` | Tiltott mező PUT/POST-on |
| `400` | `invalid_mime_type` | Cover-image |
| `400` | `file_too_large` | Cover-image > 5 MB |
| `400` | `file_too_small` | Cover-image < 1 KB |
| `400` | `must_be_https` | CityInfo.url |
| `400` | `invalid_icon_ref` | iconRef whitelist |
| `400` | `coordinates_must_be_both_or_neither` | Event koordináták |
| `400` | `ends_at_before_starts_at` | Event időpontok |
| `400` | `invalid_html_or_empty_after_sanitization` | body szanitizáció |
| `403` | — | Jogosultság (Szabály-R3) |
| `404` | — | Nem létező `id` |
| `409` | `stale` | Optimistic-concurrency |
| `409` | `invalid_transition` | Tiltott átmenet (`details.from/to`) |
| `409` | `cannot_delete_published` | DELETE `Published`-en (`details.currentStatus`) |

### 3.6 Workflow- és business-szabályok

#### 3.6.1 Átmenet és szerkesztés sorrendje

Tipikus publikálási folyamat:
1. `POST /v1/news` → `Draft`.
2. Opcionálisan `POST .../cover-image`.
3. `PUT /v1/news/{id}` → szerkesztés (egyszer vagy többször).
4. `POST .../transition { targetStatus: "Published" }`.
5. Ha hiba: `transition` `Draft`-ra vissza.
6. Ha nem aktuális: `transition` `Archived`-re.
7. Esetleg: `DELETE`.

#### 3.6.2 `coverImageRef` cseréje

Új feltöltés esetén: új fájl S3-ba, `coverImageRef` átáll, a régi
S3-objektum `AfterSaveAsync`-ben törlődik. S3-törlés meghiúsulása
**WARNING-szintű** logbejegyzéssel rögzítendő (NY-K3 takarítás).

#### 3.6.3 Bulk delete és guard

Atomi — egyetlen `Published` is `409 cannot_delete_published`-szel
zárja a teljes batchet. A kliens-UI kezeli az előszűrést.

#### 3.6.4 Publikálási push átvitele

A backend a `Draft → Published` átmenet idején **nem küld push-t a
polgárnak közvetlenül**. A `pushOnPublish == true` jelzés a polgári
oldal felé átkerül (Pull-feed minta A vagy server-push minta B,
NY-T1) — a polgári adatigény-spec eldönti.

### 3.7 Audit és naplózás

A három entitás `AuditableEntity` — `createdAt`/`createdBy`/`updatedAt`/
`updatedBy` automatikusan. Plusz `ContentAuditLog` (mezőnkénti
változás-történet) a piloton **nincs** — iterációs (NY-K4 mintával).

---

## 4. Admin felület

### 4.1 Navigáció és sitemap-érintés

**A "Tartalom" lenyíló főmenü:**

```
Tartalom ▾
  Hírek                → /tartalom/hirek
  Városi programok     → /tartalom/varosi_programok
  Városi információk   → /tartalom/varosi_informaciok
```

**Belépési oldal (K-027):**

| Szerepkör | Érkezés |
|---|---|
| `content_manager` | `/tartalom/hirek` |
| `manager` | `/fooldal` (Dashboard); Tartalom menü látható |
| `content_manager + dispatcher` (K-025) | `/bejelentesek`; Tartalom is látható |

A `/tartalom` önmagában nem oldal — kattintásra a menü nyílik, redirect a
`/tartalom/hirek`-re.

**Breadcrumb és page title:** a `NavStore` minta szerint.

### 4.2 Hírek aloldal — `/tartalom/hirek`

#### 4.2.1 Lista — `TableStateConfig` vázlat

```typescript
const newsTableState: TableStateConfig = {
  endpoint: '/v1/news',
  defaultSort: { field: 'createdAt', direction: 'desc' },
  columns: [
    { field: 'coverImageUrl', header: '', width: '60px',
      cellType: 'thumbnail', sortable: false, filterable: false },
    { field: 'title', header: 'content.news.column.title',
      sortable: true, filterable: { type: 'text', operator: 'contains' } },
    { field: 'contentStatus', header: 'content.common.column.status',
      width: '140px', sortable: true, cellType: 'statusBadge',
      filterable: { type: 'multiselect',
        options: ['Draft', 'Published', 'Archived'],
        defaultValue: ['Draft', 'Published'] } },
    { field: 'publishedAt', header: 'content.news.column.publishedAt',
      width: '160px', sortable: true, cellType: 'datetime',
      filterable: { type: 'dateRange' } },
    { field: 'pushOnPublish', header: 'content.news.column.pushOnPublish',
      width: '110px', cellType: 'iconBoolean', visible: false,
      filterable: { type: 'select', options: [true, false] } },
    { field: 'createdByName', header: 'content.common.column.createdBy',
      width: '140px', sortable: false, visible: false },
    { field: 'createdAt', header: 'content.common.column.createdAt',
      width: '160px', sortable: true, cellType: 'datetime', visible: false,
      filterable: { type: 'dateRange' } },
    { field: 'updatedAt', header: 'content.common.column.updatedAt',
      width: '160px', sortable: true, visible: false }
  ],
  rowActions: [
    { key: 'edit', label: 'common.action.edit', icon: 'pencil', primary: true },
    { key: 'publish', label: 'content.action.publish', icon: 'check',
      visible: row => row.contentStatus === 'Draft' },
    { key: 'unpublish', label: 'content.action.unpublish', icon: 'undo',
      visible: row => row.contentStatus === 'Published' },
    { key: 'archive', label: 'content.action.archive', icon: 'archive',
      visible: row => row.contentStatus === 'Published' },
    { key: 'republish', label: 'content.action.republish', icon: 'refresh',
      visible: row => row.contentStatus === 'Archived' },
    { key: 'delete', label: 'common.action.delete', icon: 'trash',
      visible: row => row.contentStatus !== 'Published', destructive: true }
  ],
  bulkActions: [
    { key: 'bulkPublish', label: 'content.action.bulkPublish' },
    { key: 'bulkArchive', label: 'content.action.bulkArchive' },
    { key: 'bulkDelete', label: 'common.action.delete', destructive: true }
  ],
  primaryAction: {
    key: 'create', label: 'content.news.action.create',
    route: '/tartalom/hirek/uj', icon: 'plus'
  }
};
```

#### 4.2.2 Kebab-menü állapot-érzékenység

| Állapot | Akciók |
|---|---|
| `Draft` | Szerkesztés, Publikálás, (elválasztó), Törlés |
| `Published` | Szerkesztés, **Archiválás**, Visszavonás vázlatba (Törlés rejtett) |
| `Archived` | Szerkesztés, Újrapublikálás, (elválasztó), Törlés |

A publikált hírnél az **Archiválás az elsődleges levételi akció** (D-T3
finomítás).

#### 4.2.3 Bulk akciók viselkedése

- **Bulk publikálás:** csak `Draft`-ot publikál; megerősítő dialógus
  jelzi a vegyes batchet.
- **Bulk archiválás:** csak `Published`-et.
- **Bulk törlés:** kliens előszűr; szerver atomi guard (3.2.7).

#### 4.2.4 Szerkesztő-űrlap

```
┌─────────────────────────────────────────────────────────────────┐
│  ← Tartalom › Hírek › Új hír / "Üzemszünet értesítő május"       │
│                                                                  │
│  ┌─ Borítókép szekció ─────────────────────────────────────────┐ │
│  │  [bélyegkép, 200×150 px]    [Borítókép feltöltése]          │ │
│  │                              [Borítókép eltávolítása]        │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  Cím *                                                           │
│  [_______________________________________________________]       │
│                                                                  │
│  Tartalom *                                                      │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │  [B] [I] [list] [numlist] [link]                            │ │
│  ├─────────────────────────────────────────────────────────────┤ │
│  │  (rich-text-szerkesztő, 6-8 sor magas)                      │ │
│  └─────────────────────────────────────────────────────────────┘ │
│  1247 / 10000                                                    │
│                                                                  │
│  [✓] Push-értesítés a publikáláskor                              │
│                                                                  │
│  ─────────────────────────────────────────────────────────────── │
│  Audit                                                           │
│  Létrehozta:    Gergő Kondor — 2026.05.18 14:32                  │
│  Módosította:   Gergő Kondor — 2026.05.20 09:15                  │
│  ─────────────────────────────────────────────────────────────── │
│  Állapot: [Draft Badge]    [Visszavonás] [Mentés] [Publikálás ▾] │
└─────────────────────────────────────────────────────────────────┘
```

#### 4.2.5 Mezősablonok (kiemelt mezők)

**`title` — Cím:**
- i18n: `content.news.field.title`
- Típus/méret: string, max 200
- Validáció: nem üres trim után; max 200
- Eredet: `60_tartalom_v2.md` 3.2

**`body` — Tartalom (rich-text):**
- i18n: `content.news.field.body`
- Típus/méret: HTML, max 10 000 (HTML együtt)
- Validáció: szanitizáció után nem üres, whitelist (3.1.3)
- Interakció: rich-text-szerkesztő, toolbar: B, I, listák, link
- Karakterszámláló: a szerkesztő alatt, "1247 / 10000"; 10 001-nél piros,
  Mentés letiltott
- Eredet: `60_tartalom_v2.md` 3.2 + SD-69

**`coverImageRef` — Borítókép:**
- i18n: `content.common.field.coverImage`
- Adatmodell: `News.coverImageRef`
- Interakció: "Borítókép feltöltése" / "Borítókép eltávolítása" gombok;
  optimista bélyegkép-csere
- **Új hír esetén (még nincs `id`)**: a feltöltő-gomb LE VAN TILTVA az
  első mentésig (SD-60 logó-mintával konzisztens). Súgó: "Először mentsd
  el a hírt."
- Eredet: `60_tartalom_v2.md` 3.2 + SD-68

**`pushOnPublish` — Push-értesítés a publikáláskor:**
- i18n: `content.news.field.pushOnPublish`
- Típus: bool, default `false`
- Interakció: jelölőnégyzet; súgó-szöveg:
  `content.news.field.pushOnPublish.help`
- Eredet: `60_tartalom_v2.md` 3.2, 6.1

#### 4.2.6 Állapot-átmenet UI-mintája

A "Publikálás ▾" gomb működése állapotonként:

| Állapot | Gomb-felirat / dropdown |
|---|---|
| `Draft` | "Publikálás" → megerősítő dialógus (push-tartalomra `pushWithPush` változat) |
| `Published` | dropdown: "Archiválás" + "Visszavonás vázlatba" |
| `Archived` | "Újrapublikálás" |

A megerősítés után `POST .../transition`; optimista frissítés, hibára
visszaáll.

### 4.3 Programok aloldal — `/tartalom/varosi_programok`

#### 4.3.1 Lista — eltérések a News-tól

**Új oszlopok:** `startsAt` (default desc), `endsAt` (rejtett),
`locationText` (rejtett).

**Új szűrők:** `startsAt`/`endsAt` dátum-szűrő.

**A `pushOnPublish`-oszlop NINCS.**

**`startsAt` + `endsAt` cellaformázás:**
- Csak `startsAt`: `2026.06.15 18:00`
- Azonos nap: `2026.06.15 18:00–22:00`
- Különböző nap: `2026.06.15 18:00 – 2026.06.17 22:00`
- Nyitott végű: `2026.06.15 18:00–`

#### 4.3.2 Szerkesztő-űrlap — új mezők

**`startsAt` — Kezdő időpont:** kötelező datetime-picker; múltbeli
megengedett (figyelmeztetés: "A megadott időpont a múltban van.
Folytatás?").

**`endsAt` — Befejező időpont:** opcionális; ha kitöltött, `≥ startsAt`.

**`locationText` — Helyszín:** opcionális, max 300, placeholder: "Pl.
Városháza, nagyterem".

**`latitude`/`longitude` — Helyszín koordinátája:** opcionális, páros
("együtt vagy egyik sem"); két szám-input; alatta "Térkép megnyitása"
link (Google Maps új lap); beágyazott térkép-szelektor iterációs
(NY-T2).

### 4.4 Városi információk aloldal — `/tartalom/varosi_informaciok`

#### 4.4.1 Lista — eltérések

**Új oszlopok:** `iconRef` (ikon-cell), `groupLabel` (szűrő:
`loadOptionsFrom: '/v1/city-info-groups'`), `url` (rejtett),
`sortOrder` (rejtett).

**Default rendezés:** `sortOrder asc`.

**Új sor-akció:** drag-handle a sor elején a reorder-hez.

**A `pushOnPublish`-oszlop NINCS.**

#### 4.4.2 Szerkesztő-űrlap — mezősablonok

**`title` — Megnevezés:** kötelező, max 200.

**`description` — Rövid leírás:** opcionális, max 500, plain text
textarea.

**`url` — Hivatkozás URL:** kötelező, `https://` előtag, max 2000; az
érték érvényessége után "Megnyitás új lapon" link.

**`iconRef` — Ikon:** opcionális, ikon-választó a közös ikonkészletből.

**`groupLabel` — Csoport:** opcionális, max 100, **autocomplete** a
meglévő distinct értékekről (`GET /v1/city-info-groups`); szabad
szöveges új érték megengedett.

**A `CityInfo`-nak nincs borítóképe.**

#### 4.4.3 Reorder UX

Drag-and-drop a soron lévő "≡" handle-rel; `POST /v1/city-infos/reorder`;
optimista frissítés.

### 4.5 i18n-kulcsok — a `hu.json` bővítése

#### 4.5.1 Modul-szintű

```json
{
  "content": {
    "menu": {
      "title": "Tartalom",
      "news": "Hírek",
      "events": "Városi programok",
      "cityInfos": "Városi információk"
    },
    "common": {
      "column": {
        "status": "Állapot",
        "createdBy": "Létrehozta",
        "createdAt": "Létrehozva",
        "updatedAt": "Módosítva"
      },
      "field": {
        "coverImage": "Borítókép",
        "coverImage.upload": "Borítókép feltöltése",
        "coverImage.delete": "Borítókép eltávolítása",
        "coverImage.uploading": "Feltöltés…"
      },
      "status": {
        "Draft": "Vázlat",
        "Published": "Publikált",
        "Archived": "Archivált"
      },
      "filter": { "showArchived": "Archív megjelenítése" }
    },
    "action": {
      "publish": "Publikálás",
      "unpublish": "Visszavonás vázlatba",
      "archive": "Archiválás",
      "republish": "Újrapublikálás",
      "bulkPublish": "Publikálás (tömeges)",
      "bulkArchive": "Archiválás (tömeges)"
    }
  }
}
```

#### 4.5.2 Hírek-specifikus

```json
{
  "content": {
    "news": {
      "title": "Hírek",
      "column": {
        "title": "Cím",
        "publishedAt": "Publikálva",
        "pushOnPublish": "Push"
      },
      "field": {
        "title": "Cím",
        "body": "Tartalom",
        "pushOnPublish": "Push-értesítés a publikáláskor",
        "pushOnPublish.help": "Bekapcsolva értesítést küld a polgárok telefonjára a publikáláskor. A polgári app a publikálás után pár percen belül kézbesíti."
      },
      "action": { "create": "Új hír" },
      "confirm": {
        "publish": "Publikálod ezt a hírt? A polgárok azonnal látni fogják.",
        "publishWithPush": "Publikálod ezt a hírt? A polgárok azonnal látni fogják, és push-értesítést is küldünk a feliratkozók telefonjára.",
        "unpublish": "Visszavonod vázlatba? A polgárok már nem fogják látni. A publikálási dátum törlődik.",
        "archive": "Archiválod ezt a hírt? A polgárok már nem fogják látni, de megmarad az archívban.",
        "republish": "Újrapublikálod ezt a hírt? A polgárok újra látni fogják, és új publikálási dátumot kap."
      }
    }
  }
}
```

#### 4.5.3 Programok-specifikus

```json
{
  "content": {
    "event": {
      "title": "Városi programok",
      "column": {
        "title": "Cím",
        "startsAt": "Kezdés",
        "endsAt": "Befejezés",
        "location": "Helyszín"
      },
      "field": {
        "title": "Cím",
        "body": "Leírás",
        "startsAt": "Kezdő időpont",
        "endsAt": "Befejező időpont",
        "locationText": "Helyszín",
        "locationText.placeholder": "Pl. Városháza, nagyterem",
        "latitude": "Helyszín szélessége",
        "longitude": "Helyszín hosszúsága",
        "openMap": "Megnyitás térképen"
      },
      "action": { "create": "Új program" },
      "confirm": {
        "publish": "Publikálod ezt a programot? A polgárok azonnal látni fogják.",
        "unpublish": "Visszavonod vázlatba? A polgárok már nem fogják látni.",
        "archive": "Archiválod ezt a programot?",
        "republish": "Újrapublikálod ezt a programot?",
        "pastDate": "A megadott időpont a múltban van. Folytatás?"
      }
    }
  }
}
```

#### 4.5.4 Városi információk-specifikus

```json
{
  "content": {
    "cityInfo": {
      "title": "Városi információk",
      "column": {
        "title": "Megnevezés",
        "group": "Csoport",
        "url": "URL",
        "sortOrder": "Sorrend"
      },
      "field": {
        "title": "Megnevezés",
        "description": "Rövid leírás",
        "url": "Hivatkozás URL",
        "url.openInNewTab": "Megnyitás új lapon",
        "iconRef": "Ikon",
        "groupLabel": "Csoport",
        "groupLabel.placeholder": "Pl. Strandok, Hivatalok"
      },
      "action": {
        "create": "Új városi információ",
        "reorder": "Sorrendezés"
      },
      "confirm": {
        "publish": "Publikálod ezt a tételt?",
        "unpublish": "Visszavonod vázlatba?",
        "archive": "Archiválod ezt a tételt?",
        "republish": "Újrapublikálod ezt a tételt?"
      }
    }
  }
}
```

#### 4.5.5 Hibakulcsok

```json
{
  "content": {
    "error": {
      "stale": "A tartalom időközben módosult. Frissítsd az oldalt, és próbáld újra.",
      "invalid_transition": "Ez az állapot-átmenet nem megengedett.",
      "cannot_delete_published": "Publikált tartalom nem törölhető. Előbb vond vissza vázlatba vagy archiváld.",
      "field_not_editable": "Ez a mező nem szerkeszthető ezzel a művelettel.",
      "invalid_mime_type": "Nem támogatott fájltípus. Csak JPEG, PNG vagy WebP kép tölthető fel.",
      "file_too_large": "A fájl mérete meghaladja az 5 MB-ot.",
      "file_too_small": "A fájl túl kicsi.",
      "must_be_https": "A linknek https:// kezdetűnek kell lennie.",
      "invalid_icon_ref": "Érvénytelen ikon-választás.",
      "coordinates_must_be_both_or_neither": "Az adott helyszín szélességét és hosszúságát együtt kell megadni vagy egyiket sem.",
      "ends_at_before_starts_at": "A befejező időpont nem lehet korábbi, mint a kezdő.",
      "invalid_html_or_empty_after_sanitization": "A tartalom üres vagy érvénytelen formátumú.",
      "bulk_published_blocked": "A kiválasztásban {{count}} publikált tartalom van, ami nem törölhető. Folytatod a többivel?"
    }
  }
}
```

#### 4.5.6 Üres és betöltési állapot

```json
{
  "content": {
    "empty": {
      "news": {
        "title": "Még nincs hír.",
        "description": "Készítsd el az első hírt a városod kommunikációjához.",
        "action": "Új hír létrehozása"
      },
      "event": {
        "title": "Még nincs program.",
        "description": "Rögzítsd a városod első rendezvényét.",
        "action": "Új program létrehozása"
      },
      "cityInfo": {
        "title": "Még nincs városi információ.",
        "description": "Adj hozzá hasznos linkeket a polgárok számára.",
        "action": "Új városi információ"
      }
    },
    "loading": "Betöltés…",
    "error": { "loadFailed": "A tartalom betöltése sikertelen. Próbáld újra." }
  }
}
```

### 4.6 Rich-text-szerkesztő — komponens-választás

**A PrimeNG Editor** használata (a meglévő `project_backend_client_angular`
PrimeNG v21 családból). Konfiguráció: `formats: ['bold', 'italic', 'list',
'bullet', 'link']`; egyéni toolbar. A tartalom HTML-string formátumban
érkezik, az SD-69 elvárt formátumával egyezően.

A karakterszámláló a szerkesztő alatt egy egyszerű directive-vel: a
HTML-tartalom hosszát számolja, és piros színre vált 10 000-nél; a Mentés
gomb letiltott a limit felett.

### 4.7 Jogosultság

| Szerepkör | Mit lát |
|---|---|
| `content_manager` | Csak a Tartalom menüt — Hírek, Programok, Városi információk; teljes CRUD |
| `manager` | Mindent (`manager ⊇ content_manager`) |
| `dispatcher` | Tartalom menü NEM látszik |
| `dispatcher + content_manager` (K-025) | Bejelentések ÉS Tartalom is látszik |

A menü-láthatóság a frontend `NavStore`-ban dől el; a route-szintű
hozzáférés a backend `authorization.json`-jával (Szabály-R3).

### 4.8 Üres, betöltési, hibaállapot

**Üres állapot:** ha a tenant DB-ben nincs egyetlen tétel, a meglévő
empty-state-komponens: illusztráció + címke + akció-gomb.

**Betöltési állapot:** standard skeleton-loading; borítókép-feltöltés
alatt spinner a bélyegkép helyén.

**Hibaállapot:**
- Lista-betöltés: error-state-komponens + Újrapróbálás.
- Mezőszintű (`400 fieldErrors`): mező alatt i18n-fordítás.
- Globális (`409 stale`, `invalid_transition`): toast / banner.

### 4.9 Reszponzivitás

Asztali alap, tableten működik (meglévő admin-mintázat). Tablet-nézeten a
rich-text-toolbar összesűrűsödik. Mobil-nézet **nem prioritás** az
adminon.

---

## 5. Polgári mobilapp adatigénye

> **Kizárólag adatszintű.** A Flutter UX **adott szerződésként** kezelendő;
> a polgári adatigény teljes specifikációja a polgári adatigény-spec dolga.
> Itt csak a `60_tartalom` feature-vetülete.

### 5.1 Érintett Flutter képernyők

| Képernyő | Mit mutat | Forrás-entitás |
|---|---|---|
| `screen_kezdooldal_*` | Hír-szekció (top 5 publikált) | `News` (Published) |
| `screen_kezdooldal_*` | Városi információk szekció (3-4 kiemelt) | `CityInfo` (Published) |
| `screen_hirek_lista` | Teljes hír-lista | `News` (Published) |
| `screen_varosi_informaciok_lista` | Városi információ-lista, csoportosítva | `CityInfo` (Published), `groupLabel`-szekcionálva |
| `screen_tovabbi_menu` | Navigáció a listákra | — |

**Programok:** a feltöltött képernyőképek **nem mutatják** explicit a
programok polgári szekcióját — NY-T4 átadva a polgári adatigény-specnek.

### 5.2 Adatáramlás-irány

A feature a polgári oldal számára **kizárólag olvasás**. A push-értesítés
egyirányú oldalsó csatorna.

### 5.3 A polgári oldal által hívott végpontok (igény-jelölés)

A polgári app **dedikált polgári-auth mögötti** végpontokat hív, nem a
manager-route-okat. A pontos route-ok a polgári adatigény-spec dolga.

#### 5.3.1 Hírek — munkanév-végpontok

| Funkció | Végpont (munkanév) | Szűrés |
|---|---|---|
| Kezdőlap (top 5) | `GET /v1/citizen/news?limit=5&sort=publishedAt:desc` | `Published` |
| Teljes lista | `GET /v1/citizen/news` | `Published` |
| Adatlap | `GET /v1/citizen/news/{id}` | `Published` + `id` |

**Polgári hír-DTO mezei:** `id`, `title`, `body`, `coverImageUrl`,
`publishedAt`.

**A polgár NEM látja:** `contentStatus`, `pushOnPublish`, audit-mezők.

#### 5.3.2 Városi programok

| Funkció | Végpont (munkanév) | Szűrés |
|---|---|---|
| Program-lista | `GET /v1/citizen/events` | `Published` |
| Program-adatlap | `GET /v1/citizen/events/{id}` | `Published` |

**Polgári program-DTO mezei:** `id`, `title`, `body`, `coverImageUrl`,
`startsAt`, `endsAt`, `locationText`, `latitude`, `longitude`,
`publishedAt`.

#### 5.3.3 Városi információk

| Funkció | Végpont (munkanév) | Szűrés |
|---|---|---|
| Kezdőlap (top 3-4) | `GET /v1/citizen/city-infos?limit=3` | `Published`, `sortOrder asc` |
| Teljes lista | `GET /v1/citizen/city-infos` | ua. |

**Polgári CityInfo-DTO mezei:** `id`, `title`, `description`, `url`,
`iconRef`, `groupLabel`, `sortOrder`.

### 5.4 A `pushOnPublish` átvitele (NY-T1)

A `News.pushOnPublish == true` jelzéssel publikált hír átviteli mintája a
polgári adatigény-spec dolga. Két lehetséges minta:

- **Minta A — Pull-feed:** a polgári app szinkronizál egy "új publikált
  hírek" feedről; kliens-oldali local notification.
- **Minta B — Server-push:** a backend a T1-átmenet idején push-szolgáltatás
  (FCM/APNs) felé bocsát értesítést.

**Az admin-oldali kötelezettség:** a `News.pushOnPublish` mező mentve; a
publikálási tényt és a `pushOnPublish` jelzést a polgári oldal felé
elérhetővé tenni (feed vagy event-rendszer). **Konkrét implementáció:
polgári adatigény-spec.**

### 5.5 A polgári app és az állapot-átmenetek — hatás

| Admin-akció | Hatás a polgári oldalon |
|---|---|
| `Draft → Published` (T1) | A tartalom megjelenik; ha `pushOnPublish == true` és hír, push (NY-T1) |
| `Published → Draft` (T2) | A tartalom eltűnik |
| `Published → Archived` (T3) | A tartalom eltűnik |
| `Archived → Published` (T4) | Újra megjelenik, új `publishedAt`-tel |
| Tartalom-szerkesztés `Published`-en | Következő szinkronizációval a frissítés látszik |

### 5.6 Az adat-szerződés stabilitása

A polgári DTO-k szűkebbek az adminhoz képest. Új admin-mező alapesetben
**nem kerül át** a polgári DTO-ba — a polgári oldali megjelenítés
explicit döntést igényel. Ezzel a feature-spec és a polgári adatigény-
spec **független ütemben fejlődhet**.

---

## 6. Acceptance criteria

A 3-5. szakaszok viselkedéseit Given/When/Then formájú, gépiesen
ellenőrizhető kritériumokká fordítva. Nyolc blokk, **75 kritérium**.

### 6.1 AC-N — Hír (`News`) CRUD és borítókép

**AC-N1 — Létrehozás (8 kritérium):**

- **AC-N1.1** — *Given* érvényes `CreateNewsRequest`, *When* `POST /v1/news`,
  *Then* válasz `201`, `contentStatus = Draft`, `publishedAt = null`,
  `coverImageRef = null`, `createdAt ≈ now`, `createdBy =` aktuális
  felhasználó.
- **AC-N1.2** — *Given* a kérés `contentStatus` mezőt tartalmaz, *When*
  `POST`, *Then* `400`, `fieldErrors.contentStatus = "field_not_editable"`.
- **AC-N1.3** — *Given* a kérés `publishedAt` mezőt tartalmaz, *When*
  `POST`, *Then* `400`, `fieldErrors.publishedAt = "field_not_editable"`.
- **AC-N1.4** — *Given* a kérés `coverImageRef` mezőt tartalmaz, *When*
  `POST`, *Then* `400`, `fieldErrors.coverImageRef = "field_not_editable"`.
- **AC-N1.5** — *Given* a kérés üres/whitespace `title`, *When* `POST`,
  *Then* `400`, `fieldErrors.title` jelen van.
- **AC-N1.6** — *Given* a kérés 201 karakteres `title`, *When* `POST`,
  *Then* `400`.
- **AC-N1.7** — *Given* a kérés üres `body`, *When* `POST`, *Then* `400`,
  `fieldErrors.body` jelen van.
- **AC-N1.8** — *Given* a kérés `pushOnPublish = true`, *When* `POST`,
  *Then* `201`, `News.pushOnPublish == true`.

**AC-N2 — Szerkesztés (6 kritérium):**

- **AC-N2.1** — *Given* `Draft` `News`, *When* `PUT` érvényes mezőkkel és
  helyes `expectedUpdatedAt`-tel, *Then* `200`, mezők frissültek,
  `updatedAt ≈ now`, `contentStatus` változatlan.
- **AC-N2.2** — *Given* `Published` `News`, *When* `PUT` helyes mezőkkel,
  *Then* `200`, a tartalom frissült, `contentStatus = Published`.
- **AC-N2.3** — *Given* a `PUT`-kérés `contentStatus` mezőt tartalmaz,
  *When* feldolgozza, *Then* `400`, `field_not_editable`.
- **AC-N2.4** — *Given* a `PUT`-kérés `publishedAt` mezőt tartalmaz,
  *When* feldolgozza, *Then* `400`, `field_not_editable`.
- **AC-N2.5** — *Given* a `PUT`-kérés `coverImageRef` mezőt tartalmaz,
  *When* feldolgozza, *Then* `400`, `field_not_editable`.
- **AC-N2.6** — *Given* két párhuzamos szerkesztés, *When* a második régi
  `expectedUpdatedAt`-tel megy, *Then* `409 stale`.

**AC-N3 — Borítókép (8 kritérium):**

- **AC-N3.1** — *Given* `News` `coverImageRef = null`, érvényes JPEG (2 MB),
  *When* `POST .../cover-image` multipart, *Then* `200`, fájl S3-ban,
  `coverImageRef` az új kulcs, `coverImageUrl` érvényes.
- **AC-N3.2** — MIME `application/pdf`, *Then* `400 invalid_mime_type`.
- **AC-N3.3** — 6 MB fájl, *Then* `400 file_too_large`.
- **AC-N3.4** — 500 byte fájl, *Then* `400 file_too_small`.
- **AC-N3.5** — Nem létező `id`, *Then* `404`.
- **AC-N3.6** — *Given* meglévő `coverImageRef`, *When* új feltöltés,
  *Then* `200`, új fájl S3-ban, régi S3-objektum törlésre kerül.
- **AC-N3.7** — *Given* meglévő `coverImageRef`, *When* `DELETE
  .../cover-image`, *Then* `204`, S3-objektum törölve, `coverImageRef = null`.
- **AC-N3.8** — *Given* `coverImageRef = null`, *When* `DELETE
  .../cover-image`, *Then* `204` (idempotens).

### 6.2 AC-E — Városi program (`Event`)

**AC-E1 — Létrehozás (11 kritérium):**

- **AC-E1.1** — Érvényes `CreateEventRequest`, *Then* `201`, `Draft`.
- **AC-E1.2** — `endsAt < startsAt`, *Then* `400 ends_at_before_starts_at`.
- **AC-E1.3** — `endsAt == startsAt`, *Then* `201`.
- **AC-E1.4** — `endsAt = null`, *Then* `201`.
- **AC-E1.5** — Csak `latitude` (`longitude = null`), *Then* `400
  coordinates_must_be_both_or_neither`.
- **AC-E1.6** — Csak `longitude`, ua.
- **AC-E1.7** — `latitude = 95.0`, *Then* `400`.
- **AC-E1.8** — `longitude = -185.0`, *Then* `400`.
- **AC-E1.9** — `latitude = 47.5`, `longitude = 19.05`, *Then* `201`.
- **AC-E1.10** — `locationText` 301 karakteres, *Then* `400`.
- **AC-E1.11** — `startsAt` múltbeli, *Then* `201`.

**AC-E2 — Szerkesztés (2):** ua. mint a News + koordináta-páros.

**AC-E3 — Borítókép:** ua. mint AC-N3.

### 6.3 AC-C — Városi információ (`CityInfo`)

**AC-C1 — Létrehozás (12):** `title`, `url` (HTTPS-only különböző
nem-HTTPS-kkel), `description`, `iconRef` whitelist, `groupLabel`,
`sortOrder` tiltott.

**AC-C2 — `groupLabel` autocomplete (3):** distinct, query-filter, üres
DB.

**AC-C3 — Reorder (4):** sorrend-beállítás, nem létező `id`, duplikátum,
üres-no-op.

### 6.4 AC-T — Állapot-átmenetek (közös)

**AC-T1 — `Draft → Published`:** `contentStatus = Published`,
`publishedAt ≈ now`.

**AC-T2 — `Published → Draft`:** `contentStatus = Draft`,
`publishedAt = null`.

**AC-T3 — `Published → Archived`:** `contentStatus = Archived`,
`publishedAt` változatlan.

**AC-T4 — `Archived → Published`:** `contentStatus = Published`,
`publishedAt ≈ now` (új).

**AC-T5 — Tiltott:** `Draft → Archived` → `409 invalid_transition`;
`Archived → Draft` → ua.; önmagába-átmenet → ua.

**AC-T6 — Konkurrencia:** régi `expectedUpdatedAt` → `409 stale`; nem
létező `id` → `404`; érvénytelen enum → `400`.

### 6.5 AC-V — Validáció (HTML-szanitizáció)

**AC-V1 — Szanitizáció (10):** whitelist tag-ek átmennek; `<script>`,
`<iframe>`, inline `style`, `on*` event-handler eltávolítva; `<a href>`
csak `https://`-t enged; `<a href="javascript:">` és `<a href="http://">`
URL eltávolítva, tag-tartalom megmarad; szanitizáció után üres tartalom
`400 invalid_html_or_empty_after_sanitization`.

**AC-V2 — Méret-korlát:** 9999 OK; 10001 → `400`.

**AC-V3 — Whitelist pozitív lefedés:** minden tag és attribútum az
elvárt formában tárolt.

### 6.6 AC-A — Admin felület

**AC-A1 — Lista alap-szűrése:** alapból `Draft + Published`; checkbox
hozzáadja `Archived`; URL query a szűrőt felülírja.

**AC-A2 — Kebab-menü:** állapot-érzékeny akciók (4.2.2 szerint).

**AC-A3 — Bulk akciók:** szelekciós szűréssel a vegyes batch megerősítő
dialógusban.

**AC-A4 — Üres állapot:** üres-illusztráció + akció-gomb.

**AC-A5 — Borítókép-flow:** új hír űrlapján a feltöltő-gomb le van tiltva
az első mentésig; mentés után aktiválódik.

**AC-A6 — Karakterszámláló:** 5000 / 10000 semleges; 10001 piros, Mentés
letiltott.

### 6.7 AC-J — Jogosultság (Szabály-R3)

**AC-J1 — Pozitív:** `tenant_content_manager`, `tenant_manager`,
`tenant_dispatcher + tenant_content_manager` (K-025) — minden
tartalom-route-ot hív.

**AC-J2 — Negatív:** csak `tenant_dispatcher` → `403`; `tenant_field_worker`
→ `403`; hitelesítetlen → `401`.

**AC-J3 — Cross-tenant védelem:** `tenant-A` felhasználó `tenant-B` `id`-jét
kéri → `404`.

### 6.8 AC-D — Delete-guard

**AC-D1 — Single DELETE:** `Draft` és `Archived` törölhető; `Published`
→ `409 cannot_delete_published`.

**AC-D2 — Bulk DELETE atomicitás:** vegyes batch → `409`, semmi nem
törlődik; mind `Draft` → `204`; `Draft + Archived` → `204`.

**AC-D3 — A többi entitáson:** `Published` `Event`/`CityInfo` ua.
`409 cannot_delete_published`.

---

## 7. Keresztmetszeti

### 7.1 i18n

Minden UI-szöveg kulcs alapú (`hu.json`); szerver kulcsot/kódot ad, nem
kész szöveget. A `content.*` kulcs-fa teljes készlete a 4.5-ben.

### 7.2 Időzóna

A `News.publishedAt`, `Event.startsAt`/`endsAt`, és minden audit-mező
UTC-ben tárolt; a megjelenítés a tenant-időzónában (alap
`Europe/Budapest`, SD-9/SD-10). Tenant-időzóna változása esetén a tárolt
UTC változatlan, a megjelenítés újraértékelődik.

### 7.3 Teljesítmény

Pilot-volumenen néhány tucat tétel entitásonként — nincs külön
cache-mechanizmus. A `groupLabel`-autocomplete egy egyszerű distinct
query, cache-elhető iterációsan.

### 7.4 Biztonság

A HTML-szanitizáció (SD-69, 3.1.3) az XSS-rés legfontosabb védelme. A
`<a href>` csak `https://`-t enged (`javascript:`, `data:` szűrve). A
`CityInfo.url` mező is csak `https://`-t enged. Cross-tenant védelem a
platform-szintű header-resolution-ön (AC-J3). Optimistic concurrency
(`expectedUpdatedAt`) minden szerkesztésen.

---

## 8. Lezárás

### 8.1 Első kiadás (MVP) — T0-ra szállítva

**Szerver:**
- 27 új API-route (4.1.1), Szabály-R3 érvényesítve
- 3 új controller
- `ContentStatus` enum, állapotgép (SD-70)
- HTML-szanitizáció (SD-69)
- Borítókép-feltöltés dedikált végpontokon (SD-68)
- `CityInfo.url` HTTPS-only
- `CityInfo` reorder
- `/v1/city-info-groups` distinct lookup (SD-71)
- Delete-guard mindhárom entitáson
- Optimistic concurrency

**Admin felület:**
- 3 új aloldal a Tartalom menü alatt
- 3 `TableStateConfig` lista-konfigurációval
- 3 szerkesztő-űrlap a mezősablonok szerint
- Rich-text-szerkesztő (PrimeNG Editor)
- Borítókép-feltöltő komponens
- `groupLabel` autocomplete
- Reorder drag-and-drop
- Kebab-menü állapot-érzékenyen
- Bulk akciók
- Üres állapot
- `content.*` i18n-kulcsok teljes készlete

**Domain-modell:**
- `News.pushOnPublish` új mező
- `body` méret-cap és HTML-formátum dokumentálva
- `CityInfo.groupLabel`/`url`/`iconRef` méret-korlátok rögzítve
- SD-23 megerősítve

**Globális állapotgép:**
- `02_globalis_allapotgep.md` új blokkal: `ContentStatus`

### 8.2 Iterációs elemek (6-12. hó vagy később)

| Elem | Indok |
|---|---|
| Ütemezett publikálás | K-016 pilot-fegyelem |
| Jóváhagyási workflow | K-016 |
| Verziótörténet / `ContentAuditLog` | NY-K4 mintával |
| Hír-kategóriák, címkék | Pilotra egyszerű lista elég |
| Kiemelt/rögzített hír | Pilotra nincs igény |
| Beágyazott videó, galéria a `body`-ban | Whitelist-bővítés |
| Teljes WYSIWYG | Minimális rich-text elég |
| Beágyazott térkép-szelektor (NY-T2) | Pilotra két szám-input elég |
| Soft-delete tartalmakra | Guard + manuális archiválás elég |
| Tömeges `groupLabel`-átnevezés | Ritka eset |
| S3 árva-takarítás (NY-K3) | Volumenen elhanyagolható |
| `coverImageRef` `Attachment`-általánosítás (VF-K1) | SD-68 elég a pilotra |
| `iconRef` soft-deprecation | Ritka eset |
| Bulk `groupLabel`-szerkesztés | Egyenként elég |
| Presence-mechanizmus | Kis volumenű probléma |
| Tartalom-kategorizálás | Iterációs igény |

### 8.3 Hatókörön kívüli — más spec hatásköre

| Elem | Hova tartozik |
|---|---|
| GYIK-szerkesztő | Urbino-admin (K-039) |
| Ötletláda manager-oldali kezelése | Koncepció: 6-12. hó |
| Közvélemény-kutatás | Koncepció: 12-18. hó |
| Polgári mobilapp értesítési rendszere | Polgári adatigény-spec (NY-T1) |
| Polgári UX a publikált tartalomra | Polgári adatigény-spec |
| `DefaultCategoryCatalog` mezőtáblája | Urbino-admin (NY-K1) |

### 8.4 Feltételezések (explicit lista)

**Platform-szintű:**
- `BaseController` minta áll (CRUD, reorder, bulk delete)
- `AuditableEntity`, FluentValidation, OpenAPI auto-discovery működnek
- Az admin-felület `TableStateConfig` direktívái és reorder-drag-handle
  elérhetők
- PrimeNG v21 (Aura, Blue) rich-text-szerkesztő (`p-editor`) elérhető és
  HTML-formátumban ad vissza tartalmat
- Multi-tenancy modell tisztázott
- HtmlSanitizer (vagy ekvivalens) NuGet-csomag használható

**Tartalom-szintű:**
- 10 000 karakteres `body` méret-cap elegendő a pilotra
- Közös ikonkészlet tenant-független, platform-szintű
- S3 (vagy ekvivalens blob storage) elérhető borítóképekhez
- A polgári mobilapp értesítési rendszere a polgári adatigény-spec
  hatásköre (NY-T1)

**Felhasználói:**
- A tartalomkezelő tipikusan 1-2 ember
- Pilot-volumen alacsony (néhány tucat tétel entitásonként)

### 8.5 Nyitott kérdések és átadott szálak

| # | Tárgy | Típus | Hatáskör |
|---|---|---|---|
| **NY-T1** | A `News.pushOnPublish` átvitele a polgári oldalra (minta A vagy B; FCM/APNs) | Átadott szál | Polgári adatigény-spec |
| **NY-T2** | Beágyazott térkép-szelektor a programok koordinátáihoz | Iterációs UX-igény | Iteráció |
| **NY-T3** | `CityInfo.groupLabel` szerinti szekcionálás a polgári listán | Átadott szál | Polgári adatigény-spec |
| **NY-T4** | Programok megjelenítési modellje a polgári oldalon | Átadott szál | Polgári adatigény-spec |
| **NY-T5** | `News.body` és `Event.body` HTML-rendelés a Flutter polgári oldalon | Átadott szál | Polgári adatigény-spec |
| **NY-T6** | Polgári end-to-end auth (`/v1/citizen/...`) | Átadott szál | Polgári adatigény-spec |
| **VF-T1** | Admin-route mintázat egységesítése (`/varosi_programok` vs. `/programok`) | Visszacsorgó jelzés | Funkcionális projekt |
| **NY-K1** | `DefaultCategoryCatalog` mezőtáblája | Megtartott hiány | Urbino-admin spec |
| **NY-K3** | S3 árva-objektum-takarítás (örökölt) | Megtartott hiány | Üzemeltetési / iterációs |

Mind a 9 szál **a feature gerincét nem érinti** — az admin oldali
tartalom-előállítás teljesen kész nélkülük.

### 8.6 Új tervezési döntések — összefoglaló

| SD | Tárgy |
|---|---|
| **SD-68** | `coverImageRef` az SD-60 "egyedi-fájl-mező" minta szerint a `News`-on és `Event`-en |
| **SD-69** | `News.body` / `Event.body` HTML-tárolás, szanitizációs whitelist, max 10 000 char |
| **SD-70** | `ContentStatus` állapotgép három állapot + négy átmenet; T3 és T2 külön akció; `DELETE` állapot-érzékeny |
| **SD-71** | `CityInfo.groupLabel` szabad szöveg (max 100), autocomplete distinct-végpontról |
| **SD-23** | **Megerősítve** — tartalmi szerző = `createdBy`-ra joinolt `TenantUser` |

Implementációs választás: **PrimeNG Editor** a rich-text-szerkesztőhöz.

### 8.7 A kísérő-dokumentumok érintése

- **`00_domain_model.md` v1.7 → v1.8** — 4. blokk frissítés (új mező,
  méret-korlátok, lábjegyzetek)
- **`02_globalis_allapotgep.md`** — új blokk: `ContentStatus`
- **`99_donesnaplo.md`** — SD-68, SD-69, SD-70, SD-71 új bejegyzések;
  SD-23 megerősítő bejegyzés
- **`CLAUDE.md` v1.2** — 6. szakasz frissítése (MH-J1 teljes lezárás); 3.
  szakasz SD-68 mintázat megemlítése
- **`05_jogosultsag_es_authorization.md`** — 3.2 G-blokk kitöltve 27
  route-tal; MH-1 és MH-2 lezárva erre a modulra

### 8.8 Hivatkozott dokumentumok

**Funkcionális források:**
- `60_tartalom_v2.md`
- `00_architektura_v4.md`
- `05_jogosultsagok_v2.md`
- `90_sitemap_v3.md`

**Spec-szintű kísérő-dokumentumok:**
- `00_domain_model.md` v1.7
- `00_terminologia.md` v0.5
- `02_globalis_allapotgep.md`
- `99_donesnaplo.md`
- `CLAUDE.md` v1.2

**Kanonikus döntések:** K-007, K-008, K-016, K-024, K-025, K-027, K-028,
K-038, K-039.

**Hivatkozott korábbi SD-k:** SD-1, SD-9, SD-10, SD-15, SD-21, SD-22,
SD-23, SD-32, SD-60, SD-61, SD-63, SD-64, SD-66.

**Referencia-dokumentumok:**
- `05_jogosultsag_es_authorization.md` — Szabály-R3
- `30_beallitasok.md` — VF-K1 forrása, SD-60 mintázat eredete
- `20_felhasznalokezeles.md` — `TenantUser`-projekció (SD-3)

**Platform-szintű:**
- `01_kozos_mintak.md`
- `project_backend` `CLAUDE.md`
- `project_backend_client_angular` `CLAUDE.md`

**Polgári oldali hivatkozások:**
- `screen_hirek_lista`, `screen_varosi_informaciok_lista`,
  `screen_kezdooldal_*`, `screen_tovabbi_menu`

---

## Verziónapló

- **v1.0 (2026-05-20)** — Első kiadás. A `60_tartalom` modul teljes
  feature-spec-je. 27 új API-route az `authorization.json`-höz (MH-J1
  utolsó modulja lezárva); 4 új SD (SD-68, SD-69, SD-70, SD-71); SD-23
  megerősítése; NY-6, NY-7 lezárása; MH-2 lezárása erre a modulra; 75
  acceptance criterion 8 blokkban; 6 új átadott szál a polgári adatigény-
  spec felé (NY-T1 – NY-T6); 1 visszacsorgó jelzés a funkcionális projekt
  felé (VF-T1). A `30_beallitasok.md` VF-K1 visszacsorgó jelzése befogadva
  az SD-68-tal. A `00_domain_model.md` v1.7 → v1.8 frissítés; a
  `02_globalis_allapotgep.md` új `ContentStatus`-blokkal bővül; a
  `CLAUDE.md` 6. szakasza a hézagos modulokra vonatkozóan **teljesen
  zárható** (már nincs hézagos modul). Implementációs választás:
  PrimeNG Editor.
