Přeskočit obsah

Spec: docs-portal — entitlement-gated produkční dokumentační portál

Status: NÁVRH v1 (2026-07-01, max/ultrathink). Rozšiřuje/nahrazuje platform-docs.md (docs systém) + docs-production-deploy.md (prod deploy) + návrh 3-publikového portálu (auto-nav apps). Spec-first: review → ~95 % shoda → implementace.

Autor kontext: Michal chce (1) docs na PRODUKCI (docs.avaxis.cz) robustně — ať se to „zase někde nezamotá" (viz outage níže), (2) uživatel vidí PLNOU dokumentaci jen k software, který (koupený/přiřazený); u ostatních jen info co umí + poznámka že plná dokumentace se zobrazí po zakoupení; NE all-or-nothing na celou platformu.


0. Kontext & poučení z outage

Dnešní stav (2026-07-01): jeden MkDocs (avax-platform/mkdocs.yml, Material, česky), servírovaný z avaxdev host nginx (avax-docs vhost server_name docs.avaxdev.local docs.avaxis.cz/srv/avax-docs). Build avax-docs-build.timer (5 min). Vendor docs pull-sync (pull_vendor_docs.py) + metadata sync (sync_app_docs.py). Auth-gate zárodek: gen_docs_auth_nginx.pyavax-docs-auth.conf (dnes gateuje 9 paths).

🔴 Outage (28 dní, silent): dev1→dev2 migrace ztratila 4 FS objekty (ne kód): /srv/avax-docs, /var/lib/avax-docs, avax-owned /var/log/avax-docs-build.log, symlink /usr/local/bin/avax-docs-rebuild.sh. Build padal 41506× (226/NAMESPACE + set -e na nezapisovatelném logu), site prázdný, docs.avaxis.cz externě mrtvý (NPM ho navíc vůbec nezná). Poučení = návrhové cíle: (a) žádný netrackovaný host-stav (FS dirs/symlinky/ownership, co migrace/rebuild ztratí), (b) idempotentní provisioning, (c) healthcheck + alert (tichý 28denní výpadek nesmí být možný), (d) reprodukovatelný build+deploy (CI, ne ruční).

Proxy realita: *.avaxis.cz routuje NPM (nginx-proxy-manager 2.11.1, proxy-app-1 @ 192.168.1.24, UI/API-managed). docs.avaxis.cz v NPM chybí (proto externě down).


1. Cíl

  1. Produkční docs.avaxis.cz — robustní, reprodukovatelný, monitorovaný.
  2. Jeden portál, 3 publika (taby): Uživatelská / Programátorská / Admin (superadmin). V každém levé kolaps menu: Platforma (cross-cutting) + Aplikace/ (auto-nav všech apps).
  3. Entitlement-gated viditelnost (jádro tohoto specu): plná dokumentace app jen pro toho, kdo app ; jinak teaser + upsell; per-app, ne na celou platformu.

2. Model viditelnosti (autoritativní matice)

Rozhodnutí per HTTP request = funkce (identita z JWT, tab/publikum, scope). Scope: platform (obecné) | app:<slug>:teaser | app:<slug>:full | admin.

Kdo \ Co Platforma (user/dev) App teaser (co umí + upsell) App plný obsah Admin tab
Anonym (bez JWT) login redirect¹ login redirect¹ login redirect login redirect
Přihlášený, app NEMÁ ✅ teaser ❌ 403 → teaser/upsell
Přihlášený, app MÁ (entitled) ✅ plná
super_admin ✅ (vše)

¹ OTEVŘENÁ OTÁZKA A: teasery jako marketing chtějí být veřejné (bez loginu) → konflikt s „celý web za login". Doporučení: teaser = veřejný (láká k nákupu), plný obsah + Programátorská + Admin = za login + entitlement. Viz §10.

„Entitled" = má appstejná logika jako launcher katalog (single source of truth): company má aktivní company_subscriptions / app_assignments na <slug> A projde apps.required_roles gate (viz reference_role_gate). Docs viditelnost = katalogová viditelnost → žádný duplicitní entitlement kód, reuse existující catalog service.


3. Architektura — statický MkDocs + edge auth_request

Zvolený model: MkDocs builduje VŠE (plný obsah) → statický site; reverzní proxy (nginx / NPM) gateuje per-path přes auth_request na backend. Per-user rozhodnutí na edge.

