HonzaH commited on
Commit
4c73bfc
·
verified ·
1 Parent(s): 3f037e6

Upload 180 files

Browse files
docs/lessons-learned.md CHANGED
@@ -1368,74 +1368,6 @@ timestamp = get_current_timestamp()
1368
 
1369
  ---
1370
 
1371
- ### Synchronizace Dat Mezi Stores (2025-11-11)
1372
-
1373
- **Problém:** Nekonzistence mezi journal store (zobrazení historie) a completedActions store (single source of truth pro statistiky). Uživatel viděl v deníku 7 záznamů, ale profil ukazoval "Deník: 25/25" kvůli historickým datům, kde se záznamy přidávaly do journal bez odpovídajících akcí v completedActions.
1374
-
1375
- **Architektonický kontext:**
1376
- - **journal store**: Zobrazení historie pro uživatele (medaile, fáze, aktivity, tipy, meditace)
1377
- - **completedActions store**: Single source of truth pro všechny statistiky a medaile
1378
- - **Store-First Architecture**: Všechny akce se ukládají do stores PRVNÍ, pak se logují do chatu/journal
1379
-
1380
- **Původní problém:**
1381
- ```typescript
1382
- // ❌ Nekonzistentní výpočet v ProfileView
1383
- journalEntries: (journal && journal.length) || profileStats.journal_entries || completedActions.filter(a => a.type === 'journal').length || 0
1384
- ```
1385
-
1386
- **Řešení - Zjednodušení:**
1387
- ```typescript
1388
- // ✅ Pouze journal.length jako zdroj pravdy pro zobrazení
1389
- journalEntries: journal?.length || 0
1390
- ```
1391
-
1392
- **Řešení - Synchronizace:**
1393
- ```typescript
1394
- // ✅ Automatická synchronizace při načítání aplikace
1395
- export async function loadCompletedActions() {
1396
- // Načtení existujících dat...
1397
-
1398
- // Synchronizace s journal entries
1399
- setTimeout(async () => {
1400
- const journalState = (await import('./journal')).useJournal.getState()
1401
- const journalEntries = journalState.journal || []
1402
- const currentActions = useCompletedActions.getState().actions
1403
-
1404
- const journalActions = currentActions.filter(a => a.type === 'journal')
1405
- const missingActions: CompletedAction[] = []
1406
-
1407
- for (const entry of journalEntries) {
1408
- const hasAction = journalActions.some(a => a.id === entry.id)
1409
- if (!hasAction) {
1410
- missingActions.push({
1411
- type: 'journal',
1412
- id: entry.id,
1413
- completedAt: entry.ts
1414
- })
1415
- }
1416
- }
1417
-
1418
- if (missingActions.length > 0) {
1419
- const updated = [...currentActions, ...missingActions]
1420
- useCompletedActions.setState({ actions: updated })
1421
- await idbSet('completedActions', updated)
1422
- }
1423
- }, 100)
1424
- }
1425
- ```
1426
-
1427
- **Architektonické principy:**
1428
- - **Single Source of Truth:** Každý typ dat má jasně definovaný primární store
1429
- - **Store-First Pattern:** Ukládání do stores → logování do chatu/journal → kontrola medailí
1430
- - **Backward Compatibility:** Synchronizace starých dat bez ztráty informací
1431
- - **Lazy Synchronization:** Synchronizace probíhá při načítání aplikace, neblokuje UI
1432
-
1433
- **Benefit:**
1434
- - ✅ Konzistentní statistiky napříč aplikací
1435
- - ✅ Oprava historických nekonzistencí bez ztráty dat
1436
- - ✅ Jasná odpovědnost každého store
1437
- - ✅ Robustní architektura odolná vůči datovým nekonzistencím
1438
-
1439
  ---
1440
 
1441
  **💡 Tip:** Při vývoji vždy nejdříve zkontroluj tento dokument, zda podobný problém už nebyl vyřešen.
 
1368
 
1369
  ---
1370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1371
  ---
1372
 
1373
  **💡 Tip:** Při vývoji vždy nejdříve zkontroluj tento dokument, zda podobný problém už nebyl vyřešen.
