# STROMCOM.cz — instrukce pro AI asistenta k integraci

> Tento soubor je určený **AI asistentovi** (Claude, ChatGPT, Copilot, Cursor…), který
> pomáhá vývojáři zapojit STROMCOM.cz do jeho interního systému. Ulož ho do repozitáře
> (např. `docs/stromcom.md`) a odkaž na něj z `CLAUDE.md` / `AGENTS.md` / pravidel editoru.
>
> Aktuální verze: https://www.stromcom.cz/public/static/stromcom-ai.md
> Lidský přehled: https://www.stromcom.cz/ai-integrace/

---

## 0. Pravidla, která nikdy neporušuj

1. **Nikdy nesahej do datové vrstvy STROMCOM.**
   - `window.stromComDL` (nebo `window.<název>DL`) je **interní** objekt snippetu. Nečti ho,
     nezapisuj do něj, nevolej na něm `.push()`, nepřiřazuj mu nový objekt, nedrž si na něj
     referenci. Snippet ho po načtení nahradí proxy a čte z něj data v interním tvaru —
     jakýkoli přímý zápis se tiše ztratí nebo rozbije vykreslení.
   - Se snippetem komunikuj **výhradně přes fasádu `window.stromCom`** (`initUser`, `thread`,
     `home`, `conf`) nebo přes oficiální balíky (PHP / React) a Customer API.
   - Nesahej do vnitřku widgetu: Shadow DOM, iframe aplikace, `postMessage` protokol mezi
     stránkou a iframem, URL parametry iframu, interní třídy a CSS selektory uvnitř. Nic z toho
     není veřejné API a mění se bez ohlášení. Vzhled měň jen přes podporované CSS klíče (§7.4).
   - Nevolej App API (`/api/app/v1/`) ze serveru jako „integrační“ API — je to interní API
     widgetu jednající jménem koncového uživatele. Pro serverovou integraci je Customer API (§10).
   - Neobcházej limity plánu (např. zakládáním více projektů, rotací uživatelů).
2. **Tajemství patří na server.** Klíč typu `customer`/`both` (Customer API), tajný klíč pro
   hashování kódů a secret WebHooků nikdy nesmí do HTML, JS bundlu, mobilní aplikace ani URL.
3. **Identitu uživatele určuje backend zákazníka**, ne prohlížeč. Snippet vykresli až po
   ověření přihlášení ve vlastní aplikaci (§5).
4. **Neodhaduj API.** Když tu něco chybí, podívej se do OpenAPI specifikace
   (https://www.stromcom.cz/docs/customer/openapi.json) nebo se zeptej vývojáře. Nevymýšlej
   metody fasády, conf klíče ani endpointy, které tu nejsou.
5. **Pokud oficiální balík danou věc neumí, nepatchuj ho a nepiš kolem něj hacky** — přepni na
   ruční JS integraci přes `window.stromCom` (§6), která umí všechno. Kdy přesně, viz §8.4 a §9.4.
6. **Měň jen to, co integrace potřebuje.** Úpravy omez na vložení snippetu, předání uživatele,
   kontejnery vláken, konfiguraci widgetu a volání Customer API / WebHooky. Nesahej do ostatní
   logiky, dat, databáze, autentizace ani nastavení zákazníkova systému a nedělej refaktory
   „po cestě“. Když integrace zásah jinam opravdu vyžaduje (např. nový sloupec, změna routy,
   úprava CSP hlavičky), nejdřív to vývojáři popiš a počkej na souhlas.

---

## 1. Co STROMCOM je

STROMCOM.cz je B2B SaaS pro **interní firemní komunikaci v kontextu business entit**.
Zákazník vloží JS snippet do svého **interního** systému (administrace e-shopu, ERP, CRM,
helpdesk) a jeho **zaměstnanci** pak u konkrétní objednávky / ticketu / zakázky vedou vlákno
zpráv (zmínky `@kolega`, přílohy, reakce, notifikace).

**Není to live chat pro návštěvníky webu.** Neintegruj ho na veřejný web pro anonymní
uživatele — každý uživatel widgetu je přihlášený zaměstnanec zákazníka.

### Doménový model

| Pojem | Význam | Identifikace |
|---|---|---|
| Zákazník | firma, která si službu platí; spravuje se v panelu `/customer/` | účet v panelu |
| Projekt | jedna integrace (jeden systém). Má plán (předplatné), klíče a uživatele | `clientKey` |
| Uživatel | zaměstnanec, který píše zprávy | `code` = tvoje stabilní ID (hash), STROMCOM přidělí `hash` |
| Vlákno | diskuse u jedné business entity | `code` = tvůj klíč entity, např. `order-12345` |
| Zpráva | HTML zpráva ve vlákně, s přílohami a reakcemi | `hash` |

Uživatelé i vlákna vznikají **automaticky** při prvním použití ve snippetu (pokud to projekt
povoluje — výchozí stav ano). Nic není potřeba předem synchronizovat.

---

## 2. Registrace a nastavení projektu (dělá člověk, AI navede)

AI nemůže zaregistrovat účet ani vygenerovat klíče — vyzvi vývojáře k těmto krokům:

1. **Registrace:** https://www.stromcom.cz/registration/ (jméno, příjmení, e-mail, heslo;
   firma / IČO / DIČ volitelně). Přihlásit jde i přes Google, Microsoft, GitHub, Facebook.
   Když je registrace v režimu jen na pozvánku, je potřeba kód pozvánky.
2. **Potvrzení e-mailu:** odkaz z e-mailu (`/confirmation/emailAddress/?token=…`). Službu
   neblokuje, ale bez potvrzení nejde obnova hesla. Znovu poslat jde z profilu.
