# Performance, Flickering, UX & Offline Issues - Comprehensive Analysis

**Date:** November 2, 2025  
**Scope:** Full app analysis (frontend + backend + offline/sync infrastructure)  
**Priority:** CRITICAL - Affects core user experience & data integrity

---

## Executive Summary

Your app has **3 critical problem zones:**

| Issue | Severity | Impact | Root Cause |
|-------|----------|--------|-----------|
| **Flickering on page load** | 🔴 CRITICAL | UX degradation | Race conditions in initialization + no loading skeleton + double renders |
| **Offline pages broken after restart** | 🔴 CRITICAL | Data access failure | IndexedDB corruption/schema mismatch after crash + missing recovery logic |
| **Sensitive data changeable offline** | 🟠 HIGH | Security risk | Profile/auth changes written to IndexedDB without sync validation |
| **Performance: 3x slower offline** | 🟠 HIGH | Poor UX | No pagination in IndexedDB queries + full table scans |
| **Redis/CRDT desync after crashes** | 🟠 HIGH | Data loss risk | No transaction rollback + missing checksum validation |

---

## PART 1: FLICKERING & UX ISSUES

### Root Cause #1: Uncontrolled Cascading Initialization

**Location:** `apps/web/src/app/main.ts` (lines 86-105)

```typescript
// Current problematic flow:
await authStore.initialize()          // 1. Auth loads (500-800ms)
if (authStore.isAuthenticated) {
  await plannerSession.activate()      // 2. Planner loads (200-500ms) - BLOCKS UI
}
app.mount('#app')                      // 3. Mount happens only AFTER all this
```

**Problems:**
- ❌ All async init happens BEFORE mount → blank white screen (500ms+)
- ❌ No fallback if any init fails
- ❌ Vue components can't render fallback UI during load
- ❌ Multiple re-renders as stores populate

**Impact:** User sees 1-2 second blank screen, then flash of loading spinner, then content appears

---

### Root Cause #2: Profile Page Triple-Renders

**Location:** `apps/web/src/modules/profile/pages/ProfilePageBase.vue` (lines 1-150)

```typescript
// Current flow in ProfilePageBase:
setup() {
  const profileStore = useProfileStore()
  const profile = computed(() => profileStore.profile)
  const loading = computed(() => profileStore.loading)
  
  onMounted(async () => {
    await profileStore.fetchProfile()  // BLOCKS until complete
  })
  // NO RETURN - renders 3 times:
  // 1. Initial: profile = null, loading = true → Shows spinner
  // 2. After fetch: profile updated → Full re-render
  // 3. Computed deps update → Re-render again
}
```

**Problems:**
- ❌ No skeleton loaders = feels slow
- ❌ `watch()` triggers missing to debounce updates
- ❌ 3 renders per route change
- ❌ No error boundary = crashes silently if fetch fails offline

---

### Root Cause #3: Missing Suspense Boundaries

**Location:** `apps/web/src/app/App.vue` (all route transitions)

```typescript
// Current: Router loads component THEN component fetches data
<RouterView />  // Component mounts, THEN fetches in onMounted

// Should be:
<Suspense>
  <RouterView />
  <template #fallback>
    <PageSkeleton />
  </template>
</Suspense>
```

**Missing:**
- ❌ No Suspense wrapper → components render before data ready
- ❌ No error fallback UI → errors cause white screen
- ❌ No progress indicator during data load

---

## PART 2: OFFLINE BROKEN PAGES (CRITICAL)

### Root Cause #1: IndexedDB Schema Desync After App Crash

**Location:** `apps/web/src/modules/offline/services/indexedDBService.ts` (line 75)

```typescript
export class PlannerDB extends Dexie {
  constructor() {
    super('PlannerDB')
    
    // ❌ PROBLEM: Only 1 version ever created
    // If crash happens mid-upgrade, DB is left in corrupted state
    this.version(1).stores({
      plans: 'id, userId, updatedAt',
      syncQueue: '++id, planId, action, timestamp, retries'
    })
  }
}
```

**What happens on restart after crash:**
1. App crashes mid-sync
2. IndexedDB transaction is interrupted
3. App restarts → tries to access IndexedDB
4. Schema version mismatch or corrupted metadata
5. **All pages fail** - ProfilePage can't load profile from cache

**Missing:**
- ❌ No version migrations (only v1)
- ❌ No schema validation/recovery
- ❌ No transaction rollback on crash
- ❌ No fallback if IndexedDB is unavailable

