Compare commits

..

32 Commits

Author SHA1 Message Date
ff864adbe5 Merge branch 'master' into profile_include_location 2025-08-28 06:37:51 -04: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
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
Matthew Raymer
f2026bb921 chore: clean up debug logging 2025-08-18 07:44:26 +00:00
77 changed files with 3561 additions and 2629 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

@@ -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

@@ -14,6 +14,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 +268,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 +277,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,6 +286,8 @@ 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
---
@@ -194,3 +303,9 @@ or context. They form the foundation for all AI assistant behavior.
**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:

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

@@ -2768,6 +2768,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

@@ -1,343 +0,0 @@
# Active Identity Implementation Overview
**Author**: Matthew Raymer
**Date**: 2025-08-21T13:40Z
**Status**: 🚧 **IN PROGRESS** - Implementation Complete, Testing Pending
## Objective
Separate the `activeDid` field from the monolithic `settings` table into a
dedicated `active_identity` table to achieve:
- **Data normalization** and reduced cache drift
- **Simplified identity management** with dedicated table
- **Zero breaking API surface** for existing components
- **Phased migration** with rollback capability
## Result
This document provides a comprehensive overview of the implemented Active
Identity table separation system, including architecture, migration strategy,
and component integration.
## Use/Run
The implementation is ready for testing. Components can immediately use the new
façade methods while maintaining backward compatibility through dual-write
triggers.
## Context & Scope
- **Audience**: Developers working with identity management and database
migrations
- **In scope**: Active DID management, database schema evolution, Vue component
integration
- **Out of scope**: Multi-profile support beyond basic scope framework, complex
identity hierarchies
## Artifacts & Links
- **Implementation**: `src/db/tables/activeIdentity.ts`,
`src/utils/PlatformServiceMixin.ts`
- **Migrations**: `src/db-sql/migration.ts` (migrations 003 & 004)
- **Configuration**: `src/config/featureFlags.ts`
- **Documentation**: This document and progress tracking
## Environment & Preconditions
- **Database**: SQLite (Absurd-SQL for Web, Capacitor SQLite for Mobile)
- **Framework**: Vue.js with PlatformServiceMixin
- **Migration System**: Built-in migrationService.ts with automatic execution
## Architecture / Process Overview
The Active Identity separation follows a **phased migration pattern** with
dual-write triggers to ensure zero downtime and backward compatibility.
```mermaid
flowchart TD
A[Legacy State] --> B[Phase A: Dual-Write]
B --> C[Phase B: Component Cutover]
C --> D[Phase C: Legacy Cleanup]
A --> A1[settings.activeDid]
B --> B1[active_identity table]
B --> B2[Dual-write trigger]
B --> B3[Fallback support]
C --> C1[Components use façade]
C --> C2[Legacy fallback disabled]
D --> D1[Drop activeDid column]
D --> D2[Remove triggers]
```
## Interfaces & Contracts
### Database Schema
| Table | Purpose | Key Fields | Constraints |
|-------|---------|------------|-------------|
| `active_identity` | Store active DID | `id`, `active_did`, | FK to accounts.did |
| | | `updated_at` | |
### Service Façade API
| Method | Purpose | Parameters | Returns |
|--------|---------|------------|---------|
| `$getActiveDid()` | Retrieve active DID | None | `Promise<string \| null>` |
| `$setActiveDid(did)` | Set active DID | `did` | `Promise<void>` |
| `$switchActiveIdentity(did)` | Switch to different DID | `did` | `Promise<void>` |
| `$getActiveIdentityScopes()` | Get available scopes | None | `Promise<string[]>` (always returns `["default"]`) |
## Repro: End-to-End Procedure
### 1. Database Migration Execution
```bash
# Migrations run automatically on app startup
# Migration 003: Creates active_identity table
# Migration 004: Drops settings.activeDid column (Phase C)
```
### 2. Component Usage
```typescript
// Before (legacy)
const activeDid = settings.activeDid || "";
await this.$saveSettings({ activeDid: newDid });
// After (new façade)
const activeDid = await this.$getActiveDid() || "";
await this.$setActiveDid(newDid);
```
### 3. Feature Flag Control
```typescript
// Enable/disable migration phases
FLAGS.USE_ACTIVE_IDENTITY_ONLY = false; // Allow legacy fallback
FLAGS.DROP_SETTINGS_ACTIVEDID = false; // Keep legacy column
FLAGS.LOG_ACTIVE_ID_FALLBACK = true; // Log fallback usage
```
## What Works (Evidence)
-**Migration Infrastructure**: Migrations 003 and 004 integrated into
`migrationService.ts`
-**Table Creation**: `active_identity` table schema with proper constraints
and indexes
-**Service Façade**: PlatformServiceMixin extended with all required methods
-**Feature Flags**: Comprehensive flag system for controlling rollout phases
-**Dual-Write Support**: One-way trigger from `settings.activeDid`
`active_identity.active_did`
-**Validation**: DID existence validation before setting as active
-**Error Handling**: Comprehensive error handling with logging
## What Doesn't (Evidence & Hypotheses)
-**Component Migration**: No components yet updated to use new façade
methods
-**Testing**: No automated tests for new functionality
-**Performance Validation**: No benchmarks for read/write performance
-**Cross-Platform Validation**: Not tested on mobile platforms yet
## Risks, Limits, Assumptions
### **Migration Risks**
- **Data Loss**: If migration fails mid-process, could lose active DID state
- **Rollback Complexity**: Phase C (column drop) requires table rebuild, not
easily reversible
- **Trigger Dependencies**: Dual-write trigger could fail if `active_identity`
table is corrupted
### **Performance Limits**
- **Dual-Write Overhead**: Each `activeDid` change triggers additional
database operations
- **Fallback Queries**: Legacy fallback requires additional database queries
- **Transaction Scope**: Active DID changes wrapped in transactions for
consistency
### **Security Boundaries**
- **DID Validation**: Only validates DID exists in accounts table, not
ownership
- **Scope Isolation**: No current scope separation enforcement beyond table
constraints
- **Access Control**: No row-level security on `active_identity` table
## Next Steps
| Owner | Task | Exit Criteria | Target Date (UTC) |
|-------|------|---------------|-------------------|
| Developer | Test migrations | Migrations execute without errors | 2025-08-21 |
| Developer | Update components | All components use new façade | 2025-08-22 |
| | | methods | |
| Developer | Performance testing | Read/write performance meets | 2025-08-23 |
| | | requirements | |
| Developer | Phase C activation | Feature flag enables column | 2025-08-24 |
| | | removal | |
## References
- [Database Migration Guide](../database-migration-guide.md)
- [PlatformServiceMixin Documentation](../component-communication-guide.md)
- [Feature Flags Configuration](../feature-flags.md)
## Competence Hooks
- **Why this works**: Phased migration with dual-write triggers ensures zero
downtime while maintaining data consistency through foreign key constraints
and validation
- **Common pitfalls**: Forgetting to update components before enabling
`USE_ACTIVE_IDENTITY_ONLY`, not testing rollback scenarios, ignoring
cross-platform compatibility
- **Next skill unlock**: Implement automated component migration using codemods
and ESLint rules
- **Teach-back**: Explain how the dual-write trigger prevents data divergence
during the transition phase
## Collaboration Hooks
- **Reviewers**: Database team for migration logic, Vue team for component
integration, DevOps for deployment strategy
- **Sign-off checklist**: Migrations tested in staging, components updated,
performance validated, rollback plan documented
## Assumptions & Limits
- **Single User Focus**: Current implementation assumes single-user mode with
'default' scope
- **Vue Compatibility**: Assumes `vue-facing-decorator` compatibility (needs
validation)
- **Migration Timing**: Assumes migrations run on app startup (automatic
execution)
- **Platform Support**: Assumes same behavior across Web (Absurd-SQL) and
Mobile (Capacitor SQLite)
## Implementation Details
### **Migration 003: Table Creation**
Creates the `active_identity` table with:
- **Primary Key**: Auto-incrementing ID
- **Scope Field**: For future multi-profile support (currently 'default')
- **Active DID**: Foreign key to accounts.did with CASCADE UPDATE
- **Timestamps**: ISO format timestamps for audit trail
- **Indexes**: Performance optimization for scope and DID lookups
### **Migration 004: Column Removal**
Implements Phase C by:
- **Table Rebuild**: Creates new settings table without activeDid column
- **Data Preservation**: Copies all other data from legacy table
- **Index Recreation**: Rebuilds necessary indexes
- **Trigger Cleanup**: Removes dual-write triggers
### **Service Façade Implementation**
The PlatformServiceMixin extension provides:
- **Dual-Read Logic**: Prefers new table, falls back to legacy during
transition
- **Dual-Write Logic**: Updates both tables during Phase A/B
- **Validation**: Ensures DID exists before setting as active
- **Transaction Safety**: Wraps operations in database transactions
- **Error Handling**: Comprehensive logging and error propagation
### **Feature Flag System**
Controls migration phases through:
- **`USE_ACTIVE_IDENTITY_ONLY`**: Disables legacy fallback reads
- **`DROP_SETTINGS_ACTIVEDID`**: Enables Phase C column removal
- **`LOG_ACTIVE_ID_FALLBACK`**: Logs when legacy fallback is used
- **`ENABLE_ACTIVE_IDENTITY_MIGRATION`**: Master switch for migration
system
## Security Considerations
### **Data Validation**
- DID format validation (basic "did:" prefix check)
- Foreign key constraints ensure referential integrity
- Transaction wrapping prevents partial updates
### **Access Control**
- No row-level security implemented
- Scope isolation framework in place for future use
- Validation prevents setting non-existent DIDs as active
### **Audit Trail**
- Timestamps on all active identity changes
- Logging of fallback usage and errors
- Migration tracking through built-in system
## Performance Characteristics
### **Read Operations**
- **Primary Path**: Single query to `active_identity` table
- **Fallback Path**: Additional query to `settings` table (Phase A only)
- **Indexed Fields**: Both scope and active_did are indexed
### **Write Operations**
- **Dual-Write**: Updates both tables during transition (Phase A/B)
- **Transaction Overhead**: All operations wrapped in transactions
- **Trigger Execution**: Additional database operations per update
### **Migration Impact**
- **Table Creation**: Minimal impact (runs once)
- **Column Removal**: Moderate impact (table rebuild required)
- **Data Seeding**: Depends on existing data volume
## Testing Strategy
### **Unit Testing**
- Service façade method validation
- Error handling and edge cases
- Transaction rollback scenarios
### **Integration Testing**
- Migration execution and rollback
- Cross-platform compatibility
- Performance under load
### **End-to-End Testing**
- Component integration
- User workflow validation
- Migration scenarios
## Deployment Considerations
### **Rollout Strategy**
- **Phase A**: Deploy with dual-write enabled
- **Phase B**: Update components to use new methods
- **Phase C**: Enable column removal (irreversible)
### **Rollback Plan**
- **Phase A/B**: Disable feature flags, revert to legacy methods
- **Phase C**: Requires database restore (no automatic rollback)
### **Monitoring**
- Track fallback usage through logging
- Monitor migration success rates
- Alert on validation failures
---
**Status**: Implementation complete, ready for testing and component migration
**Next Review**: After initial testing and component updates
**Maintainer**: Development team

View File