3. **Založení projektu:** panel → `/customer/project/create/` → výběr plánu → název projektu.
   Placený plán vytvoří objednávku k úhradě. Nový projekt může začít zkušebním obdobím;
   aktuální podmínky jsou na ceníku (§3).
4. **Klíče:** `/customer/project/{id}/credentials/` (záložka **API Klíče**).
   - **Client Key** — 50 znaků `[a-zA-Z0-9]`, veřejný identifikátor projektu.
   - **Client Secret** (bearer token) — 50 znaků, je v detailu klíče
     `/customer/project/{id}/credentials/{credential_id}/` (tlačítko kopírovat). Tamtéž hotový snippet.
   - **Typ klíče:**
     | Typ | Panel | K čemu |
     |---|---|---|
     | `app` | „Snippet API (výchozí)“ | do snippetu v prohlížeči — **jediný typ, který smí do HTML** |
     | `customer` | „Projekt API (vlastní)“ | server→server Customer API; nikdy do prohlížeče |
     | `both` | „Obojí“ | nedoporučeno — v prohlížeči by odhalil Customer API |
     Typ `customer`/`both` jde zvolit jen s plánem, který má API. Doporučení: **dva klíče** —
     výchozí `app` pro snippet, samostatný `customer` pro server.
   - **Omezení klíče:** „Dovolené adresy (origin)“ (např. `https://admin.example.com, https://*.example.com`),
     „Povolené IP (IPv4)“ (masky i CIDR), expirace. Snippetový klíč omez na origin interního
     systému, serverový na IP serveru.
   - Klíč jde **přegenerovat** (starý přestane platit). Výchozí klíč nejde smazat.
5. **Další nastavení projektu** (záložky pod `/customer/project/{id}/`): Uživatelé, Skupiny,
   WebHooky, Konfigurace editoru (pravidla URL → entita, §7.5), Nastavení (vlastní CSS…),
   Konfigurace (mj. „Důvěřovat jakékoli doméně“ = vypnutí kontroly originu), Náhled, Integrace.
6. **Kolegové v panelu** (správa fakturace, projektů): `/customer/user/` s rolemi Administrátor,
   Správce, Fakturace. To jsou účty do panelu, **ne** uživatelé widgetu.

Vyzkoušet bez integrace: https://www.stromcom.cz/playground/ (vygeneruje kód pro JS / PHP / React
proti vlastnímu projektu).

---

## 3. Plány a co odemykají

Plány, ceny i limity se mění — **nikdy je neopisuj z paměti ani z tohoto souboru.**

- Lidský ceník: https://www.stromcom.cz/cenik/
- **Strojově čitelný ceník: https://www.stromcom.cz/cenik.json** (veřejný, bez autentizace;
  `/sk/cenik.json` = slovenský trh v EUR). Načti ho, když potřebuješ zjistit, který plán
  odemyká funkci, kterou integrace potřebuje:

```json
{"status":"success","data":{
  "plans":[{"name":"…","slug":"…","url":"/balicek/…/","type":"free|subscription|…",
            "available":<bool>,"currency":"CZK","pricing":[{"pricePerMonth":…,"pricePerYear":…,"vatRate":…}],
            "features":{"maxActiveUser":<int>,"maxThreadInMonth":<int|null>,"attachmentMaxSize":<int>,
                        "allowProjectCustomization":<bool>,"allowAPI":<bool>,"allowEmbed":<bool>,"allowWebHook":<bool>,…}}],
  "features":[{"code":"maxActiveUser","name":"…","type":"limit","unit":null},…]}}
```
  - `type: "limit"` → číslo, `null` = neomezeno; `type: "boolean"` → `true`/`false`, nebo
    `"inDevelopment"` (funkce se připravuje — nepočítej s ní).
  - Klíče, které integraci rozhodují: `allowAPI` (Customer API, §10), `allowEmbed` (§10.4),
    `allowWebHook` (§11), `allowUserGroup` (skupiny), `allowProjectCustomization` (`appCSSPath`, §7.4),
    `maxActiveUser`, `maxThreadInMonth`, `attachmentMaxSize` (MB), `messageMaxHistory` (měsíce).
  - `available: false` → plán teď nejde objednat (nákup je dočasně vypnutý), `pricing` je prázdné.
  - Plány mimo veřejný ceník (zkušební období, individuální smlouvy) v JSON nejsou.
- Co má **konkrétní projekt**, je v panelu na záložce Předplatné, přes Customer API
  `GET project/` (pole `allow*` = skutečně platná hodnota) — to je zdroj pravdy pro běžící integraci.

Doplňky (uživatelé, úložiště, balíčky vláken) jdou dokoupit v záložce Doplňky.

**Jak se projeví limit:**
- Customer API vrací **HTTP 402** s kódem `USER_LIMIT_REACHED`, `THREAD_LIMIT_REACHED`,
  `MESSAGE_LIMIT_REACHED`, `STORAGE_LIMIT_REACHED`, `MESSAGE_STORAGE_LIMIT_REACHED`.
- Uživatelé nad limit jsou **uspáni** (suspended), založení přes API nad limit vytvoří uživatele
  rovnou uspaného (s varováním v `meta.warnings`).
- Zprávy starší než historie plánu se nevracejí.
- Měsíční limity se nulují 1. dne v měsíci 00:00 UTC.
- Funkce, kterou plán nemá, vrací `PROJECT_FUNCTION_NOT_ALLOWED` (HTTP 401) s `details.function`
  (`embed`, `webHook`, `userGroup`, …); nastavení mimo plán se v `PATCH project/` tiše ignoruje.
- `appCSSPath` / vlastní CSS aplikace se bez nároku tiše nepoužije (rám widgetu jde stylovat vždy).

