Otevrena stranka sekala prehravani videa

Pri otevrenem portalu zacalo vedle nej sekat prehravani videa, po zavreni bylo
hned dobre. Neslo o smycku v kodu: zivy prenos udalosti je SSE, jedno spojeni na
celou aplikaci, a setTimeout je v nem jen na odstup pri znovupripojeni. Byly to
tri veci.

Rozmazavani plne barvy. `.glass`, podklad skoro kazdeho panelu, mel
backdrop-filter blur(14px). Jenze pod kartami je plna barva (bg-ink-950 na
dashboardu), takze se rozmazavala jednolita plocha: zadny rozdil videt nebyl
a kazda karta si za to drzela vlastni rozmazanou vrstvu ve skladaci. V kodu je
glass na 58 mistech, na jedne obrazovce jich je snadno dvacet. Rozmazani zustava
na peti mistech, kde pod nim neco opravdu projizdi: lepici hlavicky, podklad
modalu, prekryv menu na mobilu.

Nekonecne animace nad velkymi rozmazanymi plochami. Prihlaseni a verejny web
mely zare o velikosti 26 az 34 rem s blur(120px), kterym se donekonecna
animovala pruhlednost - to znamena prepocitavat obrovskou vrstvu sedesatkrat za
sekundu, i kdyz se na strance nic nedeje. Zare zustaly, animace zmizela.
Pribylo prefers-reduced-motion.

Kazda udalost prekreslila cely dashboard. EventStreamProvider mel v tomtez
kontextu stav spojeni, odber i seznam poslednich udalosti. Seznam se meni pri
kazde udalosti, takze se menila cela hodnota kontextu a prekreslil se kazdy, kdo
si jen zaridil odber - tedy vsechny dotazy s refetchOn na strance, i kdyz o ten
typ udalosti nestaly. Pri behu automatizace se cely dashboard prekresloval
nekolikrat za sekundu, a s nim dvacet rozmazanych vrstev. Kontext je proto
rozdeleny: useEventStream da stav a odber, useEventLog seznam udalosti.

Filtr sekci nefiltroval. Seznam ticketu sklada adresu dotazu v useMemo, ale
v zavislostech chybely groupId, typeId, tag a stage - adresa se po prepnuti
sekce neprepocitala a data se nenacetla znovu. Driv to nevadilo, byly to filtry
nastavitelne jen odkazem, ktere se po nacteni nemenily. Se zalozkou "Vsechny
tickety" a jejim vyberem sekce je to chyba, ktera je videt.