---

### Root Cause #2: No IndexedDB Recovery Logic

**Location:** `apps/web/src/modules/offline/stores/offlineStore.ts` (line 99+)

```typescript
async function initialize(): Promise<void> {
  if (initialized.value) return
  
  initialized.value = true
  
  void refreshQueueSnapshot()        // ❌ If IndexedDB fails, crash silently
  void refreshDatabaseStats()        // ❌ No error handling
  void refreshNetworkStatus()
  
  // No try-catch = unhandled promise rejection
}
```

**What happens:**
- IndexedDB errors propagate uncaught
- App state becomes inconsistent
- `offlineStore.initialized = true` but no data loaded
- Pages try to read from store → get null/undefined

---

### Root Cause #3: Firestore Cache Not Updated on Restart

**Location:** `apps/web/src/modules/planner/services/plannerService.ts` (lines 82-95)

```typescript
async getPlans(): Promise<Plan[]> {
  try {
    // Try network first
    const plans = await this.request<Plan[]>({ path: '/plans' })
    
    // Save to IndexedDB
    for (const plan of plans) {
      await indexedDBService.savePlan(plan)
    }
    
    return plans
  } catch (error) {
    // Fallback to IndexedDB if offline
    console.warn('[PlannerService] Using cached plans', error)
    const user = await this.getCurrentUser()  // ❌ PROBLEM: This fails if offline
    return indexedDBService.getUserPlans(user.uid)
  }
}
```

**Problem:** After restart when offline:
1. Network request fails (offline)
2. Falls back to IndexedDB
3. Tries to get current user (which requires auth)
4. Auth data might be stale/corrupt
5. `getUserPlans()` fails with no data

---

### Root Cause #4: Sync Queue Resume Bug After Crash

**Location:** `apps/web/src/modules/offline/services/syncQueueService.ts` (lines 40-60)

```typescript
startAutoProcess(baseIntervalMs: number = 10000): void {
  if (this.processInterval) {
    return  // ❌ PROBLEM: Called multiple times, early return
  }

  this.currentInterval = baseIntervalMs
  this.scheduleNextProcess()
  void this.processQueue()  // ❌ Sync starts but might fail
}
```

**After crash:**
- Queue was processing updates
- Crash happens mid-sync
- On restart, `startAutoProcess()` is called again
- Early return prevents restarting if already initialized
- Queue state is corrupt (mix of old updates + new ones)
- Retries use stale timestamps → backoff calculations wrong

---

## PART 3: SENSITIVE DATA SECURITY ISSUE

### Root Cause: Profile/Auth Changes Writable Offline Without Server Validation

**Location:** `apps/web/src/modules/profile/stores/profileStore.ts` (lines 88-120)

```typescript
async function updateProfile(updates: Record<string, unknown>): Promise<void> {
  try {
    profile.value = { ...profile.value, ...updates }  // ❌ Update local immediately
    
    const response = await profileService.updateProfile(updates)
    profile.value = response
  } catch (error) {
    // ❌ PROBLEM: If offline, update already persisted locally
    // User goes online later → change syncs to server (insecure!)
    profile.value = originalProfile  // No original saved
  }
}
```

**Security risk:**
- User changes email/password offline
- App saves to IndexedDB
- App goes online
- Sync happens → change reaches server (unvalidated)
- No 2FA/re-auth required

**For plans/trips (with Redis):**
- ✅ Correct: Uses sync queue + exponential backoff
- ❌ But: No checksum validation on Redis merge

---

## PART 4: REDIS/CRDT DESYNC AFTER CRASHES

### Root Cause #1: No Transaction Atomicity

**Location:** `apps/api/src/modules/collab/collab.service.ts` (lines 42-100)

```typescript
async syncPlan(
  planId: string,
  clientUpdate: Uint8Array,
  serverState: Uint8Array
): Promise<Uint8Array> {
  // Load server Y.Doc from Redis
  const serverDoc = new Y.Doc()
  const cachedState = await this.redis.getBuffer(`ydoc:${planId}`)
  
  // ❌ PROBLEM: No transaction here
  // If crash happens between next lines, state is inconsistent:
  
  Y.applyUpdate(serverDoc, stateBuffer)  // Apply old state
  Y.applyUpdate(serverDoc, clientUpdate)  // Apply client changes
  // ❌ CRASH POINT 1: Crash here = client update is lost
  
  const update = Y.encodeStateAsUpdate(serverDoc)
  await this.redis.setBuffer(`ydoc:${planId}`, update)  // Write to Redis
  // ❌ CRASH POINT 2: Crash here = update lost + Firestore out of sync
  
  await db.collection('plans').doc(planId).set(...)  // Async write
  // ❌ CRASH POINT 3: Crash here = Firestore incomplete
}
```