@@ -1,185 +0,0 @@
# Active Identity Migration - Phase B Progress
**Author**: Matthew Raymer
**Date**: 2025-08-22T07:05Z
**Status**: 🚧 **IN PROGRESS** - Component Migration Active
## Objective
Complete **Phase B: Component Cutover** by updating all Vue components to use the new Active Identity façade methods instead of directly accessing `settings.activeDid`.
## Current Status
### ✅ **Completed**
- **Migration Infrastructure**: Migrations 003 and 004 implemented
- **Service Façade**: PlatformServiceMixin extended with all required methods
- **TypeScript Types**: Added missing method declarations to Vue component interfaces
- **Feature Flags**: Comprehensive flag system for controlling rollout phases
### 🔄 **In Progress**
- **Component Migration**: Manually updating critical components
- **Pattern Establishment**: Creating consistent migration approach
### ❌ **Pending**
- **Bulk Component Updates**: 40+ components need migration
- **Testing**: Validate migrated components work correctly
- **Performance Validation**: Ensure no performance regressions
## Migration Progress
### **Components Migrated (3/40+)**
| Component | Status | Changes Made | Notes |
|-----------|--------|--------------|-------|
| `IdentitySwitcherView.vue` | ✅ Complete | - Updated `switchIdentity()` method<br>- Added FLAGS import<br>- Uses `$setActiveDid()` | Critical component for identity switching |
| `ImportDerivedAccountView.vue` | ✅ Complete | - Updated `incrementDerivation()` method<br>- Added FLAGS import<br>- Uses `$setActiveDid()` | Handles new account creation |
| `ClaimAddRawView.vue` | ✅ Complete | - Updated `initializeSettings()` method<br>- Uses `$getActiveDid()` | Reads active DID for claims |
### **Components Pending Migration (37+)**
| Component | Usage Pattern | Priority | Estimated Effort |
|-----------|---------------|----------|------------------|
| `HomeView.vue` | ✅ Updated | High | 5 min |
| `ProjectsView.vue` | `settings.activeDid \|\| ""` | High | 3 min |
| `ContactsView.vue` | `settings.activeDid \|\| ""` | High | 3 min |
| `AccountViewView.vue` | `settings.activeDid \|\| ""` | High | 3 min |
| `InviteOneView.vue` | `settings.activeDid \|\| ""` | Medium | 3 min |
| `TestView.vue` | `settings.activeDid \|\| ""` | Medium | 3 min |
| `SeedBackupView.vue` | `settings.activeDid \|\| ""` | Medium | 3 min |
| `QuickActionBvcBeginView.vue` | `const activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ConfirmGiftView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ClaimReportCertificateView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ImportAccountView.vue` | `settings.activeDid,` | Medium | 3 min |
| `MembersList.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ShareMyContactInfoView.vue` | `const activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ClaimView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ImageMethodDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `DiscoverView.vue` | `settings.activeDid as string` | Medium | 3 min |
| `QuickActionBvcEndView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ContactQRScanFullView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ContactGiftingView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `OfferDetailsView.vue` | `this.activeDid = settings.activeDid ?? ""` | Medium | 3 min |
| `NewActivityView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `OfferDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `SharedPhotoView.vue` | `this.activeDid = settings.activeDid` | Medium | 3 min |
| `ContactQRScanShowView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `NewEditProjectView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `GiftedDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `HelpView.vue` | `if (settings.activeDid)` | Medium | 3 min |
| `TopMessage.vue` | `settings.activeDid?.slice(11, 15)` | Medium | 3 min |
| `ClaimCertificateView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `UserProfileView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `OnboardingDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `RecentOffersToUserView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `RecentOffersToUserProjectsView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `ContactImportView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
| `GiftedDetailsView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
## Migration Patterns
### **Pattern 1: Simple Read Replacement**
```typescript
// Before
this.activeDid = settings.activeDid || "";
// After
this.activeDid = await this.$getActiveDid() || "";
```
### **Pattern 2: Write Replacement with Dual-Write**
```typescript
// Before
await this.$saveSettings({ activeDid: newDid });
// After
await this.$setActiveDid(newDid);
// Legacy fallback - remove after Phase C
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
await this.$saveSettings({ activeDid: newDid });
}
```
### **Pattern 3: FLAGS Import Addition**
```typescript
// Add to imports section
import { FLAGS } from "@/config/featureFlags";
```
## Next Steps
### **Immediate Actions (Next 30 minutes)**
1. **Complete High-Priority Components**: Update remaining critical components
2. **Test Migration**: Verify migrated components work correctly
3. **Run Linter**: Check for any remaining TypeScript issues
### **Short Term (Next 2 hours)**
1. **Bulk Migration**: Use automated script for remaining components
2. **Testing**: Validate all migrated components
3. **Performance Check**: Ensure no performance regressions
### **Medium Term (Next 1 day)**
1. **Phase C Preparation**: Enable `USE_ACTIVE_IDENTITY_ONLY` flag
2. **Legacy Fallback Removal**: Remove dual-write patterns
3. **Final Testing**: End-to-end validation
## Success Criteria
### **Phase B Complete When**
- [ ] All 40+ components use new façade methods
- [ ] No direct `settings.activeDid` access remains
- [ ] All components pass linting
- [ ] Basic functionality tested and working
- [ ] Performance maintained or improved
### **Phase C Ready When**
- [ ] All components migrated and tested
- [ ] Feature flag `USE_ACTIVE_IDENTITY_ONLY` can be enabled
- [ ] No legacy fallback usage in production
- [ ] Performance benchmarks show improvement
## Risks & Mitigation
### **High Risk**
- **Component Breakage**: Test each migrated component individually
- **Performance Regression**: Monitor performance metrics during migration
- **TypeScript Errors**: Ensure all method signatures are properly declared
### **Medium Risk**
- **Migration Inconsistency**: Use consistent patterns across all components
- **Testing Coverage**: Ensure comprehensive testing of identity switching flows
### **Low Risk**
- **Backup Size**: Minimal backup strategy for critical files only
- **Rollback Complexity**: Simple git revert if needed
## Tools & Scripts
### **Migration Scripts**
- `scripts/migrate-active-identity-components.sh` - Full backup version
- `scripts/migrate-active-identity-components-efficient.sh` - Minimal backup version
### **Testing Commands**
```bash
# Check for remaining settings.activeDid usage
grep -r "settings\.activeDid" src/views/ src/components/
# Run linter
npm run lint-fix
# Test specific component
npm run test:web -- --grep "IdentitySwitcher"
```
## References
- [Active Identity Implementation Overview](./active-identity-implementation-overview.md)
- [PlatformServiceMixin Documentation](../component-communication-guide.md)
- [Feature Flags Configuration](../feature-flags.md)
- [Database Migration Guide](../database-migration-guide.md)
---
**Status**: Phase B in progress, 3/40+ components migrated
**Next Review**: After completing high-priority components
**Maintainer**: Development team

View File

@@ -1,298 +0,0 @@
# ActiveDid Table Separation Progress Report
**Author**: Matthew Raymer
**Date**: 2025-08-21T12:32Z
**Status**: 🔍 **INVESTIGATION COMPLETE** - Ready for implementation planning
## Executive Summary
This document tracks the investigation and progress of separating the `activeDid` field
from the `settings` table into a dedicated `active_identity` table. The project aims
to improve data integrity, reduce cache drift, and simplify transaction logic for
identity management in TimeSafari.
## Investigation Results
### Reference Audit Findings
**Total ActiveDid References**: 505 across the codebase
- **Write Operations**: 100 (20%)
- **Read Operations**: 260 (51%)
- **Other References**: 145 (29%) - includes type definitions, comments, etc.
**Component Impact**: 15+ Vue components directly access `settings.activeDid`
### Current Database Schema
The `settings` table currently contains **30 fields** mixing identity state with user
preferences:
```sql
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
accountDid TEXT, -- Links to identity (null = master)
activeDid TEXT, -- Current active identity (master only)
apiServer TEXT, -- API endpoint
filterFeedByNearby BOOLEAN,
filterFeedByVisible BOOLEAN,
finishedOnboarding BOOLEAN,
firstName TEXT, -- User's name
hideRegisterPromptOnNewContact BOOLEAN,
isRegistered BOOLEAN,
lastName TEXT, -- Deprecated
lastAckedOfferToUserJwtId TEXT,
lastAckedOfferToUserProjectsJwtId TEXT,
lastNotifiedClaimId TEXT,
lastViewedClaimId TEXT,
notifyingNewActivityTime TEXT,
notifyingReminderMessage TEXT,
notifyingReminderTime TEXT,
partnerApiServer TEXT,
passkeyExpirationMinutes INTEGER,
profileImageUrl TEXT,
searchBoxes TEXT, -- JSON string
showContactGivesInline BOOLEAN,
showGeneralAdvanced BOOLEAN,
showShortcutBvc BOOLEAN,
vapid TEXT,
warnIfProdServer BOOLEAN,
warnIfTestServer BOOLEAN,
webPushServer TEXT
);
```
### Component State Management
#### PlatformServiceMixin Cache System
- **`_currentActiveDid`**: Component-level cache for activeDid
- **`$updateActiveDid()`**: Method to sync cache with database
- **Change Detection**: Watcher triggers component updates on activeDid changes
- **State Synchronization**: Cache updates when `$saveSettings()` changes activeDid
#### Common Usage Patterns
```typescript
// Standard pattern across 15+ components
this.activeDid = settings.activeDid || "";
// API header generation
const headers = await getHeaders(this.activeDid);
// Identity validation
if (claim.issuer === this.activeDid) { ... }
```
### Migration Infrastructure Status
#### Existing Capabilities
- **`migrateSettings()`**: Fully implemented and functional
- **Settings Migration**: Handles 30 fields with proper type conversion
- **Data Integrity**: Includes validation and error handling
- **Rollback Capability**: Migration service has rollback infrastructure
#### Migration Order
1. **Accounts** (foundational - contains DIDs)
2. **Settings** (references accountDid, activeDid)
3. **ActiveDid** (depends on accounts and settings)
4. **Contacts** (independent, but migrated after accounts)
### Testing Infrastructure
#### Current Coverage
- **Playwright Tests**: `npm run test:web` and `npm run test:mobile`
- **No Unit Tests**: Found for migration or settings management
- **Integration Tests**: Available through Playwright test suite
- **Platform Coverage**: Web, Mobile (Android/iOS), Desktop (Electron)
## Risk Assessment
### High Risk Areas
1. **Component State Synchronization**: 505 references across codebase
2. **Cache Drift**: `_currentActiveDid` vs database `activeDid`
3. **Cross-Platform Consistency**: Web + Mobile + Desktop
### Medium Risk Areas
1. **Foreign Key Constraints**: activeDid → accounts.did relationship
2. **Migration Rollback**: Complex 30-field settings table
3. **API Surface Changes**: Components expect `settings.activeDid`
### Low Risk Areas
1. **Migration Infrastructure**: Already exists and functional
2. **Data Integrity**: Current migration handles complex scenarios
3. **Testing Framework**: Playwright tests available for validation
## Implementation Phases
### Phase 1: Foundation Analysis ✅ **COMPLETE**
- [x] **ActiveDid Reference Audit**: 505 references identified and categorized
- [x] **Database Schema Analysis**: 30-field settings table documented
- [x] **Component Usage Mapping**: 15+ components usage patterns documented
- [x] **Migration Infrastructure Assessment**: Existing service validated
### Phase 2: Design & Implementation (Medium Complexity)
- [ ] **New Table Schema Design**
- Define `active_identity` table structure
- Plan foreign key relationships to `accounts.did`
- Design migration SQL statements
- Validate against existing data patterns
- [ ] **Component Update Strategy**
- Map all 505 references for update strategy
- Plan computed property changes
- Design state synchronization approach
- Preserve existing API surface
- [ ] **Testing Infrastructure Planning**
- Unit tests for new table operations
- Integration tests for identity switching
- Migration rollback validation
- Cross-platform testing strategy
### Phase 3: Migration & Validation (Complex Complexity)
- [ ] **Migration Execution Testing**
- Test on development database
- Validate data integrity post-migration
- Measure performance impact
- Test rollback scenarios
- [ ] **Cross-Platform Validation**
- Web platform functionality
- Mobile platform functionality
- Desktop platform functionality
- Cross-platform consistency
- [ ] **User Acceptance Testing**
- Identity switching workflows
- Settings persistence
- Error handling scenarios
- Edge case validation
## Technical Requirements
### New Table Schema
```sql
-- Proposed active_identity table
CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activeDid TEXT NOT NULL,
lastUpdated TEXT NOT NULL,
FOREIGN KEY (activeDid) REFERENCES accounts(did)
);
-- Index for performance
CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid);
```
### Migration Strategy
1. **Extract activeDid**: Copy from settings table to new table
2. **Update References**: Modify components to use new table
3. **Remove Field**: Drop activeDid from settings table
4. **Validate**: Ensure data integrity and functionality
### Component Updates Required
- **PlatformServiceMixin**: Update activeDid management
- **15+ Vue Components**: Modify activeDid access patterns
- **Migration Service**: Add activeDid table migration
- **Database Utilities**: Update settings operations
## Success Criteria
### Phase 1 ✅ **ACHIEVED**
- Complete activeDid usage audit with counts
- Database schema validation with data integrity check
- Migration service health assessment
- Clear dependency map for component updates
### Phase 2
- New table schema designed and validated
- Component update strategy documented
- Testing infrastructure planned
- Migration scripts developed
### Phase 3
- Migration successfully executed
- All platforms functional
- Performance maintained or improved
- Zero data loss
## Dependencies
### Technical Dependencies
- **Existing Migration Infrastructure**: Settings migration service
- **Database Access Patterns**: PlatformServiceMixin methods
- **Component Architecture**: Vue component patterns
### Platform Dependencies
- **Cross-Platform Consistency**: Web + Mobile + Desktop
- **Testing Framework**: Playwright test suite
- **Build System**: Vite configuration for all platforms
### Testing Dependencies
- **Migration Validation**: Rollback testing
- **Integration Testing**: Cross-platform functionality
- **User Acceptance**: Identity switching workflows
## Next Steps
### Immediate Actions (Next Session)
1. **Create New Table Schema**: Design `active_identity` table structure
2. **Component Update Planning**: Map all 505 references for update strategy
3. **Migration Script Development**: Create activeDid extraction migration
### Success Metrics
- **Data Integrity**: 100% activeDid data preserved
- **Performance**: No degradation in identity switching
- **Platform Coverage**: All platforms functional
- **Testing Coverage**: Comprehensive migration validation
## References
- **Codebase Analysis**: `src/views/*.vue`, `src/utils/PlatformServiceMixin.ts`
- **Database Schema**: `src/db-sql/migration.ts`
- **Migration Service**: `src/services/indexedDBMigrationService.ts`
- **Settings Types**: `src/db/tables/settings.ts`
## Competence Hooks
- **Why this works**: Separation of concerns improves data integrity, reduces
cache drift, simplifies transaction logic
- **Common pitfalls**: Missing component updates, foreign key constraint
violations, migration rollback failures
- **Next skill**: Database schema normalization and migration planning
- **Teach-back**: "How would you ensure zero downtime during the activeDid
table migration?"
## Collaboration Hooks
- **Reviewers**: Database team for schema design, Frontend team for component
updates, QA team for testing strategy
- **Sign-off checklist**: Migration tested, rollback verified, performance
validated, component state consistent
---
**Status**: Investigation complete, ready for implementation planning
**Next Review**: 2025-08-28
**Estimated Complexity**: High (cross-platform refactoring with 505 references)

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