frontend/src/state/modules/completedActions.ts CHANGED
@@ -56,33 +56,4 @@ export async function loadCompletedActions() {
56
  if (saved && Array.isArray(saved)) {
57
  useCompletedActions.setState({ actions: saved })
58
  }
59
-
60
- // Synchronize with journal entries - ensure every journal entry has a corresponding 'journal' action
61
- // This fixes historical inconsistencies where journal entries were added without completedActions
62
- setTimeout(async () => {
63
- const journalState = (await import('./journal')).useJournal.getState()
64
- const journalEntries = journalState.journal || []
65
- const currentActions = useCompletedActions.getState().actions
66
-
67
- const journalActions = currentActions.filter(a => a.type === 'journal')
68
- const missingActions: CompletedAction[] = []
69
-
70
- for (const entry of journalEntries) {
71
- const hasAction = journalActions.some(a => a.id === entry.id)
72
- if (!hasAction) {
73
- missingActions.push({
74
- type: 'journal',
75
- id: entry.id,
76
- completedAt: entry.ts
77
- })
78
- }
79
- }
80
-
81
- if (missingActions.length > 0) {
82
- console.log('loadCompletedActions: Adding missing journal actions:', missingActions.length)
83
- const updated = [...currentActions, ...missingActions]
84
- useCompletedActions.setState({ actions: updated })
85
- await idbSet('completedActions', updated)
86
- }
87
- }, 100) // Small delay to ensure journal is loaded
88
  }
 
56
  if (saved && Array.isArray(saved)) {
57
  useCompletedActions.setState({ actions: saved })
58
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  }
frontend/src/state/modules/journal.ts CHANGED
@@ -16,13 +16,6 @@ export const useJournal = create<{ journal: JournalEntry[]; addJournal: (text:st
16
  } catch (err) {
17
  console.error('Error persisting journal entry:', err)
18
  }
19
- // Record journal entry also in completedActions so completedActions
20
- // remains the single source of truth for all counts/metrics.
21
- try {
22
- await useCompletedActions.getState().addAction('journal', entry.id)
23
- } catch (err) {
24
- console.error('Error adding journal entry to completedActions:', err)
25
- }
26
  // Check for journal-related medals (like "3 deníkové zápisy")
27
  await checkAndLogMedals(['journal'])
28
  // Recompute user stage after journaling (affects phase detection)
 
16
  } catch (err) {
17
  console.error('Error persisting journal entry:', err)
18
  }
 
 
 
 
 
 
 
19
  // Check for journal-related medals (like "3 deníkové zápisy")
20
  await checkAndLogMedals(['journal'])
21
  // Recompute user stage after journaling (affects phase detection)
