Compare commits

...

211 Commits

Author SHA1 Message Date
6fe4f21ea8 fix: use "starred" instead of "favorite", fix tests 2025-10-05 10:43:05 -06:00
Jose Olarte III
088b9eff7f feat: remove text size class
- Removed `text-sm` so Description row has the same text size as the rest of the Changes table
2025-09-29 16:52:07 +08:00
ee587ac3fc fix: remaining starred-project issues, plus better Error logging and user verbiage 2025-09-28 19:07:48 -06:00
b3112a4086 Merge branch 'star-projects' into star-projects2, bringing star-projects onto master 2025-09-27 14:23:35 -06:00
db4496c57b fix: linting 2025-09-27 13:24:28 -06:00
a51fd90659 feat: add starred project list in search, refactor variable names 2025-09-26 20:13:18 -06:00
0c627f4822 chore: remove old 'master' settings concept outside PlatformServiceMixin 2025-09-25 21:17:38 -06:00
c7276f0b4d Merge pull request 'Copy important settings from previous MASTER settings' (#202) from copy-settings into master
Reviewed-on: #202
2025-09-24 21:29:33 -04:00
d6524cbd43 fix: don't lose the name when running the migration 2025-09-24 19:29:28 -06:00
f5bea24921 fix: linting 2025-09-24 18:51:48 -06:00
46d7fee95e fix: remove settings, too, when deleting an identity 2025-09-24 09:10:21 -06:00
c0f407eb72 chore: remove saveMySettings that depended on an implicit variable 2025-09-22 20:18:38 -06:00
e8e0f315f8 feat: copy important old settings from master record to others 2025-09-22 20:17:56 -06:00
1ea4608f0d feat: remove unused settings DB entries, only uninstall Android on request, bump version to 1.1.1-beta 2025-09-20 21:49:49 -06:00
2dc9b509ce Merge pull request 'fix: load environment-specific .env files in iOS/Android/Electron build scripts' (#201) from load-build-mode-env-file into master
Reviewed-on: #201
2025-09-19 04:35:32 -04:00
f4569d8b98 Merge branch 'master' into load-build-mode-env-file 2025-09-19 04:35:05 -04:00
7575895f75 Merge pull request 'fix: initialize notification helpers in lifecycle methods' (#200) from notify-initialization-fix into master
Reviewed-on: #200
2025-09-19 03:56:49 -04:00
67a9ecf6c6 Merge branch 'master' into notify-initialization-fix 2025-09-19 03:56:24 -04:00
823fa51275 Merge pull request 'feat(NewActivityView): enhance "See all" links to mark offers as read before navigation' (#198) from new-activity-mark-read into master
Reviewed-on: #198
2025-09-19 03:14:23 -04:00
Jose Olarte III
e2c2d54c20 Merge branch 'master' into new-activity-mark-read 2025-09-19 15:14:42 +08:00
Jose Olarte III
6fd53b020e refactor: simplify notification messages for offer viewing
- Remove conditional notification logic in NewActivityView
- Remove redundant notification in RecentOffersToUserView and RecentOffersToUserProjectsView
- Standardize to single notification message format
2025-09-19 15:00:17 +08:00
Jose Olarte III
a3d6b458b1 fix: load environment-specific .env files in iOS/Android/Electron build scripts
- iOS, Android, and Electron build scripts now load .env.development, .env.test, .env.production files
- Previously only loaded generic .env file which doesn't exist
- Ensures consistent image server URL across all build targets
- Fixes issue where build:ios:dev used production image URL instead of test URL
- Aligns with web build script behavior for environment variable precedence

Resolves inconsistent VITE_DEFAULT_IMAGE_API_SERVER values between build targets.
2025-09-18 22:38:53 +08:00
Jose Olarte III
b1fcb49e7c fix: initialize notification helpers in lifecycle methods
- Fix 't is not a function' error during image upload by properly initializing notification helpers
- Move notification helper initialization from class-level to lifecycle methods (created/mounted)
- Affected components: ImageMethodDialog, SeedBackupView, QuickActionBvcBeginView, HelpNotificationsView
- Ensures $notify is available when createNotifyHelpers() is called
- Resolves notification errors in image upload functionality
2025-09-18 21:42:15 +08:00
Matthew Raymer
299762789b docs: remove obsolete migration and planning documents
- Delete active-identity-upgrade-plan.md (390 lines)
- Delete active-pointer-smart-deletion-pattern.md (392 lines)
- Delete activeDid-migration-plan.md (559 lines)
- Delete migration-004-complexity-resolution-plan.md (198 lines)
- Delete verification-party-system-plan.md (375 lines)

These documents were created during migration development phases
and are no longer needed after successful implementation. Removing
them reduces repository clutter and eliminates outdated information.

Total cleanup: 1,914 lines of obsolete documentation removed.
2025-09-18 03:37:56 +00:00
Matthew Raymer
7a961af750 refactor(migration): simplify logging by removing specialized migrationLog
- Remove isDevelopment environment checks and migrationLog variable
- Replace conditional logging with consistent logger.debug() calls
- Remove development-only validation restrictions
- Maintain all error handling and warning messages
- Let existing logger handle development mode behavior automatically

This simplifies the migration service logging while preserving all
functionality. The existing logger already handles development vs
production mode appropriately.
2025-09-18 03:25:54 +00:00
Jose Olarte III
1790a6c5d6 fix: resolve migration 004 transaction and executeSet errors
- Remove explicit transaction wrapping in migration service that caused
  "cannot start a transaction within a transaction" errors
- Fix executeSet method call format to include both statement and values
  properties as required by Capacitor SQLite plugin
- Update CapacitorPlatformService to properly handle multi-statement SQL
  using executeSet for migration SQL blocks
- Ensure migration 004 (active_identity_management) executes atomically
  without nested transaction conflicts
- Remove unnecessary try/catch wrapper

Fixes iOS simulator migration failures where:
- Migration 004 would fail with transaction errors
- executeSet would fail with "Must provide a set as Array of {statement,values}"
- Database initialization would fail after migration errors

Tested on iOS simulator with successful migration completion and
active_identity table creation with proper data migration.
2025-09-17 16:56:10 +08:00
Matthew Raymer
1cbed4d1c2 chore: linting 2025-09-17 06:53:06 +00:00
Matthew Raymer
2f495f6767 feat: minimal stabilization of migration 004 with atomic execution
- Single SQL source: Define MIG_004_SQL constant to eliminate duplicate SQL definitions
- Atomic execution: Add BEGIN IMMEDIATE/COMMIT/ROLLBACK around migration execution
- Name-only check: Skip migrations already recorded in migrations table
- Guarded operations: Replace table-wide cleanups with conditional UPDATE/DELETE

Changes:
- migration.ts: Extract migration 004 SQL into MIG_004_SQL constant
- migration.ts: Use guarded DELETE/UPDATE to prevent accidental data loss
- migrationService.ts: Wrap migration execution in explicit transactions
- migrationService.ts: Reorder checks to prioritize name-only skipping

Benefits:
- Prevents partial migration failures from corrupting database state
- Eliminates SQL duplication and maintenance overhead
- Maintains existing APIs and logging behavior
- Reduces risk of data loss during migration execution

Test results: All migration tests passing, ID generation working correctly
2025-09-17 06:52:43 +00:00
Matthew Raymer
0fae8bbda6 feat: Complete Migration 004 Complexity Resolution (Phases 1-4)
- Phase 1: Simplify Migration Definition 
  * Remove duplicate SQL definitions from migration 004
  * Eliminate recovery logic that could cause duplicate execution
  * Establish single source of truth for migration SQL

- Phase 2: Fix Database Result Handling 
  * Remove DatabaseResult type assumptions from migration code
  * Implement database-agnostic result extraction with extractSingleValue()
  * Normalize results from AbsurdSqlDatabaseService and CapacitorPlatformService

- Phase 3: Ensure Atomic Execution 
  * Remove individual statement execution logic
  * Execute migrations as single atomic SQL blocks only
  * Add explicit rollback instructions and failure cause logging
  * Ensure migration tracking is accurate

- Phase 4: Remove Excessive Debugging 
  * Move detailed logging to development-only mode
  * Preserve essential error logging for production
  * Optimize startup performance by reducing logging overhead
  * Maintain full debugging capability in development

Migration system now follows single-source, atomic execution principle
with improved performance and comprehensive error handling.

Timestamp: 2025-09-17 05:08:05 UTC
2025-09-17 05:08:26 +00:00
297fe3cec6 feat: fix raw results to really show the raw DB results 2025-09-16 21:08:22 -06:00
2a932af806 feat: add ability to see raw SQL results on test page 2025-09-16 20:26:23 -06:00
28cea8f55b fix: add a JSON-parseable field, make small data tweaks, and add commentary on JSON fields 2025-09-16 19:54:11 -06:00
Jose Olarte III
f31a76b816 fix: resolve iOS migration 004 failure with enhanced error handling
- Fix multi-statement SQL execution issue in Capacitor SQLite
- Add individual statement execution for migration 004_active_identity_management
- Implement automatic recovery for missing active_identity table
- Enhance migration system with better error handling and logging

Problem:
Migration 004 was marked as applied but active_identity table wasn't created
due to multi-statement SQL execution failing silently in Capacitor SQLite.

Solution:
- Extended Migration interface with optional statements array
- Modified migration execution to handle individual statements
- Added bootstrapping hook recovery for missing tables
- Enhanced logging for better debugging

Files changed:
- src/services/migrationService.ts: Enhanced migration execution logic
- src/db-sql/migration.ts: Added recovery mechanism and individual statements

This fix ensures the app automatically recovers from the current broken state
and prevents similar issues in future migrations.
2025-09-16 20:14:58 +08:00
Jose Olarte III
5d9f455fc8 feat: move mark-as-read logic from navigation to view loading
- Remove mark-as-read logic from NewActivityView navigation handlers
- Add mark-as-read logic to RecentOffersToUserView and RecentOffersToUserProjectsView after data loading
- Improve "You've already seen all the following" marker positioning
- Update marker styling with dashed border and centered text

This ensures the marker appears at the correct position in the list
instead of always at the top, providing better UX when viewing offers.
2025-09-16 18:10:17 +08:00
Matthew Raymer
afe0f5e019 Merge branch 'master' into active_did_redux 2025-09-16 08:22:59 +00:00
Matthew Raymer
e0e8af3fff report: areas we may want to improve 2025-09-16 08:21:57 +00:00
c3ff471ea1 Merge branch 'master' into new-activity-mark-read 2025-09-16 04:19:17 -04:00
0072db1595 Merge pull request 'fix(ios): resolve clipboard and notification issues in ContactQRScanFullView' (#199) from ios-qr-code-copy into master
Reviewed-on: #199
2025-09-16 04:15:11 -04:00
Matthew Raymer
24ec81b0ba refactor: consolidate active identity migrations 004-006 into single migration
- Consolidate migrations 004, 005, and 006 into single 004_active_identity_management
- Remove redundant migrations 005 (constraint_fix) and 006 (settings_cleanup)
- Implement security-first approach with ON DELETE RESTRICT constraint from start
- Include comprehensive data migration from settings.activeDid to active_identity.activeDid
- Add proper cleanup of orphaned settings records and legacy activeDid values
- Update migrationService.ts validation logic to reflect consolidated structure
- Fix migration name references and enhance validation for hasBackedUpSeed column
- Reduce migration complexity from 3 separate operations to 1 atomic operation
- Maintain data integrity with foreign key constraints and performance indexes

Migration successfully tested on web platform with no data loss or corruption.
Active DID properly migrated: did:ethr:0xCA26A3959D32D2eB5459cE08203DbC4e62e79F5D

Files changed:
- src/db-sql/migration.ts: Consolidated 3 migrations into 1 (-46 lines)
- src/services/migrationService.ts: Updated validation logic (+13 lines)
2025-09-16 03:20:33 +00:00
Matthew Raymer
2c439ef439 experiment: setting up emulators 2025-09-15 10:14:09 +00:00
Matthew Raymer
0ca70b0f4e feat: complete Active Pointer + Smart Deletion Pattern implementation
- Add Migration 006: Settings cleanup to remove orphaned records
- Remove orphaned settings records (accountDid=null)
- Clear legacy activeDid values from settings table
- Update documentation with current state analysis and compliance metrics
- Achieve 100% compliance with Active Pointer + Smart Deletion Pattern

Security Impact: COMPLETE - All critical vulnerabilities fixed
Migrations: 005 (constraint fix) + 006 (settings cleanup)
Pattern Compliance: 6/6 components (100%)

Performance: All migrations execute instantly with no delays
Architecture: Complete separation of identity management vs user settings

Author: Matthew Raymer
2025-09-15 07:38:22 +00:00
Matthew Raymer
d01c6c2e9b feat: implement Migration 005 - fix foreign key constraint to ON DELETE RESTRICT
- Add Migration 005 to fix critical security vulnerability
- Change foreign key constraint from ON DELETE SET NULL to ON DELETE RESTRICT
- Prevents accidental account deletion through database constraints
- Update Active Pointer pattern documentation with current state analysis
- Achieve 83% compliance with Active Pointer + Smart Deletion Pattern

Security Impact: HIGH - Fixes critical data loss vulnerability
Migration: 005_active_identity_constraint_fix
Pattern Compliance: 5/6 components (83%)

Author: Matthew Raymer
2025-09-15 07:24:17 +00:00
Matthew Raymer
2b3c83c21c Merge branch 'active_did_redux' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into active_did_redux 2025-09-15 06:45:19 +00:00
Matthew Raymer
8b8566c578 fix: resolve build errors and test timing issues
- Fix syntax error in logger.ts: change 'typeof import' to 'typeof import.meta'
  to resolve ESBuild compilation error preventing web build

- Align CapacitorPlatformService.insertNewDidIntoSettings with WebPlatformService:
  * Add dynamic constants import to avoid circular dependencies
  * Use INSERT OR REPLACE for data integrity
  * Set proper default values (finishedOnboarding=false, API servers)
  * Remove TODO comment as implementation is now parallel

- Fix Playwright test timing issues in 60-new-activity.spec.ts:
  * Replace generic alert selectors with specific alert type targeting
  * Change Info alerts from 'Success' to 'Info' filter for proper targeting
  * Fix "strict mode violation" errors caused by multiple simultaneous alerts
  * Improve test reliability by using established alert handling patterns

- Update migrationService.ts and vite.config.common.mts with related improvements

Test Results: Improved from 2 failed tests to 42/44 passing (95.5% success rate)
Build Status: Web build now compiles successfully without syntax errors
2025-09-15 06:44:37 +00:00
a1e2d635f7 chore: switch more debug logging to debug 2025-09-14 17:46:18 -06:00
f371ce88a0 chore: remove extra code & logging & error messages, fix quick-start documentation 2025-09-14 17:25:11 -06:00
Matthew Raymer
69e29ecf85 fix: had to remove a select from migration for Android to migrate. 2025-09-12 08:57:41 +00:00
Matthew Raymer
23b97d483d Android testing 2025-09-12 08:19:42 +00:00
Jose Olarte III
4c218c4786 feat: migrate all clipboard operations from useClipboard to ClipboardService
- Replace useClipboard with platform-agnostic ClipboardService across 13 files
- Add proper error handling with user notifications for all clipboard operations
- Fix naming conflicts between method names and imported function names
- Ensure consistent async/await patterns throughout the codebase
- Add notification system to HelpView.vue for user feedback on clipboard errors
- Remove unnecessary wrapper methods for cleaner code

Files migrated:
- View components: UserProfileView, QuickActionBvcEndView, ProjectViewView,
  InviteOneView, SeedBackupView, HelpView, AccountViewView, DatabaseMigration,
  ConfirmGiftView, ClaimView, OnboardMeetingSetupView
- Utility functions: libs/util.ts (doCopyTwoSecRedo)
- Components: HiddenDidDialog

Naming conflicts resolved:
- DatabaseMigration: copyToClipboard() → copyExportedDataToClipboard()
- ShareMyContactInfoView: copyToClipboard() → copyContactMessageToClipboard() → removed
- HiddenDidDialog: copyToClipboard() → copyTextToClipboard()
- ClaimView: copyToClipboard() → copyTextToClipboard()
- ConfirmGiftView: copyToClipboard() → copyTextToClipboard()

This migration ensures reliable clipboard functionality across iOS, Android,
and web platforms with proper error handling and user feedback.

Closes: Platform-specific clipboard issues on mobile devices
2025-09-12 14:33:09 +08:00
Matthew Raymer
31f66909fa refactor: implement team feedback for active identity migration structure
- Update migration 003 to match master deployment (hasBackedUpSeed)
- Rename migration 004 for active_identity table creation
- Update migration service validation for new structure
- Fix TypeScript compatibility issue in migration.ts
- Streamline active identity upgrade plan documentation
- Ensure all migrations are additional per team guidance

Migration structure now follows "additional migrations only" principle:
- 003: hasBackedUpSeed (assumes master deployment)
- 004: active_identity table with data migration
- iOS/Android compatibility confirmed with SQLCipher 4.9.0

Files: migration.ts, migrationService.ts, active-identity-upgrade-plan.md
2025-09-11 13:08:37 +00:00
Jose Olarte III
7917e707e9 fix: resolve iOS database migration failure for active_identity table
The migration 003_active_identity_and_seed_backup was failing on iOS when
switching from master to active_did_redux branch because it attempted to
execute multiple SQL operations in a single block. When the ALTER TABLE
statement for hasBackedUpSeed failed (due to column already existing),
the entire migration was marked as "already applied" even though the
active_identity table was never created.

Changes:
- Split migration 003 into two separate migrations:
  - 003_active_identity_and_seed_backup: Creates active_identity table
  - 003b_add_hasBackedUpSeed_to_settings: Adds hasBackedUpSeed column
- Added data migration logic to copy existing activeDid from settings
  to active_identity table during migration
- Added debug logging to track migration results
- Ensured atomic operations so table creation doesn't depend on column addition

This fix ensures that:
- The active_identity table is always created successfully
- Existing activeDid values are preserved during migration
- The app remembers the active identity between master and active_did_redux builds
- Migration errors are handled gracefully without affecting other operations

Fixes iOS migration issue where app would lose active identity state
when switching between branches, causing users to lose their selected
identity and requiring manual re-selection.

Tested: Migration now works correctly on iOS simulator when switching
from master branch (with old schema) to active_did_redux branch.
2025-09-11 17:39:26 +08:00
Matthew Raymer
a9fe862dda chore: possible upgrade 2025-09-11 07:21:45 +00:00
Matthew Raymer
79b2f9a273 chore: lagging file 2025-09-11 06:14:38 +00:00
Matthew Raymer
cf854d5054 refactor: clean up $getActiveIdentity method and fix null handling
- Remove excessive debug logging statements
- Fix critical bug: cast activeDid as string | null instead of string
- Refactor to use early return pattern, reducing nesting from 4 to 2-3 levels
- Eliminate redundant logic and improve code readability
- Maintain all original functionality while simplifying flow
- Fix null activeDid case that was breaking app initialization
2025-09-11 06:14:07 +00:00
Matthew Raymer
8eb4ad5c74 refactor: Remove defunct $needsActiveIdentitySelection method and fix activeDid consistency
## Changes Made

### Code Cleanup
- Remove unused $needsActiveIdentitySelection() method (36 lines)
- Remove method signature from IPlatformServiceMixin interface
- Remove method signature from Vue module declaration

### Database Consistency Fix
- Change activeDid clearing from empty string ('') to NULL for consistency
- Ensures proper foreign key constraint compatibility
- Maintains API compatibility by still returning empty string to components

## Impact
- Reduces codebase complexity by removing unused functionality
- Improves database integrity with consistent NULL usage
- No breaking changes to component APIs
- Migration and auto-selection now handle all identity management

## Files Changed
- src/utils/PlatformServiceMixin.ts: -42 lines, +1 line
2025-09-11 05:37:40 +00:00
Matthew Raymer
eb77547ba1 chore: a couple missed files 2025-09-11 05:07:52 +00:00
Matthew Raymer
616bef655a fix: Resolve database migration issues and consolidate 003 migrations
- Fix $getActiveIdentity() logic flow preventing false "empty table" warnings
- Implement auto-selection of first account when activeDid is null after migration
- Consolidate 003 and 003b migrations into single 003 migration
- Re-introduce foreign key constraint for activeDid referential integrity
- Add comprehensive debug logging for migration troubleshooting
- Remove 003b validation logic and update migration name mapping

Fixes migration from master to active_did_redux branch and ensures system
always has valid activeDid for proper functionality.
2025-09-11 05:07:23 +00:00
Matthew Raymer
6da9e14b8a feat: complete ActiveDid migration for remaining Vue components
Replace `settings.activeDid` with `$getActiveIdentity()` API call across 8 components.
All Vue components now use the new active_identity table pattern.

Components migrated:
- TestView.vue: Update logging to use new pattern consistently
- ShareMyContactInfoView.vue: Refactor retrieveAccount method signature
- UserNameDialog.vue: Update user settings save logic
- SeedBackupView.vue: Update both created() and revealSeed() methods
- HelpView.vue: Update onboarding reset functionality
- ImportAccountView.vue: Update post-import settings check
- NewEditAccountView.vue: Update account save logic
- QuickActionBvcBeginView.vue: Update BVC recording functionality

 TypeScript compilation passes
 Linting standards met
 Functionality preserved across all components

Part of ActiveDid migration following "One Component + Test Pattern".
All Vue components now use centralized active_identity table.
2025-09-11 02:01:39 +00:00
Matthew Raymer
e856ace61f feat: migrate DiscoverView.vue to use new ActiveDid pattern
Replace `settings.activeDid` with `$getActiveIdentity()` API call.
Updates discover search functionality to use active_identity table.

- Add `const activeIdentity = await this.$getActiveIdentity();`
- Update activeDid assignment in mounted() lifecycle
- Maintain existing search and discovery functionality

 TypeScript compilation passes
 Linting standards met
 Functionality preserved

Part of ActiveDid migration following "One Component + Test Pattern".
2025-09-11 01:43:10 +00:00
855448d07a feat: add a page to see all the starred projects 2025-09-10 14:40:36 -06:00
Matthew Raymer
5da1591ad8 Merge branch 'active_did_redux' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into active_did_redux 2025-09-10 13:21:51 +00:00
Matthew Raymer
b06e2b46f6 feat: migrate TopMessage.vue to use new ActiveDid pattern
Replace `settings.activeDid` with `$getActiveIdentity()` API call.
Updates test/prod server warning logic to use active_identity table.

- Add `const activeIdentity = await this.$getActiveIdentity();`
- Update didPrefix extraction for both warning conditions
- Maintain existing warning functionality

 TypeScript compilation passes
 Linting standards met
 Functionality preserved

Part of ActiveDid migration following "One Component + Test Pattern".
2025-09-10 13:21:34 +00:00
626071281f feat: make the default 'create identifier' be from a new seed 2025-09-10 07:04:58 -06:00
Jose Olarte III
5fc5b958af fix(ios): resolve clipboard and notification issues in ContactQRScanFullView
- Replace useClipboard() with ClipboardService for iOS compatibility
- Fix notification helper initialization timing issue
- Add proper error handling for clipboard operations
- Ensure consistent behavior across all platforms

Fixes clipboard copy functionality on iOS builds where QR code clicks
failed to copy content and showed notification errors. The ClipboardService
provides platform-specific handling using Capacitor's clipboard plugin,
while moving notification initialization to created() lifecycle hook
prevents undefined function errors.

Resolves: iOS clipboard copy failure and notification system errors
2025-09-10 21:04:36 +08:00
69c922284e fix: remove the 'migrations' table creation that is done elsewhere 2025-09-10 06:53:35 -06:00
Jose Olarte III
ac603f66e2 Lint fix 2025-09-10 18:19:40 +08:00
Jose Olarte III
9bdd66b9c9 feat(NewActivityView): enhance "See all" links to mark offers as read before navigation
- Replace router-links with click handlers for both "See all" offers links
- Add handleSeeAllOffersToUser and handleSeeAllOffersToUserProjects methods
- Modify expandOffersToUserAndMarkRead to accept fromSeeAll parameter for contextual notifications
- Modify expandOffersToUserProjectsAndMarkRead to accept fromSeeAll parameter for contextual notifications
- Show shorter notification messages when called from "See all" vs chevron expand buttons
- Add safety checks to prevent errors when offers arrays are empty
- Standardize notification message text consistency
- TypeScript and formatting lint fixes

Both "See all" links now properly mark offers as viewed before navigation,
preventing users from seeing unread offers in the detailed views.
2025-09-10 18:19:17 +08:00
Jose Olarte III
6fb4ceab81 fix(playwright): re-route after affirming onboarding dialog
After calling OnboardingDialog from ProjectsView, route back to projects page again

The onboarding dialog was designed to route back to HomeView when called from ProjectsView. The tests need to be updated to account for this intended behavior.
2025-09-09 15:57:36 +08:00
Jose Olarte III
7b40012df4 fix: implement missing $getAllAccountDids method in PlatformServiceMixin
- Add $getAllAccountDids() implementation to resolve TypeError in ProjectsView
- Method queries accounts table and returns array of DIDs
- Includes proper error handling and logging
- Fixes "this.$getAllAccountDids is not a function" console error on /projects route

The method was declared in TypeScript interfaces but never implemented,
causing runtime errors when ProjectsView tried to initialize user identities.
2025-09-09 15:42:39 +08:00
Matthew Raymer
79cb52419e fix(tests): improve Playwright test reliability with robust onboarding and timing fixes
- Fix onboarding dialog handling in project creation tests
  * Replace blocking onboarding dismissal with try-catch approach
  * Use short timeout (2000ms) to detect dialog presence
  * Gracefully handle missing onboarding dialogs on projects page
  * Add console logging for debugging dialog state

- Improve project creation timing and synchronization
  * Add networkidle wait after project save operation
  * Add networkidle wait before project list search
  * Increase timeout for project visibility check (10s)
  * Add debug logging to show all projects in list

- Apply consistent pattern across both test files
  * 20-create-project.spec.ts: Enhanced with timing fixes
  * 25-create-project-x10.spec.ts: Applied onboarding fix

These changes resolve test failures caused by UI timing issues
and onboarding dialog state variability, improving test reliability
from 42/44 passing to expected 44/44 passing tests.
2025-09-09 06:44:06 +00:00
Matthew Raymer
d6b5e13499 fix(tests): resolve dialog button selector issues in Playwright tests
- Fix 50-record-offer.spec.ts multiple alert button conflict
  * Replace generic alert selector with success-specific selector
  * Use getByRole('alert').filter({ hasText: 'Success' }) pattern

- Fix 20-create-project.spec.ts onboarding dialog timeout
  * Replace unreliable div > svg.fa-xmark selector
  * Use established closeOnboardingAndFinish testId pattern
  * Add waitForFunction to ensure dialog dismissal

- Fix 25-create-project-x10.spec.ts onboarding dialog timeout
  * Apply same onboarding dismissal pattern as other tests
  * Ensure consistent dialog handling across test suite

These fixes use established patterns from working tests to resolve
6 failing tests caused by UI selector conflicts and timing issues.
2025-09-09 06:33:51 +00:00
Matthew Raymer
61117a0f03 Merge branch 'active_did_redux' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into active_did_redux 2025-09-09 06:14:26 +00:00
Matthew Raymer
e1cf27be05 refactor(db): restructure migrations to preserve master compatibility
- Split consolidated migration into 3 separate migrations
- Preserve master's 001_initial and 002_add_iViewContent structure
- Move active_identity creation to new 003_active_identity_and_seed_backup
- Add hasBackedUpSeed field from registration-prompt-parity branch
- Remove activeDid performance index for simplified migration
- Maintain foreign key constraints and smart deletion pattern
- Remove unused $getAllAccountDids method from PlatformServiceMixin

This restructure ensures backward compatibility with master branch
while adding advanced features (active_identity table, seed backup
tracking) in a clean, maintainable migration sequence.
2025-09-09 06:13:25 +00:00
Matthew Raymer
ccb1f29df4 fix: improve type safety and fix Playwright test dialog handling
**Type Safety Improvements:**
- Replace `unknown[]` with proper `SqlValue[]` type in database query methods
- Add `SqlValue` import to PlatformServiceMixin.ts for better type definitions
- Update interface definitions for `$dbGetOneRow` and `$one` methods
- Fix database row mapping to use `Array<SqlValue>` instead of `unknown[]`

**Test Reliability Fix:**
- Add backup seed modal handling to 60-new-activity.spec.ts
- Follow established dialog handling pattern from 00-noid-tests.spec.ts
- Use `waitForFunction` to detect backup seed modal appearance
- Gracefully handle modal dismissal with "No, Remind me Later" button
- Add error handling for cases where backup modal doesn't appear

**Files Changed:**
- src/utils/PlatformServiceMixin.ts: Enhanced type safety for database operations
- test-playwright/60-new-activity.spec.ts: Fixed dialog interception causing test failures

**Impact:**
- Eliminates TypeScript linting errors for database query types
- Resolves Playwright test timeout caused by backup seed modal blocking clicks
- Improves test reliability by following established dialog handling patterns
- Maintains backward compatibility while enhancing type safety

**Testing:**
- TypeScript compilation passes without errors
- Linting checks pass with improved type definitions
- Playwright test now handles backup seed modal properly
2025-09-08 12:03:15 +00:00
Matthew Raymer
f55ef85981 fix: from merge 2025-09-08 11:38:51 +00:00
Matthew Raymer
d9569922eb docs: merge notification system docs into single Native-First guide
- Consolidate 5 notification-system-* files into doc/notification-system.md
- Add web-push cleanup guide and Start-on-Login glossary entry
- Configure markdownlint for consistent formatting
- Remove web-push references, focus on native OS scheduling

Reduces maintenance overhead while preserving all essential information
in a single, well-formatted reference document.
2025-09-08 11:36:59 +00:00
8815f36596 Merge pull request 'refactor: modernize registration prompt notification in ContactQRScanShowView' (#197) from registration-prompt-parity into master
Reviewed-on: #197
2025-09-08 04:38:22 -04:00
631aa468e6 Merge branch 'master' into registration-prompt-parity 2025-09-08 04:37:54 -04:00
ee29b517ce Merge pull request 'feat: implement seed phrase backup reminder system' (#195) from seed-phrase-backup-prompt into master
Reviewed-on: #195
2025-09-08 04:37:33 -04:00
f34c567ab4 Merge branch 'master' into seed-phrase-backup-prompt 2025-09-08 04:37:23 -04:00
bd072d95eb Merge pull request 'Fix offer fulfillment detection + consistencies between ClaimView and ConfirmGiftView' (#167) from claimview-fullfills-offer into master
Reviewed-on: #167
2025-09-08 04:37:03 -04:00
030960dd59 Merge branch 'master' into claimview-fullfills-offer 2025-09-08 04:36:48 -04:00
Matthew Raymer
72872935ae Merge branch 'master' into active_did_redux 2025-09-08 06:32:45 +00:00
b138441d10 chore: change logging level to debug for debug messages 2025-09-07 18:34:57 -06:00
Matthew Raymer
a20c321a16 feat: implement Active Pointer + Smart Deletion Pattern for accounts
- Consolidate migrations: merge 002/003 into 001_initial with UNIQUE did constraint
- Add foreign key: active_identity.activeDid REFERENCES accounts.did ON DELETE RESTRICT
- Replace empty string defaults with NULL for proper empty state handling
- Implement atomic smart deletion with auto-switch logic in IdentitySwitcherView
- Add DAL methods: $getAllAccountDids, $getActiveDid, $setActiveDid, $pickNextAccountDid
- Add migration bootstrapping to auto-select first account if none selected
- Block deletion of last remaining account with user notification

Refs: doc/active-pointer-smart-deletion-pattern.md
2025-09-07 10:30:48 +00:00
c9cfeafd50 fix: change non-existent 'mirror' icon to 'circle-user' 2025-09-05 20:02:44 -06:00
52b1e8ffa3 chore: move more logger infos to debugs 2025-09-05 19:52:53 -06:00
Jose Olarte III
ca1190aa47 refactor: modernize registration prompt notification in ContactQRScanShowView
- Replace deprecated notify.confirm() with modern $notify() API
- Add structured notification object with group, type, and title properties
- Extract registration prompt response logic into reusable handleRegistrationPromptResponse method
- Update settings method from $updateSettings to $saveSettings for consistency
- Align implementation with ContactsView.vue for better code consistency

This brings the registration prompt notification in ContactQRScanShowView up to parity with the modern implementation used in ContactsView.
2025-09-05 21:39:47 +08:00
Jose Olarte III
448d8a68d2 fix: improve code formatting in migrationService.ts
- Fix line breaks and indentation for long SQL queries
- Improve readability of error message formatting
- Remove trailing whitespace and standardize spacing
- Apply consistent formatting to active_identity table validation logic
2025-09-05 17:51:32 +08:00
Jose Olarte III
578dbe6177 fix: simplify active_identity migration to resolve iOS SQLite failures
The complex table rewrite approach in migration 003_active_did_separation was
failing on iOS SQLite, causing "no such table: active_identity" errors. The
migration was being marked as applied despite validation failures.

Changes:
- Simplify migration SQL to only create active_identity table and migrate data
- Remove complex table rewrite that was failing on iOS SQLite versions
- Remove foreign key constraint that could cause compatibility issues
- Update validation logic to focus on active_identity table existence only
- Remove validation check for activeDid column removal from settings table

This approach is more reliable across different SQLite versions and platforms
while maintaining the core functionality of separating activeDid into its own
table for better database architecture.

Fixes iOS build database errors and ensures migration completes successfully.
2025-09-05 17:48:15 +08:00
Matthew Raymer
704e495f5d refactor(db): consolidate database migrations from 6 to 3
- Add missing migrations table creation to 001_initial migration
- Consolidate migrations 003-006 into single 003_active_did_separation migration
- Rename migration for better clarity and logical grouping
- Preserve all original SQL operations and data integrity constraints
- Reduce migration complexity while maintaining functionality

This consolidation improves maintainability by grouping related schema changes
into logical atomic operations, reducing the total migration count by 50%.
2025-09-05 04:57:11 +00:00
Matthew Raymer
04178bf9f8 style: fix HomeView.vue formatting from linter 2025-09-05 04:03:12 +00:00
Matthew Raymer
b57be7670c refactor: improve logging levels and environment configuration
- Fix logging levels: change verbose debugging from info to debug level
  - TestView: component mounting, boot-time config, URL flow testing
  - main.capacitor.ts: deeplink processing steps and router state
  - HomeView: API call details, component state updates, template rendering

- Remove redundant environment variable override in vite.config.common.mts
  - Environment loading via dotenv works correctly
  - Manual override was defensive programming but unnecessary
  - Simplifies configuration and reduces maintenance burden

- Add comprehensive Playwright timeout behavior documentation
  - README.md: detailed timeout types, failure behavior, debugging guide
  - TESTING.md: timeout failure troubleshooting and common scenarios
  - Clarifies that timeout failures indicate real issues, not flaky tests

- Fix TypeScript configuration for .mts imports
  - tsconfig.node.json: add allowImportingTsExtensions for Vite config files
  - Resolves import path linting errors for .mts extensions

All changes maintain existing functionality while improving code quality
and reducing log noise in production environments.
2025-09-05 04:02:53 +00:00
Matthew Raymer
10a1f435ed fix(platform): remove auto-fix identity selection and fix feed loading race condition
- Remove problematic $ensureActiveIdentityPopulated() that auto-selected identities
- Add user-friendly $needsActiveIdentitySelection() and $getAvailableAccountDids() methods
- Fix missing updateActiveDid implementation in CapacitorPlatformService
- Resolve race condition in HomeView initialization causing feed loading failures
- Improve TypeScript error handling in ContactsView invite processing

Addresses team concerns about data consistency and user control for identity selection.
2025-09-04 10:36:50 +00:00
Matthew Raymer
720be1aa4d Merge branch 'master' into active_did_redux 2025-09-04 07:43:42 +00:00
Matthew Raymer
4c761d8fd5 feat(db)!: complete ActiveDid migration to active_identity table
Migrate all 34 Vue components from settings.activeDid to $getActiveIdentity()
pattern. This completes the database architecture improvement that separates
identity selection from user preferences and prevents data corruption.

- Replace this.activeDid = settings.activeDid with $getActiveIdentity() calls
- Add ESLint ignore comments for TypeScript type assertions
- Update migration plan documentation to reflect completion
- All components tested with passing results

BREAKING CHANGE: Components now use active_identity table as single source
of truth for activeDid values instead of settings table
2025-09-04 07:28:26 +00:00
de45e83ffb Merge pull request 'test: add a check that the newly invited person can indeed log a claim' (#194) from invited-check into master
Reviewed-on: #194

https://app.clickup.com/t/86b6gc7ag
2025-09-03 22:06:36 -04:00
Jose Olarte III
f38ec1daff feat: implement seed phrase backup reminder modal
Add comprehensive seed phrase backup reminder system to encourage users
to secure their identity after creating content.

Core Features:
- Modal dialog with "Backup Identifier Seed" and "Remind me Later" options
- 24-hour localStorage cooldown to prevent notification fatigue
- 1-second delay after success messages for better UX flow
- Focuses on claim creation actions, not confirmations

New Files:
- src/utils/seedPhraseReminder.ts: Core utility for reminder logic
- doc/seed-phrase-reminder-implementation.md: Comprehensive documentation

Trigger Points Added:
- Profile saving (AccountViewView)
- Claim creation (ClaimAddRawView, GiftedDialog, GiftedDetailsView)
- Offer creation (OfferDialog)
- QR code view exit (ContactQRScanFullView, ContactQRScanShowView)

Technical Implementation:
- Uses existing notification group modal system from App.vue
- Integrates with PlatformServiceMixin for account settings access
- Graceful error handling with logging fallbacks
- Non-blocking implementation that doesn't affect main functionality
- Modal stays open indefinitely (timeout: -1) until user interaction

User Experience:
- Non-intrusive reminders that respect user preferences
- Clear call-to-action for security-conscious users
- Seamless integration with existing workflows
- Maintains focus on content creation rather than confirmation actions
2025-09-03 19:50:29 +08:00
Jose Olarte III
ec2cab768b feat: Add seed backup tracking with database migration
- Add hasBackedUpSeed boolean flag to Settings interface
- Create database migration 003_add_hasBackedUpSeed_to_settings
- Update SeedBackupView to set flag when user reveals seed phrase
- Modify DataExportSection to conditionally show notification dot
- Implement robust error handling for database operations

The notification dot on the "Backup Identifier Seed" button only
appears while the user hasn't backed up their seed phrase. Once they
visit SeedBackupView and click "Reveal my Seed Phrase", the setting
is persisted and the notification dot disappears.
2025-09-03 15:52:29 +08:00
Matthew Raymer
4cb1d8848f migrate: PhotoDialog.vue to use () API
- Replace settings.activeDid with () pattern
- Maintains backward compatibility with existing functionality
- Component now uses active_identity table as single source of truth
- Part of ActiveDid migration (2/32 components completed)
- Updated migration plan to include lint-fix step
2025-09-03 07:48:55 +00:00
Matthew Raymer
3e03aaf1e8 migrate: OfferDialog.vue to use () API
- Replace settings.activeDid with () pattern
- Maintains backward compatibility with existing functionality
- Component now uses active_identity table as single source of truth
- Part of ActiveDid migration (1/32 components completed)
2025-09-03 07:45:58 +00:00
Matthew Raymer
9ae9bed8a9 doc: update status of migration 2025-09-03 06:45:59 +00:00
Matthew Raymer
b2536adc4e feat: stabilize Playwright tests after ActiveDid migration
- Fix dialog overlay handling across multiple test files
- Implement adaptive timeouts and retry logic for load resilience
- Add robust activity feed verification in gift recording tests
- Resolve Vue reactivity issues with proper type assertions
- Achieve 98% test success rate (88/90 tests passing across 3 runs)

The test suite now passes consistently under normal conditions with only
intermittent load-related timeouts remaining.
2025-09-03 06:34:14 +00:00
Matthew Raymer
22d6b08623 Merge branch 'master' into active_did_redux 2025-09-03 03:43:53 +00:00
ba587471f9 doc: update the in-app help doc 2025-09-02 19:11:50 -06:00
Matthew Raymer
61703930f3 chore: dog walk 2025-09-02 10:30:01 +00:00
Matthew Raymer
4c96a234e3 chore: take the dog for a walk 2025-09-02 10:28:47 +00:00
Matthew Raymer
1a5aa7a5ef docs: update development rules and documentation
- Update cursor rules for improved development workflow
- Add guidelines for ActiveDid migration process
- Update version control workflow documentation
- Improve development process documentation
2025-09-02 10:27:03 +00:00
Matthew Raymer
aa49a5d8a4 chore: update utilities and configuration for ActiveDid migration
- Update utility functions for new active_identity table structure
- Modify Vite configuration for improved build process
- Add support for new API endpoints and data structures
2025-09-02 10:26:45 +00:00
Matthew Raymer
2db4f8f894 refactor: update components for ActiveDid migration compatibility
- Update all components to use new active_identity API methods
- Ensure consistent activeDid retrieval across all views
- Add proper error handling for activeDid migration
- Update component interfaces for new API structure
2025-09-02 10:24:02 +00:00
Matthew Raymer
552de23ef2 refactor: enhance platform service for ActiveDid migration
- Update PlatformServiceMixin interface to include $getActiveIdentity
- Improve apiServer default handling across all platforms
- Add better error handling for platform service methods
- Ensure consistent behavior across web and electron platforms
2025-09-02 10:23:06 +00:00
Matthew Raymer
2b423b8d7b fix: resolve Playwright test flakiness with robust dialog handling
- Implement comprehensive dialog overlay handling for all test files
- Add robust page state checking for Firefox navigation issues
- Fix alert button timing issues with combined find/click approach
- Add force close dialog overlay as fallback for persistent dialogs
- Handle page close scenarios during dialog dismissal
- Add page readiness checks before interactions
- Resolve race conditions between dialog close and page navigation
- Achieve consistent 40/40 test runs with systematic fixes
2025-09-02 10:22:23 +00:00
Matthew Raymer
8024688561 docs: document critical Vue reactivity bug and migration progress
- 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
2025-09-02 10:21:52 +00:00
Matthew Raymer
b374f2e5a1 feat: implement ActiveDid migration to active_identity table
- Add $getActiveIdentity() method to PlatformServiceMixin interface
- Update HomeView.vue to use new active_identity API methods
- Update ContactsView.vue to use new active_identity API methods
- Fix apiServer default handling in PlatformServiceMixin
- Ensure DEFAULT_ENDORSER_API_SERVER is used when apiServer is empty
- Add comprehensive logging for debugging ActiveDid migration
- Resolve TypeScript interface issues with Vue mixins
2025-09-02 10:20:54 +00:00
9f1495e185 feat: alloww markdown in the descriptions and render them appropriately 2025-09-01 18:40:35 -06:00
f61cb6eea7 fix: on project changes, truncate the description properly (to avoid screen zooming in) and widen the table 2025-09-01 14:53:39 -06:00
2f05d27b51 Merge pull request 'fix: clean up "register random person" test' (#190) from playwright-test-00-fix into master
Reviewed-on: #190
2025-09-01 10:00:15 -04:00
40c8189c51 Merge pull request 'feat: add duplicate account import prevention' (#189) from account-import-duplicate-prevention into master
Reviewed-on: #189
2025-09-01 09:58:49 -04:00
cd7755979f Merge pull request 'enhance the verbiage & display for bulk confirmations & visibility' (#183) from better-confirms into master
Reviewed-on: #183
2025-09-01 09:56:14 -04:00
4fa8c8f4cb test: add a check that the newly invited person can indeed log a claim 2025-09-01 07:51:02 -06:00
Jose Olarte III
1eeb013638 refactor(claims): extract offer fulfillment logic to utility function
Created extractOfferFulfillment utility in libs/util.ts to handle both
array and single object cases for fulfills field. Updated ClaimView and
ConfirmGiftView to use the shared utility, eliminating code duplication
and improving maintainability.
2025-09-01 21:24:46 +08:00
Jose Olarte III
3e5e2cd0bb fix(claims): handle single Offer object in fulfills field for ConfirmGiftView
Updated extractOfferFulfillment to support both array and single object
cases for the fulfills field, matching the fix applied to ClaimView.
Now handles when fulfills contains a single Offer object with @type "Offer".
2025-09-01 21:18:08 +08:00
Jose Olarte III
d87f44b75d fix(claims): handle single Offer object in fulfills field
Updated extractOfferFulfillment to support both array and single object
cases for the fulfills field. Previously only handled array format,
now also checks if fulfills is a single Offer object with @type "Offer".
2025-09-01 21:06:48 +08:00
2c7cb9333e chore: remove error logging for errors that are propagated 2025-09-01 06:59:36 -06:00
fa8956fb38 chore: explicitly share error message used for logic 2025-09-01 06:42:00 -06:00
Jose Olarte III
1499211018 refactor: simplify duplicate account error detection
Replace dual string check with single unique identifier for more precise error handling
2025-09-01 20:03:17 +08:00
Jose Olarte III
25e37cc415 refactor: consolidate duplicate account checking logic into unified utility
- Extract checkForDuplicateAccount methods from ImportAccountView and ImportDerivedAccountView
- Create unified utility function in src/libs/util.ts with TypeScript overloads
- Support both direct DID checking and mnemonic+derivation path checking
- Improve error handling with centralized logging via PlatformServiceFactory
- Add comprehensive JSDoc documentation for both function overloads
- Remove unused imports (deriveAddress, newIdentifier) from ImportAccountView

The utility function now provides a clean API:
- checkForDuplicateAccount(did) - for direct DID checking
- checkForDuplicateAccount(mnemonic, derivationPath) - for derivation + checking

Both components maintain identical functionality while using centralized logic.
2025-09-01 19:36:01 +08:00
Jose Olarte III
d339f1a274 chore: remove generated doc
- Generated document reads more like a log, and does not contribute to actual documentation of app
2025-09-01 19:33:18 +08:00
Jose Olarte III
c2e7531554 Merge branch 'master' into account-import-duplicate-prevention 2025-09-01 18:06:36 +08:00
aa64f426f3 Merge pull request 'feat(electron): add editMenu to enable copy/paste keyboard shortcuts' (#192) from electron-copy-paste-keyboard-shortcuts into master
Reviewed-on: #192
2025-09-01 05:05:27 -04:00
Jose Olarte III
e6f0c7a079 Merge branch 'master' into electron-copy-paste-keyboard-shortcuts 2025-09-01 17:06:05 +08:00
2b9b43d08f Merge pull request 'fix: persist identity names per user instead of globally' (#191) from switching-identities-change-name into master
Reviewed-on: #191
2025-09-01 04:54:40 -04:00
Jose Olarte III
5f8d1fc8c6 refactor: remove deprecated lastName field from user settings
- Remove lastName field from $saveUserSettings and $saveSettings calls
- Clean up deprecated pre v0.1.3 code
2025-09-01 16:54:36 +08:00
Jose Olarte III
c9082fa57b refactor: remove single-use notification constant
- Replace constant usage with direct message string in ImportDerivedAccountView.vue
- Clean up import statement to remove unused import
- Remove unused constant from notifications.ts
2025-09-01 16:02:48 +08:00
a7608429be Merge pull request 'fix(electron): resolve TypeScript errors in Electron build configuration' (#187) from electron-build-config-overwrite into master
Reviewed-on: #187
2025-09-01 03:49:44 -04:00
Matthew Raymer
a522a10fb7 feat(activeDid): complete API layer with minimal safe $accountSettings update
- Add minimal change to prioritize activeDid from active_identity table
- Maintain all existing complex logic and backward compatibility
- Update migration plan to reflect API layer completion

The $accountSettings method now uses the new active_identity table as primary
source while preserving all existing settings merging and fallback behavior.
2025-09-01 06:16:44 +00:00
Matthew Raymer
b4e1313b22 fix(activeDid): implement dual-write pattern with proper MASTER_SETTINGS_KEY usage
- Fix $updateActiveDid() to use MASTER_SETTINGS_KEY constant instead of hardcoded "1"
- Update migration plan to reflect current state after rollback
- Ensure backward compatibility during activeDid migration transition

The dual-write pattern now correctly updates both active_identity and settings tables
using the proper MASTER_SETTINGS_KEY constant for settings table targeting.
2025-09-01 06:06:00 +00:00
d3f54d6bff feat: move the user profile up on page, reword "star" to "favorite" 2025-08-31 08:21:25 -06:00
2bb733a9ea feat: make each of the "new" buttons on the home page the same size 2025-08-31 07:46:10 -06:00
Matthew Raymer
f63f4856bf feat(migration): complete Step 2 of ActiveDid migration - implement dual-write pattern
- Add database persistence to $updateActiveDid() method
- Implement dual-write to both active_identity and settings tables
- Add error handling with graceful fallback to in-memory updates
- Include debug logging for migration monitoring
2025-08-31 05:28:39 +00:00
Matthew Raymer
eb4ddaba50 feat(migration): complete Step 1 of ActiveDid migration - update () to use new API
- Update () to call () with fallback to settings
- Maintain backward compatibility while using new active_identity table
- Update migration plan documentation to reflect completed Step 1
- Restore Playwright workers to 4 (was accidentally set to 1)

Tests: 39/40 passing (1 unrelated UI failure)
Migration progress: Step 1 complete, ready for Step 2 dual-write implementation
2025-08-31 05:18:05 +00:00
Matthew Raymer
971bc68a74 temp: whitelist unused table defintion since I'm doing step-wise changes 2025-08-31 03:50:06 +00:00
Matthew Raymer
d2e04fe2a0 feat(api)!: fix $getActiveIdentity return type for ActiveDid migration
Update $getActiveIdentity() method to return { activeDid: string } instead
of full ActiveIdentity object. Add validation to ensure activeDid exists
in accounts table and clear corrupted values. Update migration plan to
reflect completed first step of API layer implementation.

- Change return type from Promise<ActiveIdentity> to Promise<{ activeDid: string }>
- Add account validation with automatic corruption cleanup
- Simplify query to only select activeDid field
- Improve error handling to return empty string instead of throwing
- Update migration plan documentation with current status
2025-08-31 03:48:46 +00:00
7da6f722f5 fix: fix remaining problems with recent plan changes 2025-08-30 21:30:03 -06:00
Matthew Raymer
18ca6baded docs(migration): update Phase 2 status to COMPLETE with testing notes
Updated activeDid migration plan to reflect Phase 2 API layer implementation
completion. Added critical blocker notes about IndexedDB database inspection
requirements and updated next steps with priority levels.

- Marked Phase 2 as COMPLETE with dual-write pattern implementation
- Added critical blocker for IndexedDB database inspection
- Updated next steps with priority levels and realistic timelines
- Clarified database state requirements for testing
2025-08-31 00:57:13 +00:00
475f4d5ce5 feat: add changed details for plans with recent changes (not all are accurate yet) 2025-08-30 17:18:56 -06:00
Matthew Raymer
ae4e9b3420 chore: sync adjustments 2025-08-30 04:31:43 +00:00
Matthew Raymer
0bda040f15 Merge branch 'active_did_redux' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into active_did_redux 2025-08-30 04:31:23 +00:00
Matthew Raymer
a2e6ae5c28 docs(migration): restructure activeDid migration plan for implementation
Transform verbose planning document into actionable implementation guide:
- Replace theoretical sections with specific code changes required
- Add missing $getActiveIdentity() method implementation
- List 35+ components requiring activeDid pattern updates
- Include exact code patterns to replace in components
- Add implementation checklist with clear phases
- Remove redundant architecture diagrams and explanations

Focuses on practical implementation steps rather than planning theory.
2025-08-30 04:28:15 +00:00
24a7cf5eb6 feat: add a notification for changes to starred projects 2025-08-29 17:31:00 -06:00
da0621c09a feat: Start the ability to star/bookmark a project.
- Currently toggles & stores correctly locally on project. Does not show on other screens.
2025-08-29 15:03:13 -06:00
Matthew Raymer
4a22a35b3e feat(activeDid): implement migration to separate active_identity table
- Add migration 003 with data migration logic to prevent data loss
- Create dedicated ActiveIdentity interface in separate file for better architecture
- Implement $getActiveIdentity method in PlatformServiceMixin
- Enhance $updateActiveDid with dual-write pattern for backward compatibility
- Maintain separation of concerns between settings and active identity types
- Follow project architectural pattern with dedicated type definition files

The migration creates active_identity table alongside existing settings,
automatically copying existing activeDid data to prevent user data loss.
Dual-write pattern ensures backward compatibility during transition.

Migration includes:
- Schema creation with proper constraints and indexes
- Automatic data transfer from settings.activeDid to active_identity.activeDid
- Validation to ensure data exists before migration
- Atomic operation: schema and data migration happen together
2025-08-29 11:48:22 +00:00
Matthew Raymer
95b0cbca78 docs(activeDid): add critical data migration logic to prevent data loss
- Add data migration SQL to migration 003 for existing databases
- Automatically copy activeDid from settings table to active_identity table
- Prevent users from losing active identity selection during migration
- Include validation to ensure data exists before migration
- Maintain atomic operation: schema and data migration happen together
- Update risk assessment to reflect data loss prevention
- Add data migration strategy documentation

The migration now safely handles both new and existing databases,
ensuring no user data is lost during the activeDid table separation.
2025-08-29 11:06:40 +00:00
Matthew Raymer
1227cdee76 docs(activeDid): streamline migration plan for existing migration service
- Remove unnecessary complexity and focus on essential changes only
- Integrate with existing IndexedDB migration service (indexedDBMigrationService.ts)
- Maintain backward compatibility with existing migration paths
- Focus on core requirements: database schema, API methods, type definitions
- Eliminate duplicate migration logic already handled by existing service
- Preserve MASTER_SETTINGS_KEY = "1" for legacy support
- Add clear rollback strategy and integration points

The plan now focuses only on necessary changes while maintaining full
compatibility with existing systems and migration infrastructure.
2025-08-29 10:51:40 +00:00
Jose Olarte III
4a1249d166 feat(electron): add editMenu to enable copy/paste keyboard shortcuts
- Add 'editMenu' role to AppMenuBarMenuTemplate in setup.ts and index.ts
- Enables standard keyboard shortcuts (Cmd+C, Cmd+V, etc.) in Electron app
- Fixes issue where copy/paste shortcuts were not working in text inputs
- Maintains existing clipboard service functionality for programmatic operations

Resolves keyboard shortcut functionality for better user experience in desktop app.
2025-08-29 18:05:37 +08:00
Jose Olarte III
6225cd7f8f Merge branch 'electron-build-config-overwrite' into electron-copy-paste-keyboard-shortcuts 2025-08-29 17:09:34 +08:00
Matthew Raymer
fad7093fbd chore: update plan for handling MASTER_SETTINGS_KEY 2025-08-29 08:54:08 +00:00
Jose Olarte III
dde37e73e1 Lint fixes 2025-08-29 16:41:46 +08:00
Jose Olarte III
83c0c18db2 fix: persist identity names per user instead of globally
Fixes issue where identity names were not saved when switching between
multiple identities. Names were being saved to master settings instead
of user-specific settings.

Changes:
- UserNameDialog: Load/save names from/to user-specific settings
- NewEditAccountView: Save names to user-specific settings for active DID
- Both components now use $accountSettings() and $saveUserSettings()
  instead of $settings() and $updateSettings()

Each identity now properly retains their assigned name when switching
between identities. Previously only "User Zero" would show their name
due to using master settings instead of per-identity settings.

Fixes: Identity name persistence across identity switches
2025-08-29 16:41:19 +08:00
Matthew Raymer
fddb2ac959 feat(migration)!: enhance ActiveDid migration plan with focused implementation
- Add foreign key constraints to prevent data corruption
- Implement comprehensive migration validation and rollback
- Focus API updates on PlatformServiceMixin only (no component changes)
- Add enhanced error handling and data integrity checks
- Streamline plan to focus only on what needs to change
- Update timestamps and implementation details for current state

Breaking Changes:
- Database schema requires new active_identity table with constraints
- PlatformServiceMixin methods need updates for new table structure

Migration Impact:
- 50+ components work automatically through API layer
- Only core database and API methods require changes
- Comprehensive rollback procedures for risk mitigation
2025-08-29 07:58:50 +00:00
Matthew Raymer
40babae05d Merge branch 'master' into active_did_redux 2025-08-29 07:15:41 +00:00
Matthew Raymer
5780d96cdc chore: linting 2025-08-29 06:50:16 +00:00
Jose Olarte III
e67c97821a fix: change import User Zero function
- Use the ./account route to mimic real-world use
2025-08-28 21:06:26 +08:00
Jose Olarte III
40fa38a9ce fix: clean up "register random person" test
- Remove redundant "import User Zero" action
- Remove out-of-scope actions from test (sending a gift to an unrelated entity, deleting the contact)
- Update imports based on changes
2025-08-28 20:50:57 +08:00
Matthew Raymer
acbc276ef6 docs: enhance activeDid migration plan with implementation details
- Add master settings functions implementation strategy
- Correct IdentitySection.vue analysis (prop-based, no changes required)
- Simplify ContactAmountsView.vue (phased-out method, separate refactoring)
- Add new getMasterSettings() function with active_identity integration
- Include helper methods _getSettingsWithoutActiveDid() and _getActiveIdentity()
- Enhance evidence section with master settings architecture support
- Update risk assessment for phased-out methods
- Clean up migration timeline formatting

This commit focuses the migration plan on components requiring immediate
active_identity table changes, separating concerns from broader API refactoring.
2025-08-28 12:32:39 +00:00
ff864adbe5 Merge branch 'master' into profile_include_location 2025-08-28 06:37:51 -04:00
Jose Olarte III
96e4d3c394 chore - reorder duplication test
- Rename the test to run it earlier in the test suite
2025-08-28 18:34:38 +08:00
Jose Olarte III
c4f2bb5e3a refactor: move duplicate account import warnings to notification constants
- Add NOTIFY_DUPLICATE_ACCOUNT_IMPORT constant for import warnings
- Add NOTIFY_DUPLICATE_DERIVED_ACCOUNT constant for derived account warnings
- Update ImportAccountView.vue to use notification constants
- Update ImportDerivedAccountView.vue to use notification constants
- Update test file to use notification constants for assertions

Centralizes notification messages for better maintainability and consistency
with the existing notification system.

Files modified:
- src/constants/notifications.ts: Add new notification constants
- src/views/ImportAccountView.vue: Replace hardcoded messages with constants
- src/views/ImportDerivedAccountView.vue: Replace hardcoded messages with constants
- test-playwright/duplicate-import-test.spec.ts: Update test assertions
2025-08-28 16:44:17 +08:00
Jose Olarte III
f51408e32a feat: add duplicate account import prevention
- Add duplicate check in ImportAccountView before account import
- Add duplicate check in ImportDerivedAccountView for derived accounts
- Add safety check in saveNewIdentity function to prevent duplicate saves
- Implement user-friendly warning messages for duplicate attempts
- Add comprehensive error handling to catch duplicate errors from saveNewIdentity
- Create Playwright tests to verify duplicate prevention functionality
- Add documentation for duplicate prevention implementation

The system now prevents users from importing the same account multiple times
by checking for existing DIDs both before import (pre-check) and during
save (post-check). Users receive clear warning messages instead of
technical errors when attempting to import duplicate accounts.

Files modified:
- src/views/ImportAccountView.vue: Add duplicate check and error handling
- src/views/ImportDerivedAccountView.vue: Add duplicate check for derived accounts
- src/libs/util.ts: Add duplicate prevention in saveNewIdentity
- test-playwright/duplicate-import-test.spec.ts: Add comprehensive tests
- doc/duplicate-account-import-implementation.md: Add implementation docs

Resolves: Prevent duplicate account imports in IdentitySwitcherView
2025-08-28 16:35:04 +08:00
Matthew Raymer
649786ae01 chore: a bit more planning 2025-08-27 12:52:21 +00:00
Matthew Raymer
4aea8d9ed3 linting 2025-08-27 12:36:15 +00:00
Matthew Raymer
0079ca252d chore: add plan 2025-08-27 12:35:37 +00:00
Jose Olarte III
8827c4a973 fix(electron): resolve TypeScript errors in Electron build configuration
- Create separate Electron-specific capacitor config
- Update build script to not copy main config to Electron directory
- Fix TypeScript compilation by excluding main config from Electron tsconfig

Resolves TypeScript compilation errors in npm run build:electron:dev
2025-08-27 19:04:27 +08:00
6f9847b524 Merge pull request 'ProjectView: hide offer link from unregistered users' (#178) from projectview-hide-offer-link-unregistered into master
Reviewed-on: #178
2025-08-27 06:51:42 -04:00
01279b61f5 Merge branch 'master' into projectview-hide-offer-link-unregistered 2025-08-27 06:51:25 -04:00
Matthew Raymer
98f97f2dc9 refactor(settings): simplify $getSettings to $getMasterSettings
- Rename $getSettings to $getMasterSettings for clarity
- Remove unused account-specific logic (never called with accountDid)
- Simplify method signature by removing unused key parameter
- Update all 8 call sites and interface definitions
- Maintain backward compatibility for all existing functionality

All calls to $getSettings used MASTER_SETTINGS_KEY, so the complex
branching logic for account-specific settings was unnecessary.
2025-08-27 09:45:42 +00:00
Matthew Raymer
4c7c2d48e9 Merge branch 'master' into profile_include_location 2025-08-27 09:12:03 +00:00
Matthew Raymer
0a0a17ef9c Merge branch 'master' into profile_include_location 2025-08-27 02:50:05 +00:00
Matthew Raymer
8dab4ed016 feat: suppress console spam for expected HTTP errors in profile operations
- Change server switching logs from info to debug level
- Implement structured error logging for profile CRUD operations
- Handle HTTP status codes 400, 401, 403, 404, 409 gracefully
- Suppress full error stack traces for expected API responses
- Maintain user notifications while improving console readability
- Add timestamp and context to all profile-related error logs

Improves developer experience by reducing console noise while preserving
debugging information and user-facing error handling.
2025-08-26 09:32:18 +00:00
Matthew Raymer
4f78bfe744 feat: suppress console spam for expected HTTP errors
- Change server switching logs to debug level
- Implement structured error logging for profile operations
- Handle common HTTP status codes gracefully
- Maintain user notifications while cleaning console output
2025-08-26 09:22:25 +00:00
Matthew Raymer
ceceabf7b5 git commit -m "feat(performance): implement request deduplication for plan loading
- Add inFlightRequests tracking to prevent duplicate API calls
- Eliminate race condition causing 10+ redundant requests
- Maintain existing cache behavior and error handling
- 90%+ reduction in redundant server load"
2025-08-26 08:14:12 +00:00
Matthew Raymer
9386b2e96f refactor(services): inline ProfileService logic into AccountViewView
Removes over-engineered ProfileService and ServiceInitializationManager
classes that were only used in one place. Inlines all profile logic
directly into AccountViewView.vue to reduce complexity and improve
maintainability.

- Deletes ProfileService.ts (325 lines)
- Deletes ServiceInitializationManager.ts (207 lines)
- Inlines ProfileData interface and methods into AccountViewView
- Maintains all existing functionality while reducing code footprint

perf(logging): convert excessive info logs to debug level

Reduces console noise by converting high-frequency, low-value logging
from info to debug level across navigation, API calls, and component
lifecycle operations. Improves performance and reduces log verbosity
for normal application flow.

- Router navigation guards: info → debug
- Plan loading operations: info → debug
- User registration checks: info → debug
- Image server rate limits: info → debug
- Component lifecycle events: info → debug
- Settings loading operations: info → debug

Maintains warn/error levels for actual issues while reducing noise
from expected application behavior.
2025-08-26 07:23:24 +00:00
Matthew Raymer
128ddff467 Merge branch 'master' into profile_include_location 2025-08-26 04:37:01 +00:00
Matthew Raymer
b834596ba6 feat(meta-rules): create change evaluation and breaking change detection rule
- Create comprehensive meta-rule for evaluating changes between branches
- Implement systematic breaking change detection for API contracts, data structures, and behavior
- Add risk assessment framework with LOW/MEDIUM/HIGH/CRITICAL classification
- Include change pattern recognition and dependency impact assessment
- Provide structured output format for change evaluation reports
- Apply markdown core standards for consistent formatting and readability

Problem: Need systematic approach to catch problematic model behavior by analyzing
changes before they cause issues.

Solution: Create meta-rule that evaluates changes between branches, detects
breaking changes, assesses risk levels, and provides actionable recommendations.

Files changed:
- .cursor/rules/meta_change_evaluation.mdc: New meta-rule with comprehensive
  change evaluation capabilities

Testing: Rule provides structured approach for analyzing changes, detecting
breaking changes, and generating risk assessment reports.
2025-08-25 13:22:31 +00:00
Matthew Raymer
77a4c60656 fix(ProfileService): revert to working endpoint for profile loading
- Revert ProfileService from broken /api/partner/userProfile endpoint to working /api/partner/userProfileForIssuer/${did}
- Fix location data display by restoring single profile object response parsing
- Remove complex array handling logic that was unnecessary for current user profiles
- Restore original working functionality that was broken by recent refactoring

Problem: Recent ProfileService creation changed endpoint from working userProfileForIssuer/${did}
to broken userProfile (list endpoint), causing location data to not display properly.

Solution: Revert to original working endpoint and response parsing logic that returns
single profile objects with location data instead of arrays of all profiles.

Files changed:
- src/services/ProfileService.ts: Restore working endpoint and simplify response parsing

Testing: Profile loading now works correctly for both existing and new profiles,
location data is properly extracted and displayed, maps render correctly.
2025-08-25 13:03:06 +00:00
Matthew Raymer
a11443dc3a feat(logging): enhance diagnostic logging for API operations
- Add unique request IDs for claim submission tracking
- Add unique request IDs for plan loading tracking
- Include comprehensive request context (endpoint, server, timestamp)
- Enhance error logging with specific error codes and messages
- Improve debugging capabilities for server operations

Testing: TypeScript compilation passes, no linting errors
Impact: Better debugging and monitoring of API operations
2025-08-25 11:51:22 +00:00
Matthew Raymer
7f7680f4a6 fix(config): remove forced API server overrides
- Remove forced production server override in PlatformServiceMixin.()
- Remove forced production server override in PlatformServiceMixin.()
- Remove forced production server override in HomeView.ensureCorrectApiServer()
- Fix type error in HomeView.latLongInAnySearchBox coordinates

Testing: TypeScript compilation passes, no linting errors
Impact: Users can now switch API servers freely without forced overrides
2025-08-25 11:51:12 +00:00
Matthew Raymer
271a45afa3 fix(database): resolve ambiguous settings query logic
- Fix PlatformServiceMixin.() ambiguous OR condition
- Use specific queries for master settings (id) vs account settings (accountDid)
- Resolve settings priority conflicts affecting server switching
- Maintain backward compatibility with existing settings structure

Testing: TypeScript compilation passes, no linting errors
Impact: Settings now load correctly without query ambiguity
2025-08-25 11:50:55 +00:00
Matthew Raymer
6aac3ca35f fix(api): resolve image server hardcoding and add comprehensive diagnostics
- Fix fetchImageRateLimits to accept configurable imageServer parameter instead
  of hardcoded DEFAULT_IMAGE_API_SERVER
- Add enhanced diagnostic logging for image server operations with server
  context, error tracking, and user registration status
- Update AccountViewView.vue to pass correct image server parameter
- Ensure consistent server switching behavior across all API endpoints
- Prevent similar server configuration issues in image operations

Fixes server switching not applying to image rate limit checks and provides
complete visibility into image server operations for debugging and monitoring.
2025-08-25 10:25:20 +00:00
Matthew Raymer
f0fd8c0f12 feat(diagnostics): add comprehensive logging for server switching and user registration
- Enhanced user registration diagnostics with detailed error context
- Server switching flow logging with before/after values
- Improved error handling with server context
- Fixed type safety issues

Confirms server switching fix is working perfectly while providing
comprehensive debugging for user migration and environment issues.
2025-08-25 10:18:42 +00:00
Matthew Raymer
fd30343ec4 fix(settings): resolve server switching not applying immediately
- Fix database query logic in PlatformServiceMixin.$getSettings() to properly
  distinguish between master settings (ID) and account settings (DID)
- Add comprehensive logging for settings debugging with request IDs and
  component tracking
- Fix ProfileService initialization order in AccountViewView to use correct
  partnerApiServer after settings load
- Add URL flow testing interface in TestView for debugging server switching
- Enhance settings consistency validation and error handling

Resolves issue where profile server changes were saved but not applied due to
incorrect database query logic and settings priority handling.

Files changed: PlatformServiceMixin.ts, AccountViewView.vue, TestView.vue,
TopMessage.vue, main.ts, router/index.ts

Testing: Added comprehensive URL flow testing interface for validation
2025-08-25 09:57:13 +00:00
Matthew Raymer
e70faff5ce Merge branch 'master' into profile_include_location 2025-08-25 07:43:48 +00:00
Matthew Raymer
9512e8192f Merge branch 'master' into profile_include_location 2025-08-25 02:56:53 +00:00
Matthew Raymer
a6126ecac3 chore: update .cursor rules for workflow state 2025-08-25 02:54:56 +00:00
528a68ef6c fix: reorder and reword visibility messages on confirmation & DID view pages 2025-08-24 18:15:08 -06:00
8991b36a56 fix: give consistent "you" verbiage on button 2025-08-24 17:49:01 -06:00
6f5661d61c fix: enhance the message & provide link on confirmation page when something isn't seen 2025-08-24 17:44:15 -06:00
Matthew Raymer
d66d8ce1c1 chore: ignore state files 2025-08-24 05:47:24 +00:00
Matthew Raymer
277fe49aa8 chore: update meta_rule_usage_guide with state and override 2025-08-24 05:46:55 +00:00
Matthew Raymer
a85b508f44 feat: implement enhanced workflow state system with commit override capability
- Add commit override mechanism to workflow state constraints
- Enhance meta_bug_diagnosis.mdc with comprehensive investigation workflow
- Implement workflow state enforcement across all meta-rules
- Add always_on_rules.mdc for core rule bundling
- Update all meta-rules to support workflow state management
- Maintain version control standards while providing workflow flexibility

Security: No code execution changes, workflow rule updates only
Migration: Workflow infrastructure enhancement, no database changes
Testing: Manual validation of workflow state constraints
Documentation: Comprehensive workflow documentation and examples

Closes: Workflow state rigidity issue
Enables: Commits on demand during investigation phases
2025-08-24 05:45:37 +00:00
Matthew Raymer
be4ab16b00 Merge branch 'master' into profile_include_location 2025-08-24 02:47:14 +00:00
Matthew Raymer
1305eed9bc Merge branch 'master' into profile_include_location 2025-08-23 04:03:19 +00:00
Jose Olarte III
aa55588cbb UI: spacing and text styling consistencies 2025-08-21 16:27:00 +08:00
Jose Olarte III
5f63e05090 Fix: hide offer link from unregistered users 2025-08-21 16:25:57 +08:00
Matthew Raymer
3be7001d1b chore: linting 2025-08-20 10:04:40 +00:00
Matthew Raymer
95a8f5ebe1 Merge branch 'master' into profile_include_location 2025-08-20 09:48:26 +00:00
Jose Olarte III
e5ad71505c Chore: move function to serverUtil
- capitalizeAndInsertSpacesBeforeCapsWithAPrefix() defined in two places, unified and moved to endorserServer.ts
- Use capitalizeAndInsertSpacesBeforeCaps() that's already defined in endorserServer.ts
2025-08-18 17:47:33 +08:00
Matthew Raymer
f2026bb921 chore: clean up debug logging 2025-08-18 07:44:26 +00:00
19f0c270d3 chore: Rename variable for clarity 2025-08-17 14:13:50 -06:00
Jose Olarte III
693173f09d UI: wording and spacing consistencies
- Added grouped conditional spacing to ensure a top margin before fulfills links
- Brought over icons and wording from ConfirmGiftView to ClaimView
2025-08-14 20:12:28 +08:00
Jose Olarte III
a1388539c1 Fix: improve offer fulfillment detection in ClaimView
- Remove outdated fulfillsType logic that was checking for non-PlanAction items
- Keep only the new offer fulfillment extraction from fullClaim.fulfills array
- Apply consistent changes to both ClaimView and ConfirmGiftView

This ensures that "Fulfills Offer..." links appear correctly when gives are created from offers, by directly parsing the fulfills array instead of relying on API-processed fields that only capture the first relationship.
2025-08-14 18:53:12 +08:00
137 changed files with 10090 additions and 1564 deletions

View File

@@ -104,6 +104,161 @@ High-level meta-rules that bundle related sub-rules for specific workflows.
- **`meta_bug_diagnosis.mdc`** - Bug investigation workflow bundling
- **`meta_bug_fixing.mdc`** - Bug fix implementation workflow bundling
- **`meta_feature_implementation.mdc`** - Feature implementation workflow bundling
- **`meta_research.mdc`** - Investigation and research workflow bundling
### **Workflow State Management**
The project uses a sophisticated workflow state management system to ensure systematic development processes and maintain code quality across all phases of development.
#### **Workflow State System**
The workflow state is managed through `.cursor/rules/.workflow_state.json` and enforces different modes with specific constraints. The system automatically tracks workflow progression and maintains a complete history of mode transitions.
**Available Modes**:
- **`diagnosis`** - Investigation and analysis phase (read-only)
- **`fixing`** - Implementation and bug fixing phase (full access)
- **`planning`** - Design and architecture phase (design only)
- **`research`** - Investigation and research phase (investigation only)
- **`documentation`** - Documentation writing phase (writing only)
**Mode Constraints**:
```json
{
"diagnosis": {
"mode": "read_only",
"forbidden": ["modify", "create", "build", "commit"],
"allowed": ["read", "search", "analyze", "document"]
},
"fixing": {
"mode": "implementation",
"forbidden": [],
"allowed": ["modify", "create", "build", "commit", "test"]
}
}
```
**Workflow History Tracking**:
The system automatically maintains a `workflowHistory` array that records all mode transitions and meta-rule invocations:
```json
{
"workflowHistory": [
{
"mode": "research",
"invoked": "meta_core_always_on.mdc",
"timestamp": "2025-08-25T02:14:37Z"
},
{
"mode": "diagnosis",
"invoked": "meta_bug_diagnosis.mdc",
"timestamp": "2025-08-25T02:14:37Z"
}
]
}
```
**History Entry Format**:
- **`mode`**: The workflow mode that was activated
- **`invoked`**: The specific meta-rule that triggered the mode change
- **`timestamp`**: UTC timestamp when the mode transition occurred
**History Purpose**:
- **Workflow Continuity**: Track progression through development phases
- **Meta-Rule Usage**: Monitor which rules are invoked and when
- **Temporal Context**: Maintain chronological order of workflow changes
- **State Persistence**: Preserve workflow history across development sessions
- **Debugging Support**: Help diagnose workflow state issues
- **Process Analysis**: Understand development patterns and meta-rule effectiveness
#### **Commit Override System**
The workflow includes a flexible commit override mechanism that allows commits on demand while maintaining workflow integrity:
```json
{
"overrides": {
"commit": {
"allowed": true,
"requires_override": true,
"override_reason": "user_requested"
}
}
}
```
**Override Benefits**:
-**Investigation Commits**: Document findings during diagnosis phases
-**Work-in-Progress**: Commit partial solutions during complex investigations
-**Emergency Fixes**: Commit critical fixes without mode transitions
-**Flexible Workflow**: Maintain systematic approach while accommodating real needs
**Override Limitations**:
-**Does NOT bypass**: Version control rules, commit message standards, or security requirements
-**Does NOT bypass**: Code quality standards, testing requirements, or documentation requirements
#### **Workflow Enforcement**
The system automatically enforces workflow constraints through the core always-on rules:
**Before Every Interaction**:
1. **Read current workflow state** from `.cursor/rules/.workflow_state.json`
2. **Identify current mode** and its constraints
3. **Validate user request** against current mode constraints
4. **Enforce constraints** before generating response
5. **Guide model behavior** based on current mode
**Mode-Specific Enforcement**:
- **Diagnosis Mode**: Blocks modification, creation, building, and commits
- **Fixing Mode**: Allows full implementation and testing capabilities
- **Planning Mode**: Focuses on design and architecture, blocks implementation
- **Research Mode**: Enables investigation and analysis, blocks modification
- **Documentation Mode**: Allows writing and editing, blocks implementation
#### **Workflow Transitions**
To change workflow modes, invoke the appropriate meta-rule:
```bash
# Switch to bug fixing mode
@meta_bug_fixing.mdc
# Switch to feature planning mode
@meta_feature_planning.mdc
# Switch to documentation mode
@meta_documentation.mdc
```
**Transition Requirements**:
- **Mode Changes**: Require explicit meta-rule invocation
- **State Updates**: Automatically update workflow state file
- **Constraint Enforcement**: Immediately apply new mode constraints
- **History Tracking**: Automatically maintained in `workflowHistory` array
- **Timestamp Recording**: Each transition recorded with UTC timestamp
#### **Integration with Development Process**
The workflow system integrates seamlessly with existing development practices:
**Version Control**:
- All commits must follow TimeSafari commit message standards
- Security audit checklists are enforced regardless of workflow mode
- Documentation updates are required for substantial changes
**Quality Assurance**:
- Code quality standards (PEP8, TypeScript, etc.) are always enforced
- Testing requirements apply to all implementation work
- Documentation standards are maintained across all phases
**Build System**:
- Build Architecture Guard protects critical build files
- Platform-specific build processes respect workflow constraints
- Asset generation follows established patterns
**Migration Context**:
- Database migration work respects investigation vs. implementation phases
- Component migration progress is tracked through workflow states
## Usage Guidelines

View File

@@ -0,0 +1,192 @@
# Meta-Rule: Core Always-On Rules
**Author**: Matthew Raymer
**Date**: 2025-08-21
**Status**: 🎯 **ACTIVE** - Core rules for every prompt
## Purpose
This meta-rule bundles the core rules that should be applied to **every single
prompt** because they define fundamental behaviors, principles, and context
that are essential for all AI interactions.
## When to Use
**ALWAYS** - These rules apply to every single prompt, regardless of the task
or context. They form the foundation for all AI assistant behavior.
## Bundled Rules
### **Core Human Competence Principles**
- **`core/base_context.mdc`** - Human competence first principles, interaction
guidelines, and output contract requirements
- **`core/less_complex.mdc`** - Minimalist solution principle and complexity
guidelines
### **Time & Context Standards**
- **`development/time.mdc`** - Time handling principles and UTC standards
- **`development/time_examples.mdc`** - Practical time implementation examples
- **`development/time_implementation.mdc`** - Detailed time implementation
guidelines
### **Version Control & Process**
- **`workflow/version_control.mdc`** - Version control principles and commit
guidelines
- **`workflow/commit_messages.mdc`** - Commit message format and conventions
### **Application Context**
- **`app/timesafari.mdc`** - Core TimeSafari application context and
development principles
- **`app/timesafari_development.mdc`** - TimeSafari-specific development
workflow and quality standards
## Why These Rules Are Always-On
### **Base Context**
- **Human Competence First**: Every interaction must increase human competence
- **Output Contract**: All responses must follow the required structure
- **Competence Hooks**: Learning and collaboration must be built into every response
### **Time Standards**
- **UTC Consistency**: All timestamps must use UTC for system operations
- **Evidence Collection**: Time context is essential for debugging and investigation
- **Cross-Platform**: Time handling affects all platforms and features
### **Version Control**
- **Commit Standards**: Every code change must follow commit message conventions
- **Process Consistency**: Version control affects all development work
- **Team Collaboration**: Commit standards enable effective team communication
### **Application Context**
- **Platform Awareness**: Every task must consider web/mobile/desktop platforms
- **Architecture Principles**: All work must follow TimeSafari patterns
- **Development Standards**: Quality and testing requirements apply to all work
## Application Priority
### **Primary (Apply First)**
1. **Base Context** - Human competence and output contract
2. **Time Standards** - UTC and timestamp requirements
3. **Application Context** - TimeSafari principles and platforms
### **Secondary (Apply as Needed)**
1. **Version Control** - When making code changes
2. **Complexity Guidelines** - When evaluating solution approaches
## Integration with Other Meta-Rules
### **Feature Planning**
- Base context ensures human competence focus
- Time standards inform planning and estimation
- Application context drives platform considerations
### **Bug Diagnosis**
- Base context ensures systematic investigation
- Time standards enable proper evidence collection
- Application context provides system understanding
### **Bug Fixing**
- Base context ensures quality implementation
- Time standards maintain logging consistency
- Application context guides testing strategy
### **Feature Implementation**
- Base context ensures proper development approach
- Time standards maintain system consistency
- Application context drives architecture decisions
## Success Criteria
- [ ] **Base context applied** to every single prompt
- [ ] **Time standards followed** for all timestamps and logging
- [ ] **Version control standards** applied to all code changes
- [ ] **Application context considered** for all platform work
- [ ] **Human competence focus** maintained in all interactions
- [ ] **Output contract structure** followed in all responses
## Common Pitfalls
- **Don't skip base context** - loses human competence focus
- **Don't ignore time standards** - creates inconsistent timestamps
- **Don't forget application context** - misses platform considerations
- **Don't skip version control** - creates inconsistent commit history
- **Don't lose competence focus** - reduces learning value
## Feedback & Improvement
### **Rule Effectiveness Ratings (1-5 scale)**
- **Base Context**: ___/5 - Comments: _______________
- **Time Standards**: ___/5 - Comments: _______________
- **Version Control**: ___/5 - Comments: _______________
- **Application Context**: ___/5 - Comments: _______________
### **Always-On Effectiveness**
- **Consistency**: Are these rules applied consistently across all prompts?
- **Value**: Do these rules add value to every interaction?
- **Overhead**: Are these rules too burdensome for simple tasks?
### **Integration Feedback**
- **With Other Meta-Rules**: How well do these integrate with workflow rules?
- **Context Switching**: Do these rules help or hinder context switching?
- **Learning Curve**: Are these rules easy for new users to understand?
### **Overall Experience**
- **Quality Improvement**: Do these rules improve response quality?
- **Efficiency**: Do these rules make interactions more efficient?
- **Recommendation**: Would you recommend keeping these always-on?
## Model Implementation Checklist
### Before Every Prompt
- [ ] **Base Context**: Ensure human competence principles are active
- [ ] **Time Standards**: Verify UTC and timestamp requirements are clear
- [ ] **Application Context**: Confirm TimeSafari context is loaded
- [ ] **Version Control**: Prepare commit standards if code changes are needed
### During Response Creation
- [ ] **Output Contract**: Follow required response structure
- [ ] **Competence Hooks**: Include learning and collaboration elements
- [ ] **Time Consistency**: Apply UTC standards for all time references
- [ ] **Platform Awareness**: Consider all target platforms
### After Response Creation
- [ ] **Validation**: Verify all always-on rules were applied
- [ ] **Quality Check**: Ensure response meets competence standards
- [ ] **Context Review**: Confirm application context was properly considered
- [ ] **Feedback Collection**: Note any issues with always-on application
---
**See also**:
- `.cursor/rules/meta_feature_planning.mdc` for workflow-specific rules
- `.cursor/rules/meta_bug_diagnosis.mdc` for investigation workflows
- `.cursor/rules/meta_bug_fixing.mdc` for fix implementation
- `.cursor/rules/meta_feature_implementation.mdc` for feature development
**Status**: Active core always-on meta-rule
**Priority**: Critical (applies to every prompt)
**Estimated Effort**: Ongoing reference
**Dependencies**: All bundled sub-rules
**Stakeholders**: All AI interactions, Development team

View File

@@ -12,6 +12,7 @@ language: Match repository languages and conventions
## Rules
0. **Principle:** just the facts m'am.
1. **Default to the least complex solution.** Fix the problem directly
where it occurs; avoid new layers, indirection, or patterns unless
strictly necessary.

View File

@@ -4,7 +4,6 @@ alwaysApply: false
---
✅ use system date command to timestamp all interactions with accurate date and
time
✅ python script files must always have a blank line at their end
✅ remove whitespace at the end of lines
✅ use npm run lint-fix to check for warnings
✅ do not use npm run dev let me handle running and supplying feedback
@@ -22,12 +21,10 @@ alwaysApply: false
- [ ] **Timestamp Usage**: Include accurate timestamps in all interactions
- [ ] **Code Quality**: Use npm run lint-fix to check for warnings
- [ ] **File Standards**: Ensure Python files have blank line at end
- [ ] **Whitespace**: Remove trailing whitespace from all lines
### After Development
- [ ] **Linting Check**: Run npm run lint-fix to verify code quality
- [ ] **File Validation**: Confirm Python files end with blank line
- [ ] **Whitespace Review**: Verify no trailing whitespace remains
- [ ] **Documentation**: Update relevant documentation with changes

View File

@@ -1,6 +1,5 @@
---
alwaysApply: true
inherits: base_context.mdc
alwaysApply: false
---
```json
{

View File

@@ -1,169 +1,285 @@
# Meta-Rule: Bug Diagnosis
# Meta-Rule: Bug Diagnosis Workflow
**Author**: Matthew Raymer
**Date**: 2025-08-21
**Status**: 🎯 **ACTIVE** - Bug investigation workflow bundling
**Date**: August 24, 2025
**Status**: 🎯 **ACTIVE** - Core workflow for all bug investigation
## Purpose
This meta-rule bundles all the rules needed for systematic bug investigation
and root cause analysis. Use this when bugs are reported, performance
issues occur, or unexpected behavior happens.
This meta-rule defines the systematic approach for investigating and diagnosing
bugs, defects, and unexpected behaviors in the TimeSafari application. It ensures
consistent, thorough, and efficient problem-solving workflows.
## Workflow Constraints
**This meta-rule enforces DIAGNOSIS MODE for all bundled sub-rules:**
```json
{
"workflowMode": "diagnosis",
"constraints": {
"mode": "read_only",
"forbidden": ["modify", "create", "build", "commit"],
"required": "complete_investigation_before_fixing"
}
}
```
**All bundled sub-rules automatically inherit these constraints.**
## Workflow State Update
**When this meta-rule is invoked, update the workflow state file:**
```json
{
"currentMode": "diagnosis",
"lastInvoked": "meta_bug_diagnosis.mdc",
"timestamp": "2025-01-27T15:30:00Z",
"constraints": {
"mode": "read_only",
"forbidden": ["modify", "create", "build", "commit"],
"allowed": ["read", "search", "analyze", "document"],
"required": "complete_investigation_before_fixing"
}
}
```
**State File Location**: `.cursor/rules/.workflow_state.json`
**This enables the core always-on rule to enforce diagnosis mode constraints.**
## When to Use
- **Bug Reports**: Investigating reported bugs or issues
- **Performance Issues**: Diagnosing slow performance or bottlenecks
- **Unexpected Behavior**: Understanding why code behaves unexpectedly
- **Production Issues**: Investigating issues in live environments
- **Test Failures**: Understanding why tests are failing
- **Integration Problems**: Diagnosing issues between components
**ALWAYS** - Apply this workflow to every bug investigation, regardless of
severity or complexity. This ensures systematic problem-solving and prevents
common investigation pitfalls.
## Bundled Rules
### **Investigation Process**
### **Investigation Foundation**
- **`development/research_diagnostic.mdc`** - Systematic investigation
workflow with evidence collection and analysis
- **`development/investigation_report_example.mdc`** - Investigation
documentation templates and examples
- **`core/harbor_pilot_universal.mdc`** - Technical guide creation
for complex investigations
- **`development/research_diagnostic.mdc`** - Research and investigation methodologies
- **`development/logging_standards.mdc`** - Logging and debugging best practices
- **`development/type_safety_guide.mdc`** - Type safety and error prevention
### **Evidence Collection**
### **Development Workflow**
- **`development/logging_standards.mdc`** - Logging implementation
standards for debugging and evidence collection
- **`development/time.mdc`** - Timestamp requirements and time
handling standards for evidence
- **`development/time_examples.mdc`** - Practical examples of
proper time handling in investigations
- **`workflow/version_control.mdc`** - Version control during investigation
- **`development/software_development.mdc`** - Development best practices
### **Technical Context**
## Critical Development Constraints
- **`app/timesafari.mdc`** - Core application context and
architecture for understanding the system
- **`app/timesafari_platforms.mdc`** - Platform-specific
considerations and constraints
### **🚫 NEVER Use Build Commands During Diagnosis**
## Workflow Sequence
**Critical Rule**: Never use `npm run build:web` or similar build commands during bug diagnosis
### **Phase 1: Initial Investigation (Start Here)**
- **Reason**: These commands block the chat and prevent effective troubleshooting
- **Impact**: Blocks user interaction, prevents real-time problem solving
- **Alternative**: Use safe, fast commands for investigation
- **When to use build**: Only after diagnosis is complete and fixes are ready for testing
1. **Research Diagnostic** - Use `research_diagnostic.mdc` for
systematic investigation approach
2. **Evidence Collection** - Apply `logging_standards.mdc` and
`time.mdc` for proper evidence gathering
3. **Context Understanding** - Review `timesafari.mdc` for
application context
### **Safe Diagnosis Commands**
### **Phase 2: Deep Investigation**
✅ **Safe to use during diagnosis:**
- `npm run lint-fix` - Syntax and style checking
- `npm run type-check` - TypeScript validation (if available)
- `git status` - Version control status
- `ls` / `dir` - File listing
- `cat` / `read_file` - File content inspection
- `grep_search` - Text pattern searching
1. **Platform Analysis** - Check `timesafari_platforms.mdc` for
platform-specific issues
2. **Technical Guide Creation** - Use `harbor_pilot_universal.mdc`
for complex investigation documentation
3. **Evidence Analysis** - Apply `time_examples.mdc` for proper
timestamp handling
**Never use during diagnosis:**
- `npm run build:web` - Blocks chat
- `npm run build:electron` - Blocks chat
- `npm run build:capacitor` - Blocks chat
- Any long-running build processes
### **Phase 3: Documentation & Reporting**
## Investigation Workflow
1. **Investigation Report** - Use `investigation_report_example.mdc`
for comprehensive documentation
2. **Root Cause Analysis** - Synthesize findings into actionable
insights
### **Phase 1: Problem Definition**
## Success Criteria
1. **Gather Evidence**
- Error messages and stack traces
- User-reported symptoms
- System logs and timestamps
- Reproduction steps
- [ ] **Root cause identified** with supporting evidence
- [ ] **Evidence properly collected** with timestamps and context
- [ ] **Investigation documented** using appropriate templates
- [ ] **Platform factors considered** in diagnosis
- [ ] **Reproduction steps documented** for verification
- [ ] **Impact assessment completed** with scope defined
- [ ] **Next steps identified** for resolution
2. **Context Analysis**
- When did the problem start?
- What changed recently?
- Which platform/environment?
- User actions leading to the issue
### **Phase 2: Systematic Investigation**
1. **Code Inspection**
- Relevant file examination
- Import and dependency analysis
- Syntax and type checking
- Logic flow analysis
2. **Environment Analysis**
- Platform-specific considerations
- Configuration and settings
- Database and storage state
- Network and API connectivity
### **Phase 3: Root Cause Identification**
1. **Pattern Recognition**
- Similar issues in codebase
- Common failure modes
- Platform-specific behaviors
- Recent changes impact
2. **Hypothesis Testing**
- Targeted code changes
- Configuration modifications
- Environment adjustments
- Systematic elimination
## Investigation Techniques
### **Safe Code Analysis**
- **File Reading**: Use `read_file` tool for targeted inspection
- **Pattern Searching**: Use `grep_search` for code patterns
- **Semantic Search**: Use `codebase_search` for related functionality
- **Import Tracing**: Follow dependency chains systematically
### **Error Analysis**
- **Stack Trace Analysis**: Identify error origin and propagation
- **Log Correlation**: Match errors with system events
- **Timeline Reconstruction**: Build sequence of events
- **Context Preservation**: Maintain investigation state
### **Platform Considerations**
- **Web Platform**: Browser-specific behaviors and limitations
- **Electron Platform**: Desktop app considerations
- **Capacitor Platform**: Mobile app behaviors
- **Cross-Platform**: Shared vs. platform-specific code
## Evidence Collection Standards
### **Timestamps**
- **UTC Format**: All timestamps in UTC for consistency
- **Precision**: Include milliseconds for precise correlation
- **Context**: Include relevant system state information
- **Correlation**: Link events across different components
### **Error Context**
- **Full Error Objects**: Capture complete error information
- **Stack Traces**: Preserve call stack for analysis
- **User Actions**: Document steps leading to error
- **System State**: Capture relevant configuration and state
### **Reproduction Steps**
- **Clear Sequence**: Step-by-step reproduction instructions
- **Environment Details**: Platform, version, configuration
- **Data Requirements**: Required data or state
- **Expected vs. Actual**: Clear behavior comparison
## Investigation Documentation
### **Problem Summary**
- **Issue Description**: Clear, concise problem statement
- **Impact Assessment**: Severity and user impact
- **Scope Definition**: Affected components and users
- **Priority Level**: Based on impact and frequency
### **Investigation Log**
- **Timeline**: Chronological investigation steps
- **Evidence**: Collected information and findings
- **Hypotheses**: Tested theories and results
- **Conclusions**: Root cause identification
### **Solution Requirements**
- **Fix Description**: Required changes and approach
- **Testing Strategy**: Validation and verification steps
- **Rollback Plan**: Reversion strategy if needed
- **Prevention Measures**: Future issue prevention
## Quality Standards
### **Investigation Completeness**
- **Evidence Sufficiency**: Adequate information for root cause
- **Alternative Theories**: Considered and eliminated
- **Platform Coverage**: All relevant platforms investigated
- **Edge Cases**: Unusual scenarios considered
### **Documentation Quality**
- **Clear Communication**: Understandable to all stakeholders
- **Technical Accuracy**: Precise technical details
- **Actionable Insights**: Clear next steps and recommendations
- **Knowledge Transfer**: Lessons learned for future reference
## Common Pitfalls
- **Don't skip evidence collection** - leads to speculation
- **Don't ignore platform differences** - misses platform-specific issues
- **Don't skip documentation** - loses investigation insights
- **Don't assume root cause** - verify with evidence
- **Don't ignore time context** - misses temporal factors
- **Don't skip reproduction steps** - makes verification impossible
### **Investigation Mistakes**
## Integration Points
- **Jumping to Solutions**: Implementing fixes before understanding
- **Insufficient Evidence**: Making assumptions without data
- **Platform Blindness**: Ignoring platform-specific behaviors
- **Scope Creep**: Expanding investigation beyond original problem
### **With Other Meta-Rules**
### **Communication Issues**
- **Feature Planning**: Use complexity assessment for investigation planning
- **Bug Fixing**: Investigation results feed directly into fix implementation
- **Feature Implementation**: Investigation insights inform future development
- **Technical Jargon**: Using unclear terminology
- **Missing Context**: Insufficient background information
- **Unclear Recommendations**: Vague or ambiguous next steps
- **Poor Documentation**: Incomplete or unclear investigation records
### **With Development Workflow**
## Success Criteria
- Investigation findings inform testing strategy
- Root cause analysis drives preventive measures
- Evidence collection improves logging standards
- [ ] **Problem clearly defined** with sufficient evidence
- [ ] **Root cause identified** through systematic investigation
- [ ] **Solution approach determined** with clear requirements
- [ ] **Documentation complete** for knowledge transfer
- [ ] **No chat-blocking commands** used during investigation
- [ ] **Platform considerations** properly addressed
- [ ] **Timeline and context** properly documented
## Feedback & Improvement
## Integration with Other Meta-Rules
### **Sub-Rule Ratings (1-5 scale)**
### **Bug Fixing**
- **Research Diagnostic**: ___/5 - Comments: _______________
- **Investigation Report**: ___/5 - Comments: _______________
- **Technical Guide Creation**: ___/5 - Comments: _______________
- **Logging Standards**: ___/5 - Comments: _______________
- **Time Standards**: ___/5 - Comments: _______________
- **Investigation Results**: Provide foundation for fix implementation
- **Solution Requirements**: Define what needs to be built
- **Testing Strategy**: Inform validation approach
- **Documentation**: Support implementation guidance
### **Workflow Feedback**
### **Feature Planning**
- **Investigation Effectiveness**: How well did the process help find root cause?
- **Missing Steps**: What investigation steps should be added?
- **Process Gaps**: Where did the workflow break down?
- **Root Cause Analysis**: Identify systemic issues
- **Prevention Measures**: Plan future issue avoidance
- **Architecture Improvements**: Identify structural enhancements
- **Process Refinements**: Improve development workflows
### **Sub-Rule Improvements**
### **Research and Documentation**
- **Clarity Issues**: Which rules were unclear or confusing?
- **Missing Examples**: What examples would make rules more useful?
- **Template Improvements**: How could investigation templates be better?
### **Overall Experience**
- **Time Saved**: How much time did this meta-rule save you?
- **Quality Improvement**: Did following these rules improve your investigation?
- **Recommendation**: Would you recommend this meta-rule to others?
## Model Implementation Checklist
### Before Bug Investigation
- [ ] **Problem Definition**: Clearly define what needs to be investigated
- [ ] **Scope Definition**: Determine investigation scope and boundaries
- [ ] **Evidence Planning**: Plan evidence collection strategy
- [ ] **Stakeholder Identification**: Identify who needs to be involved
### During Bug Investigation
- [ ] **Rule Application**: Apply bundled rules in recommended sequence
- [ ] **Evidence Collection**: Collect evidence systematically with timestamps
- [ ] **Documentation**: Document investigation process and findings
- [ ] **Validation**: Verify findings with reproduction steps
### After Bug Investigation
- [ ] **Report Creation**: Create comprehensive investigation report
- [ ] **Root Cause Analysis**: Document root cause with evidence
- [ ] **Feedback Collection**: Collect feedback on meta-rule effectiveness
- [ ] **Process Improvement**: Identify improvements for future investigations
- **Knowledge Base**: Contribute to troubleshooting guides
- **Pattern Recognition**: Identify common failure modes
- **Best Practices**: Develop investigation methodologies
- **Team Training**: Improve investigation capabilities
---
**See also**:
- `.cursor/rules/meta_feature_planning.mdc` for planning investigation work
- `.cursor/rules/meta_bug_fixing.mdc` for implementing fixes
- `.cursor/rules/meta_feature_implementation.mdc` for preventive measures
- `.cursor/rules/meta_feature_planning.mdc` for planning improvements
- `.cursor/rules/meta_documentation.mdc` for documentation standards
**Status**: Active meta-rule for bug diagnosis
**Priority**: High

View File

@@ -10,6 +10,45 @@ This meta-rule bundles all the rules needed for implementing bug fixes
with proper testing and validation. Use this after diagnosis when
implementing the actual fix.
## Workflow Constraints
**This meta-rule enforces FIXING MODE for all bundled sub-rules:**
```json
{
"workflowMode": "fixing",
"constraints": {
"mode": "implementation",
"allowed": ["modify", "create", "build", "test", "commit"],
"required": "diagnosis_complete_before_fixing"
}
}
```
**All bundled sub-rules automatically inherit these constraints.**
## Workflow State Update
**When this meta-rule is invoked, update the workflow state file:**
```json
{
"currentMode": "fixing",
"lastInvoked": "meta_bug_fixing.mdc",
"timestamp": "2025-01-27T15:30:00Z",
"constraints": {
"mode": "implementation",
"allowed": ["modify", "create", "build", "test", "commit"],
"forbidden": [],
"required": "diagnosis_complete_before_fixing"
}
}
```
**State File Location**: `.cursor/rules/.workflow_state.json`
**This enables the core always-on rule to enforce fixing mode constraints.**
## When to Use
- **Post-Diagnosis**: After root cause is identified and fix is planned

View File

@@ -0,0 +1,383 @@
# Meta-Rule: Change Evaluation and Breaking Change Detection
**Author**: Matthew Raymer
**Date**: 2025-08-25
**Status**: 🎯 **ACTIVE** - Manually activated change evaluation rule
## Purpose
This meta-rule provides a systematic approach to evaluate changes between
branches and detect potential breaking changes. It's designed to catch
problematic model behavior by analyzing the nature, scope, and impact of
code changes before they cause issues.
## When to Use
**Manual Activation Only** - This rule should be invoked when:
- Reviewing changes before merging branches
- Investigating unexpected behavior after updates
- Validating that model-generated changes are safe
- Analyzing the impact of recent commits
- Debugging issues that may be caused by recent changes
## Workflow State Enforcement
**This meta-rule enforces current workflow mode constraints:**
### **Current Workflow State**
```json
{
"workflowState": {
"currentMode": "diagnosis|fixing|planning|research|documentation",
"constraints": {
"mode": "read_only|implementation|design_only|investigation|writing_only",
"allowed": ["array", "of", "allowed", "actions"],
"forbidden": ["array", "of", "forbidden", "actions"]
}
}
}
```
### **Mode-Specific Enforcement**
**Diagnosis Mode (read_only):**
- ❌ **Forbidden**: File modification, code creation, build commands, git
commits
- ✅ **Allowed**: File reading, code analysis, investigation, documentation
- **Response**: Focus on analysis and documentation, not implementation
**Fixing Mode (implementation):**
- ✅ **Allowed**: File modification, code creation, build commands, testing,
git commits
- ❌ **Forbidden**: None (full implementation mode)
- **Response**: Proceed with implementation and testing
**Planning Mode (design_only):**
- ❌ **Forbidden**: Implementation, coding, building, deployment
- ✅ **Allowed**: Analysis, design, estimation, documentation, architecture
- **Response**: Focus on planning and design, not implementation
**Research Mode (investigation):**
- ❌ **Forbidden**: File modification, implementation, deployment
- ✅ **Allowed**: Investigation, analysis, research, documentation
- **Response**: Focus on investigation and analysis
**Documentation Mode (writing_only):**
- ❌ **Forbidden**: Implementation, coding, building, deployment
- ✅ **Allowed**: Writing, editing, formatting, structuring, reviewing
- **Response**: Focus on documentation creation and improvement
## Change Evaluation Process
### **Phase 1: Change Discovery and Analysis**
1. **Branch Comparison Analysis**
- Compare working branch with master/main branch
- Identify all changed files and their modification types
- Categorize changes by scope and impact
2. **Change Pattern Recognition**
- Identify common change patterns (refactoring, feature addition, bug
fixes)
- Detect unusual or suspicious change patterns
- Flag changes that deviate from established patterns
3. **Dependency Impact Assessment**
- Analyze changes to imports, exports, and interfaces
- Identify potential breaking changes to public APIs
- Assess impact on dependent components and services
### **Phase 2: Breaking Change Detection**
1. **API Contract Analysis**
- Check for changes to function signatures, method names, class
interfaces
- Identify removed or renamed public methods/properties
- Detect changes to configuration options and constants
2. **Data Structure Changes**
- Analyze database schema modifications
- Check for changes to data models and interfaces
- Identify modifications to serialization/deserialization logic
3. **Behavioral Changes**
- Detect changes to business logic and algorithms
- Identify modifications to error handling and validation
- Check for changes to user experience and workflows
### **Phase 3: Risk Assessment and Recommendations**
1. **Risk Level Classification**
- **LOW**: Cosmetic changes, documentation updates, minor refactoring
- **MEDIUM**: Internal API changes, configuration modifications,
performance improvements
- **HIGH**: Public API changes, breaking interface modifications, major
architectural changes
- **CRITICAL**: Database schema changes, authentication modifications,
security-related changes
2. **Impact Analysis**
- Identify affected user groups and use cases
- Assess potential for data loss or corruption
- Evaluate impact on system performance and reliability
3. **Mitigation Strategies**
- Recommend testing approaches for affected areas
- Suggest rollback strategies if needed
- Identify areas requiring additional validation
## Implementation Guidelines
### **Change Analysis Tools**
1. **Git Diff Analysis**
```bash
# Compare working branch with master
git diff master..HEAD --name-only
git diff master..HEAD --stat
git log master..HEAD --oneline
```
2. **File Change Categorization**
- **Core Files**: Application entry points, main services, critical
utilities
- **Interface Files**: Public APIs, component interfaces, data models
- **Configuration Files**: Environment settings, build configurations,
deployment scripts
- **Test Files**: Unit tests, integration tests, test utilities
3. **Change Impact Mapping**
- Map changed files to affected functionality
- Identify cross-dependencies and ripple effects
- Document potential side effects and unintended consequences
### **Breaking Change Detection Patterns**
1. **Function Signature Changes**
```typescript
// BEFORE
function processData(data: string, options?: Options): Result
// AFTER - BREAKING CHANGE
function processData(data: string, options: Required<Options>): Result
```
2. **Interface Modifications**
```typescript
// BEFORE
interface UserProfile {
name: string;
email: string;
}
// AFTER - BREAKING CHANGE
interface UserProfile {
name: string;
email: string;
phone: string; // Required new field
}
```
3. **Configuration Changes**
```typescript
// BEFORE
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
// AFTER - BREAKING CHANGE
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3 // New required configuration
};
```
## Output Format
### **Change Evaluation Report**
```markdown
# Change Evaluation Report
## Executive Summary
- **Risk Level**: [LOW|MEDIUM|HIGH|CRITICAL]
- **Overall Assessment**: [SAFE|CAUTION|DANGEROUS|CRITICAL]
- **Recommendation**: [PROCEED|REVIEW|HALT|IMMEDIATE_ROLLBACK]
## Change Analysis
### Files Modified
- **Total Changes**: [X] files
- **Core Files**: [X] files
- **Interface Files**: [X] files
- **Configuration Files**: [X] files
- **Test Files**: [X] files
### Change Categories
- **Refactoring**: [X] changes
- **Feature Addition**: [X] changes
- **Bug Fixes**: [X] changes
- **Configuration**: [X] changes
- **Documentation**: [X] changes
## Breaking Change Detection
### API Contract Changes
- **Function Signatures**: [X] modified
- **Interface Definitions**: [X] modified
- **Public Methods**: [X] added/removed/modified
### Data Structure Changes
- **Database Schema**: [X] modifications
- **Data Models**: [X] changes
- **Serialization**: [X] changes
### Behavioral Changes
- **Business Logic**: [X] modifications
- **Error Handling**: [X] changes
- **User Experience**: [X] changes
## Risk Assessment
### Impact Analysis
- **User Groups Affected**: [Description]
- **Use Cases Impacted**: [Description]
- **Performance Impact**: [Description]
- **Reliability Impact**: [Description]
### Dependencies
- **Internal Dependencies**: [List]
- **External Dependencies**: [List]
- **Configuration Dependencies**: [List]
## Recommendations
### Testing Requirements
- [ ] Unit tests for modified components
- [ ] Integration tests for affected workflows
- [ ] Performance tests for changed algorithms
- [ ] User acceptance tests for UI changes
### Validation Steps
- [ ] Code review by domain experts
- [ ] API compatibility testing
- [ ] Database migration testing
- [ ] End-to-end workflow testing
### Rollback Strategy
- **Rollback Complexity**: [LOW|MEDIUM|HIGH]
- **Rollback Time**: [Estimated time]
- **Data Preservation**: [Strategy description]
## Conclusion
[Summary of findings and final recommendation]
```
## Usage Examples
### **Example 1: Safe Refactoring**
```bash
@meta_change_evaluation.mdc analyze changes between feature-branch and master
```
### **Example 2: Breaking Change Investigation**
```bash
@meta_change_evaluation.mdc evaluate potential breaking changes in recent commits
```
### **Example 3: Pre-Merge Validation**
```bash
@meta_change_evaluation.mdc validate changes before merging feature-branch to master
```
## Success Criteria
- [ ] **Change Discovery**: All modified files are identified and categorized
- [ ] **Pattern Recognition**: Unusual change patterns are detected and flagged
- [ ] **Breaking Change Detection**: All potential breaking changes are identified
- [ ] **Risk Assessment**: Accurate risk levels are assigned with justification
- [ ] **Recommendations**: Actionable recommendations are provided
- [ ] **Documentation**: Complete change evaluation report is generated
## Common Pitfalls
- **Missing Dependencies**: Failing to identify all affected components
- **Underestimating Impact**: Not considering ripple effects of changes
- **Incomplete Testing**: Missing critical test scenarios for changes
- **Configuration Blindness**: Overlooking configuration file changes
- **Interface Assumptions**: Assuming internal changes won't affect external
users
## Integration with Other Meta-Rules
### **With Bug Diagnosis**
- Use change evaluation to identify recent changes that may have caused
bugs
- Correlate change patterns with reported issues
### **With Feature Planning**
- Evaluate the impact of planned changes before implementation
- Identify potential breaking changes early in the planning process
### **With Bug Fixing**
- Validate that fixes don't introduce new breaking changes
- Ensure fixes maintain backward compatibility
---
**See also**:
- `.cursor/rules/meta_core_always_on.mdc` for core always-on rules
- `.cursor/rules/meta_feature_planning.mdc` for feature development
workflows
- `.cursor/rules/meta_bug_diagnosis.mdc` for bug investigation workflows
- `.cursor/rules/meta_bug_fixing.mdc` for fix implementation workflows
**Status**: Active change evaluation meta-rule
**Priority**: High (applies to all change evaluation tasks)
**Estimated Effort**: Ongoing reference
**Dependencies**: All bundled sub-rules
**Stakeholders**: Development team, Quality Assurance team, Release
Management team

View File

@@ -1,7 +1,6 @@
---
alwaysApply: true
alwaysApply: false
---
# Meta-Rule: Core Always-On Rules
**Author**: Matthew Raymer
@@ -14,6 +13,109 @@ This meta-rule bundles the core rules that should be applied to **every single
prompt** because they define fundamental behaviors, principles, and context
that are essential for all AI interactions.
## Workflow Constraints
**This meta-rule enforces ALWAYS-ON MODE for all bundled sub-rules:**
```json
{
"workflowMode": "always_on",
"constraints": {
"mode": "foundation",
"alwaysApplied": true,
"required": "applied_to_every_prompt"
}
}
```
**All bundled sub-rules automatically inherit these constraints.**
## Workflow State Enforcement
**This meta-rule enforces current workflow mode constraints for all interactions:**
### **Current Workflow State**
```json
{
"workflowState": {
"currentMode": "diagnosis|fixing|planning|research|documentation",
"constraints": {
"mode": "read_only|implementation|design_only|investigation|writing_only",
"allowed": ["array", "of", "allowed", "actions"],
"forbidden": ["array", "of", "forbidden", "actions"]
}
}
}
```
### **Constraint Enforcement Rules**
**Before responding to any user request, enforce current mode constraints:**
1. **Read current workflow state** from `.cursor/rules/.workflow_state.json`
2. **Identify current mode** and its constraints
3. **Validate user request** against current mode constraints
4. **Enforce constraints** before generating response
5. **Guide model behavior** based on current mode
### **Mode-Specific Enforcement**
**Diagnosis Mode (read_only):**
- ❌ **Forbidden**: File modification, code creation, build commands, git commits
- ✅ **Allowed**: File reading, code analysis, investigation, documentation
- **Response**: Guide user toward investigation and analysis, not implementation
**Fixing Mode (implementation):**
- ✅ **Allowed**: File modification, code creation, build commands, testing, git commits
- ❌ **Forbidden**: None (full implementation mode)
- **Response**: Proceed with implementation and testing
**Planning Mode (design_only):**
- ❌ **Forbidden**: Implementation, coding, building, deployment
- ✅ **Allowed**: Analysis, design, estimation, documentation, architecture
- **Response**: Focus on planning and design, not implementation
**Research Mode (investigation):**
- ❌ **Forbidden**: File modification, implementation, deployment
- ✅ **Allowed**: Investigation, analysis, research, documentation
- **Response**: Focus on investigation and analysis
**Documentation Mode (writing_only):**
- ❌ **Forbidden**: Implementation, coding, building, deployment
- ✅ **Allowed**: Writing, editing, formatting, structuring, reviewing
- **Response**: Focus on documentation creation and improvement
### **Constraint Violation Response**
**If user request violates current mode constraints:**
```
❌ **WORKFLOW CONSTRAINT VIOLATION**
**Current Mode**: [MODE_NAME]
**Requested Action**: [ACTION]
**Constraint Violation**: [DESCRIPTION]
**What You Can Do Instead**:
- [LIST OF ALLOWED ALTERNATIVES]
**To Enable This Action**: Invoke @meta_[appropriate_mode].mdc
```
### **Mode Transition Guidance**
**When user needs to change modes, provide clear guidance:**
```
🔄 **MODE TRANSITION REQUIRED**
**Current Mode**: [CURRENT_MODE]
**Required Mode**: [REQUIRED_MODE]
**Action**: Invoke @meta_[required_mode].mdc
**This will enable**: [DESCRIPTION OF NEW CAPABILITIES]
```
## When to Use
**ALWAYS** - These rules apply to every single prompt, regardless of the task
@@ -165,6 +267,8 @@ or context. They form the foundation for all AI assistant behavior.
- [ ] **Time Standards**: Verify UTC and timestamp requirements are clear
- [ ] **Application Context**: Confirm TimeSafari context is loaded
- [ ] **Version Control**: Prepare commit standards if code changes are needed
- [ ] **Workflow State**: Read current mode constraints from state file
- [ ] **Constraint Validation**: Validate user request against current mode
### During Response Creation
@@ -172,6 +276,8 @@ or context. They form the foundation for all AI assistant behavior.
- [ ] **Competence Hooks**: Include learning and collaboration elements
- [ ] **Time Consistency**: Apply UTC standards for all time references
- [ ] **Platform Awareness**: Consider all target platforms
- [ ] **Mode Enforcement**: Apply current mode constraints to response
- [ ] **Constraint Violations**: Block forbidden actions and guide alternatives
### After Response Creation
@@ -179,18 +285,23 @@ or context. They form the foundation for all AI assistant behavior.
- [ ] **Quality Check**: Ensure response meets competence standards
- [ ] **Context Review**: Confirm application context was properly considered
- [ ] **Feedback Collection**: Note any issues with always-on application
- [ ] **Mode Compliance**: Verify response stayed within current mode constraints
- [ ] **Transition Guidance**: Provide clear guidance for mode changes if needed
---
**See also**:
- `.cursor/rules/meta_feature_planning.mdc` for workflow-specific rules
- `.cursor/rules/meta_bug_diagnosis.mdc` for investigation workflows
- `.cursor/rules/meta_bug_fixing.mdc` for fix implementation
- `.cursor/rules/meta_feature_implementation.mdc` for feature development
**Status**: Active core always-on meta-rule
**Priority**: Critical (applies to every prompt)
**Estimated Effort**: Ongoing reference
**Dependencies**: All bundled sub-rules
**Stakeholders**: All AI interactions, Development team
**Dependencies**: All bundled sub-rules
**Stakeholders**: All AI interactions, Development team
**Dependencies**: All bundled sub-rules
**Stakeholders**: All AI interactions, Development team

View File

@@ -10,6 +10,44 @@ This meta-rule bundles documentation-related rules to create comprehensive,
educational documentation that increases human competence rather than just
providing technical descriptions.
## Workflow Constraints
**This meta-rule enforces DOCUMENTATION MODE for all bundled sub-rules:**
```json
{
"workflowMode": "documentation",
"constraints": {
"mode": "writing_only",
"allowed": ["write", "edit", "format", "structure", "review"],
"forbidden": ["implement", "code", "build", "deploy"]
}
}
```
**All bundled sub-rules automatically inherit these constraints.**
## Workflow State Update
**When this meta-rule is invoked, update the workflow state file:**
```json
{
"currentMode": "documentation",
"lastInvoked": "meta_documentation.mdc",
"timestamp": "2025-01-27T15:30:00Z",
"constraints": {
"mode": "writing_only",
"allowed": ["write", "edit", "format", "structure", "review"],
"forbidden": ["implement", "code", "build", "deploy"]
}
}
```
**State File Location**: `.cursor/rules/.workflow_state.json`
**This enables the core always-on rule to enforce documentation mode constraints.**
## When to Use
**Use this meta-rule when**:

View File

@@ -10,6 +10,45 @@ This meta-rule bundles all the rules needed for building features with
proper architecture and cross-platform support. Use this when implementing
planned features or refactoring existing code.
## Workflow Constraints
**This meta-rule enforces IMPLEMENTATION MODE for all bundled sub-rules:**
```json
{
"workflowMode": "implementation",
"constraints": {
"mode": "development",
"allowed": ["code", "build", "test", "refactor", "deploy"],
"required": "planning_complete_before_implementation"
}
}
```
**All bundled sub-rules automatically inherit these constraints.**
## Workflow State Update
**When this meta-rule is invoked, update the workflow state file:**
```json
{
"currentMode": "implementation",
"lastInvoked": "meta_feature_implementation.mdc",
"timestamp": "2025-01-27T15:30:00Z",
"constraints": {
"mode": "development",
"allowed": ["code", "build", "test", "refactor", "deploy"],
"forbidden": [],
"required": "planning_complete_before_implementation"
}
}
```
**State File Location**: `.cursor/rules/.workflow_state.json`
**This enables the core always-on rule to enforce implementation mode constraints.**
## When to Use
- **Feature Development**: Building new features from planning

View File

@@ -10,6 +10,44 @@ This meta-rule bundles all the rules needed for comprehensive feature planning
across all platforms. Use this when starting any new feature development,
planning sprints, or estimating work effort.
## Workflow Constraints
**This meta-rule enforces PLANNING MODE for all bundled sub-rules:**
```json
{
"workflowMode": "planning",
"constraints": {
"mode": "design_only",
"allowed": ["analyze", "plan", "design", "estimate", "document"],
"forbidden": ["implement", "code", "build", "test", "deploy"]
}
}
```
**All bundled sub-rules automatically inherit these constraints.**
## Workflow State Update
**When this meta-rule is invoked, update the workflow state file:**
```json
{
"currentMode": "planning",
"lastInvoked": "meta_feature_planning.mdc",
"timestamp": "2025-01-27T15:30:00Z",
"constraints": {
"mode": "design_only",
"allowed": ["analyze", "plan", "design", "estimate", "document"],
"forbidden": ["implement", "code", "build", "test", "deploy"]
}
}
```
**State File Location**: `.cursor/rules/.workflow_state.json`
**This enables the core always-on rule to enforce planning mode constraints.**
## When to Use
- **New Feature Development**: Planning features from concept to implementation

View File

@@ -11,6 +11,44 @@ systematic investigation, analysis, evidence collection, or research tasks. It p
a comprehensive framework for thorough, methodical research workflows that produce
actionable insights and evidence-based conclusions.
## Workflow Constraints
**This meta-rule enforces RESEARCH MODE for all bundled sub-rules:**
```json
{
"workflowMode": "research",
"constraints": {
"mode": "investigation",
"allowed": ["read", "search", "analyze", "plan"],
"forbidden": ["modify", "create", "build", "commit"]
}
}
```
**All bundled sub-rules automatically inherit these constraints.**
## Workflow State Update
**When this meta-rule is invoked, update the workflow state file:**
```json
{
"currentMode": "research",
"lastInvoked": "meta_research.mdc",
"timestamp": "2025-01-27T15:30:00Z",
"constraints": {
"mode": "investigation",
"allowed": ["read", "search", "analyze", "plan"],
"forbidden": ["modify", "create", "build", "commit"]
}
}
```
**State File Location**: `.cursor/rules/.workflow_state.json`
**This enables the core always-on rule to enforce research mode constraints.**
## When to Use
**RESEARCH TASKS** - Apply this meta-rule when:

View File

@@ -5,7 +5,7 @@
**Status**: 🎯 **ACTIVE** - Version control guidelines
## Core Principles
### 0) let the developer control git
### 1) Version-Control Ownership
- **MUST NOT** run `git add`, `git commit`, or any write action.

3
.gitignore vendored
View File

@@ -54,6 +54,9 @@ build_logs/
# Guard feedback logs (for continuous improvement analysis)
.guard-feedback.log
# Workflow state file (contains dynamic state, not version controlled)
.cursor/rules/.workflow_state.json
# PWA icon files generated by capacitor-assets
icons

View File

@@ -19,15 +19,16 @@ npm run lint-fix || {
}
# Then run Build Architecture Guard
echo "🏗️ Running Build Architecture Guard..."
bash ./scripts/build-arch-guard.sh --staged || {
echo
echo "❌ Build Architecture Guard failed. Please fix the issues and try again."
echo "💡 To bypass this check for emergency commits, use:"
echo " git commit --no-verify"
echo
exit 1
}
#echo "🏗️ Running Build Architecture Guard..."
#bash ./scripts/build-arch-guard.sh --staged || {
# echo
# echo "❌ Build Architecture Guard failed. Please fix the issues and try again."
# echo "💡 To bypass this check for emergency commits, use:"
# echo " git commit --no-verify"
# echo
# exit 1
#}
echo "✅ All pre-commit checks passed!"

View File

@@ -18,10 +18,10 @@ else
RANGE="HEAD~1..HEAD"
fi
bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
echo
echo "💡 To bypass this check for emergency pushes, use:"
echo " git push --no-verify"
echo
exit 1
}
#bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
# echo
# echo "💡 To bypass this check for emergency pushes, use:"
# echo " git push --no-verify"
# echo
# exit 1
#}

View File

@@ -617,7 +617,8 @@ The Electron build process follows a multi-stage approach:
#### **Stage 2: Capacitor Sync**
- Copies web assets to Electron app directory
- Syncs Capacitor configuration and plugins
- Uses Electron-specific Capacitor configuration (not copied from main config)
- Syncs Capacitor plugins for Electron platform
- Prepares native module bindings
#### **Stage 3: TypeScript Compile**
@@ -2768,6 +2769,45 @@ configuration files in the repository.
---
### 2025-08-26 - Capacitor Plugin Additions
#### New Capacitor Plugins Added
- **Added**: `@capacitor/clipboard` v6.0.2 - Clipboard functionality for mobile platforms
- **Purpose**: Enable copy/paste operations on mobile devices
- **Platforms**: iOS and Android
- **Features**: Read/write clipboard content, text handling
- **Integration**: Automatically included in mobile builds
- **Added**: `@capacitor/status-bar` v6.0.2 - Status bar management for mobile platforms
- **Purpose**: Control mobile device status bar appearance and behavior
- **Platforms**: iOS and Android
- **Features**: Status bar styling, visibility control, color management
- **Integration**: Automatically included in mobile builds
#### Android Build System Updates
- **Modified**: `android/capacitor.settings.gradle` - Added new plugin project includes
- **Added**: `:capacitor-clipboard` project directory mapping
- **Added**: `:capacitor-status-bar` project directory mapping
- **Impact**: New plugins now properly integrated into Android build process
#### Package Dependencies
- **Updated**: `package.json` - Added new Capacitor plugin dependencies
- **Updated**: `package-lock.json` - Locked dependency versions for consistency
- **Version**: All new plugins use Capacitor 6.x compatible versions
#### Build Process Impact
- **No Breaking Changes**: Existing build commands continue to work unchanged
- **Enhanced Mobile Features**: New clipboard and status bar capabilities available
- **Automatic Integration**: Plugins automatically included in mobile builds
- **Platform Support**: Both iOS and Android builds now include new functionality
#### Testing Requirements
- **Mobile Builds**: Verify new plugins integrate correctly in iOS and Android builds
- **Functionality**: Test clipboard operations and status bar management on devices
- **Fallback**: Ensure graceful degradation when plugins are unavailable
---
**Note**: This documentation is maintained alongside the build system. For the
most up-to-date information, refer to the actual script files and Vite
configuration files in the repository.

View File

@@ -0,0 +1,852 @@
# TimeSafari Code Quality: Comprehensive Deep Analysis
**Author**: Matthew Raymer
**Date**: Tue Sep 16 05:22:10 AM UTC 2025
**Status**: 🎯 **COMPREHENSIVE ANALYSIS** - Complete code quality assessment with actionable recommendations
## Executive Summary
The TimeSafari codebase demonstrates **exceptional code quality** with mature patterns, minimal technical debt, and excellent separation of concerns. This comprehensive analysis covers **291 source files** totaling **104,527 lines** of code, including detailed examination of **94 Vue components and views**.
**Key Quality Metrics:**
- **Technical Debt**: Extremely low (6 TODO/FIXME comments across entire codebase)
- **Database Migration**: 99.5% complete (1 remaining legacy import)
- **File Complexity**: High variance (largest file: 2,215 lines)
- **Type Safety**: Mixed patterns (41 "as any" assertions in Vue files, 62 total)
- **Error Handling**: Comprehensive (367 catch blocks with good coverage)
- **Architecture**: Consistent Vue 3 Composition API with TypeScript
## Vue Components & Views Analysis (94 Files)
### Component Analysis (40 Components)
#### Component Size Distribution
```
Large Components (>500 lines): 5 components (12.5%)
├── ImageMethodDialog.vue (947 lines) 🔴 CRITICAL
├── GiftedDialog.vue (670 lines) ⚠️ HIGH PRIORITY
├── PhotoDialog.vue (669 lines) ⚠️ HIGH PRIORITY
├── PushNotificationPermission.vue (660 lines) ⚠️ HIGH PRIORITY
└── MembersList.vue (550 lines) ⚠️ MODERATE PRIORITY
Medium Components (200-500 lines): 12 components (30%)
├── GiftDetailsStep.vue (450 lines)
├── EntityGrid.vue (348 lines)
├── ActivityListItem.vue (334 lines)
├── OfferDialog.vue (327 lines)
├── OnboardingDialog.vue (314 lines)
├── EntitySelectionStep.vue (313 lines)
├── GiftedPrompts.vue (293 lines)
├── ChoiceButtonDialog.vue (250 lines)
├── DataExportSection.vue (251 lines)
├── AmountInput.vue (224 lines)
├── HiddenDidDialog.vue (220 lines)
└── FeedFilters.vue (218 lines)
Small Components (<200 lines): 23 components (57.5%)
├── ContactListItem.vue (217 lines)
├── EntitySummaryButton.vue (202 lines)
├── IdentitySection.vue (186 lines)
├── ContactInputForm.vue (173 lines)
├── SpecialEntityCard.vue (156 lines)
├── RegistrationNotice.vue (154 lines)
├── ContactNameDialog.vue (154 lines)
├── PersonCard.vue (153 lines)
├── UserNameDialog.vue (147 lines)
├── InfiniteScroll.vue (132 lines)
├── LocationSearchSection.vue (124 lines)
├── UsageLimitsSection.vue (123 lines)
├── QuickNav.vue (118 lines)
├── ProjectCard.vue (104 lines)
├── ContactListHeader.vue (101 lines)
├── TopMessage.vue (98 lines)
├── InviteDialog.vue (95 lines)
├── ImageViewer.vue (94 lines)
├── EntityIcon.vue (86 lines)
├── ShowAllCard.vue (66 lines)
├── ContactBulkActions.vue (53 lines)
├── ProjectIcon.vue (47 lines)
└── LargeIdenticonModal.vue (44 lines)
```
#### Critical Component Analysis
**1. `ImageMethodDialog.vue` (947 lines) 🔴 CRITICAL REFACTORING NEEDED**
**Issues Identified:**
- **Excessive Single Responsibility**: Handles camera preview, file upload, URL input, cropping, diagnostics, and error handling
- **Complex State Management**: 20+ reactive properties with interdependencies
- **Mixed Concerns**: Camera API, file handling, UI state, and business logic intertwined
- **Template Complexity**: ~300 lines of template with deeply nested conditions
**Refactoring Strategy:**
```typescript
// Current monolithic structure
ImageMethodDialog.vue (947 lines) {
CameraPreview: ~200 lines
FileUpload: ~150 lines
URLInput: ~100 lines
CroppingInterface: ~200 lines
DiagnosticsPanel: ~150 lines
ErrorHandling: ~100 lines
StateManagement: ~47 lines
}
// Proposed component decomposition
ImageMethodDialog.vue (coordinator, ~200 lines)
CameraPreviewComponent.vue (~250 lines)
FileUploadComponent.vue (~150 lines)
URLInputComponent.vue (~100 lines)
ImageCropperComponent.vue (~200 lines)
DiagnosticsPanelComponent.vue (~150 lines)
ImageUploadErrorHandler.vue (~100 lines)
```
**2. `GiftedDialog.vue` (670 lines) ⚠️ HIGH PRIORITY**
**Assessment**: **GOOD** - Already partially refactored with step components extracted.
**3. `PhotoDialog.vue` (669 lines) ⚠️ HIGH PRIORITY**
**Issues**: Similar to ImageMethodDialog with significant code duplication.
**4. `PushNotificationPermission.vue` (660 lines) ⚠️ HIGH PRIORITY**
**Issues**: Complex permission logic with platform-specific code mixed together.
### View Analysis (54 Views)
#### View Size Distribution
```
Large Views (>1000 lines): 9 views (16.7%)
├── AccountViewView.vue (2,215 lines) 🔴 CRITICAL
├── HomeView.vue (1,852 lines) ⚠️ HIGH PRIORITY
├── ProjectViewView.vue (1,479 lines) ⚠️ HIGH PRIORITY
├── DatabaseMigration.vue (1,438 lines) ⚠️ HIGH PRIORITY
├── ContactsView.vue (1,382 lines) ⚠️ HIGH PRIORITY
├── TestView.vue (1,259 lines) ⚠️ MODERATE PRIORITY
├── ClaimView.vue (1,225 lines) ⚠️ MODERATE PRIORITY
├── NewEditProjectView.vue (957 lines) ⚠️ MODERATE PRIORITY
└── ContactQRScanShowView.vue (929 lines) ⚠️ MODERATE PRIORITY
Medium Views (500-1000 lines): 8 views (14.8%)
├── ConfirmGiftView.vue (898 lines)
├── DiscoverView.vue (888 lines)
├── DIDView.vue (848 lines)
├── GiftedDetailsView.vue (840 lines)
├── OfferDetailsView.vue (781 lines)
├── HelpView.vue (780 lines)
├── ProjectsView.vue (742 lines)
└── ContactQRScanFullView.vue (701 lines)
Small Views (<500 lines): 37 views (68.5%)
├── OnboardMeetingSetupView.vue (687 lines)
├── ContactImportView.vue (568 lines)
├── HelpNotificationsView.vue (566 lines)
├── OnboardMeetingListView.vue (507 lines)
├── InviteOneView.vue (475 lines)
├── QuickActionBvcEndView.vue (442 lines)
├── ContactAmountsView.vue (416 lines)
├── SearchAreaView.vue (384 lines)
├── SharedPhotoView.vue (379 lines)
├── ContactGiftingView.vue (373 lines)
├── ContactEditView.vue (345 lines)
├── IdentitySwitcherView.vue (324 lines)
├── UserProfileView.vue (323 lines)
├── NewActivityView.vue (323 lines)
├── QuickActionBvcBeginView.vue (303 lines)
├── SeedBackupView.vue (292 lines)
├── InviteOneAcceptView.vue (292 lines)
├── ClaimCertificateView.vue (279 lines)
├── StartView.vue (271 lines)
├── ImportAccountView.vue (265 lines)
├── ClaimAddRawView.vue (249 lines)
├── OnboardMeetingMembersView.vue (247 lines)
├── DeepLinkErrorView.vue (239 lines)
├── ClaimReportCertificateView.vue (236 lines)
├── DeepLinkRedirectView.vue (219 lines)
├── ImportDerivedAccountView.vue (207 lines)
├── ShareMyContactInfoView.vue (196 lines)
├── RecentOffersToUserProjectsView.vue (176 lines)
├── RecentOffersToUserView.vue (166 lines)
├── NewEditAccountView.vue (142 lines)
├── StatisticsView.vue (133 lines)
├── HelpOnboardingView.vue (118 lines)
├── LogView.vue (104 lines)
├── NewIdentifierView.vue (97 lines)
├── HelpNotificationTypesView.vue (73 lines)
├── ConfirmContactView.vue (57 lines)
└── QuickActionBvcView.vue (54 lines)
```
#### Critical View Analysis
**1. `AccountViewView.vue` (2,215 lines) 🔴 CRITICAL REFACTORING NEEDED**
**Issues Identified:**
- **Monolithic Architecture**: Handles 7 distinct concerns in single file
- **Template Complexity**: ~750 lines of template with deeply nested conditions
- **Method Proliferation**: 50+ methods handling disparate concerns
- **State Management**: 25+ reactive properties without clear organization
**Refactoring Strategy:**
```typescript
// Current monolithic structure
AccountViewView.vue (2,215 lines) {
ProfileSection: ~400 lines
SettingsSection: ~300 lines
NotificationSection: ~200 lines
ServerConfigSection: ~250 lines
ExportImportSection: ~300 lines
LimitsSection: ~150 lines
MapSection: ~200 lines
StateManagement: ~415 lines
}
// Proposed component extraction
AccountViewView.vue (coordinator, ~400 lines)
ProfileManagementSection.vue (~300 lines)
ServerConfigurationSection.vue (~250 lines)
NotificationSettingsSection.vue (~200 lines)
DataExportImportSection.vue (~300 lines)
UsageLimitsDisplay.vue (~150 lines)
LocationProfileSection.vue (~200 lines)
AccountViewStateManager.ts (~200 lines)
```
**2. `HomeView.vue` (1,852 lines) ⚠️ HIGH PRIORITY**
**Issues Identified:**
- **Multiple Concerns**: Activity feed, projects, contacts, notifications in one file
- **Complex State Management**: 20+ reactive properties with interdependencies
- **Mixed Lifecycle Logic**: Mount, update, and destroy logic intertwined
**3. `ProjectViewView.vue` (1,479 lines) ⚠️ HIGH PRIORITY**
**Issues Identified:**
- **Project Management Complexity**: Handles project details, members, offers, and activities
- **Mixed Concerns**: Project data, member management, and activity feed in single view
### Vue Component Quality Patterns
#### Excellent Patterns Found:
**1. EntityIcon.vue (86 lines) ✅ EXCELLENT**
```typescript
// Clean, focused responsibility
@Component({ name: "EntityIcon" })
export default class EntityIcon extends Vue {
@Prop() contact?: Contact;
@Prop({ default: "" }) entityId!: string;
@Prop({ default: 0 }) iconSize!: number;
generateIcon(): string {
// Clear priority order: profile image → avatar → fallback
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
if (imageUrl) return `<img src="${imageUrl}" ... />`;
const identifier = this.contact?.did || this.entityId;
if (!identifier) return `<img src="${blankSquareSvg}" ... />`;
return createAvatar(avataaars, { seed: identifier, size: this.iconSize }).toString();
}
}
```
**2. QuickNav.vue (118 lines) ✅ EXCELLENT**
```typescript
// Simple, focused navigation component
@Component({ name: "QuickNav" })
export default class QuickNav extends Vue {
@Prop selected = "";
// Clean template with consistent patterns
// Proper accessibility attributes
// Responsive design with safe area handling
}
```
**3. Small Focused Views ✅ EXCELLENT**
```typescript
// QuickActionBvcView.vue (54 lines) - Perfect size
// ConfirmContactView.vue (57 lines) - Focused responsibility
// HelpNotificationTypesView.vue (73 lines) - Clear purpose
// LogView.vue (104 lines) - Simple utility view
```
#### Problematic Patterns Found:
**1. Excessive Props in Dialog Components**
```typescript
// GiftedDialog.vue - Too many props
@Prop() fromProjectId = "";
@Prop() toProjectId = "";
@Prop() isFromProjectView = false;
@Prop() hideShowAll = false;
@Prop({ default: "person" }) giverEntityType = "person";
@Prop({ default: "person" }) recipientEntityType = "person";
// ... 10+ more props
```
**2. Complex State Machines**
```typescript
// ImageMethodDialog.vue - Complex state management
cameraState: "off" | "initializing" | "active" | "error" | "retrying" | "stopped" = "off";
showCameraPreview = false;
isRetrying = false;
showDiagnostics = false;
// ... 15+ more state properties
```
**3. Excessive Reactive Properties**
```typescript
// AccountViewView.vue - Too many reactive properties
downloadUrl: string = "";
loadingLimits: boolean = false;
loadingProfile: boolean = true;
showAdvanced: boolean = false;
showB64Copy: boolean = false;
showContactGives: boolean = false;
showDidCopy: boolean = false;
showDerCopy: boolean = false;
showGeneralAdvanced: boolean = false;
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
showPubCopy: boolean = false;
showShortcutBvc: boolean = false;
warnIfProdServer: boolean = false;
warnIfTestServer: boolean = false;
zoom: number = 2;
isMapReady: boolean = false;
// ... 10+ more properties
```
## File Size and Complexity Analysis (All Files)
### Problematic Large Files
#### 1. `AccountViewView.vue` (2,215 lines) 🔴 **CRITICAL**
**Issues Identified:**
- **Excessive Single File Responsibility**: Handles profile, settings, notifications, server configuration, export/import, limits checking
- **Template Complexity**: ~750 lines of template with deeply nested conditions
- **Method Proliferation**: 50+ methods handling disparate concerns
- **State Management**: 25+ reactive properties without clear organization
#### 2. `PlatformServiceMixin.ts` (2,091 lines) ⚠️ **HIGH PRIORITY**
**Issues Identified:**
- **God Object Pattern**: Single file handling 80+ methods across multiple concerns
- **Mixed Abstraction Levels**: Low-level SQL utilities mixed with high-level business logic
- **Method Length Variance**: Some methods 100+ lines, others single-line wrappers
**Refactoring Strategy:**
```typescript
// Current monolithic mixin
PlatformServiceMixin.ts (2,091 lines)
// Proposed separation of concerns
CoreDatabaseMixin.ts // $db, $exec, $query, $first (200 lines)
SettingsManagementMixin.ts // $settings, $saveSettings (400 lines)
ContactManagementMixin.ts // $contacts, $insertContact (300 lines)
EntityOperationsMixin.ts // $insertEntity, $updateEntity (400 lines)
CachingMixin.ts // Cache management (150 lines)
ActiveIdentityMixin.ts // Active DID management (200 lines)
UtilityMixin.ts // Mapping, JSON parsing (200 lines)
LoggingMixin.ts // $log, $logError (100 lines)
```
#### 3. `HomeView.vue` (1,852 lines) ⚠️ **MODERATE PRIORITY**
**Issues Identified:**
- **Multiple Concerns**: Activity feed, projects, contacts, notifications in one file
- **Complex State Management**: 20+ reactive properties with interdependencies
- **Mixed Lifecycle Logic**: Mount, update, and destroy logic intertwined
### File Size Distribution Analysis
```
Files > 1000 lines: 9 files (4.6% of codebase)
Files 500-1000 lines: 23 files (11.7% of codebase)
Files 200-500 lines: 45 files (22.8% of codebase)
Files < 200 lines: 120 files (60.9% of codebase)
```
**Assessment**: Good distribution with most files reasonably sized, but critical outliers need attention.
## Type Safety Analysis
### Type Assertion Patterns
#### "as any" Usage (62 total instances) ⚠️
**Vue Components & Views (41 instances):**
```typescript
// ImageMethodDialog.vue:504
const activeIdentity = await (this as any).$getActiveIdentity();
// GiftedDialog.vue:228
const activeIdentity = await (this as any).$getActiveIdentity();
// AccountViewView.vue: Multiple instances for:
// - PlatformServiceMixin method access
// - Vue refs with complex typing
// - External library integration (Leaflet)
```
**Other Files (21 instances):**
- **Vue Component References** (23 instances): `(this.$refs.dialog as any)`
- **Platform Detection** (12 instances): `(navigator as any).standalone`
- **External Library Integration** (15 instances): Leaflet, Axios extensions
- **Legacy Code Compatibility** (8 instances): Temporary migration code
- **Event Handler Workarounds** (4 instances): Vue event typing issues
**Example Problematic Pattern:**
```typescript
// src/views/AccountViewView.vue:934
const iconDefault = L.Icon.Default.prototype as unknown as Record<string, unknown>;
// Better approach:
interface LeafletIconPrototype {
_getIconUrl?: unknown;
}
const iconDefault = L.Icon.Default.prototype as LeafletIconPrototype;
```
#### "unknown" Type Usage (755 instances)
**Analysis**: Generally good practice showing defensive programming, but some areas could benefit from more specific typing.
### Recommended Type Safety Improvements
1. **Create Interface Extensions**:
```typescript
// src/types/platform-service-mixin.ts
interface VueWithPlatformServiceMixin extends Vue {
$getActiveIdentity(): Promise<{ activeDid: string }>;
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
// ... other methods
}
// src/types/external.ts
declare global {
interface Navigator {
standalone?: boolean;
}
}
interface VueRefWithOpen {
open: (callback: (result?: unknown) => void) => void;
}
```
2. **Component Ref Typing**:
```typescript
// Instead of: (this.$refs.dialog as any).open()
// Use: (this.$refs.dialog as VueRefWithOpen).open()
```
## Error Handling Consistency Analysis
### Error Handling Patterns (367 catch blocks)
#### Pattern Distribution:
1. **Structured Logging** (85%): Uses logger.error with context
2. **User Notification** (78%): Shows user-friendly error messages
3. **Graceful Degradation** (92%): Provides fallback behavior
4. **Error Propagation** (45%): Re-throws when appropriate
#### Excellent Pattern Example:
```typescript
// src/views/AccountViewView.vue:1617
try {
const response = await this.axios.delete(url, { headers });
if (response.status === 204) {
this.profileImageUrl = "";
this.notify.success("Image deleted successfully.");
}
} catch (error) {
if (isApiError(error) && error.response?.status === 404) {
// Graceful handling - image already gone
this.profileImageUrl = "";
} else {
this.notify.error("Failed to delete image", TIMEOUTS.STANDARD);
}
}
```
#### Areas for Improvement:
1. **Inconsistent Error Typing**: Some catch(error: any), others catch(error: unknown)
2. **Missing Error Boundaries**: No Vue error boundary components
3. **Silent Failures**: 15% of catch blocks don't notify users
## Code Duplication Analysis
### Significant Duplication Patterns
#### 1. **Toggle Component Pattern** (12 occurrences)
```html
<!-- Repeated across multiple files -->
<div class="relative ml-2 cursor-pointer" @click="toggleMethod()">
<input v-model="property" type="checkbox" class="sr-only" />
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<div class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"></div>
</div>
```
**Solution**: Create `ToggleSwitch.vue` component with props for value, label, and change handler.
#### 2. **API Error Handling Pattern** (25 occurrences)
```typescript
try {
const response = await this.axios.post(url, data, { headers });
if (response.status === 200) {
this.notify.success("Operation successful");
}
} catch (error) {
if (isApiError(error)) {
this.notify.error(`Failed: ${error.message}`);
}
}
```
**Solution**: Create `ApiRequestMixin.ts` with standardized request/response handling.
#### 3. **Settings Update Pattern** (40+ occurrences)
```typescript
async methodName() {
await this.$saveSettings({ property: this.newValue });
this.property = this.newValue;
}
```
**Solution**: Enhanced PlatformServiceMixin already provides `$saveSettings()` - migrate remaining manual patterns.
## Dependency and Coupling Analysis
### Import Dependency Patterns
#### Legacy Database Coupling (EXCELLENT)
- **Status**: 99.5% resolved (1 remaining databaseUtil import)
- **Remaining**: `src/views/DeepLinkErrorView.vue:import { logConsoleAndDb }`
- **Resolution**: Replace with PlatformServiceMixin `$logAndConsole()`
#### Circular Dependency Status (EXCELLENT)
- **Status**: 100% resolved, no active circular dependencies
- **Previous Issues**: All resolved through PlatformServiceMixin architecture
#### Component Coupling Analysis
```typescript
// High coupling components (>10 imports)
AccountViewView.vue: 15 imports (understandable given scope)
HomeView.vue: 12 imports
ProjectViewView.vue: 11 imports
// Well-isolated components (<5 imports)
QuickActionViews: 3-4 imports each
Component utilities: 2-3 imports each
```
**Assessment**: Reasonable coupling levels with clear architectural boundaries.
## Console Logging Analysis (129 instances)
### Logging Pattern Distribution:
1. **console.log**: 89 instances (69%)
2. **console.warn**: 24 instances (19%)
3. **console.error**: 16 instances (12%)
### Vue Components & Views Logging (3 instances):
- **Components**: 1 console.* call
- **Views**: 2 console.* calls
### Inconsistent Logging Approach:
```typescript
// Mixed patterns found:
console.log("Direct console logging"); // 89 instances
logger.debug("Structured logging"); // Preferred pattern
this.$logAndConsole("Mixin logging"); // PlatformServiceMixin
```
### Recommended Standardization:
1. **Migration Strategy**: Replace all console.* with logger.* calls
2. **Structured Context**: Add consistent metadata to log entries
3. **Log Levels**: Standardize debug/info/warn/error usage
## Technical Debt Analysis (6 total)
### Components (1 TODO):
```typescript
// PushNotificationPermission.vue
// TODO: secretDB functionality needs to be migrated to PlatformServiceMixin
```
### Views (2 TODOs):
```typescript
// AccountViewView.vue
// TODO: Implement this for SQLite
// TODO: implement this for SQLite
```
### Other Files (3 TODOs):
```typescript
// src/db/tables/accounts.ts
// TODO: When finished with migration, move these fields to Account and move identity and mnemonic here.
// src/util.d.ts
// TODO: , inspect: inspect
// src/libs/crypto/vc/passkeyHelpers.ts
// TODO: If it's after February 2025 when you read this then consider whether it still makes sense
```
**Assessment**: **EXCELLENT** - Only 6 TODO comments across 291 files.
## Performance Anti-Patterns
### Identified Issues:
#### 1. **Excessive Reactive Properties**
```typescript
// AccountViewView.vue has 25+ reactive properties
// Many could be computed or moved to component state
```
#### 2. **Inline Method Calls in Templates**
```html
<!-- Anti-pattern: -->
<span>{{ readableDate(timeStr) }}</span>
<!-- Better: -->
<span>{{ readableTime }}</span>
<!-- With computed property -->
```
#### 3. **Missing Key Attributes in Lists**
```html
<!-- Several v-for loops missing :key attributes -->
<li v-for="item in items">
```
#### 4. **Complex Template Logic**
```html
<!-- AccountViewView.vue - Complex nested conditions -->
<div v-if="!activeDid" id="noticeBeforeShare" class="bg-amber-200...">
<p class="mb-4">
<b>Note:</b> Before you can share with others or take any action, you need an identifier.
</p>
<router-link :to="{ name: 'new-identifier' }" class="inline-block...">
Create An Identifier
</router-link>
</div>
<!-- Identity Details -->
<IdentitySection
:given-name="givenName"
:profile-image-url="profileImageUrl"
:active-did="activeDid"
:is-registered="isRegistered"
:show-large-identicon-id="showLargeIdenticonId"
:show-large-identicon-url="showLargeIdenticonUrl"
:show-did-copy="showDidCopy"
@edit-name="onEditName"
@show-qr-code="onShowQrCode"
@add-image="onAddImage"
@delete-image="onDeleteImage"
@show-large-identicon-id="onShowLargeIdenticonId"
@show-large-identicon-url="onShowLargeIdenticonUrl"
/>
```
## Specific Actionable Recommendations
### Priority 1: Critical File Refactoring
1. **Split AccountViewView.vue**:
- **Timeline**: 2-3 sprints
- **Strategy**: Extract 6 major sections into focused components
- **Risk**: Medium (requires careful state management coordination)
- **Benefit**: Massive maintainability improvement, easier testing
2. **Decompose ImageMethodDialog.vue**:
- **Timeline**: 2-3 sprints
- **Strategy**: Extract 6 focused components (camera, file upload, cropping, etc.)
- **Risk**: Medium (complex camera state management)
- **Benefit**: Massive maintainability improvement
3. **Decompose PlatformServiceMixin.ts**:
- **Timeline**: 1-2 sprints
- **Strategy**: Create focused mixins by concern area
- **Risk**: Low (well-defined interfaces already exist)
- **Benefit**: Better code organization, reduced cognitive load
### Priority 2: Component Extraction
1. **HomeView.vue** → 4 focused sections
- **Timeline**: 1-2 sprints
- **Risk**: Low (clear separation of concerns)
- **Benefit**: Better code organization
2. **ProjectViewView.vue** → 4 focused sections
- **Timeline**: 1-2 sprints
- **Risk**: Low (well-defined boundaries)
- **Benefit**: Improved maintainability
### Priority 3: Shared Component Creation
1. **CameraPreviewComponent.vue**
- Extract from ImageMethodDialog.vue and PhotoDialog.vue
- **Benefit**: Eliminate code duplication
2. **FileUploadComponent.vue**
- Extract from ImageMethodDialog.vue and PhotoDialog.vue
- **Benefit**: Consistent file handling
3. **ToggleSwitch.vue**
- Replace 12 duplicate toggle patterns
- **Benefit**: Consistent UI components
4. **DiagnosticsPanelComponent.vue**
- Extract from ImageMethodDialog.vue
- **Benefit**: Reusable debugging component
### Priority 4: Type Safety Enhancement
1. **Eliminate "as any" Assertions**:
- **Timeline**: 1 sprint
- **Strategy**: Create proper interface extensions
- **Risk**: Low
- **Benefit**: Better compile-time error detection
2. **Standardize Error Typing**:
- **Timeline**: 0.5 sprint
- **Strategy**: Use consistent `catch (error: unknown)` pattern
- **Risk**: None
- **Benefit**: Better error handling consistency
### Priority 5: State Management Optimization
1. **Create Composables for Complex State**:
```typescript
// src/composables/useCameraState.ts
export function useCameraState() {
const cameraState = ref<CameraState>("off");
const showPreview = ref(false);
const isRetrying = ref(false);
const startCamera = async () => { /* ... */ };
const stopCamera = () => { /* ... */ };
return { cameraState, showPreview, isRetrying, startCamera, stopCamera };
}
```
2. **Group Related Reactive Properties**:
```typescript
// Instead of:
showB64Copy: boolean = false;
showDidCopy: boolean = false;
showDerCopy: boolean = false;
showPubCopy: boolean = false;
// Use:
copyStates = {
b64: false,
did: false,
der: false,
pub: false
};
```
### Priority 6: Code Standardization
1. **Logging Standardization**:
- **Timeline**: 1 sprint
- **Strategy**: Replace all console.* with logger.*
- **Risk**: None
- **Benefit**: Consistent logging, better debugging
2. **Template Optimization**:
- Add missing `:key` attributes
- Convert inline method calls to computed properties
- Implement virtual scrolling for large lists
## Quality Metrics Summary
### Vue Component Quality Distribution:
| Size Category | Count | Percentage | Quality Assessment |
|---------------|-------|------------|-------------------|
| Large (>500 lines) | 5 | 12.5% | 🔴 Needs Refactoring |
| Medium (200-500 lines) | 12 | 30% | 🟡 Good with Minor Issues |
| Small (<200 lines) | 23 | 57.5% | 🟢 Excellent |
### Vue View Quality Distribution:
| Size Category | Count | Percentage | Quality Assessment |
|---------------|-------|------------|-------------------|
| Large (>1000 lines) | 9 | 16.7% | 🔴 Needs Refactoring |
| Medium (500-1000 lines) | 8 | 14.8% | 🟡 Good with Minor Issues |
| Small (<500 lines) | 37 | 68.5% | 🟢 Excellent |
### Overall Quality Metrics:
| Metric | Components | Views | Overall Assessment |
|--------|------------|-------|-------------------|
| Technical Debt | 1 TODO | 2 TODOs | 🟢 Excellent |
| Type Safety | 6 "as any" | 35 "as any" | 🟡 Good |
| Console Logging | 1 instance | 2 instances | 🟢 Excellent |
| Architecture Consistency | 100% | 100% | 🟢 Excellent |
| Component Reuse | High | High | 🟢 Excellent |
### Before vs. Target State:
| Metric | Current | Target | Status |
|--------|---------|---------|---------|
| Files >1000 lines | 9 files | 3 files | 🟡 Needs Work |
| "as any" assertions | 62 | 15 | 🟡 Moderate |
| Console.* calls | 129 | 0 | 🔴 Needs Work |
| Component reuse | 40% | 75% | 🟡 Moderate |
| Error consistency | 85% | 95% | 🟢 Good |
| Type coverage | 88% | 95% | 🟢 Good |
## Risk Assessment
### Low Risk Improvements (High Impact):
- Logging standardization
- Type assertion cleanup
- Missing key attributes
- Component extraction from AccountViewView.vue
- Shared component creation (ToggleSwitch, CameraPreview)
### Medium Risk Improvements:
- PlatformServiceMixin decomposition
- State management optimization
- ImageMethodDialog decomposition
### High Risk Items:
- None identified - project demonstrates excellent architectural discipline
## Conclusion
The TimeSafari codebase demonstrates **exceptional code quality** with:
**Key Strengths:**
- **Consistent Architecture**: 100% Vue 3 Composition API with TypeScript
- **Minimal Technical Debt**: Only 6 TODO comments across 291 files
- **Excellent Small Components**: 68.5% of views and 57.5% of components are well-sized
- **Strong Type Safety**: Minimal "as any" usage, mostly justified
- **Clean Logging**: Minimal console.* usage, structured logging preferred
- **Excellent Database Migration**: 99.5% complete
- **Comprehensive Error Handling**: 367 catch blocks with good coverage
- **No Circular Dependencies**: 100% resolved
**Primary Focus Areas:**
1. **Decompose Large Files**: 5 components and 9 views need refactoring
2. **Extract Shared Components**: Camera, file upload, and diagnostics components
3. **Optimize State Management**: Group related properties and create composables
4. **Improve Type Safety**: Create proper interface extensions for mixin methods
5. **Logging Standardization**: Replace 129 console.* calls with structured logger.*
**The component architecture is production-ready** with these improvements representing **strategic optimization** rather than critical fixes. The codebase demonstrates **mature Vue.js development practices** with excellent separation of concerns and consistent patterns.
---
**Investigation Methodology:**
- Static analysis of 291 source files (197 general + 94 Vue components/views)
- Pattern recognition across 104,527 lines of code
- Manual review of large files and complexity patterns
- Dependency analysis and coupling assessment
- Performance anti-pattern identification
- Architecture consistency evaluation

View File

@@ -68,16 +68,16 @@ TimeSafari supports configurable logging levels via the `VITE_LOG_LEVEL` environ
```bash
# Show only errors
VITE_LOG_LEVEL=error npm run dev
VITE_LOG_LEVEL=error npm run build:web:dev
# Show warnings and errors
VITE_LOG_LEVEL=warn npm run dev
VITE_LOG_LEVEL=warn npm run build:web:dev
# Show info, warnings, and errors (default)
VITE_LOG_LEVEL=info npm run dev
VITE_LOG_LEVEL=info npm run build:web:dev
# Show all log levels including debug
VITE_LOG_LEVEL=debug npm run dev
VITE_LOG_LEVEL=debug npm run build:web:dev
```
### Available Levels
@@ -305,6 +305,17 @@ timesafari/
└── 📄 doc/README-BUILD-GUARD.md # Guard system documentation
```
## Known Issues
### Critical Vue Reactivity Bug
A critical Vue reactivity bug was discovered during ActiveDid migration testing where component properties fail to trigger template updates correctly.
**Impact**: The `newDirectOffersActivityNumber` element in HomeView.vue requires a watcher workaround to render correctly.
**Status**: Workaround implemented, investigation ongoing.
**Documentation**: See [Vue Reactivity Bug Report](doc/vue-reactivity-bug-report.md) for details.
## 🤝 Contributing
1. **Follow the Build Architecture Guard** - Update BUILDING.md when modifying build files

View File

@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 40
versionName "1.0.7"
versionCode 41
versionName "1.0.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -0,0 +1,655 @@
# Android Emulator Deployment Guide (No Android Studio)
**Author**: Matthew Raymer
**Date**: 2025-01-27
**Status**: 🎯 **ACTIVE** - Complete guide for deploying TimeSafari to Android emulator using command-line tools
## Overview
This guide provides comprehensive instructions for building and deploying TimeSafari to Android emulators using only command-line tools, without requiring Android Studio. It leverages the existing build system and adds emulator-specific deployment workflows.
## Prerequisites
### Required Tools
1. **Android SDK Command Line Tools**
```bash
# Install via package manager (Arch Linux)
sudo pacman -S android-sdk-cmdline-tools-latest
# Or download from Google
# https://developer.android.com/studio/command-line
```
2. **Android SDK Platform Tools**
```bash
# Install via package manager
sudo pacman -S android-sdk-platform-tools
# Or via Android SDK Manager
sdkmanager "platform-tools"
```
3. **Android SDK Build Tools**
```bash
sdkmanager "build-tools;34.0.0"
```
4. **Android Platform**
```bash
sdkmanager "platforms;android-34"
```
5. **Android Emulator**
```bash
sdkmanager "emulator"
```
6. **System Images**
```bash
# For API 34 (Android 14)
sdkmanager "system-images;android-34;google_apis;x86_64"
# For API 33 (Android 13) - alternative
sdkmanager "system-images;android-33;google_apis;x86_64"
```
### Environment Setup
```bash
# Add to ~/.bashrc or ~/.zshrc
export ANDROID_HOME=$HOME/Android/Sdk
export ANDROID_AVD_HOME=$HOME/.android/avd # Important for AVD location
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
export PATH=$PATH:$ANDROID_HOME/build-tools/34.0.0
# Reload shell
source ~/.bashrc
```
### Verify Installation
```bash
# Check all tools are available
adb version
emulator -version
avdmanager list
```
## Resource-Aware Emulator Setup
### ⚡ **Quick Start Recommendation**
**For best results, always start with resource analysis:**
```bash
# 1. Check your system capabilities
./scripts/avd-resource-checker.sh
# 2. Use the generated optimal startup script
/tmp/start-avd-TimeSafari_Emulator.sh
# 3. Deploy your app
npm run build:android:dev
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
This prevents system lockups and ensures optimal performance.
### AVD Resource Checker Script
**New Feature**: TimeSafari includes an intelligent resource checker that automatically detects your system capabilities and recommends optimal AVD configurations.
```bash
# Check system resources and get recommendations
./scripts/avd-resource-checker.sh
# Check resources for specific AVD
./scripts/avd-resource-checker.sh TimeSafari_Emulator
# Test AVD startup performance
./scripts/avd-resource-checker.sh TimeSafari_Emulator --test
# Create optimized AVD with recommended settings
./scripts/avd-resource-checker.sh TimeSafari_Emulator --create
```
**What the script analyzes:**
- **System Memory**: Total and available RAM
- **CPU Cores**: Available processing power
- **GPU Capabilities**: NVIDIA, AMD, Intel, or software rendering
- **Hardware Acceleration**: Optimal graphics settings
**What it generates:**
- **Optimal configuration**: Memory, cores, and GPU settings
- **Startup command**: Ready-to-use emulator command
- **Startup script**: Saved to `/tmp/start-avd-{name}.sh` for reuse
## Emulator Management
### Create Android Virtual Device (AVD)
```bash
# List available system images
avdmanager list target
# Create AVD for API 34
avdmanager create avd \
--name "TimeSafari_Emulator" \
--package "system-images;android-34;google_apis;x86_64" \
--device "pixel_7"
# List created AVDs
avdmanager list avd
```
### Start Emulator
```bash
# Start emulator with hardware acceleration (recommended)
emulator -avd TimeSafari_Emulator -gpu host -no-audio &
# Start with reduced resources (if system has limited RAM)
emulator -avd TimeSafari_Emulator \
-no-audio \
-memory 2048 \
-cores 2 \
-gpu swiftshader_indirect &
# Start with minimal resources (safest for low-end systems)
emulator -avd TimeSafari_Emulator \
-no-audio \
-memory 1536 \
-cores 1 \
-gpu swiftshader_indirect &
# Check if emulator is running
adb devices
```
### Resource Management
**Important**: Android emulators can consume significant system resources. Choose the appropriate configuration based on your system:
- **High-end systems** (16GB+ RAM, dedicated GPU): Use `-gpu host`
- **Mid-range systems** (8-16GB RAM): Use `-memory 2048 -cores 2`
- **Low-end systems** (4-8GB RAM): Use `-memory 1536 -cores 1 -gpu swiftshader_indirect`
### Emulator Control
```bash
# Stop emulator
adb emu kill
# Restart emulator
adb reboot
# Check emulator status
adb get-state
```
## Build and Deploy Workflow
### Method 1: Using Existing Build Scripts
The TimeSafari project already has comprehensive Android build scripts that can be adapted for emulator deployment:
```bash
# Development build with auto-run
npm run build:android:dev:run
# Test build with auto-run
npm run build:android:test:run
# Production build with auto-run
npm run build:android:prod:run
```
### Method 2: Custom Emulator Deployment Script
Create a new script specifically for emulator deployment:
```bash
# Create emulator deployment script
cat > scripts/deploy-android-emulator.sh << 'EOF'
#!/bin/bash
# deploy-android-emulator.sh
# Author: Matthew Raymer
# Date: 2025-01-27
# Description: Deploy TimeSafari to Android emulator without Android Studio
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Default values
BUILD_MODE="development"
AVD_NAME="TimeSafari_Emulator"
START_EMULATOR=true
CLEAN_BUILD=true
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--dev|--development)
BUILD_MODE="development"
shift
;;
--test)
BUILD_MODE="test"
shift
;;
--prod|--production)
BUILD_MODE="production"
shift
;;
--avd)
AVD_NAME="$2"
shift 2
;;
--no-start-emulator)
START_EMULATOR=false
shift
;;
--no-clean)
CLEAN_BUILD=false
shift
;;
-h|--help)
echo "Usage: $0 [options]"
echo "Options:"
echo " --dev, --development Build for development"
echo " --test Build for testing"
echo " --prod, --production Build for production"
echo " --avd NAME Use specific AVD name"
echo " --no-start-emulator Don't start emulator"
echo " --no-clean Skip clean build"
echo " -h, --help Show this help"
exit 0
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
# Function to check if emulator is running
check_emulator_running() {
if adb devices | grep -q "emulator.*device"; then
return 0
else
return 1
fi
}
# Function to start emulator
start_emulator() {
log_info "Starting Android emulator: $AVD_NAME"
# Check if AVD exists
if ! avdmanager list avd | grep -q "$AVD_NAME"; then
log_error "AVD '$AVD_NAME' not found. Please create it first."
log_info "Create AVD with: avdmanager create avd --name $AVD_NAME --package system-images;android-34;google_apis;x86_64"
exit 1
fi
# Start emulator in background
emulator -avd "$AVD_NAME" -no-audio -no-snapshot &
EMULATOR_PID=$!
# Wait for emulator to boot
log_info "Waiting for emulator to boot..."
adb wait-for-device
# Wait for boot to complete
log_info "Waiting for boot to complete..."
while [ "$(adb shell getprop sys.boot_completed)" != "1" ]; do
sleep 2
done
log_success "Emulator is ready!"
}
# Function to build and deploy
build_and_deploy() {
log_info "Building TimeSafari for $BUILD_MODE mode..."
# Clean build if requested
if [ "$CLEAN_BUILD" = true ]; then
log_info "Cleaning previous build..."
npm run clean:android
fi
# Build based on mode
case $BUILD_MODE in
"development")
npm run build:android:dev
;;
"test")
npm run build:android:test
;;
"production")
npm run build:android:prod
;;
esac
# Deploy to emulator
log_info "Deploying to emulator..."
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
# Launch app
log_info "Launching TimeSafari..."
adb shell am start -n app.timesafari/.MainActivity
log_success "TimeSafari deployed and launched successfully!"
}
# Main execution
main() {
log_info "TimeSafari Android Emulator Deployment"
log_info "Build Mode: $BUILD_MODE"
log_info "AVD Name: $AVD_NAME"
# Start emulator if requested and not running
if [ "$START_EMULATOR" = true ]; then
if ! check_emulator_running; then
start_emulator
else
log_info "Emulator already running"
fi
fi
# Build and deploy
build_and_deploy
log_success "Deployment completed successfully!"
}
# Run main function
main "$@"
EOF
# Make script executable
chmod +x scripts/deploy-android-emulator.sh
```
### Method 3: Direct Command Line Deployment
For quick deployments without scripts:
```bash
# 1. Ensure emulator is running
adb devices
# 2. Build the app
npm run build:android:dev
# 3. Install APK
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
# 4. Launch app
adb shell am start -n app.timesafari/.MainActivity
# 5. View logs
adb logcat | grep -E "(TimeSafari|Capacitor|MainActivity)"
```
## Advanced Deployment Options
### Custom API Server Configuration
For development with custom API endpoints:
```bash
# Build with custom API IP
npm run build:android:dev:custom
# Or modify capacitor.config.ts for specific IP
# Then build normally
npm run build:android:dev
```
### Debug vs Release Builds
```bash
# Debug build (default)
npm run build:android:debug
# Release build
npm run build:android:release
# Install specific build
adb install -r android/app/build/outputs/apk/release/app-release.apk
```
### Asset Management
```bash
# Validate Android assets
npm run assets:validate:android
# Generate assets only
npm run build:android:assets
# Clean assets
npm run assets:clean
```
## Troubleshooting
### Common Issues
1. **Emulator Not Starting / AVD Not Found**
```bash
# Check available AVDs
avdmanager list avd
# If AVD exists but emulator can't find it, check AVD location
echo $ANDROID_AVD_HOME
ls -la ~/.android/avd/
# Fix AVD path issue (common on Arch Linux)
export ANDROID_AVD_HOME=/home/$USER/.config/.android/avd
# Or create symlinks if AVDs are in different location
mkdir -p ~/.android/avd
ln -s /home/$USER/.config/.android/avd/* ~/.android/avd/
# Create new AVD if needed
avdmanager create avd --name "TimeSafari_Emulator" --package "system-images;android-34;google_apis;x86_64"
# Check emulator logs
emulator -avd TimeSafari_Emulator -verbose
```
2. **System Lockup / High Resource Usage**
```bash
# Kill any stuck emulator processes
pkill -f emulator
# Check system resources
free -h
nvidia-smi # if using NVIDIA GPU
# Start with minimal resources
emulator -avd TimeSafari_Emulator \
-no-audio \
-memory 1536 \
-cores 1 \
-gpu swiftshader_indirect &
# Monitor resource usage
htop
# If still having issues, try software rendering only
emulator -avd TimeSafari_Emulator \
-no-audio \
-no-snapshot \
-memory 1024 \
-cores 1 \
-gpu off &
```
3. **ADB Device Not Found**
```bash
# Restart ADB server
adb kill-server
adb start-server
# Check devices
adb devices
# Check emulator status
adb get-state
```
3. **Build Failures**
```bash
# Clean everything
npm run clean:android
# Rebuild
npm run build:android:dev
# Check Gradle logs
cd android && ./gradlew clean --stacktrace
```
4. **Installation Failures**
```bash
# Uninstall existing app
adb uninstall app.timesafari
# Reinstall
adb install android/app/build/outputs/apk/debug/app-debug.apk
# Check package info
adb shell pm list packages | grep timesafari
```
### Performance Optimization
1. **Emulator Performance**
```bash
# Start with hardware acceleration
emulator -avd TimeSafari_Emulator -gpu host
# Use snapshot for faster startup
emulator -avd TimeSafari_Emulator -snapshot default
# Allocate more RAM
emulator -avd TimeSafari_Emulator -memory 4096
```
2. **Build Performance**
```bash
# Use Gradle daemon
echo "org.gradle.daemon=true" >> android/gradle.properties
# Increase heap size
echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties
# Enable parallel builds
echo "org.gradle.parallel=true" >> android/gradle.properties
```
## Integration with Existing Build System
### NPM Scripts Integration
Add emulator-specific scripts to `package.json`:
```json
{
"scripts": {
"emulator:check": "./scripts/avd-resource-checker.sh",
"emulator:check:test": "./scripts/avd-resource-checker.sh TimeSafari_Emulator --test",
"emulator:check:create": "./scripts/avd-resource-checker.sh TimeSafari_Emulator --create",
"emulator:start": "emulator -avd TimeSafari_Emulator -no-audio &",
"emulator:start:optimized": "/tmp/start-avd-TimeSafari_Emulator.sh",
"emulator:stop": "adb emu kill",
"emulator:deploy": "./scripts/deploy-android-emulator.sh",
"emulator:deploy:dev": "./scripts/deploy-android-emulator.sh --dev",
"emulator:deploy:test": "./scripts/deploy-android-emulator.sh --test",
"emulator:deploy:prod": "./scripts/deploy-android-emulator.sh --prod",
"emulator:logs": "adb logcat | grep -E '(TimeSafari|Capacitor|MainActivity)'",
"emulator:shell": "adb shell"
}
}
```
### CI/CD Integration
For automated testing and deployment:
```bash
# GitHub Actions example
- name: Start Android Emulator
run: |
emulator -avd TimeSafari_Emulator -no-audio -no-snapshot &
adb wait-for-device
adb shell getprop sys.boot_completed
- name: Build and Deploy
run: |
npm run build:android:test
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n app.timesafari/.MainActivity
- name: Run Tests
run: |
npm run test:android
```
## Best Practices
### Development Workflow
1. **Start emulator once per session**
```bash
emulator -avd TimeSafari_Emulator -no-audio &
```
2. **Use incremental builds**
```bash
# For rapid iteration
npm run build:android:sync
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
3. **Monitor logs continuously**
```bash
adb logcat | grep -E "(TimeSafari|Capacitor|MainActivity)" --color=always
```
### Performance Tips
1. **Use snapshots for faster startup**
2. **Enable hardware acceleration**
3. **Allocate sufficient RAM (4GB+)**
4. **Use SSD storage for AVDs**
5. **Close unnecessary applications**
### Security Considerations
1. **Use debug builds for development only**
2. **Never commit debug keystores**
3. **Use release builds for testing**
4. **Validate API endpoints in production builds**
## Conclusion
This guide provides a complete solution for deploying TimeSafari to Android emulators without Android Studio. The approach leverages the existing build system while adding emulator-specific deployment capabilities.
The key benefits:
- ✅ **No Android Studio required**
- ✅ **Command-line only workflow**
- ✅ **Integration with existing build scripts**
- ✅ **Automated deployment options**
- ✅ **Comprehensive troubleshooting guide**
For questions or issues, refer to the troubleshooting section or check the existing build documentation in `BUILDING.md`.

View File

@@ -60,13 +60,49 @@ For complex tasks, you might combine multiple meta-rules:
meta_core_always_on + meta_research + meta_bug_diagnosis
```
## Workflow Flexibility: Phase-Based, Not Waterfall
**Important**: Meta-rules represent **workflow phases**, not a rigid sequence. You can:
### **Jump Between Phases Freely**
- **Start with diagnosis** if you already know the problem
- **Go back to research** if your fix reveals new issues
- **Switch to planning** mid-implementation if scope changes
- **Document at any phase** - not just at the end
### **Mode Switching by Invoking Meta-Rules**
Each meta-rule invocation **automatically switches your workflow mode**:
```
Research Mode → Invoke @meta_bug_diagnosis → Diagnosis Mode
Diagnosis Mode → Invoke @meta_bug_fixing → Fixing Mode
Planning Mode → Invoke @meta_feature_implementation → Implementation Mode
```
### **Phase Constraints, Not Sequence Constraints**
- **Within each phase**: Clear constraints on what you can/cannot do
- **Between phases**: Complete freedom to move as needed
- **No forced order**: Choose the phase that matches your current need
### **Example of Flexible Workflow**
```
1. Start with @meta_research (investigation mode)
2. Jump to @meta_bug_diagnosis (diagnosis mode)
3. Realize you need more research → back to @meta_research
4. Complete diagnosis → @meta_bug_fixing (implementation mode)
5. Find new issues → back to @meta_bug_diagnosis
6. Complete fix → @meta_documentation (documentation mode)
```
**The "sticky" part means**: Each phase has clear boundaries, but you control when to enter/exit phases.
## Practical Usage Examples
### **Example 1: Bug Investigation**
### **Example 1: Bug Investigation (Flexible Flow)**
**Scenario**: User reports that the contact list isn't loading properly
**Meta-Rule Selection**:
**Initial Meta-Rule Selection**:
```
meta_core_always_on + meta_research + meta_bug_diagnosis
```
@@ -76,13 +112,15 @@ meta_core_always_on + meta_research + meta_bug_diagnosis
- **Research**: Systematic investigation methodology, evidence collection
- **Bug Diagnosis**: Defect analysis framework, root cause identification
**Workflow**:
**Flexible Workflow**:
1. Apply core always-on for foundation
2. Use research meta-rule for systematic investigation
3. Apply bug diagnosis for defect analysis
4. Follow the bundled workflow automatically
3. Switch to bug diagnosis when you have enough evidence
4. **Can go back to research** if diagnosis reveals new questions
5. **Can jump to bug fixing** if root cause is obvious
6. **Can document findings** at any phase
### **Example 2: Feature Development**
### **Example 2: Feature Development (Iterative Flow)**
**Scenario**: Building a new contact search feature
@@ -96,12 +134,15 @@ meta_core_always_on + meta_feature_planning + meta_feature_implementation
- **Feature Planning**: Requirements analysis, architecture planning
- **Feature Implementation**: Development workflow, testing strategy
**Workflow**:
**Iterative Workflow**:
1. Start with core always-on
2. Use feature planning for design and requirements
3. Switch to feature implementation for coding and testing
4. **Can return to planning** if implementation reveals design issues
5. **Can go back to research** if you need to investigate alternatives
6. **Can document progress** throughout the process
### **Example 3: Documentation Creation**
### **Example 3: Documentation Creation (Parallel Flow)**
**Scenario**: Writing a migration guide for the new database system
@@ -114,10 +155,13 @@ meta_core_always_on + meta_documentation
- **Core Always-On**: Foundation and context
- **Documentation**: Educational focus, templates, quality standards
**Workflow**:
**Parallel Workflow**:
1. Apply core always-on for foundation
2. Use documentation meta-rule for educational content creation
3. Follow educational templates and quality standards
3. **Can research** while documenting if you need more information
4. **Can plan** documentation structure as you write
5. **Can implement** examples or code snippets as needed
6. Follow educational templates and quality standards
## Meta-Rule Application Process

View File

@@ -0,0 +1,181 @@
# Seed Phrase Backup Reminder Implementation
## Overview
This implementation adds a modal dialog that reminds users to back up their seed phrase if they haven't done so yet. The reminder appears after specific user actions and includes a 24-hour cooldown to avoid being too intrusive.
## Features
- **Modal Dialog**: Uses the existing notification group modal system from `App.vue`
- **Smart Timing**: Only shows when `hasBackedUpSeed = false`
- **24-Hour Cooldown**: Uses localStorage to prevent showing more than once per day
- **Action-Based Triggers**: Shows after specific user actions
- **User Choice**: "Backup Identifier Seed" or "Remind me Later" options
## Implementation Details
### Core Utility (`src/utils/seedPhraseReminder.ts`)
The main utility provides:
- `shouldShowSeedReminder(hasBackedUpSeed)`: Checks if reminder should be shown
- `markSeedReminderShown()`: Updates localStorage timestamp
- `createSeedReminderNotification()`: Creates the modal configuration
- `showSeedPhraseReminder(hasBackedUpSeed, notifyFunction)`: Main function to show reminder
### Trigger Points
The reminder is shown after these user actions:
**Note**: The reminder is triggered by **claim creation** actions, not claim confirmations. This focuses on when users are actively creating new content rather than just confirming existing claims.
1. **Profile Saving** (`AccountViewView.vue`)
- After clicking "Save Profile" button
- Only when profile save is successful
2. **Claim Creation** (Multiple views)
- `ClaimAddRawView.vue`: After submitting raw claims
- `GiftedDialog.vue`: After creating gifts/claims
- `GiftedDetailsView.vue`: After recording gifts/claims
- `OfferDialog.vue`: After creating offers
3. **QR Code Views Exit**
- `ContactQRScanFullView.vue`: When exiting via back button
- `ContactQRScanShowView.vue`: When exiting via back button
### Modal Configuration
```typescript
{
group: "modal",
type: "confirm",
title: "Backup Your Identifier Seed?",
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
yesText: "Backup Identifier Seed",
noText: "Remind me Later",
onYes: () => navigate to /seed-backup,
onNo: () => mark as shown for 24 hours,
onCancel: () => mark as shown for 24 hours
}
```
**Important**: The modal is configured with `timeout: -1` to ensure it stays open until the user explicitly interacts with one of the buttons. This prevents the dialog from closing automatically.
### Cooldown Mechanism
- **Storage Key**: `seedPhraseReminderLastShown`
- **Cooldown Period**: 24 hours (24 * 60 * 60 * 1000 milliseconds)
- **Implementation**: localStorage with timestamp comparison
- **Fallback**: Shows reminder if timestamp is invalid or missing
## User Experience
### When Reminder Appears
- User has not backed up their seed phrase (`hasBackedUpSeed = false`)
- At least 24 hours have passed since last reminder
- User performs one of the trigger actions
- **1-second delay** after the success message to allow users to see the confirmation
### User Options
1. **"Backup Identifier Seed"**: Navigates to `/seed-backup` page
2. **"Remind me Later"**: Dismisses and won't show again for 24 hours
3. **Cancel/Close**: Same behavior as "Remind me Later"
### Frequency Control
- **First Time**: Always shows if user hasn't backed up
- **Subsequent**: Only shows after 24-hour cooldown
- **Automatic Reset**: When user completes seed backup (`hasBackedUpSeed = true`)
## Technical Implementation
### Error Handling
- Graceful fallback if localStorage operations fail
- Logging of errors for debugging
- Non-blocking implementation (doesn't affect main functionality)
### Integration Points
- **Platform Service**: Uses `$accountSettings()` to check backup status
- **Notification System**: Integrates with existing `$notify` system
- **Router**: Uses `window.location.href` for navigation
### Performance Considerations
- Minimal localStorage operations
- No blocking operations
- Efficient timestamp comparisons
- **Timing Behavior**: 1-second delay before showing reminder to improve user experience flow
## Testing
### Manual Testing Scenarios
1. **First Time User**
- Create new account
- Perform trigger action (save profile, create claim, exit QR view)
- Verify reminder appears
2. **Repeat User (Within 24h)**
- Perform trigger action
- Verify reminder does NOT appear
3. **Repeat User (After 24h)**
- Wait 24+ hours
- Perform trigger action
- Verify reminder appears again
4. **User Who Has Backed Up**
- Complete seed backup
- Perform trigger action
- Verify reminder does NOT appear
5. **QR Code View Exit**
- Navigate to QR code view (full or show)
- Exit via back button
- Verify reminder appears (if conditions are met)
### Browser Testing
- Test localStorage functionality
- Verify timestamp handling
- Check navigation to seed backup page
## Future Enhancements
### Potential Improvements
1. **Customizable Cooldown**: Allow users to set reminder frequency
2. **Progressive Urgency**: Increase reminder frequency over time
3. **Analytics**: Track reminder effectiveness and user response
4. **A/B Testing**: Test different reminder messages and timing
### Configuration Options
- Reminder frequency settings
- Custom reminder messages
- Different trigger conditions
- Integration with other notification systems
## Maintenance
### Monitoring
- Check localStorage usage in browser dev tools
- Monitor user feedback about reminder frequency
- Track navigation success to seed backup page
### Updates
- Modify reminder text in `createSeedReminderNotification()`
- Adjust cooldown period in `REMINDER_COOLDOWN_MS` constant
- Add new trigger points as needed
## Conclusion
This implementation provides a non-intrusive way to remind users about seed phrase backup while respecting their preferences and avoiding notification fatigue. The 24-hour cooldown ensures users aren't overwhelmed while maintaining the importance of the security reminder.
The feature is fully integrated with the existing codebase architecture and follows established patterns for notifications, error handling, and user interaction.

View File

@@ -0,0 +1,116 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'app.timesafari',
appName: 'TimeSafari',
webDir: 'dist',
server: {
cleartext: true
},
plugins: {
App: {
appUrlOpen: {
handlers: [
{
url: 'timesafari://*',
autoVerify: true
}
]
}
},
SplashScreen: {
launchShowDuration: 3000,
launchAutoHide: true,
backgroundColor: '#ffffff',
androidSplashResourceName: 'splash',
androidScaleType: 'CENTER_CROP',
showSpinner: false,
androidSpinnerStyle: 'large',
iosSpinnerStyle: 'small',
spinnerColor: '#999999',
splashFullScreen: true,
splashImmersive: true
},
CapSQLite: {
iosDatabaseLocation: 'Library/CapacitorDatabase',
iosIsEncryption: false,
iosBiometric: {
biometricAuth: false,
biometricTitle: 'Biometric login for TimeSafari'
},
androidIsEncryption: false,
androidBiometric: {
biometricAuth: false,
biometricTitle: 'Biometric login for TimeSafari'
},
electronIsEncryption: false
}
},
ios: {
contentInset: 'never',
allowsLinkPreview: true,
scrollEnabled: true,
limitsNavigationsToAppBoundDomains: true,
backgroundColor: '#ffffff',
allowNavigation: [
'*.timesafari.app',
'*.jsdelivr.net',
'api.endorser.ch'
]
},
android: {
allowMixedContent: true,
captureInput: true,
webContentsDebuggingEnabled: false,
allowNavigation: [
'*.timesafari.app',
'*.jsdelivr.net',
'api.endorser.ch',
'10.0.2.2:3000'
]
},
electron: {
deepLinking: {
schemes: ['timesafari']
},
buildOptions: {
appId: 'app.timesafari',
productName: 'TimeSafari',
directories: {
output: 'dist-electron-packages'
},
files: [
'dist/**/*',
'electron/**/*'
],
mac: {
category: 'public.app-category.productivity',
target: [
{
target: 'dmg',
arch: ['x64', 'arm64']
}
]
},
win: {
target: [
{
target: 'nsis',
arch: ['x64']
}
]
},
linux: {
target: [
{
target: 'AppImage',
arch: ['x64']
}
],
category: 'Utility'
}
}
}
};
export default config;

View File

@@ -56,7 +56,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
"license": "MIT",
"dependencies": {
"jeep-sqlite": "^2.7.2"
},

View File

@@ -50,6 +50,7 @@ process.stderr.on('error', (err) => {
const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })];
const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
{ role: 'editMenu' },
{ role: 'viewMenu' },
];

View File

@@ -53,6 +53,7 @@ export class ElectronCapacitorApp {
];
private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
{ role: 'editMenu' },
{ role: 'viewMenu' },
];
private mainWindowState;

View File

@@ -1,6 +1,6 @@
{
"compileOnSave": true,
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
"include": ["./src/**/*"],
"compilerOptions": {
"outDir": "./build",
"importHelpers": true,

View File

@@ -403,7 +403,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.7;
MARKETING_VERSION = 1.0.8;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -430,7 +430,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.7;
MARKETING_VERSION = 1.0.8;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

91
package-lock.json generated
View File

@@ -27,6 +27,7 @@
"@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7",
@@ -90,6 +91,7 @@
"vue": "3.5.13",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0",
@@ -106,6 +108,7 @@
"@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11",
@@ -6786,6 +6789,17 @@
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
@@ -10147,6 +10161,12 @@
"@types/geojson": "*"
}
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true
},
"node_modules/@types/luxon": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
@@ -10154,6 +10174,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true
},
"node_modules/@types/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@@ -32883,6 +32919,61 @@
"vue": "^3.0.0"
}
},
"node_modules/vue-markdown-render": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.2.1.tgz",
"integrity": "sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==",
"dependencies": {
"markdown-it": "^13.0.2"
},
"peerDependencies": {
"vue": "^3.3.4"
}
},
"node_modules/vue-markdown-render/node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/vue-markdown-render/node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/vue-markdown-render/node_modules/markdown-it": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/vue-markdown-render/node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"node_modules/vue-markdown-render/node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/vue-picture-cropper": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "timesafari",
"version": "1.1.0-beta",
"version": "1.1.1-beta",
"description": "Time Safari Application",
"author": {
"name": "Time Safari Team"
@@ -106,7 +106,7 @@
"guard": "bash ./scripts/build-arch-guard.sh",
"guard:test": "bash ./scripts/build-arch-guard.sh --staged",
"guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'",
"clean:android": "./scripts/clean-android.sh",
"clean:android": "./scripts/uninstall-android.sh",
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
"clean:electron": "./scripts/build-electron.sh --clean",
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
@@ -136,7 +136,6 @@
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
},
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2",
@@ -157,6 +156,7 @@
"@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7",
@@ -220,6 +220,7 @@
"vue": "3.5.13",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0",
@@ -236,6 +237,7 @@
"@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11",

View File

@@ -21,7 +21,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: 1,
workers: 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['list'],

View File

@@ -0,0 +1,46 @@
{
"icons": [
{
"src": "../icons/icon-48.webp",
"type": "image/png",
"sizes": "48x48",
"purpose": "any maskable"
},
{
"src": "../icons/icon-72.webp",
"type": "image/png",
"sizes": "72x72",
"purpose": "any maskable"
},
{
"src": "../icons/icon-96.webp",
"type": "image/png",
"sizes": "96x96",
"purpose": "any maskable"
},
{
"src": "../icons/icon-128.webp",
"type": "image/png",
"sizes": "128x128",
"purpose": "any maskable"
},
{
"src": "../icons/icon-192.webp",
"type": "image/png",
"sizes": "192x192",
"purpose": "any maskable"
},
{
"src": "../icons/icon-256.webp",
"type": "image/png",
"sizes": "256x256",
"purpose": "any maskable"
},
{
"src": "../icons/icon-512.webp",
"type": "image/png",
"sizes": "512x512",
"purpose": "any maskable"
}
]
}

389
scripts/avd-resource-checker.sh Executable file
View File

@@ -0,0 +1,389 @@
#!/bin/bash
# avd-resource-checker.sh
# Author: Matthew Raymer
# Date: 2025-01-27
# Description: Check system resources and recommend optimal AVD configuration
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Colors for output
RED_COLOR='\033[0;31m'
GREEN_COLOR='\033[0;32m'
YELLOW_COLOR='\033[1;33m'
BLUE_COLOR='\033[0;34m'
NC_COLOR='\033[0m' # No Color
# Function to print colored output
print_status() {
local color=$1
local message=$2
echo -e "${color}${message}${NC_COLOR}"
}
# Function to get system memory in MB
get_system_memory() {
if command -v free >/dev/null 2>&1; then
free -m | awk 'NR==2{print $2}'
else
echo "0"
fi
}
# Function to get available memory in MB
get_available_memory() {
if command -v free >/dev/null 2>&1; then
free -m | awk 'NR==2{print $7}'
else
echo "0"
fi
}
# Function to get CPU core count
get_cpu_cores() {
if command -v nproc >/dev/null 2>&1; then
nproc
elif [ -f /proc/cpuinfo ]; then
grep -c ^processor /proc/cpuinfo
else
echo "1"
fi
}
# Function to check GPU capabilities
check_gpu_capabilities() {
local gpu_type="unknown"
local gpu_memory="0"
# Check for NVIDIA GPU
if command -v nvidia-smi >/dev/null 2>&1; then
gpu_type="nvidia"
gpu_memory=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 || echo "0")
print_status $GREEN_COLOR "✓ NVIDIA GPU detected (${gpu_memory}MB VRAM)"
return 0
fi
# Check for AMD GPU
if command -v rocm-smi >/dev/null 2>&1; then
gpu_type="amd"
print_status $GREEN_COLOR "✓ AMD GPU detected"
return 0
fi
# Check for Intel GPU
if lspci 2>/dev/null | grep -i "vga.*intel" >/dev/null; then
gpu_type="intel"
print_status $YELLOW_COLOR "✓ Intel integrated GPU detected"
return 1
fi
# Check for generic GPU
if lspci 2>/dev/null | grep -i "vga" >/dev/null; then
gpu_type="generic"
print_status $YELLOW_COLOR "✓ Generic GPU detected"
return 1
fi
print_status $RED_COLOR "✗ No GPU detected"
return 2
}
# Function to check if hardware acceleration is available
check_hardware_acceleration() {
local gpu_capable=$1
if [ $gpu_capable -eq 0 ]; then
print_status $GREEN_COLOR "✓ Hardware acceleration recommended"
return 0
elif [ $gpu_capable -eq 1 ]; then
print_status $YELLOW_COLOR "⚠ Limited hardware acceleration"
return 1
else
print_status $RED_COLOR "✗ No hardware acceleration available"
return 2
fi
}
# Function to recommend AVD configuration
recommend_avd_config() {
local total_memory=$1
local available_memory=$2
local cpu_cores=$3
local gpu_capable=$4
print_status $BLUE_COLOR "\n=== AVD Configuration Recommendation ==="
# Calculate recommended memory (leave 2GB for system)
local system_reserve=2048
local recommended_memory=$((available_memory - system_reserve))
# Cap memory at reasonable limits
if [ $recommended_memory -gt 4096 ]; then
recommended_memory=4096
elif [ $recommended_memory -lt 1024 ]; then
recommended_memory=1024
fi
# Calculate recommended cores (leave 2 cores for system)
local recommended_cores=$((cpu_cores - 2))
if [ $recommended_cores -lt 1 ]; then
recommended_cores=1
elif [ $recommended_cores -gt 4 ]; then
recommended_cores=4
fi
# Determine GPU setting
local gpu_setting=""
case $gpu_capable in
0) gpu_setting="-gpu host" ;;
1) gpu_setting="-gpu swiftshader_indirect" ;;
2) gpu_setting="-gpu swiftshader_indirect" ;;
esac
# Generate recommendation
print_status $GREEN_COLOR "Recommended AVD Configuration:"
echo " Memory: ${recommended_memory}MB"
echo " Cores: ${recommended_cores}"
echo " GPU: ${gpu_setting}"
# Get AVD name from function parameter (passed from main)
local avd_name=$5
local command="emulator -avd ${avd_name} -no-audio -memory ${recommended_memory} -cores ${recommended_cores} ${gpu_setting} &"
print_status $BLUE_COLOR "\nGenerated Command:"
echo " ${command}"
# Save to file for easy execution
local script_file="/tmp/start-avd-${avd_name}.sh"
cat > "$script_file" << EOF
#!/bin/bash
# Auto-generated AVD startup script
# Generated by avd-resource-checker.sh on $(date)
echo "Starting AVD: ${avd_name}"
echo "Memory: ${recommended_memory}MB"
echo "Cores: ${recommended_cores}"
echo "GPU: ${gpu_setting}"
${command}
echo "AVD started in background"
echo "Check status with: adb devices"
echo "View logs with: adb logcat"
EOF
chmod +x "$script_file"
print_status $GREEN_COLOR "\n✓ Startup script saved to: ${script_file}"
return 0
}
# Function to test AVD startup
test_avd_startup() {
local avd_name=$1
local test_duration=${2:-30}
print_status $BLUE_COLOR "\n=== Testing AVD Startup ==="
# Check if AVD exists
if ! avdmanager list avd | grep -q "$avd_name"; then
print_status $RED_COLOR "✗ AVD '$avd_name' not found"
return 1
fi
print_status $YELLOW_COLOR "Testing AVD startup for ${test_duration} seconds..."
# Start emulator in test mode
emulator -avd "$avd_name" -no-audio -no-window -no-snapshot -memory 1024 -cores 1 -gpu swiftshader_indirect &
local emulator_pid=$!
# Wait for boot
local boot_time=0
local max_wait=$test_duration
while [ $boot_time -lt $max_wait ]; do
if adb devices | grep -q "emulator.*device"; then
print_status $GREEN_COLOR "✓ AVD booted successfully in ${boot_time} seconds"
break
fi
sleep 2
boot_time=$((boot_time + 2))
done
# Cleanup
kill $emulator_pid 2>/dev/null || true
adb emu kill 2>/dev/null || true
if [ $boot_time -ge $max_wait ]; then
print_status $RED_COLOR "✗ AVD failed to boot within ${test_duration} seconds"
return 1
fi
return 0
}
# Function to list available AVDs
list_available_avds() {
print_status $BLUE_COLOR "\n=== Available AVDs ==="
if ! command -v avdmanager >/dev/null 2>&1; then
print_status $RED_COLOR "✗ avdmanager not found. Please install Android SDK command line tools."
return 1
fi
local avd_list=$(avdmanager list avd 2>/dev/null)
if [ -z "$avd_list" ]; then
print_status $YELLOW_COLOR "⚠ No AVDs found. Create one with:"
echo " avdmanager create avd --name TimeSafari_Emulator --package system-images;android-34;google_apis;x86_64"
return 1
fi
echo "$avd_list"
return 0
}
# Function to create optimized AVD
create_optimized_avd() {
local avd_name=$1
local memory=$2
local cores=$3
print_status $BLUE_COLOR "\n=== Creating Optimized AVD ==="
# Check if system image is available
local system_image="system-images;android-34;google_apis;x86_64"
if ! sdkmanager --list | grep -q "$system_image"; then
print_status $YELLOW_COLOR "Installing system image: $system_image"
sdkmanager "$system_image"
fi
# Create AVD
print_status $YELLOW_COLOR "Creating AVD: $avd_name"
avdmanager create avd \
--name "$avd_name" \
--package "$system_image" \
--device "pixel_7" \
--force
# Configure AVD
local avd_config_file="$HOME/.android/avd/${avd_name}.avd/config.ini"
if [ -f "$avd_config_file" ]; then
print_status $YELLOW_COLOR "Configuring AVD settings..."
# Set memory
sed -i "s/vm.heapSize=.*/vm.heapSize=${memory}/" "$avd_config_file"
# Set cores
sed -i "s/hw.cpu.ncore=.*/hw.cpu.ncore=${cores}/" "$avd_config_file"
# Disable unnecessary features
echo "hw.audioInput=no" >> "$avd_config_file"
echo "hw.audioOutput=no" >> "$avd_config_file"
echo "hw.camera.back=none" >> "$avd_config_file"
echo "hw.camera.front=none" >> "$avd_config_file"
echo "hw.gps=no" >> "$avd_config_file"
echo "hw.sensors.orientation=no" >> "$avd_config_file"
echo "hw.sensors.proximity=no" >> "$avd_config_file"
print_status $GREEN_COLOR "✓ AVD configured successfully"
fi
return 0
}
# Main function
main() {
print_status $BLUE_COLOR "=== TimeSafari AVD Resource Checker ==="
print_status $BLUE_COLOR "Checking system resources and recommending optimal AVD configuration\n"
# Get system information
local total_memory=$(get_system_memory)
local available_memory=$(get_available_memory)
local cpu_cores=$(get_cpu_cores)
print_status $BLUE_COLOR "=== System Information ==="
echo "Total Memory: ${total_memory}MB"
echo "Available Memory: ${available_memory}MB"
echo "CPU Cores: ${cpu_cores}"
# Check GPU capabilities
print_status $BLUE_COLOR "\n=== GPU Analysis ==="
check_gpu_capabilities
local gpu_capable=$?
# Check hardware acceleration
check_hardware_acceleration $gpu_capable
local hw_accel=$?
# List available AVDs
list_available_avds
# Get AVD name from user or use default
local avd_name="TimeSafari_Emulator"
if [ $# -gt 0 ]; then
avd_name="$1"
fi
# Recommend configuration
recommend_avd_config $total_memory $available_memory $cpu_cores $gpu_capable "$avd_name"
# Test AVD if requested
if [ "$2" = "--test" ]; then
test_avd_startup "$avd_name"
fi
# Create optimized AVD if requested
if [ "$2" = "--create" ]; then
local recommended_memory=$((available_memory - 2048))
if [ $recommended_memory -gt 4096 ]; then
recommended_memory=4096
elif [ $recommended_memory -lt 1024 ]; then
recommended_memory=1024
fi
local recommended_cores=$((cpu_cores - 2))
if [ $recommended_cores -lt 1 ]; then
recommended_cores=1
elif [ $recommended_cores -gt 4 ]; then
recommended_cores=4
fi
create_optimized_avd "$avd_name" $recommended_memory $recommended_cores
fi
print_status $GREEN_COLOR "\n=== Resource Check Complete ==="
print_status $YELLOW_COLOR "Tip: Use the generated startup script for consistent AVD launches"
}
# Show help
show_help() {
echo "Usage: $0 [AVD_NAME] [OPTIONS]"
echo ""
echo "Options:"
echo " --test Test AVD startup (30 second test)"
echo " --create Create optimized AVD with recommended settings"
echo " --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Check resources and recommend config"
echo " $0 TimeSafari_Emulator # Check resources for specific AVD"
echo " $0 TimeSafari_Emulator --test # Test AVD startup"
echo " $0 TimeSafari_Emulator --create # Create optimized AVD"
echo ""
echo "The script will:"
echo " - Analyze system resources (RAM, CPU, GPU)"
echo " - Recommend optimal AVD configuration"
echo " - Generate startup command and script"
echo " - Optionally test or create AVD"
}
# Parse command line arguments
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
show_help
exit 0
fi
# Run main function
main "$@"

View File

@@ -22,6 +22,7 @@
# --sync Sync Capacitor only
# --assets Generate assets only
# --deploy Deploy APK to connected device
# --uninstall Uninstall app from connected device
# -h, --help Show this help message
# -v, --verbose Enable verbose logging
#
@@ -196,6 +197,7 @@ SYNC_ONLY=false
ASSETS_ONLY=false
DEPLOY_APP=false
AUTO_RUN=false
UNINSTALL=false
CUSTOM_API_IP=""
# Function to parse Android-specific arguments
@@ -246,6 +248,9 @@ parse_android_args() {
--auto-run)
AUTO_RUN=true
;;
--uninstall)
UNINSTALL=true
;;
--api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}"
@@ -291,6 +296,7 @@ print_android_usage() {
echo " --assets Generate assets only"
echo " --deploy Deploy APK to connected device"
echo " --auto-run Auto-run app after build"
echo " --uninstall Uninstall app from connected device"
echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)"
echo ""
echo "Common Options:"
@@ -305,6 +311,7 @@ print_android_usage() {
echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device"
echo " $0 --uninstall # Uninstall app from device"
echo " $0 --dev # Dev build with default 10.0.2.2"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo ""
@@ -351,8 +358,18 @@ fi
# Setup application directories
setup_app_directories
# Load environment from .env file if it exists
load_env_file ".env"
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Handle clean-only mode
if [ "$CLEAN_ONLY" = true ]; then
@@ -407,8 +424,13 @@ safe_execute "Validating asset configuration" "npm run assets:validate" || {
log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available."
}
# Step 2: Clean Android app
safe_execute "Cleaning Android app" "npm run clean:android" || exit 1
# Step 2: Uninstall Android app
if [ "$UNINSTALL" = true ]; then
log_info "Uninstall: uninstalling app from device"
safe_execute "Uninstalling Android app" "./scripts/uninstall-android.sh" || exit 1
log_success "Uninstall completed successfully!"
exit 0
fi
# Step 3: Clean dist directory
log_info "Cleaning dist directory..."

View File

@@ -181,7 +181,7 @@ sync_capacitor() {
copy_web_assets() {
log_info "Copying web assets to Electron"
safe_execute "Copying assets" "cp -r dist/* electron/app/"
safe_execute "Copying config" "cp capacitor.config.json electron/capacitor.config.json"
# Note: Electron has its own capacitor.config.ts file, so we don't copy the main config
}
# Compile TypeScript
@@ -341,7 +341,19 @@ main_electron_build() {
# Setup environment
setup_build_env "electron" "$BUILD_MODE"
setup_app_directories
load_env_file ".env"
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Step 1: Clean Electron build artifacts
clean_electron_artifacts

View File

@@ -324,8 +324,18 @@ fi
# Setup application directories
setup_app_directories
# Load environment from .env file if it exists
load_env_file ".env"
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Validate iOS environment
validate_ios_environment

View File

@@ -1,8 +1,8 @@
#!/bin/bash
# clean-android.sh
# uninstall-android.sh
# Author: Matthew Raymer
# Date: 2025-08-19
# Description: Clean Android app with timeout protection to prevent hanging
# Description: Uninstall Android app with timeout protection to prevent hanging
# This script safely uninstalls the TimeSafari app from connected Android devices
# with a 30-second timeout to prevent indefinite hanging.

View File

@@ -386,7 +386,7 @@ export default class App extends Vue {
let allGoingOff = false;
try {
const settings: Settings = await this.$settings();
const settings: Settings = await this.$accountSettings();
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
const notifyingReminder = !!settings?.notifyingReminderTime;

View File

@@ -22,4 +22,24 @@
.dialog {
@apply bg-white p-4 rounded-lg w-full max-w-lg;
}
/* Markdown content styling to restore list elements */
.markdown-content ul {
@apply list-disc list-inside ml-4;
}
.markdown-content ol {
@apply list-decimal list-inside ml-4;
}
.markdown-content li {
@apply mb-1;
}
.markdown-content ul ul,
.markdown-content ol ol,
.markdown-content ul ol,
.markdown-content ol ul {
@apply ml-4 mt-1;
}
}

View File

@@ -80,7 +80,10 @@
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
{{ description }}
<vue-markdown
:source="truncatedDescription"
class="markdown-content"
/>
</a>
</p>
@@ -258,11 +261,13 @@ import {
NOTIFY_UNKNOWN_PERSON,
} from "@/constants/notifications";
import { TIMEOUTS } from "@/utils/notify";
import VueMarkdown from "vue-markdown-render";
@Component({
components: {
EntityIcon,
ProjectIcon,
VueMarkdown,
},
})
export default class ActivityListItem extends Vue {
@@ -303,6 +308,14 @@ export default class ActivityListItem extends Vue {
return `${claim?.description || ""}`;
}
get truncatedDescription(): string {
const desc = this.description;
if (desc.length <= 300) {
return desc;
}
return desc.substring(0, 300) + "...";
}
private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
}

View File

@@ -16,6 +16,12 @@ messages * - Conditional UI based on platform capabilities * * @component *
:to="{ name: 'seed-backup' }"
:class="backupButtonClasses"
>
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome
v-if="showRedNotificationDot"
icon="circle"
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
></font-awesome>
Backup Identifier Seed
</router-link>
@@ -98,6 +104,12 @@ export default class DataExportSection extends Vue {
*/
isExporting = false;
/**
* Flag indicating if the user has backed up their seed phrase
* Used to control the visibility of the notification dot
*/
showRedNotificationDot = false;
/**
* Notification helper for consistent notification patterns
* Created as a getter to ensure $notify is available when called
@@ -129,7 +141,7 @@ export default class DataExportSection extends Vue {
* CSS classes for the backup button (router link)
*/
get backupButtonClasses(): string {
return "block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2";
return "block relative w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2";
}
/**
@@ -218,6 +230,23 @@ export default class DataExportSection extends Vue {
created() {
this.notify = createNotifyHelpers(this.$notify);
this.loadSeedBackupStatus();
}
/**
* Loads the seed backup status from account settings
* Updates the hasBackedUpSeed flag to control notification dot visibility
*/
private async loadSeedBackupStatus(): Promise<void> {
try {
const settings = await this.$accountSettings();
this.showRedNotificationDot =
!!settings.isRegistered && !settings.hasBackedUpSeed;
} catch (err: unknown) {
logger.error("Failed to load seed backup status:", err);
// Default to false (show notification dot) if we can't load the setting
this.showRedNotificationDot = false;
}
}
}
</script>

View File

@@ -82,6 +82,7 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
@@ -219,9 +220,18 @@ export default class GiftedDialog extends Vue {
this.stepType = "giver";
try {
const settings = await this.$settings();
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
logger.debug("[GiftedDialog] Settings received:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
});
this.allContacts = await this.$contacts();
@@ -411,6 +421,15 @@ export default class GiftedDialog extends Vue {
);
} else {
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
if (this.callbackOnSuccess) {
this.callbackOnSuccess(amount);
}

View File

@@ -74,7 +74,7 @@
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', deepLinkUrl)"
@click="copyTextToClipboard('A link to this page', deepLinkUrl)"
>click here to copy this page, paste it into a message, and ask if
they'll tell you more about the {{ roleName }}.</a
>
@@ -110,7 +110,7 @@
* @since 2024-12-19
*/
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import * as R from "ramda";
import * as serverUtil from "../libs/endorserServer";
import { Contact } from "../db/tables/contacts";
@@ -197,19 +197,24 @@ export default class HiddenDidDialog extends Vue {
);
}
copyToClipboard(name: string, text: string) {
useClipboard()
.copy(text)
.then(() => {
this.notify.success(
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
TIMEOUTS.SHORT,
);
});
async copyTextToClipboard(name: string, text: string) {
try {
await copyToClipboard(text);
this.notify.success(
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
TIMEOUTS.SHORT,
);
} catch (error) {
this.$logAndConsole(
`Error copying ${name || "content"} to clipboard: ${error}`,
true,
);
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
}
}
onClickShareClaim() {
this.copyToClipboard("A link to this page", this.deepLinkUrl);
this.copyTextToClipboard("A link to this page", this.deepLinkUrl);
window.navigator.share({
title: "Help Connect Me",
text: "I'm trying to find the people who recorded this. Can you help me?",

View File

@@ -132,7 +132,7 @@
v-if="shouldMirrorVideo"
class="absolute top-2 left-2 bg-black/50 text-white px-2 py-1 rounded text-xs"
>
<font-awesome icon="mirror" class="w-[1em] mr-1" />
<font-awesome icon="circle-user" class="w-[1em] mr-1" />
Mirrored
</div>
<div :class="cameraControlsClasses">
@@ -293,7 +293,7 @@ const inputImageFileNameRef = ref<Blob>();
export default class ImageMethodDialog extends Vue {
$notify!: NotifyFunction;
$router!: Router;
notify = createNotifyHelpers(this.$notify);
notify!: ReturnType<typeof createNotifyHelpers>;
/** Active DID for user authentication */
activeDid = "";
@@ -498,9 +498,14 @@ export default class ImageMethodDialog extends Vue {
* @throws {Error} When settings retrieval fails
*/
async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
} catch (error) {
logger.error("Error retrieving settings from database:", error);
this.notify.error(

View File

@@ -26,7 +26,7 @@
:weight="2"
color="#3b82f6"
fill-color="#3b82f6"
fill-opacity="0.2"
:fill-opacity="0.2"
/>
</l-map>
</div>

View File

@@ -232,7 +232,12 @@ export default class MembersList extends Vue {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";
await this.fetchMembers();

View File

@@ -64,6 +64,7 @@ import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_OFFER_SETTINGS_ERROR,
NOTIFY_OFFER_RECORDING,
@@ -175,7 +176,11 @@ export default class OfferDialog extends Vue {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
@@ -299,6 +304,14 @@ export default class OfferDialog extends Vue {
);
} else {
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {

View File

@@ -270,7 +270,12 @@ export default class OnboardingDialog extends Vue {
async open(page: OnboardPage) {
this.page = page;
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.isRegistered = !!settings.isRegistered;
const contacts = await this.$getAllContacts();

View File

@@ -268,7 +268,12 @@ export default class PhotoDialog extends Vue {
// logger.log("PhotoDialog mounted");
try {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.isRegistered = !!settings.isRegistered;
logger.log("isRegistered:", this.isRegistered);
} catch (error: unknown) {

View File

@@ -20,14 +20,15 @@ import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import { logger } from "../utils/logger";
@Component({
mixins: [PlatformServiceMixin],
})
export default class TopMessage extends Vue {
// Enhanced PlatformServiceMixin v4.0 provides:
// - Cached database operations: this.$contacts(), this.$settings(), this.$accountSettings()
// - Settings shortcuts: this.$saveSettings(), this.$saveMySettings()
// - Cached database operations: this.$contacts(), this.$accountSettings()
// - Settings shortcuts: this.$saveSettings()
// - Cache management: this.$refreshSettings(), this.$clearAllCaches()
// - Ultra-concise database methods: this.$db(), this.$exec(), this.$query()
// - All methods use smart caching with TTL for massive performance gains
@@ -44,26 +45,52 @@ export default class TopMessage extends Vue {
this.notify = createNotifyHelpers(this.$notify);
try {
// Ultra-concise cached settings loading - replaces 50+ lines of logic!
const settings = await this.$accountSettings(undefined, {
activeDid: undefined,
apiServer: AppString.PROD_ENDORSER_API_SERVER,
// Load settings without overriding database values - fixes settings inconsistency
logger.debug("[TopMessage] 📥 Loading settings without overrides...");
const settings = await this.$accountSettings();
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
logger.debug("[TopMessage] 📊 Settings loaded:", {
activeDid: activeIdentity.activeDid,
apiServer: settings.apiServer,
warnIfTestServer: settings.warnIfTestServer,
warnIfProdServer: settings.warnIfProdServer,
component: "TopMessage",
timestamp: new Date().toISOString(),
});
// Only show warnings if the user has explicitly enabled them
if (
settings.warnIfTestServer &&
settings.apiServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
const didPrefix = activeIdentity.activeDid?.slice(11, 15);
this.message = "You're not using prod, user " + didPrefix;
logger.debug("[TopMessage] ⚠️ Test server warning displayed:", {
apiServer: settings.apiServer,
didPrefix: didPrefix,
});
} else if (
settings.warnIfProdServer &&
settings.apiServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
const didPrefix = activeIdentity.activeDid?.slice(11, 15);
this.message = "You are using prod, user " + didPrefix;
logger.debug("[TopMessage] ⚠️ Production server warning displayed:", {
apiServer: settings.apiServer,
didPrefix: didPrefix,
});
} else {
logger.debug(
"[TopMessage] No warnings displayed - conditions not met",
);
}
} catch (err: unknown) {
logger.error("[TopMessage] ❌ Error loading settings:", err);
this.notify.error(JSON.stringify(err), TIMEOUTS.MODAL);
}
}

View File

@@ -84,7 +84,7 @@ export default class UserNameDialog extends Vue {
*/
async open(aCallback?: (name?: string) => void) {
this.callback = aCallback || this.callback;
const settings = await this.$settings();
const settings = await this.$accountSettings();
this.givenName = settings.firstName || "";
this.visible = true;
}
@@ -95,7 +95,18 @@ export default class UserNameDialog extends Vue {
*/
async onClickSaveChanges() {
try {
await this.$updateSettings({ firstName: this.givenName });
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
const activeDid = activeIdentity.activeDid;
if (activeDid) {
// Save to user-specific settings for the current identity
await this.$saveUserSettings(activeDid, { firstName: this.givenName });
} else {
// Fallback to master settings if no active DID
await this.$saveSettings({ firstName: this.givenName });
}
this.visible = false;
this.callback(this.givenName);
} catch (error) {

View File

@@ -86,7 +86,7 @@ export const ACCOUNT_VIEW_CONSTANTS = {
CANNOT_UPLOAD_IMAGES: "You cannot upload images.",
BAD_SERVER_RESPONSE: "Bad server response.",
ERROR_RETRIEVING_LIMITS:
"No limits were found, so no actions are allowed. You will need to get registered.",
"No limits were found, so no actions are allowed. You need to get registered.",
},
// Project assignment errors

View File

@@ -1689,3 +1689,11 @@ export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
title: "They're Added To Your List",
message: "Would you like to go to the main page now?",
};
// ImportAccountView.vue specific constants
// Used in: ImportAccountView.vue (onImportClick method - duplicate account warning)
export const NOTIFY_DUPLICATE_ACCOUNT_IMPORT = {
title: "Account Already Imported",
message:
"This account has already been imported. Please use a different seed phrase or check your existing accounts.",
};

View File

@@ -4,6 +4,7 @@ import {
} from "../services/migrationService";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
import { logger } from "@/utils/logger";
// Generate a random secret for the secret table
@@ -28,7 +29,61 @@ import { arrayBufferToBase64 } from "@/libs/crypto";
// where they couldn't take action because they couldn't unlock that identity.)
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
const secretBase64 = arrayBufferToBase64(randomBytes);
const secretBase64 = arrayBufferToBase64(randomBytes.buffer);
// Single source of truth for migration 004 SQL
const MIG_004_SQL = `
-- Migration 004: active_identity_management (CONSOLIDATED)
-- Combines original migrations 004, 005, and 006 into single atomic operation
-- CRITICAL SECURITY: Uses ON DELETE RESTRICT constraint from the start
-- Assumes master code deployed with migration 003 (hasBackedUpSeed)
-- Enable foreign key constraints for data integrity
PRAGMA foreign_keys = ON;
-- Add UNIQUE constraint to accounts.did for foreign key support
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did);
-- Create active_identity table with SECURE constraint (ON DELETE RESTRICT)
-- This prevents accidental account deletion - critical security feature
CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY CHECK (id = 1),
activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Add performance indexes
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id);
-- Seed singleton row (only if not already exists)
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 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 != '');
-- Copy important settings that were set in the MASTER_SETTINGS_KEY to the main identity.
-- (We're not doing them all because some were already identity-specific and others aren't as critical.)
UPDATE settings
SET lastViewedClaimId = (SELECT lastViewedClaimId FROM settings WHERE id = 1),
profileImageUrl = (SELECT profileImageUrl FROM settings WHERE id = 1),
showShortcutBvc = (SELECT showShortcutBvc FROM settings WHERE id = 1),
warnIfProdServer = (SELECT warnIfProdServer FROM settings WHERE id = 1),
warnIfTestServer = (SELECT warnIfTestServer FROM settings WHERE id = 1)
WHERE id = 2;
-- CLEANUP: Remove orphaned settings records and clear legacy activeDid values
-- which usually simply deletes the MASTER_SETTINGS_KEY record.
-- This completes the migration from settings-based to table-based active identity
DELETE FROM settings WHERE accountDid IS NULL;
UPDATE settings SET activeDid = NULL;
`;
// Each migration can include multiple SQL statements (with semicolons)
const MIGRATIONS = [
@@ -124,8 +179,52 @@ const MIGRATIONS = [
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
`,
},
{
name: "003_add_hasBackedUpSeed_to_settings",
sql: `
-- Add hasBackedUpSeed field to settings
-- This migration assumes master code has been deployed
-- The error handling will catch this if column already exists and mark migration as applied
ALTER TABLE settings ADD COLUMN hasBackedUpSeed BOOLEAN DEFAULT FALSE;
`,
},
{
name: "004_active_identity_management",
sql: MIG_004_SQL,
},
{
name: "005_add_starredPlanHandleIds_to_settings",
sql: `
ALTER TABLE settings ADD COLUMN starredPlanHandleIds TEXT DEFAULT '[]'; -- JSON string
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
`,
},
];
/**
* Extract single value from database query result
* Works with different database service result formats
*/
function extractSingleValue<T>(result: T): string | number | null {
if (!result) return null;
// Handle AbsurdSQL format: QueryExecResult[]
if (Array.isArray(result) && result.length > 0 && result[0]?.values) {
const values = result[0].values;
return values.length > 0 ? values[0][0] : null;
}
// Handle Capacitor SQLite format: { values: unknown[][] }
if (typeof result === "object" && result !== null && "values" in result) {
const values = (result as { values: unknown[][] }).values;
return values && values.length > 0
? (values[0][0] as string | number)
: null;
}
return null;
}
/**
* @param sqlExec - A function that executes a SQL statement and returns the result
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
@@ -135,8 +234,73 @@ export async function runMigrations<T>(
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
// Only log migration start in development
const isDevelopment = process.env.VITE_PLATFORM === "development";
if (isDevelopment) {
logger.debug("[Migration] Starting database migrations");
}
for (const migration of MIGRATIONS) {
if (isDevelopment) {
logger.debug("[Migration] Registering migration:", migration.name);
}
registerMigration(migration);
}
if (isDevelopment) {
logger.debug("[Migration] Running migration service");
}
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
if (isDevelopment) {
logger.debug("[Migration] Database migrations completed");
}
// Bootstrapping: Ensure active account is selected after migrations
if (isDevelopment) {
logger.debug("[Migration] Running bootstrapping hooks");
}
try {
// Check if we have accounts but no active selection
const accountsResult = await sqlQuery("SELECT COUNT(*) FROM accounts");
const accountsCount = (extractSingleValue(accountsResult) as number) || 0;
// Check if active_identity table exists, and if not, try to recover
let activeDid: string | null = null;
try {
const activeResult = await sqlQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
);
activeDid = (extractSingleValue(activeResult) as string) || null;
} catch (error) {
// Table doesn't exist - migration 004 may not have run yet
if (isDevelopment) {
logger.debug(
"[Migration] active_identity table not found - migration may not have run",
);
}
activeDid = null;
}
if (accountsCount > 0 && (!activeDid || activeDid === "")) {
if (isDevelopment) {
logger.debug("[Migration] Auto-selecting first account as active");
}
const firstAccountResult = await sqlQuery(
"SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1",
);
const firstAccountDid =
(extractSingleValue(firstAccountResult) as string) || null;
if (firstAccountDid) {
await sqlExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[firstAccountDid],
);
logger.info(`[Migration] Set active account to: ${firstAccountDid}`);
}
}
} catch (error) {
logger.warn("[Migration] Bootstrapping hook failed (non-critical):", error);
}
}

View File

@@ -9,34 +9,6 @@ import { logger } from "@/utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QueryExecResult } from "@/interfaces/database";
export async function updateDefaultSettings(
settingsChanges: Settings,
): Promise<boolean> {
delete settingsChanges.accountDid; // just in case
// ensure there is no "id" that would override the key
delete settingsChanges.id;
try {
const platformService = PlatformServiceFactory.getInstance();
const { sql, params } = generateUpdateStatement(
settingsChanges,
"settings",
"id = ?",
[MASTER_SETTINGS_KEY],
);
const result = await platformService.dbExec(sql, params);
return result.changes === 1;
} catch (error) {
logger.error("Error updating default settings:", error);
if (error instanceof Error) {
throw error; // Re-throw if it's already an Error with a message
} else {
throw new Error(
`Failed to update settings. We recommend you try again or restart the app.`,
);
}
}
}
export async function insertDidSpecificSettings(
did: string,
settings: Partial<Settings> = {},
@@ -91,6 +63,7 @@ export async function updateDidSpecificSettings(
? mapColumnsToValues(postUpdateResult.columns, postUpdateResult.values)[0]
: null;
// Note that we want to eliminate this check (and fix the above if it doesn't work).
// Check if any of the target fields were actually changed
let actuallyUpdated = false;
if (currentRecord && updatedRecord) {
@@ -157,10 +130,11 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
result.columns,
result.values,
)[0] as Settings;
if (settings.searchBoxes) {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
}
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
settings.starredPlanHandleIds = parseJsonField(
settings.starredPlanHandleIds,
[],
);
return settings;
}
}
@@ -226,10 +200,11 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
);
}
// Handle searchBoxes parsing
if (settings.searchBoxes) {
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
}
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
settings.starredPlanHandleIds = parseJsonField(
settings.starredPlanHandleIds,
[],
);
return settings;
} catch (error) {
@@ -567,6 +542,8 @@ export async function debugSettingsData(did?: string): Promise<void> {
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
* - Capacitor SQLite: Returns raw strings that need manual parsing
*
* Maybe consolidate with PlatformServiceMixin._parseJsonField
*
* @param value The value to parse (could be string or already parsed object)
* @param defaultValue Default value if parsing fails
* @returns Parsed object or default value

View File

@@ -0,0 +1,14 @@
/**
* ActiveIdentity type describes the active identity selection.
* This replaces the activeDid field in the settings table for better
* database architecture and data integrity.
*
* @author Matthew Raymer
* @since 2025-08-29
*/
export interface ActiveIdentity {
id: number;
activeDid: string;
lastUpdated: string;
}

View File

@@ -9,6 +9,8 @@ export type Contact = {
// When adding a property:
// - Consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
// - If it's a JSON string, it should be converted to an object/array in PlatformServiceMixin._mapColumnsToValues
//
did: string;
contactMethods?: Array<ContactMethod>;

View File

@@ -14,6 +14,12 @@ export type BoundingBox = {
* New entries that are boolean should also be added to PlatformServiceMixin._mapColumnsToValues
*/
export type Settings = {
//
// When adding a property:
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
// - If it's a JSON string, it should be converted to an object/array in PlatformServiceMixin._mapColumnsToValues
//
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
id?: string | number; // this is erased for all those entries that are keyed with accountDid
@@ -29,6 +35,7 @@ export type Settings = {
finishedOnboarding?: boolean; // the user has completed the onboarding process
firstName?: string; // user's full name, may be null if unwanted for a particular account
hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase
hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean;
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
@@ -36,6 +43,7 @@ export type Settings = {
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
lastAckedStarredPlanChangesJwtId?: string; // the last JWT ID for starred plan changes that they've acknowledged seeing
// The claim list has a most recent one used in notifications that's separate from the last viewed
lastNotifiedClaimId?: string;
@@ -60,15 +68,18 @@ export type Settings = {
showContactGivesInline?: boolean; // Display contact inline or not
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
starredPlanHandleIds?: string[]; // Array of starred plan handle IDs
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL
};
// type of settings where the searchBoxes are JSON strings instead of objects
// type of settings where the values are JSON strings instead of objects
export type SettingsWithJsonStrings = Settings & {
searchBoxes: string;
starredPlanHandleIds: string;
};
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
@@ -85,6 +96,11 @@ export const SettingsSchema = {
/**
* Constants.
*/
/**
* This is deprecated.
* It only remains for those with a PWA who have not migrated, but we'll soon remove it.
*/
export const MASTER_SETTINGS_KEY = "1";
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;

View File

@@ -72,11 +72,15 @@ export interface PlanActionClaim extends ClaimObject {
name: string;
agent?: { identifier: string };
description?: string;
endTime?: string;
identifier?: string;
image?: string;
lastClaimId?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
startTime?: string;
url?: string;
}
// AKA Registration & RegisterAction

View File

@@ -1,4 +1,5 @@
import { GiveActionClaim, OfferClaim } from "./claims";
import { GiveActionClaim, OfferClaim, PlanActionClaim } from "./claims";
import { GenericCredWrapper } from "./common";
// a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord {
@@ -61,6 +62,11 @@ export interface PlanSummaryRecord {
jwtId?: string;
}
export interface PlanSummaryAndPreviousClaim {
plan: PlanSummaryRecord;
wrappedClaimBefore: GenericCredWrapper<PlanActionClaim>;
}
/**
* Represents data about a project
*
@@ -87,7 +93,10 @@ export interface PlanData {
name: string;
/**
* The identifier of the project record -- different from jwtId
* (Maybe we should use the jwtId to iterate through the records instead.)
*
* This has been used to iterate through plan records, because jwtId ordering doesn't match
* chronological create ordering, though it does match most recent edit order (in reverse order).
* (It may be worthwhile to order by jwtId instead. It is an indexed field.)
**/
rowId?: string;
}

View File

@@ -16,7 +16,7 @@
* @module endorserServer
*/
import { Axios, AxiosRequestConfig } from "axios";
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
import { Buffer } from "buffer";
import { sha256 } from "ethereum-cryptography/sha256";
import { LRUCache } from "lru-cache";
@@ -56,7 +56,12 @@ import {
KeyMetaWithPrivate,
KeyMetaMaybeWithPrivate,
} from "../interfaces/common";
import { PlanSummaryRecord } from "../interfaces/records";
import {
OfferSummaryRecord,
OfferToPlanSummaryRecord,
PlanSummaryAndPreviousClaim,
PlanSummaryRecord,
} from "../interfaces/records";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app";
@@ -315,7 +320,7 @@ export function didInfoForContact(
return { displayName: "You", known: true };
} else if (contact) {
return {
displayName: contact.name || "Contact With No Name",
displayName: contact.name || "Contact Without a Name",
known: true,
profileImageUrl: contact.profileImageUrl,
};
@@ -362,6 +367,22 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
}
/**
* In some contexts (eg. agent), a blank really is nobody.
*/
export function didInfoOrNobody(
did: string | undefined,
activeDid: string | undefined,
allMyDids: string[],
contacts: Contact[],
): string {
if (did == null) {
return "Nobody";
} else {
return didInfo(did, activeDid, allMyDids, contacts);
}
}
/**
* return text description without any references to "you" as user
*/
@@ -486,6 +507,15 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
/**
* Tracks in-flight requests to prevent duplicate API calls for the same plan
* @constant {Map}
*/
const inFlightRequests = new Map<
string,
Promise<PlanSummaryRecord | undefined>
>();
/**
* Retrieves plan data from cache or server
* @param {string} handleId - Plan handle ID
@@ -505,40 +535,140 @@ export async function getPlanFromCache(
if (!handleId) {
return undefined;
}
let cred = planCache.get(handleId);
if (!cred) {
const url =
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(requesterDid);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
// Use debug level for development to reduce console noise
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log(
"[EndorserServer] Plan cache is empty for handle",
handleId,
" Got data:",
JSON.stringify(resp.data),
);
}
} catch (error) {
logger.error(
"[EndorserServer] Failed to load plan with handle",
handleId,
" Got error:",
JSON.stringify(error),
);
}
// Check cache first (existing behavior)
const cred = planCache.get(handleId);
if (cred) {
return cred;
}
// Check if request is already in flight (NEW: request deduplication)
if (inFlightRequests.has(handleId)) {
logger.debug(
"[Plan Loading] 🔄 Request already in flight, reusing promise:",
{
handleId,
requesterDid,
timestamp: new Date().toISOString(),
},
);
return inFlightRequests.get(handleId);
}
// Create new request promise (NEW: request coordination)
const requestPromise = performPlanRequest(
handleId,
axios,
apiServer,
requesterDid,
);
inFlightRequests.set(handleId, requestPromise);
try {
const result = await requestPromise;
return result;
} finally {
// Clean up in-flight request tracking (NEW: cleanup)
inFlightRequests.delete(handleId);
}
}
/**
* Performs the actual plan request to the server
* @param {string} handleId - Plan handle ID
* @param {Axios} axios - Axios instance
* @param {string} apiServer - API server URL
* @param {string} [requesterDid] - Optional requester DID for private info
* @returns {Promise<PlanSummaryRecord|undefined>} Plan data or undefined if not found
*
* @throws {Error} If server request fails
*/
async function performPlanRequest(
handleId: string,
axios: Axios,
apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> {
const url =
apiServer + "/api/v2/report/plans?handleId=" + encodeURIComponent(handleId);
const headers = await getHeaders(requesterDid);
// Enhanced diagnostic logging for plan loading
const requestId = `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[Plan Loading] 🔍 Loading plan from server:", {
requestId,
handleId,
apiServer,
endpoint: url,
requesterDid,
timestamp: new Date().toISOString(),
});
try {
const resp = await axios.get(url, { headers });
logger.debug("[Plan Loading] ✅ Plan loaded successfully:", {
requestId,
handleId,
status: resp.status,
hasData: !!resp.data?.data,
dataLength: resp.data?.data?.length || 0,
timestamp: new Date().toISOString(),
});
if (resp.status === 200 && resp.data?.data?.length > 0) {
const cred = resp.data.data[0];
planCache.set(handleId, cred);
logger.debug("[Plan Loading] 💾 Plan cached:", {
requestId,
handleId,
planName: cred?.name,
planIssuer: cred?.issuerDid,
});
return cred;
} else {
// Use debug level for development to reduce console noise
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log(
"[Plan Loading] ⚠️ Plan cache is empty for handle",
handleId,
" Got data:",
JSON.stringify(resp.data),
);
return undefined;
}
} catch (error) {
// Enhanced error logging for plan loading failures
const axiosError = error as {
response?: {
data?: unknown;
status?: number;
statusText?: string;
};
message?: string;
};
logger.error("[Plan Loading] ❌ Failed to load plan:", {
requestId,
handleId,
apiServer,
endpoint: url,
requesterDid,
errorStatus: axiosError.response?.status,
errorStatusText: axiosError.response?.statusText,
errorData: axiosError.response?.data,
errorMessage: axiosError.message || String(error),
timestamp: new Date().toISOString(),
});
throw error;
}
return cred;
}
/**
@@ -621,7 +751,7 @@ export async function getNewOffersToUser(
activeDid: string,
afterOfferJwtId?: string,
beforeOfferJwtId?: string,
) {
): Promise<{ data: Array<OfferSummaryRecord>; hitLimit: boolean }> {
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
if (afterOfferJwtId) {
url += "&afterId=" + afterOfferJwtId;
@@ -643,7 +773,7 @@ export async function getNewOffersToUserProjects(
activeDid: string,
afterOfferJwtId?: string,
beforeOfferJwtId?: string,
) {
): Promise<{ data: Array<OfferToPlanSummaryRecord>; hitLimit: boolean }> {
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
if (afterOfferJwtId) {
url += "?afterId=" + afterOfferJwtId;
@@ -657,6 +787,46 @@ export async function getNewOffersToUserProjects(
return response.data;
}
/**
* Get starred projects that have been updated since the last check
*
* @param axios - axios instance
* @param apiServer - endorser API server URL
* @param activeDid - user's DID for authentication
* @param starredPlanHandleIds - array of starred project handle IDs
* @param afterId - JWT ID to check for changes after (from lastAckedStarredPlanChangesJwtId)
* @returns { data: Array<PlanSummaryAndPreviousClaim>, hitLimit: boolean }
*/
export async function getStarredProjectsWithChanges(
axios: Axios,
apiServer: string,
activeDid: string,
starredPlanHandleIds: string[],
afterId?: string,
): Promise<{ data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }> {
if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) {
return { data: [], hitLimit: false };
}
if (!afterId) {
// This doesn't make sense: there should always be some previous one they've seen.
// We'll just return blank.
return { data: [], hitLimit: false };
}
// Use POST method for larger lists of project IDs
const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`;
const headers = await getHeaders(activeDid);
const requestBody = {
planIds: starredPlanHandleIds,
afterId: afterId,
};
const response = await axios.post(url, requestBody, { headers });
return response.data;
}
/**
* Construct GiveAction VC for submission to server
*
@@ -1019,19 +1189,82 @@ export async function createAndSubmitClaim(
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload);
// Enhanced diagnostic logging for claim submission
const requestId = `claim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[Claim Submission] 🚀 Starting claim submission:", {
requestId,
apiServer,
requesterDid: issuerDid,
endpoint: `${apiServer}/api/v2/claim`,
timestamp: new Date().toISOString(),
jwtLength: vcJwt.length,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = `${apiServer}/api/v2/claim`;
logger.debug("[Claim Submission] 📡 Making API request:", {
requestId,
url,
payloadSize: payload.length,
headers: { "Content-Type": "application/json" },
});
const response = await axios.post(url, payload, {
headers: {
"Content-Type": "application/json",
},
});
logger.debug("[Claim Submission] ✅ Claim submitted successfully:", {
requestId,
status: response.status,
handleId: response.data?.handleId,
responseSize: JSON.stringify(response.data).length,
timestamp: new Date().toISOString(),
});
return { success: true, handleId: response.data?.handleId };
} catch (error: unknown) {
logger.error("Error submitting claim:", error);
// Enhanced error logging with comprehensive context
const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
statusText?: string;
headers?: Record<string, string>;
};
config?: {
url?: string;
method?: string;
headers?: Record<string, string>;
};
message?: string;
};
logger.error("[Claim Submission] ❌ Claim submission failed:", {
requestId,
apiServer,
requesterDid: issuerDid,
endpoint: `${apiServer}/api/v2/claim`,
errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status,
httpStatusText: axiosError.response?.statusText,
responseHeaders: axiosError.response?.headers,
requestConfig: {
url: axiosError.config?.url,
method: axiosError.config?.method,
headers: axiosError.config?.headers,
},
originalError: axiosError.message || String(error),
timestamp: new Date().toISOString(),
});
const errorMessage: string =
serverMessageForUser(error) ||
(error && typeof error === "object" && "message" in error
@@ -1141,6 +1374,28 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
};
/**
* Formats type string for display by adding spaces before capitals
* and optionally adds an appropriate article prefix (a/an)
*
* @param text - Text to format
* @returns Formatted string with article prefix
*/
export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = (
text: string,
): string => {
const word = capitalizeAndInsertSpacesBeforeCaps(text);
if (word) {
// if the word starts with a vowel, use "an" instead of "a"
const firstLetter = word[0].toLowerCase();
const vowels = ["a", "e", "i", "o", "u"];
const particle = vowels.includes(firstLetter) ? "an" : "a";
return particle + " " + word;
} else {
return "";
}
};
/**
return readable summary of claim, or something generic
@@ -1494,16 +1749,28 @@ export async function fetchEndorserRateLimits(
) {
const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(issuerDid);
try {
const response = await axios.get(url, { headers } as AxiosRequestConfig);
return response;
} catch (error) {
logger.error(
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
errorStringForLog(error),
);
throw error;
}
// Enhanced diagnostic logging for user registration tracking
logger.debug("[User Registration] Checking user status on server:", {
did: issuerDid,
server: apiServer,
endpoint: url,
timestamp: new Date().toISOString(),
});
// not wrapped in a 'try' because the error returned is self-explanatory
const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful registration check
logger.debug("[User Registration] User registration check successful:", {
did: issuerDid,
server: apiServer,
status: response.status,
isRegistered: true,
timestamp: new Date().toISOString(),
});
return response;
}
/**
@@ -1514,8 +1781,55 @@ export async function fetchEndorserRateLimits(
* @param {string} issuerDid - The DID for which to check rate limits.
* @returns {Promise<AxiosResponse>} The Axios response object.
*/
export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
export async function fetchImageRateLimits(
axios: Axios,
issuerDid: string,
imageServer?: string,
): Promise<AxiosResponse | null> {
const server = imageServer || DEFAULT_IMAGE_API_SERVER;
const url = server + "/image-limits";
const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig);
// Enhanced diagnostic logging for image server calls
logger.debug("[Image Server] Checking image rate limits:", {
did: issuerDid,
server: server,
endpoint: url,
timestamp: new Date().toISOString(),
});
try {
const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful image server call
logger.debug("[Image Server] Image rate limits check successful:", {
did: issuerDid,
server: server,
status: response.status,
timestamp: new Date().toISOString(),
});
return response;
} catch (error) {
// Enhanced error logging for image server failures
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
logger.warn(
"[Image Server] Image rate limits check failed, which is expected for users not registered on test server (eg. when only registered on local server).",
{
did: issuerDid,
server: server,
errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(),
},
);
return null;
}
}

View File

@@ -86,6 +86,7 @@ import {
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faStar,
faThumbtack,
faTrashCan,
faTriangleExclamation,
@@ -94,6 +95,9 @@ import {
faXmark,
} from "@fortawesome/free-solid-svg-icons";
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
// Initialize Font Awesome library with all required icons
library.add(
faArrowDown,
@@ -168,14 +172,16 @@ library.add(
faPlus,
faQrcode,
faQuestion,
faRotate,
faRightFromBracket,
faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faStar,
faStarRegular,
faThumbtack,
faTrashCan,
faTriangleExclamation,

View File

@@ -3,7 +3,7 @@
import axios, { AxiosResponse } from "axios";
import { Buffer } from "buffer";
import * as R from "ramda";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
import { Account, AccountEncrypted } from "../db/tables/accounts";
@@ -160,6 +160,49 @@ export const isGiveAction = (
return isGiveClaimType(veriClaim.claimType);
};
export interface OfferFulfillment {
offerHandleId: string;
offerType: string;
}
interface FulfillmentItem {
"@type": string;
identifier?: string;
[key: string]: unknown;
}
/**
* Extract offer fulfillment information from the fulfills field
* Handles both array and single object cases
*/
export const extractOfferFulfillment = (
fulfills: FulfillmentItem | FulfillmentItem[] | null | undefined,
): OfferFulfillment | null => {
if (!fulfills) {
return null;
}
// Handle both array and single object cases
let offerFulfill = null;
if (Array.isArray(fulfills)) {
// Find the Offer in the fulfills array
offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
} else if (fulfills["@type"] === "Offer") {
// fulfills is a single Offer object
offerFulfill = fulfills;
}
if (offerFulfill) {
return {
offerHandleId: offerFulfill.identifier || "",
offerType: offerFulfill["@type"],
};
}
return null;
};
export const shortDid = (did: string) => {
if (did.startsWith("did:peer:")) {
return (
@@ -197,11 +240,19 @@ export const nameForContact = (
);
};
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
export const doCopyTwoSecRedo = async (
text: string,
fn: () => void,
): Promise<void> => {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
try {
await copyToClipboard(text);
setTimeout(fn, 2000);
} catch (error) {
// Note: This utility function doesn't have access to notification system
// The calling component should handle error notifications
// Error is silently caught to avoid breaking the 2-second redo pattern
}
};
export interface ConfirmerData {
@@ -614,57 +665,65 @@ export const retrieveAllAccountsMetadata = async (): Promise<
return result;
};
export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account.";
/**
* Saves a new identity to both SQL and Dexie databases
* Saves a new identity to SQL database
*/
export async function saveNewIdentity(
identity: IIdentifier,
mnemonic: string,
derivationPath: string,
): Promise<void> {
try {
// add to the new sql db
const platformService = await getPlatformService();
// add to the new sql db
const platformService = await getPlatformService();
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
);
}
// Check if account already exists before attempting to save
const existingAccount = await platformService.dbQuery(
"SELECT did FROM accounts WHERE did = ?",
[identity.did],
);
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const identityStr = JSON.stringify(identity);
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`;
const params = [
new Date().toISOString(),
derivationPath,
identity.did,
encryptedIdentityBase64,
encryptedMnemonicBase64,
identity.keys[0].publicKeyHex,
];
await platformService.dbExec(sql, params);
await platformService.updateDefaultSettings({ activeDid: identity.did });
await platformService.insertNewDidIntoSettings(identity.did);
} catch (error) {
logger.error("Failed to update default settings:", error);
if (existingAccount?.values?.length) {
throw new Error(
"Failed to set default settings. Please try again or restart the app.",
`Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
);
}
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
);
}
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const identityStr = JSON.stringify(identity);
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`;
const params = [
new Date().toISOString(),
derivationPath,
identity.did,
encryptedIdentityBase64,
encryptedMnemonicBase64,
identity.keys[0].publicKeyHex,
];
await platformService.dbExec(sql, params);
// Update active identity in the active_identity table instead of settings
await platformService.updateActiveDid(identity.did);
await platformService.insertNewDidIntoSettings(identity.did);
}
/**
@@ -715,7 +774,8 @@ export const registerSaveAndActivatePasskey = async (
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
const platformService = await getPlatformService();
await platformService.updateDefaultSettings({ activeDid: account.did });
// Update active identity in the active_identity table instead of settings
await platformService.updateActiveDid(account.did);
await platformService.updateDidSpecificSettings(account.did, {
isRegistered: false,
});
@@ -974,13 +1034,16 @@ export async function importFromMnemonic(
const firstName = settings[0];
const isRegistered = settings[1];
logger.info("[importFromMnemonic] Test User #0 settings verification", {
did: newId.did,
firstName,
isRegistered,
expectedFirstName: "User Zero",
expectedIsRegistered: true,
});
logger.debug(
"[importFromMnemonic] Test User #0 settings verification",
{
did: newId.did,
firstName,
isRegistered,
expectedFirstName: "User Zero",
expectedIsRegistered: true,
},
);
// If settings weren't saved correctly, try individual updates
if (firstName !== "User Zero" || isRegistered !== 1) {
@@ -1006,7 +1069,7 @@ export async function importFromMnemonic(
if (retryResult?.values?.length) {
const retrySettings = retryResult.values[0];
logger.info(
logger.debug(
"[importFromMnemonic] Test User #0 settings after retry",
{
firstName: retrySettings[0],
@@ -1029,3 +1092,58 @@ export async function importFromMnemonic(
}
}
}
/**
* Checks if an account with the given DID already exists in the database
*
* @param did - The DID to check for duplicates
* @returns Promise<boolean> - True if account already exists, false otherwise
* @throws Error if database query fails
*/
export async function checkForDuplicateAccount(did: string): Promise<boolean>;
/**
* Checks if an account with the given DID already exists in the database
*
* @param mnemonic - The mnemonic phrase to derive DID from
* @param derivationPath - The derivation path to use
* @returns Promise<boolean> - True if account already exists, false otherwise
* @throws Error if database query fails
*/
export async function checkForDuplicateAccount(
mnemonic: string,
derivationPath: string,
): Promise<boolean>;
/**
* Implementation of checkForDuplicateAccount with overloaded signatures
*/
export async function checkForDuplicateAccount(
didOrMnemonic: string,
derivationPath?: string,
): Promise<boolean> {
let didToCheck: string;
if (derivationPath) {
// Derive the DID from mnemonic and derivation path
const [address, privateHex, publicHex] = deriveAddress(
didOrMnemonic.trim().toLowerCase(),
derivationPath,
);
const newId = newIdentifier(address, privateHex, publicHex, derivationPath);
didToCheck = newId.did;
} else {
// Use the provided DID directly
didToCheck = didOrMnemonic;
}
// Check if an account with this DID already exists
const platformService = await getPlatformService();
const existingAccount = await platformService.dbQuery(
"SELECT did FROM accounts WHERE did = ?",
[didToCheck],
);
return (existingAccount?.values?.length ?? 0) > 0;
}

View File

@@ -69,18 +69,18 @@ const deepLinkHandler = new DeepLinkHandler(router);
*/
const handleDeepLink = async (data: { url: string }) => {
const { url } = data;
logger.info(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
logger.debug(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
try {
// Wait for router to be ready
logger.info(`[Main] ⏳ Waiting for router to be ready...`);
logger.debug(`[Main] ⏳ Waiting for router to be ready...`);
await router.isReady();
logger.info(`[Main] ✅ Router is ready, processing deeplink`);
logger.debug(`[Main] ✅ Router is ready, processing deeplink`);
// Process the deeplink
logger.info(`[Main] 🚀 Starting deeplink processing`);
logger.debug(`[Main] 🚀 Starting deeplink processing`);
await deepLinkHandler.handleDeepLink(url);
logger.info(`[Main] ✅ Deeplink processed successfully`);
logger.debug(`[Main] ✅ Deeplink processed successfully`);
} catch (error) {
logger.error(`[Main] ❌ Deeplink processing failed:`, {
url,
@@ -115,25 +115,25 @@ const registerDeepLinkListener = async () => {
);
// Check if Capacitor App plugin is available
logger.info(`[Main] 🔍 Checking Capacitor App plugin availability...`);
logger.debug(`[Main] 🔍 Checking Capacitor App plugin availability...`);
if (!CapacitorApp) {
throw new Error("Capacitor App plugin not available");
}
logger.info(`[Main] ✅ Capacitor App plugin is available`);
// Check available methods on CapacitorApp
logger.info(
logger.debug(
`[Main] 🔍 Capacitor App plugin methods:`,
Object.getOwnPropertyNames(CapacitorApp),
);
logger.info(
logger.debug(
`[Main] 🔍 Capacitor App plugin addListener method:`,
typeof CapacitorApp.addListener,
);
// Wait for router to be ready first
await router.isReady();
logger.info(
logger.debug(
`[Main] ✅ Router is ready, proceeding with listener registration`,
);
@@ -148,9 +148,6 @@ const registerDeepLinkListener = async () => {
listenerHandle,
);
// Test the listener registration by checking if it's actually registered
logger.info(`[Main] 🧪 Verifying listener registration...`);
return listenerHandle;
} catch (error) {
logger.error(`[Main] ❌ Failed to register deeplink listener:`, {

View File

@@ -13,14 +13,23 @@ const platform = process.env.VITE_PLATFORM || "web";
logger.info(`[Main] 🚀 Loading TimeSafari for platform: ${platform}`);
// Log all relevant environment variables for boot-time debugging
logger.info("[Main] 🌍 Boot-time environment configuration:", {
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
nodeEnv: process.env.NODE_ENV,
timestamp: new Date().toISOString(),
});
// Dynamically import the appropriate main entry point
if (platform === "capacitor") {
logger.info(`[Main] 📱 Loading Capacitor-specific entry point`);
logger.debug(`[Main] 📱 Loading Capacitor-specific entry point`);
import("./main.capacitor");
} else if (platform === "electron") {
logger.info(`[Main] 💻 Loading Electron-specific entry point`);
logger.debug(`[Main] 💻 Loading Electron-specific entry point`);
import("./main.electron");
} else {
logger.info(`[Main] 🌐 Loading Web-specific entry point`);
logger.debug(`[Main] 🌐 Loading Web-specific entry point`);
import("./main.web");
}

View File

@@ -327,7 +327,7 @@ router.onError(errorHandler); // Assign the error handler to the router instance
* @param next - Navigation function
*/
router.beforeEach(async (to, _from, next) => {
logger.info(`[Router] 🧭 Navigation guard triggered:`, {
logger.debug(`[Router] 🧭 Navigation guard triggered:`, {
from: _from?.path || "none",
to: to.path,
name: to.name,
@@ -337,6 +337,22 @@ router.beforeEach(async (to, _from, next) => {
});
try {
// Log boot-time configuration on first navigation
if (!_from) {
logger.info(
"[Router] 🚀 First navigation detected - logging boot-time configuration:",
{
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer:
process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
nodeEnv: process.env.NODE_ENV,
targetRoute: to.path,
timestamp: new Date().toISOString(),
},
);
}
// Skip identity check for routes that handle identity creation manually
const skipIdentityRoutes = [
"/start",
@@ -352,11 +368,11 @@ router.beforeEach(async (to, _from, next) => {
return next();
}
logger.info(`[Router] 🔍 Checking user identity for route: ${to.path}`);
logger.debug(`[Router] 🔍 Checking user identity for route: ${to.path}`);
// Check if user has any identities
const allMyDids = await retrieveAccountDids();
logger.info(`[Router] 📋 Found ${allMyDids.length} user identities`);
logger.debug(`[Router] 📋 Found ${allMyDids.length} user identities`);
if (allMyDids.length === 0) {
logger.info("[Router] ⚠️ No identities found, creating default identity");
@@ -366,12 +382,12 @@ router.beforeEach(async (to, _from, next) => {
logger.info("[Router] ✅ Default identity created successfully");
} else {
logger.info(
logger.debug(
`[Router] ✅ User has ${allMyDids.length} identities, proceeding`,
);
}
logger.info(`[Router] ✅ Navigation guard passed for: ${to.path}`);
logger.debug(`[Router] ✅ Navigation guard passed for: ${to.path}`);
next();
} catch (error) {
logger.error("[Router] ❌ Identity creation failed in navigation guard:", {
@@ -392,7 +408,7 @@ router.beforeEach(async (to, _from, next) => {
// Add navigation success logging
router.afterEach((to, from) => {
logger.info(`[Router] ✅ Navigation completed:`, {
logger.debug(`[Router] ✅ Navigation completed:`, {
from: from?.path || "none",
to: to.path,
name: to.name,

View File

@@ -155,6 +155,16 @@ export interface PlatformService {
*/
dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
/**
* Not recommended except for debugging.
* Return the raw result of a SQL query.
*
* @param sql - The SQL query to execute
* @param params - The parameters to pass to the query
* @returns Promise resolving to the raw query result, or undefined if no results
*/
dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
// Database utility methods
/**
* Generates an INSERT SQL statement for a given model and table.
@@ -173,6 +183,7 @@ export interface PlatformService {
* @returns Promise that resolves when the update is complete
*/
updateDefaultSettings(settings: Record<string, unknown>): Promise<void>;
updateActiveDid(did: string): Promise<void>;
/**
* Inserts a new DID into the settings table.

View File

@@ -1,305 +0,0 @@
/**
* ProfileService - Handles user profile operations and API calls
* Extracted from AccountViewView.vue to improve separation of concerns
*/
import { AxiosInstance, AxiosError } from "axios";
import { UserProfile } from "@/libs/partnerServer";
import { UserProfileResponse } from "@/interfaces/accountView";
import { getHeaders, errorStringForLog } from "@/libs/endorserServer";
import { handleApiError } from "./api";
import { logger } from "@/utils/logger";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
/**
* Profile data interface
*/
export interface ProfileData {
description: string;
latitude: number;
longitude: number;
includeLocation: boolean;
}
/**
* Profile service class
*/
export class ProfileService {
private axios: AxiosInstance;
private partnerApiServer: string;
constructor(axios: AxiosInstance, partnerApiServer: string) {
this.axios = axios;
this.partnerApiServer = partnerApiServer;
}
/**
* Load user profile from the server
* @param activeDid - The user's DID
* @returns ProfileData or null if profile doesn't exist
*/
async loadProfile(activeDid: string): Promise<ProfileData | null> {
try {
const headers = await getHeaders(activeDid);
const response = await this.axios.get<UserProfileResponse>(
`${this.partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`,
{ headers },
);
if (response.status === 200) {
const data = response.data.data;
const profileData: ProfileData = {
description: data.description || "",
latitude: data.locLat || 0,
longitude: data.locLon || 0,
includeLocation: !!(data.locLat && data.locLon),
};
return profileData;
} else {
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.UNABLE_TO_LOAD_PROFILE);
}
} catch (error) {
if (this.isApiError(error) && error.response?.status === 404) {
// Profile doesn't exist yet - this is normal
return null;
}
logger.error("Error loading profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfileForIssuer");
return null;
}
}
/**
* Save user profile to the server
* @param activeDid - The user's DID
* @param profileData - The profile data to save
* @returns true if successful, false otherwise
*/
async saveProfile(
activeDid: string,
profileData: ProfileData,
): Promise<boolean> {
try {
const headers = await getHeaders(activeDid);
const payload: UserProfile = {
description: profileData.description,
issuerDid: activeDid,
};
// Add location data if location is included
if (
profileData.includeLocation &&
profileData.latitude &&
profileData.longitude
) {
payload.locLat = profileData.latitude;
payload.locLon = profileData.longitude;
}
const response = await this.axios.post(
`${this.partnerApiServer}/api/partner/userProfile`,
payload,
{ headers },
);
if (response.status === 201) {
return true;
} else {
logger.error("Error saving profile:", response);
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
}
} catch (error) {
logger.error("Error saving profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfile");
return false;
}
}
/**
* Delete user profile from the server
* @param activeDid - The user's DID
* @returns true if successful, false otherwise
*/
async deleteProfile(activeDid: string): Promise<boolean> {
try {
const headers = await getHeaders(activeDid);
const url = `${this.partnerApiServer}/api/partner/userProfile`;
const response = await this.axios.delete(url, { headers });
if (response.status === 204 || response.status === 200) {
logger.info("Profile deleted successfully");
return true;
} else {
logger.error("Unexpected response status when deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data,
});
throw new Error(
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
);
}
} catch (error) {
if (this.isApiError(error) && error.response) {
const response = error.response;
logger.error("API error deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data,
url: this.getErrorUrl(error),
});
// Handle specific HTTP status codes
if (response.status === 204) {
logger.debug("Profile deleted successfully (204 No Content)");
return true; // 204 is success for DELETE operations
} else if (response.status === 404) {
logger.warn("Profile not found - may already be deleted");
return true; // Consider this a success if profile doesn't exist
} else if (response.status === 400) {
logger.error("Bad request when deleting profile:", response.data);
const errorMessage =
typeof response.data === "string"
? response.data
: response.data?.message || "Bad request";
throw new Error(`Profile deletion failed: ${errorMessage}`);
} else if (response.status === 401) {
logger.error("Unauthorized to delete profile");
throw new Error("You are not authorized to delete this profile");
} else if (response.status === 403) {
logger.error("Forbidden to delete profile");
throw new Error("You are not allowed to delete this profile");
}
}
logger.error("Error deleting profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfile");
return false;
}
}
/**
* Update profile location
* @param profileData - Current profile data
* @param latitude - New latitude
* @param longitude - New longitude
* @returns Updated profile data
*/
updateProfileLocation(
profileData: ProfileData,
latitude: number,
longitude: number,
): ProfileData {
return {
...profileData,
latitude,
longitude,
includeLocation: true,
};
}
/**
* Toggle location inclusion in profile
* @param profileData - Current profile data
* @returns Updated profile data
*/
toggleProfileLocation(profileData: ProfileData): ProfileData {
const includeLocation = !profileData.includeLocation;
return {
...profileData,
latitude: includeLocation ? profileData.latitude : 0,
longitude: includeLocation ? profileData.longitude : 0,
includeLocation,
};
}
/**
* Clear profile location
* @param profileData - Current profile data
* @returns Updated profile data
*/
clearProfileLocation(profileData: ProfileData): ProfileData {
return {
...profileData,
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Reset profile to default state
* @returns Default profile data
*/
getDefaultProfile(): ProfileData {
return {
description: "",
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Type guard for API errors with proper typing
*/
private isApiError(error: unknown): error is {
response?: {
status?: number;
statusText?: string;
data?: { message?: string } | string;
};
} {
return typeof error === "object" && error !== null && "response" in error;
}
/**
* Extract error URL safely from error object
*/
private getErrorUrl(error: unknown): string | undefined {
if (this.isAxiosError(error)) {
return error.config?.url;
}
if (this.isApiError(error) && this.hasConfigProperty(error)) {
const config = this.getConfigProperty(error);
return config?.url;
}
return undefined;
}
/**
* Type guard to check if error has config property
*/
private hasConfigProperty(
error: unknown,
): error is { config?: { url?: string } } {
return typeof error === "object" && error !== null && "config" in error;
}
/**
* Safely extract config property from error
*/
private getConfigProperty(error: {
config?: { url?: string };
}): { url?: string } | undefined {
return error.config;
}
/**
* Type guard for AxiosError
*/
private isAxiosError(error: unknown): error is AxiosError {
return error instanceof AxiosError;
}
}
/**
* Factory function to create a ProfileService instance
*/
export function createProfileService(
axios: AxiosInstance,
partnerApiServer: string,
): ProfileService {
return new ProfileService(axios, partnerApiServer);
}

View File

@@ -36,7 +36,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Generate a short random ID for this scanner instance
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
this.options = options ?? {};
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
{
...this.options,
@@ -49,7 +49,7 @@ export class WebInlineQRScanner implements QRScannerService {
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
this.video = document.createElement("video");
this.video.setAttribute("playsinline", "true"); // Required for iOS
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
);
}
@@ -60,7 +60,7 @@ export class WebInlineQRScanner implements QRScannerService {
this.cameraStateListeners.forEach((listener) => {
try {
listener.onStateChange(state, message);
logger.info(
logger.debug(
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
{
state,
@@ -89,7 +89,7 @@ export class WebInlineQRScanner implements QRScannerService {
async checkPermissions(): Promise<boolean> {
try {
this.updateCameraState("initializing", "Checking camera permissions...");
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
);
@@ -99,7 +99,7 @@ export class WebInlineQRScanner implements QRScannerService {
const permissions = await navigator.permissions.query({
name: "camera" as PermissionName,
});
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
permissions.state,
);
@@ -165,7 +165,7 @@ export class WebInlineQRScanner implements QRScannerService {
"initializing",
"Requesting camera permissions...",
);
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
);
@@ -175,7 +175,7 @@ export class WebInlineQRScanner implements QRScannerService {
(device) => device.kind === "videoinput",
);
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
logger.debug(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
count: videoDevices.length,
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
userAgent: navigator.userAgent,
@@ -188,7 +188,7 @@ export class WebInlineQRScanner implements QRScannerService {
}
// Try to get a stream with specific constraints
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
{
facingMode: "environment",
@@ -210,7 +210,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Stop the test stream immediately
stream.getTracks().forEach((track) => {
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
kind: track.kind,
label: track.label,
readyState: track.readyState,
@@ -275,12 +275,12 @@ export class WebInlineQRScanner implements QRScannerService {
async isSupported(): Promise<boolean> {
try {
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
);
// Check for secure context first
if (!window.isSecureContext) {
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
);
return false;
@@ -300,7 +300,7 @@ export class WebInlineQRScanner implements QRScannerService {
(device) => device.kind === "videoinput",
);
logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, {
logger.debug(`[WebInlineQRScanner:${this.id}] Device support check:`, {
hasSecureContext: window.isSecureContext,
hasMediaDevices: !!navigator.mediaDevices,
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
@@ -379,7 +379,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Log scan attempt every 100 frames or 1 second
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
logger.debug(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
attempt: this.scanAttempts,
dimensions: {
width: this.canvas.width,
@@ -421,7 +421,7 @@ export class WebInlineQRScanner implements QRScannerService {
!code.data ||
code.data.length === 0;
logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
logger.debug(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
data: code.data,
location: code.location,
attempts: this.scanAttempts,
@@ -512,13 +512,13 @@ export class WebInlineQRScanner implements QRScannerService {
this.scanAttempts = 0;
this.lastScanTime = Date.now();
this.updateCameraState("initializing", "Starting camera...");
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
this.options,
);
// Get camera stream with options
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
);
this.stream = await navigator.mediaDevices.getUserMedia({
@@ -531,7 +531,7 @@ export class WebInlineQRScanner implements QRScannerService {
this.updateCameraState("active", "Camera is active");
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
logger.debug(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
tracks: this.stream.getTracks().map((t) => ({
kind: t.kind,
label: t.label,
@@ -550,14 +550,14 @@ export class WebInlineQRScanner implements QRScannerService {
this.video.style.display = "none";
}
await this.video.play();
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Video element started playing`,
);
}
// Emit stream to component
this.events.emit("stream", this.stream);
logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
logger.debug(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
// Start QR code scanning
this.scanQRCode();
@@ -595,7 +595,7 @@ export class WebInlineQRScanner implements QRScannerService {
}
try {
logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
scanAttempts: this.scanAttempts,
duration: Date.now() - this.lastScanTime,
});
@@ -604,7 +604,7 @@ export class WebInlineQRScanner implements QRScannerService {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
);
}
@@ -613,13 +613,13 @@ export class WebInlineQRScanner implements QRScannerService {
if (this.video) {
this.video.pause();
this.video.srcObject = null;
logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`);
logger.debug(`[WebInlineQRScanner:${this.id}] Video element stopped`);
}
// Stop all tracks in the stream
if (this.stream) {
this.stream.getTracks().forEach((track) => {
logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
kind: track.kind,
label: track.label,
readyState: track.readyState,
@@ -631,7 +631,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Emit stream stopped event
this.events.emit("stream", null);
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
);
} catch (error) {
@@ -643,17 +643,17 @@ export class WebInlineQRScanner implements QRScannerService {
throw error;
} finally {
this.isScanning = false;
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
logger.debug(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
}
}
addListener(listener: ScanListener): void {
logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
logger.debug(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
this.scanListener = listener;
}
onStream(callback: (stream: MediaStream | null) => void): void {
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
);
this.events.on("stream", callback);
@@ -661,24 +661,24 @@ export class WebInlineQRScanner implements QRScannerService {
async cleanup(): Promise<void> {
try {
logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
logger.debug(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
await this.stopScan();
this.events.removeAllListeners();
logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
logger.debug(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
// Clean up DOM elements
if (this.video) {
this.video.remove();
this.video = null;
logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`);
logger.debug(`[WebInlineQRScanner:${this.id}] Video element removed`);
}
if (this.canvas) {
this.canvas.remove();
this.canvas = null;
logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
logger.debug(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
}
this.context = null;
logger.error(
logger.debug(
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
);
} catch (error) {

View File

@@ -19,7 +19,6 @@ import { logger, safeStringify } from "../utils/logger";
* @remarks
* Special handling includes:
* - Enhanced logging for Capacitor platform
* - Rate limit detection and handling
* - Detailed error information logging including:
* - Error message
* - HTTP status
@@ -50,11 +49,5 @@ export const handleApiError = (error: AxiosError, endpoint: string) => {
});
}
// Specific handling for rate limits
if (error.response?.status === 400) {
logger.warn(`[Rate Limit] ${endpoint}`);
return null;
}
throw error;
};

View File

@@ -73,6 +73,8 @@ interface Migration {
name: string;
/** SQL statement(s) to execute for this migration */
sql: string;
/** Optional array of individual SQL statements for better error handling */
statements?: string[];
}
/**
@@ -225,6 +227,104 @@ export function registerMigration(migration: Migration): void {
* }
* ```
*/
/**
* Helper function to check if a SQLite result indicates a table exists
* @param result - The result from a sqlite_master query
* @returns true if the table exists
*/
function checkSqliteTableResult(result: unknown): boolean {
return (
(result as unknown as { values: unknown[][] })?.values?.length > 0 ||
(Array.isArray(result) && result.length > 0)
);
}
/**
* Helper function to validate that a table exists in the database
* @param tableName - Name of the table to check
* @param sqlQuery - Function to execute SQL queries
* @returns Promise resolving to true if table exists
*/
async function validateTableExists<T>(
tableName: string,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
try {
const result = await sqlQuery(
`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
);
return checkSqliteTableResult(result);
} catch (error) {
logger.error(`❌ [Validation] Error checking table ${tableName}:`, error);
return false;
}
}
/**
* Helper function to validate that a column exists in a table
* @param tableName - Name of the table
* @param columnName - Name of the column to check
* @param sqlQuery - Function to execute SQL queries
* @returns Promise resolving to true if column exists
*/
async function validateColumnExists<T>(
tableName: string,
columnName: string,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
try {
await sqlQuery(`SELECT ${columnName} FROM ${tableName} LIMIT 1`);
return true;
} catch (error) {
logger.error(
`❌ [Validation] Error checking column ${columnName} in ${tableName}:`,
error,
);
return false;
}
}
/**
* Helper function to validate multiple tables exist
* @param tableNames - Array of table names to check
* @param sqlQuery - Function to execute SQL queries
* @returns Promise resolving to array of validation results
*/
async function validateMultipleTables<T>(
tableNames: string[],
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<{ exists: boolean; missing: string[] }> {
const missing: string[] = [];
for (const tableName of tableNames) {
const exists = await validateTableExists(tableName, sqlQuery);
if (!exists) {
missing.push(tableName);
}
}
return {
exists: missing.length === 0,
missing,
};
}
/**
* Helper function to add validation error with consistent logging
* @param validation - The validation object to update
* @param message - Error message to add
* @param error - The error object for logging
*/
function addValidationError(
validation: MigrationValidation,
message: string,
error: unknown,
): void {
validation.isValid = false;
validation.errors.push(message);
logger.error(`❌ [Migration-Validation] ${message}:`, error);
}
async function validateMigrationApplication<T>(
migration: Migration,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
@@ -248,36 +348,82 @@ async function validateMigrationApplication<T>(
"temp",
];
for (const tableName of tables) {
try {
await sqlQuery(
`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
);
// Reduced logging - only log on error
} catch (error) {
validation.isValid = false;
validation.errors.push(`Table ${tableName} missing`);
logger.error(
`❌ [Migration-Validation] Table ${tableName} missing:`,
error,
);
}
}
validation.tableExists = validation.errors.length === 0;
} else if (migration.name === "002_add_iViewContent_to_contacts") {
// Validate iViewContent column exists in contacts table
try {
await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`);
validation.hasExpectedColumns = true;
// Reduced logging - only log on error
} catch (error) {
const tableValidation = await validateMultipleTables(tables, sqlQuery);
if (!tableValidation.exists) {
validation.isValid = false;
validation.errors.push(
`Column iViewContent missing from contacts table`,
`Missing tables: ${tableValidation.missing.join(", ")}`,
);
logger.error(
`❌ [Migration-Validation] Column iViewContent missing:`,
error,
`❌ [Migration-Validation] Missing tables:`,
tableValidation.missing,
);
}
validation.tableExists = tableValidation.exists;
} else if (migration.name === "002_add_iViewContent_to_contacts") {
// Validate iViewContent column exists in contacts table
const columnExists = await validateColumnExists(
"contacts",
"iViewContent",
sqlQuery,
);
if (!columnExists) {
addValidationError(
validation,
"Column iViewContent missing from contacts table",
new Error("Column not found"),
);
} else {
validation.hasExpectedColumns = true;
}
} else if (migration.name === "004_active_identity_management") {
// Validate active_identity table exists and has correct structure
const activeIdentityExists = await validateTableExists(
"active_identity",
sqlQuery,
);
if (!activeIdentityExists) {
addValidationError(
validation,
"Table active_identity missing",
new Error("Table not found"),
);
} else {
validation.tableExists = true;
// Check that active_identity has the expected structure
const hasExpectedColumns = await validateColumnExists(
"active_identity",
"id, activeDid, lastUpdated",
sqlQuery,
);
if (!hasExpectedColumns) {
addValidationError(
validation,
"active_identity table missing expected columns",
new Error("Columns not found"),
);
} else {
validation.hasExpectedColumns = true;
}
}
// Check that hasBackedUpSeed column exists in settings table
// Note: This validation is included here because migration 004 is consolidated
// and includes the functionality from the original migration 003
const hasBackedUpSeedExists = await validateColumnExists(
"settings",
"hasBackedUpSeed",
sqlQuery,
);
if (!hasBackedUpSeedExists) {
addValidationError(
validation,
"Column hasBackedUpSeed missing from settings table",
new Error("Column not found"),
);
}
}
@@ -343,6 +489,55 @@ async function isSchemaAlreadyPresent<T>(
// Reduced logging - only log on error
return false;
}
} else if (migration.name === "003_add_hasBackedUpSeed_to_settings") {
// Check if hasBackedUpSeed column exists in settings table
try {
await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
return true;
} catch (error) {
return false;
}
} else if (migration.name === "004_active_identity_management") {
// Check if active_identity table exists and has correct structure
try {
// Check that active_identity table exists
const activeIdentityResult = await sqlQuery(
`SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'`,
);
const hasActiveIdentityTable =
(activeIdentityResult as unknown as { values: unknown[][] })?.values
?.length > 0 ||
(Array.isArray(activeIdentityResult) &&
activeIdentityResult.length > 0);
if (!hasActiveIdentityTable) {
return false;
}
// Check that active_identity has the expected structure
try {
await sqlQuery(
`SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1`,
);
// Also check that hasBackedUpSeed column exists in settings
// This is included because migration 004 is consolidated
try {
await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
return true;
} catch (error) {
return false;
}
} catch (error) {
return false;
}
} catch (error) {
logger.error(
`🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`,
error,
);
return false;
}
}
// Add schema checks for future migrations here
@@ -404,15 +599,10 @@ export async function runMigrations<T>(
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
const isDevelopment = process.env.VITE_PLATFORM === "development";
// Use debug level for routine migration messages in development
const migrationLog = isDevelopment ? logger.debug : logger.log;
try {
migrationLog("📋 [Migration] Starting migration process...");
logger.debug("📋 [Migration] Starting migration process...");
// Step 1: Create migrations table if it doesn't exist
// Create migrations table if it doesn't exist
// Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
await sqlExec(`
CREATE TABLE IF NOT EXISTS migrations (
@@ -436,7 +626,8 @@ export async function runMigrations<T>(
return;
}
migrationLog(
// Only log migration counts in development
logger.debug(
`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`,
);
@@ -448,22 +639,22 @@ export async function runMigrations<T>(
// Check 1: Is it recorded as applied in migrations table?
const isRecordedAsApplied = appliedMigrations.has(migration.name);
// Check 2: Does the schema already exist in the database?
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
// Skip if already recorded as applied
// Skip if already recorded as applied (name-only check)
if (isRecordedAsApplied) {
skippedCount++;
continue;
}
// Check 2: Does the schema already exist in the database?
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
// Handle case where schema exists but isn't recorded
if (isSchemaPresent) {
try {
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
migrationLog(
logger.debug(
`✅ [Migration] Marked existing schema as applied: ${migration.name}`,
);
skippedCount++;
@@ -478,11 +669,20 @@ export async function runMigrations<T>(
}
// Apply the migration
migrationLog(`🔄 [Migration] Applying migration: ${migration.name}`);
logger.debug(`🔄 [Migration] Applying migration: ${migration.name}`);
try {
// Execute the migration SQL
await sqlExec(migration.sql);
// Execute the migration SQL as single atomic operation
logger.debug(`🔧 [Migration] Executing SQL for: ${migration.name}`);
logger.debug(`🔧 [Migration] SQL content: ${migration.sql}`);
// Execute the migration SQL directly - it should be atomic
// The SQL itself should handle any necessary transactions
const execResult = await sqlExec(migration.sql);
logger.debug(
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
);
// Validate the migration was applied correctly
const validation = await validateMigrationApplication(
@@ -501,11 +701,33 @@ export async function runMigrations<T>(
migration.name,
]);
migrationLog(`🎉 [Migration] Successfully applied: ${migration.name}`);
logger.debug(`🎉 [Migration] Successfully applied: ${migration.name}`);
appliedCount++;
} catch (error) {
logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);
// Provide explicit rollback instructions for migration failures
logger.error(
`🔄 [Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
);
logger.error(` 1. Stop the application immediately`);
logger.error(
` 2. Restore database from pre-migration backup/snapshot`,
);
logger.error(
` 3. Remove migration entry: DELETE FROM migrations WHERE name = '${migration.name}'`,
);
logger.error(
` 4. Verify database state matches pre-migration condition`,
);
logger.error(` 5. Restart application and investigate root cause`);
logger.error(
` FAILURE CAUSE: ${error instanceof Error ? error.message : String(error)}`,
);
logger.error(
` REQUIRED OPERATOR ACTION: Manual database restoration required`,
);
// Handle specific cases where the migration might be partially applied
const errorMessage = String(error).toLowerCase();
@@ -517,7 +739,7 @@ export async function runMigrations<T>(
(errorMessage.includes("table") &&
errorMessage.includes("already exists"))
) {
migrationLog(
logger.debug(
`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`,
);
@@ -531,6 +753,8 @@ export async function runMigrations<T>(
`⚠️ [Migration] Schema validation failed for ${migration.name}:`,
validation.errors,
);
// Don't mark as applied if validation fails
continue;
}
// Mark the migration as applied since the schema change already exists
@@ -538,7 +762,7 @@ export async function runMigrations<T>(
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
migrationLog(`✅ [Migration] Marked as applied: ${migration.name}`);
logger.debug(`✅ [Migration] Marked as applied: ${migration.name}`);
appliedCount++;
} catch (insertError) {
// If we can't insert the migration record, log it but don't fail
@@ -558,7 +782,7 @@ export async function runMigrations<T>(
}
}
// Step 5: Final validation - verify all migrations are properly recorded
// Step 6: Final validation - verify all migrations are properly recorded
const finalMigrationsResult = await sqlQuery("SELECT name FROM migrations");
const finalAppliedMigrations = extractMigrationNames(finalMigrationsResult);
@@ -574,7 +798,7 @@ export async function runMigrations<T>(
);
}
// Always show completion message
// Only show completion message in development
logger.log(
`🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
);

View File

@@ -24,7 +24,7 @@ import {
import { logger } from "../../utils/logger";
interface QueuedOperation {
type: "run" | "query";
type: "run" | "query" | "rawQuery";
sql: string;
params: unknown[];
resolve: (value: unknown) => void;
@@ -66,13 +66,13 @@ export class CapacitorPlatformService implements PlatformService {
return this.initializationPromise;
}
// Start initialization
this.initializationPromise = this._initialize();
try {
// Start initialization
this.initializationPromise = this._initialize();
await this.initializationPromise;
} catch (error) {
logger.error(
"[CapacitorPlatformService] Initialize method failed:",
"[CapacitorPlatformService] Initialize database method failed:",
error,
);
this.initializationPromise = null; // Reset on failure
@@ -159,6 +159,14 @@ export class CapacitorPlatformService implements PlatformService {
};
break;
}
case "rawQuery": {
const queryResult = await this.db.query(
operation.sql,
operation.params,
);
result = queryResult;
break;
}
}
operation.resolve(result);
} catch (error) {
@@ -500,9 +508,24 @@ export class CapacitorPlatformService implements PlatformService {
// This is essential for proper parameter binding and SQL injection prevention
await this.db!.run(sql, params);
} else {
// Use execute method for non-parameterized queries
// This is more efficient for simple DDL statements
await this.db!.execute(sql);
// For multi-statement SQL (like migrations), use executeSet method
// This handles multiple statements properly
if (
sql.includes(";") &&
sql.split(";").filter((s) => s.trim()).length > 1
) {
// Multi-statement SQL - use executeSet for proper handling
const statements = sql.split(";").filter((s) => s.trim());
await this.db!.executeSet(
statements.map((stmt) => ({
statement: stmt.trim(),
values: [], // Empty values array for non-parameterized statements
})),
);
} else {
// Single statement - use execute method
await this.db!.execute(sql);
}
}
};
@@ -1270,6 +1293,14 @@ export class CapacitorPlatformService implements PlatformService {
return undefined;
}
/**
* @see PlatformService.dbRawQuery
*/
async dbRawQuery(sql: string, params?: unknown[]): Promise<unknown> {
await this.waitForInitialization();
return this.queueOperation("rawQuery", sql, params || []);
}
/**
* Checks if running on Capacitor platform.
* @returns true, as this is the Capacitor implementation
@@ -1319,8 +1350,24 @@ export class CapacitorPlatformService implements PlatformService {
await this.dbExec(sql, params);
}
async updateActiveDid(did: string): Promise<void> {
await this.dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did],
);
}
async insertNewDidIntoSettings(did: string): Promise<void> {
await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
// Import constants dynamically to avoid circular dependencies
const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
await import("@/constants/app");
// Use INSERT OR REPLACE to handle case where settings already exist for this DID
// This prevents duplicate accountDid entries and ensures data integrity
await this.dbExec(
"INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
[did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
);
}
async updateDidSpecificSettings(

View File

@@ -636,6 +636,17 @@ export class WebPlatformService implements PlatformService {
} as GetOneRowRequest);
}
/**
* @see PlatformService.dbRawQuery
*/
async dbRawQuery(
sql: string,
params?: unknown[],
): Promise<unknown | undefined> {
// This class doesn't post-process the result, so we can just use it.
return this.dbQuery(sql, params);
}
/**
* Rotates the camera between front and back cameras.
* @returns Promise that resolves when the camera is rotated
@@ -674,15 +685,51 @@ export class WebPlatformService implements PlatformService {
async updateDefaultSettings(
settings: Record<string, unknown>,
): Promise<void> {
// Get current active DID and update that identity's settings
const activeIdentity = await this.getActiveIdentity();
const activeDid = activeIdentity.activeDid;
if (!activeDid) {
logger.warn(
"[WebPlatformService] No active DID found, cannot update default settings",
);
return;
}
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE id = 1`;
const params = keys.map((key) => settings[key]);
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), activeDid];
await this.dbExec(sql, params);
}
async updateActiveDid(did: string): Promise<void> {
await this.dbExec(
"INSERT OR REPLACE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, ?)",
[did, new Date().toISOString()],
);
}
async getActiveIdentity(): Promise<{ activeDid: string }> {
const result = await this.dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
);
return {
activeDid: (result?.values?.[0]?.[0] as string) || "",
};
}
async insertNewDidIntoSettings(did: string): Promise<void> {
await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
// Import constants dynamically to avoid circular dependencies
const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
await import("@/constants/app");
// Use INSERT OR REPLACE to handle case where settings already exist for this DID
// This prevents duplicate accountDid entries and ensures data integrity
await this.dbExec(
"INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
[did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
);
}
async updateDidSpecificSettings(

View File

@@ -85,7 +85,6 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({
mixins: [PlatformServiceMixin],
@@ -197,10 +196,10 @@ This tests the helper method only - no database interaction`;
const success = await this.$saveSettings(testSettings);
if (success) {
// Now query the raw database to see how it's actually stored
// Now query the raw database to see how it's actually stored.
// Note that new users probably have settings with ID of 1 but old migrated users might skip to 2.
const rawResult = await this.$dbQuery(
"SELECT searchBoxes FROM settings WHERE id = ?",
[MASTER_SETTINGS_KEY],
"SELECT searchBoxes FROM settings limit 1",
);
if (rawResult?.values?.length) {

View File

@@ -66,7 +66,7 @@ export async function testServerRegisterUser() {
// Make a payload for the claim
const vcPayload = {
sub: "RegisterAction",
sub: identity0.did,
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],

View File

@@ -45,7 +45,6 @@ import type {
PlatformCapabilities,
} from "@/services/PlatformService";
import {
MASTER_SETTINGS_KEY,
type Settings,
type SettingsWithJsonStrings,
} from "@/db/tables/settings";
@@ -53,7 +52,11 @@ import { logger } from "@/utils/logger";
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
import { Account } from "@/db/tables/accounts";
import { Temp } from "@/db/tables/temp";
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
import {
QueryExecResult,
DatabaseExecResult,
SqlValue,
} from "@/interfaces/database";
import {
generateInsertStatement,
generateUpdateStatement,
@@ -210,11 +213,53 @@ export const PlatformServiceMixin = {
logger.debug(
`[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`,
);
// Write only to active_identity table (single source of truth)
try {
await this.$dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[newDid || ""],
);
logger.debug(
`[PlatformServiceMixin] ActiveDid updated in active_identity table: ${newDid}`,
);
} catch (error) {
logger.error(
`[PlatformServiceMixin] Error updating activeDid in active_identity table ${newDid}:`,
error,
);
// Continue with in-memory update even if database write fails
}
// // Clear caches that might be affected by the change
// this.$clearAllCaches();
}
},
/**
* Get available account DIDs for user selection
* Returns array of DIDs that can be set as active identity
*/
async $getAvailableAccountDids(): Promise<string[]> {
try {
const result = await this.$dbQuery(
"SELECT did FROM accounts ORDER BY did",
);
if (!result?.values?.length) {
return [];
}
return result.values.map((row: SqlValue[]) => row[0] as string);
} catch (error) {
logger.error(
"[PlatformServiceMixin] Error getting available account DIDs:",
error,
);
return [];
}
},
/**
* Map database columns to values with proper type conversion
* Handles boolean conversion from SQLite integers (0/1) to boolean values
@@ -230,16 +275,22 @@ export const PlatformServiceMixin = {
// Convert SQLite integer booleans to JavaScript booleans
if (
// settings
column === "isRegistered" ||
column === "finishedOnboarding" ||
column === "filterFeedByVisible" ||
column === "filterFeedByNearby" ||
column === "hasBackedUpSeed" ||
column === "hideRegisterPromptOnNewContact" ||
column === "showContactGivesInline" ||
column === "showGeneralAdvanced" ||
column === "showShortcutBvc" ||
column === "warnIfProdServer" ||
column === "warnIfTestServer"
column === "warnIfTestServer" ||
// contacts
column === "iViewContent" ||
column === "registered" ||
column === "seesMe"
) {
if (value === 1) {
value = true;
@@ -249,13 +300,13 @@ export const PlatformServiceMixin = {
// Keep null values as null
}
// Handle JSON fields like contactMethods
if (column === "contactMethods" && typeof value === "string") {
try {
value = JSON.parse(value);
} catch {
value = [];
}
// Convert SQLite JSON strings to objects/arrays
if (
column === "contactMethods" ||
column === "searchBoxes" ||
column === "starredPlanHandleIds"
) {
value = this._parseJsonField(value, []);
}
obj[column] = value;
@@ -265,10 +316,13 @@ export const PlatformServiceMixin = {
},
/**
* Self-contained implementation of parseJsonField
* Safely parses JSON strings with fallback to default value
* Safely parses JSON strings with fallback to default value.
* Handles different SQLite implementations:
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
* - Capacitor SQLite: Returns raw strings that need manual parsing
*
* Consolidate this with src/libs/util.ts parseJsonField
* See also src/db/databaseUtil.ts parseJsonField
* and maybe consolidate
*/
_parseJsonField<T>(value: unknown, defaultValue: T): T {
if (typeof value === "string") {
@@ -299,6 +353,14 @@ export const PlatformServiceMixin = {
? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes);
}
if (settings.starredPlanHandleIds !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(converted as any).starredPlanHandleIds = Array.isArray(
settings.starredPlanHandleIds,
)
? JSON.stringify(settings.starredPlanHandleIds)
: String(settings.starredPlanHandleIds);
}
return converted;
},
@@ -418,7 +480,10 @@ export const PlatformServiceMixin = {
/**
* Enhanced database single row query method with error handling
*/
async $dbGetOneRow(sql: string, params?: unknown[]) {
async $dbGetOneRow(
sql: string,
params?: unknown[],
): Promise<SqlValue[] | undefined> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
@@ -437,17 +502,46 @@ export const PlatformServiceMixin = {
},
/**
* Utility method for retrieving and parsing settings
* Database raw query method with error handling
*/
async $dbRawQuery(sql: string, params?: unknown[]) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbRawQuery(sql, params);
} catch (error) {
logger.error(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
`[${(this as any).$options.name}] Database raw query failed:`,
{
sql,
params,
error,
},
);
throw error;
}
},
/**
* Utility method for retrieving master settings
* Common pattern used across many components
*/
async $getSettings(
key: string,
async _getMasterSettings(
fallback: Settings | null = null,
): Promise<Settings | null> {
try {
// Get current active identity
const activeIdentity = await this.$getActiveIdentity();
const activeDid = activeIdentity.activeDid;
if (!activeDid) {
return fallback;
}
// Get identity-specific settings
const result = await this.$dbQuery(
"SELECT * FROM settings WHERE id = ? OR accountDid = ?",
[key, key],
"SELECT * FROM settings WHERE accountDid = ?",
[activeDid],
);
if (!result?.values?.length) {
@@ -469,11 +563,16 @@ export const PlatformServiceMixin = {
if (settings.searchBoxes) {
settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []);
}
if (settings.starredPlanHandleIds) {
settings.starredPlanHandleIds = this._parseJsonField(
settings.starredPlanHandleIds,
[],
);
}
return settings;
} catch (error) {
logger.error(`[Settings Trace] ❌ Failed to get settings:`, {
key,
logger.error(`[Settings Trace] ❌ Failed to get master settings:`, {
error,
});
return fallback;
@@ -485,16 +584,12 @@ export const PlatformServiceMixin = {
* Handles the common pattern of layered settings
*/
async $getMergedSettings(
defaultKey: string,
accountDid?: string,
defaultFallback: Settings = {},
): Promise<Settings> {
try {
// Get default settings
const defaultSettings = await this.$getSettings(
defaultKey,
defaultFallback,
);
const defaultSettings = await this._getMasterSettings(defaultFallback);
// If no account DID, return defaults
if (!accountDid) {
@@ -540,11 +635,16 @@ export const PlatformServiceMixin = {
[],
);
}
if (mergedSettings.starredPlanHandleIds) {
mergedSettings.starredPlanHandleIds = this._parseJsonField(
mergedSettings.starredPlanHandleIds,
[],
);
}
return mergedSettings;
} catch (error) {
logger.error(`[Settings Trace] ❌ Failed to get merged settings:`, {
defaultKey,
accountDid,
error,
});
@@ -552,6 +652,73 @@ export const PlatformServiceMixin = {
}
},
/**
* Get active identity from the new active_identity table
* This replaces the activeDid field in settings for better architecture
*/
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 - this may indicate a migration issue",
);
return { activeDid: "" };
}
const activeDid = result.values[0][0] as string | null;
// Handle null activeDid (initial state after migration) - 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: "" };
}
},
/**
* Transaction wrapper with automatic rollback on error
*/
@@ -567,6 +734,76 @@ export const PlatformServiceMixin = {
}
},
// =================================================
// SMART DELETION PATTERN DAL METHODS
// =================================================
/**
* Get account DID by ID
* Required for smart deletion pattern
*/
async $getAccountDidById(id: number): Promise<string> {
const result = await this.$dbQuery(
"SELECT did FROM accounts WHERE id = ?",
[id],
);
return result?.values?.[0]?.[0] as string;
},
/**
* Get active DID (returns null if none selected)
* Required for smart deletion pattern
*/
async $getActiveDid(): Promise<string | null> {
const result = await this.$dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
);
return (result?.values?.[0]?.[0] as string) || null;
},
/**
* Set active DID (can be null for no selection)
* Required for smart deletion pattern
*/
async $setActiveDid(did: string | null): Promise<void> {
await this.$dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did],
);
},
/**
* Count total accounts
* Required for smart deletion pattern
*/
async $countAccounts(): Promise<number> {
const result = await this.$dbQuery("SELECT COUNT(*) FROM accounts");
return (result?.values?.[0]?.[0] as number) || 0;
},
/**
* Deterministic "next" picker for account selection
* Required for smart deletion pattern
*/
$pickNextAccountDid(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];
},
/**
* Ensure an active account is selected (repair hook)
* Required for smart deletion pattern bootstrapping
*/
async $ensureActiveSelected(): Promise<void> {
const active = await this.$getActiveDid();
const all = await this.$getAllAccountDids();
if (active === null && all.length > 0) {
await this.$setActiveDid(this.$pickNextAccountDid(all));
}
},
// =================================================
// ULTRA-CONCISE DATABASE METHODS (shortest names)
// =================================================
@@ -605,7 +842,7 @@ export const PlatformServiceMixin = {
async $one(
sql: string,
params: unknown[] = [],
): Promise<unknown[] | undefined> {
): Promise<SqlValue[] | undefined> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
},
@@ -757,19 +994,20 @@ export const PlatformServiceMixin = {
* @returns Fresh settings object from database
*/
async $settings(defaults: Settings = {}): Promise<Settings> {
const settings = await this.$getSettings(MASTER_SETTINGS_KEY, defaults);
const settings = await this._getMasterSettings(defaults);
if (!settings) {
return defaults;
}
// **ELECTRON-SPECIFIC FIX**: Apply platform-specific API server override
// This ensures Electron always uses production endpoints regardless of cached settings
if (process.env.VITE_PLATFORM === "electron") {
// FIXED: Set default apiServer for all platforms, not just Electron
// Only set default if no user preference exists
if (!settings.apiServer) {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
// Set default for all platforms when apiServer is empty
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
@@ -789,17 +1027,15 @@ export const PlatformServiceMixin = {
): Promise<Settings> {
try {
// Get default settings first
const defaultSettings = await this.$getSettings(
MASTER_SETTINGS_KEY,
defaults,
);
const defaultSettings = await this._getMasterSettings(defaults);
if (!defaultSettings) {
return defaults;
}
// Determine which DID to use
const targetDid = did || defaultSettings.activeDid;
// Get DID from active_identity table (single source of truth)
const activeIdentity = await this.$getActiveIdentity();
const targetDid = did || activeIdentity.activeDid;
// If no target DID, return default settings
if (!targetDid) {
@@ -808,19 +1044,29 @@ export const PlatformServiceMixin = {
// Get merged settings using existing method
const mergedSettings = await this.$getMergedSettings(
MASTER_SETTINGS_KEY,
targetDid,
defaultSettings,
);
// **ELECTRON-SPECIFIC FIX**: Force production API endpoints for Electron
// This ensures Electron doesn't use localhost development servers that might be saved in user settings
if (process.env.VITE_PLATFORM === "electron") {
// Set activeDid from active_identity table (single source of truth)
mergedSettings.activeDid = activeIdentity.activeDid;
logger.debug(
"[PlatformServiceMixin] Using activeDid from active_identity table:",
{ activeDid: activeIdentity.activeDid },
);
logger.debug(
"[PlatformServiceMixin] $accountSettings() returning activeDid:",
{ activeDid: mergedSettings.activeDid },
);
// FIXED: Set default apiServer for all platforms, not just Electron
// Only set default if no user preference exists
if (!mergedSettings.apiServer) {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
// Set default for all platforms when apiServer is empty
mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
@@ -858,16 +1104,36 @@ export const PlatformServiceMixin = {
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
try {
// Remove fields that shouldn't be updated
const { accountDid, id, ...safeChanges } = changes;
const {
accountDid,
id,
activeDid: activeDidField,
...safeChanges
} = changes;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void accountDid;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void id;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void activeDidField;
logger.debug(
"[PlatformServiceMixin] $saveSettings - Original changes:",
changes,
);
logger.debug(
"[PlatformServiceMixin] $saveSettings - Safe changes:",
safeChanges,
);
if (Object.keys(safeChanges).length === 0) return true;
// Convert settings for database storage (handles searchBoxes conversion)
const convertedChanges = this._convertSettingsForStorage(safeChanges);
logger.debug(
"[PlatformServiceMixin] $saveSettings - Converted changes:",
convertedChanges,
);
const setParts: string[] = [];
const params: unknown[] = [];
@@ -879,17 +1145,33 @@ export const PlatformServiceMixin = {
}
});
logger.debug(
"[PlatformServiceMixin] $saveSettings - Set parts:",
setParts,
);
logger.debug("[PlatformServiceMixin] $saveSettings - Params:", params);
if (setParts.length === 0) return true;
params.push(MASTER_SETTINGS_KEY);
await this.$dbExec(
`UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`,
params,
);
// Get current active DID and update that identity's settings
const activeIdentity = await this.$getActiveIdentity();
const currentActiveDid = activeIdentity.activeDid;
if (currentActiveDid) {
params.push(currentActiveDid);
await this.$dbExec(
`UPDATE settings SET ${setParts.join(", ")} WHERE accountDid = ?`,
params,
);
} else {
logger.warn(
"[PlatformServiceMixin] No active DID found, cannot save settings",
);
}
// Update activeDid tracking if it changed
if (changes.activeDid !== undefined) {
await this.$updateActiveDid(changes.activeDid);
if (activeDidField !== undefined) {
await this.$updateActiveDid(activeDidField);
}
return true;
@@ -954,6 +1236,11 @@ export const PlatformServiceMixin = {
* @param changes Settings changes to save
* @returns Promise<boolean> Success status
*/
/**
* Since this is unused, and since it relies on this.activeDid which isn't guaranteed to exist,
* let's take this out for the sake of safety.
* Totally remove after start of 2026 (since it would be obvious by then that it's not used).
*
async $saveMySettings(changes: Partial<Settings>): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentDid = (this as any).activeDid;
@@ -963,6 +1250,7 @@ export const PlatformServiceMixin = {
}
return await this.$saveUserSettings(currentDid, changes);
},
**/
// =================================================
// CACHE MANAGEMENT METHODS
@@ -1213,8 +1501,15 @@ export const PlatformServiceMixin = {
*/
async $getAllAccountDids(): Promise<string[]> {
try {
const accounts = await this.$query<Account>("SELECT did FROM accounts");
return accounts.map((account) => account.did);
const result = await this.$dbQuery(
"SELECT did FROM accounts ORDER BY did",
);
if (!result?.values?.length) {
return [];
}
return result.values.map((row: SqlValue[]) => row[0] as string);
} catch (error) {
logger.error(
"[PlatformServiceMixin] Error getting all account DIDs:",
@@ -1339,13 +1634,16 @@ export const PlatformServiceMixin = {
fields: string[],
did?: string,
): Promise<unknown[] | undefined> {
// Use correct settings table schema
const whereClause = did ? "WHERE accountDid = ?" : "WHERE id = ?";
const params = did ? [did] : [MASTER_SETTINGS_KEY];
// Use current active DID if no specific DID provided
const targetDid = did || (await this.$getActiveIdentity()).activeDid;
if (!targetDid) {
return undefined;
}
return await this.$one(
`SELECT ${fields.join(", ")} FROM settings ${whereClause}`,
params,
`SELECT ${fields.join(", ")} FROM settings WHERE accountDid = ?`,
[targetDid],
);
},
@@ -1548,7 +1846,7 @@ export const PlatformServiceMixin = {
const settings = mappedResults[0] as Settings;
logger.info(`[PlatformServiceMixin] Settings for DID ${did}:`, {
logger.debug(`[PlatformServiceMixin] Settings for DID ${did}:`, {
firstName: settings.firstName,
isRegistered: settings.isRegistered,
activeDid: settings.activeDid,
@@ -1574,11 +1872,8 @@ export const PlatformServiceMixin = {
async $debugMergedSettings(did: string): Promise<void> {
try {
// Get default settings
const defaultSettings = await this.$getSettings(
MASTER_SETTINGS_KEY,
{},
);
logger.info(
const defaultSettings = await this._getMasterSettings({});
logger.debug(
`[PlatformServiceMixin] Default settings:`,
defaultSettings,
);
@@ -1588,12 +1883,11 @@ export const PlatformServiceMixin = {
// Get merged settings
const mergedSettings = await this.$getMergedSettings(
MASTER_SETTINGS_KEY,
did,
defaultSettings || {},
);
logger.info(`[PlatformServiceMixin] Merged settings for ${did}:`, {
logger.debug(`[PlatformServiceMixin] Merged settings for ${did}:`, {
defaultSettings,
didSettings,
mergedSettings,
@@ -1623,17 +1917,19 @@ export interface IPlatformServiceMixin {
params?: unknown[],
): Promise<QueryExecResult | undefined>;
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
$getSettings(
key: string,
fallback?: Settings | null,
): Promise<Settings | null>;
$dbGetOneRow(
sql: string,
params?: unknown[],
): Promise<SqlValue[] | undefined>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMergedSettings(
defaultKey: string,
accountDid?: string,
defaultFallback?: Settings,
): Promise<Settings>;
$getActiveIdentity(): Promise<{ activeDid: string }>;
$withTransaction<T>(callback: () => Promise<T>): Promise<T>;
$getAvailableAccountDids(): Promise<string[]>;
isCapacitor: boolean;
isWeb: boolean;
isElectron: boolean;
@@ -1727,7 +2023,7 @@ declare module "@vue/runtime-core" {
// Ultra-concise database methods (shortest possible names)
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$one(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
$one(sql: string, params?: unknown[]): Promise<SqlValue[] | undefined>;
// Query + mapping combo methods
$query<T = Record<string, unknown>>(
@@ -1749,16 +2045,15 @@ declare module "@vue/runtime-core" {
sql: string,
params?: unknown[],
): Promise<unknown[] | undefined>;
$getSettings(
key: string,
defaults?: Settings | null,
): Promise<Settings | null>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMergedSettings(
key: string,
did?: string,
defaults?: Settings,
): Promise<Settings>;
$getActiveIdentity(): Promise<{ activeDid: string }>;
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
$getAvailableAccountDids(): Promise<string[]>;
// Specialized shortcuts - contacts cached, settings fresh
$contacts(): Promise<Contact[]>;
@@ -1773,7 +2068,8 @@ declare module "@vue/runtime-core" {
did: string,
changes: Partial<Settings>,
): Promise<boolean>;
$saveMySettings(changes: Partial<Settings>): Promise<boolean>;
// @deprecated; see implementation note above
// $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
// Cache management methods
$refreshSettings(): Promise<Settings>;

298
src/utils/errorHandler.ts Normal file
View File

@@ -0,0 +1,298 @@
/**
* Standardized Error Handler
*
* Provides consistent error handling patterns across the TimeSafari codebase
* to improve debugging, user experience, and maintainability.
*
* @author Matthew Raymer
* @since 2025-08-25
*/
import { AxiosError } from "axios";
import { logger } from "./logger";
/**
* Standard error context for consistent logging
*/
export interface ErrorContext {
component: string;
operation: string;
timestamp: string;
[key: string]: unknown;
}
/**
* Enhanced error information for better debugging
*/
export interface EnhancedErrorInfo {
errorType: "AxiosError" | "NetworkError" | "ValidationError" | "UnknownError";
status?: number;
statusText?: string;
errorData?: unknown;
errorMessage: string;
errorStack?: string;
requestContext?: {
url?: string;
method?: string;
headers?: Record<string, unknown>;
};
}
/**
* Standardized error handler for API operations
*
* @param error - The error that occurred
* @param context - Context information about the operation
* @param operation - Description of the operation being performed
* @returns Enhanced error information for consistent handling
*/
export function handleApiError(
error: unknown,
context: ErrorContext,
operation: string,
): EnhancedErrorInfo {
const baseContext = {
...context,
operation,
timestamp: new Date().toISOString(),
};
if (error instanceof AxiosError) {
const axiosError = error as AxiosError;
const status = axiosError.response?.status;
const statusText = axiosError.response?.statusText;
const errorData = axiosError.response?.data;
const enhancedError: EnhancedErrorInfo = {
errorType: "AxiosError",
status,
statusText,
errorData,
errorMessage: axiosError.message,
errorStack: axiosError.stack,
requestContext: {
url: axiosError.config?.url,
method: axiosError.config?.method,
headers: axiosError.config?.headers,
},
};
// Log with consistent format
logger.error(
`[${context.component}] ❌ ${operation} failed (AxiosError):`,
{
...baseContext,
...enhancedError,
},
);
return enhancedError;
}
if (error instanceof Error) {
const enhancedError: EnhancedErrorInfo = {
errorType: "UnknownError",
errorMessage: error.message,
errorStack: error.stack,
};
logger.error(`[${context.component}] ❌ ${operation} failed (Error):`, {
...baseContext,
...enhancedError,
});
return enhancedError;
}
// Handle unknown error types
const enhancedError: EnhancedErrorInfo = {
errorType: "UnknownError",
errorMessage: String(error),
};
logger.error(`[${context.component}] ❌ ${operation} failed (Unknown):`, {
...baseContext,
...enhancedError,
});
return enhancedError;
}
/**
* Extract human-readable error message from various error response formats
*
* @param errorData - Error response data
* @returns Human-readable error message
*/
export function extractErrorMessage(errorData: unknown): string {
if (typeof errorData === "string") {
return errorData;
}
if (typeof errorData === "object" && errorData !== null) {
const obj = errorData as Record<string, unknown>;
// Try common error message fields
if (obj.message && typeof obj.message === "string") {
return obj.message;
}
if (obj.error && typeof obj.error === "string") {
return obj.error;
}
if (obj.detail && typeof obj.detail === "string") {
return obj.detail;
}
if (obj.reason && typeof obj.reason === "string") {
return obj.reason;
}
// Fallback to stringified object
return JSON.stringify(errorData);
}
return String(errorData);
}
/**
* Create user-friendly error message from enhanced error info
*
* @param errorInfo - Enhanced error information
* @param fallbackMessage - Fallback message if error details are insufficient
* @returns User-friendly error message
*/
export function createUserMessage(
errorInfo: EnhancedErrorInfo,
fallbackMessage: string,
): string {
if (errorInfo.errorType === "AxiosError") {
const status = errorInfo.status;
const statusText = errorInfo.statusText;
const errorMessage = extractErrorMessage(errorInfo.errorData);
if (status && statusText) {
if (errorMessage && errorMessage !== "{}") {
return `${fallbackMessage}: ${status} ${statusText} - ${errorMessage}`;
}
return `${fallbackMessage}: ${status} ${statusText}`;
}
}
if (
errorInfo.errorMessage &&
errorInfo.errorMessage !== "Request failed with status code 0"
) {
return `${fallbackMessage}: ${errorInfo.errorMessage}`;
}
return fallbackMessage;
}
/**
* Handle specific HTTP status codes with appropriate user messages
*
* @param status - HTTP status code
* @param errorData - Error response data
* @param operation - Description of the operation
* @returns User-friendly error message
*/
export function handleHttpStatus(
status: number,
errorData: unknown,
operation: string,
): string {
const errorMessage = extractErrorMessage(errorData);
switch (status) {
case 400:
return errorMessage || `${operation} failed: Bad request`;
case 401:
return `${operation} failed: Authentication required`;
case 403:
return `${operation} failed: Access denied`;
case 404:
return errorMessage || `${operation} failed: Resource not found`;
case 409:
return errorMessage || `${operation} failed: Conflict with existing data`;
case 422:
return errorMessage || `${operation} failed: Validation error`;
case 429:
return `${operation} failed: Too many requests. Please try again later.`;
case 500:
return `${operation} failed: Server error. Please try again later.`;
case 502:
case 503:
case 504:
return `${operation} failed: Service temporarily unavailable. Please try again later.`;
default:
return errorMessage || `${operation} failed: HTTP ${status}`;
}
}
/**
* Check if an error is a network-related error
*
* @param error - The error to check
* @returns True if the error is network-related
*/
export function isNetworkError(error: unknown): boolean {
if (error instanceof AxiosError) {
return !error.response && !error.request;
}
if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes("network") ||
message.includes("timeout") ||
message.includes("connection") ||
message.includes("fetch")
);
}
return false;
}
/**
* Check if an error is a timeout error
*
* @param error - The error to check
* @returns True if the error is a timeout
*/
export function isTimeoutError(error: unknown): boolean {
if (error instanceof AxiosError) {
return (
error.code === "ECONNABORTED" ||
error.message.toLowerCase().includes("timeout")
);
}
if (error instanceof Error) {
return error.message.toLowerCase().includes("timeout");
}
return false;
}
/**
* Create standardized error context for components
*
* @param component - Component name
* @param operation - Operation being performed
* @param additionalContext - Additional context information
* @returns Standardized error context
*/
export function createErrorContext(
component: string,
operation: string,
additionalContext: Record<string, unknown> = {},
): ErrorContext {
return {
component,
operation,
timestamp: new Date().toISOString(),
...additionalContext,
};
}

View File

@@ -24,10 +24,28 @@ export function getMemoryLogs(): string[] {
return [..._memoryLogs];
}
/**
* Stringify an object with proper handling of circular references and functions
*
* Don't use for arrays; map with this over the array.
*
* @param obj - The object to stringify
* @returns The stringified object, plus 'message' and 'stack' for Error objects
*/
export function safeStringify(obj: unknown) {
const seen = new WeakSet();
return JSON.stringify(obj, (_key, value) => {
// since 'message' & 'stack' are not enumerable for errors, let's add those
let objToStringify = obj;
if (obj instanceof Error) {
objToStringify = {
...obj,
message: obj.message,
stack: obj.stack,
};
}
return JSON.stringify(objToStringify, (_key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]";
@@ -59,10 +77,27 @@ type LogLevel = keyof typeof LOG_LEVELS;
// Parse VITE_LOG_LEVEL environment variable
const getLogLevel = (): LogLevel => {
const envLogLevel = process.env.VITE_LOG_LEVEL?.toLowerCase();
// Try to get VITE_LOG_LEVEL from different sources
let envLogLevel: string | undefined;
if (envLogLevel && envLogLevel in LOG_LEVELS) {
return envLogLevel as LogLevel;
try {
// In browser/Vite environment, use import.meta.env
if (
typeof import.meta !== "undefined" &&
import.meta?.env?.VITE_LOG_LEVEL
) {
envLogLevel = import.meta.env.VITE_LOG_LEVEL;
}
// Fallback to process.env for Node.js environments
else if (process.env.VITE_LOG_LEVEL) {
envLogLevel = process.env.VITE_LOG_LEVEL;
}
} catch (error) {
// Silently handle cases where import.meta is not available
}
if (envLogLevel && envLogLevel.toLowerCase() in LOG_LEVELS) {
return envLogLevel.toLowerCase() as LogLevel;
}
// Default log levels based on environment
@@ -161,7 +196,8 @@ export const logger = {
}
// Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "info");
},
@@ -172,7 +208,8 @@ export const logger = {
}
// Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "info");
},
@@ -183,7 +220,8 @@ export const logger = {
}
// Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "warn");
},
@@ -194,9 +232,9 @@ export const logger = {
}
// Database logging
const messageString = safeStringify(message);
const argsString = args.length > 0 ? safeStringify(args) : "";
logToDatabase(messageString + argsString, "error");
const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "error");
},
// New database-focused methods (self-contained)

View File

@@ -0,0 +1,482 @@
/**
* Performance Optimizer
*
* Provides utilities for optimizing API calls, database queries, and component
* rendering to improve TimeSafari application performance.
*
* @author Matthew Raymer
* @since 2025-08-25
*/
import { logger } from "./logger";
/**
* Batch operation configuration
*/
export interface BatchConfig {
maxBatchSize: number;
maxWaitTime: number;
retryAttempts: number;
retryDelay: number;
}
/**
* Default batch configuration
*/
export const DEFAULT_BATCH_CONFIG: BatchConfig = {
maxBatchSize: 10,
maxWaitTime: 100, // milliseconds
retryAttempts: 3,
retryDelay: 1000, // milliseconds
};
/**
* Batched operation item
*/
export interface BatchItem<T, R> {
id: string;
data: T;
resolve: (value: R) => void;
reject: (error: Error) => void;
timestamp: number;
}
/**
* Batch processor for API operations
*
* Groups multiple similar operations into batches to reduce
* the number of API calls and improve performance.
*/
export class BatchProcessor<T, R> {
private items: BatchItem<T, R>[] = [];
private timer: NodeJS.Timeout | null = null;
private processing = false;
private config: BatchConfig;
constructor(
private batchHandler: (items: T[]) => Promise<R[]>,
private itemIdExtractor: (item: T) => string,
config: Partial<BatchConfig> = {},
) {
this.config = { ...DEFAULT_BATCH_CONFIG, ...config };
}
/**
* Add an item to the batch
*
* @param data - Data to process
* @returns Promise that resolves when the item is processed
*/
async add(data: T): Promise<R> {
return new Promise((resolve, reject) => {
const item: BatchItem<T, R> = {
id: this.itemIdExtractor(data),
data,
resolve,
reject,
timestamp: Date.now(),
};
this.items.push(item);
// Start timer if this is the first item
if (this.items.length === 1) {
this.startTimer();
}
// Process immediately if batch is full
if (this.items.length >= this.config.maxBatchSize) {
this.processBatch();
}
});
}
/**
* Start the batch timer
*/
private startTimer(): void {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.processBatch();
}, this.config.maxWaitTime);
}
/**
* Process the current batch
*/
private async processBatch(): Promise<void> {
if (this.processing || this.items.length === 0) {
return;
}
this.processing = true;
// Clear timer
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
// Get current batch
const currentItems = [...this.items];
this.items = [];
try {
logger.debug("[BatchProcessor] 🔄 Processing batch:", {
batchSize: currentItems.length,
itemIds: currentItems.map((item) => item.id),
timestamp: new Date().toISOString(),
});
// Process batch
const results = await this.batchHandler(
currentItems.map((item) => item.data),
);
// Map results back to items
const resultMap = new Map<string, R>();
results.forEach((result, index) => {
const item = currentItems[index];
if (item) {
resultMap.set(item.id, result);
}
});
// Resolve promises
currentItems.forEach((item) => {
const result = resultMap.get(item.id);
if (result !== undefined) {
item.resolve(result);
} else {
item.reject(new Error(`No result found for item ${item.id}`));
}
});
logger.debug("[BatchProcessor] ✅ Batch processed successfully:", {
batchSize: currentItems.length,
resultsCount: results.length,
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error("[BatchProcessor] ❌ Batch processing failed:", {
batchSize: currentItems.length,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
// Reject all items in the batch
currentItems.forEach((item) => {
item.reject(error instanceof Error ? error : new Error(String(error)));
});
} finally {
this.processing = false;
// Start timer for remaining items if any
if (this.items.length > 0) {
this.startTimer();
}
}
}
/**
* Get current batch status
*/
getStatus(): {
pendingItems: number;
isProcessing: boolean;
hasTimer: boolean;
} {
return {
pendingItems: this.items.length,
isProcessing: this.processing,
hasTimer: this.timer !== null,
};
}
/**
* Clear all pending items
*/
clear(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
// Reject all pending items
this.items.forEach((item) => {
item.reject(new Error("Batch processor cleared"));
});
this.items = [];
this.processing = false;
}
}
/**
* Database query optimizer
*
* Provides utilities for optimizing database queries and reducing
* the number of database operations.
*/
export class DatabaseOptimizer {
/**
* Batch multiple SELECT queries into a single query
*
* @param baseQuery - Base SELECT query
* @param ids - Array of IDs to query
* @param idColumn - Name of the ID column
* @returns Optimized query string
*/
static batchSelectQuery(
baseQuery: string,
ids: (string | number)[],
idColumn: string,
): string {
if (ids.length === 0) {
return baseQuery;
}
if (ids.length === 1) {
return `${baseQuery} WHERE ${idColumn} = ?`;
}
const placeholders = ids.map(() => "?").join(", ");
return `${baseQuery} WHERE ${idColumn} IN (${placeholders})`;
}
/**
* Create a query plan for multiple operations
*
* @param operations - Array of database operations
* @returns Optimized query plan
*/
static createQueryPlan(
operations: Array<{
type: "SELECT" | "INSERT" | "UPDATE" | "DELETE";
table: string;
priority: number;
}>,
): Array<{
type: "SELECT" | "INSERT" | "UPDATE" | "DELETE";
table: string;
priority: number;
batchable: boolean;
}> {
return operations
.map((op) => ({
...op,
batchable: op.type === "SELECT" || op.type === "INSERT",
}))
.sort((a, b) => {
// Sort by priority first, then by type
if (a.priority !== b.priority) {
return b.priority - a.priority;
}
// SELECT operations first, then INSERT, UPDATE, DELETE
const typeOrder = { SELECT: 0, INSERT: 1, UPDATE: 2, DELETE: 3 };
return typeOrder[a.type] - typeOrder[b.type];
});
}
}
/**
* Component rendering optimizer
*
* Provides utilities for optimizing Vue component rendering
* and reducing unnecessary re-renders.
*/
export class ComponentOptimizer {
/**
* Debounce function calls to prevent excessive execution
*
* @param func - Function to debounce
* @param wait - Wait time in milliseconds
* @returns Debounced function
*/
static debounce<T extends (...args: unknown[]) => unknown>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func(...args);
}, wait);
};
}
/**
* Throttle function calls to limit execution frequency
*
* @param func - Function to throttle
* @param limit - Time limit in milliseconds
* @returns Throttled function
*/
static throttle<T extends (...args: unknown[]) => unknown>(
func: T,
limit: number,
): (...args: Parameters<T>) => void {
let inThrottle = false;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
/**
* Memoize function results to avoid redundant computation
*
* @param func - Function to memoize
* @param keyGenerator - Function to generate cache keys
* @returns Memoized function
*/
static memoize<T extends (...args: unknown[]) => unknown, K>(
func: T,
keyGenerator: (...args: Parameters<T>) => K,
): T {
const cache = new Map<K, unknown>();
return ((...args: Parameters<T>) => {
const key = keyGenerator(...args);
if (cache.has(key)) {
return cache.get(key);
}
const result = func(...args);
cache.set(key, result);
return result;
}) as T;
}
}
/**
* Performance monitoring utility
*
* Tracks and reports performance metrics for optimization analysis.
*/
export class PerformanceMonitor {
private static instance: PerformanceMonitor;
private metrics = new Map<
string,
Array<{ timestamp: number; duration: number }>
>();
private constructor() {}
/**
* Get singleton instance
*/
static getInstance(): PerformanceMonitor {
if (!PerformanceMonitor.instance) {
PerformanceMonitor.instance = new PerformanceMonitor();
}
return PerformanceMonitor.instance;
}
/**
* Start timing an operation
*
* @param operationName - Name of the operation
* @returns Function to call when operation completes
*/
startTiming(operationName: string): () => void {
const startTime = performance.now();
return () => {
const duration = performance.now() - startTime;
this.recordMetric(operationName, duration);
};
}
/**
* Record a performance metric
*
* @param operationName - Name of the operation
* @param duration - Duration in milliseconds
*/
private recordMetric(operationName: string, duration: number): void {
if (!this.metrics.has(operationName)) {
this.metrics.set(operationName, []);
}
const operationMetrics = this.metrics.get(operationName)!;
operationMetrics.push({
timestamp: Date.now(),
duration,
});
// Keep only last 100 metrics per operation
if (operationMetrics.length > 100) {
operationMetrics.splice(0, operationMetrics.length - 100);
}
}
/**
* Get performance summary for an operation
*
* @param operationName - Name of the operation
* @returns Performance statistics
*/
getPerformanceSummary(operationName: string): {
count: number;
average: number;
min: number;
max: number;
recentAverage: number;
} | null {
const metrics = this.metrics.get(operationName);
if (!metrics || metrics.length === 0) {
return null;
}
const durations = metrics.map((m) => m.duration);
const recentMetrics = metrics.slice(-10); // Last 10 metrics
return {
count: metrics.length,
average: durations.reduce((a, b) => a + b, 0) / durations.length,
min: Math.min(...durations),
max: Math.max(...durations),
recentAverage:
recentMetrics.reduce((a, b) => a + b.duration, 0) /
recentMetrics.length,
};
}
/**
* Get all performance metrics
*/
getAllMetrics(): Map<string, Array<{ timestamp: number; duration: number }>> {
return new Map(this.metrics);
}
/**
* Clear all performance metrics
*/
clearMetrics(): void {
this.metrics.clear();
}
}
/**
* Convenience function to get the performance monitor
*/
export const getPerformanceMonitor = (): PerformanceMonitor => {
return PerformanceMonitor.getInstance();
};

View File

@@ -0,0 +1,90 @@
import { NotificationIface } from "@/constants/app";
const SEED_REMINDER_KEY = "seedPhraseReminderLastShown";
const REMINDER_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
/**
* Checks if the seed phrase backup reminder should be shown
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
* @returns true if the reminder should be shown, false otherwise
*/
export function shouldShowSeedReminder(hasBackedUpSeed: boolean): boolean {
// Don't show if user has already backed up
if (hasBackedUpSeed) {
return false;
}
// Check localStorage for last shown time
const lastShown = localStorage.getItem(SEED_REMINDER_KEY);
if (!lastShown) {
return true; // First time, show the reminder
}
try {
const lastShownTime = parseInt(lastShown, 10);
const now = Date.now();
const timeSinceLastShown = now - lastShownTime;
// Show if more than 24 hours have passed
return timeSinceLastShown >= REMINDER_COOLDOWN_MS;
} catch (error) {
// If there's an error parsing the timestamp, show the reminder
return true;
}
}
/**
* Marks the seed phrase reminder as shown by updating localStorage
*/
export function markSeedReminderShown(): void {
localStorage.setItem(SEED_REMINDER_KEY, Date.now().toString());
}
/**
* Creates the seed phrase backup reminder notification
* @returns NotificationIface configuration for the reminder modal
*/
export function createSeedReminderNotification(): NotificationIface {
return {
group: "modal",
type: "confirm",
title: "Backup Your Identifier Seed?",
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
yesText: "Backup Identifier Seed",
noText: "Remind me Later",
onYes: async () => {
// Navigate to seed backup page
window.location.href = "/seed-backup";
},
onNo: async () => {
// Mark as shown so it won't appear again for 24 hours
markSeedReminderShown();
},
onCancel: async () => {
// Mark as shown so it won't appear again for 24 hours
markSeedReminderShown();
},
};
}
/**
* Shows the seed phrase backup reminder if conditions are met
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
* @param notifyFunction - Function to show notifications
* @returns true if the reminder was shown, false otherwise
*/
export function showSeedPhraseReminder(
hasBackedUpSeed: boolean,
notifyFunction: (notification: NotificationIface, timeout?: number) => void,
): boolean {
if (shouldShowSeedReminder(hasBackedUpSeed)) {
const notification = createSeedReminderNotification();
// Add 1-second delay before showing the modal to allow success message to be visible
setTimeout(() => {
// Pass -1 as timeout to ensure modal stays open until user interaction
notifyFunction(notification, -1);
}, 1000);
return true;
}
return false;
}

View File

@@ -27,7 +27,7 @@
need an identifier.
</p>
<router-link
:to="{ name: 'start' }"
:to="{ name: 'new-identifier' }"
class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
>
Create An Identifier
@@ -58,8 +58,10 @@
v-if="!isRegistered"
:passkeys-enabled="PASSKEYS_ENABLED"
:given-name="givenName"
message="Before you can publicly announce a new project or time commitment,
a friend needs to register you."
:message="
`Before you can publicly announce a new project or time commitment, ` +
`a friend needs to register you.`
"
/>
<!-- Notifications -->
@@ -148,8 +150,6 @@
</section>
<PushNotificationPermission ref="pushNotificationPermission" />
<LocationSearchSection :search-box="searchBox" />
<!-- User Profile -->
<section
v-if="isRegistered"
@@ -242,6 +242,8 @@
<div v-else>Saving...</div>
</section>
<LocationSearchSection :search-box="searchBox" />
<UsageLimitsSection
v-if="activeDid"
:loading-limits="loadingLimits"
@@ -752,6 +754,7 @@ import "leaflet/dist/leaflet.css";
import { Buffer } from "buffer/";
import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
@@ -761,7 +764,7 @@ import { IIdentifier } from "@veramo/core";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { Capacitor } from "@capacitor/core";
@@ -808,16 +811,19 @@ import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
AccountSettings,
isApiError,
ImportContent,
} from "@/interfaces/accountView";
import {
ProfileService,
createProfileService,
ProfileData,
} from "@/services/ProfileService";
// Profile data interface (inlined from ProfileService)
interface ProfileData {
description: string;
latitude: number;
longitude: number;
includeLocation: boolean;
}
const inputImportFileNameRef = ref<Blob>();
@@ -916,7 +922,6 @@ export default class AccountViewView extends Vue {
imageLimits: ImageRateLimits | null = null;
limitsMessage: string = "";
private profileService!: ProfileService;
private notify!: ReturnType<typeof createNotifyHelpers>;
created() {
@@ -926,7 +931,10 @@ export default class AccountViewView extends Vue {
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
if (L.Icon.Default) {
// Type-safe way to handle Leaflet icon prototype
const iconDefault = L.Icon.Default.prototype as Record<string, unknown>;
const iconDefault = L.Icon.Default.prototype as unknown as Record<
string,
unknown
>;
if ("_getIconUrl" in iconDefault) {
delete iconDefault._getIconUrl;
}
@@ -948,17 +956,21 @@ export default class AccountViewView extends Vue {
* @throws Will display specific messages to the user based on different errors.
*/
async mounted(): Promise<void> {
this.profileService = createProfileService(
this.axios,
this.partnerApiServer,
);
try {
await this.initializeState();
await this.processIdentity();
// Profile service logic now inlined - no need for external service
logger.debug(
"[AccountViewView] Profile logic ready with partnerApiServer:",
{
partnerApiServer: this.partnerApiServer,
},
);
if (this.isRegistered) {
try {
const profile = await this.profileService.loadProfile(this.activeDid);
const profile = await this.loadProfile(this.activeDid);
if (profile) {
this.userProfileDesc = profile.description;
this.userProfileLatitude = profile.latitude;
@@ -1039,7 +1051,11 @@ export default class AccountViewView extends Vue {
// Then get the account-specific settings
const settings: AccountSettings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.apiServerInput = settings.apiServer || "";
this.givenName =
@@ -1048,8 +1064,8 @@ export default class AccountViewView extends Vue {
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings?.isRegistered;
this.isSearchAreasSet = !!settings.searchBoxes;
this.searchBox = settings.searchBoxes?.[0] || null;
this.isSearchAreasSet =
!!settings.searchBoxes && settings.searchBoxes.length > 0;
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime;
@@ -1063,6 +1079,7 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationMinutes =
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.searchBox = settings.searchBoxes?.[0] || null;
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.showShortcutBvc = !!settings.showShortcutBvc;
this.warnIfProdServer = !!settings.warnIfProdServer;
@@ -1072,11 +1089,15 @@ export default class AccountViewView extends Vue {
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void): void {
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
try {
await copyToClipboard(text);
setTimeout(fn, 2000);
} catch (error) {
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
this.notify.error("Failed to copy to clipboard.");
}
}
async toggleShowContactAmounts(): Promise<void> {
@@ -1411,28 +1432,27 @@ export default class AccountViewView extends Vue {
async checkLimits(): Promise<void> {
this.loadingLimits = true;
const did = this.activeDid;
if (!did) {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
return;
}
try {
const did = this.activeDid;
if (!did) {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
return;
}
await this.$saveUserSettings(did, {
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
webPushServer: this.webPushServer,
});
const imageResp = await fetchImageRateLimits(this.axios, did);
const imageResp = await fetchImageRateLimits(
this.axios,
did,
this.DEFAULT_IMAGE_API_SERVER,
);
if (imageResp.status === 200) {
if (imageResp && imageResp.status === 200) {
this.imageLimits = imageResp.data;
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES);
return;
}
const endorserResp = await fetchEndorserRateLimits(
@@ -1443,15 +1463,31 @@ export default class AccountViewView extends Vue {
if (endorserResp.status === 200) {
this.endorserLimits = endorserResp.data;
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE);
return;
}
} catch (error) {
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
logger.error("Error retrieving limits: ", error);
// Enhanced error logging with server context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
logger.error("[Server Limits] Error retrieving limits:", {
error: error instanceof Error ? error.message : String(error),
did: did,
apiServer: this.apiServer,
imageServer: this.DEFAULT_IMAGE_API_SERVER,
partnerApiServer: this.partnerApiServer,
errorCode: axiosError?.response?.data?.error?.code,
errorMessage: axiosError?.response?.data?.error?.message,
httpStatus: axiosError?.response?.status,
needsUserMigration: true,
timestamp: new Date().toISOString(),
});
// this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD);
} finally {
this.loadingLimits = false;
@@ -1459,24 +1495,70 @@ export default class AccountViewView extends Vue {
}
async onClickSaveApiServer(): Promise<void> {
await this.$saveSettings({
apiServer: this.apiServerInput,
// Enhanced diagnostic logging for claim URL changes
const previousApiServer = this.apiServer;
const newApiServer = this.apiServerInput;
logger.debug("[Server Switching] Claim URL change initiated:", {
did: this.activeDid,
previousServer: previousApiServer,
newServer: newApiServer,
changeType: "apiServer",
timestamp: new Date().toISOString(),
});
this.apiServer = this.apiServerInput;
await this.$saveSettings({
apiServer: newApiServer,
});
this.apiServer = newApiServer;
// Add this line to save to user-specific settings
await this.$saveUserSettings(this.activeDid, {
apiServer: this.apiServer,
});
// Log successful server switch
logger.debug("[Server Switching] Claim URL change completed:", {
did: this.activeDid,
previousServer: previousApiServer,
newServer: newApiServer,
changeType: "apiServer",
settingsSaved: true,
timestamp: new Date().toISOString(),
});
}
async onClickSavePartnerServer(): Promise<void> {
await this.$saveSettings({
partnerApiServer: this.partnerApiServerInput,
// Enhanced diagnostic logging for partner server changes
const previousPartnerServer = this.partnerApiServer;
const newPartnerServer = this.partnerApiServerInput;
logger.debug("[Server Switching] Partner server change initiated:", {
did: this.activeDid,
previousServer: previousPartnerServer,
newServer: newPartnerServer,
changeType: "partnerApiServer",
timestamp: new Date().toISOString(),
});
this.partnerApiServer = this.partnerApiServerInput;
await this.$saveSettings({
partnerApiServer: newPartnerServer,
});
this.partnerApiServer = newPartnerServer;
await this.$saveUserSettings(this.activeDid, {
partnerApiServer: this.partnerApiServer,
});
// Log successful partner server switch
logger.debug("[Server Switching] Partner server change completed:", {
did: this.activeDid,
previousServer: previousPartnerServer,
newServer: newPartnerServer,
changeType: "partnerApiServer",
settingsSaved: true,
timestamp: new Date().toISOString(),
});
}
async onClickSavePushServer(): Promise<void> {
@@ -1550,7 +1632,6 @@ export default class AccountViewView extends Vue {
onMapReady(map: L.Map): void {
try {
logger.debug("Map ready event fired, map object:", map);
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
const zoom =
this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
@@ -1579,19 +1660,15 @@ export default class AccountViewView extends Vue {
// Try to set map ready after component is mounted
setTimeout(() => {
this.isMapReady = true;
logger.debug("Map ready set to true after mounted");
}, 500);
}
// Fallback method to handle map initialization failures
private handleMapInitFailure(): void {
logger.debug("Starting map initialization timeout (5 seconds)");
setTimeout(() => {
if (!this.isMapReady) {
logger.warn("Map failed to initialize, forcing ready state");
this.isMapReady = true;
} else {
logger.debug("Map initialized successfully, timeout not needed");
}
}, 5000); // 5 second timeout
}
@@ -1615,12 +1692,20 @@ export default class AccountViewView extends Vue {
logger.debug("Saving profile data:", profileData);
const success = await this.profileService.saveProfile(
const success = await this.saveProfileToServer(
this.activeDid,
profileData,
);
if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
} else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
}
@@ -1634,7 +1719,7 @@ export default class AccountViewView extends Vue {
toggleUserProfileLocation(): void {
try {
const updated = this.profileService.toggleProfileLocation({
const updated = this.toggleProfileLocation({
description: this.userProfileDesc,
latitude: this.userProfileLatitude,
longitude: this.userProfileLongitude,
@@ -1679,8 +1764,7 @@ export default class AccountViewView extends Vue {
async deleteProfile(): Promise<void> {
try {
logger.debug("Attempting to delete profile for DID:", this.activeDid);
const success = await this.profileService.deleteProfile(this.activeDid);
const success = await this.deleteProfileFromServer(this.activeDid);
if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
this.userProfileDesc = "";
@@ -1688,7 +1772,6 @@ export default class AccountViewView extends Vue {
this.userProfileLongitude = 0;
this.includeUserProfileLocation = false;
this.isMapReady = false; // Reset map state
logger.debug("Profile deleted successfully, UI state reset");
} else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
}
@@ -1734,7 +1817,6 @@ export default class AccountViewView extends Vue {
this.isMapReady = false;
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
logger.debug("Location unchecked, map state reset");
} else {
// Location checkbox was checked, start map initialization timeout
this.isMapReady = false;
@@ -1743,7 +1825,6 @@ export default class AccountViewView extends Vue {
// Try to set map ready after a short delay to allow Vue to render
setTimeout(() => {
if (!this.isMapReady) {
logger.debug("Setting map ready after timeout");
this.isMapReady = true;
}
}, 1000); // 1 second delay
@@ -1796,5 +1877,338 @@ export default class AccountViewView extends Vue {
onRecheckLimits() {
this.checkLimits();
}
// Inlined profile methods (previously in ProfileService)
/**
* Load user profile from the partner API
*/
private async loadProfile(did: string): Promise<ProfileData | null> {
try {
const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Loading profile:", {
requestId,
did,
partnerApiServer: this.partnerApiServer,
});
// Get authentication headers
const headers = await getHeaders(did);
const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`;
logger.debug("[AccountViewView] Making API request:", {
requestId,
did,
fullUrl,
hasAuthHeader: !!headers.Authorization,
});
const response = await this.axios.get(fullUrl, { headers });
logger.debug("[AccountViewView] Profile loaded successfully:", {
requestId,
status: response.status,
hasData: !!response.data,
});
if (response.data && response.data.data) {
const profileData = response.data.data;
logger.debug("[AccountViewView] Parsing profile data:", {
requestId,
locLat: profileData.locLat,
locLon: profileData.locLon,
description: profileData.description,
});
const result = {
description: profileData.description || "",
latitude: profileData.locLat || 0,
longitude: profileData.locLon || 0,
includeLocation: !!(profileData.locLat && profileData.locLon),
};
logger.debug("[AccountViewView] Parsed profile result:", {
requestId,
result,
hasLocation: result.includeLocation,
});
return result;
} else {
logger.debug("[AccountViewView] No profile data found in response:", {
requestId,
hasData: !!response.data,
hasDataData: !!(response.data && response.data.data),
});
}
return null;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
logger.info(
"[Profile] No profile found - this is normal for new users",
{
did,
server: this.partnerApiServer,
status: 404,
timestamp: new Date().toISOString(),
},
);
return null;
}
if (axiosError.response?.status === 400) {
logger.warn("[Profile] Bad request - user may not be registered", {
did,
server: this.partnerApiServer,
status: 400,
timestamp: new Date().toISOString(),
});
return null;
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn("[Profile] Authentication/authorization issue", {
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
});
return null;
}
}
// Only log full errors for unexpected issues (5xx, network errors, etc.)
logger.error("[Profile] Unexpected error loading profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
return null;
}
}
/**
* Save user profile to the partner API
*/
private async saveProfileToServer(
did: string,
profileData: ProfileData,
): Promise<boolean> {
try {
const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Saving profile:", {
requestId,
did,
profileData,
});
// Get authentication headers
const headers = await getHeaders(did);
// Prepare payload in the format expected by the partner API
const payload = {
description: profileData.description,
issuerDid: did,
...(profileData.includeLocation &&
profileData.latitude &&
profileData.longitude
? {
locLat: profileData.latitude,
locLon: profileData.longitude,
}
: {}),
};
logger.debug("[AccountViewView] Sending payload to server:", {
requestId,
payload,
hasLocation: profileData.includeLocation,
});
const response = await this.axios.post(
`${this.partnerApiServer}/api/partner/userProfile`,
payload,
{ headers },
);
logger.debug("[AccountViewView] Profile saved successfully:", {
requestId,
status: response.status,
});
return true;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 400) {
logger.warn("[Profile] Bad request saving profile", {
did,
server: this.partnerApiServer,
status: 400,
timestamp: new Date().toISOString(),
});
throw new Error("Invalid profile data");
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn(
"[Profile] Authentication/authorization issue saving profile",
{
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
},
);
throw new Error("Authentication required");
}
if (axiosError.response?.status === 409) {
logger.warn("[Profile] Profile conflict - may already exist", {
did,
server: this.partnerApiServer,
status: 409,
timestamp: new Date().toISOString(),
});
throw new Error("Profile already exists");
}
}
// Only log full errors for unexpected issues
logger.error("[Profile] Unexpected error saving profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
throw new Error("Failed to save profile");
}
}
/**
* Toggle profile location visibility
*/
private toggleProfileLocation(profileData: ProfileData): ProfileData {
const includeLocation = !profileData.includeLocation;
return {
...profileData,
latitude: includeLocation ? profileData.latitude : 0,
longitude: includeLocation ? profileData.longitude : 0,
includeLocation,
};
}
/**
* Clear profile location
*/
private clearProfileLocation(profileData: ProfileData): ProfileData {
return {
...profileData,
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Get default profile data
*/
private getDefaultProfile(): ProfileData {
return {
description: "",
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Delete user profile from the partner API
*/
private async deleteProfileFromServer(did: string): Promise<boolean> {
try {
const requestId = `profile_delete_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Deleting profile:", {
requestId,
did,
});
// Get authentication headers
const headers = await getHeaders(did);
const response = await this.axios.delete(
`${this.partnerApiServer}/api/partner/userProfile/${did}`,
{ headers },
);
logger.debug("[AccountViewView] Profile deleted successfully:", {
requestId,
status: response.status,
});
return true;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
logger.info(
"[Profile] Profile not found for deletion - may already be deleted",
{
did,
server: this.partnerApiServer,
status: 404,
timestamp: new Date().toISOString(),
},
);
return true; // Consider it successful if already deleted
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn(
"[Profile] Authentication/authorization issue deleting profile",
{
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
},
);
return false;
}
}
// Only log full errors for unexpected issues
logger.error("[Profile] Unexpected error deleting profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
return false;
}
}
}
</script>

View File

@@ -41,6 +41,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
// Type guard for API responses
function isApiResponse(response: unknown): response is AxiosResponse {
@@ -112,7 +113,12 @@ export default class ClaimAddRawView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
}
@@ -223,6 +229,14 @@ export default class ClaimAddRawView extends Vue {
);
if (result.success) {
this.notify.success("Claim submitted.", TIMEOUTS.LONG);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
} else {
logger.error("Got error submitting the claim:", result);
this.notify.error(

View File

@@ -40,7 +40,12 @@ export default class ClaimCertificateView extends Vue {
async created() {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,

View File

@@ -53,8 +53,13 @@ export default class ClaimReportCertificateView extends Vue {
// Initialize notification helper
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$settings();
this.activeDid = settings.activeDid || "";
const settings = await this.$accountSettings();
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,

View File

@@ -24,7 +24,9 @@
<div class="flex columns-3">
<h2 class="text-md font-bold w-full">
{{
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
serverUtil.capitalizeAndInsertSpacesBeforeCaps(
veriClaim.claimType || "",
)
}}
<button
v-if="canEditClaim"
@@ -56,7 +58,7 @@
title="Copy Printable Certificate Link"
aria-label="Copy printable certificate link"
@click="
copyToClipboard(
copyTextToClipboard(
'A link to the certificate page',
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
)
@@ -70,7 +72,9 @@
<button
title="Copy Link"
aria-label="Copy page link"
@click="copyToClipboard('A link to this page', windowDeepLink)"
@click="
copyTextToClipboard('A link to this page', windowDeepLink)
"
>
<font-awesome icon="link" class="text-slate-500" />
</button>
@@ -79,7 +83,10 @@
<div class="text-sm">
<div data-testId="description">
<font-awesome icon="message" class="fa-fw text-slate-400" />
{{ claimDescription }}
<vue-markdown
:source="claimDescription"
class="markdown-content"
/>
</div>
<div>
<font-awesome icon="user" class="fa-fw text-slate-400" />
@@ -106,77 +113,91 @@
</div>
<!-- Fullfills Links -->
<!-- fullfills links for a give -->
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4">
<router-link
:to="
'/project/' +
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
"
class="text-blue-500 mt-2"
>
Fulfills a bigger plan...
</router-link>
</div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div
v-if="
detailsForGive?.fulfillsType &&
detailsForGive?.fulfillsType !== 'PlanAction' &&
detailsForGive?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path -->
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
"
>
Fulfills
{{
capitalizeAndInsertSpacesBeforeCaps(
detailsForGive.fulfillsType,
)
}}...
</a>
</div>
<!-- fullfills links for an offer -->
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
"
class="text-blue-500 mt-4"
>
Offered to a bigger plan...
</router-link>
</div>
<!-- Providers -->
<div v-if="providersForGive?.length > 0" class="mt-4">
<span>Other assistance provided by:</span>
<ul class="ml-4">
<li
v-for="provider of providersForGive"
:key="provider.identifier"
class="list-disc ml-4"
<div class="mt-4 empty:hidden">
<!-- fullfills links for a give -->
<div v-if="detailsForGive?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
"
class="text-blue-500 mt-2"
>
<div class="flex gap-4">
<div class="grow overflow-hidden">
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="handleProviderClick(provider)"
>
an activity...
</a>
This fulfills a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</div>
<!-- Show offer fulfillment if this give fulfills an offer -->
<div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
<!-- router-link to /claim/ only changes URL path -->
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="
showDifferentClaimPage(
detailsForGiveOfferFulfillment.offerHandleId,
)
"
>
This fulfills
{{
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
detailsForGiveOfferFulfillment.offerType || "Offer",
)
}}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</div>
<!-- fullfills links for an offer -->
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
"
class="text-blue-500 mt-4"
>
Offered to a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</div>
<!-- Providers -->
<div v-if="providersForGive?.length > 0">
<span>Other assistance provided by:</span>
<ul class="ml-4">
<li
v-for="provider of providersForGive"
:key="provider.identifier"
class="list-disc ml-4"
>
<div class="flex gap-4">
<div class="grow overflow-hidden">
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="handleProviderClick(provider)"
>
an activity
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</div>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -383,7 +404,7 @@
contacts can see more details:
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowDeepLink)"
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -406,7 +427,7 @@
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowDeepLink)"
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -515,8 +536,10 @@ import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import VueMarkdown from "vue-markdown-render";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { GenericVerifiableCredential } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
@@ -535,7 +558,7 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { APP_SERVER } from "@/constants/app";
@Component({
components: { GiftedDialog, QuickNav },
components: { GiftedDialog, QuickNav, VueMarkdown },
mixins: [PlatformServiceMixin],
})
export default class ClaimView extends Vue {
@@ -556,6 +579,17 @@ export default class ClaimView extends Vue {
fulfillsPlanHandleId?: string;
fulfillsType?: string;
fulfillsHandleId?: string;
fullClaim?: {
fulfills?: Array<{
"@type": string;
identifier?: string;
}>;
};
} | null = null;
// Additional offer information extracted from the fulfills array
detailsForGiveOfferFulfillment: {
offerHandleId?: string;
offerType?: string;
} | null = null;
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
// Project information for fulfillsPlanHandleId
@@ -689,6 +723,7 @@ export default class ClaimView extends Vue {
this.confsVisibleToIdList = [];
this.detailsForGive = null;
this.detailsForOffer = null;
this.detailsForGiveOfferFulfillment = null;
this.projectInfo = null;
this.fullClaim = null;
this.fullClaimDump = "";
@@ -701,6 +736,15 @@ export default class ClaimView extends Vue {
this.veriClaimDidsVisible = {};
}
/**
* Extract offer fulfillment information from the fulfills array
*/
extractOfferFulfillment() {
this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment(
this.detailsForGive?.fullClaim?.fulfills,
);
}
// =================================================
// UTILITY METHODS
// =================================================
@@ -728,7 +772,11 @@ export default class ClaimView extends Vue {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await this.$contacts();
@@ -758,13 +806,6 @@ export default class ClaimView extends Vue {
this.canShare = !!navigator.share;
}
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
if (!text) return "";
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
totalConfirmers() {
return (
this.numConfsNotVisible +
@@ -821,6 +862,8 @@ export default class ClaimView extends Vue {
});
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
this.detailsForGive = giveResp.data.data[0];
// Extract offer information from the fulfills array
this.extractOfferFulfillment();
} else {
await this.$logError(
"Error getting detailed give info: " + JSON.stringify(giveResp),
@@ -1097,16 +1140,21 @@ export default class ClaimView extends Vue {
);
}
copyToClipboard(name: string, text: string) {
useClipboard()
.copy(text)
.then(() => {
this.notify.copied(name || "That");
});
async copyTextToClipboard(name: string, text: string) {
try {
await copyToClipboard(text);
this.notify.copied(name || "That");
} catch (error) {
this.$logAndConsole(
`Error copying ${name || "content"} to clipboard: ${error}`,
true,
);
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
}
}
onClickShareClaim() {
this.copyToClipboard("A link to this page", this.windowDeepLink);
this.copyTextToClipboard("A link to this page", this.windowDeepLink);
window.navigator.share({
title: "Help Connect Me",
text: "I'm trying to find the people who recorded this. Can you help me?",

View File

@@ -96,50 +96,50 @@
</div>
<!-- Fullfills Links -->
<div class="mt-4">
<!-- fullfills links for a give -->
<div v-if="giveDetails?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
encodeURIComponent(
giveDetails?.fulfillsPlanHandleId || '',
)
"
class="text-blue-500 mt-2 cursor-pointer"
>
This fulfills a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</div>
<!-- fullfills links for a give -->
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
<router-link
:to="
'/project/' +
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '')
"
class="text-blue-500 mt-2 cursor-pointer"
>
This fulfills a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div
v-if="
giveDetails?.fulfillsType &&
giveDetails?.fulfillsType !== 'PlanAction' &&
giveDetails?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path -->
<router-link
:to="
'/claim/' +
encodeURIComponent(giveDetails?.fulfillsHandleId || '')
"
class="text-blue-500 mt-2 cursor-pointer"
>
This fulfills
{{
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
giveDetails?.fulfillsType || "",
)
}}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
<!-- Show offer fulfillment if this give fulfills an offer -->
<div v-if="giveDetailsOfferFulfillment?.offerHandleId">
<!-- router-link to /claim/ only changes URL path -->
<router-link
:to="
'/claim/' +
encodeURIComponent(
giveDetailsOfferFulfillment.offerHandleId || '',
)
"
class="text-blue-500 mt-2 cursor-pointer"
>
This fulfills
{{
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
giveDetailsOfferFulfillment.offerType || "Offer",
)
}}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</div>
</div>
</div>
</div>
@@ -192,7 +192,7 @@
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
<button
@click="
copyToClipboard(
copyTextToClipboard(
'The DID of ' + confirmerId,
confirmerId,
)
@@ -238,7 +238,7 @@
>
<button
@click="
copyToClipboard(
copyTextToClipboard(
'The DID of ' + confsVisibleTo,
confsVisibleTo,
)
@@ -309,7 +309,9 @@
contacts can see more details:
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
@click="
copyTextToClipboard('A link to this page', windowLocation)
"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -332,7 +334,9 @@
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
@click="
copyTextToClipboard('A link to this page', windowLocation)
"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -360,7 +364,7 @@
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<button
@click="
copyToClipboard('The DID of ' + visDid, visDid)
copyTextToClipboard('The DID of ' + visDid, visDid)
"
>
<font-awesome
@@ -433,7 +437,7 @@
import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -493,6 +497,11 @@ export default class ConfirmGiftView extends Vue {
confsVisibleErrorMessage = "";
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
giveDetails?: GiveSummaryRecord;
// Additional offer information extracted from the fulfills array
giveDetailsOfferFulfillment: {
offerHandleId?: string;
offerType?: string;
} | null = null;
giverName = "";
issuerName = "";
isLoading = false;
@@ -547,7 +556,12 @@ export default class ConfirmGiftView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await this.$getAllContacts();
this.isRegistered = settings.isRegistered || false;
@@ -648,6 +662,8 @@ export default class ConfirmGiftView extends Vue {
if (resp.status === 200) {
this.giveDetails = resp.data.data[0];
// Extract offer information from the fulfills array
this.extractOfferFulfillment();
} else {
throw new Error("Error getting detailed give info: " + resp.status);
}
@@ -707,6 +723,15 @@ export default class ConfirmGiftView extends Vue {
}
}
/**
* Extract offer fulfillment information from the fulfills array
*/
private extractOfferFulfillment() {
this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment(
this.giveDetails?.fullClaim?.fulfills,
);
}
/**
* Fetches confirmer information for the claim
*/
@@ -763,16 +788,21 @@ export default class ConfirmGiftView extends Vue {
* @param description - Description of copied content
* @param text - Text to copy
*/
copyToClipboard(description: string, text: string): void {
useClipboard()
.copy(text)
.then(() => {
this.notify.toast(
NOTIFY_COPIED_TO_CLIPBOARD.title,
NOTIFY_COPIED_TO_CLIPBOARD.message(description),
TIMEOUTS.SHORT,
);
});
async copyTextToClipboard(description: string, text: string): Promise<void> {
try {
await copyToClipboard(text);
this.notify.toast(
NOTIFY_COPIED_TO_CLIPBOARD.title,
NOTIFY_COPIED_TO_CLIPBOARD.message(description),
TIMEOUTS.SHORT,
);
} catch (error) {
this.$logAndConsole(
`Error copying ${description} to clipboard: ${error}`,
true,
);
this.notify.error(`Failed to copy ${description} to clipboard.`);
}
}
/**
@@ -849,33 +879,12 @@ export default class ConfirmGiftView extends Vue {
);
}
/**
* Formats type string for display by adding spaces before capitals
* Optionally adds a prefix
*
* @param text - Text to format
* @param prefix - Optional prefix to add
* @returns Formatted string
*/
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string {
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
if (word) {
// if the word starts with a vowel, use "an" instead of "a"
const firstLetter = word[0].toLowerCase();
const vowels = ["a", "e", "i", "o", "u"];
const particle = vowels.includes(firstLetter) ? "an" : "a";
return particle + " " + word;
} else {
return "";
}
}
/**
* Initiates sharing of claim information
* Handles share functionality based on platform capabilities
*/
async onClickShareClaim(): Promise<void> {
this.copyToClipboard("A link to this page", this.windowLocation);
this.copyTextToClipboard("A link to this page", this.windowLocation);
window.navigator.share({
title: "Help Connect Me",
text: "I'm trying to find the full details of this claim. Can you help me?",
@@ -894,11 +903,5 @@ export default class ConfirmGiftView extends Vue {
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
this.veriClaimDump = "";
}
capitalizeAndInsertSpacesBeforeCaps(text: string) {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
}
</script>

View File

@@ -124,7 +124,7 @@ import {
NOTIFY_CONFIRMATION_RESTRICTION,
} from "../constants/notifications";
import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { GiveSummaryRecord, GiveActionClaim } from "../interfaces";
import { AgreeActionClaim } from "../interfaces/claims";
import {
@@ -223,8 +223,13 @@ export default class ContactAmountssView extends Vue {
const contact = await this.$getContact(contactDid);
this.contact = contact;
const settings = await this.$getSettings(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
const settings = await this.$accountSettings();
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) {

View File

@@ -164,7 +164,11 @@ export default class ContactGiftingView extends Vue {
try {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.allContacts = await this.$getAllContacts();

View File

@@ -340,7 +340,12 @@ export default class ContactImportView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
}

View File

@@ -104,7 +104,7 @@ import { Buffer } from "buffer/";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { logger } from "../utils/logger";
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
@@ -144,6 +144,7 @@ import {
QR_TIMEOUT_LONG,
} from "@/constants/notifications";
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
interface QRScanResult {
rawValue?: string;
@@ -195,7 +196,7 @@ export default class ContactQRScanFull extends Vue {
$router!: Router;
// Notification helper system
private notify = createNotifyHelpers(this.$notify);
private notify!: ReturnType<typeof createNotifyHelpers>;
isScanning = false;
error: string | null = null;
@@ -263,9 +264,17 @@ export default class ContactQRScanFull extends Vue {
* Loads user settings and generates QR code for contact sharing
*/
async created() {
// Initialize notification helper system
this.notify = createNotifyHelpers(this.$notify);
try {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.isRegistered = !!settings.isRegistered;
@@ -389,7 +398,7 @@ export default class ContactQRScanFull extends Vue {
this.isCleaningUp = true;
try {
logger.info("Cleaning up QR scanner resources");
logger.debug("Cleaning up QR scanner resources");
await this.stopScanning();
await QRScannerFactory.cleanup();
} catch (error) {
@@ -423,7 +432,7 @@ export default class ContactQRScanFull extends Vue {
rawValue === this.lastScannedValue &&
now - this.lastScanTime < this.SCAN_DEBOUNCE_MS
) {
logger.info("Ignoring duplicate scan:", rawValue);
logger.debug("Ignoring duplicate scan:", rawValue);
return;
}
@@ -431,7 +440,7 @@ export default class ContactQRScanFull extends Vue {
this.lastScannedValue = rawValue;
this.lastScanTime = now;
logger.info("Processing QR code scan result:", rawValue);
logger.debug("Processing QR code scan result:", rawValue);
let contact: Contact;
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
@@ -444,7 +453,7 @@ export default class ContactQRScanFull extends Vue {
}
// Process JWT and contact info
logger.info("Decoding JWT payload from QR code");
logger.debug("Decoding JWT payload from QR code");
const decodedJwt = await decodeEndorserJwt(jwt);
if (!decodedJwt?.payload?.own) {
logger.warn("Invalid JWT payload - missing 'own' field");
@@ -483,7 +492,7 @@ export default class ContactQRScanFull extends Vue {
}
// Add contact but keep scanning
logger.info("Adding new contact to database:", {
logger.debug("Adding new contact to database:", {
did: contact.did,
name: contact.name,
});
@@ -542,7 +551,7 @@ export default class ContactQRScanFull extends Vue {
*/
async addNewContact(contact: Contact) {
try {
logger.info("Opening database connection for new contact");
logger.debug("Opening database connection for new contact");
// Check if contact already exists
const existingContact = await this.$getContact(contact.did);
@@ -556,7 +565,7 @@ export default class ContactQRScanFull extends Vue {
await this.$insertContact(contact);
if (this.activeDid) {
logger.info("Setting contact visibility", { did: contact.did });
logger.debug("Setting contact visibility", { did: contact.did });
await this.setVisibility(contact, true);
contact.seesMe = true;
}
@@ -603,7 +612,7 @@ export default class ContactQRScanFull extends Vue {
async handleAppPause() {
if (!this.isMounted) return;
logger.info("App paused, stopping scanner");
logger.debug("App paused, stopping scanner");
await this.stopScanning();
}
@@ -613,7 +622,7 @@ export default class ContactQRScanFull extends Vue {
handleAppResume() {
if (!this.isMounted) return;
logger.info("App resumed, scanner can be restarted by user");
logger.debug("App resumed, scanner can be restarted by user");
this.isScanning = false;
}
@@ -622,6 +631,15 @@ export default class ContactQRScanFull extends Vue {
*/
async handleBack() {
await this.cleanupScanner();
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
this.$router.back();
}
@@ -636,36 +654,51 @@ export default class ContactQRScanFull extends Vue {
* Copies contact URL to clipboard for sharing
*/
async onCopyUrlToClipboard() {
const account = (await libsUtil.retrieveFullyDecryptedAccount(
this.activeDid,
)) as Account;
const jwtUrl = await generateEndorserJwtUrlForAccount(
account,
this.isRegistered,
this.givenName,
this.profileImageUrl,
true,
);
useClipboard()
.copy(jwtUrl)
.then(() => {
this.notify.toast(
NOTIFY_QR_URL_COPIED.title,
NOTIFY_QR_URL_COPIED.message,
QR_TIMEOUT_MEDIUM,
);
try {
const account = (await libsUtil.retrieveFullyDecryptedAccount(
this.activeDid,
)) as Account;
const jwtUrl = await generateEndorserJwtUrlForAccount(
account,
this.isRegistered,
this.givenName,
this.profileImageUrl,
true,
);
// Use the platform-specific ClipboardService for reliable iOS support
await copyToClipboard(jwtUrl);
this.notify.toast(
NOTIFY_QR_URL_COPIED.title,
NOTIFY_QR_URL_COPIED.message,
QR_TIMEOUT_MEDIUM,
);
} catch (error) {
logger.error("Error copying URL to clipboard:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
this.notify.error("Failed to copy URL to clipboard.");
}
}
/**
* Copies DID to clipboard for manual sharing
*/
onCopyDidToClipboard() {
useClipboard()
.copy(this.activeDid)
.then(() => {
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
async onCopyDidToClipboard() {
try {
// Use the platform-specific ClipboardService for reliable iOS support
await copyToClipboard(this.activeDid);
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
} catch (error) {
logger.error("Error copying DID to clipboard:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
this.notify.error("Failed to copy DID to clipboard.");
}
}
/**

View File

@@ -140,6 +140,7 @@ import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { copyToClipboard } from "../services/ClipboardService";
import { QrcodeStream } from "vue-qrcode-reader";
@@ -163,6 +164,7 @@ import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
import { CameraState } from "@/services/QRScanner/types";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_QR_INITIALIZATION_ERROR,
NOTIFY_QR_CAMERA_IN_USE,
@@ -286,7 +288,12 @@ export default class ContactQRScanShow extends Vue {
try {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.hideRegisterPromptOnNewContact =
@@ -319,6 +326,15 @@ export default class ContactQRScanShow extends Vue {
async handleBack(): Promise<void> {
await this.cleanupScanner();
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
this.$router.back();
}
@@ -417,7 +433,7 @@ export default class ContactQRScanShow extends Vue {
this.isCleaningUp = true;
try {
logger.info("Cleaning up QR scanner resources");
logger.debug("Cleaning up QR scanner resources");
await this.stopScanning();
await QRScannerFactory.cleanup();
} catch (error) {
@@ -451,7 +467,7 @@ export default class ContactQRScanShow extends Vue {
rawValue === this.lastScannedValue &&
now - this.lastScanTime < this.SCAN_DEBOUNCE_MS
) {
logger.info("Ignoring duplicate scan:", rawValue);
logger.debug("Ignoring duplicate scan:", rawValue);
return;
}
@@ -459,7 +475,7 @@ export default class ContactQRScanShow extends Vue {
this.lastScannedValue = rawValue;
this.lastScanTime = now;
logger.info("Processing QR code scan result:", rawValue);
logger.debug("Processing QR code scan result:", rawValue);
let contact: Contact;
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
@@ -469,7 +485,7 @@ export default class ContactQRScanShow extends Vue {
this.notify.error(NOTIFY_QR_INVALID_QR_CODE.message);
return;
}
logger.info("Decoding JWT payload from QR code");
logger.debug("Decoding JWT payload from QR code");
const decodedJwt = await decodeEndorserJwt(jwt);
// Process JWT and contact info
@@ -504,7 +520,7 @@ export default class ContactQRScanShow extends Vue {
}
// Add contact but keep scanning
logger.info("Adding new contact to database:", {
logger.debug("Adding new contact to database:", {
did: contact.did,
name: contact.name,
});
@@ -538,7 +554,7 @@ export default class ContactQRScanShow extends Vue {
}
async register(contact: Contact) {
logger.info("Submitting contact registration", {
logger.debug("Submitting contact registration", {
did: contact.did,
name: contact.name,
});
@@ -554,7 +570,7 @@ export default class ContactQRScanShow extends Vue {
if (regResult.success) {
contact.registered = true;
await this.$updateContact(contact.did, { registered: true });
logger.info("Contact registration successful", { did: contact.did });
logger.debug("Contact registration successful", { did: contact.did });
this.notify.success(
createQRRegistrationSuccessMessage(contact.name || ""),
@@ -618,7 +634,6 @@ export default class ContactQRScanShow extends Vue {
);
// Copy the URL to clipboard
const { copyToClipboard } = await import("../services/ClipboardService");
await copyToClipboard(jwtUrl);
this.notify.toast(
NOTIFY_QR_URL_COPIED.title,
@@ -637,7 +652,6 @@ export default class ContactQRScanShow extends Vue {
async onCopyDidToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
try {
const { copyToClipboard } = await import("../services/ClipboardService");
await copyToClipboard(this.activeDid);
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
} catch (error) {
@@ -682,20 +696,20 @@ export default class ContactQRScanShow extends Vue {
async handleAppPause() {
if (!this.isMounted) return;
logger.info("App paused, stopping scanner");
logger.debug("App paused, stopping scanner");
await this.stopScanning();
}
handleAppResume() {
if (!this.isMounted) return;
logger.info("App resumed, scanner can be restarted by user");
logger.debug("App resumed, scanner can be restarted by user");
this.isScanning = false;
}
async addNewContact(contact: Contact) {
try {
logger.info("Opening database connection for new contact");
logger.debug("Opening database connection for new contact");
// Check if contact already exists
const existingContact = await this.$getContact(contact.did);
@@ -722,7 +736,7 @@ export default class ContactQRScanShow extends Vue {
await this.$insertContact(contact);
if (this.activeDid) {
logger.info("Setting contact visibility", { did: contact.did });
logger.debug("Setting contact visibility", { did: contact.did });
await this.setVisibility(contact, true);
contact.seesMe = true;
}
@@ -738,24 +752,17 @@ export default class ContactQRScanShow extends Vue {
!contact.registered
) {
setTimeout(() => {
this.notify.confirm(
"Do you want to register them?",
this.$notify(
{
group: "modal",
type: "confirm",
title: "Register",
text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$updateSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
await this.handleRegistrationPromptResponse(stopAsking);
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$updateSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
await this.handleRegistrationPromptResponse(stopAsking);
},
onYes: async () => {
await this.register(contact);
@@ -885,6 +892,17 @@ export default class ContactQRScanShow extends Vue {
videoElement.style.transform = shouldMirror ? "scaleX(-1)" : "none";
}
}
private async handleRegistrationPromptResponse(
stopAsking?: boolean,
): Promise<void> {
if (stopAsking) {
await this.$saveSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
}
}
</script>

View File

@@ -174,7 +174,7 @@ import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { isDatabaseError } from "@/interfaces/common";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { APP_SERVER } from "@/constants/app";
import { APP_SERVER, DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QRNavigationService } from "@/services/QRNavigationService";
import {
NOTIFY_CONTACT_NO_INFO,
@@ -294,10 +294,19 @@ export default class ContactsView extends Vue {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || DEFAULT_ENDORSER_API_SERVER;
this.isRegistered = !!settings.isRegistered;
logger.debug("[ContactsView] Created with settings:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
isRegistered: this.isRegistered,
});
// if these detect a query parameter, they can and then redirect to this URL without a query parameter
// to avoid problems when they reload or they go forward & back and it tries to reprocess
await this.processContactJwt();
@@ -346,15 +355,34 @@ export default class ContactsView extends Vue {
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
this.notify.error(NOTIFY_BLANK_INVITE.message, TIMEOUTS.VERY_LONG);
} else if (importedInviteJwt) {
logger.debug("[ContactsView] Processing invite JWT, current activeDid:", {
activeDid: this.activeDid,
});
// Re-fetch settings after ensuring active_identity is populated
const updatedSettings = await this.$accountSettings();
this.activeDid = updatedSettings.activeDid || "";
this.apiServer = updatedSettings.apiServer || DEFAULT_ENDORSER_API_SERVER;
// Identity creation should be handled by router guard, but keep as fallback for invite processing
if (!this.activeDid) {
logger.info(
"[ContactsView] No active DID found, creating identity as fallback for invite processing",
);
this.activeDid = await generateSaveAndActivateIdentity();
logger.info("[ContactsView] Created new identity:", {
activeDid: this.activeDid,
});
}
// send invite directly to server, with auth for this user
const headers = await getHeaders(this.activeDid);
logger.debug("[ContactsView] Making API request to claim invite:", {
apiServer: this.apiServer,
activeDid: this.activeDid,
hasApiServer: !!this.apiServer,
apiServerLength: this.apiServer?.length || 0,
fullUrl: this.apiServer + "/api/v2/claim",
});
try {
const response = await this.axios.post(
this.apiServer + "/api/v2/claim",
@@ -376,6 +404,9 @@ export default class ContactsView extends Vue {
const payload: JWTPayload =
decodeEndorserJwt(importedInviteJwt).payload;
const registration = payload as VerifiableCredential;
logger.debug(
"[ContactsView] Opening ContactNameDialog for invite processing",
);
(this.$refs.contactNameDialog as ContactNameDialog).open(
"Who Invited You?",
"",
@@ -414,17 +445,28 @@ export default class ContactsView extends Vue {
this.$logAndConsole(fullError, true);
let message = "Got an error sending the invite.";
if (
error &&
typeof error === "object" &&
"response" in error &&
error.response &&
typeof error.response === "object" &&
"data" in error.response &&
error.response.data &&
error.response.data.error
typeof error.response.data === "object" &&
"error" in error.response.data
) {
if (error.response.data.error.message) {
message = error.response.data.error.message;
const responseData = error.response.data as { error: unknown };
if (
responseData.error &&
typeof responseData.error === "object" &&
"message" in responseData.error
) {
message = (responseData.error as { message: string }).message;
} else {
message = error.response.data.error;
message = String(responseData.error);
}
} else if (error.message) {
message = error.message;
} else if (error && typeof error === "object" && "message" in error) {
message = (error as { message: string }).message;
}
this.notify.error(message, TIMEOUTS.MODAL);
}

View File

@@ -71,22 +71,22 @@
contactFromDid?.seesMe && contactFromDid.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They can see you"
title="They can see your activity"
@click="confirmSetVisibility(contactFromDid, false)"
>
<font-awesome icon="eye" class="fa-fw" />
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They cannot see you"
title="They cannot see your activity"
@click="confirmSetVisibility(contactFromDid, true)"
>
<font-awesome icon="eye-slash" class="fa-fw" />
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<button
@@ -95,11 +95,11 @@
contactFromDid.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="I view their content"
title="You watch their activity"
@click="confirmViewContent(contactFromDid, false)"
>
<font-awesome icon="eye" class="fa-fw" />
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
@@ -107,11 +107,11 @@
contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="I do not view their content"
title="You do not watch their activity"
@click="confirmViewContent(contactFromDid, true)"
>
<font-awesome icon="eye-slash" class="fa-fw" />
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<button
@@ -376,7 +376,12 @@ export default class DIDView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
}

View File

@@ -1003,7 +1003,7 @@
<h2>Exported Data</h2>
<span
class="text-blue-500 cursor-pointer hover:text-blue-700"
@click="copyToClipboard"
@click="copyExportedDataToClipboard"
>
Copy to Clipboard
</span>
@@ -1014,7 +1014,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { Router } from "vue-router";
import {
@@ -1072,8 +1072,6 @@ export default class DatabaseMigration extends Vue {
private exportedData: Record<string, any> | null = null;
private successMessage = "";
useClipboard = useClipboard;
/**
* Computed property to get the display name for a setting
* Handles both live comparison data and exported JSON format
@@ -1133,13 +1131,11 @@ export default class DatabaseMigration extends Vue {
/**
* Copies exported data to clipboard and shows success message
*/
async copyToClipboard(): Promise<void> {
async copyExportedDataToClipboard(): Promise<void> {
if (!this.exportedData) return;
try {
await this.useClipboard().copy(
JSON.stringify(this.exportedData, null, 2),
);
await copyToClipboard(JSON.stringify(this.exportedData, null, 2));
// Use global window object properly
if (typeof window !== "undefined") {
window.alert("Copied to clipboard!");
@@ -1265,7 +1261,7 @@ export default class DatabaseMigration extends Vue {
this.comparison.differences.settings.added.length +
this.comparison.differences.accounts.added.length;
this.successMessage = `Comparison completed successfully. Found ${totalItems} items to migrate.`;
logger.info(
logger.debug(
"[DatabaseMigration] Database comparison completed successfully",
);
} catch (error) {
@@ -1317,7 +1313,7 @@ export default class DatabaseMigration extends Vue {
this.successMessage += ` ${result.warnings.length} warnings.`;
this.warning += result.warnings.join(", ");
}
logger.info(
logger.debug(
"[DatabaseMigration] Settings migration completed successfully",
result,
);
@@ -1360,7 +1356,7 @@ export default class DatabaseMigration extends Vue {
this.successMessage += ` ${result.warnings.length} warnings.`;
this.warning += result.warnings.join(", ");
}
logger.info(
logger.debug(
"[DatabaseMigration] Account migration completed successfully",
result,
);
@@ -1410,7 +1406,7 @@ export default class DatabaseMigration extends Vue {
URL.revokeObjectURL(url);
this.successMessage = "Comparison data exported successfully";
logger.info("[DatabaseMigration] Comparison data exported successfully");
logger.debug("[DatabaseMigration] Comparison data exported successfully");
} catch (error) {
this.error = `Failed to export comparison data: ${error}`;
logger.error("[DatabaseMigration] Export failed:", error);

View File

@@ -51,6 +51,33 @@
<!-- Secondary Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300">
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
<li v-if="isProjectsActive">
<a
href="#"
:class="computedStarredTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
isStarredActive = true;
isLocalActive = false;
isMappedActive = false;
isAnywhereActive = false;
isSearchVisible = false;
tempSearchBox = null;
searchStarred();
"
>
Starred
<!-- restore when the links don't jump around for different numbers
<span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
v-if="isLocalActive"
>
{{ localCount > -1 ? localCount : "?" }}
</span>
-->
</a>
</li>
<li>
<a
href="#"
@@ -58,9 +85,11 @@
@click="
projects = [];
userProfiles = [];
isStarredActive = false;
isLocalActive = true;
isMappedActive = false;
isAnywhereActive = false;
isStarredActive = false;
isSearchVisible = true;
tempSearchBox = null;
searchLocal();
@@ -84,9 +113,11 @@
@click="
projects = [];
userProfiles = [];
isStarredActive = false;
isLocalActive = false;
isMappedActive = true;
isAnywhereActive = false;
isStarredActive = false;
isSearchVisible = false;
searchTerms = '';
tempSearchBox = null;
@@ -103,9 +134,11 @@
@click="
projects = [];
userProfiles = [];
isStarredActive = false;
isLocalActive = false;
isMappedActive = false;
isAnywhereActive = true;
isStarredActive = false;
isSearchVisible = true;
tempSearchBox = null;
searchAll();
@@ -201,6 +234,15 @@
>No {{ isProjectsActive ? "projects" : "people" }} were found with
that search.</span
>
<span v-else-if="isStarredActive">
<p>
You have no starred projects. Star some projects to see them here.
</p>
<p class="mt-4">
When you star projects, you will get a notice on the front page when
they change.
</p>
</span>
</p>
</div>
@@ -383,9 +425,12 @@ export default class DiscoverView extends Vue {
allMyDids: Array<string> = [];
apiServer = "";
isLoading = false;
isLocalActive = false;
isMappedActive = false;
isAnywhereActive = true;
isStarredActive = false;
isProjectsActive = true;
isPeopleActive = false;
isSearchVisible = true;
@@ -415,7 +460,11 @@ export default class DiscoverView extends Vue {
const searchPeople = !!this.$route.query["searchPeople"];
const settings = await this.$accountSettings();
this.activeDid = (settings.activeDid as string) || "";
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = (settings.apiServer as string) || "";
this.partnerApiServer =
(settings.partnerApiServer as string) || this.partnerApiServer;
@@ -470,6 +519,8 @@ export default class DiscoverView extends Vue {
leafletObject: L.Map;
};
this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
} else if (this.isStarredActive) {
await this.searchStarred();
} else {
await this.searchAll();
}
@@ -540,6 +591,60 @@ export default class DiscoverView extends Vue {
}
}
public async searchStarred() {
this.resetCounts();
// Clear any previous results
this.projects = [];
this.userProfiles = [];
try {
this.isLoading = true;
// Get starred project IDs from settings
const settings = await this.$accountSettings();
const starredIds = settings.starredPlanHandleIds || [];
if (starredIds.length === 0) {
// No starred projects
return;
}
// This could be optimized to only pull those not already in the cache (endorserServer.ts)
const planHandleIdsJson = JSON.stringify(starredIds);
const endpoint =
this.apiServer +
"/api/v2/report/plans?planHandleIds=" +
encodeURIComponent(planHandleIdsJson);
const response = await this.axios.get(endpoint, {
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
this.notify.error("Failed to load starred projects", TIMEOUTS.SHORT);
return;
}
const starredPlans: PlanData[] = response.data.data;
if (response.data.hitLimit) {
// someday we'll have to let them incrementally load the rest
this.notify.warning(
"Beware: you have so many starred projects that we cannot load them all.",
TIMEOUTS.SHORT,
);
}
this.projects = starredPlans;
} catch (error: unknown) {
logger.error("Error loading starred projects:", error);
this.notify.error(
"Failed to load starred projects. Please try again.",
TIMEOUTS.LONG,
);
} finally {
this.isLoading = false;
}
}
public async searchLocal(beforeId?: string) {
this.resetCounts();
@@ -633,9 +738,12 @@ export default class DiscoverView extends Vue {
const latestProject = this.projects[this.projects.length - 1];
if (this.isLocalActive || this.isMappedActive) {
this.searchLocal(latestProject.rowId);
} else if (this.isStarredActive) {
this.searchStarred();
} else if (this.isAnywhereActive) {
this.searchAll(latestProject.rowId);
}
// Note: Starred tab doesn't support pagination since we load all starred projects at once
} else if (this.isPeopleActive && this.userProfiles.length > 0) {
const latestProfile = this.userProfiles[this.userProfiles.length - 1];
if (this.isLocalActive || this.isMappedActive) {
@@ -775,6 +883,24 @@ export default class DiscoverView extends Vue {
this.$router.push(route);
}
public computedStarredTabStyleClassNames() {
return {
"inline-block": true,
"py-3": true,
"rounded-t-lg": true,
"border-b-2": true,
active: this.isStarredActive,
"text-black": this.isStarredActive,
"border-black": this.isStarredActive,
"font-semibold": this.isStarredActive,
"text-blue-600": !this.isStarredActive,
"border-transparent": !this.isStarredActive,
"hover:border-slate-400": !this.isStarredActive,
};
}
public computedLocalTabStyleClassNames() {
return {
"inline-block": true,

View File

@@ -280,6 +280,7 @@ import { logger } from "../utils/logger";
import { Contact } from "@/db/tables/contacts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
@@ -441,7 +442,11 @@ export default class GiftedDetails extends Vue {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
if (
(this.giverDid && !this.giverName) ||
@@ -770,6 +775,15 @@ export default class GiftedDetails extends Vue {
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
TIMEOUTS.SHORT,
);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
localStorage.removeItem("imageUrl");
if (this.destinationPathAfter) {
(this.$router as Router).push({ path: this.destinationPathAfter });

View File

@@ -394,7 +394,7 @@ export default class HelpNotificationsView extends Vue {
notifyingReminderTime = "";
// Notification helper system
notify = createNotifyHelpers(this.$notify);
notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Computed property for consistent button styling
@@ -430,6 +430,9 @@ export default class HelpNotificationsView extends Vue {
* Handles errors gracefully with proper logging without exposing sensitive data.
*/
async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try {
const registration = await navigator.serviceWorker?.ready;
const fullSub = await registration?.pushManager.getSubscription();

View File

@@ -319,8 +319,9 @@
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
Beware that this will erase your existing contact & settings.
click Advanced, and follow the instructions to "Import Contacts".
(There is currently no way to import other settings, so you'll have to recreate
by hand your search area, filters, etc.)
</li>
</ul>
</div>
@@ -336,14 +337,18 @@
<h2 class="text-xl font-semibold">How do I erase my data from my device?</h2>
<p>
Before doing this, you may want to back up your data with the instructions above.
Before doing this, you should back up your data with the instructions above.
Note that this does not erase data sent to our servers (see contact info below)
</p>
<ul>
<li class="list-disc list-outside ml-4">
Mobile
<ul>
<li class="list-disc list-outside ml-4">
Home Screen: hold down on the icon, and choose to delete it
App Store app: hold down on the icon, then uninstall it
</li>
<li class="list-disc list-outside ml-4">
Home Screen PWA: hold down on the icon, and delete it
</li>
<li class="list-disc list-outside ml-4">
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
@@ -415,15 +420,6 @@
different page.
</p>
<h2 class="text-xl font-semibold">
Where do I get help with notifications?
</h2>
<p>
<router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link
>
</p>
<h2 class="text-xl font-semibold">
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
What can I do?
@@ -434,10 +430,13 @@
</p>
<ul class="list-disc list-outside ml-4">
<li>
Drag down on the screen to refresh it; do that multiple times, because
For mobile apps, make sure you're connected to the internet.
</li>
<li>
For PWAs, drag down on the screen to refresh it; do that multiple times, because
it sometimes takes multiple tries for the app to refresh to the latest version.
You can see the version information at the bottom of this page; the best
way to determine the latest version is to open this page in an incognito/private
way to determine the latest version is to open TimeSafari.app in an incognito/private
browser window and look at the version there.
</li>
<li>
@@ -468,9 +467,6 @@
</ul>
Then reload Time Safari.
</li>
<li>
Restart your device.
</li>
</ul>
<p>
If you still have problems, you can clear the cache (see "erase my data" above)
@@ -508,16 +504,12 @@
</p>
<ul class="list-disc list-outside ml-4">
<li>
If using notifications, a server stores push token data. That can be revoked at any time
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
</li>
<li>
If sending images, a server stores them, too. They can be removed by editing the claim
and deleting them.
If sending images, a server stores them. They can be removed by editing each claim
and deleting the image.
</li>
<li>
If sending other partner system data (eg. to Trustroots) a public key and message
data are stored on a server. Those can be removed via direct personal request.
data are stored on a server. Those can be removed via direct personal request (via contact below).
</li>
<li>
For all other claim data,
@@ -592,15 +584,16 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
// Capacitor import removed - using QRNavigationService instead
import * as Package from "../../package.json";
import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER } from "../constants/app";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { QRNavigationService } from "@/services/QRNavigationService";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
/**
* HelpView.vue - Comprehensive Help System Component
@@ -634,8 +627,10 @@ import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
})
export default class HelpView extends Vue {
$router!: Router;
$notify!: (notification: NotificationIface, timeout?: number) => void;
package = Package;
notify!: ReturnType<typeof createNotifyHelpers>;
commitHash = import.meta.env.VITE_GIT_HASH;
showAlpha = false;
showBasics = false;
@@ -648,6 +643,13 @@ export default class HelpView extends Vue {
APP_SERVER = APP_SERVER;
// Capacitor reference removed - using QRNavigationService instead
/**
* Initialize notification helpers
*/
created() {
this.notify = createNotifyHelpers(this.$notify);
}
/**
* Get the unnamed entity name constant
*/
@@ -668,11 +670,15 @@ export default class HelpView extends Vue {
* @param {string} text - The text to copy to clipboard
* @param {Function} fn - Callback function to execute before and after copying
*/
doCopyTwoSecRedo(text: string, fn: () => void): void {
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
try {
await copyToClipboard(text);
setTimeout(fn, 2000);
} catch (error) {
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
this.notify.error("Failed to copy to clipboard.", TIMEOUTS.SHORT);
}
}
/**
@@ -688,7 +694,10 @@ export default class HelpView extends Vue {
try {
const settings = await this.$accountSettings();
if (settings.activeDid) {
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
if (activeIdentity.activeDid) {
await this.$updateSettings({
...settings,
finishedOnboarding: false,
@@ -696,7 +705,7 @@ export default class HelpView extends Vue {
this.$log(
"[HelpView] Onboarding reset successfully for DID: " +
settings.activeDid,
activeIdentity.activeDid,
);
}

Some files were not shown because too many files have changed in this diff Show More