Když integrace potřebuje funkci, kterou plán nemá, **řekni to vývojáři** a navrhni upgrade
(`/customer/project/{id}/subscription/upgrade/`) — nehledej obchvat.

---

## 4. Ověření (co existuje a co ne)

| Ověření | Stav | Jak |
|---|---|---|
| E-mail zákazníka / účtu | existuje | odkaz v e-mailu po registraci, znovu z profilu |
| Omezení klíče na origin | existuje, volitelné | „Dovolené adresy (origin)“ u klíče; bez nastavení widget běží s varováním |
| Omezení klíče na IP | existuje, volitelné | „Povolené IP“ u klíče — ideální pro serverový klíč a intranety |
| Expirace klíče | existuje, volitelné | v detailu klíče |
| Podpis WebHooků | existuje, volitelné | `secret` u WebHooku → hlavička `X-Stromcom-Signature` (§11) |
| Podepsaná identita uživatele ve snippetu (JWT/HMAC) | **neexistuje** | viz §5 — chraň se hashováním kódů |
| Ověření firmy (ARES/VIES), domény (DNS TXT), KYC | **neexistuje** | nenavrhuj to jako krok integrace |

---

## 5. Bezpečnostní model identity — přečti před psaním kódu

- Snippet posílá na server `Bearer <clientSecret>:<userCode>@<origin>`. Server ověří **projekt**
  podle secretu a uživatele najde **jen podle `code`**. Neexistuje podpis ani token per uživatel.
- `clientSecret` snippetového klíče je **nutně veřejný** (je v HTML v `data-cs`). Kdo ho zná a
  uhodne `code` uživatele, může se za něj vydávat.
- Proto:
  1. **Nikdy nepoužívej jako `code` čitelné interní ID** (`1`, `42`, e-mail). Posílej
     **HMAC hash** ID spočítaný na serveru vlastním tajným klíčem (doporučeně HMAC-SHA256,
     base62 nebo hex). Klíč pro hash drž na serveru a **neměň ho** — změna = všichni uživatelé
     i vlákna dostanou novou identitu.
  2. Totéž doporučeně pro `code` vláken (aby nešlo hádat cizí entity).
  3. Omez snippetový klíč na origin interního systému (případně IP).
  4. `initUser` vypisuj **až po ověření přihlášení** ve vlastní aplikaci; nepřihlášenému nic.
  5. Read-only uživatele (např. externí) označ `readOnly: true`.
- Pravidla kódů (server je vynucuje):
  - **uživatel** `code`: 1–100 znaků `[a-zA-Z0-9._@+-]` (zakázané `:` a `/`). Po založení je
    to identita uživatele — musí být stabilní.
  - **vlákno** `code`: 1–100 znaků `[a-zA-Z0-9_-]`. Doporučený tvar `{typ}-{id}` (`order-12345`),
    resp. hash z toho.

Hash v PHP bez balíku (stejný výsledek jako `stromcom/php-snippet` s `base62: false`):

```php
$userCode = hash_hmac('sha256', 'user-' . $user->id, $_ENV['STROMCOM_CODE_SECRET']); // 64 hex znaků
```

V Node.js: `crypto.createHmac('sha256', secret).update('user-' + id).digest('hex')`.

---

## 6. Rozhodovací strom — kterou cestu integrace zvolit

```
Kde poběží widget?
├─ Ve webové aplikaci, kde ovládám HTML
│   ├─ Server-renderované stránky v PHP (klasická administrace, Laravel/Nette/Symfony bez SPA)
│   │     → stromcom/php-snippet (§8); dynamiku po načtení dopiš ručně přes window.stromCom
│   ├─ React SPA / Next.js
│   │     → @stromcom/react-snippet (§9); co neumí, přes useStromcom() (§9.4)
│   └─ Cokoli jiného (Vue, Angular, Svelte, jQuery, Python/Java/.NET backend, Shoptet admin…)
│         → ruční JS přes window.stromCom (§7); hashe kódů počítej na backendu
├─ V desktopové / mobilní aplikaci, webview, nebo nemám kontrolu nad HTML / přísné CSP
│     → Embed odkaz z Customer API (§10.4) — plán s Embed
└─ Na serveru bez UI (synchronizace, automatické zprávy, reporty)
      → Customer API: PHP SDK stromcom/customer-sdk nebo REST (§10) — plán s API
      → reakce na události → WebHooky (§11) — plán s WebHooky
```

Cesty jdou kombinovat (snippet v administraci + Customer API pro systémové zprávy + WebHooky).

---

## 7. Ruční JS integrace (`window.stromCom`) — referenční, umí vše

### 7.1 Bootstrap snippet

Vlož **jednou** na stránku, co nejdřív (do `<head>`). Vytvoří fasádu `window.stromCom`,
která volání do načtení skriptu zafrontuje — pořadí volání pak nehraje roli.

```html
<script>
(function(win, d, e, l, k, s) {
  var dl=l+'DL',c=function(s,n,a){n=n||s;(a?win[dl][n]=[]:null);win[l][s]=function(...p){a?win[dl][n].push(p):win[dl][n]=p;}},f=d.getElementsByTagName(e)[0],j=d.createElement(e);
  win[dl]=win[dl]||{};win[l]={};c('initUser','user');c('thread','threads',!0);c('conf');c('home',0,!0);
  ;j.async=true;j.dataset.type='stromcom';j.dataset.l=l;j.dataset.dl=dl;j.dataset.ck=k;j.dataset.cs=s;
  j.src = "https://cdn.stromcom.cz/loader.js?"+k;f.parentNode.insertBefore(j,f);
})(window, document, 'script', "stromCom", "<CLIENT_KEY>", "<CLIENT_SECRET>");
</script>
```

