diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index e92da47..8a48de3 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -489,6 +489,13 @@ input[type="checkbox"], input[type="radio"] { animation-delay: var(--enter-delay, 0ms); } } +/* Linux-Desktop-App (WebKitGTK, ohne Grafikbeschleunigung): keine + Einblend-Animation und kein Pulsieren der Ladeplatzhalter — beides liess den + Aufbau des Dashboards stocken (DesktopPlatformMarker setzt das Attribut). */ +html[data-desktop-os="linux"] .widget-card, +html[data-desktop-os="linux"] .animate-pulse { + animation: none; +} @keyframes widget-enter { from { opacity: 0; diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index da13f81..57bfdce 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -4,6 +4,7 @@ import { getLocale, getMessages } from 'next-intl/server'; import { ThemeProvider } from 'next-themes'; import { DesktopContextMenuGuard } from '@/components/desktop/desktop-context-menu-guard'; import { DesktopExternalLinks } from '@/components/desktop/desktop-external-links'; +import { DesktopPlatformMarker } from '@/components/desktop/desktop-platform-marker'; import { DesktopThemeSync } from '@/components/desktop/desktop-theme-sync'; import './globals.css'; @@ -29,6 +30,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo + {children} diff --git a/apps/web/src/components/desktop/desktop-platform-marker.test.tsx b/apps/web/src/components/desktop/desktop-platform-marker.test.tsx new file mode 100644 index 0000000..a9075db --- /dev/null +++ b/apps/web/src/components/desktop/desktop-platform-marker.test.tsx @@ -0,0 +1,22 @@ +import { cleanup, render } from '@testing-library/react'; +import { afterEach, describe, expect, it } from 'vitest'; +import { DESKTOP_CLIENT_COOKIE_NAME } from '@/lib/desktop-client'; +import { DesktopPlatformMarker } from './desktop-platform-marker'; + +afterEach(() => { + cleanup(); + document.cookie = `${DESKTOP_CLIENT_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; +}); + +describe('DesktopPlatformMarker', () => { + it('setzt das Betriebssystem der Desktop-App am html-Element', () => { + document.cookie = `${DESKTOP_CLIENT_COOKIE_NAME}=${encodeURIComponent('1.9.2|abc1234|linux')}; path=/`; + render(); + expect(document.documentElement.dataset.desktopOs).toBe('linux'); + }); + + it('setzt im Browser nichts', () => { + render(); + expect(document.documentElement.dataset.desktopOs).toBeUndefined(); + }); +}); diff --git a/apps/web/src/components/desktop/desktop-platform-marker.tsx b/apps/web/src/components/desktop/desktop-platform-marker.tsx new file mode 100644 index 0000000..608b77e --- /dev/null +++ b/apps/web/src/components/desktop/desktop-platform-marker.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { useEffect } from 'react'; +import { getDesktopClientInfo } from '@/lib/desktop-client'; + +/** + * DesktopPlatformMarker (quick-261002) — setzt `data-desktop-os` am + * `` (z. B. `linux`), damit CSS die Desktop-App je Betriebssystem + * anpassen kann. Anlass: Die Linux-Webview (WebKitGTK) zeichnet ohne + * Grafikbeschleunigung; die gestaffelte Einblend-Animation der Kacheln liess + * den Aufbau des Dashboards spuerbar stocken (gemessen: Haenger von + * 550-1100 ms auf ~230 ms ohne Animation). Im Browser ohne Wirkung. + */ +export function DesktopPlatformMarker() { + useEffect(() => { + const os = getDesktopClientInfo()?.os; + if (!os) return; + document.documentElement.dataset.desktopOs = os; + return () => { + delete document.documentElement.dataset.desktopOs; + }; + }, []); + + return null; +} diff --git a/apps/web/src/components/nextcloud-status/nextcloud-alert-notifier.tsx b/apps/web/src/components/nextcloud-status/nextcloud-alert-notifier.tsx index 0f9e202..e83771a 100644 --- a/apps/web/src/components/nextcloud-status/nextcloud-alert-notifier.tsx +++ b/apps/web/src/components/nextcloud-status/nextcloud-alert-notifier.tsx @@ -9,6 +9,8 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; const MODULE_SLUG = 'nextcloud-status'; /** Abstand der Abfrage beim Server. */ const POLL_MS = 60_000; +/** Mindestabstand fuer Abfragen nach Fokus-/Sichtbarkeitswechsel. */ +export const RESUME_MIN_MS = 10_000; /** Rote Grundkennung der API -> Schluessel unter `nextcloudStatus.notify.down`. */ const DOWN_KEYS: Record = { @@ -110,7 +112,14 @@ export function NextcloudAlertNotifier() { void start(); const interval = setInterval(() => void poll(), POLL_MS); + // Fokus/Sichtbarkeit loesen hoechstens alle RESUME_MIN_MS eine Abfrage aus: + // In der Linux-Desktop-App kamen diese Ereignisse beim Oeffnen des + // Dashboards in Schueben (bis zu 50 Abfragen je Sekunde im API-Log). + let lastResume = Date.now(); const resume = () => { + const now = Date.now(); + if (now - lastResume < RESUME_MIN_MS) return; + lastResume = now; pausedRef.current = false; void start(); }; diff --git a/apps/web/src/components/reminders/reminder-notifier.test.tsx b/apps/web/src/components/reminders/reminder-notifier.test.tsx index 0205bc0..7731337 100644 --- a/apps/web/src/components/reminders/reminder-notifier.test.tsx +++ b/apps/web/src/components/reminders/reminder-notifier.test.tsx @@ -143,4 +143,16 @@ describe('ReminderNotifier', () => { }); expect(mockList.mock.calls.length).toBe(after401 + 1); }); + it('viele Fokus-Ereignisse kurz hintereinander loesen nur eine Abfrage aus', async () => { + mockList.mockResolvedValue([]); + render(); + await advance(0); + await advance(15_000); + const before = mockList.mock.calls.length; + await act(async () => { + for (let i = 0; i < 50; i++) window.dispatchEvent(new Event('focus')); + await vi.advanceTimersByTimeAsync(0); + }); + expect(mockList.mock.calls.length).toBe(before + 1); + }); }); diff --git a/apps/web/src/components/reminders/reminder-notifier.tsx b/apps/web/src/components/reminders/reminder-notifier.tsx index 4642821..676ee35 100644 --- a/apps/web/src/components/reminders/reminder-notifier.tsx +++ b/apps/web/src/components/reminders/reminder-notifier.tsx @@ -9,14 +9,16 @@ import { withNotifyLock, } from '@/lib/reminder-notify'; import { + listReminders, REMINDERS_CHANGED_EVENT, type Reminder, ReminderRequestError, - listReminders, } from '@/lib/reminders-api'; /** Abstand der Abfrage beim Server. */ const POLL_MS = 60_000; +/** Mindestabstand fuer Abfragen nach Fokus-/Sichtbarkeitswechsel. */ +export const RESUME_MIN_MS = 10_000; /** Abstand der lokalen Faelligkeitspruefung gegen die geladene Liste (ohne Netz). */ const TICK_MS = 10_000; const BODY_MAX = 200; @@ -87,7 +89,14 @@ export function ReminderNotifier() { const poll = setInterval(() => void load(), POLL_MS); const tick = setInterval(() => void notifyDue(), TICK_MS); + // Fokus/Sichtbarkeit loesen hoechstens alle RESUME_MIN_MS eine Abfrage aus: + // In der Linux-Desktop-App kamen diese Ereignisse beim Oeffnen des + // Dashboards in Schueben (bis zu 50 Abfragen je Sekunde im API-Log). + let lastResume = Date.now(); const resume = () => { + const now = Date.now(); + if (now - lastResume < RESUME_MIN_MS) return; + lastResume = now; pausedRef.current = false; void load(); };