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:
co-authored by
Claude Opus 5
parent
9f4b8c0596
commit
0c405ea55a
@@ -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[],
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user