- Tento kód **neupravuj** kromě posledního řádku (klíče, případně název fasády).
  Je to jediné místo, kde se `stromComDL` legitimně objevuje — v tvém kódu nikde jinde.
- `<CLIENT_SECRET>` = secret klíče typu **`app`**.
- Výchozí jazyk před `conf` jde dát přes `j.dataset.lang='cs';` (`en`, `cs`, `sk`).
- Pro CSP bez `'unsafe-inline'` dej skriptu `nonce`, nebo ho přesuň do vlastního `.js` souboru (§12).
- Staging prostředí: `https://cdn.staging.stromcom.cz/loader.js` (jen pokud ho máte přidělené).

### 7.2 Fasáda — čtyři metody

Žádná nic nevrací a nejde z nich nic přečíst.

| Metoda | Chování |
|---|---|
| `stromCom.initUser(user)` | nastaví aktuálního uživatele; **přepíše** předchozího |
| `stromCom.conf(options)` | nastaví konfiguraci; **přepíše celou** předchozí (nespojuje!) |
| `stromCom.thread(element, thread)` | vykreslí vlákno do elementu; každé volání **přidá** další |
| `stromCom.home(element)` | vykreslí přehled vláken / notifikací do elementu |

```js
stromCom.initUser({
  code: 'Xb3k…',                       // povinné — HMAC hash ID z backendu (§5)
  name: 'Jana Nováková',               // povinné — bez jména se widget nevykreslí!
  emailAddress: 'jana@firma.cz',       // volitelné — e-mailové notifikace
  readOnly: false,                     // volitelné — true = jen čte
  avatarURL: 'https://…/jana.png',     // volitelné
});

stromCom.thread(document.querySelector('#stromcom-thread'), {
  code: 'order-12345',                 // povinné (nebo hash existujícího vlákna)
  name: 'Objednávka 12345',            // výchozí document.title
  url: 'https://admin.example.com/orders/12345', // odkaz zpět z notifikací; výchozí location.href
  userHint: true,                      // false = bez nabídky @zmínek (vlákno jako poznámky)
  attributes: [                        // experimentální: max 20 položek, krátké hodnoty
    { name: 'Stav', value: 'Expedováno' },
  ],
});

stromCom.home(document.querySelector('#stromcom-home'));
```

- `element` musí být **HTMLElement** (nebo funkce / Promise vracející element), ne selektor.
- Widget **nic nevykreslí, dokud `initUser` nemá `code` i `name`** — v konzoli je
  `[stromcom][user] not set, waiting…`.
- Kontejneru vlákna dej výšku (např. `height: 500px`), jinak bude prázdný pruh.
- Více vláken na stránce = více volání `thread()` do různých elementů.

### 7.3 `conf` — všechny klíče

**Volá se jednou s kompletním objektem.** Každé další volání nahradí celou konfiguraci —
když měníš jeden klíč, pošli znovu všechny (drž si objekt u sebe a slučuj ho sám).

| Klíč | Typ / výchozí | Účel |
|---|---|---|
| `theme` | `'stromcom-default' \| 'stromcom-dark' \| null` | téma; mění se živě |
| `language` | `'en' \| 'cs' \| 'sk' \| null` (null = jazyk prohlížeče, fallback en) | změna znovu načte iframy |
| `appFrameCSSPath` | `string \| string[]` | CSS rámu widgetu (shadow root kolem iframu) — bez omezení plánem |
| `appCSSPath` | `string \| string[]` | CSS uvnitř aplikace (iframe) — **jen plán s vlastním vzhledem** |
| `notificationElementPosition` | `null` (= žádná bublina), `1` vlevo nahoře, `2` vpravo nahoře, `3` vpravo dole, `4` vlevo dole | plovoucí bublina s počtem nepřečtených |
| `notificationElementShowAlways` | `false` | bublina i při nule |
| `notificationElementStyles` | `{cssProp: value}` | inline styly bubliny, typicky `{zIndex: 1000}` |
| `notificationElementCSSPath` | `string \| false` | **nahradí** výchozí CSS bubliny (`false`/`''` = žádné) |
| `notificationElementNoShadowRoot` | `false` | bublina do běžného DOM (výchozí CSS se pak nevkládá) |
| `notificationElementTargetElement` | element / funkce / Promise | kam bublinu vložit |
| `onNotification` | `(payload) => void` | data notifikací při každém dotazu (~120 s) — vlastní počítadlo v menu |
| `notificationRenderer` | `(api, dl, builder) => updateFn?` | úplně vlastní vykreslení bubliny |
| `notificationElementBeforeRender`, `notificationElementAfterRender` | `(api, dl, builder)` | háčky kolem bubliny |
| `homeBeforeRender` | `(node, homeAppApi, api)` | před otevřením přehledu po kliknutí na bublinu |
| `onLoad` | `(api)` | start widgetu; `api.on(api.EVENT.APP_ADD, (app) => …)`; `app.show/hide/openThread/openHome/maximalize/normalize` |
| `entityResolve` | `async ({type, id, groups, url}) => ({title, url?, fields:[{label, value}]})` | náhled entity při najetí na odkaz v zprávě (§7.5) |

Callbacky berou funkci nebo Promise funkce. Výjimka v callbacku se zaloguje a klíč se chová
jako nenastavený. Neznámé klíče se ignorují.