**Missing:**
- ❌ No Redis transaction (`MULTI/EXEC`)
- ❌ No Firestore transaction
- ❌ No checksum to validate state

---

### Root Cause #2: Client-Side No Recovery Logic

**Location:** `apps/web/src/modules/offline/services/syncQueueService.ts` (lines 150-220)

```typescript
async processQueue(): Promise<{successful, failed, skipped}> {
  const actions = await indexedDBService.getPendingActions()
  
  for (const action of actions) {
    try {
      // ❌ No checksum sent with action
      // ❌ No conflict detection on response
      // ❌ No rollback if server rejects
      
      const result = await this.apiCall(action.action, action.payload)
      
      // ❌ Assume success = update DB
      await indexedDBService.markActionProcessed(action.id!)
    } catch (error) {
      // ❌ Simple retry: increment counter + save error
      // ❌ No server-state comparison
      // ❌ No request deduplication (send same update 5x?)
    }
  }
}
```

**Missing:**
- ❌ Request idempotency (same update can process 5 times)
- ❌ Checksum validation
- ❌ Conflict resolution strategy
- ❌ Server state sync on failure

---

## PART 5: PERFORMANCE ISSUES

### Issue #1: O(n) IndexedDB Queries (No Pagination)

**Location:** `apps/web/src/modules/offline/services/indexedDBService.ts` (lines 250+)

```typescript
// ❌ Current: Load ALL plans every time
async getAllPlans(): Promise<Plan[]> {
  return this.db.plans.toArray()  // Full table scan!
}

// ❌ Used in ProfilePageBase to show all user's past trips
// With 1000 plans = 1000 objects in memory
```

**Fix needed:**
```typescript
// ✅ Use cursor pagination
async getUserPlans(userId: string, limit: number = 50, cursor?: string): Promise<Plan[]> {
  const query = this.db.plans
    .where('userId').equals(userId)
    .limit(limit)
  
  if (cursor) {
    query.above(cursor)
  }
  
  return query.toArray()
}
```

---

### Issue #2: Multiple Stores Loading Same Data

**Location:** `apps/web/src/app/main.ts` (lines 57-59)

```typescript
// ❌ Both run in parallel, duplicate work:
serviceOrchestrator.init()           // Loads all stores
initializeGamificationHandlers()     // Loads gamification store again
```

**Should merge into single initialization sequence**

---

### Issue #3: No Cache Invalidation TTL

**Location:** `apps/api/src/modules/collab/collab.service.ts` (line 45)

```typescript
// ❌ Redis cached state never expires
await this.redis.setBuffer(`ydoc:${planId}`, update)  // No TTL!

// If plan is never edited again:
// - Old CRDT state stays in Redis forever
// - Memory leak at scale
```

**Fix:**
```typescript
// ✅ Add 7-day TTL
await this.redis.setBuffer(
  `ydoc:${planId}`,
  update,
  'EX',
  86400 * 7  // 7 days
)
```

---

## RECOMMENDED FIXES (Priority Order)

### 🔴 CRITICAL (Fix This Week)

#### 1. Fix IndexedDB Corruption on Restart
**File:** `apps/web/src/modules/offline/services/indexedDBService.ts`

```typescript
// Add migration support + recovery
export class PlannerDB extends Dexie {
  constructor() {
    super('PlannerDB')
    
    // v1: Initial schema
    this.version(1).stores({
      plans: 'id, userId, updatedAt',
      syncQueue: '++id, planId, action, timestamp, retries'
    })
    
    // v2: Add recovery table (new migrations can go here)
    this.version(2).stores({
      plans: 'id, userId, updatedAt',
      syncQueue: '++id, planId, action, timestamp, retries',
      recovery: '++id, timestamp'  // For tracking failed syncs
    })
  }
}

// Add recovery initialization
async function initializeRecovery(): Promise<void> {
  try {
    const openDB = new PlannerDB()
    await openDB.open()  // Validates schema
    
    // Check for orphaned transactions
    const stuckActions = await openDB.syncQueue
      .where('retries').equals(0)
      .where('timestamp').below(Date.now() - 5000)
      .toArray()
    
    // Log for debugging
    if (stuckActions.length) {
      console.warn('[Recovery] Found stuck actions after crash', stuckActions)
    }
  } catch (error) {
    console.error('[Recovery] IndexedDB corruption detected', error)
    // Clear corrupted DB and start fresh
    await indexedDB.deleteDatabase('PlannerDB')
  }
}
```

