From 79c9a729f66cc6a1806e65ad4affa8b451bfa37e Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:04:54 +0200 Subject: [PATCH] Builder nabizi jen to, co jde zavolat V nabidce kroku byly vsechny sluzby katalogu, i ty, ke kterym firma nema napojeni. Slo tedy vybrat Raynet CRM bez konektoru a postavit strom, ktery pri prvnim behu spadne na chybejicich udajich - a to se pozna az za tyden, kdyz prijde prvni ostra udalost. Nabizi se sluzba, ktera je obecna, nebo k ni firma ma napojeni. Obecne jsou ty, co se bez konektoru obejdou: webhook, casovac, rucni spusteni, formular, incident, ticket, HTTP, transformace, pauza a log. Priznak general uz existoval, jen ho nikdo nepouzil na filtrovani nabidky. Katalog se kvuli tomu nefiltruje, jen se oznacuje: GET /api/dashboard/services prida ke kazde sluzbe connected. Sluzba z odpovedi nemizi, protoze log ticketu a detail akce podle katalogu prekladaji ID operaci na jmena - kdyby zmizela, zustalo by v uz zapsanem radku hole ID. Filtruje az builder. Pod nabidkou je veta, kolik sluzeb ceka na napojeni, aby to nevypadalo, ze neexistuji. Soukroma sluzba uz neni videt cizi firme. canSeeService vracelo spravci platformy true driv, nez se vubec podivalo na viditelnost, takze zakazkova integrace omezena na jednoho klienta se ukazovala i po prepnuti do jine firmy a spravce si ji mohl vybrat do jeji automatizace. Nove rozhoduje firma, ne clovek: kdyz je vybrana, plati jeji seznam, bez ni spravce platformy spravuje katalog a vidi vsechno. Zaroven se konecne pouziva tenantHasService, tedy zpristupneni sluzby firme pres nastaveni - dosud to bylo pole, ktere nikdo necetl. Overeno na bezici instanci: Polstryn SAP je pro tnt_logitrans v katalogu, pro tnt_automia uz ne, a to i pro spravce platformy. V nabidce builderu pro tnt_automia zbylo deset obecnych sluzeb plus iDoklad, na ktery firma napojeni ma. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/12-sluzby-a-konektory.md | 23 +++++++++++ documentation/99-zmeny.md | 41 +++++++++++++++++++ src/data/services.ts | 19 +++++++-- src/routes/dashboard.ts | 17 +++++++- .../components/dashboard/flow/StepPicker.tsx | 36 +++++++++++++++- web/src/types/dashboard.ts | 8 ++++ 6 files changed, 136 insertions(+), 8 deletions(-) diff --git a/documentation/12-sluzby-a-konektory.md b/documentation/12-sluzby-a-konektory.md index ce14355..4071f27 100644 --- a/documentation/12-sluzby-a-konektory.md +++ b/documentation/12-sluzby-a-konektory.md @@ -364,3 +364,26 @@ tim mysli i uzivatel. | Historie zmen konektoru | kdo kdy prepsal udaje, se nikde neuklada | | OAuth toky | zatim jen hlavicky, obnovovani tokenu resi sluzba | | Vyber konektoru v builderu | krok uz `connectorId` nese, UI ho zatim nenabizi | + +## Co se nabizi v builderu + +Krok automatizace jde postavit jen nad sluzbou, kterou je **cim zavolat**: + +- je `general`, tedy obejde se bez konektoru (webhook, casovac, rucni + spusteni, formular, incident, ticket, HTTP, transformace, pauza, log), +- nebo k ni firma ma aspon jedno napojeni. + +Katalog se kvuli tomu **nefiltruje**. `GET /api/dashboard/services` jen prida +ke kazde sluzbe `connected` a vybira az builder. Duvod: log ticketu a detail +akce podle katalogu prekladaji ID operaci na jmena, a sluzba, ktera by +z odpovedi zmizela, by v uz zapsanem radku zustala jako hole ID. + +## Kdo vidi soukromou sluzbu + +U `visibility: restricted` **rozhoduje firma, ne clovek**. Spravce platformy +driv videl vsechny sluzby vzdycky, i po prepnuti do firmy, ktera je nema - +takze si mohl do jeji automatizace vybrat zakazkovou integraci jineho klienta. + +Kdyz je firma vybrana, plati jeji seznam: `visibility.tenantIds` nebo +zpristupneni pres `tenantHasService`. Bez vybrane firmy spravce platformy +spravuje katalog a vidi vsechno. diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index 5642099..6a66876 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -2,6 +2,47 @@ Nejnovejsi nahore. +## 2026-09-02 - Builder nabizi jen to, co jde zavolat + +V nabidce kroku byly vsechny sluzby katalogu, i ty, ke kterym firma nema +napojeni. Slo tedy vybrat Raynet CRM bez konektoru a postavit strom, ktery pri +prvnim behu spadne na chybejicich udajich - a to se pozna az za tyden, kdyz +prijde prvni ostra udalost. + +### Pravidlo + +Nabizi se sluzba, ktera je **obecna, nebo k ni firma ma napojeni**. + +Obecne jsou ty, co se bez konektoru obejdou: webhook, casovac, rucni spusteni, +formular, incident, ticket, HTTP, transformace, pauza a zapis do logu. Ma to uz +priznak `general`, jen ho nikdo nepouzil na filtrovani nabidky. + +### Katalog se nefiltruje, jen se oznacuje + +`GET /api/dashboard/services` prida ke kazde sluzbe `connected`, tedy jestli +k ni firma ma aspon jedno napojeni. Sluzba z odpovedi **nemizi**: log ticketu +a detail akce podle katalogu prekladaji ID operaci na jmena, a kdyby zmizela, +zustalo by v uz zapsanem radku hole ID. Filtruje az builder. + +Aby nabidka nevypadala jako cely katalog, je pod ni veta, kolik sluzeb ceka na +napojeni. Bez ni to vypada, ze sluzba neexistuje, misto ze k ni chybi udaje. + +### Soukroma sluzba uz neni videt cizi firme + +`canSeeService` vracelo spravci platformy `true` driv, nez se vubec podivalo na +viditelnost sluzby. Zakazkova integrace omezena na jednoho klienta se tak +ukazovala i po prepnuti do jine firmy - spravce si ji mohl vybrat do jeji +automatizace. + +**Rozhoduje firma, ne clovek.** Kdyz je firma vybrana, plati jeji seznam; bez +vybrane firmy spravce platformy spravuje katalog a vidi vsechno. Zaroven se +konecne pouziva `tenantHasService`, tedy zpristupneni sluzby firme pres +nastaveni - dosud to bylo pole, ktere nikdo necetl. + +Overeno: Polstryn SAP je pro `tnt_logitrans` v katalogu, pro `tnt_automia` uz +ne, a to i pro spravce platformy. V nabidce builderu pro `tnt_automia` zbylo +deset obecnych sluzeb plus iDoklad, na ktery firma napojeni ma. + ## 2026-09-02 - Tickety maji zalozky, fronta umi prirazovat Seznam ticketu mel dva prepinace, "Moje tickety" a "Ve fronte", schovane mezi diff --git a/src/data/services.ts b/src/data/services.ts index a0b42a0..14a59c6 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -21,6 +21,7 @@ import type { FieldType } from './conditions.js'; import type { User } from '../types.js'; import { isMcpService, MCP_EASYWEB_SERVICE_ID, MCP_SERVICE_ID } from '../mcp/dialect.js'; +import { tenantHasService } from './tenantFeatures.js'; export type ServiceCategory = /** Obecne veci, ktere ma kazdy. Nepotrebuji konektor. */ @@ -2796,16 +2797,26 @@ export function findService(serviceId: string): Service | undefined { */ export function canSeeService(service: Service, user: User, tenantId: string | null): boolean { if (service.general) return true; - if (user.platformAdmin) return true; switch (service.visibility.mode) { case 'everyone': return true; case 'admin': - return false; - case 'restricted': + return user.platformAdmin; + case 'restricted': { + // Zakazkova integrace pro konkretniho cloveka, at uz je prepnuty kamkoliv. if (service.visibility.userIds.includes(user.id)) return true; - return tenantId !== null && service.visibility.tenantIds.includes(tenantId); + + /* + * **Rozhoduje firma, ne clovek.** Spravce platformy driv videl vsechny + * sluzby vzdycky, i po prepnuti do firmy, ktera je nema - takze si mohl + * do jeji automatizace vybrat zakazkovou integraci jineho klienta. + * Kdyz je firma vybrana, plati jeji seznam; bez vybrane firmy spravuje + * spravce platformy katalog a vidi vsechno. + */ + if (tenantId === null) return user.platformAdmin; + return service.visibility.tenantIds.includes(tenantId) || tenantHasService(tenantId, service.id); + } default: return false; } diff --git a/src/routes/dashboard.ts b/src/routes/dashboard.ts index 7baf168..d65cd3a 100644 --- a/src/routes/dashboard.ts +++ b/src/routes/dashboard.ts @@ -20,6 +20,7 @@ import { import { operatorAllowedForType, operatorsByType } from '../data/conditions.js'; import { listTenantScripts } from '../data/tenantScripts.js'; import { + connectorCountsByService, defaultConnectorFor, getConnector, storageStatus, @@ -1032,7 +1033,7 @@ dashboardRouter.use('/helpdesk', helpdeskRouter); * Vraci se **jen sluzby, ktere uzivatel vidi**. Neviditelna sluzba v odpovedi * neni vubec, ne se stavem "nemate pravo". */ -dashboardRouter.get('/services', (req, res) => { +dashboardRouter.get('/services', async (req, res) => { const access = accessFor(req.user!); const requested = typeof req.query.tenantId === 'string' ? req.query.tenantId : undefined; const tenantId = requested ?? access.defaultTenantId; @@ -1040,12 +1041,24 @@ dashboardRouter.get('/services', (req, res) => { const visible = new Set(visibleServices(req.user!, tenantId).map((service) => service.id)); const tenantIds = tenantId ? [tenantId] : []; + /* + * Ke ktere sluzbe ma firma napojeni. + * + * Katalog se **nefiltruje**, jen se oznaci: log ticketu a detail akce + * podle nej prekladaji ID operaci na jmena, a kdyby sluzba z odpovedi + * zmizela, zustalo by v uz zapsanem radku holé ID. Vybirat z ni ale nema + * smysl, kdyz ji neni cim zavolat - o to se stara builder. + */ + const counts = await connectorCountsByService(tenantIds); + res.json({ categories: serviceCategories, items: withRuntimeOptions( // Firma se predava kvuli MCP: nastroje jsou vlastnost jejiho napojeni, // ne katalogu. Bez ni se nevrati zadne. - serviceCatalog(tenantId ?? null).filter((service) => visible.has(service.id)), + serviceCatalog(tenantId ?? null) + .filter((service) => visible.has(service.id)) + .map((service) => ({ ...service, connected: (counts.get(service.id) ?? 0) > 0 })), { people: listPeople(tenantIds).map((person) => ({ id: person.id, name: person.name })), groups: listGroups(tenantIds).map((group) => ({ id: group.id, name: group.name })), diff --git a/web/src/components/dashboard/flow/StepPicker.tsx b/web/src/components/dashboard/flow/StepPicker.tsx index 89b8edd..f9ce7ab 100644 --- a/web/src/components/dashboard/flow/StepPicker.tsx +++ b/web/src/components/dashboard/flow/StepPicker.tsx @@ -46,9 +46,30 @@ export function StepPicker({ const operationsOf = (connector: Service) => mode === 'trigger' ? connector.triggers : connector.actions; - // Konektory, ktere v tomhle rezimu vubec maji co nabidnout. + /* + * Co jde ve strome opravdu pouzit. + * + * Krome toho, ze sluzba v tomhle rezimu neco nabizi, musi jit **zavolat**: + * bud je obecna (webhook, pauza, ticket), nebo k ni firma ma napojeni. + * Vybrat Raynet bez konektoru znamenalo postavit strom, ktery pri prvnim + * behu spadne na chybejicich udajich - a to se pozna az za tyden. + */ const usable = useMemo( - () => connectors.filter((connector) => operationsOf(connector).length > 0), + () => + connectors.filter( + (connector) => + operationsOf(connector).length > 0 && (connector.general || connector.connected), + ), + [connectors, mode], + ); + + /** Kolik sluzeb je schovanych jen proto, ze k nim chybi napojeni. */ + const missing = useMemo( + () => + connectors.filter( + (connector) => + operationsOf(connector).length > 0 && !connector.general && !connector.connected, + ).length, [connectors, mode], ); @@ -264,6 +285,17 @@ export function StepPicker({ )} + + {/* + Aby bylo poznat, ze nabidka neni cely katalog. Bez teto vety by to + vypadalo, ze sluzba neexistuje, misto ze k ni chybi napojeni. + */} + {!selected && missing > 0 && ( +

+ Dalších {missing} služeb se objeví, až k nim přidáte napojení v Konektorech. + Bez přístupových údajů by krok při prvním běhu spadl. +

+ )} ); diff --git a/web/src/types/dashboard.ts b/web/src/types/dashboard.ts index 23e5c9b..50753c6 100644 --- a/web/src/types/dashboard.ts +++ b/web/src/types/dashboard.ts @@ -324,6 +324,14 @@ export interface Service { status: ServiceStatus; /** true = funguje bez konektoru (webhook, pauza, transformace dat). */ general: boolean; + /** + * true = firma ma k teto sluzbe aspon jedno napojeni. + * + * Katalog se kvuli tomu nefiltruje - log ticketu podle nej preklada ID + * operaci na jmena a sluzba, ktera by z odpovedi zmizela, by v uz zapsanem + * radku zustala jako hole ID. Vybirat z ni ale nema smysl. + */ + connected: boolean; appId: string | null; /** * `smtp` = sluzba se nevola pres HTTP, adresu nese konektor v udajich.