Overeno: groupId=grp_servicedesk vrati jeden ticket, groupId=none zbyle dva.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-09-08 15:23:43 +02:00
co-authored by Claude Opus 5
parent 9f4b8c0596
commit 0c405ea55a
8 changed files with 144 additions and 16 deletions
+26
View File
@@ -144,3 +144,29 @@ zakaznika v ukazkovych datech a prejmenovat ho nema duvod.
| Materialy mimo web | prezentace, obrazky pro socialni site |
| Domena | `worknuke.cz` v `brand.ts`, ale nikde neni nasazena |
| Prochazeni `accent-*` | 341 mist, ktera uz nemaji duvod pouzivat druhy klic |
## Rozmazani jen tam, kde neco prekryva
`.glass` je podklad skoro kazdeho panelu a **nema `backdrop-filter`**.
Puvodne mel `blur(14px)`, jenze pod kartami je plna barva (`bg-ink-950` na
dashboardu): rozmazavala se jednolita plocha, zadny rozdil videt nebyl a kazda
karta si za to drzela vlastni rozmazanou vrstvu ve skladaci. Pri dvaceti
kartach na obrazovce to vytizilo GPU natolik, ze vedle portalu sekalo
prehravani videa.
Rozmazani patri jen tam, kde pod nim neco opravdu projizdi:
| Kde | Trida |
| -------------------------- | ----------------- |
| lepici hlavicka dashboardu | `backdrop-blur-xl` |
| lepici hlavicka webu | `backdrop-blur-xl` |
| podklad modalu | `backdrop-blur-sm` |
| prekryv menu na mobilu | `backdrop-blur-sm` |
**Zadna nekonecna animace nad velkou rozmazanou plochou.** Zare na prihlaseni
a na webu jsou staticke: animovat pruhlednost objektu s `blur(120px)` znamena
prepocitavat obrovskou vrstvu porad dokola, i kdyz se na strance nic nedeje.
Kdo ma v systemu vypnute animace (`prefers-reduced-motion`), nedostane ani
ty zbyle.
+54
View File
@@ -2,6 +2,60 @@
Nejnovejsi nahore.
## 2026-09-08 - Otevrena stranka sekala prehravani videa
Pri otevrenem portalu zacalo vedle nej sekat prehravani videa, po zavreni
stranky bylo hned dobre. Neslo o smycku v kodu: **zivy prenos udalosti je SSE,
jedno spojeni na celou aplikaci**, a `setTimeout` je v nem jen na odstup pri
znovupripojeni. Zdrzeni bylo jinde a byly to tri veci.
### Rozmazavani plne barvy
`.glass`, tedy podklad skoro kazdeho panelu, mel `backdrop-filter: blur(14px)`.
Jenze **pod kartami je plna barva** (`bg-ink-950` na dashboardu), takze se
rozmazavala jednolita plocha: zadny rozdil videt nebyl a kazda karta si za to
drzela vlastni rozmazanou vrstvu ve skladaci. V kodu je `glass` na 58 mistech,
na jedne obrazovce jich je snadno dvacet.
Rozmazani zustava tam, kde pod nim neco opravdu projizdi: lepici hlavicka
dashboardu i webu, podklad modalu a prekryv menu na mobilu. To je pet mist,
kazde se svym `backdrop-blur-*` primo u sebe.
### Nekonecne animace nad velkymi rozmazanymi plochami
Prihlaseni a verejny web meli zare o velikosti 26 az 34 rem s `blur(120px)`,
kterym se **donekonecna animovala pruhlednost**. Animovat pruhlednost silne
rozmazaneho objektu znamena prepocitavat obrovskou vrstvu sedesatkrat za
sekundu, porad, i kdyz se na strance nic nedeje.
Zare zustaly, animace zmizela. Krome toho pribylo `prefers-reduced-motion`: kdo
si v systemu animace vypnul, nema je dostat ani tady.
### Kazda udalost prekreslila cely dashboard
`EventStreamProvider` mel v tomtez kontextu **stav spojeni, odber i seznam
poslednich udalosti**. Seznam se meni pri kazde udalosti, takze se menila cela
hodnota kontextu - a prekreslil se kazdy, kdo si jen zaridil odber. To jsou
vsechny dotazy s `refetchOn` na strance, i kdyz o ten typ udalosti nestaly.
Pri behu automatizace, ktera posila `automation.run` a `ticket.updated`, se tak
cely dashboard prekresloval nekolikrat za sekundu - a s nim dvacet rozmazanych
vrstev.
Kontext je proto rozdeleny: `useEventStream` da stav a odber, `useEventLog`
seznam udalosti. Seznam si bere jen ten, kdo ho opravdu vypisuje, tedy toasty.
### Filtr sekci nefiltroval
Seznam ticketu sklada adresu dotazu v `useMemo`, ale v zavislostech chybely
`groupId`, `typeId`, `tag` a `stage`. Adresa se tedy po prepnuti sekce
neprepocitala a data se nenacetla znovu - filtr vypadal, ze nedela nic.
Driv to nevadilo: byly to filtry, ktere sly nastavit jen odkazem a po nacteni
stranky se uz nemenily. Se zalozkou "Vsechny tickety" a jejim vyberem sekce se
z toho stala chyba, ktera je videt.
Overeno: `groupId=grp_servicedesk` vrati jeden ticket, `groupId=none` zbyle dva.
## 2026-09-07 - Podminka se muze ptat na vic veci naraz
Podminka byla **prave jedna otazka**. Slozitejsi vetveni se muselo skladat
@@ -3,10 +3,16 @@ import type { ReactNode } from 'react';
import { connectEventStream, type StreamStatus } from '@/lib/eventStream';
import type { DashboardEvent, DashboardEventType } from '@/types/events';
/**
* Stav spojeni a odber. **Bez seznamu udalosti.**
*
* Seznam se meni pri kazde udalosti a kdyz byl v teze hodnote kontextu, prekreslil
* se pri kazde udalosti kazdy, kdo si jen zaridil odber - tedy vsechny dotazy
* s `refetchOn` na cele strance, i kdyz o ten typ udalosti nestaly. Pri behu
* automatizace to znamenalo prekreslovani celeho dashboardu nekolikrat za sekundu.
*/
interface EventStreamState {
status: StreamStatus;
/** Poslednich par udalosti, nejnovejsi prvni. */
events: DashboardEvent[];
/** Prihlaseni k odberu. Vraci funkci pro odhlaseni. */
subscribe: (listener: (event: DashboardEvent) => void) => () => void;
}
@@ -15,13 +21,15 @@ const noop = () => () => {};
const EventStreamContext = createContext<EventStreamState>({
status: 'closed',
events: [],
subscribe: () => {
console.warn('[stream] subscribe mimo EventStreamProvider, udalosti nedorazi');
return noop();
},
});
/** Poslednich par udalosti, nejnovejsi prvni. Zvlast, protoze se meni porad. */
const EventLogContext = createContext<DashboardEvent[]>([]);
const MAX_KEPT = 30;
/**
@@ -59,18 +67,29 @@ export function EventStreamProvider({ children }: { children: ReactNode }) {
};
}, []);
const value = useMemo<EventStreamState>(
() => ({ status, events, subscribe }),
[status, events, subscribe],
);
const value = useMemo<EventStreamState>(() => ({ status, subscribe }), [status, subscribe]);
return <EventStreamContext.Provider value={value}>{children}</EventStreamContext.Provider>;
return (
<EventStreamContext.Provider value={value}>
<EventLogContext.Provider value={events}>{children}</EventLogContext.Provider>
</EventStreamContext.Provider>
);
}
export function useEventStream(): EventStreamState {
return useContext(EventStreamContext);
}
/**
* Posledni udalosti. Bere si je jen ten, kdo je opravdu vypisuje.
*
* Kdo chce jen reagovat na udalost, pouzije `useEventListener` - ten se
* prekresluje jen kdyz sam neco zmeni, ne pri kazde udalosti na strance.
*/
export function useEventLog(): DashboardEvent[] {
return useContext(EventLogContext);
}
/** Zavola callback jen pri udalostech uvedenych typu. */
export function useEventListener(
types: DashboardEventType[],
+2 -2
View File
@@ -1,7 +1,7 @@
import { AlarmClock, Bell, CheckCircle2, LifeBuoy, UserCheck, Webhook, Workflow, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import type { LucideIcon } from 'lucide-react';
import { useEventStream } from '@/components/dashboard/EventStreamProvider';
import { useEventLog } from '@/components/dashboard/EventStreamProvider';
import { cn } from '@/lib/cn';
import type { DashboardEvent, DashboardEventType } from '@/types/events';
@@ -27,7 +27,7 @@ const style: Record<DashboardEventType, { icon: LucideIcon; tone: string }> = {
/** Bubliny o tom, co se prave stalo. Bez nich by zive zmeny nebyly poznat. */
export function EventToasts() {
const { events } = useEventStream();
const events = useEventLog();
const [visible, setVisible] = useState<DashboardEvent[]>([]);
const [dismissed, setDismissed] = useState<Set<string>>(new Set());
+1 -1
View File
@@ -14,7 +14,7 @@ export function CallToAction() {
<div className="relative overflow-hidden rounded-3xl border border-brand-400/25 bg-gradient-to-br from-brand-500/12 via-ink-800/60 to-accent-500/12 px-6 py-14 text-center sm:px-14">
<div
aria-hidden
className="animate-pulse-slow pointer-events-none absolute -top-24 left-1/2 size-96 -translate-x-1/2 rounded-full bg-brand-500/20 blur-[100px]"
className="pointer-events-none absolute -top-24 left-1/2 size-96 -translate-x-1/2 rounded-full bg-brand-500/20 blur-[100px]"
/>
<h2 className="relative text-3xl font-bold text-white sm:text-4xl">
{t('cta.titleFirst')} <span className="text-gradient">{t('cta.titleAccent')}</span>
+28 -2
View File
@@ -174,11 +174,21 @@
Vlastni utility. Pouzivat jen pro veci, ktere se opakuji vsude.
------------------------------------------------------------------- */
/* Sklenena karta - zaklad vetsiny panelu na webu i v dashboardu */
/*
* Sklenena karta - zaklad vetsiny panelu na webu i v dashboardu.
*
* **Bez `backdrop-filter`.** Bylo tu `blur(14px)`, jenze pod kartami je plna
* barva (`bg-ink-950` na dashboardu), takze se rozmazavala jednolita plocha:
* zadny rozdil videt nebyl a kazda karta si za to drzela vlastni rozmazanou
* vrstvu ve skladaci. Pri padesati kartach a zivem streamu udalosti to
* vytizilo GPU natolik, ze vedle toho sekalo prehravani videa.
*
* Rozmazani zustava tam, kde neco opravdu prekryva: lepici hlavicka
* a podklady modalu maji svoje `backdrop-blur-*` primo u sebe.
*/
@utility glass {
background-color: --alpha(var(--color-ink-800) / 70%);
border: 1px solid --alpha(var(--color-ink-600) / 70%);
backdrop-filter: blur(14px);
}
/*
@@ -209,3 +219,19 @@
@utility mask-fade-b {
mask-image: linear-gradient(to bottom, black, transparent);
}
/*
* Kdo si v systemu vypnul animace, nema je dostat ani tady.
*
* Nekonecne animace jsou to jedine, co na klidne strance porad zamestnava
* skladac - a prave ty jsou tady zbytne.
*/
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
+2 -2
View File
@@ -85,8 +85,8 @@ export default function Login() {
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-ink-900 px-5 py-14">
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
<div className="absolute inset-0 bg-grid opacity-25" />
<div className="animate-pulse-slow absolute -top-32 left-1/2 size-[34rem] -translate-x-1/2 rounded-full bg-brand-500/16 blur-[120px]" />
<div className="animate-pulse-slow absolute -bottom-40 right-0 size-[26rem] rounded-full bg-accent-500/14 blur-[120px]" />
<div className="absolute -top-32 left-1/2 size-[34rem] -translate-x-1/2 rounded-full bg-brand-500/16 blur-[120px]" />
<div className="absolute -bottom-40 right-0 size-[26rem] rounded-full bg-accent-500/14 blur-[120px]" />
</div>
<div className="w-full max-w-md">
+4 -1
View File
@@ -121,7 +121,10 @@ export default function Tickets() {
if (channel) params.set('channel', channel);
const search = params.toString();
return `/api/dashboard/tickets${search ? `?${search}` : ''}`;
}, [scope, tenantId, assignee, status, channel]);
// Vsechno, co se do adresy dosazuje, musi byt i tady. Chybel `groupId`
// a spol., takze prepnuti sekce adresu neprepocitalo a seznam se nenacetl
// znovu - filtr vypadal, ze nefunguje.
}, [scope, tenantId, assignee, status, channel, typeId, tag, stage, groupId]);
const workloadPath = useMemo(() => {
const params = new URLSearchParams();