Upload 176 files
Browse files- README.md +29 -0
- app.py +27 -5
- docs/lessons-learned.md +25 -0
- frontend/src/lib/strings.ts +19 -1
- frontend/src/state/modules/chat.ts +25 -17
- frontend/src/state/modules/completedActions.ts +1 -0
- frontend/src/state/modules/journal.ts +7 -5
- frontend/src/state/modules/medal.ts +1 -0
- frontend/src/state/modules/noContact.ts +28 -4
- frontend/src/views/ProfileView.tsx +3 -1
- frontend/tests/unit.test.ts +12 -10
README.md
CHANGED
|
@@ -194,6 +194,35 @@ UI update (2025-11-03): No‑Contact controls
|
|
| 194 |
- Added a single, visible primary toggle alongside the "Upravit začátek" and "Nastavit na teď" actions. The primary toggle uses the canonical `noContact.startISO` state as the single source of truth and shows the labels "Zahájení Bez Kontaktu" (inactive) / "Ukončit Bez kontaktu" (active).
|
| 195 |
- This change reduces duplicated controls, simplifies the interaction surface, and makes the No‑Contact flow deterministic (optimistic UI updates remain in handlers).
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
UI & chat updates (2025-11-03):
|
| 198 |
- Chat message metadata: chat messages now carry optional `meta` (for example `{ type: 'medal', medal: 'První krok' }`) so the frontend can render special visual variants for high-value events (medals, assessment results, no-contact milestones).
|
| 199 |
- Medal visuals & divider: the frontend renders a small medal badge/divider and a distinct `.chat-bubble.medal` style for earned medals to make them immediately visible and celebratory.
|
|
|
|
| 194 |
- Added a single, visible primary toggle alongside the "Upravit začátek" and "Nastavit na teď" actions. The primary toggle uses the canonical `noContact.startISO` state as the single source of truth and shows the labels "Zahájení Bez Kontaktu" (inactive) / "Ukončit Bez kontaktu" (active).
|
| 195 |
- This change reduces duplicated controls, simplifies the interaction surface, and makes the No‑Contact flow deterministic (optimistic UI updates remain in handlers).
|
| 196 |
|
| 197 |
+
### Localization sweep (2025-11-10)
|
| 198 |
+
|
| 199 |
+
- Performed a repo-wide localization sweep to fix runtime i18n issues and make UI texts update immediately when the user switches language.
|
| 200 |
+
- Key fixes applied:
|
| 201 |
+
- Moved `LocaleProvider` to `frontend/src/main.tsx` so `useLocale()` / `useTranslation()` are available at app mount time (fixes "useLocale must be used within a LocaleProvider").
|
| 202 |
+
- Removed module-level calls to `t()` and moved translation-dependent constants into component scope (fixes "t is not defined" runtime errors).
|
| 203 |
+
- Ensured memoized values that depend on translated text include `locale` in their dependency arrays (notably `App.activeTab`, `ProfileView.loadSummary`, `AttachmentView.allQuestions`).
|
| 204 |
+
- Attachment content flow: `AttachmentView` displays per-locale content fields as-is (content buckets are localized) and keeps `locale` in memo deps; `AttachmentStylesView` now requests `/api/content/attachment_styles?locale={locale}` so content refetches when language changes.
|
| 205 |
+
- Added English activities content (`content/activities.en.json`) and updated `ActivitiesView` to load locale-appropriate content buckets.
|
| 206 |
+
- Build: frontend production build succeeded after the sweep and produced `dist/` assets.
|
| 207 |
+
|
| 208 |
+
## Recent developer updates (2025-11-10)
|
| 209 |
+
|
| 210 |
+
- Persistence & store reliability: several frontend stores (chat, journal, noContact and related loaders) were updated to await IndexedDB writes (`idbSet`) and to log failures. This reduces race conditions where writes could be lost during reloads or app shutdown. Loaders (`loadMedals`, `loadCompletedActions`, `loadNoContact`) now include diagnostic logs to help reproduce persistence issues.
|
| 211 |
+
|
| 212 |
+
- Activity logging consistency: `activityLogger` usage has been consolidated in places to ensure store-first writes are completed before emitting assistant/journal messages. Helpers now await persistence and medal checks to avoid race conditions.
|
| 213 |
+
|
| 214 |
+
- Translation helper update: `frontend/src/lib/strings.ts` was updated
|
| 215 |
+
|
| 216 |
+
- Tests & typing: Unit-tests were updated to reflect corrected store shapes and keys (for example `medals` and `completedActions`). A small typing fix in `frontend/tests/unit.test.ts` resolves a TS2339 complaint related to a test mock shape.
|
| 217 |
+
|
| 218 |
+
- Server SPA cache: the backend serving logic was tuned to serve `index.html` with a no-cache policy so clients revalidate the HTML (avoids HF Spaces / CDN stale-hashed-asset 404s when deploys remove old hashed assets).
|
| 219 |
+
|
| 220 |
+
These developer-facing changes were made to make debugging persistence and localization issues easier. See `docs/lessons-learned.md` for guidance on interpreting the new diagnostic logs and on the recommended store-first logging pattern.
|
| 221 |
+
|
| 222 |
+
Next recommended steps:
|
| 223 |
+
- Run the runtime QA flow (start backend + frontend/dev server and verify header, tabs, Activities, Attachment and Profile update immediately on language switch).
|
| 224 |
+
- Optionally run a full automated patch to add `locale` to remaining memo/callback deps across the codebase; this is low-risk but touches many files and should be validated by the test suite.
|
| 225 |
+
|
| 226 |
UI & chat updates (2025-11-03):
|
| 227 |
- Chat message metadata: chat messages now carry optional `meta` (for example `{ type: 'medal', medal: 'První krok' }`) so the frontend can render special visual variants for high-value events (medals, assessment results, no-contact milestones).
|
| 228 |
- Medal visuals & divider: the frontend renders a small medal badge/divider and a distinct `.chat-bubble.medal` style for earned medals to make them immediately visible and celebratory.
|
app.py
CHANGED
|
@@ -34,13 +34,18 @@ def create_app() -> FastAPI:
|
|
| 34 |
app.include_router(user_state_router)
|
| 35 |
app.include_router(sse_router)
|
| 36 |
|
| 37 |
-
# Mount static assets if available
|
| 38 |
if os.path.exists(ASSETS_DIR):
|
| 39 |
app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
|
| 40 |
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
return app
|
| 46 |
|
|
@@ -60,7 +65,24 @@ def favicon():
|
|
| 60 |
def index():
|
| 61 |
path = os.path.join(DIST_DIR, "index.html")
|
| 62 |
if os.path.exists(path):
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
raise HTTPException(status_code=404, detail="index.html not found")
|
| 65 |
|
| 66 |
|
|
|
|
| 34 |
app.include_router(user_state_router)
|
| 35 |
app.include_router(sse_router)
|
| 36 |
|
| 37 |
+
# Mount static assets if available (serve hashed assets under /assets)
|
| 38 |
if os.path.exists(ASSETS_DIR):
|
| 39 |
app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
|
| 40 |
|
| 41 |
+
# Serve the SPA index.html as a controlled fallback for non-API routes.
|
| 42 |
+
# We intentionally do NOT mount the whole `dist` directory at `/` because
|
| 43 |
+
# StaticFiles would serve `index.html` without custom cache headers. To
|
| 44 |
+
# avoid clients reusing stale `index.html` (which can reference old hashed
|
| 45 |
+
# asset filenames that no longer exist and cause 404s), we return
|
| 46 |
+
# `index.html` via an explicit fallback route with Cache-Control: no-cache
|
| 47 |
+
# so browsers revalidate and fetch the latest HTML after a deploy.
|
| 48 |
+
# Note: individual hashed assets under /assets can be cached long-term.
|
| 49 |
|
| 50 |
return app
|
| 51 |
|
|
|
|
| 65 |
def index():
|
| 66 |
path = os.path.join(DIST_DIR, "index.html")
|
| 67 |
if os.path.exists(path):
|
| 68 |
+
# Serve index.html with headers that prevent long-lived caching of the
|
| 69 |
+
# HTML itself. This ensures clients revalidate the page when we deploy
|
| 70 |
+
# a new build (and prevents requests for old hashed asset names).
|
| 71 |
+
return FileResponse(path, headers={"Cache-Control": "no-cache, no-store, must-revalidate"})
|
| 72 |
+
raise HTTPException(status_code=404, detail="index.html not found")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@app.get("/{full_path:path}", include_in_schema=False)
|
| 76 |
+
def spa_fallback(full_path: str):
|
| 77 |
+
"""Fallback for SPA routes: return index.html with no-cache headers.
|
| 78 |
+
|
| 79 |
+
This lets the frontend use client-side routing while ensuring the
|
| 80 |
+
HTML is revalidated after deploys, which prevents clients from trying to
|
| 81 |
+
fetch stale hashed asset filenames that no longer exist on disk.
|
| 82 |
+
"""
|
| 83 |
+
path = os.path.join(DIST_DIR, "index.html")
|
| 84 |
+
if os.path.exists(path):
|
| 85 |
+
return FileResponse(path, headers={"Cache-Control": "no-cache, no-store, must-revalidate"})
|
| 86 |
raise HTTPException(status_code=404, detail="index.html not found")
|
| 87 |
|
| 88 |
|
docs/lessons-learned.md
CHANGED
|
@@ -50,6 +50,31 @@ const message = t('logger.activity_completed', locale, { activity: activityName
|
|
| 50 |
|
| 51 |
---
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
### SupportView t() Funkce Bez Locale
|
| 54 |
|
| 55 |
**Problém:** `Uncaught ReferenceError: t is not defined` v SupportView.tsx - pole `RESOURCES` a `SUBTAB_LABELS` používaly funkci `t()` bez locale parametru na module level.
|
|
|
|
| 50 |
|
| 51 |
---
|
| 52 |
|
| 53 |
+
## 2025-11-10: Persistence, tests & helper updates
|
| 54 |
+
|
| 55 |
+
Short summary of recent developer fixes and guidance for future contributors:
|
| 56 |
+
|
| 57 |
+
- Persistence writes now awaited: Several stores (`chat`, `journal`, `noContact`, and related save paths) were changed to update Zustand state first and then `await` the `idbSet(key, value)` call. This reduces lost-writes during reloads and shutdowns. When adding new stores, follow the pattern: update store → await persistence → emit assistant/journal messages.
|
| 58 |
+
|
| 59 |
+
- Diagnostic load logging: `loadMedals()`, `loadCompletedActions()` and `loadNoContact()` include console logs to surface what is read from IndexedDB on startup. Use these logs when reproducing persistence issues.
|
| 60 |
+
|
| 61 |
+
- ActivityLogger ordering: The atomic helper `emitAssistantAndJournalWithOptions` is the recommended API; make sure to await persistence operations before triggering medal checks or UI-only journal messages.
|
| 62 |
+
|
| 63 |
+
- `t()` helper relaxed signature: To reduce widespread TS errors from varying call-sites, `frontend/src/lib/strings.ts` now supports calls like `t(key, vars?)` (locale defaults to `getLocale()`). Avoid calling `t()` at module scope; prefer within component render/effects.
|
| 64 |
+
|
| 65 |
+
- Test updates: Unit tests were adjusted to match corrected store key names and shapes (for example `medals` and `completedActions`). When writing tests that mock stores, prefer explicit `any` typed mocks or properly shaped objects to avoid TypeScript `never` inference issues in mock expectations.
|
| 66 |
+
|
| 67 |
+
Quick checklist when debugging persistence/localization issues:
|
| 68 |
+
|
| 69 |
+
1. Inspect the browser console for `load*` logs produced by `loadMedals`, `loadCompletedActions`, and `loadNoContact` on app start.
|
| 70 |
+
2. Ensure store writes use: `set(newState)` then `await idbSet(key, newState)` inside try/catch and log errors.
|
| 71 |
+
3. If medals or counts appear missing after reload, confirm the keys used in `idbSet` exactly match keys used in loaders (`'medals'`, `'completedActions'`, `'noContact'`, `'chat'`, `'journal'`).
|
| 72 |
+
4. Avoid calling `t()` at module top-level; keep translations inside components or effects and include `locale` in memo/deps when caching translated values.
|
| 73 |
+
|
| 74 |
+
These notes were added after a targeted debugging and test-fix session on 2025-11-10. Keep them in mind when adding new stores or updating activity logging to prevent regressions.
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
### SupportView t() Funkce Bez Locale
|
| 79 |
|
| 80 |
**Problém:** `Uncaught ReferenceError: t is not defined` v SupportView.tsx - pole `RESOURCES` a `SUBTAB_LABELS` používaly funkci `t()` bez locale parametru na module level.
|
frontend/src/lib/strings.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import cs from '../i18n/cs.json'
|
| 2 |
import en from '../i18n/en.json'
|
|
|
|
| 3 |
|
| 4 |
const bundles: Record<string, any> = {
|
| 5 |
cs,
|
|
@@ -16,7 +17,24 @@ function lookup(key: string, bundle: any) {
|
|
| 16 |
return cur
|
| 17 |
}
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
const bundle = bundles[locale] || bundles['cs']
|
| 21 |
let str = lookup(key, bundle) || lookup(key, bundles['cs']) || key
|
| 22 |
if (vars) {
|
|
|
|
| 1 |
import cs from '../i18n/cs.json'
|
| 2 |
import en from '../i18n/en.json'
|
| 3 |
+
import { getLocale } from './locale'
|
| 4 |
|
| 5 |
const bundles: Record<string, any> = {
|
| 6 |
cs,
|
|
|
|
| 17 |
return cur
|
| 18 |
}
|
| 19 |
|
| 20 |
+
/**
|
| 21 |
+
* Translation helper.
|
| 22 |
+
* Supports two calling styles:
|
| 23 |
+
* t(key, locale?, vars?)
|
| 24 |
+
* t(key, vars?) // uses stored locale
|
| 25 |
+
*/
|
| 26 |
+
export function t(key: string, localeOrVars?: string | Record<string, string | number>, maybeVars?: Record<string, string | number>) {
|
| 27 |
+
let locale: string
|
| 28 |
+
let vars: Record<string, string | number> | undefined
|
| 29 |
+
|
| 30 |
+
if (typeof localeOrVars === 'string') {
|
| 31 |
+
locale = localeOrVars
|
| 32 |
+
vars = maybeVars
|
| 33 |
+
} else {
|
| 34 |
+
locale = getLocale()
|
| 35 |
+
vars = localeOrVars as Record<string, string | number> | undefined
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
const bundle = bundles[locale] || bundles['cs']
|
| 39 |
let str = lookup(key, bundle) || lookup(key, bundles['cs']) || key
|
| 40 |
if (vars) {
|
frontend/src/state/modules/chat.ts
CHANGED
|
@@ -7,32 +7,40 @@ export const useChat = create<{ chat: Msg[]; push: (m:Msg)=>void; appendToken: (
|
|
| 7 |
chat: [],
|
| 8 |
push: async (m)=> {
|
| 9 |
const withTs = { ...m, ts: m.ts || new Date().toISOString() }
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
// REMOVED: checkAndLogMedals(['chat']) - causes infinite recursion when called from logMedalEarned
|
| 12 |
// Medal checks should be explicit in activityLogger functions, not on every chat push
|
| 13 |
},
|
| 14 |
-
appendToken: async (t)=>
|
| 15 |
-
const chat =
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
if (!last || last.role !== 'assistant') {
|
| 26 |
chat.push({ role: 'assistant', content: token, agent, ts: new Date().toISOString() })
|
| 27 |
} else {
|
| 28 |
-
// Append token to existing assistant bubble. If agent metadata wasn't set,
|
| 29 |
-
// set it to the first agent that contributed to this bubble.
|
| 30 |
last.content += token
|
| 31 |
if (!last.agent) last.agent = agent
|
| 32 |
}
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
}
|
| 36 |
}))
|
| 37 |
|
| 38 |
// Načtení chat historie z IndexedDB při inicializaci
|
|
|
|
| 7 |
chat: [],
|
| 8 |
push: async (m)=> {
|
| 9 |
const withTs = { ...m, ts: m.ts || new Date().toISOString() }
|
| 10 |
+
const newChat = get().chat.concat([withTs])
|
| 11 |
+
// Update state immediately for responsive UI
|
| 12 |
+
set({ chat: newChat })
|
| 13 |
+
try {
|
| 14 |
+
await idbSet('chat', newChat)
|
| 15 |
+
} catch (err) {
|
| 16 |
+
console.error('Error persisting chat push:', err)
|
| 17 |
+
}
|
| 18 |
// REMOVED: checkAndLogMedals(['chat']) - causes infinite recursion when called from logMedalEarned
|
| 19 |
// Medal checks should be explicit in activityLogger functions, not on every chat push
|
| 20 |
},
|
| 21 |
+
appendToken: async (t)=> {
|
| 22 |
+
const chat = get().chat.slice()
|
| 23 |
+
const last = chat[chat.length-1]
|
| 24 |
+
if(!last || last.role!=='assistant'){
|
| 25 |
+
chat.push({role:'assistant', content:t, ts: new Date().toISOString()})
|
| 26 |
+
} else {
|
| 27 |
+
last.content += t
|
| 28 |
+
}
|
| 29 |
+
set({ chat })
|
| 30 |
+
try { await idbSet('chat', chat) } catch (err) { console.error('Error persisting chat appendToken:', err) }
|
| 31 |
+
},
|
| 32 |
+
appendAgentToken: async (agent, token) => {
|
| 33 |
+
const chat = get().chat.slice()
|
| 34 |
+
const last = chat[chat.length-1]
|
| 35 |
if (!last || last.role !== 'assistant') {
|
| 36 |
chat.push({ role: 'assistant', content: token, agent, ts: new Date().toISOString() })
|
| 37 |
} else {
|
|
|
|
|
|
|
| 38 |
last.content += token
|
| 39 |
if (!last.agent) last.agent = agent
|
| 40 |
}
|
| 41 |
+
set({ chat })
|
| 42 |
+
try { await idbSet('chat', chat) } catch (err) { console.error('Error persisting chat appendAgentToken:', err) }
|
| 43 |
+
}
|
| 44 |
}))
|
| 45 |
|
| 46 |
// Načtení chat historie z IndexedDB při inicializaci
|
frontend/src/state/modules/completedActions.ts
CHANGED
|
@@ -52,6 +52,7 @@ export const useCompletedActions = create<CompletedActionsStore>((set, get) => (
|
|
| 52 |
// Load completed actions from IndexedDB on init
|
| 53 |
export async function loadCompletedActions() {
|
| 54 |
const saved = await idbGet('completedActions')
|
|
|
|
| 55 |
if (saved && Array.isArray(saved)) {
|
| 56 |
useCompletedActions.setState({ actions: saved })
|
| 57 |
}
|
|
|
|
| 52 |
// Load completed actions from IndexedDB on init
|
| 53 |
export async function loadCompletedActions() {
|
| 54 |
const saved = await idbGet('completedActions')
|
| 55 |
+
console.log('loadCompletedActions: read from IndexedDB ->', saved)
|
| 56 |
if (saved && Array.isArray(saved)) {
|
| 57 |
useCompletedActions.setState({ actions: saved })
|
| 58 |
}
|
frontend/src/state/modules/journal.ts
CHANGED
|
@@ -8,11 +8,13 @@ export const useJournal = create<{ journal: JournalEntry[]; addJournal: (text:st
|
|
| 8 |
journal: [],
|
| 9 |
addJournal: async (text, meta)=> {
|
| 10 |
const entry = { id: String(Date.now()), ts: new Date().toISOString(), text, meta };
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
})
|
|
|
|
|
|
|
| 16 |
// Check for journal-related medals (like "3 deníkové zápisy")
|
| 17 |
await checkAndLogMedals(['journal'])
|
| 18 |
// Recompute user stage after journaling (affects phase detection)
|
|
|
|
| 8 |
journal: [],
|
| 9 |
addJournal: async (text, meta)=> {
|
| 10 |
const entry = { id: String(Date.now()), ts: new Date().toISOString(), text, meta };
|
| 11 |
+
const newJournal = get().journal.concat([entry])
|
| 12 |
+
set({ journal: newJournal })
|
| 13 |
+
try {
|
| 14 |
+
await idbSet('journal', newJournal)
|
| 15 |
+
} catch (err) {
|
| 16 |
+
console.error('Error persisting journal entry:', err)
|
| 17 |
+
}
|
| 18 |
// Check for journal-related medals (like "3 deníkové zápisy")
|
| 19 |
await checkAndLogMedals(['journal'])
|
| 20 |
// Recompute user stage after journaling (affects phase detection)
|
frontend/src/state/modules/medal.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const useMedal = create<MedalStore>((set, get) => ({
|
|
| 43 |
// Load medals from IndexedDB on init
|
| 44 |
export async function loadMedals() {
|
| 45 |
const saved = await idbGet('medals')
|
|
|
|
| 46 |
if (saved && Array.isArray(saved)) {
|
| 47 |
useMedal.setState({ medals: saved })
|
| 48 |
}
|
|
|
|
| 43 |
// Load medals from IndexedDB on init
|
| 44 |
export async function loadMedals() {
|
| 45 |
const saved = await idbGet('medals')
|
| 46 |
+
console.log('loadMedals: read from IndexedDB ->', saved)
|
| 47 |
if (saved && Array.isArray(saved)) {
|
| 48 |
useMedal.setState({ medals: saved })
|
| 49 |
}
|
frontend/src/state/modules/noContact.ts
CHANGED
|
@@ -9,7 +9,14 @@ export const useNoContact = create<{ noContact: NoContact; startNoContact: ()=>v
|
|
| 9 |
noContact: { startISO: null, lastPraiseISO: null, reasons: [] },
|
| 10 |
startNoContact: async ()=> {
|
| 11 |
const now = new Date().toISOString()
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
// Log milestone - always show start message
|
| 14 |
const { logNoContactMilestone } = await import('../../lib/activityLogger')
|
| 15 |
await logNoContactMilestone(0, true) // isStart = true, days = 0 for start message
|
|
@@ -18,23 +25,40 @@ export const useNoContact = create<{ noContact: NoContact; startNoContact: ()=>v
|
|
| 18 |
resetNoContact: async ()=> {
|
| 19 |
const newState = { startISO:null, lastPraiseISO:null, reasons: [] };
|
| 20 |
set({ noContact: newState });
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
// Log reset milestone
|
| 23 |
const { logNoContactMilestone } = await import('../../lib/activityLogger')
|
| 24 |
await logNoContactMilestone(0, false) // isStart = false for reset message
|
| 25 |
try { await recomputeStage() } catch (e) { /* ignore */ }
|
| 26 |
},
|
| 27 |
updateLastPraise: async (iso: string)=> {
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
},
|
| 30 |
updateReasons: async (reasons: string[])=> {
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
}))
|
| 34 |
|
| 35 |
// Načtení noContact stavu z IndexedDB při inicializaci
|
| 36 |
export async function loadNoContact() {
|
| 37 |
const saved = await idbGet('noContact')
|
|
|
|
| 38 |
if (saved && typeof saved === 'object' && 'startISO' in saved) {
|
| 39 |
useNoContact.setState({ noContact: {
|
| 40 |
startISO: saved.startISO || null,
|
|
|
|
| 9 |
noContact: { startISO: null, lastPraiseISO: null, reasons: [] },
|
| 10 |
startNoContact: async ()=> {
|
| 11 |
const now = new Date().toISOString()
|
| 12 |
+
const newState = { startISO: now, lastPraiseISO: now, reasons: get().noContact.reasons }
|
| 13 |
+
// Update state first, then persist and await to ensure the write completes
|
| 14 |
+
set({ noContact: newState })
|
| 15 |
+
try {
|
| 16 |
+
await idbSet('noContact', newState)
|
| 17 |
+
} catch (err) {
|
| 18 |
+
console.error('Error persisting noContact start:', err)
|
| 19 |
+
}
|
| 20 |
// Log milestone - always show start message
|
| 21 |
const { logNoContactMilestone } = await import('../../lib/activityLogger')
|
| 22 |
await logNoContactMilestone(0, true) // isStart = true, days = 0 for start message
|
|
|
|
| 25 |
resetNoContact: async ()=> {
|
| 26 |
const newState = { startISO:null, lastPraiseISO:null, reasons: [] };
|
| 27 |
set({ noContact: newState });
|
| 28 |
+
try {
|
| 29 |
+
await idbSet('noContact', newState);
|
| 30 |
+
} catch (err) {
|
| 31 |
+
console.error('Error persisting noContact reset:', err)
|
| 32 |
+
}
|
| 33 |
// Log reset milestone
|
| 34 |
const { logNoContactMilestone } = await import('../../lib/activityLogger')
|
| 35 |
await logNoContactMilestone(0, false) // isStart = false for reset message
|
| 36 |
try { await recomputeStage() } catch (e) { /* ignore */ }
|
| 37 |
},
|
| 38 |
updateLastPraise: async (iso: string)=> {
|
| 39 |
+
const newState = { ...get().noContact, lastPraiseISO: iso }
|
| 40 |
+
set({ noContact: newState })
|
| 41 |
+
try {
|
| 42 |
+
await idbSet('noContact', newState)
|
| 43 |
+
} catch (err) {
|
| 44 |
+
console.error('Error persisting noContact lastPraise update:', err)
|
| 45 |
+
}
|
| 46 |
},
|
| 47 |
updateReasons: async (reasons: string[])=> {
|
| 48 |
+
const newState = { ...get().noContact, reasons }
|
| 49 |
+
set({ noContact: newState })
|
| 50 |
+
try {
|
| 51 |
+
await idbSet('noContact', newState)
|
| 52 |
+
} catch (err) {
|
| 53 |
+
console.error('Error persisting noContact reasons update:', err)
|
| 54 |
+
}
|
| 55 |
}
|
| 56 |
}))
|
| 57 |
|
| 58 |
// Načtení noContact stavu z IndexedDB při inicializaci
|
| 59 |
export async function loadNoContact() {
|
| 60 |
const saved = await idbGet('noContact')
|
| 61 |
+
console.log('loadNoContact: read from IndexedDB ->', saved)
|
| 62 |
if (saved && typeof saved === 'object' && 'startISO' in saved) {
|
| 63 |
useNoContact.setState({ noContact: {
|
| 64 |
startISO: saved.startISO || null,
|
frontend/src/views/ProfileView.tsx
CHANGED
|
@@ -293,12 +293,14 @@ export default function ProfileView({ accent }: Props) {
|
|
| 293 |
|
| 294 |
console.log('Step 2: Deleting IndexedDB data...')
|
| 295 |
// Smazat všechna data z IndexedDB
|
|
|
|
| 296 |
await Promise.all([
|
| 297 |
idbDel('profile'),
|
| 298 |
idbDel('chat'),
|
| 299 |
idbDel('journal'),
|
| 300 |
idbDel('noContact'),
|
| 301 |
-
idbDel('
|
|
|
|
| 302 |
idbDel('last_active_tab')
|
| 303 |
// onboarding_completed is no longer used
|
| 304 |
])
|
|
|
|
| 293 |
|
| 294 |
console.log('Step 2: Deleting IndexedDB data...')
|
| 295 |
// Smazat všechna data z IndexedDB
|
| 296 |
+
// Note: keys must match those used by the stores (medals, completedActions, noContact, profile, chat, journal, last_active_tab)
|
| 297 |
await Promise.all([
|
| 298 |
idbDel('profile'),
|
| 299 |
idbDel('chat'),
|
| 300 |
idbDel('journal'),
|
| 301 |
idbDel('noContact'),
|
| 302 |
+
idbDel('medals'),
|
| 303 |
+
idbDel('completedActions'),
|
| 304 |
idbDel('last_active_tab')
|
| 305 |
// onboarding_completed is no longer used
|
| 306 |
])
|
frontend/tests/unit.test.ts
CHANGED
|
@@ -829,7 +829,7 @@ describe('Unit tests', () => {
|
|
| 829 |
const mockUseChatSetState = vi.fn()
|
| 830 |
const mockUseJournalSetState = vi.fn()
|
| 831 |
const mockUseNoContactSetState = vi.fn()
|
| 832 |
-
|
| 833 |
|
| 834 |
// Mock window.location.reload
|
| 835 |
const mockReload = vi.fn()
|
|
@@ -879,7 +879,8 @@ describe('Unit tests', () => {
|
|
| 879 |
mockIdbDel('chat'),
|
| 880 |
mockIdbDel('journal'),
|
| 881 |
mockIdbDel('noContact'),
|
| 882 |
-
mockIdbDel('
|
|
|
|
| 883 |
mockIdbDel('last_active_tab')
|
| 884 |
])
|
| 885 |
console.log('Data deleted from IndexedDB')
|
|
@@ -889,7 +890,7 @@ describe('Unit tests', () => {
|
|
| 889 |
mockUseChatSetState({ chat: [] })
|
| 890 |
mockUseJournalSetState({ journal: [] })
|
| 891 |
mockUseNoContactSetState({ noContact: { startISO: null, lastPraiseISO: null } })
|
| 892 |
-
|
| 893 |
console.log('Zustand reset, reloading page...')
|
| 894 |
|
| 895 |
// Reload stránky pro čistý start
|
|
@@ -899,12 +900,13 @@ describe('Unit tests', () => {
|
|
| 899 |
await handleForget()
|
| 900 |
|
| 901 |
// Verify all IndexedDB keys were deleted (except onboarding_completed which is no longer used)
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
|
|
|
| 908 |
// onboarding_completed is no longer used, so should not be called
|
| 909 |
expect(mockIdbDel).not.toHaveBeenCalledWith('onboarding_completed')
|
| 910 |
|
|
@@ -913,7 +915,7 @@ describe('Unit tests', () => {
|
|
| 913 |
expect(mockUseChatSetState).toHaveBeenCalledWith({ chat: [] })
|
| 914 |
expect(mockUseJournalSetState).toHaveBeenCalledWith({ journal: [] })
|
| 915 |
expect(mockUseNoContactSetState).toHaveBeenCalledWith({ noContact: { startISO: null, lastPraiseISO: null } })
|
| 916 |
-
|
| 917 |
|
| 918 |
// Verify page reload was called
|
| 919 |
expect(mockReload).toHaveBeenCalled()
|
|
|
|
| 829 |
const mockUseChatSetState = vi.fn()
|
| 830 |
const mockUseJournalSetState = vi.fn()
|
| 831 |
const mockUseNoContactSetState = vi.fn()
|
| 832 |
+
const mockUseMedalSetState: any = vi.fn()
|
| 833 |
|
| 834 |
// Mock window.location.reload
|
| 835 |
const mockReload = vi.fn()
|
|
|
|
| 879 |
mockIdbDel('chat'),
|
| 880 |
mockIdbDel('journal'),
|
| 881 |
mockIdbDel('noContact'),
|
| 882 |
+
mockIdbDel('medals'),
|
| 883 |
+
mockIdbDel('completedActions'),
|
| 884 |
mockIdbDel('last_active_tab')
|
| 885 |
])
|
| 886 |
console.log('Data deleted from IndexedDB')
|
|
|
|
| 890 |
mockUseChatSetState({ chat: [] })
|
| 891 |
mockUseJournalSetState({ journal: [] })
|
| 892 |
mockUseNoContactSetState({ noContact: { startISO: null, lastPraiseISO: null } })
|
| 893 |
+
mockUseMedalSetState({ medals: [] })
|
| 894 |
console.log('Zustand reset, reloading page...')
|
| 895 |
|
| 896 |
// Reload stránky pro čistý start
|
|
|
|
| 900 |
await handleForget()
|
| 901 |
|
| 902 |
// Verify all IndexedDB keys were deleted (except onboarding_completed which is no longer used)
|
| 903 |
+
expect(mockIdbDel).toHaveBeenCalledWith('profile')
|
| 904 |
+
expect(mockIdbDel).toHaveBeenCalledWith('chat')
|
| 905 |
+
expect(mockIdbDel).toHaveBeenCalledWith('journal')
|
| 906 |
+
expect(mockIdbDel).toHaveBeenCalledWith('noContact')
|
| 907 |
+
expect(mockIdbDel).toHaveBeenCalledWith('medals')
|
| 908 |
+
expect(mockIdbDel).toHaveBeenCalledWith('completedActions')
|
| 909 |
+
expect(mockIdbDel).toHaveBeenCalledWith('last_active_tab')
|
| 910 |
// onboarding_completed is no longer used, so should not be called
|
| 911 |
expect(mockIdbDel).not.toHaveBeenCalledWith('onboarding_completed')
|
| 912 |
|
|
|
|
| 915 |
expect(mockUseChatSetState).toHaveBeenCalledWith({ chat: [] })
|
| 916 |
expect(mockUseJournalSetState).toHaveBeenCalledWith({ journal: [] })
|
| 917 |
expect(mockUseNoContactSetState).toHaveBeenCalledWith({ noContact: { startISO: null, lastPraiseISO: null } })
|
| 918 |
+
expect(mockUseMedalSetState).toHaveBeenCalledWith({ medals: [] } as any)
|
| 919 |
|
| 920 |
// Verify page reload was called
|
| 921 |
expect(mockReload).toHaveBeenCalled()
|