---

#### 2. Fix Offline Store Initialization

**File:** `apps/web/src/modules/offline/stores/offlineStore.ts`

```typescript
async function initialize(): Promise<void> {
  if (initialized.value) return
  
  initialized.value = true
  
  try {
    // Add try-catch around each operation
    try {
      await refreshQueueSnapshot()
    } catch (error) {
      console.warn('[OfflineStore] Failed to refresh queue', error)
      queuedOperations.value = []
    }
    
    try {
      await refreshDatabaseStats()
    } catch (error) {
      console.warn('[OfflineStore] Failed to get DB stats', error)
      databaseStats.value = { planCount: 0, queueCount: 0, failedActions: 0 }
    }
    
    try {
      await refreshNetworkStatus()
    } catch (error) {
      console.warn('[OfflineStore] Failed to get network status', error)
      networkStatus.value = networkQualityService.getQuality()
    }
    
    // Rest of initialization...
  } catch (error) {
    console.error('[OfflineStore] Fatal initialization error', error)
    // App still works, just with empty cache
  }
}
```

---

#### 3. Fix Flickering on App Load

**File:** `apps/web/src/app/main.ts`

```typescript
// Change: Move mount BEFORE heavy async work
const app = createApp(App)
const pinia = createPinia()

// Setup all plugins
app.use(pinia)
app.use(router)
// ... other plugins ...

// Mount IMMEDIATELY with fallback UI
app.mount('#app')

// THEN initialize async stores in background
(async () => {
  try {
    const authStore = useAuthStore()
    attachAuthInterceptor(() => authStore.getAccessToken())
    
    // This now happens AFTER UI is interactive
    const redirectTarget = await authStore.initialize()
    
    if (authStore.isAuthenticated) {
      try {
        await plannerSession.activate()
      } catch (error) {
        console.error('[App] Failed to initialize planner session', error)
      }
    }
    
    if (redirectTarget && redirectTarget !== router.currentRoute.value.fullPath) {
      await router.replace(redirectTarget)
    }
  } catch (error) {
    console.error('[App] Initialization failed', error)
    Sentry.captureException(error)
  }
})()
```

---

#### 4. Add Error Boundaries to Profile Page

**File:** `apps/web/src/modules/profile/pages/ProfilePage.web.vue`

```vue
<template>
  <ErrorBoundary :fallback="profileErrorFallback">
    <Suspense>
      <ProfilePageBase variant="web" />
      <template #fallback>
        <div class="q-pa-xl flex flex-center">
          <q-spinner size="50px" color="primary" />
          <p>Chargement du profil...</p>
        </div>
      </template>
    </Suspense>
  </ErrorBoundary>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import ErrorBoundary from '@/app/components/ErrorBoundary.vue'
import Suspense from 'vue'

const profileErrorFallback = ref({
  component: 'div',
  props: { class: 'q-pa-xl text-center' },
  children: 'Impossible de charger le profil. Veuillez rafraîchir la page.'
})
</script>
```

---

#### 5. Fix Profile Update Security

**File:** `apps/web/src/modules/profile/stores/profileStore.ts`

```typescript
async function updateProfile(updates: Record<string, unknown>): Promise<void> {
  // ✅ Save original for rollback
  const originalProfile = { ...profile.value }
  
  try {
    // ✅ DON'T update UI until server confirms (especially for sensitive fields)
    const sensitiveFields = ['email', 'password', 'phone', 'twoFactorEnabled']
    const hasSensitive = Object.keys(updates).some(k => sensitiveFields.includes(k))
    
    if (!hasSensitive) {
      // Safe fields: optimistic update OK
      profile.value = { ...profile.value, ...updates }
    }
    
    // Always wait for server confirmation
    const response = await profileService.updateProfile(updates)
    profile.value = response
    
    // ❌ Don't write to IndexedDB for sensitive fields
    if (!hasSensitive) {
      await indexedDBService.savePlan(profile.value)
    }
  } catch (error) {
    // ✅ Rollback on error
    profile.value = originalProfile
    throw error
  }
}
```

