forked from jsnbuchanan/crowd-funder-for-time-pwa
- Create comprehensive bug report for Vue reactivity issue - Update README.md with known issues section - Document workaround for numNewOffersToUser watcher requirement - Add technical details about Vue template rendering issues
476 lines
17 KiB
Markdown
476 lines
17 KiB
Markdown
# ActiveDid Migration Plan - Implementation Guide
|
|
|
|
**Author**: Matthew Raymer
|
|
**Date**: 2025-09-01T05:09:47Z
|
|
**Status**: 🎯 **STABILITY** - Rollback Complete, Ready for Implementation
|
|
|
|
## 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 ✅ READY
|
|
- [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
|
|
|
|
### 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 with strategic logging. Ready for verification and component updates.
|
|
|
|
### Phase 3: Component Updates ❌ BLOCKED
|
|
- [ ] Update 35+ components to use `$getActiveIdentity()`
|
|
- [ ] Replace `this.activeDid = settings.activeDid` pattern
|
|
- [ ] Test each component individually
|
|
|
|
**Status**: Blocked until API layer is complete. 35 components identified via grep search.
|
|
|
|
### Phase 4: Testing ❌ NOT STARTED
|
|
- [ ] Test all platforms (Web, Electron, iOS, Android)
|
|
- [ ] 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<Settings> {
|
|
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<boolean> {
|
|
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):
|
|
const activeIdentity = await this.$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-01T05:09:47Z
|
|
- **Evidence**: `src/db-sql/migration.ts:125` - `003_active_did_separate_table` migration defined
|
|
- **Verify at**: Migration script contains proper table creation and data migration
|
|
|
|
- ✅ **$getActiveIdentity() method exists** in PlatformServiceMixin
|
|
- **Time**: 2025-09-01T05:09:47Z
|
|
- **Evidence**: `src/utils/PlatformServiceMixin.ts:555` - Method implemented with correct return type
|
|
- **Verify at**: Method returns `{ activeDid: string }` as documented
|
|
|
|
- ✅ **Database migration infrastructure** exists and mature
|
|
- **Time**: 2025-09-01T05:09:47Z
|
|
- **Evidence**: `src/db-sql/migration.ts:31` - migration system in place
|
|
- **Verify at**: Existing migration scripts and database versioning
|
|
|
|
## What Doesn't (Evidence & Hypotheses)
|
|
|
|
- ✅ **$accountSettings() updated** with minimal safe change
|
|
- **Time**: 2025-09-01T05:09:47Z
|
|
- **Evidence**: `src/utils/PlatformServiceMixin.ts:875` - Method now prioritizes activeDid from new table
|
|
- **Status**: Maintains all existing complex logic while using new table as primary source
|
|
|
|
- ✅ **$updateActiveDid() dual-write implemented**
|
|
- **Time**: 2025-09-01T05:09:47Z
|
|
- **Evidence**: `src/utils/PlatformServiceMixin.ts:220` - Method now updates both active_identity and settings tables
|
|
- **Status**: Uses MASTER_SETTINGS_KEY constant for proper settings table targeting
|
|
|
|
- ❌ **35 components still use old pattern** `this.activeDid = settings.activeDid`
|
|
- **Time**: 2025-09-01T05:09:47Z
|
|
- **Evidence**: Grep search found 35 instances across views and components
|
|
- **Hypothesis**: Components need updates but are blocked until API layer is ready
|
|
- **Next probe**: Update components after API layer is implemented
|
|
|
|
- ✅ **Clean architecture implemented** - active_identity is now single source of truth
|
|
- **Time**: 2025-09-01T07:09:47Z
|
|
- **Evidence**: Removed dual-write, removed fallback to settings.activeDid
|
|
- **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-01T07:09:47Z
|
|
- **Evidence**: Added migration 004_remove_activeDid_from_settings
|
|
- **Status**: Complete separation of concerns - no more confusing dual-purpose columns
|
|
|
|
## 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<boolean> {
|
|
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 | 🟡 MEDIUM |
|
|
| **Inspect IndexedDB via DevTools** | Verify active_identity table exists and contains data | 🟡 MEDIUM |
|
|
| **Update first component** | One component successfully uses new API pattern | 🟢 LOW |
|
|
| **Systematic component updates** | All 35 components use new API pattern | 🟢 LOW |
|
|
|
|
**Critical Blocker**: API layer complete. Ready to proceed with 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 |