```js
stromCom.conf({
  language: 'cs',
  theme: 'stromcom-default',
  notificationElementPosition: 3,
  notificationElementStyles: { zIndex: 1000 },
  onNotification: (payload) => updateMenuBadge(payload),
  entityResolve: async ({ type, id }) => {
    const order = await fetch(`/api/orders/${id}`).then(r => r.json());
    return { title: `Objednávka #${order.number}`, url: order.detailUrl,
             fields: [{ label: 'Stav', value: order.status }] };
  },
});
```

### 7.4 Vzhled — tři vrstvy

1. **Tvoje stránka** — jen loading overlay; styluješ si ji sama.
2. **Rám widgetu** (shadow root) — `appFrameCSSPath`. Stránkové CSS sem nedosáhne.
3. **Aplikace v iframu** — vlastní CSS z Nastavení projektu, pak `appCSSPath`. Jen plán s vlastním vzhledem.

Bublina má vlastní shadow root (`notificationElementCSSPath`). Naše CSS se načítá vždy dřív než
tvoje, takže jde přebít. **Nepiš selektory do vnitřku widgetu z CSS stránky** — nefungují a
nejsou podporované.

### 7.5 Odkazy na entity ve zprávách (`entityResolve`)

V panelu → Konfigurace editoru se nastaví pravidla: typ (`[a-z0-9_]`), popisek, regex na URL,
skupina s ID a šablona odkazu. Když uživatel vloží do zprávy URL odpovídající pravidlu, vznikne
„chip“ entity. Bez `entityResolve` funguje jako obyčejný odkaz; s ním ukáže při najetí náhled
z dat, která vrátíš (volá se v prohlížeči — data si načti z vlastního API přihlášeného uživatele).

### 7.6 SPA a dynamické změny

- **Změna stránky / entity:** vytvoř **nový** kontejner a zavolej `thread()` znovu. Na stejný
  element nejde volat `thread()` dvakrát (shadow root nejde recyklovat). Snippet nesleduje router.
- **Odebrání vlákna:** odstraň kontejner z DOM — není na to metoda; snippet se odpojí sám.
- **Změna uživatele:** `initUser(nový)` a **znovu vytvoř** kontejnery vláken a home (běžící
  iframy si uživatele převzaly při načtení).
- **Odhlášení:** odstraň kontejnery a přestaň volat `initUser`.
- **Změna jazyka/tématu za běhu:** `conf({...celý objekt...})`.

### 7.7 Ověření, že integrace funguje

1. V konzoli prohlížeče nejsou chyby s prefixem `[stromcom]` (`No project configuration found`,
   `user not set`, `Cannot create APP, argument must be an HTML element`, `Unknown key`…).
2. Network: `loader.js` z `cdn.stromcom.cz` (200), iframe z `app.stromcom.cz`, dotazy na
   `www.stromcom.cz/api/app/v1/…` (200, ne 401).
3. Uživatel se objeví v panelu → projekt → Uživatelé; vlákno po první zprávě v přehledu.
4. Druhý uživatel (jiný prohlížeč) vidí stejné vlákno u stejné entity a dostane notifikaci
   po `@zmínce`.
5. 401 = špatný secret / typ klíče / origin mimo povolené / expirovaný klíč / neaktivní projekt.

---

## 8. PHP: `stromcom/php-snippet`

Generuje bootstrap a volání fasády jako inline `<script>` při server-renderu.

```bash
composer require stromcom/php-snippet   # PHP ^8.2
```

```php
use Stromcom\Snippet\SnippetClientFactory;
use Stromcom\Snippet\Options\{UserOptions, ThreadOptions, ConfOptions};

$client = SnippetClientFactory::create(
    clientKey:      $_ENV['STROMCOM_CLIENT_KEY'],
    clientSecret:   $_ENV['STROMCOM_CLIENT_SECRET'],   // klíč typu app
    codeHashSecret: $_ENV['STROMCOM_CODE_SECRET'],     // zapne automatický HMAC kódů
    nonce:          $cspNonce,                         // volitelné, pro CSP
);

echo $client->snippet()->getHTML();                                    // 1× na stránku, první
echo $client->conf(new ConfOptions(language: 'cs', notificationElementPosition: 3))->getHTML();
echo $client->user(new UserOptions(code: 'user-' . $user->id, name: $user->name, emailAddress: $user->email))->getHTML();
// ...až za <div id="stromcom-thread"></div>:
echo $client->thread('#stromcom-thread', new ThreadOptions(code: 'order-' . $order->id, name: 'Objednávka ' . $order->number, url: $orderUrl))->getHTML();
echo $client->home('#stromcom-home')->getHTML();
```

### 8.1 Výhody
- Správný a escapovaný bootstrap, žádné ruční lepení JS.
- **Automatický HMAC + base62 hash** `code` uživatele i vlákna (interní ID nejdou do prohlížeče).
  Pro SPA/JSON API: `SnippetClientFactory::createHasher($secret)->hash('user-42')`.
- Typované options (vždy **pojmenované argumenty** — pozice se mezi verzemi měnily).
- `nonce` na všech tazích + generátor CSP: `$client->csp()->getHeaderName()/getHeaderValue()/getDirectives()/getMetaTag()`.
- Prostředí `Environment::PRODUCTION | STAGING` nebo `CustomEnvironment`.

### 8.2 Omezení
- **Jen statický výstup při renderu.** Po načtení stránky nic nemění.
- `thread()` / `home()` berou jen **CSS selektor** a `document.querySelector` se spustí hned →
  element musí být v DOM **před** výpisem skriptu.
- `home()` nemá options; v `ConfOptions` **chybí `onLoad`**; žádné odběry událostí.
- Callbacky (`onNotification`, `entityResolve`, …) se předávají jako **řetězec JS kódu** — knihovna
  ho nekontroluje, syntaktická chyba rozbije celý `<script>`. Obalí je Promise resolvovaným na
  `DOMContentLoaded` → **při vložení skriptu po DOMContentLoaded (AJAX, Turbo, PJAX) se callback
  nikdy nezavolá**.
- `ThreadOptions(attributes:)` je experimentální.
- Neřeší Embed ani Customer API.

### 8.3 Gotchas
- Callbacky v `ConfOptions` jsou **surový JS** vložený beze změny — nikdy do nich nevkládej
  hodnoty od uživatelů.
- `snippet()` jen jednou na stránku. Druhé `conf()` / `user()` nahrazuje, neslučuje.
- Téma `stromcom-default` nebo `stromcom-dark`.
- HTML s `nonce` nekešuj napříč odpověďmi.
- V šablonách (Latte/Twig/Blade) vypiš `getHTML()` bez escapování (`|noescape`, `|raw`, `{!! !!}`);
  `getCode()` = čistý JS bez `<script>` a bez nonce.
- Vlastní `dataLayer:` změní název globální fasády — ruční JS pak musí použít stejný název.

### 8.4 Kdy přejít na ruční JS (a PHP nechat jen na bootstrap + hash)
- SPA / Turbo / HTMX / AJAX navigace, otevírání vláken v modalu, záložkách nebo seznamech.
- Potřebuješ `onLoad`, ovládání widgetu (`openThread`, `maximalize`…) nebo události.
- Callbacky s netriviální logikou, které chceš lintovat/testovat jako normální JS.
- Element vzniká až po načtení stránky.

Vzor: PHP vypíše `snippet()` a spočítá hashe (`createHasher()`), předá je do JS (např. `data-`
atribut nebo JSON), zbytek volá vlastní JS přes `window.stromCom`.

---

## 9. React: `@stromcom/react-snippet`

```bash
npm i @stromcom/react-snippet   # React >= 17, ESM + CJS, typy v balíku
```

```jsx
import { StromcomProvider, StromcomConf, StromcomUser, StromcomThread, StromcomHome } from '@stromcom/react-snippet';