internet → docs.avaxis.cz (NPM :443, SSL)
        → prod nginx (docs) ── auth_request ──▶ api.avaxis.cz /v1/docs/access?app=<slug>&audience=<>
              │  200 → servíruj statickou stránku z built site
              │  401 → redirect na login (api.avaxis.cz/login?redirect=…)
              └  403 → error_page → teaser dané app (public) / upsell (žádný únik obsahu)

Teaser-as-index (klíčový trik): …/apps/<slug>/index.md = teaser (public: co app umí + CTA). Hlubší stránky (prvni-kroky, funkce/*, faq, dev/admin) = gated. Tím: - menu ukazuje všechny apps (teaser vždy) → browsable ✅ - klik do hloubky u neentitled → 403 → upsell ✅ - „info co umí" (teaser) + „plná dokumentace po zakoupení" (gated) + per-app ✅

Proč ne alternativy: - Dynamický docs backend (render per request) — ztratí MkDocs statiku/search/styl, těžké. - Pre-built varianty (public/full) — nedělá per-USER (entitlementy se liší per company), kombinatorika. - Static + edge auth_request — zachová statiku + search + per-user + výkon; precedens už je (gen_docs_auth_nginx.py path-gate + docs-production-deploy.md auth_request vzor).


4. Auth + entitlement endpoint

  • JWT cookie avax_token (Domain=.avaxis.cz; HttpOnly; Secure; SameSite=Lax) — set /auth/login (sdílený mezi api a docs). Bez toho auth_request nemá identitu.
  • nginx (prod docs):
    # per-app gated paths
    location ~ ^/(user-guide|dev)/apps/(?<docslug>[^/]+)/(?!index\.html|$) {
        auth_request /__docs_auth;
        auth_request_set $x $upstream_http_x_reason;
        error_page 401 = @login;   error_page 403 = @teaser;
        try_files $uri $uri/ $uri.html =404;
    }
    location ~ ^/admin/ { auth_request /__docs_auth_admin; error_page 401=@login 403=@denied; ... }
    location = /__docs_auth { internal; proxy_pass https://api.avaxis.cz/v1/docs/access$is_args$args;
        proxy_set_header Cookie $http_cookie; proxy_pass_request_body off; }
    location @teaser { rewrite ^/(user-guide|dev)/apps/(?<s>[^/]+)/.*$ /user-guide/apps/$s/ redirect; }
    location @login  { return 302 https://api.avaxis.cz/login?redirect=$scheme://$host$request_uri; }
    
    (auth_request cache pro výkon — TTL ~30–60 s, klíč = cookie+path.)
  • Backend NEW GET /v1/docs/access?app=<slug>&audience=<user|dev|admin>:
  • Validuj avax_token (RS256/JWKS). Neplatný → 401.
  • audience=admin → jen super_admin → jinak 403.
  • scope=platform nebo teaser → 200 (přihlášený).
  • app:<slug> full → reuse catalog entitlement: má user.company <slug>? → 200 / 403 (+ X-Reason: not-entitled).
  • audience=dev app → OTEVŘENÁ OTÁZKA C (entitled zákazník vs jen platform-dev/vendor).
  • Endpoint je lightweight (jen entitlement lookup, cache-friendly), NE těžký.

5. Teaser (neentitled pohled)

  • Obsah teaseru: název, „co umí" (capabilities z connector.json provides + app metadata), kategorie, případně screenshot; CTA: „Plná dokumentace k dispozici po zakoupení → [Obchod v launcheru]".
  • Zdroj: auto-generováno z app metadata (sync_app_docs.pydocs/apps/<slug>.md) + connector capabilities. Rozšířit generátor: vyrob apps/<slug>/index.md teaser (pokud vendor nedodá vlastní). Vendor může teaser přepsat (marketingový popis).
  • Nikdy neúnik plného obsahu v teaseru (oddělené soubory; teaser = jen index).

6. Struktura & nav (z 3-publikového návrhu)

  • 3 taby publik (navigation.tabs), levé menu Platforma + Aplikace/, kolaps (vypnout navigation.expand), auto-nav (mkdocs-awesome-pages-plugin: per-app .pages).
  • Per-app 3 flavory: user-guide/apps/<slug>/ (user) + dev/apps/<slug>/ (programátorská) + admin/apps/<slug>/ (admin). Vendor je generuje (handoff docs/dev/app-docs-handoff.md), pull_vendor_docs.py táhne všechny 3.
  • Všechny apps v menu (teaser) → „mít menu a projít" splněno.

7. Produkční hosting (robustní — anti-outage)

Princip: žádný netrackovaný host-stav. Dvě varianty (rozhodnutí §10-D):

  • B1 — Kontejnerizovaný docs (doporučeno): docker image = nginx + built site (+ auth_request konfig). Build v CI → image → deploy na prod (VM 192.168.1.20, pozn. docker tam zatím není = předpoklad) nebo na NPM/host. Kontejner = zero host-FS fragilita (co nás kouslo). Restart = čistý stav. Healthcheck v image.
  • B2 — Host nginx + idempotentní provisioning: zachovat avaxdev/prod nginx, ale systemd ExecStartPre=mkdir -p všechny ReadWritePaths + log soubor (auto-heal migrace ztrát) + ConditionPathExists alert. Levnější, ale stále host-stav.

Společné (obojí): - CI .gitea/workflows/docs.yml: mkdocs build --strict (fail fast na broken nav/links) → deploy (image push / rsync) → smoke gate (curl 200 hlavní + gated 401/403 + teaser 200). - NPM: proxy host docs.avaxis.cz → docs upstream + SSL (NPM Let's Encrypt) + auth (auth_request, ne jen Access List — kvůli entitlementu). Přidat přes NPM UI/API (pozn.: ruční edit /data/nginx/proxy_host/*.conf NEfunguje, NPM regeneruje z DB → nutné UI/API + NPM admin creds, viz §10-E). - 🔔 Healthcheck + alert: externí monitor https://docs.avaxis.cz/ 200 (+ cert expiry) → alert (chat/e-mail). Tichý výpadek = zakázaný.


8. Bezpečnost (ultrathink nálezy)

  • 🔴 Search-index leak (kritické): MkDocs Material generuje statický search/search_index.json obsahující text VŠECH stránek → neentitled uživatel by fulltextem přečetl gated obsah! Řešení (rozhodnout §10-B): (a) search_index.json gateovat stejně jako obsah + client search jen nad povolenými; (b) build oddělené indexy (public/teaser vs per-entitlement) — složité; (c) backend search (server-side, entitlement-aware) místo statického. Doporučení: (a) minimálně gateovat search_index + teaser-only public index.
  • Admin tab (deploy/M2M/S3 secrets) = super_admin only → proto plný obsah za login (Michal OK).
  • JWT RS256/JWKS validace; cookie Secure+HttpOnly; auth_request endpoint interní (ne public).
  • Teaser bez úniku (oddělené soubory od plného obsahu).
  • Rate-limit login redirect; auth_request cache neukládá napříč uživateli (klíč vč. cookie).

9. Fáze

  1. Prod robustní hosting — kontejner/idempotent + CI docs.yml (build strict + smoke) + NPM proxy host docs.avaxis.cz + SSL + healthcheck/alert. (Bez entitlementu: zatím celý web za login, nebo veřejné dokud není gate.)
  2. Entitlement gate — backend /v1/docs/access (reuse catalog entitlement) + nginx auth_request per app-path + admin gate.
  3. Teaser — generátor teaser index z metadata/capabilities + search-index leak fix.
  4. 3-publikový obsah + vendor handoff — 3 flavory per app, pull_vendor_docs.py 3-flavor, apps si generují (handoff).

10. Rozhodnutí (zamčeno 2026-07-01)

  • A. ✅ Teaser VEŘEJNÝ (Michal): nepřihlášený vidí teaser (co app umí + „plná dokumentace po zakoupení") = marketing. Plný obsah + Programátorská + Admin = za login + entitlement. → search index musí být teaser-only public (viz B), jinak leak.
  • B. ✅ Search = gateovat search_index.json (default): public search jen nad teaser/platform; plný obsah v odděleném indexu za auth_request (nebo backend search). Bez toho A leakuje.
  • C. ✅ Programátorská per-app = entitled (zákazník co app má, kvůli integraci) + platform-dev/vendor.
  • D. ✅ Hosting = KONTEJNER na prod VM 192.168.1.20 (Michal). Docker tam zatím není → instalace. nginx+built-site+auth_request v image; CI build → deploy na .20; NPM → .20. Zero host-FS fragilita.
  • E. NPM routing — docs.avaxis.cz → .20; přidání proxy hostu + SSL: NPM creds (UI/API) nebo příprava kroků / DNS. (Otevřené: creds path.)
  • F. ✅ Granularita = per-app (subscription/assignment); jedna docs verze per app (ne per-channel).

Související

  • platform-docs.md (docs systém), docs-production-deploy.md (prod runbook, částečně obsoletní), dev/vendor-docs-sync.md, dev/help-deep-links.md, reference_role_gate, catalog entitlement service.