fix: Onboarding-Tour bei gelöschtem Demo-Logbuch und GPS-Schritt stabilisieren
Bereinigt veraltete Demo-Referenzen, löst gültiges Logbuch und ersten Eintrag zur Laufzeit auf und scrollt den GPS-Track-Schritt automatisch ins Viewport. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
clearDemoLogbookRefs,
|
||||
getDemoFirstEntryStorageKey,
|
||||
getDemoLogbookStorageKey
|
||||
} from './demoLogbook.js'
|
||||
|
||||
describe('clearDemoLogbookRefs', () => {
|
||||
const userId = 'user-1'
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
localStorage.setItem('active_userid', userId)
|
||||
})
|
||||
|
||||
it('removes demo logbook and first-entry keys for the user', () => {
|
||||
const logbookId = 'lb-demo'
|
||||
localStorage.setItem(getDemoLogbookStorageKey(userId), logbookId)
|
||||
localStorage.setItem(getDemoFirstEntryStorageKey(userId), 'entry-1')
|
||||
|
||||
clearDemoLogbookRefs(userId, logbookId)
|
||||
|
||||
expect(localStorage.getItem(getDemoLogbookStorageKey(userId))).toBeNull()
|
||||
expect(localStorage.getItem(getDemoFirstEntryStorageKey(userId))).toBeNull()
|
||||
})
|
||||
|
||||
it('does not clear refs when logbookId does not match stored demo id', () => {
|
||||
localStorage.setItem(getDemoLogbookStorageKey(userId), 'other-logbook')
|
||||
localStorage.setItem(getDemoFirstEntryStorageKey(userId), 'entry-1')
|
||||
|
||||
clearDemoLogbookRefs(userId, 'deleted-logbook')
|
||||
|
||||
expect(localStorage.getItem(getDemoLogbookStorageKey(userId))).toBe('other-logbook')
|
||||
expect(localStorage.getItem(getDemoFirstEntryStorageKey(userId))).toBe('entry-1')
|
||||
})
|
||||
})
|
||||
@@ -108,6 +108,7 @@ export async function seedDemoLogbookIfNeeded(): Promise<DemoSeedResult | null>
|
||||
const title = i18n.t('demo.logbook_title')
|
||||
return { logbookId: existingId, title, firstEntryId }
|
||||
}
|
||||
clearDemoLogbookRefs(userId, existingId)
|
||||
}
|
||||
|
||||
if (!shouldSeed) return null
|
||||
@@ -152,3 +153,66 @@ export function getStoredDemoFirstEntryId(): string | null {
|
||||
if (!userId) return null
|
||||
return localStorage.getItem(getDemoFirstEntryStorageKey(userId))
|
||||
}
|
||||
|
||||
/** Remove persisted demo logbook pointers when the logbook no longer exists. */
|
||||
export function clearDemoLogbookRefs(userId: string, logbookId?: string): void {
|
||||
const storedId = localStorage.getItem(getDemoLogbookStorageKey(userId))
|
||||
if (logbookId && storedId && storedId !== logbookId) return
|
||||
localStorage.removeItem(getDemoLogbookStorageKey(userId))
|
||||
localStorage.removeItem(getDemoFirstEntryStorageKey(userId))
|
||||
}
|
||||
|
||||
export async function entryExistsInLogbook(logbookId: string, entryId: string): Promise<boolean> {
|
||||
const entry = await db.entries.get(entryId)
|
||||
return entry?.logbookId === logbookId
|
||||
}
|
||||
|
||||
export interface TourLogbookContext {
|
||||
logbookId: string
|
||||
title: string
|
||||
firstEntryId: string | null
|
||||
}
|
||||
|
||||
/** Pick a logbook + first entry for the onboarding tour (handles deleted demo data). */
|
||||
export async function resolveTourLogbookContext(
|
||||
preferLogbookId?: string | null
|
||||
): Promise<TourLogbookContext | null> {
|
||||
const userId = localStorage.getItem('active_userid')
|
||||
if (!userId || !getActiveMasterKey()) return null
|
||||
|
||||
const demoId = localStorage.getItem(getDemoLogbookStorageKey(userId))
|
||||
if (demoId && !(await db.logbooks.get(demoId))) {
|
||||
clearDemoLogbookRefs(userId, demoId)
|
||||
}
|
||||
|
||||
const { fetchLogbooks } = await import('./logbook.js')
|
||||
const books = await fetchLogbooks()
|
||||
if (books.length === 0) return null
|
||||
|
||||
const activeId = localStorage.getItem('active_logbook_id')
|
||||
const pick =
|
||||
(preferLogbookId ? books.find((b) => b.id === preferLogbookId) : undefined) ??
|
||||
(activeId ? books.find((b) => b.id === activeId) : undefined) ??
|
||||
(demoId ? books.find((b) => b.id === demoId) : undefined) ??
|
||||
books[0]
|
||||
|
||||
const firstEntryId = await resolveTourFirstEntryId(pick.id, userId)
|
||||
return { logbookId: pick.id, title: pick.title, firstEntryId }
|
||||
}
|
||||
|
||||
async function resolveTourFirstEntryId(logbookId: string, userId: string): Promise<string | null> {
|
||||
const stored = localStorage.getItem(getDemoFirstEntryStorageKey(userId))
|
||||
if (stored && (await entryExistsInLogbook(logbookId, stored))) {
|
||||
return stored
|
||||
}
|
||||
|
||||
if (stored) {
|
||||
localStorage.removeItem(getDemoFirstEntryStorageKey(userId))
|
||||
}
|
||||
|
||||
const localEntries = await db.entries.where({ logbookId }).toArray()
|
||||
if (localEntries.length === 0) return null
|
||||
|
||||
localEntries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
return localEntries[0]?.payloadId ?? null
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { encryptJson, decryptJson, encryptBuffer, decryptBuffer } from './crypto
|
||||
import { getLogbookKey, saveLogbookKey, generateLogbookKey } from './logbookKeys.js'
|
||||
import { PlausibleEvents, trackPlausibleEvent } from './analytics.js'
|
||||
import { apiFetch } from './api.js'
|
||||
import { clearDemoLogbookRefs, getDemoLogbookStorageKey } from './demoLogbook.js'
|
||||
|
||||
const API_BASE = '/api/logbooks'
|
||||
|
||||
@@ -320,6 +321,9 @@ export async function deleteLogbook(id: string): Promise<void> {
|
||||
|
||||
// Perform local cascading cleanup
|
||||
await deleteLocalLogbookCache(id)
|
||||
if (userId && id === localStorage.getItem(getDemoLogbookStorageKey(userId))) {
|
||||
clearDemoLogbookRefs(userId, id)
|
||||
}
|
||||
trackPlausibleEvent(PlausibleEvents.LOGBOOK_DELETED)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user