---

### 🟠 HIGH PRIORITY (Fix Next 2 Weeks)

#### 6. Add Redis Transaction Atomicity

**File:** `apps/api/src/modules/collab/collab.service.ts`

```typescript
async syncPlan(planId, clientUpdate, deviceId): Promise<Uint8Array> {
  // Use Redis transaction + Firestore batch
  const transaction = await this.redis.pipeline()
  
  try {
    // ✅ Get with transaction lock
    const cachedState = await transaction.getBuffer(`ydoc:${planId}`)
    
    // Apply updates
    const serverDoc = new Y.Doc()
    if (cachedState) Y.applyUpdate(serverDoc, cachedState)
    Y.applyUpdate(serverDoc, clientUpdate)
    
    // Compute new state
    const newState = Y.encodeStateAsUpdate(serverDoc)
    const checksum = crypto.createHash('sha256').update(newState).digest('hex')
    
    // ✅ Atomic write: state + checksum + version
    transaction.setBuffer(`ydoc:${planId}`, newState, 'EX', 86400 * 7)
    transaction.set(`ydoc:${planId}:checksum`, checksum, 'EX', 86400 * 7)
    transaction.set(`ydoc:${planId}:version`, Date.now())
    
    await transaction.exec()  // ✅ All or nothing
    
    // Async Firestore write (separate)
    await db.runTransaction(async (tx) => {
      const planDoc = await tx.get(db.doc(`plans/${planId}`))
      tx.set(
        db.doc(`plans/${planId}/snapshots/${Date.now()}`),
        { state: newState, checksum, timestamp: Date.now() }
      )
    })
    
    return newState
  } catch (error) {
    // Transaction already rolled back by Redis/Firestore
    throw error
  }
}
```

---

#### 7. Add Pagination to IndexedDB Queries

**File:** `apps/web/src/modules/offline/services/indexedDBService.ts`

```typescript
async getUserPlans(
  userId: string,
  limit: number = 50,
  offset: string = '0'
): Promise<{ items: Plan[]; nextOffset?: string }> {
  const offsetNum = parseInt(offset)
  
  const plans = await this.db.plans
    .where('userId').equals(userId)
    .offset(offsetNum)
    .limit(limit + 1)
    .toArray()
  
  const hasMore = plans.length > limit
  const items = plans.slice(0, limit)
  
  return {
    items,
    nextOffset: hasMore ? String(offsetNum + limit) : undefined
  }
}
```

---

#### 8. Fix Sync Queue Resume After Crash

**File:** `apps/web/src/modules/offline/services/syncQueueService.ts`

```typescript
constructor(apiCall) {
  this.apiCall = apiCall
  
  // ✅ Detect if recovery needed
  this.checkRecovery()
}

private async checkRecovery(): Promise<void> {
  try {
    const pendingActions = await indexedDBService.getPendingActions()
    const now = Date.now()
    
    // Find actions that were being processed when app crashed
    // (timestamp older than 30s + retries = 0 = not started yet)
    const stuckActions = pendingActions.filter(
      a => a.retries === 0 && (now - a.timestamp) > 30000
    )
    
    if (stuckActions.length) {
      console.warn('[SyncQueue] Recovery: Found stuck actions', stuckActions)
      // Reset timestamps so they retry correctly
      for (const action of stuckActions) {
        action.timestamp = now
        await indexedDBService.updateAction(action)
      }
    }
  } catch (error) {
    console.error('[SyncQueue] Recovery failed', error)
  }
}
```

---

#### 9. Add Checksum Validation to Sync

**File:** `apps/web/src/modules/offline/services/syncQueueService.ts`

```typescript
async processQueue(): Promise<Result> {
  const actions = await indexedDBService.getPendingActions()
  
  for (const action of actions) {
    try {
      // ✅ Add checksum to payload
      const payload = {
        ...action.payload,
        _checksum: this.computeChecksum(action.payload),
        _clientId: this.clientId,
        _requestId: action.id  // For idempotency
      }
      
      const result = await this.apiCall(action.action, payload)
      
      // ✅ Verify response checksum
      if (result._checksum && result._checksum !== this.computeChecksum(result)) {
        throw new Error('Checksum mismatch: Data corrupted in transit')
      }
      
      await indexedDBService.markActionProcessed(action.id!)
    } catch (error) {
      // Handle with retries...
    }
  }
}

private computeChecksum(data: unknown): string {
  const json = JSON.stringify(data)
  // In browser: use Web Crypto API
  return btoa(json).slice(0, 16)  // Simple hash for demo
}
```

