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:
|
| 165 |
-
|
| 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,
|
| 195 |
|
| 196 |
useLayoutEffect(() => {
|
| 197 |
loadSummary()
|
| 198 |
-
}, [chat,
|
| 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:
|
| 300 |
-
//
|
| 301 |
-
// Note: keys must match those used by the stores (medals, completedActions, noContact, profile, chat, journal, last_active_tab)
|
| 302 |
await Promise.all([
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
// onboarding_completed is no longer used
|
| 311 |
])
|
| 312 |
-
console.log('IndexedDB data
|
| 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 |
-
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
|
|
|
| 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)
|