const APP_CSS = ['https://cdn.example.com/brand.css'];            // konstanta mimo komponentu!

<StromcomProvider clientKey={CLIENT_KEY} clientSecret={CLIENT_SECRET}>   {/* klíč typu app */}
  <StromcomConf language="cs" notificationElementPosition={3} appCSSPath={APP_CSS} onNotification={onNotification /* useCallback */} />
  {user && <StromcomUser code={user.stromcomHash} name={user.name} emailAddress={user.email} />}
  <StromcomThread key={order.id} code={order.stromcomHash} name={`Objednávka ${order.number}`} url={orderUrl} style={{ height: 500 }} />
  <StromcomHome />
</StromcomProvider>
```

Provider dej co nejvýš (root layout), ať přežije změny routy. `stromcomHash` počítá backend (§5).

### 9.1 API
- `StromcomProvider` — `clientKey`, `clientSecret`, `dataLayer?` (výchozí `stromCom`),
  `environment?` (`production` | `staging` | URL loaderu), `language?` (jen počáteční).
- `StromcomUser` — `code`, `name`, `emailAddress?`, `readOnly?`, `avatarURL?`; při změně props znovu `initUser`.
- `StromcomThread` — `code`, `name?`, `url?`, `userHint?`, `className`, `style`, `data-*`; vykreslí `<div>`.
- `StromcomHome` — `<div>` s přehledem.
- `StromcomConf` — conf klíče jako props (bez `onLoad`), při změně props znovu `conf()`.
- `useStromcom()` — vrátí fasádu `window.stromCom` (nebo `null` na serveru) pro imperativní volání.

### 9.2 Výhody
- Žádný inline skript → **CSP nepotřebuje nonce ani `'unsafe-inline'`**.
- Volání před načtením loaderu se zafrontují, skript se nevloží dvakrát.
- Deklarativní vlákna (modal, záložka, řádek seznamu), reaktivní uživatel a conf, TS typy,
  callbacky jako skutečné funkce.

### 9.3 Gotchas
- **`name` u `StromcomUser` je povinné** — bez něj se nic nevykreslí.
- `StromcomThread` se inicializuje **jen jednou**; změna `code`/`name`/`url` nic neudělá →
  dej komponentě `key` podle entity, ať se při změně přemountuje.
- Nestabilní props (inline arrow funkce, pole, objekty stylů) v `StromcomConf` znamenají nové
  `conf()` při každém renderu → restart notifikací. Používej `useCallback` / `useMemo` / konstanty.
- `StromcomConf` a ruční `sc.conf()` se navzájem přepisují (conf se nahrazuje celý) — vyber jedno.
- Next.js App Router: balík nemá `'use client'` → obal ho vlastní klientskou komponentou;
  hashe počítej v server komponentě. `url` při SSR: `typeof window !== 'undefined' ? location.href : undefined`.
- Platná témata jsou `'stromcom-default'` a `'stromcom-dark'`.
- Jen **jeden projekt (clientKey) na stránku**. Odmontování provideru SDK neuvolní.
- StrictMode v dev může loader spustit dvakrát — v produkci se to neděje.
- Odhlášení: `useStromcom()?.initUser({ code: '' })` a odmontuj vlákna.

### 9.4 Kdy obejít komponenty přes `useStromcom()`
`StromcomThread` **nepředává `attributes`** (a prop by skončil jako DOM atribut na `<div>`),
neumí vlákno podle `hash` a `StromcomConf` neumí `onLoad`. Pak:

```jsx
function OrderThread({ code, name, attributes }) {
  const sc = useStromcom();
  const ref = useRef(null);
  useEffect(() => {
    if (sc && ref.current) sc.thread(ref.current, { code, name, attributes });
  }, [sc]);                               // jen jednou; při změně entity remount přes key
  return <div ref={ref} style={{ height: 500 }} />;
}
```

`onLoad` → nepoužívej `StromcomConf` a volej `sc.conf({...vše..., onLoad})` sám.
Nikdy neobcházej komponenty zápisem do `window.stromComDL`.

---

## 10. Server: Customer API a PHP SDK

Vyžaduje plán s API a klíč typu **`customer`** (doporučeně omezený na IP serveru).

- Base URL: `https://www.stromcom.cz/api/customer/v1/` (všechny cesty končí `/`)
- Dokumentace: https://www.stromcom.cz/docs/customer/ — OpenAPI: https://www.stromcom.cz/docs/customer/openapi.json
- Autentizace: `Authorization: Bearer <token>` (jen token, bez `:userCode`)
- **Nikdy nevolej z prohlížeče.** Žádné rate limity se nevracejí, ale volej rozumně.