---

### 🟡 MEDIUM PRIORITY (Next Sprint)

#### 10. Implement Request Deduplication
**File:** Add `apps/web/src/modules/offline/services/requestDeduplicationService.ts`

```typescript
// Ensure same request isn't sent twice
export class RequestDeduplicationService {
  private pendingRequests = new Map<string, Promise<unknown>>()
  
  async execute<T>(
    id: string,
    fn: () => Promise<T>
  ): Promise<T> {
    if (this.pendingRequests.has(id)) {
      return this.pendingRequests.get(id) as Promise<T>
    }
    
    const promise = fn().finally(() => {
      this.pendingRequests.delete(id)
    })
    
    this.pendingRequests.set(id, promise)
    return promise
  }
}
```

---

## Testing Checklist

### Before Deploy
- [ ] **Offline recovery:** Force crash, restart app offline → All pages load from cache
- [ ] **Profile update:** Change profile offline → Verify NOT synced until online
- [ ] **Sync resume:** Queue 5 ops, crash mid-sync → Restart → All sync correctly
- [ ] **No flickering:** Page load should show skeleton, not blank → content
- [ ] **Redis atomic:** Kill Redis mid-sync → CRDT state remains consistent
- [ ] **Pagination:** Load 1000+ plans → First 50 load fast, pagination works
- [ ] **Network recovery:** Simulate 3G, restart app → Adaptive backoff works

---

## Files to Fix (Summary Table)

| File | Issue | Severity | Est. Time |
|------|-------|----------|-----------|
| `apps/web/src/app/main.ts` | Blocking initialization | 🔴 | 30min |
| `apps/web/src/modules/offline/services/indexedDBService.ts` | No recovery + migration | 🔴 | 1h |
| `apps/web/src/modules/offline/stores/offlineStore.ts` | No error handling | 🔴 | 30min |
| `apps/web/src/modules/profile/pages/ProfilePageBase.vue` | No Suspense + triple render | 🔴 | 45min |
| `apps/web/src/modules/profile/stores/profileStore.ts` | Security: offline write sensitive data | 🔴 | 30min |
| `apps/api/src/modules/collab/collab.service.ts` | No transaction atomicity | 🟠 | 1h |
| `apps/web/src/modules/offline/services/syncQueueService.ts` | No checksum + crash recovery | 🟠 | 1.5h |

---

## Architecture Changes Needed

### Current Flow (Broken):
```
App starts
  ↓
main.ts auth init (BLOCKS)
  ↓
plannerSession init (BLOCKS)
  ↓
mount app
  ↓
pages render & fetch data (3rd fetch!)
  ↓
TOTAL: 2-3 seconds blank screen + flicker
```

### New Flow (Fixed):
```
App starts
  ↓
mount app (IMMEDIATELY)
  ↓
Show loading skeleton
  ↓
[Background] auth init
  ↓
[Background] plannerSession init
  ↓
Store updates
  ↓
Skeleton replaced with content
  ↓
TOTAL: <500ms to interactive + smooth transition
```

---

## Offline Strategy (Clarification)

Per your note "Sinon on utilise firestore":

| Data Type | Offline Behavior | Sync Method | Validation |
|-----------|------------------|-------------|-----------|
| **Planner** (plans/stages) | ✅ Can edit offline | Redis + Firestore | CRDT merge + checksum |
| **Profile** (user info) | ❌ Read-only offline | Firestore | Auth required on change |
| **Gamification** (points/badges) | ❌ Read-only offline | Firestore | Server-computed only |
| **Maps** (tile cache) | ✅ Cached, read-only | Service Worker | Asset cache |

**Your current bug:** Profile allows write offline (WRONG) → syncs when online (UNSAFE)

---

## Next Steps

1. **Today:** Implement fixes #1-5 (critical)
2. **This week:** Fixes #6-9 (high priority)
3. **Next sprint:** #10 (deduplication)
4. **Test:** Run offline recovery test suite before release

Would you like me to start implementing the critical fixes? I recommend starting with:
1. Fix app initialization (most impactful)
2. Fix IndexedDB recovery (prevents crashes)
3. Fix profile security (prevents data leak)