@@ -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: 4,
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['list'],

View File

@@ -221,10 +221,7 @@ export default class GiftedDialog extends Vue {
try {
const settings = await this.$settings();
this.apiServer = settings.apiServer || "";
// Use new façade method with legacy fallback
const retrievedActiveDid = await this.$getActiveDid();
this.activeDid = retrievedActiveDid || "";
logger.debug("[GiftedDialog] Set activeDid from new system:", this.activeDid);
this.activeDid = settings.activeDid || "";
this.allContacts = await this.$contacts();
@@ -289,9 +286,7 @@ export default class GiftedDialog extends Vue {
}
async confirm() {
logger.debug("[GiftedDialog] confirm() called with activeDid:", this.activeDid);
if (!this.activeDid) {
logger.error("[GiftedDialog] Validation failed - activeDid is empty/null:", this.activeDid);
this.safeNotify.error(
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
TIMEOUTS.SHORT,

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

@@ -175,8 +175,7 @@ export default class OfferDialog extends Vue {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Use new façade method with legacy fallback
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {

View File

@@ -270,8 +270,7 @@ export default class OnboardingDialog extends Vue {
async open(page: OnboardPage) {
this.page = page;
const settings = await this.$accountSettings();
// Use new façade method with legacy fallback
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered;
const contacts = await this.$getAllContacts();

View File

@@ -20,6 +20,7 @@ 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],
@@ -44,26 +45,49 @@ 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();
logger.debug("[TopMessage] 📊 Settings loaded:", {
activeDid: settings.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);
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);
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

@@ -1,52 +0,0 @@
/**
* Feature Flags Configuration
*
* Controls the rollout of new features and migrations
*
* @author Matthew Raymer
* @date 2025-08-21
*/
export const FLAGS = {
/**
* When true, disallow legacy fallback reads from settings.activeDid
* Set to true after all components are migrated to the new façade
*/
USE_ACTIVE_IDENTITY_ONLY: false,
/**
* Controls Phase C column removal from settings table
* Set to true when ready to drop the legacy activeDid column
*
* ✅ ENABLED: Migration 004 has dropped the activeDid column (2025-08-22T10:30Z)
*/
DROP_SETTINGS_ACTIVEDID: true,
/**
* Log warnings when dual-read falls back to legacy settings.activeDid
* Useful for monitoring migration progress
*/
LOG_ACTIVE_ID_FALLBACK: process.env.NODE_ENV === "development",
/**
* Enable the new active_identity table and migration
* Set to true to start the migration process
*/
ENABLE_ACTIVE_IDENTITY_MIGRATION: true,
};
/**
* Get feature flag value with type safety
*/
export function getFlag<K extends keyof typeof FLAGS>(
key: K,
): (typeof FLAGS)[K] {
return FLAGS[key];
}
/**
* Check if a feature flag is enabled
*/
export function isFlagEnabled<K extends keyof typeof FLAGS>(key: K): boolean {
return Boolean(FLAGS[key]);
}

View File

@@ -124,131 +124,6 @@ const MIGRATIONS = [
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
`,
},
{
name: "003_active_identity_table_separation",
sql: `
-- Create active_identity table with proper constraints
CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
active_did TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
CONSTRAINT fk_active_identity_account FOREIGN KEY (active_did)
REFERENCES accounts(did) ON UPDATE CASCADE ON DELETE RESTRICT
);
-- Create index for performance
CREATE INDEX IF NOT EXISTS idx_active_identity_active_did ON active_identity(active_did);
-- Seed from existing settings.activeDid if valid
INSERT INTO active_identity (active_did)
SELECT s.activeDid
FROM settings s
WHERE s.activeDid IS NOT NULL
AND EXISTS (SELECT 1 FROM accounts a WHERE a.did = s.activeDid)
AND s.id = 1;
-- Fallback: choose first known account if still empty
INSERT INTO active_identity (active_did)
SELECT a.did
FROM accounts a
WHERE NOT EXISTS (SELECT 1 FROM active_identity ai)
LIMIT 1;
-- Create one-way mirroring trigger (settings.activeDid → active_identity.active_did)
DROP TRIGGER IF EXISTS trg_settings_activeDid_to_active_identity;
CREATE TRIGGER trg_settings_activeDid_to_active_identity
AFTER UPDATE OF activeDid ON settings
FOR EACH ROW
WHEN NEW.activeDid IS NOT OLD.activeDid AND NEW.activeDid IS NOT NULL
BEGIN
UPDATE active_identity
SET active_did = NEW.activeDid,
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id = 1;
INSERT INTO active_identity (id, active_did, updated_at)
SELECT 1, NEW.activeDid, strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE NOT EXISTS (
SELECT 1 FROM active_identity ai WHERE ai.id = 1
);
END;
`,
},
// Migration 004 re-enabled - Phase 1 complete, critical components migrated
{
name: "004_drop_settings_activeDid_column",
sql: `
-- Phase C: Remove activeDid column from settings table
-- Note: SQLite requires table rebuild for column removal
-- Create new settings table without activeDid column
CREATE TABLE settings_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
accountDid TEXT,
-- activeDid intentionally omitted
apiServer TEXT,
filterFeedByNearby BOOLEAN,
filterFeedByVisible BOOLEAN,
finishedOnboarding BOOLEAN,
firstName TEXT,
hideRegisterPromptOnNewContact BOOLEAN,
isRegistered BOOLEAN,
lastName TEXT,
lastAckedOfferToUserJwtId TEXT,
lastAckedOfferToUserProjectsJwtId TEXT,
lastNotifiedClaimId TEXT,
lastViewedClaimId TEXT,
notifyingNewActivityTime TEXT,
notifyingReminderMessage TEXT,
notifyingReminderTime TEXT,
partnerApiServer TEXT,
passkeyExpirationMinutes INTEGER,
profileImageUrl TEXT,
searchBoxes TEXT,
showContactGivesInline BOOLEAN,
showGeneralAdvanced BOOLEAN,
showShortcutBvc BOOLEAN,
vapid TEXT,
warnIfProdServer BOOLEAN,
warnIfTestServer BOOLEAN,
webPushServer TEXT
);
-- Copy data from old table (excluding activeDid)
INSERT INTO settings_new (
id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible,
finishedOnboarding, firstName, hideRegisterPromptOnNewContact,
isRegistered, lastName, lastAckedOfferToUserJwtId,
lastAckedOfferToUserProjectsJwtId, lastNotifiedClaimId,
lastViewedClaimId, notifyingNewActivityTime, notifyingReminderMessage,
notifyingReminderTime, partnerApiServer, passkeyExpirationMinutes,
profileImageUrl, searchBoxes, showContactGivesInline,
showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer,
warnIfTestServer, webPushServer
)
SELECT
id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible,
finishedOnboarding, firstName, hideRegisterPromptOnNewContact,
isRegistered, lastName, lastAckedOfferToUserJwtId,
lastAckedOfferToUserProjectsJwtId, lastNotifiedClaimId,
lastViewedClaimId, notifyingNewActivityTime, notifyingReminderMessage,
notifyingReminderTime, partnerApiServer, passkeyExpirationMinutes,
profileImageUrl, searchBoxes, showContactGivesInline,
showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer,
warnIfTestServer, webPushServer
FROM settings;
-- Drop old table and rename new one
DROP TABLE settings;
ALTER TABLE settings_new RENAME TO settings;
-- Recreate indexes
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
-- Drop the mirroring trigger (no longer needed)
DROP TRIGGER IF EXISTS trg_settings_activeDid_to_active_identity;
`,
},
];
/**

View File

@@ -1,61 +0,0 @@
/**
* Active Identity Table Definition
*
* Manages the currently active identity/DID for the application.
* Replaces the activeDid field from the settings table to improve
* data normalization and reduce cache drift.
*
* @author Matthew Raymer
* @date 2025-08-21
*/
/**
* Active Identity record structure
*/
export interface ActiveIdentity {
/** Primary key */
id?: number;
/** The currently active DID - foreign key to accounts.did */
active_did: string;
/** Last update timestamp in ISO format */
updated_at?: string;
}
/**
* Database schema for the active_identity table
*/
export const ActiveIdentitySchema = {
active_identity: "++id, active_did, updated_at",
};
/**
* Default values for ActiveIdentity records
*/
export const ActiveIdentityDefaults = {
updated_at: new Date().toISOString(),
};
/**
* Validation function for DID format
*/
export function isValidDid(did: string): boolean {
return typeof did === "string" && did.length > 0;
}
/**
* Create a new ActiveIdentity record
*/
export function createActiveIdentity(
activeDid: string,
): ActiveIdentity {
if (!isValidDid(activeDid)) {
throw new Error(`Invalid DID format: ${activeDid}`);
}
return {
active_did: activeDid,
updated_at: new Date().toISOString(),
};
}

View File

@@ -486,6 +486,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 +514,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;
}
/**
@@ -1019,19 +1128,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.info("[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.info("[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
@@ -1494,14 +1666,56 @@ export async function fetchEndorserRateLimits(
) {
const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(issuerDid);
// 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(),
});
try {
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;
} catch (error) {
// Enhanced error logging with user registration context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
const errorCode = axiosError.response?.data?.error?.code;
const errorMessage = axiosError.response?.data?.error?.message;
const httpStatus = axiosError.response?.status;
logger.warn("[User Registration] User not registered on server:", {
did: issuerDid,
server: apiServer,
errorCode: errorCode,
errorMessage: errorMessage,
httpStatus: httpStatus,
needsRegistration: true,
timestamp: new Date().toISOString(),
});
// Log the original error for debugging
logger.error(
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
errorStringForLog(error),
);
throw error;
}
}
@@ -1514,8 +1728,53 @@ 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,
) {
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:", {
did: issuerDid,
server: server,
errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(),
});
throw error;
}
}

View File

@@ -656,38 +656,7 @@ export async function saveNewIdentity(
];
await platformService.dbExec(sql, params);
// Set the new identity as active using Active Identity façade
// Check if we need to avoid legacy settings table (Phase C)
const FLAGS = await import("@/config/featureFlags");
if (!FLAGS.FLAGS.DROP_SETTINGS_ACTIVEDID) {
// Phase A/B: Update legacy settings table
await platformService.updateDefaultSettings({ activeDid: identity.did });
}
// Always update/insert into new active_identity table
const DEFAULT_SCOPE = "default";
const existingRecord = await platformService.dbQuery(
"SELECT id FROM active_identity WHERE scope = ? LIMIT 1",
[DEFAULT_SCOPE],
);
if (existingRecord?.values?.length) {
// Update existing record
await platformService.dbExec(
`UPDATE active_identity
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE scope = ?`,
[identity.did, DEFAULT_SCOPE],
);
} else {
// Insert new record
await platformService.dbExec(
`INSERT INTO active_identity (scope, active_did, updated_at)
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[DEFAULT_SCOPE, identity.did],
);
}
await platformService.updateDefaultSettings({ activeDid: identity.did });
await platformService.insertNewDidIntoSettings(identity.did);
} catch (error) {
@@ -746,38 +715,7 @@ export const registerSaveAndActivatePasskey = async (
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
const platformService = await getPlatformService();
// Set the new account as active using Active Identity façade
// Check if we need to avoid legacy settings table (Phase C)
const FLAGS = await import("@/config/featureFlags");
if (!FLAGS.FLAGS.DROP_SETTINGS_ACTIVEDID) {
// Phase A/B: Update legacy settings table
await platformService.updateDefaultSettings({ activeDid: account.did });
}
// Always update/insert into new active_identity table
const existingRecord = await platformService.dbQuery(
"SELECT id FROM active_identity LIMIT 1",
);
if (existingRecord?.values?.length) {
// Update existing record
await platformService.dbExec(
`UPDATE active_identity
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id = ?`,
[account.did, existingRecord.values[0][0]],
);
} else {
// Insert new record
await platformService.dbExec(
`INSERT INTO active_identity (active_did, updated_at)
VALUES (?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[account.did],
);
}
await platformService.updateDefaultSettings({ activeDid: account.did });
await platformService.updateDidSpecificSettings(account.did, {
isRegistered: false,
});
@@ -1036,13 +974,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) {
@@ -1068,7 +1009,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],

View File

@@ -13,6 +13,15 @@ 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`);

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,7 +382,7 @@ 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`,
);
}
@@ -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

@@ -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

@@ -49,8 +49,6 @@ import {
type Settings,
type SettingsWithJsonStrings,
} from "@/db/tables/settings";
import { type ActiveIdentity } from "@/db/tables/activeIdentity";
import { FLAGS } from "@/config/featureFlags";
import { logger } from "@/utils/logger";
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
import { Account } from "@/db/tables/accounts";
@@ -439,17 +437,17 @@ export const PlatformServiceMixin = {
},
/**
* Utility method for retrieving and parsing settings
* 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 {
// Master settings: query by id
const result = await this.$dbQuery(
"SELECT * FROM settings WHERE id = ? OR accountDid = ?",
[key, key],
"SELECT * FROM settings WHERE id = ?",
[MASTER_SETTINGS_KEY],
);
if (!result?.values?.length) {
@@ -474,8 +472,7 @@ export const PlatformServiceMixin = {
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;
@@ -493,10 +490,7 @@ export const PlatformServiceMixin = {
): 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) {
@@ -759,19 +753,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: Remove forced override - respect user preferences
// Only set default if no user preference exists
if (!settings.apiServer && process.env.VITE_PLATFORM === "electron") {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
// Only set if user hasn't specified a preference
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
@@ -791,10 +786,7 @@ 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;
@@ -815,14 +807,17 @@ export const PlatformServiceMixin = {
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") {
// FIXED: Remove forced override - respect user preferences
// Only set default if no user preference exists
if (
!mergedSettings.apiServer &&
process.env.VITE_PLATFORM === "electron"
) {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
// Only set if user hasn't specified a preference
mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
@@ -966,156 +961,6 @@ export const PlatformServiceMixin = {
return await this.$saveUserSettings(currentDid, changes);
},
// =================================================
// ACTIVE IDENTITY METHODS (New table separation)
// =================================================
/**
* Get the current active DID from the active_identity table
* Falls back to legacy settings.activeDid during Phase A transition
*
* @returns Promise<string | null> The active DID or null if not found
*/
async $getActiveDid(): Promise<string | null> {
try {
logger.debug("[ActiveDid] Getting activeDid");
// Try new active_identity table first
const row = await this.$first<ActiveIdentity>(
"SELECT active_did FROM active_identity LIMIT 1",
);
logger.debug("[ActiveDid] New system result:", row?.active_did || "null");
if (row?.active_did) {
logger.debug("[ActiveDid] Using new system value:", row.active_did);
return row.active_did;
}
// Fallback to legacy settings.activeDid during Phase A/B (unless Phase C is complete)
if (!FLAGS.DROP_SETTINGS_ACTIVEDID) {
if (FLAGS.LOG_ACTIVE_ID_FALLBACK) {
logger.warn("[ActiveDid] Fallback to legacy settings.activeDid");
}
const legacy = await this.$first<Settings>(
"SELECT activeDid FROM settings WHERE id = ? LIMIT 1",
[MASTER_SETTINGS_KEY],
);
logger.debug("[ActiveDid] Legacy fallback result:", legacy?.activeDid || "null");
return legacy?.activeDid || null;
}
logger.debug("[ActiveDid] No fallback available, returning null");
// Log current database state for debugging
try {
const activeIdentityCount = await this.$first<{ count: number }>(
"SELECT COUNT(*) as count FROM active_identity",
);
logger.debug("[ActiveDid] Active identity records:", activeIdentityCount?.count || 0);
} catch (error) {
logger.debug("[ActiveDid] Could not count active identity records:", error);
}
return null;
} catch (error) {
logger.error("[ActiveDid] Error getting activeDid:", error);
// Fallback to legacy settings.activeDid during Phase A/B
if (!FLAGS.DROP_SETTINGS_ACTIVEDID) {
try {
const legacy = await this.$first<Settings>(
"SELECT activeDid FROM settings WHERE id = ? LIMIT 1",
[MASTER_SETTINGS_KEY],
);
return legacy?.activeDid || null;
} catch (fallbackError) {
logger.error("[ActiveDid] Legacy fallback also failed:", fallbackError);
return null;
}
}
return null;
}
},
/**
* Set the active DID in the active_identity table
* Also updates legacy settings.activeDid during Phase A/B transition
*
* @param did The DID to set as active
* @returns Promise<void>
*/
async $setActiveDid(did: string | null): Promise<void> {
try {
if (!did) {
logger.warn("[ActiveDid] Attempting to set null activeDid - this may cause issues");
}
logger.debug("[ActiveDid] Setting activeDid to:", did);
// Update/insert into new active_identity table
const existingRecord = await this.$first<ActiveIdentity>(
"SELECT id FROM active_identity LIMIT 1",
);
if (existingRecord?.id) {
// Update existing record
await this.$exec(
`UPDATE active_identity
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id = ?`,
[did, existingRecord.id],
);
logger.debug("[ActiveDid] Updated existing record");
} else {
// Insert new record
await this.$exec(
`INSERT INTO active_identity (active_did, updated_at)
VALUES (?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[did],
);
logger.debug("[ActiveDid] Inserted new record");
}
// Legacy fallback - update settings.activeDid during Phase A/B
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
await this.$exec(
"UPDATE settings SET activeDid = ? WHERE id = ?",
[did, MASTER_SETTINGS_KEY],
);
logger.debug("[ActiveDid] Updated legacy settings.activeDid");
}
logger.debug("[ActiveDid] Successfully set activeDid to:", did);
} catch (error) {
logger.error("[ActiveDid] Error setting activeDid:", error);
throw error;
}
},
/**
* Switch to a different active identity
* Convenience method that validates and sets the new active DID
*
* @param did The DID to switch to
* @returns Promise<void>
*/
async $switchActiveIdentity(did: string): Promise<void> {
await this.$setActiveDid(did);
},
/**
* Get all available identity scopes (simplified to single scope)
* @returns Promise<string[]> Array containing only 'default' scope
*/
async $getActiveIdentityScopes(): Promise<string[]> {
// Simplified to single scope since we removed multi-scope support
return ["default"];
},
// =================================================
// CACHE MANAGEMENT METHODS
// =================================================
@@ -1726,10 +1571,7 @@ export const PlatformServiceMixin = {
async $debugMergedSettings(did: string): Promise<void> {
try {
// Get default settings
const defaultSettings = await this.$getSettings(
MASTER_SETTINGS_KEY,
{},
);
const defaultSettings = await this.$getMasterSettings({});
logger.info(
`[PlatformServiceMixin] Default settings:`,
defaultSettings,
@@ -1776,10 +1618,7 @@ export interface IPlatformServiceMixin {
): 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>;
$getMasterSettings(fallback?: Settings | null): Promise<Settings | null>;
$getMergedSettings(
defaultKey: string,
accountDid?: string,
@@ -1860,12 +1699,6 @@ export interface IPlatformServiceMixin {
// Debug methods
$debugDidSettings(did: string): Promise<Settings | null>;
$debugMergedSettings(did: string): Promise<void>;
// Active Identity façade methods
$getActiveDid(): Promise<string | null>;
$setActiveDid(did: string | null): Promise<void>;
$switchActiveIdentity(did: string): Promise<void>;
$getActiveIdentityScopes(): Promise<string[]>;
}
// TypeScript declaration merging to eliminate (this as any) type assertions
@@ -1882,12 +1715,6 @@ declare module "@vue/runtime-core" {
currentActiveDid: string | null;
$updateActiveDid(newDid: string | null): Promise<void>;
// Active Identity façade methods
$getActiveDid(): Promise<string | null>;
$setActiveDid(did: string | null): Promise<void>;
$switchActiveIdentity(did: string): Promise<void>;
$getActiveIdentityScopes(): Promise<string[]>;
// Ultra-concise database methods (shortest possible names)
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
@@ -1913,10 +1740,7 @@ declare module "@vue/runtime-core" {
sql: string,
params?: unknown[],
): Promise<unknown[] | undefined>;
$getSettings(
key: string,
defaults?: Settings | null,
): Promise<Settings | null>;
$getMasterSettings(defaults?: Settings | null): Promise<Settings | null>;
$getMergedSettings(
key: string,
did?: string,

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

@@ -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

@@ -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 -->
@@ -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";
@@ -813,11 +816,13 @@ import {
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 +921,6 @@ export default class AccountViewView extends Vue {
imageLimits: ImageRateLimits | null = null;
limitsMessage: string = "";
private profileService!: ProfileService;
private notify!: ReturnType<typeof createNotifyHelpers>;
created() {
@@ -926,7 +930,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 +955,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,8 +1050,7 @@ export default class AccountViewView extends Vue {
// Then get the account-specific settings
const settings: AccountSettings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.apiServerInput = settings.apiServer || "";
this.givenName =
@@ -1412,21 +1422,24 @@ 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) {
this.imageLimits = imageResp.data;
@@ -1452,7 +1465,26 @@ export default class AccountViewView extends Vue {
} 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,
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;
@@ -1460,24 +1492,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> {
@@ -1551,7 +1629,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;
@@ -1580,19 +1657,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
}
@@ -1616,7 +1689,7 @@ 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,
);
@@ -1635,7 +1708,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,
@@ -1680,8 +1753,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 = "";
@@ -1689,7 +1761,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);
}
@@ -1735,7 +1806,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;
@@ -1744,7 +1814,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
@@ -1797,5 +1866,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(),
});
throw new Error("Failed to load profile");
}
}
/**
* 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

@@ -112,8 +112,7 @@ export default class ClaimAddRawView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
// Use new façade method with legacy fallback
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}

View File

@@ -40,8 +40,7 @@ export default class ClaimCertificateView extends Vue {
async created() {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,

View File

@@ -54,8 +54,7 @@ export default class ClaimReportCertificateView extends Vue {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$settings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,

View File

@@ -728,8 +728,7 @@ export default class ClaimView extends Vue {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await this.$contacts();

View File

@@ -547,8 +547,7 @@ export default class ConfirmGiftView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await this.$getAllContacts();
this.isRegistered = settings.isRegistered || false;

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,9 +223,8 @@ export default class ContactAmountssView extends Vue {
const contact = await this.$getContact(contactDid);
this.contact = contact;
const settings = await this.$getSettings(MASTER_SETTINGS_KEY);
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
const settings = await this.$getMasterSettings();
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) {

View File

@@ -164,8 +164,7 @@ export default class ContactGiftingView extends Vue {
try {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.allContacts = await this.$getAllContacts();

View File

@@ -340,8 +340,7 @@ export default class ContactImportView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}

View File

@@ -265,8 +265,7 @@ export default class ContactQRScanFull extends Vue {
async created() {
try {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.isRegistered = !!settings.isRegistered;

View File

@@ -286,8 +286,7 @@ export default class ContactQRScanShow extends Vue {
try {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.hideRegisterPromptOnNewContact =

View File

@@ -294,8 +294,7 @@ export default class ContactsView extends Vue {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;

View File

@@ -376,8 +376,7 @@ export default class DIDView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}

View File

@@ -415,8 +415,7 @@ export default class DiscoverView extends Vue {
const searchPeople = !!this.$route.query["searchPeople"];
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = (settings.activeDid as string) || "";
this.apiServer = (settings.apiServer as string) || "";
this.partnerApiServer =
(settings.partnerApiServer as string) || this.partnerApiServer;

View File

@@ -441,8 +441,7 @@ export default class GiftedDetails extends Vue {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
if (
(this.giverDid && !this.giverName) ||

View File

@@ -688,9 +688,7 @@ export default class HelpView extends Vue {
try {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
const activeDid = await this.$getActiveDid();
if (activeDid) {
if (settings.activeDid) {
await this.$updateSettings({
...settings,
finishedOnboarding: false,
@@ -698,7 +696,7 @@ export default class HelpView extends Vue {
this.$log(
"[HelpView] Onboarding reset successfully for DID: " +
activeDid,
settings.activeDid,
);
}

View File

@@ -431,7 +431,6 @@ export default class HomeView extends Vue {
* Called automatically by Vue lifecycle system
*/
async mounted() {
logger.debug("[HomeView] mounted() starting");
try {
await this.initializeIdentity();
// Settings already loaded in initializeIdentity()
@@ -472,7 +471,6 @@ export default class HomeView extends Vue {
* @throws Logs error if DID retrieval fails
*/
private async initializeIdentity() {
logger.debug("[HomeView] initializeIdentity() starting");
try {
// Retrieve DIDs with better error handling
try {
@@ -517,11 +515,7 @@ export default class HomeView extends Vue {
// **CRITICAL**: Ensure correct API server for platform
await this.ensureCorrectApiServer();
// Use new façade method with legacy fallback
const retrievedActiveDid = await this.$getActiveDid();
logger.debug("[HomeView] Retrieved activeDid:", retrievedActiveDid);
this.activeDid = retrievedActiveDid || "";
logger.debug("[HomeView] Set activeDid to:", this.activeDid);
this.activeDid = settings.activeDid || "";
// Load contacts with graceful fallback
try {
@@ -574,10 +568,27 @@ export default class HomeView extends Vue {
this.isRegistered = true;
}
} catch (error) {
// Enhanced error logging with server context
const errorMessage =
error instanceof Error ? error.message : String(error);
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
logger.warn(
"[HomeView Settings Trace] ⚠️ Registration check failed",
{
error: error instanceof Error ? error.message : String(error),
error: errorMessage,
did: this.activeDid,
server: this.apiServer,
errorCode: axiosError?.response?.data?.error?.code,
errorMessage: axiosError?.response?.data?.error?.message,
httpStatus: axiosError?.response?.status,
needsUserMigration: true,
timestamp: new Date().toISOString(),
},
);
}
@@ -591,8 +602,7 @@ export default class HomeView extends Vue {
}
/**
* Ensures API server is correctly set for the current platform
* For Electron, always use production endpoint regardless of saved settings
* Ensures correct API server configuration
*
* @internal
* Called after loading settings to ensure correct API endpoint
@@ -600,12 +610,9 @@ export default class HomeView extends Vue {
private async ensureCorrectApiServer() {
const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app");
if (process.env.VITE_PLATFORM === "electron") {
// **CRITICAL FIX**: Always use production API server for Electron
// This prevents the capacitor-electron:// protocol from being used for API calls
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
} else if (!this.apiServer) {
// **FIX**: Set default API server for web/development if not already set
// Only set default if no user preference exists
if (!this.apiServer) {
// Set default API server for any platform if not already set
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
}
@@ -1167,9 +1174,13 @@ export default class HomeView extends Vue {
location: fulfillsPlan
? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon }
: null,
inSearchBox: fulfillsPlan
? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon)
: null,
inSearchBox:
fulfillsPlan?.locLat && fulfillsPlan?.locLon
? this.latLongInAnySearchBox(
fulfillsPlan.locLat,
fulfillsPlan.locLon,
)
: null,
finalResult: anyMatch,
});
}

View File

@@ -46,7 +46,7 @@
<div class="flex items-center justify-between mb-2">
<div
:class="identityListItemClasses"
@click="switchIdentity(ident.did)"
@click="switchAccount(ident.did)"
>
<font-awesome
v-if="ident.did === activeDid"
@@ -94,7 +94,7 @@
<a
href="#"
:class="secondaryButtonClasses"
@click="switchIdentity(undefined)"
@click="switchAccount(undefined)"
>
No Identity
</a>
@@ -116,7 +116,6 @@ import {
NOTIFY_DELETE_IDENTITY_CONFIRM,
} from "@/constants/notifications";
import { Account } from "@/db/tables/accounts";
import { FLAGS } from "@/config/featureFlags";
@Component({
components: { QuickNav },
@@ -201,8 +200,7 @@ export default class IdentitySwitcherView extends Vue {
async created() {
try {
const settings = await this.$accountSettings();
// Use new façade method with legacy fallback
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.apiServerInput = settings.apiServer || "";
@@ -223,63 +221,46 @@ export default class IdentitySwitcherView extends Vue {
}
}
async switchIdentity(did?: string) {
try {
if (did) {
// Use new façade method instead of legacy settings
await this.$setActiveDid(did);
async switchAccount(did?: string) {
// Save the new active DID to master settings
await this.$saveSettings({ activeDid: did });
// Update local state
this.activeDid = did;
// Legacy fallback - remove after Phase C
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
await this.$saveSettings({ activeDid: did });
}
// Check if we need to load user-specific settings for the new DID
try {
const newSettings = await this.$accountSettings(did);
logger.info(
"[IdentitySwitcher Settings Trace] ✅ New account settings loaded",
{
did,
settingsKeys: Object.keys(newSettings).filter(
(k) =>
k in newSettings &&
newSettings[k as keyof typeof newSettings] !== undefined,
),
},
);
} catch (error) {
logger.warn(
"[IdentitySwitcher Settings Trace] ⚠️ Error loading new account settings",
{
did,
error: error instanceof Error ? error.message : String(error),
},
);
// Handle error silently - user settings will be loaded when needed
}
} else {
// Handle "No Identity" case
this.activeDid = "";
// Note: We don't clear active DID in database for safety
// Check if we need to load user-specific settings for the new DID
if (did) {
try {
const newSettings = await this.$accountSettings(did);
logger.debug(
"[IdentitySwitcher Settings Trace] ✅ New account settings loaded",
{
did,
settingsKeys: Object.keys(newSettings).filter(
(k) =>
k in newSettings &&
newSettings[k as keyof typeof newSettings] !== undefined,
),
},
);
} catch (error) {
logger.warn(
"[IdentitySwitcher Settings Trace] ⚠️ Error loading new account settings",
{
did,
error: error instanceof Error ? error.message : String(error),
},
);
// Handle error silently - user settings will be loaded when needed
}
logger.info(
"[IdentitySwitcher Settings Trace] 🔄 Navigating to home to trigger watcher",
{
newDid: did,
},
);
// Navigate to home page to trigger the watcher
this.$router.push({ name: "home" });
} catch (error) {
logger.error("[IdentitySwitcher] Error switching identity", error);
this.notify.error("Error switching identity", TIMEOUTS.SHORT);
}
logger.debug(
"[IdentitySwitcher Settings Trace] 🔄 Navigating to home to trigger watcher",
{
newDid: did,
},
);
// Navigate to home page to trigger the watcher
this.$router.push({ name: "home" });
}
async deleteAccount(id: string) {

View File

@@ -207,12 +207,11 @@ export default class ImportAccountView extends Vue {
// Check what was actually imported
const settings = await this.$accountSettings();
// Check account-specific settings using Active Identity façade
const activeDid = await this.$getActiveDid();
if (activeDid) {
// Check account-specific settings
if (settings?.activeDid) {
try {
await this.$query("SELECT * FROM settings WHERE accountDid = ?", [
activeDid,
settings.activeDid,
]);
} catch (error) {
// Log error but don't interrupt import flow

View File

@@ -173,13 +173,8 @@ export default class ImportAccountView extends Vue {
try {
await saveNewIdentity(newId, mne, newDerivPath);
// record that as the active DID using new façade
await this.$setActiveDid(newId.did);
// Legacy fallback - remove after Phase C
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
await this.$saveSettings({ activeDid: newId.did });
}
// record that as the active DID
await this.$saveSettings({ activeDid: newId.did });
await this.$saveUserSettings(newId.did, {
isRegistered: false,
});

View File

@@ -46,7 +46,6 @@ import { APP_SERVER } from "../constants/app";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import { errorStringForLog } from "../libs/endorserServer";
import { generateSaveAndActivateIdentity } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers } from "@/utils/notify";
import {
@@ -121,8 +120,7 @@ export default class InviteOneAcceptView extends Vue {
// Load or generate identity
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
// Identity creation should be handled by router guard, but keep as fallback for deep links

View File

@@ -283,8 +283,7 @@ export default class InviteOneView extends Vue {
try {
// Use PlatformServiceMixin for account settings
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;

View File

@@ -202,8 +202,7 @@ export default class NewActivityView extends Vue {
try {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || "";

View File

@@ -378,8 +378,7 @@ export default class NewEditProjectView extends Vue {
this.numAccounts = await retrieveAccountCount();
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;

View File

@@ -433,8 +433,7 @@ export default class OfferDetailsView extends Vue {
private async loadAccountSettings() {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer ?? "";
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) ?? "";
this.activeDid = settings.activeDid ?? "";
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
}

View File

@@ -174,8 +174,7 @@ export default class OnboardMeetingListView extends Vue {
// Load user account settings
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered;

View File

@@ -106,8 +106,7 @@ export default class OnboardMeetingMembersView extends Vue {
return;
}
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered;

View File

@@ -349,8 +349,7 @@ export default class OnboardMeetingView extends Vue {
this.$notify as Parameters<typeof createNotifyHelpers>[0],
);
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.fullName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered;

View File

@@ -226,7 +226,7 @@
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<!-- First, offers on the left-->
<div class="bg-slate-100 px-4 py-3 rounded-md">
<div v-if="activeDid && isRegistered">
<div v-if="activeDid && isRegistered" class="mb-4">
<div class="text-center">
<button
data-testId="offerButton"
@@ -243,13 +243,13 @@
:project-name="name"
/>
<h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3>
<h3 class="text-lg font-bold leading-tight mb-3">Offered To This Idea</h3>
<div v-if="offersToThis.length === 0">
(None yet. Wanna
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
>offer something... especially if others join you</span
>?)
<div v-if="offersToThis.length === 0" class="text-sm">
(None yet.<span v-if="activeDid && isRegistered"> Wanna
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
>offer something&hellip; especially if others join you</span
>?</span>)
</div>
<ul v-else class="text-sm border-t border-slate-300">
@@ -314,7 +314,7 @@
<!-- Now, gives TO this project in the middle -->
<!-- (similar to "FROM" gift display below) -->
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-to">
<div v-if="activeDid && isRegistered">
<div v-if="activeDid && isRegistered" class="mb-4">
<div class="text-center">
<button
class="block w-full 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-3 py-1.5 text-sm leading-tight rounded-md"
@@ -325,7 +325,7 @@
</div>
</div>
<h3 class="text-lg font-bold mt-4">Given To This Project</h3>
<h3 class="text-lg font-bold leading-tight mb-3">Given To This Project</h3>
<div v-if="givesToThis.length === 0" class="text-sm">
(None yet. If you've seen something, say something by clicking a
@@ -476,7 +476,7 @@
<!-- Finally, gives FROM this project on the right -->
<!-- (similar to "TO" gift display above) -->
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-from">
<div v-if="activeDid && isRegistered">
<div v-if="activeDid && isRegistered" class="mb-4">
<div class="text-center">
<button
class="block w-full 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-3 py-1.5 text-sm leading-tight rounded-md"
@@ -494,11 +494,11 @@
:is-from-project-view="true"
/>
<h3 class="text-lg font-bold mb-3 mt-4">
<h3 class="text-lg font-bold leading-tight mb-3">
Benefitted From This Project
</h3>
<div v-if="givesProvidedByThis.length === 0">(None yet.)</div>
<div v-if="givesProvidedByThis.length === 0" class="text-sm">(None yet.)</div>
<ul v-else class="text-sm border-t border-slate-300">
<li
@@ -770,8 +770,7 @@ export default class ProjectViewView extends Vue {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await this.$getAllContacts();
this.isRegistered = !!settings.isRegistered;

View File

@@ -391,8 +391,7 @@ export default class ProjectsView extends Vue {
*/
private async initializeUserSettings() {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
this.givenName = settings.firstName || "";

View File

@@ -150,8 +150,7 @@ export default class QuickActionBvcBeginView extends Vue {
// Get account settings using PlatformServiceMixin
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
const activeDid = (await this.$getActiveDid()) || "";
const activeDid = settings.activeDid || "";
const apiServer = settings.apiServer || "";
if (!activeDid || !apiServer) {

View File

@@ -227,8 +227,7 @@ export default class QuickActionBvcEndView extends Vue {
const settings = await this.$settings();
this.apiServer = settings.apiServer || "";
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.allContacts = await this.$contacts();

View File

@@ -124,8 +124,7 @@ export default class RecentOffersToUserView extends Vue {
try {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || "";

View File

@@ -116,8 +116,7 @@ export default class RecentOffersToUserView extends Vue {
try {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.allContacts = await this.$getAllContacts();

View File

@@ -207,8 +207,7 @@ export default class SeedBackupView extends Vue {
try {
let activeDid = "";
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
activeDid = (await this.$getActiveDid()) || "";
activeDid = settings.activeDid || "";
this.numAccounts = await retrieveAccountCount();
this.activeAccount = await retrieveFullyDecryptedAccount(activeDid);

View File

@@ -76,8 +76,7 @@ export default class ShareMyContactInfoView extends Vue {
async mounted() {
const settings = await this.$settings();
// Use new Active Identity façade instead of settings.activeDid
const activeDid = await this.$getActiveDid();
const activeDid = settings?.activeDid;
if (!activeDid) {
this.$router.push({ name: "home" });
}
@@ -117,8 +116,7 @@ export default class ShareMyContactInfoView extends Vue {
private async retrieveAccount(
settings: Settings,
): Promise<Account | undefined> {
// Use new Active Identity façade instead of settings.activeDid
const activeDid = (await this.$getActiveDid()) || "";
const activeDid = settings.activeDid || "";
if (!activeDid) {
return undefined;
}

View File

@@ -176,8 +176,7 @@ export default class SharedPhotoView extends Vue {
try {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid;
const temp = await this.$getTemp(SHARED_PHOTO_BASE64_KEY);
const imageB64 = temp?.blobB64 as string;

View File

@@ -203,7 +203,7 @@ export default class StartView extends Vue {
// Load account count for display logic
this.numAccounts = await retrieveAccountCount();
logger.info("[StartView] Component mounted", {
logger.debug("[StartView] Component mounted", {
hasGivenName: !!this.givenName,
accountCount: this.numAccounts,
passkeysEnabled: this.PASSKEYS_ENABLED,
@@ -221,7 +221,7 @@ export default class StartView extends Vue {
* Routes user to new identifier creation flow with seed-based approach
*/
public onClickNewSeed() {
logger.info("[StartView] User selected new seed generation");
logger.debug("[StartView] User selected new seed generation");
this.$router.push({ name: "new-identifier" });
}
@@ -235,14 +235,14 @@ export default class StartView extends Vue {
const keyName =
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
logger.info("[StartView] Initiating passkey registration", {
logger.debug("[StartView] Initiating passkey registration", {
keyName,
hasGivenName: !!this.givenName,
});
await registerSaveAndActivatePasskey(keyName);
logger.info("[StartView] Passkey registration successful");
logger.debug("[StartView] Passkey registration successful");
this.$router.push({ name: "account" });
} catch (error) {
logger.error("[StartView] Passkey registration failed", error);
@@ -255,7 +255,7 @@ export default class StartView extends Vue {
* Routes user to account import flow for existing seed phrase
*/
public onClickNo() {
logger.info("[StartView] User selected existing seed import");
logger.debug("[StartView] User selected existing seed import");
this.$router.push({ name: "import-account" });
}
@@ -264,7 +264,7 @@ export default class StartView extends Vue {
* Routes user to address derivation flow for existing seed
*/
public onClickDerive() {
logger.info("[StartView] User selected address derivation");
logger.debug("[StartView] User selected address derivation");
this.$router.push({ name: "import-derive" });
}
}

View File

@@ -91,13 +91,95 @@
name: 'shared-photo',
query: { fileName },
}"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-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"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
data-testId="fileUploadButton"
>
Go to Shared Page
</router-link>
</div>
<!-- URL Flow Testing Section -->
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">URL Flow Testing</h2>
<p class="text-sm text-gray-600 mb-3">
Test claim and partner server URL flow from initialization to change
propagation.
</p>
<div class="space-y-4">
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">Current URL State</h3>
<div class="space-y-2 text-sm">
<div>
<strong>API Server:</strong>
<span class="font-mono">{{ apiServer || "Not Set" }}</span>
</div>
<div>
<strong>Partner API Server:</strong>
<span class="font-mono">{{ partnerApiServer || "Not Set" }}</span>
</div>
<div>
<strong>Active DID:</strong>
<span class="font-mono">{{ activeDid || "Not Set" }}</span>
</div>
<div>
<strong>Platform:</strong>
<span class="font-mono">{{ getCurrentPlatform() }}</span>
</div>
</div>
</div>
<div class="space-y-2">
<button
:class="primaryButtonClasses"
:disabled="isUrlTestRunning"
@click="testUrlFlow()"
>
{{ isUrlTestRunning ? "Testing..." : "Test URL Flow" }}
</button>
<button :class="secondaryButtonClasses" @click="changeApiServer()">
Change API Server (Test Prod)
</button>
<button
:class="secondaryButtonClasses"
@click="changePartnerApiServer()"
>
Change Partner API Server (Test Prod)
</button>
<button :class="warningButtonClasses" @click="resetToDefaults()">
Reset to Defaults
</button>
<button :class="secondaryButtonClasses" @click="refreshSettings()">
Refresh Settings
</button>
<button
:class="secondaryButtonClasses"
@click="logEnvironmentState()"
>
Log Environment State
</button>
</div>
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">URL Flow Test Results</h3>
<div class="max-h-64 overflow-y-auto space-y-2">
<div
v-for="(result, index) in urlTestResults"
:key="index"
class="p-2 border border-gray-200 rounded text-xs font-mono bg-white"
>
{{ result }}
</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results.
@@ -326,6 +408,11 @@ export default class Help extends Vue {
showEntityGridTest = false;
showPlatformServiceTest = false;
// for URL flow testing
isUrlTestRunning = false;
urlTestResults: string[] = [];
partnerApiServer: string | undefined;
/**
* Computed properties for template streamlining
* Eliminates repeated classes and logic in template
@@ -534,25 +621,93 @@ export default class Help extends Vue {
}
/**
* Component initialization
*
* Loads user settings and account information for testing interface
* Uses PlatformServiceMixin for database access
*/
async mounted() {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.apiServer = settings.apiServer || "";
this.userName = settings.firstName;
logger.info(
"[TestView] 🚀 Component mounting - starting URL flow tracking",
);
const account = await retrieveAccountMetadata(this.activeDid);
if (this.activeDid) {
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
} else {
alert("No account found for DID " + this.activeDid);
// Boot-time logging for initial configuration
logger.info("[TestView] 🌍 Boot-time configuration detected:", {
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(),
});
try {
// Track settings loading
logger.info("[TestView] 📥 Loading account settings...");
const settings = await this.$accountSettings();
logger.info("[TestView] 📊 Settings loaded:", {
activeDid: settings.activeDid,
apiServer: settings.apiServer,
partnerApiServer: settings.partnerApiServer,
isRegistered: settings.isRegistered,
firstName: settings.firstName,
});
// Update component state
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
this.userName = settings.firstName;
logger.info("[TestView] ✅ Component state updated:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
// Load account metadata
if (this.activeDid) {
logger.info(
"[TestView] 🔍 Loading account metadata for DID:",
this.activeDid,
);
const account = await retrieveAccountMetadata(this.activeDid);
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
logger.info("[TestView] ✅ Account metadata loaded:", {
did: account.did,
hasPasskey: !!account.passkeyCredIdHex,
passkeyId: account.passkeyCredIdHex,
});
} else {
logger.warn(
"[TestView] ⚠️ No account found for DID:",
this.activeDid,
);
alert("No account found for DID " + this.activeDid);
}
}
logger.info("[TestView] 🎯 Component initialization complete:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
hasPasskey: !!this.credIdHex,
platform: this.getCurrentPlatform(),
});
} catch (error) {
logger.error(
"[TestView] ❌ Error during component initialization:",
error,
);
this.$notify(
{
group: "error",
type: "error",
title: "Initialization Error",
text: `Failed to initialize component: ${error instanceof Error ? error.message : String(error)}`,
},
5000,
);
}
}
@@ -825,5 +980,276 @@ export default class Help extends Vue {
);
}
}
/**
* Tests the URL flow from initialization to change propagation.
* This simulates the flow where a user's DID is set, and then the
* claim and partner server URLs are updated.
*/
public async testUrlFlow() {
this.isUrlTestRunning = true;
this.urlTestResults = [];
try {
logger.info("[TestView] 🔬 Starting comprehensive URL flow test");
this.addUrlTestResult("🚀 Starting URL flow test...");
// Test 1: Current state
this.addUrlTestResult(`📊 Current State:`);
this.addUrlTestResult(` - API Server: ${this.apiServer || "Not Set"}`);
this.addUrlTestResult(
` - Partner API Server: ${this.partnerApiServer || "Not Set"}`,
);
this.addUrlTestResult(` - Active DID: ${this.activeDid || "Not Set"}`);
this.addUrlTestResult(` - Platform: ${this.getCurrentPlatform()}`);
// Test 2: Load fresh settings
this.addUrlTestResult(`\n📥 Testing Settings Loading:`);
const startTime = Date.now();
const settings = await this.$accountSettings();
const loadTime = Date.now() - startTime;
this.addUrlTestResult(` - Settings loaded in ${loadTime}ms`);
this.addUrlTestResult(
` - API Server from settings: ${settings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Partner API Server from settings: ${settings.partnerApiServer || "Not Set"}`,
);
// Test 3: Database query
this.addUrlTestResult(`\n💾 Testing Database Query:`);
const dbStartTime = Date.now();
const dbResult = await this.$dbQuery(
"SELECT apiServer, partnerApiServer, activeDid FROM settings WHERE id = ? OR accountDid = ?",
[1, this.activeDid || ""],
);
const dbTime = Date.now() - dbStartTime;
if (dbResult?.values) {
this.addUrlTestResult(` - Database query completed in ${dbTime}ms`);
this.addUrlTestResult(
` - Raw DB values: ${JSON.stringify(dbResult.values)}`,
);
} else {
this.addUrlTestResult(
` - Database query failed or returned no results`,
);
}
// Test 4: Environment variables
this.addUrlTestResult(`\n🌍 Testing Environment Variables:`);
this.addUrlTestResult(
` - VITE_PLATFORM: ${import.meta.env.VITE_PLATFORM || "Not Set"}`,
);
this.addUrlTestResult(
` - VITE_DEFAULT_ENDORSER_API_SERVER: ${import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER || "Not Set"}`,
);
this.addUrlTestResult(
` - VITE_DEFAULT_PARTNER_API_SERVER: ${import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER || "Not Set"}`,
);
// Test 5: Constants
this.addUrlTestResult(`\n📋 Testing App Constants:`);
this.addUrlTestResult(
` - PROD_ENDORSER_API_SERVER: ${AppString.PROD_ENDORSER_API_SERVER}`,
);
this.addUrlTestResult(
` - PROD_PARTNER_API_SERVER: ${AppString.PROD_PARTNER_API_SERVER}`,
);
// Test 6: Change detection
this.addUrlTestResult(`\n🔄 Testing Change Detection:`);
const originalApiServer = this.apiServer;
const originalPartnerServer = this.partnerApiServer;
// Simulate a change
this.addUrlTestResult(` - Original API Server: ${originalApiServer}`);
this.addUrlTestResult(
` - Original Partner Server: ${originalPartnerServer}`,
);
// Test 7: Settings update
this.addUrlTestResult(`\n💾 Testing Settings Update:`);
const testChanges = {
apiServer:
originalApiServer === "https://api.endorser.ch"
? "https://test-api.endorser.ch"
: "https://api.endorser.ch",
};
this.addUrlTestResult(
` - Attempting to change API Server to: ${testChanges.apiServer}`,
);
const updateResult = await this.$saveSettings(testChanges);
this.addUrlTestResult(
` - Update result: ${updateResult ? "Success" : "Failed"}`,
);
// Test 8: Verify change propagation
this.addUrlTestResult(`\n✅ Testing Change Propagation:`);
const newSettings = await this.$accountSettings();
this.addUrlTestResult(
` - New API Server from settings: ${newSettings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Component state API Server: ${this.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Change propagated: ${newSettings.apiServer === this.apiServer ? "Yes" : "No"}`,
);
// Test 9: Revert changes
this.addUrlTestResult(`\n🔄 Reverting Changes:`);
const revertResult = await this.$saveSettings({
apiServer: originalApiServer,
});
this.addUrlTestResult(
` - Revert result: ${revertResult ? "Success" : "Failed"}`,
);
// Test 10: Final verification
this.addUrlTestResult(`\n🎯 Final Verification:`);
const finalSettings = await this.$accountSettings();
this.addUrlTestResult(
` - Final API Server: ${finalSettings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Matches original: ${finalSettings.apiServer === originalApiServer ? "Yes" : "No"}`,
);
this.addUrlTestResult(`\n✅ URL flow test completed successfully!`);
logger.info("[TestView] ✅ URL flow test completed successfully");
} catch (error) {
const errorMsg = `❌ URL flow test failed: ${error instanceof Error ? error.message : String(error)}`;
this.addUrlTestResult(errorMsg);
logger.error("[TestView] ❌ URL flow test failed:", error);
} finally {
this.isUrlTestRunning = false;
}
}
/**
* Adds a result to the URL test results array.
*/
private addUrlTestResult(message: string) {
this.urlTestResults.push(message);
}
/**
* Changes the API server to the production URL.
*/
public changeApiServer() {
const currentServer = this.apiServer;
const newServer =
currentServer === "https://api.endorser.ch"
? "https://test-api.endorser.ch"
: "https://api.endorser.ch";
logger.info("[TestView] 🔄 Changing API server:", {
from: currentServer,
to: newServer,
});
this.apiServer = newServer;
this.addUrlTestResult(
`API Server changed from ${currentServer} to ${newServer}`,
);
}
/**
* Changes the partner API server to the production URL.
*/
public changePartnerApiServer() {
const currentServer = this.partnerApiServer;
const newServer =
currentServer === "https://partner-api.endorser.ch"
? "https://test-partner-api.endorser.ch"
: "https://partner-api.endorser.ch";
logger.info("[TestView] 🔄 Changing partner API server:", {
from: currentServer,
to: newServer,
});
this.partnerApiServer = newServer;
this.addUrlTestResult(
`Partner API Server changed from ${currentServer} to ${newServer}`,
);
}
/**
* Resets all URL-related settings to their initial values.
*/
public resetToDefaults() {
this.apiServer = AppString.TEST_ENDORSER_API_SERVER;
this.partnerApiServer = AppString.TEST_PARTNER_API_SERVER;
this.activeDid = "";
this.addUrlTestResult("URL Flow Test Results Reset to Defaults.");
}
/**
* Refreshes settings from the database to verify changes.
*/
public async refreshSettings() {
try {
logger.info("[TestView] 🔄 Refreshing settings from database");
const settings = await this.$accountSettings();
// Update component state
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
logger.info("[TestView] ✅ Settings refreshed:", {
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
this.addUrlTestResult(
`Settings refreshed - API Server: ${this.apiServer}, Partner API Server: ${this.partnerApiServer}`,
);
} catch (error) {
logger.error("[TestView] ❌ Error refreshing settings:", error);
this.addUrlTestResult(
`Error refreshing settings: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Logs the current environment state to the console.
*/
public logEnvironmentState() {
logger.info("[TestView] 🌐 Current Environment State:", {
VITE_PLATFORM: import.meta.env.VITE_PLATFORM,
VITE_DEFAULT_ENDORSER_API_SERVER: import.meta.env
.VITE_DEFAULT_ENDORSER_API_SERVER,
VITE_DEFAULT_PARTNER_API_SERVER: import.meta.env
.VITE_DEFAULT_PARTNER_API_SERVER,
NODE_ENV: process.env.NODE_ENV,
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
this.$notify({
group: "info",
type: "info",
title: "Environment State Logged",
text: "Current environment state logged to console.",
});
}
/**
* Gets the current platform based on the API server.
*/
public getCurrentPlatform(): string {
if (this.apiServer?.includes(AppString.PROD_ENDORSER_API_SERVER)) {
return "Production";
} else if (this.apiServer?.includes(AppString.TEST_ENDORSER_API_SERVER)) {
return "Test";
} else {
return "Unknown";
}
}
}
</script>

View File

@@ -183,8 +183,7 @@ export default class UserProfileView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
// Use new Active Identity façade instead of settings.activeDid
this.activeDid = (await this.$getActiveDid()) || "";
this.activeDid = settings.activeDid || "";
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
}

View File

@@ -1,150 +0,0 @@
# Active Identity Migration - Test Findings & Status
**Date**: 2025-08-22T14:00Z
**Author**: Matthew Raymer
**Status**: Investigation Complete - Ready for Test Infrastructure Fixes
## Executive Summary
**The Active Identity migration is 100% successful and functional.** All test failures are due to test infrastructure issues, not migration problems. The core identity switching functionality works perfectly.
## Test Results Summary
### ✅ **Tests That Are Working (6/14)**
1. **Advanced settings state persistence** - ✅ Working perfectly
2. **Identity switching debugging** - ✅ Working perfectly
3. **Error handling gracefully** - ✅ Working perfectly
### ❌ **Tests That Are Failing (8/14)**
- All failures are due to test infrastructure issues, not migration problems
## Key Findings
### 1. **Active Identity Migration Status: SUCCESS** 🎉
#### **What's Working Perfectly**
- **`$setActiveDid()` method** - Successfully updates the `active_identity` table
- **`$getActiveDid()` method** - Correctly retrieves the active DID
- **Database schema** - `active_identity` table properly stores and retrieves data
- **Identity switching UI** - Users can click and switch between identities
- **Navigation behavior** - Properly navigates to home page after switching
- **Component state updates** - Active user changes are reflected in the UI
#### **Migration Code Quality**
- **`switchIdentity()` method** in `IdentitySwitcherView.vue` is correctly implemented
- **Façade methods** are properly calling the new Active Identity infrastructure
- **Legacy fallbacks** are working correctly for backward compatibility
- **Error handling** is robust and graceful
### 2. **Test Infrastructure Issues: CRITICAL** ❌
#### **Problem 1: Element Selector Strategy**
- **Initial approach was completely wrong**: Tests were clicking on `<code>` elements instead of clickable `<div>` elements
- **Working selector**: `page.locator('li div').filter({ hasText: did }).first()`
- **Broken selector**: `page.locator('code:has-text("${did}")')`
#### **Problem 2: Test State Management**
- **Tests expect specific users to be active** but system starts with different users
- **User context isn't properly isolated** between test runs
- **Test setup assumptions are wrong** - expecting User Zero when User One is actually active
#### **Problem 3: Test Flow Assumptions**
- **Tests assume advanced settings stay open** after identity switching, but they close
- **Navigation behavior varies** - sometimes goes to home, sometimes doesn't
- **Component state refresh timing** is unpredictable
### 3. **Technical Architecture Insights**
#### **Scope Parameter in `$getActiveDid(scope?)`**
- **Purpose**: Supports multi-profile/multi-tenant scenarios
- **Current usage**: Most calls use default scope
- **Future potential**: Different active identities for different contexts (personal vs work)
#### **Database Structure**
- **`active_identity` table** properly stores scope, DID, and metadata
- **Migration 004** successfully dropped old `settings.activeDid` column
- **New schema** supports multiple scopes and proper DID management
## What We've Fixed
### ✅ **Resolved Issues**
1. **Element selectors** - Updated `switchToUser()` function to use correct `li div` selectors
2. **Test assertions** - Fixed tests to expect "Your Identity" instead of "Account" heading
3. **Advanced settings access** - Properly handle advanced settings expansion before accessing identity switcher
### 🔄 **Partially Fixed Issues**
1. **Test setup logic** - Removed assumption that User Zero starts active
2. **Final verification steps** - Updated to handle advanced settings state changes
## What Still Needs Fixing
### 🚧 **Remaining Test Issues**
1. **Test isolation** - Ensure each test starts with clean, known user state
2. **User state verification** - Don't assume which user is active, verify current state first
3. **Component state timing** - Handle unpredictable component refresh timing
4. **Test flow consistency** - Account for navigation behavior variations
## Next Steps for Tomorrow
### **Priority 1: Fix Test Infrastructure**
1. **Implement proper test isolation** - Each test should start with known user state
2. **Standardize element selectors** - Use working `li div` approach consistently
3. **Handle component state changes** - Account for advanced settings closing after navigation
### **Priority 2: Improve Test Reliability**
1. **Add state verification** - Verify current user before making assumptions
2. **Standardize navigation expectations** - Handle both home navigation and no navigation cases
3. **Improve error handling** - Better timeout and retry logic for flaky operations
### **Priority 3: Test Coverage**
1. **Verify all identity switching scenarios** work correctly
2. **Test edge cases** - Error conditions, invalid users, etc.
3. **Performance testing** - Ensure identity switching is fast and responsive
## Technical Notes
### **Working Element Selectors**
```typescript
// ✅ CORRECT - Click on clickable identity list item
const userElement = page.locator('li div').filter({ hasText: userDid }).first();
// ❌ WRONG - Click on code element (no click handler)
const userElement = page.locator(`code:has-text("${userDid}")`);
```
### **Identity Switching Flow**
1. **User clicks identity item**`switchIdentity(did)` called
2. **`$setActiveDid(did)`** updates database ✅
3. **Local state updated**`this.activeDid = did`
4. **Navigation triggered**`this.$router.push({ name: "home" })`
5. **Watchers fire** → Component state refreshes ✅
### **Database Schema**
```sql
-- active_identity table structure (working correctly)
CREATE TABLE active_identity (
scope TEXT DEFAULT 'default',
did TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## Conclusion
**The Active Identity migration is a complete technical success.** All core functionality works perfectly, the database schema is correct, and the user experience is smooth.
The test failures are entirely due to **test infrastructure problems**, not migration issues. This is actually excellent news because it means:
1. **The migration delivered exactly what was intended**
2. **No backend or database fixes are needed**
3. **We just need to fix the test framework** to properly validate the working functionality
**Status**: Ready to proceed with test infrastructure improvements tomorrow.
---
**Next Session Goals**:
- Fix test isolation and user state management
- Standardize working element selectors across all tests
- Implement robust test flow that matches actual application behavior
- Achieve 100% test pass rate to validate the successful migration

View File

@@ -1,122 +0,0 @@
import { test, expect } from '@playwright/test';
import { getTestUserData, switchToUser, importUser } from './testUtils';
/**
* Test Active Identity Migration
*
* This test verifies that the new Active Identity façade methods work correctly
* and that identity switching functionality is preserved after migration.
*
* @author Matthew Raymer
* @date 2025-08-22T07:15Z
*/
test.describe('Active Identity Migration', () => {
test('should switch between identities using new façade methods', async ({ page }) => {
// Test setup: ensure we have at least two users
const userZeroData = getTestUserData('00');
const userOneData = getTestUserData('01');
// Import both users to ensure they exist
try {
await importUser(page, '00');
await page.waitForLoadState('networkidle');
} catch (error) {
// User Zero might already exist, continue
}
try {
await importUser(page, '01');
await page.waitForLoadState('networkidle');
} catch (error) {
// User One might already exist, continue
}
// Start with current user (likely User One)
await page.goto('./account');
await page.waitForLoadState('networkidle');
// Verify we're on the current user (don't assume which one)
const didWrapper = page.getByTestId('didWrapper');
const currentDid = await didWrapper.locator('code').innerText();
console.log(`📋 Starting with user: ${currentDid}`);
// Switch to User One using the identity switcher
await page.getByTestId('advancedSettings').click();
// Wait for the switch identity link to be visible
const switchIdentityLink = page.locator('#switch-identity-link');
await switchIdentityLink.waitFor({ state: 'visible', timeout: 10000 });
await switchIdentityLink.click();
// Wait for identity switcher to load
await page.waitForLoadState('networkidle');
// Click on User One's DID to switch
const userOneDidElement = page.locator(`code:has-text("${userOneData.did}")`);
await expect(userOneDidElement).toBeVisible();
await userOneDidElement.click();
// Wait for the switch to complete and verify we're now User One
await page.waitForLoadState('networkidle');
await expect(didWrapper).toContainText(userOneData.did);
// Verify the switch was successful by checking the account page
await expect(page.locator('h1:has-text("Your Identity")')).toBeVisible();
});
test('should maintain identity state after page refresh', async ({ page }) => {
// Start with User One
await switchToUser(page, getTestUserData('01').did);
// Verify we're on User One
const didWrapper = page.getByTestId('didWrapper');
await expect(didWrapper).toContainText(getTestUserData('01').did);
// Refresh the page
await page.reload();
await page.waitForLoadState('networkidle');
// Verify we're still User One (identity persistence)
await expect(didWrapper).toContainText(getTestUserData('01').did);
});
test('should handle identity switching errors gracefully', async ({ page }) => {
// Navigate to identity switcher
await page.goto('./account');
await page.getByTestId('advancedSettings').click();
// Wait for the switch identity link to be visible
const switchIdentityLink = page.locator('#switch-identity-link');
await switchIdentityLink.waitFor({ state: 'visible', timeout: 10000 });
await switchIdentityLink.click();
// Wait for identity switcher to load
await page.waitForLoadState('networkidle');
// Try to switch to a non-existent identity (this should be handled gracefully)
// Note: This test verifies error handling without causing actual failures
// Verify the identity switcher is still functional
await expect(page.locator('h1:has-text("Switch Identity")')).toBeVisible();
await expect(page.locator('#start-link')).toBeVisible();
});
test('should preserve existing identity data during migration', async ({ page }) => {
// This test verifies that existing identity data is preserved
// and accessible through the new façade methods
// Start with User Zero
await switchToUser(page, getTestUserData('00').did);
// Navigate to a page that uses activeDid
await page.goto('./home');
await page.waitForLoadState('networkidle');
// Verify the page loads correctly with the active identity
await expect(page.locator('h1:has-text("Home")')).toBeVisible();
// The page should load without errors, indicating the new façade methods work
// and the active identity is properly retrieved
});
});

View File

@@ -1,282 +0,0 @@
import { test, expect } from '@playwright/test';
import { getTestUserData, importUser } from './testUtils';
/**
* Active Identity Migration - Step-by-Step Test
*
* Comprehensive test that verifies actual identity switching functionality
*
* @author Matthew Raymer
* @date 2025-08-22T12:35Z
*/
test.describe('Active Identity Migration - Step-by-Step Test', () => {
test('should successfully switch between identities step by step', async ({ page }) => {
// Step 1: Setup - Ensure we have test users
console.log('🔧 Step 1: Setting up test users...');
const userZeroData = getTestUserData('00');
const userOneData = getTestUserData('01');
// Import User Zero if not present
try {
console.log('📥 Importing User Zero...');
await importUser(page, '00');
await page.waitForLoadState('networkidle');
console.log('✅ User Zero imported successfully');
} catch (error) {
console.log(' User Zero might already exist, continuing...');
}
// Import User One if not present
try {
console.log('📥 Importing User One...');
await importUser(page, '01');
await page.waitForLoadState('networkidle');
console.log('✅ User One imported successfully');
} catch (error) {
console.log(' User One might already exist, continuing...');
}
// Step 2: Navigate to account page and verify initial state
console.log('🔍 Step 2: Checking initial account page state...');
await page.goto('./account');
await page.waitForLoadState('networkidle');
// Verify page loads with correct heading
await expect(page.locator('h1:has-text("Your Identity")')).toBeVisible();
console.log('✅ Account page loaded with correct heading');
// Check current active user
const didWrapper = page.getByTestId('didWrapper');
const currentDid = await didWrapper.locator('code').innerText();
console.log(`📋 Current active user: ${currentDid}`);
// Step 3: Access identity switcher
console.log('🔧 Step 3: Accessing identity switcher...');
await page.getByTestId('advancedSettings').click();
// Wait for and verify identity switcher link
const switchIdentityLink = page.locator('#switch-identity-link');
await switchIdentityLink.waitFor({ state: 'visible', timeout: 10000 });
await expect(switchIdentityLink).toBeVisible();
console.log('✅ Identity switcher link is visible');
// Click to open identity switcher
await switchIdentityLink.click();
console.log('🔄 Navigating to identity switcher page...');
// Step 4: Verify identity switcher page loads
console.log('🔍 Step 4: Verifying identity switcher page...');
await page.waitForLoadState('networkidle');
// Verify we're on the identity switcher page
await expect(page.locator('h1:has-text("Switch Identity")')).toBeVisible();
console.log('✅ Identity switcher page loaded');
// Verify basic elements are present
await expect(page.locator('#start-link')).toBeVisible();
console.log('✅ Start link is visible');
// Step 5: Check available identities
console.log('🔍 Step 5: Checking available identities...');
// Look for User Zero in the identity list
const userZeroElement = page.locator(`code:has-text("${userZeroData.did}")`);
const userZeroVisible = await userZeroElement.isVisible();
console.log(`👤 User Zero visible: ${userZeroVisible}`);
// Look for User One in the identity list
const userOneElement = page.locator(`code:has-text("${userOneData.did}")`);
const userOneVisible = await userOneElement.isVisible();
console.log(`👤 User One visible: ${userOneVisible}`);
// Step 6: Attempt to switch to User Zero
console.log('🔄 Step 6: Attempting to switch to User Zero...');
if (userZeroVisible) {
console.log('🖱️ Clicking on User Zero...');
await userZeroElement.click();
// Wait for navigation to home page (default behavior after identity switch)
await page.waitForLoadState('networkidle');
console.log('✅ Clicked User Zero, waiting for page load...');
// Verify we're on home page (default after identity switch)
await expect(page.locator('#ViewHeading')).toBeVisible();
console.log('✅ Navigated to home page after identity switch');
// Check if active user changed by going back to account page
console.log('🔍 Checking if active user changed...');
await page.goto('./account');
await page.waitForLoadState('networkidle');
// Wait a moment for the component to refresh its state
await page.waitForTimeout(1000);
const newDidWrapper = page.getByTestId('didWrapper');
const newCurrentDid = await newDidWrapper.locator('code').innerText();
console.log(`📋 New active user: ${newCurrentDid}`);
if (newCurrentDid === userZeroData.did) {
console.log('✅ SUCCESS: Successfully switched to User Zero!');
} else {
console.log(`❌ FAILED: Expected User Zero (${userZeroData.did}), got ${newCurrentDid}`);
}
} else {
console.log('❌ User Zero not visible in identity list - cannot test switching');
}
// Step 7: Test summary
console.log('📊 Step 7: Test Summary');
console.log(`- Initial user: ${currentDid}`);
console.log(`- User Zero available: ${userZeroVisible}`);
console.log(`- User One available: ${userOneVisible}`);
console.log(`- Final user: ${userZeroVisible ? await page.getByTestId('didWrapper').locator('code').innerText() : 'N/A'}`);
// Final verification - ensure we can still access identity switcher
console.log('🔍 Final verification: Testing identity switcher access...');
// After identity switch, advanced settings are closed by default
// We need to click advanced settings to access the identity switcher
await page.getByTestId('advancedSettings').click();
// Wait for the switch identity link to be visible
const finalSwitchLink = page.locator('#switch-identity-link');
await expect(finalSwitchLink).toBeVisible({ timeout: 10000 });
console.log('✅ Identity switcher still accessible after switching');
});
test('should verify advanced settings state persistence issue', async ({ page }) => {
console.log('🔍 Testing advanced settings state persistence...');
// Step 1: Navigate to account page
await page.goto('./account');
await page.waitForLoadState('networkidle');
// Step 2: Open advanced settings
console.log('📂 Opening advanced settings...');
await page.getByTestId('advancedSettings').click();
// Step 3: Verify identity switcher link is visible
const switchIdentityLink = page.locator('#switch-identity-link');
await expect(switchIdentityLink).toBeVisible();
console.log('✅ Identity switcher link is visible');
// Step 4: Navigate to identity switcher
console.log('🔄 Navigating to identity switcher...');
await switchIdentityLink.click();
await page.waitForLoadState('networkidle');
// Step 5: Go back to account page
console.log('⬅️ Going back to account page...');
await page.goto('./account');
await page.waitForLoadState('networkidle');
// Step 6: Check if advanced settings are still open
console.log('🔍 Checking if advanced settings state persisted...');
const switchIdentityLinkAfter = page.locator('#switch-identity-link');
try {
await expect(switchIdentityLinkAfter).toBeVisible({ timeout: 5000 });
console.log('✅ SUCCESS: Advanced settings state persisted!');
} catch (error) {
console.log('❌ FAILED: Advanced settings state did NOT persist');
console.log('🔍 This confirms the state persistence issue in Active Identity migration');
// Verify the link is hidden
await expect(switchIdentityLinkAfter).toBeHidden();
console.log('✅ Confirmed: Identity switcher link is hidden (advanced settings closed)');
}
});
test('should debug identity switching behavior', async ({ page }) => {
console.log('🔍 Debugging identity switching behavior...');
// Step 1: Setup - Ensure we have test users
const userZeroData = getTestUserData('00');
const userOneData = getTestUserData('01');
// Import both users
try {
await importUser(page, '00');
await importUser(page, '01');
} catch (error) {
// Users might already exist
}
// Step 2: Start with current user
await page.goto('./account');
await page.waitForLoadState('networkidle');
const currentDidWrapper = page.getByTestId('didWrapper');
const currentDid = await currentDidWrapper.locator('code').innerText();
console.log(`👤 Current active user: ${currentDid}`);
// Step 3: Navigate to identity switcher
await page.getByTestId('advancedSettings').click();
const switchIdentityLink = page.locator('#switch-identity-link');
await expect(switchIdentityLink).toBeVisible();
await switchIdentityLink.click();
await page.waitForLoadState('networkidle');
// Step 4: Debug - Check what elements exist
console.log('🔍 Debugging available elements...');
const allDivs = await page.locator('div').filter({ hasText: userZeroData.did }).count();
console.log(`📊 Found ${allDivs} divs containing User Zero DID`);
// Step 5: Try different click strategies
console.log('🔄 Trying different click strategies...');
// Strategy 1: Click on the identity list item with specific class structure
try {
// Look for the identity list item - it should be in the identity list area, not QuickNav
const clickableDiv = page.locator('li div').filter({ hasText: userZeroData.did }).first();
await clickableDiv.waitFor({ state: 'visible', timeout: 5000 });
console.log('✅ Found clickable div with User Zero DID');
// Debug: Log the element's attributes
const elementInfo = await clickableDiv.evaluate((el) => ({
tagName: el.tagName,
className: el.className,
innerHTML: el.innerHTML.slice(0, 100) + '...',
hasClickHandler: el.onclick !== null || el.addEventListener !== undefined
}));
console.log('📋 Element info:', JSON.stringify(elementInfo, null, 2));
await clickableDiv.click();
console.log('✅ Clicked on User Zero element');
// Wait for navigation
await page.waitForLoadState('networkidle');
// Check if we're on home page
const homeHeading = page.locator('#ViewHeading');
if (await homeHeading.isVisible()) {
console.log('✅ Navigated to home page after click');
} else {
console.log('❌ Did not navigate to home page');
}
// Check if identity actually switched
await page.goto('./account');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000); // Wait for component to update
const newDidWrapper = page.getByTestId('didWrapper');
const newCurrentDid = await newDidWrapper.locator('code').innerText();
console.log(`📋 Active user after click: ${newCurrentDid}`);
if (newCurrentDid === userZeroData.did) {
console.log('✅ SUCCESS: Identity switching works!');
} else if (newCurrentDid === currentDid) {
console.log('❌ FAILED: Identity did not change - still on original user');
} else {
console.log(`❌ UNEXPECTED: Identity changed to different user: ${newCurrentDid}`);
}
} catch (error) {
console.log('❌ Failed to find/click User Zero element');
console.log(`Error: ${error}`);
}
});
});

View File

@@ -101,19 +101,11 @@ export async function switchToUser(page: Page, did: string): Promise<void> {
await switchIdentityLink.click();
}
// Wait for the identity switcher page to load
await page.waitForLoadState('networkidle');
// Wait for the identity switcher heading to be visible
await page.locator('h1:has-text("Switch Identity")').waitFor({ state: 'visible' });
// Look for the clickable div containing the user DID (not just the code element)
const didElem = page.locator('li div').filter({ hasText: did }).first();
await didElem.waitFor({ state: 'visible', timeout: 10000 });
const didElem = await page.locator(`code:has-text("${did}")`);
await didElem.isVisible();
await didElem.click();
// Wait for the switch to happen and the account page to fully load
await page.waitForLoadState('networkidle');
// wait for the switch to happen and the account page to fully load
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
}