### 10.1 Formát odpovědí
```json
{"status":"success","data":{…},"meta":{"warnings":[{"code":"…","message":"…","details":{}}]}}
{"status":"error","error":{"code":"USER_LIMIT_REACHED","message":"…","details":{}}}
```
- Větvi podle `error.code`, ne podle `message` (text může být česky).
- `meta.warnings` = něco proběhlo jinak, než bylo požadováno (oříznutá hodnota, uspaný uživatel…).
- Čas = unix timestamp (int).

| HTTP | Kódy |
|---|---|
| 400 | `HTTP_INVALID_INPUT`, `USER_GROUP_MEMBER_INVALID`, `WEBHOOK_URL_NOT_ALLOWED`, `EMOJI_UNKNOWN` |
| 401 | `PROJECT_NO_AUTH`, `USER_NO_AUTH`, `USER_FUNCTION_NOT_ALLOWED`, `PROJECT_FUNCTION_NOT_ALLOWED`, `EMBED_SESSION_EXPIRED` |
| 402 | `USER_LIMIT_REACHED`, `THREAD_LIMIT_REACHED`, `MESSAGE_LIMIT_REACHED`, `STORAGE_LIMIT_REACHED`, `MESSAGE_STORAGE_LIMIT_REACHED`, `WEBHOOK_LIMIT_REACHED` |
| 403 | `ACTION_FORBIDDEN` |
| 404 | `REPO_NOT_FOUND`, `EMBED_TOKEN_INVALID` |
| 409 | `USER_ALREADY_EXISTS`, `USER_GROUP_ALREADY_EXISTS` |

### 10.2 PHP SDK `stromcom/customer-sdk`

```bash
composer require stromcom/customer-sdk   # PHP ^8.3, ext-curl, ext-json; verze 0.x (API se může měnit)
```

```php
use Stromcom\Sdk\Client;
use Stromcom\Sdk\Exception\{NotFoundException, ApiException};

$stromcom = new Client($_ENV['STROMCOM_API_TOKEN']);

$stromcom->threads()->create([                 // upsert podle code — bezpečné volat opakovaně
    'code' => 'order-12345', 'name' => 'Objednávka 12345', 'url' => 'https://admin.example.com/orders/12345',
]);
$stromcom->messages()->create('order-12345', [
    'user_code' => 'system-bot',               // autor zprávy je povinný (user_code nebo user_hash)
    'message'   => '<p>Platba přijata.</p>',
]);
$messages = $stromcom->messages()->list('order-12345');
```

Zdroje: `project()->get/update`, `users()->list/get/create/update/delete/restore`,
`threads()->create/update/get/getWithMessages`, `messages()->create/list/delete/attachments/attachmentDownload/reactions`.
Výjimky: `AuthenticationException` (401), `ValidationException` (400/422), `NotFoundException` (404),
`ConflictException` (409), `ServerException` (5xx), `TransportException`; **402 a 403 jsou obecná
`ApiException`** — chytej ji až po specifických. `getApiCode()`, `getStatusCode()`, `getDetails()`.

**Kdy SDK nestačí a volej REST přímo** (`$stromcom->request('POST', 'embed/session/', body: [...])`
nebo vlastní HTTP klient):
- skupiny uživatelů `/user-groups/`, WebHooky `/webhooks/`, Embed `/embed/session/` — SDK je nemá;
- potřebuješ `meta` (varování, `meta.updated` z `PATCH project/`, `lastMessageHash`) — **SDK vrací jen `data`**;
- nahrání příloh (PUT na presigned URL), `?inline=1` u stažení;
- jiný jazyk než PHP — volej REST podle OpenAPI.

### 10.3 Gotchas Customer API
- `POST threads/{code}/messages/` **není idempotentní** — retry vytvoří duplikát. Retry jen při
  chybě spojení před odesláním, nebo si zprávu ověř výpisem.
- `POST threads/` je upsert (idempotentní). `DELETE` uživatele je soft + idempotentní (anonymizuje `code`).
- Stránkování zpráv: `?lastMessageHash=` je **včetně** kurzoru (první položka se opakuje) a vrací
  **vše** od kurzoru, bez velikosti stránky.
- HTML zpráv se sanitizuje: `p b strong em i u ul ol li br a img x-user x-group`. Zmínky:
  `<x-user hash="USERHASH"></x-user>`, `<x-group hash="…"></x-group>` (hash, ne code).
  Text od uživatelů vždy HTML-escapuj.
- Přílohy: v požadavku `"attachments": ["faktura.pdf"]` → odpověď `attachmentsUploadURL[]` ve
  stejném pořadí → HTTP PUT obsahu na každou URL (platí ~10 min). Bez nároku na přílohy se tiše zahodí.
- Odkazy ke stažení příloh jsou krátkodobé — nekešuj je.
- Systémové zprávy piš pod vyhrazeným uživatelem (např. `code: system-bot`, `name: Systém`),
  založeným přes `users()->create()` — počítá se do aktivních uživatelů.

### 10.4 Embed — widget bez skriptu (webview, desktop, přísné CSP)

Server vystaví jednorázový odkaz, klient ho otevře v `<iframe>` / webview. V hostitelské
stránce není žádný skript ani secret. Vyžaduje plán s Embed.