frontend/src/views/ProfileView.tsx CHANGED
@@ -2,7 +2,6 @@ import { ChangeEvent, useCallback, useEffect, useLayoutEffect, useRef, useState
2
  import '../lib/intersectionObserverPolyfill'
3
  import { useTranslation, useLocale } from '../lib/LocaleContext'
4
  import { useProfile } from '../state/modules/profile'
5
- import { useJournal } from '../state/modules/journal'
6
  import { useChat } from '../state/modules/chat'
7
  import { useNoContact } from '../state/modules/noContact'
8
  import { useMedal } from '../state/modules/medal'
@@ -54,7 +53,6 @@ export default function ProfileView({ accent }: Props) {
54
  const profile = useProfile(state => state.profile)
55
  const setProfileName = useProfile(state => state.setProfileName)
56
  const chat = useChat(state => state.chat)
57
- const journal = useJournal(state => state.journal)
58
  const noContact = useNoContact(state => state.noContact)
59
  const startNoContact = useNoContact(state => state.startNoContact)
60
  const resetNoContact = useNoContact(state => state.resetNoContact)
@@ -131,6 +129,17 @@ export default function ProfileView({ accent }: Props) {
131
  const meditationsCompleted = calculateMeditationsCompleted(completedActions)
132
  const profileStats = calculateProfileStats(completedActions, noContact.startISO, chat.length > 0)
133
 
 
 
 
 
 
 
 
 
 
 
 
134
  // Create medal progress based on earned medals and current progress
135
  const medalProgress = medalsContent?.medals?.map((medalDef: MedalDefinition) => {
136
  const isEarned = earnedMedals.some(m => m.id === medalDef.id)
@@ -161,11 +170,8 @@ export default function ProfileView({ accent }: Props) {
161
  const summary = {
162
  stats: {
163
  chatMessages: chat.length || 0,
164
- // Journal count: prefer the local `journal` display store (what the user sees)
165
- // as the primary source for the "Deník" metric. If unavailable fall back to
166
- // canonical profileStats (derived from completedActions) and finally to
167
- // counting journal-type completedActions for backwards compatibility.
168
- journalEntries: journal?.length || 0,
169
  noContactDays: noContactDays,
170
  activitiesCompleted: activitiesCompleted,
171
  tipsCompleted: tipsCompleted,
@@ -191,11 +197,11 @@ export default function ProfileView({ accent }: Props) {
191
  } finally {
192
  setLoading(false)
193
  }
194
- }, [chat, journal, noContact, medalsContent, locale, completedActions]) // React to changes in stores, medals content and locale
195
 
196
  useLayoutEffect(() => {
197
  loadSummary()
198
- }, [chat, journal, noContact, medalsContent, locale, completedActions]) // React to all data changes and locale changes
199
 
200
  const handleSave = () => {
201
  const trimmed = name.trim()
@@ -296,26 +302,23 @@ export default function ProfileView({ accent }: Props) {
296
  sessionStorage.clear()
297
  console.log('Browser storage cleared')
298
 
299
- console.log('Step 2: Deleting IndexedDB data...')
300
- // Smazat všechna data z IndexedDB
301
- // Note: keys must match those used by the stores (medals, completedActions, noContact, profile, chat, journal, last_active_tab)
302
  await Promise.all([
303
- idbDel('profile'),
304
- idbDel('chat'),
305
- idbDel('journal'),
306
- idbDel('noContact'),
307
- idbDel('medals'),
308
- idbDel('completedActions'),
309
- idbDel('last_active_tab')
310
- // onboarding_completed is no longer used
311
  ])
312
- console.log('IndexedDB data deleted')
313
 
314
  console.log('Step 3: Resetting Zustand stores...')
315
  // Resetovat Zustand stavy
316
  useProfile.setState({ profile: { name: '' } })
317
  useChat.setState({ chat: [] })
318
- useJournal.setState({ journal: [] })
319
  useNoContact.setState({ noContact: { startISO: null, lastPraiseISO: null, reasons: [] } })
320
  useMedal.setState({ medals: [] })
321
  useCompletedActions.setState({ actions: [] })
@@ -323,12 +326,27 @@ export default function ProfileView({ accent }: Props) {
323
 
324
  console.log('Step 4: Force page reload with cache busting...')
325
  // Force reload s cache busting pro čistý start
326
- const timestamp = Date.now()
327
- const random = Math.random()
328
- const newUrl = `${window.location.origin}${window.location.pathname}?reset=1&clear=${random}`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
- // Use replace to avoid back button issues
331
- window.location.replace(newUrl)
 
332
 
333
  } catch (error) {
334
  console.error('Error during forget process:', error)
 
2
  import '../lib/intersectionObserverPolyfill'
3
  import { useTranslation, useLocale } from '../lib/LocaleContext'
4
  import { useProfile } from '../state/modules/profile'
 
5
  import { useChat } from '../state/modules/chat'
6
  import { useNoContact } from '../state/modules/noContact'
7
  import { useMedal } from '../state/modules/medal'
 
53
  const profile = useProfile(state => state.profile)
54
  const setProfileName = useProfile(state => state.setProfileName)
55
  const chat = useChat(state => state.chat)
 
56
  const noContact = useNoContact(state => state.noContact)
57
  const startNoContact = useNoContact(state => state.startNoContact)
58
  const resetNoContact = useNoContact(state => state.resetNoContact)
 
129
  const meditationsCompleted = calculateMeditationsCompleted(completedActions)
130
  const profileStats = calculateProfileStats(completedActions, noContact.startISO, chat.length > 0)
131
 
132
+ // DEBUG: Log current data
133
+ console.log('=== PROFILE STATS DEBUG ===')
134
+ console.log('completedActions:', completedActions)
135
+ console.log('activitiesCompleted:', activitiesCompleted)
136
+ console.log('tipsCompleted:', tipsCompleted)
137
+ console.log('meditationsCompleted:', meditationsCompleted)
138
+ console.log('journalEntries:', completedActions.filter(a => a.type === 'journal').length)
139
+ console.log('noContactDays:', noContactDays)
140
+ console.log('earnedMedals:', earnedMedals.length)
141
+ console.log('===========================')
142
+
143
  // Create medal progress based on earned medals and current progress
144
  const medalProgress = medalsContent?.medals?.map((medalDef: MedalDefinition) => {
145
  const isEarned = earnedMedals.some(m => m.id === medalDef.id)
 
170
  const summary = {
171
  stats: {
172
  chatMessages: chat.length || 0,
173
+ // Journal count: use completedActions as single source of truth for all statistics
174
+ journalEntries: completedActions.filter(a => a.type === 'journal').length,
 
 
 
175
  noContactDays: noContactDays,
176
  activitiesCompleted: activitiesCompleted,
177
  tipsCompleted: tipsCompleted,
 
197
  } finally {
198
  setLoading(false)
199
  }
200
+ }, [chat, noContact, medalsContent, locale, completedActions]) // React to changes in stores, medals content and locale
201
 
202
  useLayoutEffect(() => {
203
  loadSummary()
204
+ }, [chat, noContact, medalsContent, locale, completedActions]) // React to all data changes and locale changes
205
 
206
  const handleSave = () => {
207
  const trimmed = name.trim()
 
302
  sessionStorage.clear()
303
  console.log('Browser storage cleared')
304
 
305
+ console.log('Step 2: Resetting IndexedDB data...')
306
+ // Místo mazání dat je resetovat na prázdná pole pro jistotu
 
307
  await Promise.all([
308
+ idbSet('profile', { name: '' }),
309
+ idbSet('chat', []),
310
+ idbSet('journal', []),
311
+ idbSet('noContact', { startISO: null, lastPraiseISO: null, reasons: [] }),
312
+ idbSet('medals', []),
313
+ idbSet('completedActions', []),
314
+ idbSet('last_active_tab', 'chat')
 
315
  ])
316
+ console.log('IndexedDB data reset to empty')
317
 
318
  console.log('Step 3: Resetting Zustand stores...')
319
  // Resetovat Zustand stavy
320
  useProfile.setState({ profile: { name: '' } })
321
  useChat.setState({ chat: [] })
 
322
  useNoContact.setState({ noContact: { startISO: null, lastPraiseISO: null, reasons: [] } })
323
  useMedal.setState({ medals: [] })
324
  useCompletedActions.setState({ actions: [] })
 
326
 
327
  console.log('Step 4: Force page reload with cache busting...')
328
  // Force reload s cache busting pro čistý start
329
+ // Přidat delay pro zajištění, že se data stihnou smazat
330
+ setTimeout(async () => {
331
+ // Ověřit, že se data skutečně resetovala
332
+ const checkReset = await Promise.all([
333
+ idbGet('profile'),
334
+ idbGet('chat'),
335
+ idbGet('journal'),
336
+ idbGet('noContact'),
337
+ idbGet('medals'),
338
+ idbGet('completedActions'),
339
+ idbGet('last_active_tab')
340
+ ])
341
+ console.log('Verification: data after reset ->', checkReset)
342
+
343
+ const timestamp = Date.now()
344
+ const random = Math.random()
345
+ const newUrl = `${window.location.origin}${window.location.pathname}?reset=1&clear=${random}`
346
 
347
+ // Use replace to avoid back button issues
348
+ window.location.replace(newUrl)
349
+ }, 1000)
350
 
351
  } catch (error) {
352
  console.error('Error during forget process:', error)