diff --git a/doc/active-identity-upgrade-plan.md b/doc/active-identity-upgrade-plan.md deleted file mode 100644 index 2adff9af..00000000 --- a/doc/active-identity-upgrade-plan.md +++ /dev/null @@ -1,390 +0,0 @@ -# Active Identity Upgrade Plan - -**Author**: Matthew Raymer -**Date**: 2025-09-11 -**Status**: 🎯 **PLANNING** - Database migration and active identity system upgrade - -## Overview - -Comprehensive upgrade to the active identity system, addressing architectural issues and implementing enhanced database constraints. Includes database migration enhancements and settings table cleanup based on team feedback. - -## Implementation Status - -**✅ COMPLETED**: Migration structure updated according to team member feedback - -### Implemented Changes - -1. **✅ Migration 003**: `003_add_hasBackedUpSeed_to_settings` - Adds `hasBackedUpSeed` column to settings (assumes master deployment) -2. **✅ Migration 004**: `004_active_identity_and_seed_backup` - Creates `active_identity` table with data migration -3. **✅ Migration Service**: Updated validation and schema detection logic for new migration structure -4. **✅ TypeScript**: Fixed type compatibility issues - -### Migration Structure Now Follows Team Guidance - -- **Migration 003**: `003_add_hasBackedUpSeed_to_settings` (assumes master code deployed) -- **Migration 004**: `004_active_identity_and_seed_backup` (creates active_identity table) -- **All migrations are additional** - no editing of previous migrations -- **Data migration logic** preserves existing `activeDid` from settings -- **iOS/Android compatibility** confirmed with SQLCipher 4.9.0 (SQLite 3.44.2) - -## Educational Context - -### Why This Upgrade Matters - -The active identity system is **critical infrastructure** affecting every user interaction: - -1. **Data Integrity**: Current `ON DELETE SET NULL` allows accidental deletion of active accounts -2. **Manual Maintenance**: Timestamps require manual updates, creating inconsistency opportunities -3. **Architectural Clarity**: Separating active identity from user settings improves maintainability - -### What This Upgrade Achieves - -- **Prevents Data Loss**: `ON DELETE RESTRICT` prevents accidental account deletion -- **Automatic Consistency**: Database triggers ensure timestamps are always current -- **Cleaner Architecture**: Complete separation of identity management from user preferences -- **Better Performance**: Optimized indexes for faster account selection - -## Current State Analysis - -### Existing Migration Structure - -- **Migration 003**: `003_add_hasBackedUpSeed_to_settings` - Adds `hasBackedUpSeed` column to settings (already deployed in master) -- **Migration 004**: `004_active_identity_and_seed_backup` - Creates `active_identity` table with data migration -- **Foreign Key**: `ON DELETE SET NULL` constraint -- **Data Migration**: Copies existing `activeDid` from settings to `active_identity` table -- **Bootstrapping**: Auto-selects first account if `activeDid` is null/empty - -**Important**: All migrations are **additional** - no editing of previous migrations since master code has been deployed. - -### Current Schema (Migration 004) - IMPLEMENTED - -```sql --- Migration 004: active_identity_and_seed_backup --- Assumes master code deployed with migration 003 - -PRAGMA foreign_keys = ON; -CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did); - -CREATE TABLE IF NOT EXISTS active_identity ( - id INTEGER PRIMARY KEY CHECK (id = 1), - activeDid TEXT DEFAULT NULL, - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE SET NULL -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); - --- Seed singleton row -INSERT INTO active_identity (id, activeDid, lastUpdated) -SELECT 1, NULL, datetime('now') -WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1); - --- MIGRATE EXISTING DATA: Copy activeDid from settings -UPDATE active_identity -SET activeDid = (SELECT activeDid FROM settings WHERE id = 1), - lastUpdated = datetime('now') -WHERE id = 1 -AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != ''); -``` - -## Current Implementation Details - -### PlatformServiceMixin.ts Implementation - -The current `$getActiveIdentity()` method in `src/utils/PlatformServiceMixin.ts`: - -```typescript -async $getActiveIdentity(): Promise<{ activeDid: string }> { - try { - const result = await this.$dbQuery("SELECT activeDid FROM active_identity WHERE id = 1"); - - if (!result?.values?.length) { - logger.warn("[PlatformServiceMixin] Active identity table is empty - migration issue"); - return { activeDid: "" }; - } - - const activeDid = result.values[0][0] as string | null; - - // Handle null activeDid - auto-select first account - if (activeDid === null) { - const firstAccount = await this.$dbQuery("SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1"); - if (firstAccount?.values?.length) { - const firstAccountDid = firstAccount.values[0][0] as string; - await this.$dbExec("UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", [firstAccountDid]); - return { activeDid: firstAccountDid }; - } - logger.warn("[PlatformServiceMixin] No accounts available for auto-selection"); - return { activeDid: "" }; - } - - // Validate activeDid exists in accounts - const accountExists = await this.$dbQuery("SELECT did FROM accounts WHERE did = ?", [activeDid]); - if (accountExists?.values?.length) { - return { activeDid }; - } - - // Clear corrupted activeDid and return empty - logger.warn("[PlatformServiceMixin] Active identity not found in accounts, clearing"); - await this.$dbExec("UPDATE active_identity SET activeDid = NULL, lastUpdated = datetime('now') WHERE id = 1"); - return { activeDid: "" }; - } catch (error) { - logger.error("[PlatformServiceMixin] Error getting active identity:", error); - return { activeDid: "" }; - } -} -``` - -### Key Implementation Notes - -1. **Null Handling**: Auto-selects first account when `activeDid` is null -2. **Corruption Detection**: Clears invalid `activeDid` values -3. **Manual Timestamps**: Updates `lastUpdated` manually in code -4. **Error Handling**: Returns empty string on any error with appropriate logging - -## Proposed Changes Impact - -### 1. Foreign Key Constraint Change -**Current**: `ON DELETE SET NULL` → **Proposed**: `ON DELETE RESTRICT` -- **Data Safety**: Prevents accidental deletion of active account -- **New Migration**: Add migration 005 to update constraint - -### 2. Automatic Timestamp Updates -**Current**: Manual `lastUpdated` updates → **Proposed**: Database trigger -- **Code Simplification**: Remove manual timestamp updates from `PlatformServiceMixin` -- **Consistency**: Ensures `lastUpdated` is always current - -### 3. Enhanced Indexing -**Current**: Single unique index on `id` → **Proposed**: Additional index on `accounts(dateCreated, did)` -- **Performance Improvement**: Faster account selection queries -- **Minimal Risk**: Additive change only - -## Implementation Strategy - -### Add Migration 005 - -Since the `active_identity` table already exists and is working, we can add a new migration to enhance it: - -```sql -{ - name: "005_active_identity_enhancements", - sql: ` - PRAGMA foreign_keys = ON; - - -- Recreate table with ON DELETE RESTRICT constraint - CREATE TABLE active_identity_new ( - id INTEGER PRIMARY KEY CHECK (id = 1), - activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT, - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Copy existing data - INSERT INTO active_identity_new (id, activeDid, lastUpdated) - SELECT id, activeDid, lastUpdated FROM active_identity; - - -- Replace old table - DROP TABLE active_identity; - ALTER TABLE active_identity_new RENAME TO active_identity; - - -- Add performance indexes - CREATE INDEX IF NOT EXISTS idx_accounts_pick ON accounts(dateCreated, did); - - -- Create automatic timestamp trigger - CREATE TRIGGER IF NOT EXISTS trg_active_identity_touch - AFTER UPDATE ON active_identity - FOR EACH ROW - BEGIN - UPDATE active_identity - SET lastUpdated = datetime('now') - WHERE id = 1; - END; - ` -} -``` - -## Migration Service Updates Required - -### Enhanced Validation Logic - -**File**: `src/services/migrationService.ts` - -**Migration 004 validation**: -- **Table existence**: `SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'` -- **Column structure**: `SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1` -- **Schema detection**: Uses `isSchemaAlreadyPresent()` to check if migration was already applied - -**Migration 005 validation**: -- **Trigger existence**: `trg_active_identity_touch` -- **Performance index**: `idx_accounts_pick` -- **Foreign key constraint**: `ON DELETE RESTRICT` -- **Table recreation**: Verify table was successfully recreated - -### Enhanced Schema Detection - -**Migration 004 verification**: -- **Table structure**: Checks `active_identity` table exists and has correct columns -- **Data integrity**: Validates that the table can be queried successfully -- **Migration tracking**: Uses `isSchemaAlreadyPresent()` to avoid re-applying migrations - -**Migration 005 verification**: -- **Table structure**: Enhanced constraints with `ON DELETE RESTRICT` -- **Trigger presence**: Automatic timestamp updates -- **Index presence**: Performance optimization -- **Data integrity**: Existing data was preserved during table recreation - -## Risk Assessment - -### Low Risk Changes -- **Performance Index**: Additive only, no data changes -- **Trigger Creation**: Additive only, improves consistency -- **New Migration**: Clean implementation, no modification of existing migrations - -### Medium Risk Changes -- **Foreign Key Change**: `ON DELETE RESTRICT` is more restrictive -- **Table Recreation**: Requires careful data preservation -- **Validation Updates**: Need to test enhanced validation logic - -### Mitigation Strategies -1. **Comprehensive Testing**: Test migration on various database states -2. **Data Preservation**: Verify existing data is copied correctly -3. **Clean Implementation**: New migration with all enhancements -4. **Validation Coverage**: Enhanced validation ensures correctness -5. **Rollback Plan**: Can drop new table and restore original if needed - -## Implementation Timeline - -### Phase 1: Migration Enhancement -- [ ] Add migration 005 with enhanced constraints -- [ ] Add enhanced validation logic -- [ ] Add enhanced schema detection logic -- [ ] Test migration on clean database - -### Phase 2: Testing -- [ ] Test migration on existing databases -- [ ] Validate foreign key constraints work correctly -- [ ] Test trigger functionality -- [ ] Test performance improvements -- [ ] Verify data preservation during table recreation - -### Phase 3: Deployment -- [ ] Deploy enhanced migration to development -- [ ] Monitor migration success rates -- [ ] Deploy to production -- [ ] Monitor for any issues - -### Phase 4: Settings Table Cleanup -- [ ] Create migration 006 to clean up settings table -- [ ] Remove orphaned settings records (accountDid is null) -- [ ] Clear any remaining activeDid values in settings -- [ ] Consider removing activeDid column entirely (future task) - -## Settings Table Cleanup Strategy - -### Current State Analysis -The settings table currently contains: -- **Legacy activeDid column**: Still present from original design -- **Orphaned records**: Settings with `accountDid = null` that may be obsolete -- **Redundant data**: Some settings may have been copied unnecessarily - -Based on team feedback, the cleanup should include: - -1. **Remove orphaned settings records**: - ```sql - DELETE FROM settings WHERE accountDid IS NULL; - ``` - -2. **Clear any remaining activeDid values**: - ```sql - UPDATE settings SET activeDid = NULL; - ``` - -3. **Future consideration**: Remove the activeDid column entirely from settings table - -### Migration 006: Settings Cleanup - -```sql -{ - name: "006_settings_cleanup", - sql: ` - -- Remove orphaned settings records (accountDid is null) - DELETE FROM settings WHERE accountDid IS NULL; - - -- Clear any remaining activeDid values in settings - UPDATE settings SET activeDid = NULL; - - -- Optional: Consider removing the activeDid column entirely - -- ALTER TABLE settings DROP COLUMN activeDid; - ` -} -``` - -### Benefits of Settings Cleanup -- **Reduced confusion**: Eliminates dual-purpose columns -- **Cleaner architecture**: Settings table focuses only on user preferences -- **Reduced storage**: Removes unnecessary data -- **Clearer separation**: Active identity vs. user settings are distinct concerns - -### Risk Assessment: LOW -- **Data safety**: Only removes orphaned/obsolete records -- **Backward compatibility**: Maintains existing column structure -- **Rollback**: Easy to restore if needed -- **Testing**: Can be validated with existing data - -## Code Changes Required - -### Files to Modify -1. **`src/db-sql/migration.ts`** - Add migration 005 with enhanced constraints -2. **`src/db-sql/migration.ts`** - Add migration 006 for settings cleanup -3. **`src/services/migrationService.ts`** - Add enhanced validation and detection logic -4. **`src/utils/PlatformServiceMixin.ts`** - Remove manual timestamp updates - -### Estimated Impact -- **Migration File**: ~25 lines added (migration 005) + ~15 lines added (migration 006) -- **Migration Service**: ~50 lines added (enhanced validation) -- **PlatformServiceMixin**: ~20 lines removed (manual timestamps) -- **Total**: ~90 lines changed - -## Conclusion - -**✅ IMPLEMENTATION COMPLETE**: The active identity upgrade plan has been successfully applied to the current project. - -### Successfully Implemented - -**✅ Migration Structure Updated**: -- **Migration 003**: `003_add_hasBackedUpSeed_to_settings` (assumes master deployment) -- **Migration 004**: `004_active_identity_and_seed_backup` (creates active_identity table) -- **All migrations are additional** - follows team member feedback exactly - -**✅ Technical Implementation**: -- **Data Migration**: Preserves existing `activeDid` from settings table -- **Foreign Key Constraints**: `ON DELETE SET NULL` for data safety -- **iOS/Android Compatibility**: Confirmed with SQLCipher 4.9.0 (SQLite 3.44.2) -- **Migration Service**: Updated validation and schema detection logic - -**✅ Code Quality**: -- **TypeScript**: All type errors resolved -- **Linting**: No linting errors -- **Team Guidance**: Follows "additional migrations only" requirement - -### Next Steps (Future Enhancements) - -The foundation is now in place for future enhancements: - -1. **Migration 005**: `005_active_identity_enhancements` (ON DELETE RESTRICT, triggers, indexes) -2. **Migration 006**: `006_settings_cleanup` (remove orphaned settings, clear legacy activeDid) -3. **Code Simplification**: Remove manual timestamp updates from PlatformServiceMixin - -### Current Status - -**Migration 004 is ready for deployment** and will: -- ✅ Create `active_identity` table with proper constraints -- ✅ Migrate existing `activeDid` data from settings -- ✅ Work identically on iOS and Android -- ✅ Follow team member feedback for additional migrations only - -**Key Point**: All migrations are **additional** - no editing of previous migrations since master code has been deployed. This ensures compatibility and proper testing. - ---- - -**Status**: Ready for team review and implementation approval -**Last Updated**: 2025-09-11 -**Next Review**: After team feedback and approval diff --git a/doc/active-pointer-smart-deletion-pattern.md b/doc/active-pointer-smart-deletion-pattern.md deleted file mode 100644 index 254e689c..00000000 --- a/doc/active-pointer-smart-deletion-pattern.md +++ /dev/null @@ -1,392 +0,0 @@ -# Engineering Directive v2 — Active Pointer + Smart Deletion Pattern (hardened) - -**Author**: Matthew Raymer -**Date**: 2025-01-27 -**Status**: 🎯 **ACTIVE** - Production-grade engineering directive for implementing smart deletion patterns - -## Overview - -This supersedes the previous draft and is **copy-pasteable** for any ``. It keeps UX smooth, guarantees data integrity, and adds production-grade safeguards (bootstrapping, races, soft deletes, bulk ops, and testability). Built on your prior pattern. - -## 0) Objectives (non-negotiable) - -1. Exactly **one active ``** pointer (or `NULL` during first-run). -2. **Block deletion** when it would leave **zero** ``. -3. If deleting the **active** item, **atomically re-point** to a deterministic **next** item **before** delete. -4. Enforce with **app logic** + **FK `RESTRICT`** (and `ON UPDATE CASCADE` if `ref` can change). - ---- - -## 1) Schema / Migration (SQLite) - -```sql --- __active_.sql -PRAGMA foreign_keys = ON; - --- Stable external key on (e.g., did/slug/uuid) --- ALTER TABLE ADD COLUMN ref TEXT UNIQUE NOT NULL; -- if missing - -CREATE TABLE IF NOT EXISTS active_ ( - id INTEGER PRIMARY KEY CHECK (id = 1), - activeRef TEXT UNIQUE, -- allow NULL on first run - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (activeRef) REFERENCES (ref) - ON UPDATE CASCADE - ON DELETE RESTRICT -); - --- Seed singleton row (idempotent) -INSERT INTO active_ (id, activeRef) -SELECT 1, NULL -WHERE NOT EXISTS (SELECT 1 FROM active_ WHERE id = 1); -``` - -**Rules** - -* **Never** default `activeRef` to `''`—use `NULL` for "no selection yet". -* Ensure `PRAGMA foreign_keys = ON` for **every connection**. - ---- - -## 2) Data Access API (TypeScript) - -```ts -// Required DAL -async function getAllRefs(): Promise { /* SELECT ref FROM ORDER BY created_at, ref */ } -async function getRefById(id: number): Promise { /* SELECT ref FROM WHERE id=? */ } -async function getActiveRef(): Promise { /* SELECT activeRef FROM active_ WHERE id=1 */ } -async function setActiveRef(ref: string|null): Promise { /* UPDATE active_ SET activeRef=?, lastUpdated=datetime('now') WHERE id=1 */ } -async function deleteById(id: number): Promise { /* DELETE FROM WHERE id=? */ } -async function countModels(): Promise { /* SELECT COUNT(*) FROM */ } - -// Deterministic "next" -function pickNextRef(all: string[], current?: string): string { - const sorted = [...all].sort(); - if (!current) return sorted[0]; - const i = sorted.indexOf(current); - return sorted[(i + 1) % sorted.length]; -} -``` - ---- - -## 3) Smart Delete (Atomic, Race-safe) - -```ts -async function smartDeleteModelById(id: number, notify: (m: string) => void) { - await db.transaction(async trx => { - const total = await countModels(); - if (total <= 1) { - notify("Cannot delete the last item. Keep at least one."); - throw new Error("blocked:last-item"); - } - - const refToDelete = await getRefById(id); - const activeRef = await getActiveRef(); - - if (activeRef === refToDelete) { - const all = (await getAllRefs()).filter(r => r !== refToDelete); - const next = pickNextRef(all, refToDelete); - await setActiveRef(next); - notify(`Switched active to ${next} before deletion.`); - } - - await deleteById(id); // RESTRICT prevents orphaning if we forgot to switch - }); - - // Post-tx: emit events / refresh UI -} -``` - ---- - -## 4) Bootstrapping & Repair - -```ts -async function ensureActiveSelected() { - const active = await getActiveRef(); - const all = await getAllRefs(); - if (active === null && all.length > 0) { - await setActiveRef(pickNextRef(all)); // first stable choice - } -} -``` - -Invoke after migrations and after bulk imports. - ---- - -## 5) Concurrency & Crash Safety - -* **Always** wrap "switch → delete" inside a **single transaction**. -* Treat any FK violation as a **logic regression**; surface telemetry (`fk:restrict`). - ---- - -## 6) Soft Deletes (if applicable) - -If `` uses `deleted_at`: - -* Replace `DELETE` with `UPDATE SET deleted_at = datetime('now') WHERE id=?`. -* Add a **partial uniqueness** strategy for `ref`: - - * SQLite workaround: make `ref` unique globally and never reuse; or maintain a shadow `refs` ledger to prevent reuse. -* Adjust `getAllRefs()` to filter `WHERE deleted_at IS NULL`. - ---- - -## 7) Bulk Ops & Imports - -* For batch deletes: - - 1. Compute survivors. - 2. If a batch would remove **all** survivors → **refuse**. - 3. If the **active** is included, precompute a deterministic **new active** and set it **once** before deleting. -* After imports, run `ensureActiveSelected()`. - ---- - -## 8) Multi-Scope Actives (optional) - -To support **one active per workspace/tenant**: - -* Replace singleton with scoped pointer: - - ```sql - CREATE TABLE active_ ( - scope TEXT NOT NULL, -- e.g., workspace_id - activeRef TEXT, - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (scope), - FOREIGN KEY (activeRef) REFERENCES (ref) ON UPDATE CASCADE ON DELETE RESTRICT - ); - ``` -* All APIs gain `scope` parameter; transactions remain unchanged in spirit. - ---- - -## 9) UX Contract - -* Delete confirmation must state: - - * Deleting the **active** item will **auto-switch**. - * Deleting the **last** item is **not allowed**. -* Keep list ordering aligned with `pickNextRef` strategy for predictability. - ---- - -## 10) Observability - -* Log categories: - - * `blocked:last-item` - * `fk:restrict` - * `repair:auto-selected-active` - * `active:switch:pre-delete` -* Emit metrics counters; attach `` and (if used) `scope`. - ---- - -## 11) Test Matrix (must pass) - -1. **Non-active delete** (≥2): deleted; active unchanged. -2. **Active delete** (≥2): active switches deterministically, then delete succeeds. -3. **Last item delete** (==1): blocked with message. -4. **First-run**: 0 items → `activeRef` stays `NULL`; add first → `ensureActiveSelected()` selects it. -5. **Ref update** (if allowed): `activeRef` follows via `ON UPDATE CASCADE`. -6. **Soft delete** mode: filters respected; invariants preserved. -7. **Bulk delete** that includes active but not all: pre-switch then delete set. -8. **Foreign keys disabled** (fault injection): tests must fail to surface missing PRAGMA. - ---- - -## 12) Rollout & Rollback - -* **Feature-flag** the new deletion path. -* Migrations are **idempotent**; ship `ensureActiveSelected()` with them. -* Keep a pre-migration backup for `` on first rollout. -* Rollback leaves `active_` table harmlessly present. - ---- - -## 13) Replace-Me Cheatsheet - -* `` → singular (e.g., `project`) -* `` → plural table (e.g., `projects`) -* `ref` → stable external key (`did` | `slug` | `uuid`) - ---- - -**Outcome:** You get **predictable UX**, **atomic state changes**, and **hard integrity guarantees** across single- or multi-scope actives, with clear tests and telemetry to keep it honest. - ---- - -## TimeSafari Implementation Guide - -### Current State Analysis (2025-01-27) - -**Status**: ✅ **FULLY COMPLIANT** - Active Pointer + Smart Deletion Pattern implementation complete. - -**Compliance Score**: 100% (6/6 components compliant) - -#### ✅ **What's Working** -- **Smart Deletion Logic**: `IdentitySwitcherView.vue` implements atomic transaction-safe deletion -- **Data Access API**: All required DAL methods exist in `PlatformServiceMixin.ts` -- **Schema Structure**: `active_identity` table follows singleton pattern correctly -- **Bootstrapping**: `$ensureActiveSelected()` method implemented -- **Foreign Key Constraint**: ✅ **FIXED** - Now uses `ON DELETE RESTRICT` (Migration 005) -- **Settings Cleanup**: ✅ **COMPLETED** - Orphaned records removed (Migration 006) - -#### ✅ **All Issues Resolved** -- ✅ Foreign key constraint fixed to `ON DELETE RESTRICT` -- ✅ Settings table cleaned up (orphaned records removed) - -### Updated Implementation Plan - -**Note**: Smart deletion logic is already implemented correctly. Focus on fixing security issues and cleanup. - -#### 1) Critical Security Fix (Migration 005) - -**Fix Foreign Key Constraint:** -```sql --- Migration 005: Fix foreign key constraint to ON DELETE RESTRICT -{ - name: "005_active_identity_constraint_fix", - sql: ` - PRAGMA foreign_keys = ON; - - -- Recreate table with ON DELETE RESTRICT constraint (SECURITY FIX) - CREATE TABLE active_identity_new ( - id INTEGER PRIMARY KEY CHECK (id = 1), - activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT, - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Copy existing data - INSERT INTO active_identity_new (id, activeDid, lastUpdated) - SELECT id, activeDid, lastUpdated FROM active_identity; - - -- Replace old table - DROP TABLE active_identity; - ALTER TABLE active_identity_new RENAME TO active_identity; - - -- Recreate indexes - CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); - ` -} -``` - -### Updated Implementation Plan - -**Note**: Smart deletion logic is already implemented correctly. Migration 005 (security fix) completed successfully. - -#### ✅ **Phase 1: Critical Security Fix (COMPLETED)** -- **Migration 005**: ✅ **COMPLETED** - Fixed foreign key constraint to `ON DELETE RESTRICT` -- **Impact**: Prevents accidental account deletion -- **Status**: ✅ **Successfully applied and tested** - -#### **Phase 2: Settings Cleanup (CURRENT)** -- **Migration 006**: Remove orphaned settings records -- **Impact**: Cleaner architecture, reduced confusion -- **Risk**: LOW - Only removes obsolete data - -#### 3) Optional Future Enhancement (Migration 007) - -**Remove Legacy activeDid Column:** -```sql --- Migration 007: Remove activeDid column entirely (future task) -{ - name: "007_remove_activeDid_column", - sql: ` - -- Remove the legacy activeDid column from settings table - ALTER TABLE settings DROP COLUMN activeDid; - ` -} -``` - -### Current Implementation Status - -#### ✅ **Already Implemented Correctly** -- **Smart Deletion Logic**: `IdentitySwitcherView.vue` lines 285-315 -- **Data Access API**: All methods exist in `PlatformServiceMixin.ts` -- **Transaction Safety**: Uses `$withTransaction()` for atomicity -- **Last Account Protection**: Blocks deletion when `total <= 1` -- **Deterministic Selection**: `$pickNextAccountDid()` method -- **Bootstrapping**: `$ensureActiveSelected()` method - -#### ❌ **Requires Immediate Fix** -1. **Foreign Key Constraint**: Change from `ON DELETE SET NULL` to `ON DELETE RESTRICT` -2. **Settings Cleanup**: Remove orphaned records with `accountDid=null` - -### Implementation Priority - -#### **Phase 1: Critical Security Fix (IMMEDIATE)** -- **Migration 005**: Fix foreign key constraint to `ON DELETE RESTRICT` -- **Impact**: Prevents accidental account deletion -- **Risk**: HIGH - Current implementation allows data loss - -#### **Phase 2: Settings Cleanup (HIGH PRIORITY)** -- **Migration 006**: Remove orphaned settings records -- **Impact**: Cleaner architecture, reduced confusion -- **Risk**: LOW - Only removes obsolete data - -#### **Phase 3: Future Enhancement (OPTIONAL)** -- **Migration 007**: Remove `activeDid` column from settings -- **Impact**: Complete separation of concerns -- **Risk**: LOW - Architectural cleanup - -#### **Phase 2: Settings Cleanup Implementation (Migration 006)** - -**Remove Orphaned Records:** -```sql --- Migration 006: Settings cleanup -{ - name: "006_settings_cleanup", - sql: ` - -- Remove orphaned settings records (accountDid is null) - DELETE FROM settings WHERE accountDid IS NULL; - - -- Clear any remaining activeDid values in settings - UPDATE settings SET activeDid = NULL; - ` -} -``` - -### Updated Compliance Assessment - -#### **Current Status**: ✅ **FULLY COMPLIANT** (100%) - -| Component | Status | Compliance | -|-----------|--------|------------| -| Smart Deletion Logic | ✅ Complete | 100% | -| Data Access API | ✅ Complete | 100% | -| Schema Structure | ✅ Complete | 100% | -| Foreign Key Constraint | ✅ Fixed (`RESTRICT`) | 100% | -| Settings Cleanup | ✅ Completed | 100% | -| **Overall** | ✅ **Complete** | **100%** | - -### Implementation Benefits - -**Current implementation already provides:** -- ✅ **Atomic Operations**: Transaction-safe account deletion -- ✅ **Last Account Protection**: Prevents deletion of final account -- ✅ **Smart Switching**: Auto-switches active account before deletion -- ✅ **Deterministic Behavior**: Predictable "next account" selection -- ✅ **NULL Handling**: Proper empty state management - -**After fixes will add:** -- ✅ **Data Integrity**: Foreign key constraints prevent orphaned references -- ✅ **Clean Architecture**: Complete separation of identity vs. settings -- ✅ **Production Safety**: No accidental account deletion possible - -### Implementation Complete - -✅ **All Required Steps Completed:** -1. ✅ **Migration 005**: Foreign key constraint fixed to `ON DELETE RESTRICT` -2. ✅ **Migration 006**: Settings cleanup completed (orphaned records removed) -3. ✅ **Testing**: All migrations executed successfully with no performance delays - -**Optional Future Enhancement:** -- **Migration 007**: Remove `activeDid` column from settings table (architectural cleanup) - -The Active Pointer + Smart Deletion Pattern is now **fully implemented** with **100% compliance**. diff --git a/doc/activeDid-migration-plan.md b/doc/activeDid-migration-plan.md deleted file mode 100644 index 26b68f7f..00000000 --- a/doc/activeDid-migration-plan.md +++ /dev/null @@ -1,559 +0,0 @@ -# ActiveDid Migration Plan - Implementation Guide - -**Author**: Matthew Raymer -**Date**: 2025-09-03T06:40:54Z -**Status**: 🚀 **ACTIVE MIGRATION** - API Layer Complete, Component Updates Complete ✅ - -## Objective - -Move the `activeDid` field from the `settings` table to a dedicated `active_identity` table to improve database architecture, prevent data corruption, and separate identity selection from user preferences. - -## Result - -This document provides the specific implementation steps required to complete the ActiveDid migration with all necessary code changes. - -## Use/Run - -Follow this implementation checklist step-by-step to complete the migration. - -## Context & Scope - -- **In scope**: Database migration, API updates, component updates, testing -- **Out of scope**: UI changes, authentication flow changes, MASTER_SETTINGS_KEY elimination (future improvement) - -## Critical Vue Reactivity Bug Discovery - -### Issue -During testing of the ActiveDid migration, a critical Vue reactivity bug was discovered: - -**Problem**: The `newDirectOffersActivityNumber` element in HomeView.vue fails to render correctly without a watcher on `numNewOffersToUser`. - -**Symptoms**: -- Element not found in DOM even when `numNewOffersToUser` has correct value -- Test failures with "element not found" errors -- Inconsistent rendering behavior - -**Root Cause**: Unknown Vue reactivity issue where property changes don't trigger proper template updates - -**Workaround**: A watcher on `numNewOffersToUser` with debug logging is required: -```typescript -@Watch("numNewOffersToUser") -onNumNewOffersToUserChange(newValue: number, oldValue: number) { - logger.debug("[HomeView] numNewOffersToUser changed", { - oldValue, - newValue, - willRender: !!newValue, - timestamp: new Date().toISOString() - }); -} -``` - -**Impact**: This watcher must remain in the codebase until the underlying Vue reactivity issue is resolved. - -**Files Affected**: `src/views/HomeView.vue` - -### Investigation Needed -- [ ] Investigate why Vue reactivity is not working correctly -- [ ] Check for race conditions in component lifecycle -- [ ] Verify if this affects other components -- [ ] Consider Vue version upgrade or configuration changes - -## Implementation Checklist - -### Phase 1: Database Migration ✅ COMPLETE -- [x] Add migration to MIGRATIONS array in `src/db-sql/migration.ts` -- [x] Create active_identity table with constraints -- [x] Include data migration from settings to active_identity table - -**Status**: All migrations executed successfully. active_identity table created and populated with data. - -### Phase 2: API Layer Updates ✅ COMPLETE -- [x] Implement `$getActiveIdentity()` method (exists with correct return type) -- [x] Fix `$getActiveIdentity()` return type to match documented interface -- [x] Update `$accountSettings()` to use new method (minimal safe change) -- [x] Update `$updateActiveDid()` with dual-write pattern -- [x] Add strategic logging for migration verification - -**Status**: All API layer updates complete and verified working. Methods return correct data format and maintain backward compatibility. - -### Phase 3: Component Updates ✅ COMPLETE -- [x] Update HomeView.vue to use `$getActiveIdentity()` (completed) -- [x] Update OfferDialog.vue to use `$getActiveIdentity()` (completed) -- [x] Update PhotoDialog.vue to use `$getActiveIdentity()` (completed) -- [x] Update GiftedDialog.vue to use `$getActiveIdentity()` (completed) -- [x] Update MembersList.vue to use `$getActiveIdentity()` (completed) -- [x] Update OnboardingDialog.vue to use `$getActiveIdentity()` (completed) -- [x] Update ImageMethodDialog.vue to use `$getActiveIdentity()` (completed) -- [x] Update DIDView.vue to use `$getActiveIdentity()` (completed) -- [x] Update TestView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ContactAmountsView.vue to use `$getActiveIdentity()` (completed) -- [x] Update UserProfileView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ClaimView.vue to use `$getActiveIdentity()` (completed) -- [x] Update OfferDetailsView.vue to use `$getActiveIdentity()` (completed) -- [x] Update QuickActionBvcEndView.vue to use `$getActiveIdentity()` (completed) -- [x] Update SharedPhotoView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ClaimReportCertificateView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ProjectsView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ClaimAddRawView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ContactQRScanShowView.vue to use `$getActiveIdentity()` (completed) -- [x] Update InviteOneAcceptView.vue to use `$getActiveIdentity()` (completed) -- [x] Update RecentOffersToUserView.vue to use `$getActiveIdentity()` (completed) -- [x] Update NewEditProjectView.vue to use `$getActiveIdentity()` (completed) -- [x] Update GiftedDetailsView.vue to use `$getActiveIdentity()` (completed) -- [x] Update IdentitySwitcherView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ContactQRScanFullView.vue to use `$getActiveIdentity()` (completed) -- [x] Update NewActivityView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ContactImportView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ProjectViewView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ClaimCertificateView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ContactGiftingView.vue to use `$getActiveIdentity()` (completed) -- [x] Update ConfirmGiftView.vue to use `$getActiveIdentity()` (completed) -- [x] Update RecentOffersToUserProjectsView.vue to use `$getActiveIdentity()` (completed) -- [x] Update InviteOneView.vue to use `$getActiveIdentity()` (completed) -- [x] Update AccountViewView.vue to use `$getActiveIdentity()` (completed) -- [x] All component migrations complete! ✅ -- [ ] Replace `this.activeDid = settings.activeDid` pattern -- [ ] Test each component individually - -**Status**: 23 components successfully migrated. 11 components remaining. API layer ready for systematic updates. - -### Phase 4: Testing 🟡 PARTIALLY STARTED - -- [x] Test Web platform (verified working) -- [ ] Test Electron platform -- [ ] Test iOS platform -- [ ] Test Android platform -- [ ] Test migration rollback scenarios -- [ ] Test data corruption recovery - -## Required Code Changes - -### 1. Database Migration ✅ COMPLETE - -```typescript -// Already added to MIGRATIONS array in src/db-sql/migration.ts -{ - name: "003_active_did_separate_table", - sql: ` - -- Create new active_identity table with proper constraints - CREATE TABLE IF NOT EXISTS active_identity ( - id INTEGER PRIMARY KEY CHECK (id = 1), - activeDid TEXT NOT NULL, - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE CASCADE - ); - - -- Add performance indexes - CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid); - CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); - - -- Insert default record (will be updated during migration) - INSERT OR IGNORE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, '', datetime('now')); - - -- MIGRATE EXISTING DATA: Copy activeDid from settings to active_identity - -- This prevents data loss when migration runs on existing databases - UPDATE active_identity - SET activeDid = (SELECT activeDid FROM settings WHERE id = 1), - lastUpdated = datetime('now') - WHERE id = 1 - AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != ''); - `, -}, -``` - -### 2. $getActiveIdentity() Method ✅ EXISTS - -```typescript -// Already exists in PlatformServiceMixin.ts with correct return type -async $getActiveIdentity(): Promise<{ activeDid: string }> { - try { - const result = await this.$dbQuery( - "SELECT activeDid FROM active_identity WHERE id = 1" - ); - - if (result?.values?.length) { - const activeDid = result.values[0][0] as string; - - // Validate activeDid exists in accounts - if (activeDid) { - const accountExists = await this.$dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [activeDid] - ); - - if (accountExists?.values?.length) { - return { activeDid }; - } else { - // Clear corrupted activeDid - await this.$dbExec( - "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1" - ); - return { activeDid: "" }; - } - } - } - - return { activeDid: "" }; - } catch (error) { - logger.error("[PlatformServiceMixin] Error getting active identity:", error); - return { activeDid: "" }; - } -} -``` - -### 3. Update $accountSettings Method - -```typescript -// Update in PlatformServiceMixin.ts -async $accountSettings(did?: string, defaults: Settings = {}): Promise { - try { - // Get settings without activeDid (unchanged logic) - const settings = await this.$getMasterSettings(defaults); - - if (!settings) { - return defaults; - } - - // Get activeDid from new table (new logic) - const activeIdentity = await this.$getActiveIdentity(); - - // Return combined result (maintains backward compatibility) - return { ...settings, activeDid: activeIdentity.activeDid }; - } catch (error) { - logger.error("[Settings Trace] ❌ Error in $accountSettings:", error); - return defaults; - } -} -``` - -### 4. Update $updateActiveDid Method - -```typescript -// Update in PlatformServiceMixin.ts -async $updateActiveDid(newDid: string | null): Promise { - try { - if (newDid === null) { - // Clear active identity in both tables - await this.$dbExec( - "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1" - ); - - // Keep legacy field in sync (backward compatibility) - await this.$dbExec( - "UPDATE settings SET activeDid = '' WHERE id = ?", - [MASTER_SETTINGS_KEY] - ); - } else { - // Validate DID exists before setting - const accountExists = await this.$dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [newDid] - ); - - if (!accountExists?.values?.length) { - logger.error(`[PlatformServiceMixin] Cannot set activeDid to non-existent DID: ${newDid}`); - return false; - } - - // Update active identity in new table - await this.$dbExec( - "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", - [newDid] - ); - - // Keep legacy field in sync (backward compatibility) - await this.$dbExec( - "UPDATE settings SET activeDid = ? WHERE id = ?", - [newDid, MASTER_SETTINGS_KEY] - ); - } - - // Update internal tracking - await this._updateInternalActiveDid(newDid); - return true; - } catch (error) { - logger.error("[PlatformServiceMixin] Error updating activeDid:", error); - return false; - } -} -``` - -### 5. Component Updates Required - -**35 components need this pattern change:** - -```typescript -// CURRENT PATTERN (replace in all components): -this.activeDid = settings.activeDid || ""; - -// NEW PATTERN (use in all components): -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const activeIdentity = await (this as any).$getActiveIdentity(); -this.activeDid = activeIdentity.activeDid || ""; -``` - -**Components requiring updates:** - -#### Views (28 components) -- `src/views/DIDView.vue` (line 378) -- `src/views/TestView.vue` (line 654) -- `src/views/ContactAmountsView.vue` (line 226) -- `src/views/HomeView.vue` (line 517) -- `src/views/UserProfileView.vue` (line 185) -- `src/views/ClaimView.vue` (line 730) -- `src/views/OfferDetailsView.vue` (line 435) -- `src/views/QuickActionBvcEndView.vue` (line 229) -- `src/views/SharedPhotoView.vue` (line 178) -- `src/views/ClaimReportCertificateView.vue` (line 56) -- `src/views/ProjectsView.vue` (line 393) -- `src/views/ClaimAddRawView.vue` (line 114) -- `src/views/ContactQRScanShowView.vue` (line 288) -- `src/views/InviteOneAcceptView.vue` (line 122) -- `src/views/RecentOffersToUserView.vue` (line 118) -- `src/views/NewEditProjectView.vue` (line 380) -- `src/views/GiftedDetailsView.vue` (line 443) -- `src/views/ProjectViewView.vue` (line 782) -- `src/views/ContactsView.vue` (line 296) -- `src/views/ContactQRScanFullView.vue` (line 267) -- `src/views/NewActivityView.vue` (line 204) -- `src/views/ClaimCertificateView.vue` (line 42) -- `src/views/ContactGiftingView.vue` (line 166) -- `src/views/RecentOffersToUserProjectsView.vue` (line 126) -- `src/views/InviteOneView.vue` (line 285) -- `src/views/IdentitySwitcherView.vue` (line 202) -- `src/views/AccountViewView.vue` (line 1052) -- `src/views/ConfirmGiftView.vue` (line 549) -- `src/views/ContactImportView.vue` (line 342) - -#### Components (7 components) -- `src/components/OfferDialog.vue` (line 177) -- `src/components/PhotoDialog.vue` (line 270) -- `src/components/GiftedDialog.vue` (line 223) -- `src/components/MembersList.vue` (line 234) -- `src/components/OnboardingDialog.vue` (line 272) -- `src/components/ImageMethodDialog.vue` (line 502) -- `src/components/FeedFilters.vue` (line 89) - -**Implementation Strategy:** - -1. **Systematic Replacement**: Use grep search to find all instances -2. **Pattern Matching**: Replace `this.activeDid = settings.activeDid` with new pattern -3. **Error Handling**: Ensure proper error handling in each component -4. **Testing**: Test each component individually after update - -**Example Component Update:** - -```typescript -// BEFORE (in any component): -private async initializeSettings() { - const settings = await this.$accountSettings(); - this.activeDid = settings.activeDid || ""; - this.apiServer = settings.apiServer || ""; -} - -// AFTER (in any component): -private async initializeSettings() { - const settings = await this.$accountSettings(); - const activeIdentity = await this.$getActiveIdentity(); - this.activeDid = activeIdentity.activeDid || ""; - this.apiServer = settings.apiServer || ""; -} -``` - -**Alternative Pattern (if settings still needed):** - -```typescript -// If component needs both settings and activeDid: -private async initializeSettings() { - const settings = await this.$accountSettings(); - const activeIdentity = await this.$getActiveIdentity(); - - // Use activeDid from new table - this.activeDid = activeIdentity.activeDid || ""; - - // Use other settings from settings table - this.apiServer = settings.apiServer || ""; - this.partnerApiServer = settings.partnerApiServer || ""; - // ... other settings -} -``` - -## What Works (Evidence) - -- ✅ **Migration code exists** in MIGRATIONS array - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Console log shows successful execution of migrations 003 and 004 - - **Verify at**: `🎉 [Migration] Successfully applied: 003_active_did_separate_table` - -- ✅ **$getActiveIdentity() method exists** in PlatformServiceMixin - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Console log shows method calls returning correct data format - - **Verify at**: `[PlatformServiceMixin] $getActiveIdentity(): activeDid resolved {activeDid: 'did:ethr:0xAe6ea6A4c20aDeE7B1c7Ee1fEFAa6fBe0986a671'}` - -- ✅ **Database migration infrastructure** exists and mature - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Console log shows 6 migrations applied successfully - - **Verify at**: `🎉 [Migration] Migration process complete! Summary: 6 applied, 0 skipped` - -- ✅ **$accountSettings() updated** with minimal safe change - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Console log shows method returning activeDid from new table - - **Status**: Maintains all existing complex logic while using new table as primary source - -- ✅ **$updateActiveDid() dual-write implemented** - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Method exists and ready for testing - - **Status**: Uses MASTER_SETTINGS_KEY constant for proper settings table targeting - -- ✅ **HomeView.vue successfully migrated** to use new API - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Console log shows `[HomeView] ActiveDid migration - using new API` - - **Status**: Component successfully uses `$getActiveIdentity()` instead of `settings.activeDid` - -- ✅ **Clean architecture implemented** - active_identity is now single source of truth - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Console log shows consistent activeDid values from active_identity table - - **Status**: active_identity table is the only source for activeDid, settings table handles app config only - -- ✅ **Schema cleanup** - activeDid column removed from settings table - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Console log shows successful execution of migration 004 - - **Status**: Complete separation of concerns - no more confusing dual-purpose columns - -## What Doesn't (Evidence & Hypotheses) - -- ❌ **11 components still use old pattern** `this.activeDid = settings.activeDid` - - **Time**: 2025-09-03T06:40:54Z - - **Evidence**: Grep search found 11 remaining instances across views and components - - **Hypothesis**: Components need updates but API layer is now ready - - **Next probe**: Systematic component updates can now proceed - -## Risks, Limits, Assumptions - -- **Data Loss Risk**: Migration failure could lose activeDid values -- **Breaking Changes**: API updates required in PlatformServiceMixin -- **Testing Overhead**: All platforms must be tested with new structure -- **Component Updates**: 35+ components need individual updates and testing - -## Rollback Strategy - -### Schema Rollback -```sql --- If migration fails, restore original schema -DROP TABLE IF EXISTS active_identity; -``` - -### Data Rollback -```typescript -// Rollback function to restore activeDid to settings table -async function rollbackActiveDidMigration(): Promise { - try { - const activeIdentityResult = await dbQuery( - "SELECT activeDid FROM active_identity WHERE id = 1" - ); - - if (activeIdentityResult?.values?.length) { - const activeDid = activeIdentityResult.values[0][0] as string; - - await dbExec( - "UPDATE settings SET activeDid = ? WHERE id = ?", - [activeDid, MASTER_SETTINGS_KEY] - ); - - return true; - } - - return false; - } catch (error) { - logger.error("[Rollback] Failed to restore activeDid:", error); - return false; - } -} -``` - -## Next Steps - -| Task | Exit Criteria | Priority | -|------|---------------|----------| -| **Update $accountSettings() method** | Method calls $getActiveIdentity and combines with settings | ✅ COMPLETE | -| **Implement $updateActiveDid() dual-write** | Method updates both active_identity and settings tables | ✅ COMPLETE | -| **Start application in browser** | Application loads and initializes IndexedDB database | ✅ COMPLETE | -| **Inspect IndexedDB via DevTools** | Verify active_identity table exists and contains data | ✅ COMPLETE | -| **Update first component** | One component successfully uses new API pattern | ✅ COMPLETE (HomeView.vue) | -| **Systematic component updates** | All 26 remaining components use new API pattern (with test:web after each) | 🟢 HIGH | -| **Test all platforms** | Web, Electron, iOS, Android platforms verified working | 🟡 MEDIUM | -| **Performance optimization** | Reduce excessive $getActiveIdentity() calls | 🟡 MEDIUM | - -**Critical Blocker**: API layer complete. Ready to proceed with component updates. - -## Migration Execution Rule - -### **One Component + Test Pattern** -**Rule**: After migrating each component, run `npm run test:web` and `npm run lint-fix` to verify the change doesn't break existing functionality and meets code standards. - -**Workflow**: -1. **Migrate one component** - Update to use `$getActiveIdentity()` pattern -2. **Run lint-fix** - Ensure code meets project standards -3. **Run test:web** - Verify no regressions introduced -4. **Commit if passing** - Only commit after tests and linting pass -5. **Repeat** - Move to next component - -**Benefits**: -- Catch issues immediately after each change -- Maintain code quality throughout migration -- Easy rollback if problems arise -- Systematic progress tracking - -**Exit Criteria**: All 26 components migrated with passing tests - -## Performance Observations - -### Excessive API Calls Detected -The console log shows `$getActiveIdentity()` being called very frequently (multiple times per component mount). This suggests: -- Components may be calling the API more than necessary -- Could be optimized for better performance -- Not a blocker, but worth monitoring during component updates - -### Recommended Optimization Strategy -1. **Audit component lifecycle** - Ensure API calls happen only when needed -2. **Implement caching** - Consider short-term caching of activeDid values -3. **Batch updates** - Group related API calls where possible -4. **Monitor performance** - Track API call frequency during component updates - -## Future Improvement: MASTER_SETTINGS_KEY Elimination - -**Not critical for this task** but logged for future improvement: - -```typescript -// Current: WHERE id = "1" -// Future: WHERE accountDid IS NULL - -// This eliminates the confusing concept of "master" settings -// and uses a cleaner pattern for default settings -``` - -## References - -- [Database Migration Guide](./database-migration-guide.md) -- [Dexie to SQLite Mapping](./dexie-to-sqlite-mapping.md) -- [PlatformServiceMixin Documentation](./component-communication-guide.md) - -## Competence Hooks - -- *Why this works*: Separates concerns between identity selection and user preferences, prevents data corruption with foreign key constraints -- *Common pitfalls*: Method signature mismatches, forgetting dual-write pattern, not testing database state -- *Next skill unlock*: Systematic API updates with backward compatibility -- *Teach-back*: Explain why dual-write pattern is needed during migration transition - -## Collaboration Hooks - -- **Reviewers**: Database team, PlatformServiceMixin maintainers, QA team -- **Sign-off checklist**: - - [ ] Migration script integrated with existing MIGRATIONS array - - [x] $getActiveIdentity() method returns correct type - - [x] $accountSettings() method updated to use new API (minimal safe change) - - [x] $updateActiveDid() method implements dual-write pattern - - [ ] All 35+ components updated to use new API - - [ ] Rollback procedures validated - - [ ] All platforms tested - - [ ] All stakeholders approve deployment timeline \ No newline at end of file diff --git a/doc/migration-004-complexity-resolution-plan.md b/doc/migration-004-complexity-resolution-plan.md deleted file mode 100644 index 1aec724c..00000000 --- a/doc/migration-004-complexity-resolution-plan.md +++ /dev/null @@ -1,198 +0,0 @@ -# Migration 004 Complexity Resolution Plan - -**Document Version**: 1.3 -**Author**: Matthew Raymer -**Date**: 2025-01-27 -**Status**: Implementation Phase - Phase 1 Complete - -## Problem Summary - -The current migration 004 implementation has become overly complex with multiple critical issues that create serious risks for data integrity and application performance. - -### Four Most Critical Issues - -1. **Duplicate SQL Definitions**: Migration 004 SQL exists in three separate locations (main sql field, statements array, recovery logic), making it impossible to ensure all users run identical statements. - -2. **Non-Atomic Execution**: Individual statements continue executing even if earlier statements fail, causing partial data migration and potential data loss. - -3. **Incorrect Database Result Handling**: Code assumes PlatformService abstraction format when called directly from raw database services, causing runtime errors. - -4. **Duplicate Execution Risk**: Recovery logic could re-run statements that already executed successfully, leading to data corruption. - -## Resolution Principles - -**Guiding Principle**: All migrations must execute from a single SQL source in the MIGRATIONS array, as one atomic statement. - -- **Single Source of Truth**: Only one place defines migration SQL -- **Atomic Operations**: Migration succeeds completely or fails completely -- **Database Agnostic**: Result handling works with any database service -- **Minimal Overhead**: No unnecessary logging or validation -- **Simple Recovery**: If migration fails, it should be obvious and fixable - -## Implementation Phases - -### Phase 1: Simplify Migration Definition ✅ COMPLETED -**Objective**: Establish single source of truth for migration SQL - -**Actions**: -- ✅ Remove `statements` array from migration 004 definition -- ✅ Keep only the single `sql` field as the authoritative source -- ✅ Remove all recovery logic that duplicates SQL statements -- ✅ Ensure migration SQL is self-contained and atomic - -**Deliverables**: -- ✅ Clean migration definition with single SQL source -- ✅ Removed duplicate SQL definitions -- ✅ Eliminated recovery logic complexity - -### Phase 2: Fix Database Result Handling ✅ COMPLETED -**Objective**: Make result handling database-agnostic - -**Actions**: -- ✅ Remove DatabaseResult type assumptions from migration code -- ✅ Implement proper result extraction based on actual database service -- ✅ Use the `extractMigrationNames` function pattern consistently -- ✅ Make result handling work with any database service implementation -- ✅ Normalize results from AbsurdSqlDatabaseService and CapacitorPlatformService into shared internal format - -**Deliverables**: -- ✅ Database-agnostic result handling -- ✅ Consistent result extraction across all database services -- ✅ Removed type casting assumptions -- ✅ Shared internal result format for all database services - -### Phase 3: Ensure Atomic Execution ✅ COMPLETED -**Objective**: Guarantee migration succeeds completely or fails completely - -**Actions**: -- ✅ Modify migration service to execute single SQL block only -- ✅ Remove individual statement execution logic -- ✅ Implement proper error handling that prevents partial execution -- ✅ Ensure migration tracking is accurate -- ✅ Provide explicit rollback/restore instructions for migration failures -- ✅ Ensure migration logs indicate failure cause and required operator action - -**Deliverables**: -- ✅ Atomic migration execution -- ✅ Proper error handling -- ✅ Accurate migration tracking -- ✅ Clear recovery procedures - -### Phase 4: Remove Excessive Debugging ✅ COMPLETED -**Objective**: Eliminate performance overhead from debugging code - -**Actions**: -- ✅ Remove detailed logging that slows startup -- ✅ Keep only essential error logging -- ✅ Remove complex validation logic that runs on every startup -- ✅ Move debugging code to test page or development-only mode - -**Deliverables**: -- ✅ Faster application startup -- ✅ Cleaner production code -- ✅ Debugging available only when needed - -### Phase 5: Testing & Validation -**Objective**: Ensure simplified migration works correctly - -**Actions**: -- Test migration execution with different database services -- Verify no duplicate execution occurs -- Confirm proper error handling -- Validate data integrity after migration -- Test rollback/restore scenarios to confirm system recovery paths -- Test edge cases: empty database, partially migrated database, already-migrated database -- Test concurrency scenarios (multiple app instances/migrations starting simultaneously) -- Test cross-platform/device differences (SQLite, AbsurdSQL, Capacitor DB adapters) - -**Deliverables**: -- Working migration system -- No duplicate execution -- Proper error handling -- Data integrity maintained -- Validated recovery procedures -- Edge case coverage confirmed -- Documented test results as artifacts for future regression testing - -## Performance & Debugging - -**Current Issue**: Excessive logging and validation code runs on every app startup, slowing application performance. - -**Solution**: -- Debugging/logging is acceptable in development/test environments -- Production startup must not be slowed by migration debugging -- Move complex validation to test page or development-only mode -- Keep only essential error logging for production - -## Rollback & Recovery Procedures - -### Manual Rollback Steps -1. **Stop Application**: Ensure no active database connections -2. **Restore Database**: Use snapshot/backup to restore pre-migration state -3. **Clear Migration Tracking**: Remove migration 004 entry from migrations table -4. **Verify State**: Confirm active_identity table is removed and settings.activeDid is restored -5. **Restart Application**: Test normal operation - -### Automated Rollback -- **Automated Detection**: Migration service detects failure and triggers rollback -- **Database Restore**: Automated restoration from pre-migration snapshot -- **Logging**: Detailed rollback logs with failure cause and recovery actions -- **Validation**: Automated verification of rollback success - -### Recovery Validation -- **Data Integrity Check**: Verify all data is consistent with pre-migration state -- **Migration Status**: Confirm migration tracking reflects correct state -- **Application Functionality**: Test core features work correctly -- **Performance Baseline**: Confirm startup performance matches pre-migration levels - -## Files Requiring Changes - -### Core Migration Files (Primary Changes) -- `src/db-sql/migration.ts` - Remove duplicate SQL definitions, fix DatabaseResult usage, remove recovery logic -- `src/services/migrationService.ts` - Remove individual statement execution, ensure atomic execution - -### Database Service Files (Result Handling Fixes) -- `src/services/AbsurdSqlDatabaseService.ts` - Fix result extraction for migration queries -- `src/services/platforms/CapacitorPlatformService.ts` - Fix result extraction for migration queries - -**Note**: Verify all file paths match repository reality as part of CI validation. - -## Success Criteria - -- [ ] Migration 004 SQL defined in single location only -- [ ] Migration executes atomically (all-or-nothing) -- [ ] Database result handling works with all database services -- [ ] No duplicate statement execution possible -- [ ] Startup time reduced by at least 20% compared to pre-fix baseline (measured via cold app start profiling logs) -- [ ] Migration tracking is accurate and reliable -- [ ] Error handling is clear and actionable - -## Next Steps - -1. **Review and Approve Plan**: Get stakeholder approval for this approach -2. **Phase 1 Implementation**: Begin with simplifying migration definition -3. **Testing**: Validate each phase before proceeding -4. **Assign Migration Owner**: Designate clear owner for future migration reviews -5. **Create Review Checklist**: Define lightweight checklist (SQL duplication, atomicity, error handling) to prevent recurrence - -## Dependencies - -- Migration service architecture -- Database service implementations -- Testing infrastructure -- Documentation system -- Seed datasets or controlled test states for reproducible validation -- Snapshot/restore utilities for rollback testing - -## Lessons Learned - -**Process Improvement Note**: This migration complexity highlights the importance of closer review and consolidation of AI-generated code. Uncontrolled proliferation of generated logic leads to fragmentation, review fatigue, and system instability. Future development should prioritize: - -- Single source of truth for all critical logic -- Atomic operations over complex multi-step processes -- Regular consolidation and simplification of generated code -- Clear ownership and review processes for migration logic - ---- - -*This document will be updated as the implementation progresses and new insights are gained.* diff --git a/doc/verification-party-system-plan.md b/doc/verification-party-system-plan.md deleted file mode 100644 index bee3e196..00000000 --- a/doc/verification-party-system-plan.md +++ /dev/null @@ -1,375 +0,0 @@ -# TimeSafari Identity Verification Party System Plan - -## Objectives - -* Maintain strict conformity with TimeSafari's existing **DID, contact, and identity management**. -* Ensure **offline-first reliability** with background sync and retry logic. -* Provide **minimal, mobile-first UX** with single-tap core actions and QR-driven flows. - -## Architecture - -* Use a **single atomic migration** (`005_verification_party_system.sql`) following `registerMigration()` + `MIGRATIONS` array pattern. -* Standardize timestamps (`dateCreated`, `dateVerified`) in **ISO-8601 UTC**. -* Add `verification_session_logs` for audit trail and debugging. - -## Workflow - -* **Pre-Party**: Enforce RSVP via DID signing challenge; cache DID QR locally. -* **In-Party**: Dual-mode verification (Fast Scan + Deep Verify) with **trust presets**. -* **Post-Party**: Queue verifications for delayed sync; issue signed receipts; auto-create verified contacts. - -## Services - -* `VerificationPartyService`: Monolithic class aligned with existing service pattern. -* `DidVerificationService`: Pluggable methods (QR, NFC, manual, photo ID). -* `TrustNetworkService`: Add caching + **trust decay** unless renewed. - -## Security - -* Store **hashes of evidence** only (not raw PII). -* Encrypt data with **per-user derived keys**. -* Provide **per-verification sharing controls** (private, party-only, global). - -## UI/UX - -* Single-tap flows for RSVP, scan, verify. -* Embed **trust level criteria** in UI to reduce inconsistency. -* Optimize QR scanning and trust graph for **battery savings**. -* Follow existing **i18n service** for multi-language support. - -## Priorities - -1. Migration + offline queue -2. Dual-mode verification UI -3. Trust graph caching + decay -4. Privacy-hardened evidence handling -5. Notification constants + helper integration - ---- - -## Database Schema - -### Migration 005: Verification Party System -Add to `src/db-sql/migration.ts` in the `MIGRATIONS` array: - -```typescript -{ - name: "005_verification_party_system", - sql: ` - -- Migration 005: verification_party_system - -- Adds identity verification party functionality - - -- Enable foreign key constraints for data integrity - PRAGMA foreign_keys = ON; - - -- Create verification_parties table - CREATE TABLE IF NOT EXISTS verification_parties ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - partyId TEXT UNIQUE NOT NULL, - organizerDid TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - location TEXT, - scheduledDate TEXT, - maxParticipants INTEGER DEFAULT 50, - status TEXT DEFAULT 'planned', - dateCreated TEXT DEFAULT (datetime('now')), - FOREIGN KEY (organizerDid) REFERENCES accounts(did) - ); - - -- Create party_participants table - CREATE TABLE IF NOT EXISTS party_participants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - partyId TEXT NOT NULL, - participantDid TEXT NOT NULL, - status TEXT DEFAULT 'invited', - verificationCount INTEGER DEFAULT 0, - rsvpDate TEXT, - checkInDate TEXT, - dateCreated TEXT DEFAULT (datetime('now')), - FOREIGN KEY (partyId) REFERENCES verification_parties(partyId), - FOREIGN KEY (participantDid) REFERENCES accounts(did) - ); - - -- Create did_verifications table - CREATE TABLE IF NOT EXISTS did_verifications ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - verifierDid TEXT NOT NULL, - verifiedDid TEXT NOT NULL, - partyId TEXT, - verificationMethod TEXT, - verificationNotes TEXT, - verificationLevel INTEGER DEFAULT 1, - verificationEvidenceHash TEXT, - dateVerified TEXT DEFAULT (datetime('now')), - FOREIGN KEY (verifierDid) REFERENCES accounts(did), - FOREIGN KEY (verifiedDid) REFERENCES accounts(did), - FOREIGN KEY (partyId) REFERENCES verification_parties(partyId) - ); - - -- Create verification_session_logs table - CREATE TABLE IF NOT EXISTS verification_session_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - partyId TEXT NOT NULL, - sessionAction TEXT NOT NULL, - participantDid TEXT, - actionData TEXT, - dateCreated TEXT DEFAULT (datetime('now')), - FOREIGN KEY (partyId) REFERENCES verification_parties(partyId), - FOREIGN KEY (participantDid) REFERENCES accounts(did) - ); - - -- Create indexes for performance - CREATE INDEX IF NOT EXISTS idx_verification_parties_organizer ON verification_parties(organizerDid); - CREATE INDEX IF NOT EXISTS idx_verification_parties_status ON verification_parties(status); - CREATE INDEX IF NOT EXISTS idx_party_participants_party ON party_participants(partyId); - CREATE INDEX IF NOT EXISTS idx_party_participants_did ON party_participants(participantDid); - CREATE INDEX IF NOT EXISTS idx_did_verifications_verifier ON did_verifications(verifierDid); - CREATE INDEX IF NOT EXISTS idx_did_verifications_verified ON did_verifications(verifiedDid); - CREATE INDEX IF NOT EXISTS idx_did_verifications_party ON did_verifications(partyId); - CREATE INDEX IF NOT EXISTS idx_session_logs_party ON verification_session_logs(partyId); - ` -} -``` - ---- - -## TypeScript Interfaces - -### Required Interface Definitions -Add to `src/interfaces/verification-party.ts`: - -```typescript -/** - * Verification Party entity interface - */ -export interface VerificationParty { - id: number; - partyId: string; - organizerDid: string; - name: string; - description?: string; - location?: string; - scheduledDate?: string; - maxParticipants: number; - status: 'planned' | 'active' | 'completed' | 'cancelled'; - dateCreated: string; -} - -/** - * Party Participant entity interface - */ -export interface PartyParticipant { - id: number; - partyId: string; - participantDid: string; - status: 'invited' | 'confirmed' | 'attended' | 'verified'; - verificationCount: number; - rsvpDate?: string; - checkInDate?: string; - dateCreated: string; -} - -/** - * DID Verification entity interface - */ -export interface DidVerification { - id: number; - verifierDid: string; - verifiedDid: string; - partyId?: string; - verificationMethod: 'qr_scan' | 'manual_entry' | 'photo_id' | 'nfc'; - verificationNotes?: string; - verificationLevel: number; // 1-5 trust level - verificationEvidenceHash?: string; // Hash of verification evidence - dateVerified: string; -} - -/** - * Verification Session Log entity interface - */ -export interface VerificationSessionLog { - id: number; - partyId: string; - sessionAction: 'party_started' | 'participant_joined' | 'verification_completed' | 'sync_attempted'; - participantDid?: string; - actionData?: string; // JSON blob of action-specific data - dateCreated: string; -} -``` - ---- - -## PlatformServiceMixin Integration - -### Required Methods -Add to `PlatformServiceMixin`: - -```typescript -// Add to PlatformServiceMixin methods -async $insertVerificationParty(party: Partial): Promise { - return this.$insertEntity('verification_parties', party, [ - 'partyId', 'organizerDid', 'name', 'description', 'location', - 'scheduledDate', 'maxParticipants', 'status', 'dateCreated' - ]); -} - -async $insertPartyParticipant(participant: Partial): Promise { - return this.$insertEntity('party_participants', participant, [ - 'partyId', 'participantDid', 'status', 'verificationCount', - 'rsvpDate', 'checkInDate', 'dateCreated' - ]); -} - -async $insertDidVerification(verification: Partial): Promise { - return this.$insertEntity('did_verifications', verification, [ - 'verifierDid', 'verifiedDid', 'partyId', 'verificationMethod', - 'verificationNotes', 'verificationLevel', 'verificationEvidenceHash', 'dateVerified' - ]); -} - -async $getVerificationParties(): Promise { - const results = await this.$dbQuery('SELECT * FROM verification_parties ORDER BY dateCreated DESC'); - return this.$mapResults(results, (row) => ({ - id: row[0] as number, - partyId: row[1] as string, - organizerDid: row[2] as string, - name: row[3] as string, - description: row[4] as string, - location: row[5] as string, - scheduledDate: row[6] as string, - maxParticipants: row[7] as number, - status: row[8] as VerificationParty['status'], - dateCreated: row[9] as string, - })); -} - -async $getPartyParticipants(partyId: string): Promise { - const results = await this.$dbQuery( - 'SELECT * FROM party_participants WHERE partyId = ? ORDER BY dateCreated DESC', - [partyId] - ); - return this.$mapResults(results, (row) => ({ - id: row[0] as number, - partyId: row[1] as string, - participantDid: row[2] as string, - status: row[3] as PartyParticipant['status'], - verificationCount: row[4] as number, - rsvpDate: row[5] as string, - checkInDate: row[6] as string, - dateCreated: row[7] as string, - })); -} -``` - ---- - -## Notification Constants - -### Required Notification Constants -Add to `src/constants/notifications.ts`: - -```typescript -// Used in: VerificationPartyCreateView.vue (createParty method) -export const NOTIFY_PARTY_CREATED = { - title: "Verification Party Created", - message: "Your verification party has been created successfully." -}; - -// Used in: VerificationPartyJoinView.vue (joinParty method) -export const NOTIFY_PARTY_JOINED = { - title: "Party Joined", - message: "You have successfully joined the verification party." -}; - -// Used in: VerificationPartyActiveView.vue (submitManualVerification method) -export const NOTIFY_VERIFICATION_COMPLETED = { - title: "Identity Verified", - message: "You have successfully verified this person's identity." -}; - -// Used in: VerificationPartyService.ts (syncVerifications method) -export const NOTIFY_VERIFICATION_SYNCED = { - title: "Verifications Synced", - message: "Your verification data has been synchronized successfully." -}; - -// Used in: VerificationPartyActiveView.vue (error handling) -export const NOTIFY_VERIFICATION_FAILED = { - title: "Verification Failed", - message: "There was an error completing the verification. Please try again." -}; -``` - -### Notification Helper Integration -Use existing `createNotifyHelpers()` pattern in components: - -```typescript -// In VerificationPartyCreateView.vue -const { success, error } = createNotifyHelpers(this.$notify); - -// Usage -success("Party created successfully!"); -error("Failed to create party. Please try again."); -``` - ---- - -## Component Implementation Pattern - -### VerificationPartyCreateView.vue Structure -```typescript -@Component({ - name: "VerificationPartyCreateView", - components: { - QuickNav, - TopMessage, - EntityIcon, - }, - mixins: [PlatformServiceMixin], -}) -export default class VerificationPartyCreateView extends Vue { - // Use PlatformServiceMixin methods - async createParty(): Promise { - const partyData: Partial = { - partyId: `party_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - organizerDid: (await this.$getActiveIdentity()).activeDid, - name: this.partyForm.name, - description: this.partyForm.description, - location: this.partyForm.location, - scheduledDate: this.partyForm.scheduledDate, - maxParticipants: this.partyForm.maxParticipants, - status: 'planned', - dateCreated: new Date().toISOString(), - }; - - const success = await this.$insertVerificationParty(partyData); - if (success) { - this.$notify(NOTIFY_PARTY_CREATED); - this.$router.push(`/verification-party/${partyData.partyId}`); - } else { - this.$notify(NOTIFY_VERIFICATION_FAILED); - } - } -} -``` - ---- - -## Architecture Conformity Checklist - -### ✅ **100% CONFORMANT PATTERNS** -- **Migration Structure**: ✅ Follows existing `registerMigration()` and `MIGRATIONS` array pattern -- **Database Schema**: ✅ Uses `INTEGER PRIMARY KEY AUTOINCREMENT` and `camelCase` field naming -- **Component Architecture**: ✅ Integrates `@Component` decorator and `PlatformServiceMixin` -- **Service Pattern**: ✅ Single monolithic service class following TimeSafari conventions -- **Notification System**: ✅ Uses existing `NOTIFY_*` constants and `createNotifyHelpers()` -- **UI Components**: ✅ Leverages existing `QuickNav`, `TopMessage`, `EntityIcon` components -- **TypeScript Interfaces**: ✅ Proper interface definitions following existing patterns -- **PlatformServiceMixin Integration**: ✅ Uses existing `$insertEntity()` and `$mapResults()` methods -- **Database Operations**: ✅ Follows existing `$dbQuery()`, `$dbExec()` patterns -- **Error Handling**: ✅ Uses existing logger and error handling patterns - -### 📊 **FINAL CONFORMITY SCORE: 100%** - -The verification party system plan now achieves complete conformity with TimeSafari's existing architecture patterns, naming conventions, and integration approaches. \ No newline at end of file