```http
POST /api/customer/v1/embed/session/
Authorization: Bearer <customer token>

{
  "user":   {"code": "Xb3k…", "name": "Jana Nováková", "emailAddress": null, "readOnly": false},
  "thread": {"code": "order-12345", "name": "Objednávka 12345", "url": "https://erp.example.com/order/12345"},
  "conf":   {"language": "cs", "theme": "stromcom-default"},
  "expireIn": 120,
  "sessionExpireIn": 86400
}
```
- Pošli buď `thread`, nebo `"home": true`.
- Povolené conf klíče: `theme`, `language`, `appFrameCSSPath`, `appCSSPath`,
  `notificationElementPosition`, `notificationElementShowAlways`, `notificationElementStyles`,
  `notificationElementCSSPath`, `notificationElementNoShadowRoot`. **Callbacky (`onNotification`,
  `entityResolve`, `onLoad`…) v embedu nejsou.** Neznámý klíč = 400.
- Odpověď: `data.url` (token ve fragmentu `#t=…`). Odkaz je **jednorázový**, otevřít do `expireIn`
  (max 900 s); widget pak běží `sessionExpireIn` (max 7 dní) od otevření.
- Odkaz vydávej **při každém zobrazení**, nekešuj ho a nesdílej. Vypršení → `EMBED_SESSION_EXPIRED`
  / `EMBED_TOKEN_INVALID` → vystav nový.

---

## 11. WebHooky

Plán s WebHooky. Správa v panelu `/customer/project/{id}/webhook/` nebo `/api/customer/v1/webhooks/`.

- Max 5 na projekt, URL musí být veřejné HTTPS. Nový WebHook bez `url`/`events` je vypnutý a po
  doplnění ho je potřeba zapnout (`PATCH {"state": "active"}`). `PATCH events` nahrazuje celý seznam.
- Události: `thread.created`, `message.created`, `mention.created` (jedno volání na zmíněného),
  `message.reaction`. Payload obsahuje **jen identifikátory** (`event`, `project_id`, `timestamp`,
  `threadCode`, `threadHash`, `messageHash`, `userCode`…) — obsah si načti přes Customer API.
- Doručení: jedním pokusem, **bez opakování**, timeout 2 s. Odpovídej rychle (200) a práci dělej
  asynchronně; na WebHooky nestav nic, co nesmí vynechat událost — doplňuj přes API.
- Hlavičky: `X-Stromcom-Event`, `X-Stromcom-Project`, volitelně `Authorization` (bearer/basic dle
  nastavení), `X-Stromcom-Signature: sha256=<hex>` při nastaveném `secret`.

Ověření podpisu (POST, JSON tělo):

```php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $_ENV['STROMCOM_WEBHOOK_SECRET']);
if (!hash_equals($expected, $_SERVER['HTTP_X_STROMCOM_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
```

U metody GET se podepisuje `http_build_query` **jen našich parametrů** seřazených podle klíče
(parametry, které jsi do URL přidal sám, odeber před výpočtem).

---

## 12. Content-Security-Policy

```
script-src  https://cdn.stromcom.cz 'nonce-…';   (nonce jen pro inline bootstrap; React ho nepotřebuje)
connect-src https://www.stromcom.cz;
style-src   https://cdn.stromcom.cz;             (+ hosty vlastních appFrameCSSPath / notificationElementCSSPath)
img-src     data:;
frame-src   https://app.stromcom.cz;
```

`'unsafe-inline'` není potřeba. `stromcom/php-snippet` umí hlavičku vygenerovat (`$client->csp()`).
Embed potřebuje jen `frame-src` pro doménu z `data.url`.

---

## 13. Kontrolní seznam před odevzdáním

- [ ] Ve snippetu je klíč typu **`app`**; klíč `customer` je jen na serveru (env proměnná, ne v repu).
- [ ] `code` uživatele je **stabilní HMAC hash** spočítaný na serveru; tajný klíč hashe je v env a nemění se.
- [ ] `initUser` dostává `code` **i `name`** a vykresluje se jen přihlášeným.
- [ ] Ruční JS: hodnoty od uživatelů vypisované do `<script>` jsou escapované (`json_encode` s `JSON_HEX_TAG`); v callbaccích `ConfOptions` nejsou data od uživatelů.
- [ ] `conf` se volá s kompletním objektem (ne po kouscích).
- [ ] Kontejnery vláken mají výšku; při změně entity vzniká nový kontejner / React `key`.
- [ ] Žádný kód nečte ani nezapisuje `window.stromComDL`, nesahá do Shadow DOM / iframu widgetu.
- [ ] Snippetový klíč má nastavený povolený origin; serverový povolené IP.
- [ ] CSP obsahuje direktivy z §12.
- [ ] Funkce mimo plán (API, Embed, WebHooky, vlastní CSS) jsou ověřené v `cenik.json` a v projektu.
- [ ] Prošel test z §7.7 (konzole bez `[stromcom]` chyb, dva uživatelé vidí stejné vlákno).

## 14. Odkazy a podpora

- Integrace: https://www.stromcom.cz/integrace/ · podrobně: https://www.stromcom.cz/integrace-podrobneji/
- Technické FAQ: https://www.stromcom.cz/technicke-faq/ · Playground: https://www.stromcom.cz/playground/
- Customer API: https://www.stromcom.cz/docs/customer/
- Ceník: https://www.stromcom.cz/cenik/ · JSON: https://www.stromcom.cz/cenik.json
- Balíky: https://github.com/stromcom/php-snippet · https://github.com/stromcom/react-snippet · https://github.com/stromcom/php-customer-sdk
- Stav služby: https://status.stromcom.cz/
- Podpora: support@stromcom.cz
