From 04743b93d5e4201df84b9f04d5e855487bffb3ee Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Fri, 26 Sep 2025 11:32:28 +0000 Subject: [PATCH 1/6] feat: implement comprehensive caching strategy with TanStack Query (Issue #8) - Add @tanstack/vue-query dependency for advanced caching - Implement QueryClient with optimized cache defaults (stale-while-revalidate) - Create query composables for all modules (tags, appointments, groups, logs, users) - Migrate all Card and Admin components to use TanStack Query - Add bulk cache strategy for logger data with 5000 entry limit - Implement client-side pagination for instant performance - Add cache debug components for development monitoring - Fix LoggerSummaryAdminBulk table structure to match original - Maintain exact UI/UX while improving performance significantly - Add proper error handling and retry mechanisms - Include background refetching and request deduplication Co-authored-by: Ona --- CACHE_TESTING.md | 142 +++++ package-lock.json | 84 +++ package.json | 1 + src/App.vue | 9 +- .../automatic-groups/AutomaticGroupsAdmin.vue | 15 +- .../automatic-groups/AutomaticGroupsCard.vue | 109 +--- .../automatic-groups/useAutomaticGroups.ts | 112 ---- src/components/common/CacheDebug.vue | 217 +++++++ src/components/common/CacheTest.vue | 230 +++++++ .../ExpiringAppointmentsAdmin.vue | 48 +- .../ExpiringAppointmentsCard.vue | 90 +-- .../loggerSummary/LoggerSummaryAdmin.vue | 1 + .../loggerSummary/LoggerSummaryAdminBulk.vue | 561 ++++++++++++++++++ .../loggerSummary/LoggerSummaryCard.vue | 155 +++-- src/components/tags/TagsAdmin.vue | 43 +- src/components/tags/TagsCard.vue | 104 +--- .../user-statistics/UserStatisticsCard.vue | 97 ++- src/composables/useAutomaticGroups.ts | 115 ++++ src/composables/useExpiringAppointments.ts | 45 ++ src/composables/useLoggerBulkCache.ts | 339 +++++++++++ src/composables/useLoggerSummaryQuery.ts | 258 ++++++++ src/composables/useTags.ts | 72 +++ src/composables/useUserStatistics.ts | 76 +++ src/main.ts | 23 +- src/services/churchtools.ts | 2 +- 25 files changed, 2500 insertions(+), 448 deletions(-) create mode 100644 CACHE_TESTING.md delete mode 100644 src/components/automatic-groups/useAutomaticGroups.ts create mode 100644 src/components/common/CacheDebug.vue create mode 100644 src/components/common/CacheTest.vue create mode 100644 src/components/loggerSummary/LoggerSummaryAdminBulk.vue create mode 100644 src/composables/useAutomaticGroups.ts create mode 100644 src/composables/useExpiringAppointments.ts create mode 100644 src/composables/useLoggerBulkCache.ts create mode 100644 src/composables/useLoggerSummaryQuery.ts create mode 100644 src/composables/useTags.ts create mode 100644 src/composables/useUserStatistics.ts diff --git a/CACHE_TESTING.md b/CACHE_TESTING.md new file mode 100644 index 0000000..ecdc3d2 --- /dev/null +++ b/CACHE_TESTING.md @@ -0,0 +1,142 @@ +# 🚀 Cache Testing Guide + +## So testest du die Caching-Performance + +### 1. **Cache Debug Panel** + +- Oben rechts siehst du ein schwarzes Debug-Panel (nur im Development-Modus) +- Zeigt den Status aller Queries: Frisch, Veraltet, Lädt... +- Zeigt das Alter der Daten (z.B. "30s" = vor 30 Sekunden geladen) + +### 2. **Cache-Wirkung testen** + +#### **Szenario A: Navigation zwischen Dashboard und Admin** + +1. Öffne das Dashboard → warte bis alle Daten geladen sind +2. Klicke auf "Details" bei einer Karte (z.B. Tags) +3. Klicke "← Zurück zum Dashboard" +4. **Ergebnis**: Daten erscheinen SOFORT (aus Cache) + +#### **Szenario B: Browser-Tab wechseln** + +1. Lade das Dashboard +2. Wechsle zu einem anderen Tab (5+ Minuten) +3. Komme zurück zum Dashboard-Tab +4. **Ergebnis**: Daten sind sofort da + werden im Hintergrund aktualisiert + +#### **Szenario C: Refresh-Button** + +1. Klicke "Aktualisieren" bei einer Karte +2. **Ergebnis**: Nur diese Karte lädt neu, andere bleiben unberührt + +### 3. **Performance-Messungen** + +#### **Browser-Konsole öffnen (F12)** + +Schaue nach diesen Logs: + +``` +🏷️ Tags fetched: 25 tags in 234ms +📅 Setting up expiring appointments query +⚙️ Groups fetched: 12 groups in 456ms +``` + +#### **Network Tab (F12 → Network)** + +- **Erster Besuch**: Alle API-Calls sichtbar +- **Wiederholter Besuch**: Keine/wenige API-Calls (Cache!) + +### 4. **Cache-Strategien pro Modul** + +| Modul | Cache-Zeit | Background-Update | Test | +| ----------- | ---------- | ----------------- | ----------------------------- | +| **Tags** | 1 Stunde | Nie | Sehr schnell bei Wiederholung | +| **Termine** | 30 Min | 15 Min | Mittlere Geschwindigkeit | +| **Gruppen** | 10 Min | 5 Min | Häufige Updates | +| **Logs** | 2 Min | 1 Min | Sehr häufige Updates | + +### 5. **Erwartete Performance-Verbesserungen** + +#### **Ohne Cache (vorher)** + +- Dashboard laden: 3-8 Sekunden +- Navigation: 2-5 Sekunden pro Wechsel +- Jeder Besuch = neue API-Calls + +#### **Mit Cache (jetzt)** + +- Erster Besuch: 3-8 Sekunden (normal) +- **Wiederholte Besuche: <1 Sekunde** ⚡ +- Navigation: Sofort (aus Cache) +- Background-Updates: Unsichtbar für User + +### 6. **Debug-Befehle** + +#### **Browser-Konsole** + +```javascript +// Cache-Status anzeigen +queryClient.getQueryCache().getAll() + +// Bestimmte Query prüfen +queryClient.getQueryState(["tags"]) + +// Cache leeren +queryClient.clear() +``` + +### 7. **Typische Cache-Szenarien** + +#### **🟢 Cache Hit (schnell)** + +``` +Status: Frisch (grün) +Alter: 2m +Ladezeit: <100ms +``` + +#### **🟡 Stale-While-Revalidate** + +``` +Status: Veraltet (gelb) +Verhalten: Sofortige Anzeige + Background-Update +``` + +#### **🔵 Background Fetch** + +``` +Status: Lädt... (blau) +Verhalten: Alte Daten sichtbar + neue werden geladen +``` + +### 8. **Troubleshooting** + +#### **"Ich sehe keinen Unterschied"** + +- Warte bis alle Daten initial geladen sind +- Navigiere zwischen Dashboard ↔ Admin mehrmals +- Schaue auf das Debug-Panel (Status sollte "Frisch" sein) + +#### **"Cache funktioniert nicht"** + +- Browser-Konsole prüfen auf Fehler +- Network-Tab: Sollten weniger Requests sein +- Debug-Panel: Status sollte nicht immer "Nicht geladen" sein + +### 9. **Erweiterte Tests** + +#### **Offline-Verhalten** + +1. Lade Dashboard vollständig +2. Gehe offline (Network → Offline) +3. Navigiere zwischen Seiten +4. **Ergebnis**: Cached Daten bleiben verfügbar + +#### **Concurrent Requests** + +1. Öffne mehrere Tabs mit dem Dashboard +2. **Ergebnis**: Nur ein API-Call pro Query (Request Deduplication) + +--- + +**💡 Tipp**: Die größten Performance-Gewinne siehst du bei wiederholten Aktionen, nicht beim ersten Laden! diff --git a/package-lock.json b/package-lock.json index c95c2ac..afbcdf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@fortawesome/fontawesome-svg-core": "^7.0.1", "@fortawesome/free-solid-svg-icons": "^7.0.1", "@fortawesome/vue-fontawesome": "^3.1.2", + "@tanstack/vue-query": "^5.90.2", "@vitejs/plugin-vue": "^6.0.1", "vue": "^3.5.21", "vue-i18n": "^9.14.5" @@ -889,6 +890,83 @@ "win32" ] }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", + "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==", + "license": "MIT", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.90.2.tgz", + "integrity": "sha512-DLLY/B5QCbpi6AM2aaCowukQx2rXsQ4mH8RuDd8wQz0/L2bZ9Z/GgXlV310ouo47pJBmeibMVTmuoWsleT8llg==", + "license": "MIT", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.19.4", + "@tanstack/query-core": "5.90.2", + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.2", + "vue": "^2.6.0 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@tanstack/vue-query/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1645,6 +1723,12 @@ "dev": true, "license": "MIT" }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.50.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", diff --git a/package.json b/package.json index 5eeb957..bb270a6 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@fortawesome/fontawesome-svg-core": "^7.0.1", "@fortawesome/free-solid-svg-icons": "^7.0.1", "@fortawesome/vue-fontawesome": "^3.1.2", + "@tanstack/vue-query": "^5.90.2", "@vitejs/plugin-vue": "^6.0.1", "vue": "^3.5.21", "vue-i18n": "^9.14.5" diff --git a/src/App.vue b/src/App.vue index 3bf7352..520e02e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,9 @@ diff --git a/src/components/common/CacheTest.vue b/src/components/common/CacheTest.vue new file mode 100644 index 0000000..f221a30 --- /dev/null +++ b/src/components/common/CacheTest.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue b/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue index 74db297..360321d 100644 --- a/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue +++ b/src/components/expiring-appointments/ExpiringAppointmentsAdmin.vue @@ -120,10 +120,11 @@ diff --git a/src/components/expiring-appointments/ExpiringAppointmentsCard.vue b/src/components/expiring-appointments/ExpiringAppointmentsCard.vue index 150401a..2d8e26d 100644 --- a/src/components/expiring-appointments/ExpiringAppointmentsCard.vue +++ b/src/components/expiring-appointments/ExpiringAppointmentsCard.vue @@ -20,8 +20,11 @@ diff --git a/src/components/loggerSummary/LoggerSummaryAdmin.vue b/src/components/loggerSummary/LoggerSummaryAdmin.vue index 580653b..a9cef58 100644 --- a/src/components/loggerSummary/LoggerSummaryAdmin.vue +++ b/src/components/loggerSummary/LoggerSummaryAdmin.vue @@ -155,6 +155,7 @@ import { getCategoryCssClass, getAllCategories, } from './useLoggerSummary' +import { useLoggerSummary as useLoggerSummaryQuery } from '@/composables/useLoggerSummaryQuery' // Use the ProcessedLogEntry type from the composable type LogEntry = ProcessedLogEntry diff --git a/src/components/loggerSummary/LoggerSummaryAdminBulk.vue b/src/components/loggerSummary/LoggerSummaryAdminBulk.vue new file mode 100644 index 0000000..d7e85e5 --- /dev/null +++ b/src/components/loggerSummary/LoggerSummaryAdminBulk.vue @@ -0,0 +1,561 @@ + + + + + diff --git a/src/components/loggerSummary/LoggerSummaryCard.vue b/src/components/loggerSummary/LoggerSummaryCard.vue index 1bf7539..c1f4291 100644 --- a/src/components/loggerSummary/LoggerSummaryCard.vue +++ b/src/components/loggerSummary/LoggerSummaryCard.vue @@ -20,14 +20,14 @@ diff --git a/src/components/tags/TagsAdmin.vue b/src/components/tags/TagsAdmin.vue index d429ed5..0f3511b 100644 --- a/src/components/tags/TagsAdmin.vue +++ b/src/components/tags/TagsAdmin.vue @@ -269,6 +269,7 @@ import AdminTable from '@/components/common/AdminTable.vue' import ColorPicker from '@/components/common/ColorPicker.vue' import { useToast } from '@/composables/useToast' +import { useTags as useTagsQuery } from '@/composables/useTags' import { useTags } from './useTags' defineProps<{ @@ -278,11 +279,14 @@ defineProps<{ // Toast functionality const { showSuccess, showError, showInfo } = useToast() -// Use tags composable +// Use cached tags data from TanStack Query +const { data: cachedTags, isLoading: cacheLoading, error: cacheError, refetch } = useTagsQuery() + +// Use local tags composable for admin functions const { tags, - loading: isLoading, - error, + loading: localLoading, + error: localError, selectedDomain, personTagsCount, songTagsCount, @@ -294,6 +298,27 @@ const { bulkDeleteTags, } = useTags() +// Prefer cached data when available, fallback to local +const isLoading = computed(() => cacheLoading.value || localLoading.value) +const error = computed(() => cacheError.value?.message || localError.value) + +// Use cached tags if available, otherwise use local tags +watch( + cachedTags, + (newCachedTags) => { + if (newCachedTags && newCachedTags.length > 0) { + console.log('🏷️ TagsAdmin: Using cached tags:', newCachedTags.length) + tags.value = newCachedTags + } else { + console.log('🏷️ TagsAdmin: No cached tags available') + } + }, + { immediate: true } +) + +// Log when component mounts +console.log('🏷️ TagsAdmin: Component mounted') + // Local state for UI const selectedTags = ref([]) const prefixFilter = ref('') @@ -418,9 +443,10 @@ const selectByPrefix = () => { .map((tag) => tag.id) } -// Data loading +// Data loading - use cache refetch for better performance const refreshData = () => { - fetchTags() + refetch() // Refresh cached data + fetchTags() // Also refresh local data for admin functions } const showCreateModal = () => { @@ -608,9 +634,12 @@ watch( { immediate: true } ) -// Initialize +// Initialize - no need to fetch since cache will provide data onMounted(() => { - fetchTags() + // Only fetch if no cached data available + if (!cachedTags.value || cachedTags.value.length === 0) { + fetchTags() + } }) diff --git a/src/components/tags/TagsCard.vue b/src/components/tags/TagsCard.vue index 15f9d61..4fc80a9 100644 --- a/src/components/tags/TagsCard.vue +++ b/src/components/tags/TagsCard.vue @@ -20,22 +20,10 @@ diff --git a/src/components/user-statistics/UserStatisticsCard.vue b/src/components/user-statistics/UserStatisticsCard.vue index 8f4f189..daf796d 100644 --- a/src/components/user-statistics/UserStatisticsCard.vue +++ b/src/components/user-statistics/UserStatisticsCard.vue @@ -1,30 +1,91 @@ diff --git a/src/components/common/CacheTest.vue b/src/components/common/CacheTest.vue deleted file mode 100644 index f221a30..0000000 --- a/src/components/common/CacheTest.vue +++ /dev/null @@ -1,230 +0,0 @@ - - - - - diff --git a/src/components/loggerSummary/LoggerSummaryAdminBulk.vue b/src/components/loggerSummary/LoggerSummaryAdminBulk.vue index d7e85e5..ff59505 100644 --- a/src/components/loggerSummary/LoggerSummaryAdminBulk.vue +++ b/src/components/loggerSummary/LoggerSummaryAdminBulk.vue @@ -452,6 +452,32 @@ onMounted(() => { justify-content: center; } +/* Action Button Styles */ +.action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: calc(var(--spacing-xl) + var(--spacing-sm)); + height: calc(var(--spacing-xl) + var(--spacing-sm)); + border: var(--spacing-xs) solid var(--color-border); + border-radius: var(--border-radius-sm); + background: var(--color-background-card); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + cursor: pointer; + transition: var(--transition-base); + padding: 0; + text-decoration: none; +} + +.action-btn:hover { + border-color: var(--color-primary); + background: var(--color-background); + color: var(--color-primary); + transform: translateY(calc(-1 * var(--spacing-xs))); + box-shadow: var(--shadow-sm); +} + /* Modal Styles */ .modal-overlay { position: fixed; From a5b3ba60172aaa4256115c49448290f513b5cfc5 Mon Sep 17 00:00:00 2001 From: Bernhard Weichel Date: Fri, 26 Sep 2025 12:13:29 +0000 Subject: [PATCH 3/6] fix: improve cache toast timing and standardize Details button styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix toast to show during data loading instead of after completion - Update toast message to explain wait time: '🔄 Logger-Daten werden aktualisiert...' - Remove confusing cache reference from component description - Standardize Details buttons to match other admin tables (blue outline styling) - Add ct-btn-primary-outline class for consistent blue button appearance - Ensure Details buttons look identical to 'Öffnen' buttons in other components Co-authored-by: Ona --- .../loggerSummary/LoggerSummaryAdmin.vue | 61 +++++++++++++++++-- .../loggerSummary/LoggerSummaryAdminBulk.vue | 44 +++++++++++-- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/components/loggerSummary/LoggerSummaryAdmin.vue b/src/components/loggerSummary/LoggerSummaryAdmin.vue index a9cef58..47adcde 100644 --- a/src/components/loggerSummary/LoggerSummaryAdmin.vue +++ b/src/components/loggerSummary/LoggerSummaryAdmin.vue @@ -74,7 +74,7 @@