From f2026bb921761ff9793dd6cfa11d2382f898032c Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 18 Aug 2025 07:44:26 +0000 Subject: [PATCH 01/23] chore: clean up debug logging --- src/services/ProfileService.ts | 14 ++++---------- src/views/AccountViewView.vue | 20 -------------------- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/services/ProfileService.ts b/src/services/ProfileService.ts index bdb27f46..f26b7052 100644 --- a/src/services/ProfileService.ts +++ b/src/services/ProfileService.ts @@ -124,17 +124,12 @@ export class ProfileService { async deleteProfile(activeDid: string): Promise { try { const headers = await getHeaders(activeDid); - logger.debug("Attempting to delete profile for DID:", activeDid); - logger.debug("Using partner API server:", this.partnerApiServer); - logger.debug("Request headers:", headers); - - const url = `${this.partnerApiServer}/api/partner/userProfile`; - logger.debug("DELETE request URL:", url); - - const response = await this.axios.delete(url, { headers }); + const response = await this.axios.delete( + `${this.partnerApiServer}/api/partner/userProfile`, + { headers }, + ); if (response.status === 200 || response.status === 204) { - logger.debug("Profile deleted successfully"); return true; } else { logger.error("Unexpected response status when deleting profile:", { @@ -156,7 +151,6 @@ export class ProfileService { // 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"); diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 1c38a8bb..ceffa5cd 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -182,7 +182,6 @@ @change="onLocationCheckboxChange" /> - (Debug: {{ isMapReady ? 'Map Ready' : 'Map Loading' }})

@@ -1541,14 +1540,12 @@ 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; const lat = this.userProfileLatitude || 0; const lng = this.userProfileLongitude || 0; map.setView([lat, lng], zoom); this.isMapReady = true; - logger.debug("Map ready state set to true, coordinates:", [lat, lng], "zoom:", zoom); } catch (error) { logger.error("Error in onMapReady:", error); this.isMapReady = true; // Set to true even on error to prevent infinite loading @@ -1556,27 +1553,18 @@ export default class AccountViewView extends Vue { } onMapMounted(): void { - logger.debug("Map component mounted"); - // Check if map ref is available - const mapRef = this.$refs.profileMap; - logger.debug("Map ref:", mapRef); - // 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 } @@ -1598,8 +1586,6 @@ export default class AccountViewView extends Vue { includeLocation: this.includeUserProfileLocation, }; - logger.debug("Saving profile data:", profileData); - const success = await this.profileService.saveProfile( this.activeDid, profileData, @@ -1664,7 +1650,6 @@ export default class AccountViewView extends Vue { async deleteProfile(): Promise { try { - logger.debug("Attempting to delete profile for DID:", this.activeDid); const success = await this.profileService.deleteProfile(this.activeDid); if (success) { this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED); @@ -1673,7 +1658,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); } @@ -1710,22 +1694,18 @@ export default class AccountViewView extends Vue { onLocationCheckboxChange(): void { try { - logger.debug("Location checkbox changed, new value:", this.includeUserProfileLocation); if (!this.includeUserProfileLocation) { // Location checkbox was unchecked, clean up map state 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; - logger.debug("Location checked, starting map initialization timeout"); // 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 From 3be7001d1ba31b8b3067ec5f80c828c28e16a18c Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 20 Aug 2025 10:04:40 +0000 Subject: [PATCH 02/23] chore: linting --- src/components/LocationSearchSection.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/LocationSearchSection.vue b/src/components/LocationSearchSection.vue index 1e0f59bb..c39373b6 100644 --- a/src/components/LocationSearchSection.vue +++ b/src/components/LocationSearchSection.vue @@ -26,7 +26,7 @@ :weight="2" color="#3b82f6" fill-color="#3b82f6" - fill-opacity="0.2" + :fill-opacity="0.2" />

From a85b508f44d0f78cb6245a6786c711b3adb0b242 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sun, 24 Aug 2025 05:45:37 +0000 Subject: [PATCH 03/23] 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 --- .cursor/rules/always_on_rules.mdc | 192 ++++++++++ .cursor/rules/meta_bug_diagnosis.mdc | 340 ++++++++++++------ .cursor/rules/meta_bug_fixing.mdc | 39 ++ .cursor/rules/meta_core_always_on.mdc | 115 ++++++ .cursor/rules/meta_documentation.mdc | 38 ++ .cursor/rules/meta_feature_implementation.mdc | 39 ++ .cursor/rules/meta_feature_planning.mdc | 38 ++ .cursor/rules/meta_research.mdc | 38 ++ 8 files changed, 727 insertions(+), 112 deletions(-) create mode 100644 .cursor/rules/always_on_rules.mdc diff --git a/.cursor/rules/always_on_rules.mdc b/.cursor/rules/always_on_rules.mdc new file mode 100644 index 00000000..33128c17 --- /dev/null +++ b/.cursor/rules/always_on_rules.mdc @@ -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 diff --git a/.cursor/rules/meta_bug_diagnosis.mdc b/.cursor/rules/meta_bug_diagnosis.mdc index a919d583..22319342 100644 --- a/.cursor/rules/meta_bug_diagnosis.mdc +++ b/.cursor/rules/meta_bug_diagnosis.mdc @@ -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 -## Common Pitfalls +### **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** -- **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 +- **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 -## Integration Points +### **Error Analysis** -### **With Other Meta-Rules** +- **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 -- **Feature Planning**: Use complexity assessment for investigation planning -- **Bug Fixing**: Investigation results feed directly into fix implementation -- **Feature Implementation**: Investigation insights inform future development +### **Platform Considerations** -### **With Development Workflow** +- **Web Platform**: Browser-specific behaviors and limitations +- **Electron Platform**: Desktop app considerations +- **Capacitor Platform**: Mobile app behaviors +- **Cross-Platform**: Shared vs. platform-specific code -- Investigation findings inform testing strategy -- Root cause analysis drives preventive measures -- Evidence collection improves logging standards +## Evidence Collection Standards -## Feedback & Improvement +### **Timestamps** -### **Sub-Rule Ratings (1-5 scale)** +- **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 -- **Research Diagnostic**: ___/5 - Comments: _______________ -- **Investigation Report**: ___/5 - Comments: _______________ -- **Technical Guide Creation**: ___/5 - Comments: _______________ -- **Logging Standards**: ___/5 - Comments: _______________ -- **Time Standards**: ___/5 - Comments: _______________ +### **Error Context** -### **Workflow Feedback** +- **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 -- **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? +### **Reproduction Steps** -### **Sub-Rule Improvements** +- **Clear Sequence**: Step-by-step reproduction instructions +- **Environment Details**: Platform, version, configuration +- **Data Requirements**: Required data or state +- **Expected vs. Actual**: Clear behavior comparison -- **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? +## Investigation Documentation -### **Overall Experience** +### **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 + +### **Investigation Mistakes** + +- **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 + +### **Communication Issues** + +- **Technical Jargon**: Using unclear terminology +- **Missing Context**: Insufficient background information +- **Unclear Recommendations**: Vague or ambiguous next steps +- **Poor Documentation**: Incomplete or unclear investigation records + +## Success Criteria -- **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? +- [ ] **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 -## Model Implementation Checklist +## Integration with Other Meta-Rules -### Before Bug Investigation +### **Bug Fixing** -- [ ] **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 +- **Investigation Results**: Provide foundation for fix implementation +- **Solution Requirements**: Define what needs to be built +- **Testing Strategy**: Inform validation approach +- **Documentation**: Support implementation guidance -### During Bug Investigation +### **Feature Planning** -- [ ] **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 +- **Root Cause Analysis**: Identify systemic issues +- **Prevention Measures**: Plan future issue avoidance +- **Architecture Improvements**: Identify structural enhancements +- **Process Refinements**: Improve development workflows -### After Bug Investigation +### **Research and Documentation** -- [ ] **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 diff --git a/.cursor/rules/meta_bug_fixing.mdc b/.cursor/rules/meta_bug_fixing.mdc index d1c2a5d1..538b5cae 100644 --- a/.cursor/rules/meta_bug_fixing.mdc +++ b/.cursor/rules/meta_bug_fixing.mdc @@ -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 diff --git a/.cursor/rules/meta_core_always_on.mdc b/.cursor/rules/meta_core_always_on.mdc index 6824f5a7..ac9ee1dd 100644 --- a/.cursor/rules/meta_core_always_on.mdc +++ b/.cursor/rules/meta_core_always_on.mdc @@ -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 diff --git a/.cursor/rules/meta_documentation.mdc b/.cursor/rules/meta_documentation.mdc index 67e23c0c..56c44000 100644 --- a/.cursor/rules/meta_documentation.mdc +++ b/.cursor/rules/meta_documentation.mdc @@ -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**: diff --git a/.cursor/rules/meta_feature_implementation.mdc b/.cursor/rules/meta_feature_implementation.mdc index a24a25fa..ff165969 100644 --- a/.cursor/rules/meta_feature_implementation.mdc +++ b/.cursor/rules/meta_feature_implementation.mdc @@ -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 diff --git a/.cursor/rules/meta_feature_planning.mdc b/.cursor/rules/meta_feature_planning.mdc index 6bcd2a2e..f76b09b6 100644 --- a/.cursor/rules/meta_feature_planning.mdc +++ b/.cursor/rules/meta_feature_planning.mdc @@ -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 diff --git a/.cursor/rules/meta_research.mdc b/.cursor/rules/meta_research.mdc index 2e7d3175..0bbd0fbe 100644 --- a/.cursor/rules/meta_research.mdc +++ b/.cursor/rules/meta_research.mdc @@ -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: From 277fe49aa85fe26604b32a777ef250db1ca17b3b Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sun, 24 Aug 2025 05:46:55 +0000 Subject: [PATCH 04/23] chore: update meta_rule_usage_guide with state and override --- doc/meta_rule_usage_guide.md | 64 ++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/doc/meta_rule_usage_guide.md b/doc/meta_rule_usage_guide.md index 85d2b25b..3b2a8251 100644 --- a/doc/meta_rule_usage_guide.md +++ b/doc/meta_rule_usage_guide.md @@ -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 From d66d8ce1c1a18b6cc025f2a09cfc5712ced8f04f Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sun, 24 Aug 2025 05:47:24 +0000 Subject: [PATCH 05/23] chore: ignore state files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index bd97efcc..f628f58b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 From a6126ecac3b9143b69ecf92cd341ca478ecd7381 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 02:54:56 +0000 Subject: [PATCH 06/23] chore: update .cursor rules for workflow state --- .cursor/rules/README.md | 155 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md index f28acf01..ae4db587 100644 --- a/.cursor/rules/README.md +++ b/.cursor/rules/README.md @@ -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 From fd30343ec401c667f6911f2c5d0067fce7d1804d Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 09:57:13 +0000 Subject: [PATCH 07/23] 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 --- src/components/TopMessage.vue | 33 ++- src/main.ts | 9 + src/router/index.ts | 16 ++ src/views/AccountViewView.vue | 29 ++- src/views/TestView.vue | 455 ++++++++++++++++++++++++++++++++-- 5 files changed, 518 insertions(+), 24 deletions(-) diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue index 519a0585..7cd8f3b3 100644 --- a/src/components/TopMessage.vue +++ b/src/components/TopMessage.vue @@ -18,6 +18,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], @@ -29,6 +30,7 @@ export default class TopMessage extends Vue { // - Cache management: this.$refreshSettings(), this.$clearAllCaches() // - Ultra-concise database methods: this.$db(), this.$exec(), this.$query() // - All methods use smart caching with TTL for massive performance gains + // - FIXED: Now properly respects database settings without forcing API server overrides $notify!: (notification: NotificationIface, timeout?: number) => void; @@ -42,26 +44,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.info("[TopMessage] đŸ“Ĩ Loading settings without overrides..."); + const settings = await this.$accountSettings(); + + logger.info("[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.info("[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.info("[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); } } diff --git a/src/main.ts b/src/main.ts index a58becb7..cc05e386 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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`); diff --git a/src/router/index.ts b/src/router/index.ts index f80d7a48..e63d54b0 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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", diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 9f8db67b..1c368676 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -58,7 +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.` + " /> @@ -925,7 +928,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; + const iconDefault = L.Icon.Default.prototype as unknown as Record< + string, + unknown + >; if ("_getIconUrl" in iconDefault) { delete iconDefault._getIconUrl; } @@ -947,14 +953,25 @@ export default class AccountViewView extends Vue { * @throws Will display specific messages to the user based on different errors. */ async mounted(): Promise { - this.profileService = createProfileService( - this.axios, - this.partnerApiServer, - ); try { await this.initializeState(); await this.processIdentity(); + // FIXED: Create ProfileService AFTER settings are loaded to get correct partnerApiServer + this.profileService = createProfileService( + this.axios, + this.partnerApiServer, + ); + + logger.info( + "[AccountViewView] ✅ ProfileService created with correct partnerApiServer:", + { + partnerApiServer: this.partnerApiServer, + component: "AccountViewView", + timestamp: new Date().toISOString(), + }, + ); + if (this.isRegistered) { try { const profile = await this.profileService.loadProfile(this.activeDid); diff --git a/src/views/TestView.vue b/src/views/TestView.vue index df879ad2..f6f1888d 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -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 + +
+

URL Flow Testing

+

+ Test claim and partner server URL flow from initialization to change + propagation. +

+ +
+
+

Current URL State

+
+
+ API Server: + {{ apiServer || "Not Set" }} +
+
+ Partner API Server: + {{ partnerApiServer || "Not Set" }} +
+
+ Active DID: + {{ activeDid || "Not Set" }} +
+
+ Platform: + {{ getCurrentPlatform() }} +
+
+
+ +
+ + + + + + + + + + + +
+ +
+

URL Flow Test Results

+
+
+ {{ result }} +
+
+
+
+
+

Passkeys

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,24 +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(); - this.activeDid = settings.activeDid || ""; - this.apiServer = settings.apiServer || ""; - this.userName = settings.firstName; - - 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); + logger.info( + "[TestView] 🚀 Component mounting - starting URL flow tracking", + ); + + // 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, + ); } } @@ -824,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"; + } + } } From f0fd8c0f12272989cee448ca5c486e9d25a10754 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 10:18:42 +0000 Subject: [PATCH 08/23] 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. --- src/libs/endorserServer.ts | 42 +++++++++++++++++ src/views/AccountViewView.vue | 88 ++++++++++++++++++++++++++++++----- src/views/HomeView.vue | 19 +++++++- 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index a543ea8d..bf0d3f95 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1493,14 +1493,56 @@ export async function fetchEndorserRateLimits( ) { const url = `${apiServer}/api/report/rateLimits`; const headers = await getHeaders(issuerDid); + + // Enhanced diagnostic logging for user registration tracking + logger.info("[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.info("[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; } } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 1c368676..1a072cd2 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1427,14 +1427,13 @@ export default class AccountViewView extends Vue { async checkLimits(): Promise { this.loadingLimits = true; - try { - const did = this.activeDid; - - if (!did) { - this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER; - return; - } + const did = this.activeDid; + if (!did) { + this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER; + return; + } + try { await this.$saveUserSettings(did, { apiServer: this.apiServer, partnerApiServer: this.partnerApiServer, @@ -1467,7 +1466,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; @@ -1475,24 +1493,70 @@ export default class AccountViewView extends Vue { } async onClickSaveApiServer(): Promise { + // Enhanced diagnostic logging for claim URL changes + const previousApiServer = this.apiServer; + const newApiServer = this.apiServerInput; + + logger.info("[Server Switching] Claim URL change initiated:", { + did: this.activeDid, + previousServer: previousApiServer, + newServer: newApiServer, + changeType: "apiServer", + timestamp: new Date().toISOString(), + }); + await this.$saveSettings({ - apiServer: this.apiServerInput, + apiServer: newApiServer, }); - this.apiServer = this.apiServerInput; + 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.info("[Server Switching] Claim URL change completed:", { + did: this.activeDid, + previousServer: previousApiServer, + newServer: newApiServer, + changeType: "apiServer", + settingsSaved: true, + timestamp: new Date().toISOString(), + }); } async onClickSavePartnerServer(): Promise { + // Enhanced diagnostic logging for partner server changes + const previousPartnerServer = this.partnerApiServer; + const newPartnerServer = this.partnerApiServerInput; + + logger.info("[Server Switching] Partner server change initiated:", { + did: this.activeDid, + previousServer: previousPartnerServer, + newServer: newPartnerServer, + changeType: "partnerApiServer", + timestamp: new Date().toISOString(), + }); + await this.$saveSettings({ - partnerApiServer: this.partnerApiServerInput, + partnerApiServer: newPartnerServer, }); - this.partnerApiServer = this.partnerApiServerInput; + this.partnerApiServer = newPartnerServer; + await this.$saveUserSettings(this.activeDid, { partnerApiServer: this.partnerApiServer, }); + + // Log successful partner server switch + logger.info("[Server Switching] Partner server change completed:", { + did: this.activeDid, + previousServer: previousPartnerServer, + newServer: newPartnerServer, + changeType: "partnerApiServer", + settingsSaved: true, + timestamp: new Date().toISOString(), + }); } async onClickSavePushServer(): Promise { diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index fe6fbc28..5609096f 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -567,10 +567,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(), }, ); } From 6aac3ca35f60fab7fc0ffcbaa3230a7403361f30 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 10:25:20 +0000 Subject: [PATCH 09/23] 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. --- src/libs/endorserServer.ts | 51 ++++++++++++++++++++++++++++++++--- src/views/AccountViewView.vue | 6 ++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index bf0d3f95..23eed625 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1555,8 +1555,53 @@ export async function fetchEndorserRateLimits( * @param {string} issuerDid - The DID for which to check rate limits. * @returns {Promise} 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.info("[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.info("[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; + } } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 1a072cd2..669cf8e2 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1440,7 +1440,11 @@ export default class AccountViewView extends Vue { 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; From 271a45afa3c0fdcf0ccab8091519c55d0e457872 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 11:50:55 +0000 Subject: [PATCH 10/23] 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 --- src/utils/PlatformServiceMixin.ts | 38 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 68c09720..c36ede9a 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -445,10 +445,22 @@ export const PlatformServiceMixin = { fallback: Settings | null = null, ): Promise { try { - const result = await this.$dbQuery( - "SELECT * FROM settings WHERE id = ? OR accountDid = ?", - [key, key], - ); + // FIXED: Use specific queries instead of ambiguous OR condition + let result; + + // Check if this is a master settings key (numeric or "1") + if (key === MASTER_SETTINGS_KEY || key === "1" || !isNaN(Number(key))) { + // Master settings: query by id + result = await this.$dbQuery("SELECT * FROM settings WHERE id = ?", [ + key, + ]); + } else { + // Account settings: query by accountDid + result = await this.$dbQuery( + "SELECT * FROM settings WHERE accountDid = ?", + [key], + ); + } if (!result?.values?.length) { return fallback; @@ -763,13 +775,14 @@ export const PlatformServiceMixin = { 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; } @@ -813,14 +826,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; } From 7f7680f4a6c2dc9c272221b5e8ffad10eb63b178 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 11:51:12 +0000 Subject: [PATCH 11/23] 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 --- src/views/HomeView.vue | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 5609096f..3a910119 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -601,8 +601,9 @@ 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 + * + * FIXED: Now respects user preferences instead of forcing production server * * @internal * Called after loading settings to ensure correct API endpoint @@ -610,12 +611,10 @@ 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 + // FIXED: Remove forced override - respect user preferences + // 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; } } @@ -1177,9 +1176,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, }); } From a11443dc3a93ee0f654f9bec4a88131182ecf957 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 11:51:22 +0000 Subject: [PATCH 12/23] 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 --- src/libs/endorserServer.ts | 123 ++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 7 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 23eed625..5c7073b3 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -511,30 +511,76 @@ export async function getPlanFromCache( "/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.info("[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.info("[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) { cred = resp.data.data[0]; planCache.set(handleId, cred); + + logger.debug("[Plan Loading] 💾 Plan cached:", { + requestId, + handleId, + planName: cred?.name, + planIssuer: cred?.issuerDid, + }); } 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", + "[Plan Loading] âš ī¸ Plan cache is empty for handle", handleId, " Got data:", JSON.stringify(resp.data), ); } } catch (error) { - logger.error( - "[EndorserServer] Failed to load plan with handle", + // 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, - " Got error:", - JSON.stringify(error), - ); + 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(), + }); } } return cred; @@ -1018,19 +1064,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; + }; + config?: { + url?: string; + method?: string; + headers?: Record; + }; + 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 From 77a4c60656081cdf4802d6615d51873a068011ca Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 13:03:06 +0000 Subject: [PATCH 13/23] 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. --- src/services/ProfileService.ts | 413 ++++++++-------- src/services/ServiceInitializationManager.ts | 207 ++++++++ src/utils/errorHandler.ts | 298 ++++++++++++ src/utils/performanceOptimizer.ts | 482 +++++++++++++++++++ 4 files changed, 1204 insertions(+), 196 deletions(-) create mode 100644 src/services/ServiceInitializationManager.ts create mode 100644 src/utils/errorHandler.ts create mode 100644 src/utils/performanceOptimizer.ts diff --git a/src/services/ProfileService.ts b/src/services/ProfileService.ts index 690f03de..6d861c98 100644 --- a/src/services/ProfileService.ts +++ b/src/services/ProfileService.ts @@ -1,18 +1,23 @@ /** * ProfileService - Handles user profile operations and API calls * Extracted from AccountViewView.vue to improve separation of concerns + * + * @author Matthew Raymer + * @since 2025-08-25 */ -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"; +import { AxiosInstance } from "axios"; +import { logger } from "../utils/logger"; +import { getServiceInitManager } from "./ServiceInitializationManager"; +import { + handleApiError, + createErrorContext, + createUserMessage, +} from "../utils/errorHandler"; +import { getHeaders } from "../libs/endorserServer"; /** - * Profile data interface + * Profile data structure */ export interface ProfileData { description: string; @@ -22,7 +27,10 @@ export interface ProfileData { } /** - * Profile service class + * Profile service for managing user profile information + * + * @author Matthew Raymer + * @since 2025-08-25 */ export class ProfileService { private axios: AxiosInstance; @@ -31,71 +39,192 @@ export class ProfileService { constructor(axios: AxiosInstance, partnerApiServer: string) { this.axios = axios; this.partnerApiServer = partnerApiServer; + + // Register with service initialization manager + const initManager = getServiceInitManager(); + initManager.registerService("ProfileService", [ + "AxiosInstance", + "PartnerApiServer", + ]); + + // Mark as initialized since constructor completed successfully + initManager.markInitialized("ProfileService"); + + logger.debug("[ProfileService] 🔧 Service initialized:", { + partnerApiServer, + hasAxios: !!axios, + timestamp: new Date().toISOString(), + }); } /** - * Load user profile from the server - * @param activeDid - The user's DID - * @returns ProfileData or null if profile doesn't exist + * Load user profile from the partner API + * + * @param did - User's DID + * @returns Profile data or null if not found + * @throws Error if API call fails */ - async loadProfile(activeDid: string): Promise { + async loadProfile(did: string): Promise { + const operation = "Load Profile"; + const context = createErrorContext("ProfileService", operation, { + did, + partnerApiServer: this.partnerApiServer, + endpoint: `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`, + }); + try { - const headers = await getHeaders(activeDid); - const response = await this.axios.get( - `${this.partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`, - { headers }, - ); + // Enhanced request tracking + const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + logger.info("[ProfileService] 🔍 Loading profile:", { + requestId, + ...context, + }); + + // Get authentication headers + const headers = await getHeaders(did); + + // FIXED: Use the original working endpoint that was working before recent changes + // The working endpoint is /api/partner/userProfileForIssuer/{did} for getting a specific user's profile + // NOT /api/partner/userProfile which returns a list of all profiles + const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`; + + logger.info("[ProfileService] 🔗 Making API request:", { + requestId, + did, + fullUrl, + partnerApiServer: this.partnerApiServer, + hasAuthHeader: !!headers.Authorization, + authHeaderLength: headers.Authorization?.length || 0, + }); + + const response = await this.axios.get(fullUrl, { 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), + logger.info("[ProfileService] ✅ Profile loaded successfully:", { + requestId, + ...context, + status: response.status, + hasData: !!response.data, + dataKeys: response.data ? Object.keys(response.data) : [], + responseData: response.data, + responseDataType: typeof response.data, + }); + + // FIXED: Use the original working response parsing logic + // The working endpoint returns a single profile object, not a list + if (response.data && response.data.data) { + const profileData = response.data.data; + logger.info("[ProfileService] 🔍 Parsing profile data:", { + requestId, + profileData, + profileDataKeys: Object.keys(profileData), + locLat: profileData.locLat, + locLon: profileData.locLon, + description: profileData.description, + issuerDid: profileData.issuerDid, + hasLocationFields: !!(profileData.locLat || profileData.locLon), + }); + + const result = { + description: profileData.description || "", + latitude: profileData.locLat || 0, + longitude: profileData.locLon || 0, + includeLocation: !!(profileData.locLat && profileData.locLon), }; - return profileData; + + logger.info("[ProfileService] 📊 Parsed profile result:", { + requestId, + result, + hasLocation: result.includeLocation, + locationValues: { + original: { locLat: profileData.locLat, locLon: profileData.locLon }, + parsed: { latitude: result.latitude, longitude: result.longitude }, + }, + }); + + return result; } else { - throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.UNABLE_TO_LOAD_PROFILE); + logger.warn("[ProfileService] âš ī¸ No profile data found in response:", { + requestId, + responseData: response.data, + hasData: !!response.data, + hasDataData: !!(response.data && response.data.data), + }); } - } catch (error) { - if (this.isApiError(error) && error.response?.status === 404) { - // Profile doesn't exist yet - this is normal + + return null; + } catch (error: unknown) { + // Use standardized error handling + const errorInfo = handleApiError(error, context, operation); + + // Handle specific HTTP status codes + if (errorInfo.errorType === "AxiosError" && errorInfo.status === 404) { + logger.info( + "[ProfileService] â„šī¸ Profile not found (404) - this is normal for new users", + ); return null; } - logger.error("Error loading profile:", errorStringForLog(error)); - handleApiError(error as AxiosError, "/api/partner/userProfileForIssuer"); - return null; + // Create user-friendly error message + const userMessage = createUserMessage( + errorInfo, + "Failed to load profile", + ); + throw new Error(userMessage); } } /** - * 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 + * Save user profile to the partner API + * + * @param did - User's DID + * @param profileData - Profile data to save + * @returns Success status + * @throws Error if API call fails */ - async saveProfile( - activeDid: string, - profileData: ProfileData, - ): Promise { + async saveProfile(did: string, profileData: ProfileData): Promise { + const operation = "Save Profile"; + const context = createErrorContext("ProfileService", operation, { + did, + partnerApiServer: this.partnerApiServer, + endpoint: `${this.partnerApiServer}/api/partner/userProfile`, + profileData, + }); + try { - const headers = await getHeaders(activeDid); - const payload: UserProfile = { - description: profileData.description, - issuerDid: activeDid, - }; + // Enhanced request tracking + const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + logger.info("[ProfileService] 💾 Saving profile:", { + requestId, + ...context, + }); + + // Get authentication headers + const headers = await getHeaders(did); - // Add location data if location is included - if ( - profileData.includeLocation && + // Prepare payload in the format expected by the partner API + const payload = { + description: profileData.description, + issuerDid: did, + ...(profileData.includeLocation && profileData.latitude && profileData.longitude - ) { - payload.locLat = profileData.latitude; - payload.locLon = profileData.longitude; - } + ? { + locLat: profileData.latitude, + locLon: profileData.longitude, + } + : {}), + }; + + logger.info("[ProfileService] 📤 Sending payload to server:", { + requestId, + payload, + hasLocation: profileData.includeLocation, + latitude: profileData.latitude, + longitude: profileData.longitude, + payloadKeys: Object.keys(payload), + }); const response = await this.axios.post( `${this.partnerApiServer}/api/partner/userProfile`, @@ -103,103 +232,32 @@ export class ProfileService { { 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 { - 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), - }); + logger.info("[ProfileService] ✅ Profile saved successfully:", { + requestId, + ...context, + status: response.status, + hasData: !!response.data, + responseData: response.data, + responseDataKeys: response.data ? Object.keys(response.data) : [], + }); - // Handle specific HTTP status codes - if (response.status === 204) { - 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"); - } - } + return true; + } catch (error: unknown) { + // Use standardized error handling + const errorInfo = handleApiError(error, context, operation); - logger.error("Error deleting profile:", errorStringForLog(error)); - handleApiError(error as AxiosError, "/api/partner/userProfile"); - return false; + // Create user-friendly error message + const userMessage = createUserMessage( + errorInfo, + "Failed to save profile", + ); + throw new Error(userMessage); } } /** - * 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 + * Toggle profile location visibility + * * @param profileData - Current profile data * @returns Updated profile data */ @@ -215,6 +273,7 @@ export class ProfileService { /** * Clear profile location + * * @param profileData - Current profile data * @returns Updated profile data */ @@ -229,6 +288,7 @@ export class ProfileService { /** * Reset profile to default state + * * @returns Default profile data */ getDefaultProfile(): ProfileData { @@ -239,66 +299,27 @@ export class ProfileService { 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 + * + * @param axios - Axios instance for HTTP requests + * @param partnerApiServer - Partner API server URL + * @returns ProfileService instance */ export function createProfileService( axios: AxiosInstance, partnerApiServer: string, ): ProfileService { + // Register dependencies with service initialization manager + const initManager = getServiceInitManager(); + initManager.registerService("AxiosInstance", []); + initManager.registerService("PartnerApiServer", []); + + // Mark dependencies as initialized + initManager.markInitialized("AxiosInstance"); + initManager.markInitialized("PartnerApiServer"); + return new ProfileService(axios, partnerApiServer); } diff --git a/src/services/ServiceInitializationManager.ts b/src/services/ServiceInitializationManager.ts new file mode 100644 index 00000000..765c5e7a --- /dev/null +++ b/src/services/ServiceInitializationManager.ts @@ -0,0 +1,207 @@ +/** + * Service Initialization Manager + * + * Manages the proper initialization order of services to prevent race conditions + * and ensure dependencies are available when services are created. + * + * @author Matthew Raymer + * @since 2025-08-25 + */ + +import { logger } from "../utils/logger"; + +/** + * Service initialization status tracking + */ +interface ServiceStatus { + name: string; + initialized: boolean; + dependencies: string[]; + error?: string; +} + +/** + * Service initialization manager to prevent race conditions + */ +export class ServiceInitializationManager { + private static instance: ServiceInitializationManager; + private serviceStatuses = new Map(); + private initializationPromise: Promise | null = null; + + private constructor() {} + + /** + * Get singleton instance + */ + static getInstance(): ServiceInitializationManager { + if (!ServiceInitializationManager.instance) { + ServiceInitializationManager.instance = + new ServiceInitializationManager(); + } + return ServiceInitializationManager.instance; + } + + /** + * Register a service that needs initialization + */ + registerService(name: string, dependencies: string[] = []): void { + this.serviceStatuses.set(name, { + name, + initialized: false, + dependencies, + }); + + logger.debug("[ServiceInit] 🔧 Service registered:", { + name, + dependencies, + totalServices: this.serviceStatuses.size, + }); + } + + /** + * Mark a service as initialized + */ + markInitialized(name: string): void { + const status = this.serviceStatuses.get(name); + if (status) { + status.initialized = true; + logger.debug("[ServiceInit] ✅ Service initialized:", { + name, + totalInitialized: this.getInitializedCount(), + totalServices: this.serviceStatuses.size, + }); + } + } + + /** + * Mark a service as failed + */ + markFailed(name: string, error: string): void { + const status = this.serviceStatuses.get(name); + if (status) { + status.error = error; + logger.error("[ServiceInit] ❌ Service failed:", { + name, + error, + totalFailed: this.getFailedCount(), + }); + } + } + + /** + * Get count of initialized services + */ + private getInitializedCount(): number { + return Array.from(this.serviceStatuses.values()).filter( + (s) => s.initialized, + ).length; + } + + /** + * Get count of failed services + */ + private getFailedCount(): number { + return Array.from(this.serviceStatuses.values()).filter((s) => s.error) + .length; + } + + /** + * Wait for all services to be initialized + */ + async waitForInitialization(): Promise { + if (this.initializationPromise) { + return this.initializationPromise; + } + + this.initializationPromise = new Promise((resolve, reject) => { + const checkInterval = setInterval(() => { + const totalServices = this.serviceStatuses.size; + const initializedCount = this.getInitializedCount(); + const failedCount = this.getFailedCount(); + + logger.debug("[ServiceInit] 🔍 Initialization progress:", { + totalServices, + initializedCount, + failedCount, + remaining: totalServices - initializedCount - failedCount, + }); + + if (failedCount > 0) { + clearInterval(checkInterval); + const failedServices = Array.from(this.serviceStatuses.values()) + .filter((s) => s.error) + .map((s) => `${s.name}: ${s.error}`); + + const error = new Error( + `Service initialization failed: ${failedServices.join(", ")}`, + ); + logger.error("[ServiceInit] ❌ Initialization failed:", error); + reject(error); + } else if (initializedCount === totalServices) { + clearInterval(checkInterval); + logger.info( + "[ServiceInit] 🎉 All services initialized successfully:", + { + totalServices, + initializedCount, + }, + ); + resolve(); + } + }, 100); + + // Timeout after 30 seconds + setTimeout(() => { + clearInterval(checkInterval); + const error = new Error( + "Service initialization timeout after 30 seconds", + ); + logger.error("[ServiceInit] ⏰ Initialization timeout:", error); + reject(error); + }, 30000); + }); + + return this.initializationPromise; + } + + /** + * Get initialization status summary + */ + getStatusSummary(): { + total: number; + initialized: number; + failed: number; + pending: number; + services: ServiceStatus[]; + } { + const services = Array.from(this.serviceStatuses.values()); + const total = services.length; + const initialized = services.filter((s) => s.initialized).length; + const failed = services.filter((s) => s.error).length; + const pending = total - initialized - failed; + + return { + total, + initialized, + failed, + pending, + services, + }; + } + + /** + * Reset the manager (useful for testing) + */ + reset(): void { + this.serviceStatuses.clear(); + this.initializationPromise = null; + logger.debug("[ServiceInit] 🔄 Manager reset"); + } +} + +/** + * Convenience function to get the service initialization manager + */ +export const getServiceInitManager = (): ServiceInitializationManager => { + return ServiceInitializationManager.getInstance(); +}; diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts new file mode 100644 index 00000000..ee608844 --- /dev/null +++ b/src/utils/errorHandler.ts @@ -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; + }; +} + +/** + * 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; + + // 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 = {}, +): ErrorContext { + return { + component, + operation, + timestamp: new Date().toISOString(), + ...additionalContext, + }; +} diff --git a/src/utils/performanceOptimizer.ts b/src/utils/performanceOptimizer.ts new file mode 100644 index 00000000..8a77c599 --- /dev/null +++ b/src/utils/performanceOptimizer.ts @@ -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 { + 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 { + private items: BatchItem[] = []; + private timer: NodeJS.Timeout | null = null; + private processing = false; + private config: BatchConfig; + + constructor( + private batchHandler: (items: T[]) => Promise, + private itemIdExtractor: (item: T) => string, + config: Partial = {}, + ) { + 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 { + return new Promise((resolve, reject) => { + const item: BatchItem = { + 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 { + 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(); + 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 unknown>( + func: T, + wait: number, + ): (...args: Parameters) => void { + let timeout: NodeJS.Timeout | null = null; + + return (...args: Parameters) => { + 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 unknown>( + func: T, + limit: number, + ): (...args: Parameters) => void { + let inThrottle = false; + + return (...args: Parameters) => { + 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 unknown, K>( + func: T, + keyGenerator: (...args: Parameters) => K, + ): T { + const cache = new Map(); + + return ((...args: Parameters) => { + 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> { + 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(); +}; From b834596ba68a518b63fe9a737bc298409a41ec80 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 25 Aug 2025 13:22:31 +0000 Subject: [PATCH 14/23] 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. --- .cursor/rules/meta_change_evaluation.mdc | 383 +++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 .cursor/rules/meta_change_evaluation.mdc diff --git a/.cursor/rules/meta_change_evaluation.mdc b/.cursor/rules/meta_change_evaluation.mdc new file mode 100644 index 00000000..dd4e1f74 --- /dev/null +++ b/.cursor/rules/meta_change_evaluation.mdc @@ -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): 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 From 9386b2e96fc3def870e2c4214b260cebe5c80fec Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 26 Aug 2025 07:23:24 +0000 Subject: [PATCH 15/23] refactor(services): inline ProfileService logic into AccountViewView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/TopMessage.vue | 9 +- src/libs/endorserServer.ts | 12 +- src/libs/util.ts | 19 +- src/router/index.ts | 10 +- src/services/ProfileService.ts | 325 ------------------- src/services/ServiceInitializationManager.ts | 207 ------------ src/views/AccountViewView.vue | 245 ++++++++++++-- src/views/HomeView.vue | 3 - src/views/IdentitySwitcherView.vue | 4 +- src/views/StartView.vue | 12 +- 10 files changed, 259 insertions(+), 587 deletions(-) delete mode 100644 src/services/ProfileService.ts delete mode 100644 src/services/ServiceInitializationManager.ts diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue index 7cd8f3b3..e96c8cb0 100644 --- a/src/components/TopMessage.vue +++ b/src/components/TopMessage.vue @@ -30,7 +30,6 @@ export default class TopMessage extends Vue { // - Cache management: this.$refreshSettings(), this.$clearAllCaches() // - Ultra-concise database methods: this.$db(), this.$exec(), this.$query() // - All methods use smart caching with TTL for massive performance gains - // - FIXED: Now properly respects database settings without forcing API server overrides $notify!: (notification: NotificationIface, timeout?: number) => void; @@ -45,10 +44,10 @@ export default class TopMessage extends Vue { try { // Load settings without overriding database values - fixes settings inconsistency - logger.info("[TopMessage] đŸ“Ĩ Loading settings without overrides..."); + logger.debug("[TopMessage] đŸ“Ĩ Loading settings without overrides..."); const settings = await this.$accountSettings(); - logger.info("[TopMessage] 📊 Settings loaded:", { + logger.debug("[TopMessage] 📊 Settings loaded:", { activeDid: settings.activeDid, apiServer: settings.apiServer, warnIfTestServer: settings.warnIfTestServer, @@ -65,7 +64,7 @@ export default class TopMessage extends Vue { ) { const didPrefix = settings.activeDid?.slice(11, 15); this.message = "You're not using prod, user " + didPrefix; - logger.info("[TopMessage] âš ī¸ Test server warning displayed:", { + logger.debug("[TopMessage] âš ī¸ Test server warning displayed:", { apiServer: settings.apiServer, didPrefix: didPrefix, }); @@ -76,7 +75,7 @@ export default class TopMessage extends Vue { ) { const didPrefix = settings.activeDid?.slice(11, 15); this.message = "You are using prod, user " + didPrefix; - logger.info("[TopMessage] âš ī¸ Production server warning displayed:", { + logger.debug("[TopMessage] âš ī¸ Production server warning displayed:", { apiServer: settings.apiServer, didPrefix: didPrefix, }); diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 5c7073b3..8ab98ac6 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -515,7 +515,7 @@ export async function getPlanFromCache( // Enhanced diagnostic logging for plan loading const requestId = `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - logger.info("[Plan Loading] 🔍 Loading plan from server:", { + logger.debug("[Plan Loading] 🔍 Loading plan from server:", { requestId, handleId, apiServer, @@ -527,7 +527,7 @@ export async function getPlanFromCache( try { const resp = await axios.get(url, { headers }); - logger.info("[Plan Loading] ✅ Plan loaded successfully:", { + logger.debug("[Plan Loading] ✅ Plan loaded successfully:", { requestId, handleId, status: resp.status, @@ -1604,7 +1604,7 @@ export async function fetchEndorserRateLimits( const headers = await getHeaders(issuerDid); // Enhanced diagnostic logging for user registration tracking - logger.info("[User Registration] Checking user status on server:", { + logger.debug("[User Registration] Checking user status on server:", { did: issuerDid, server: apiServer, endpoint: url, @@ -1615,7 +1615,7 @@ export async function fetchEndorserRateLimits( const response = await axios.get(url, { headers } as AxiosRequestConfig); // Log successful registration check - logger.info("[User Registration] User registration check successful:", { + logger.debug("[User Registration] User registration check successful:", { did: issuerDid, server: apiServer, status: response.status, @@ -1674,7 +1674,7 @@ export async function fetchImageRateLimits( const headers = await getHeaders(issuerDid); // Enhanced diagnostic logging for image server calls - logger.info("[Image Server] Checking image rate limits:", { + logger.debug("[Image Server] Checking image rate limits:", { did: issuerDid, server: server, endpoint: url, @@ -1685,7 +1685,7 @@ export async function fetchImageRateLimits( const response = await axios.get(url, { headers } as AxiosRequestConfig); // Log successful image server call - logger.info("[Image Server] Image rate limits check successful:", { + logger.debug("[Image Server] Image rate limits check successful:", { did: issuerDid, server: server, status: response.status, diff --git a/src/libs/util.ts b/src/libs/util.ts index e21d1932..1f4bf068 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -973,13 +973,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) { @@ -1005,7 +1008,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], diff --git a/src/router/index.ts b/src/router/index.ts index e63d54b0..cf450c37 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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, @@ -368,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"); @@ -382,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`, ); } @@ -408,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, diff --git a/src/services/ProfileService.ts b/src/services/ProfileService.ts deleted file mode 100644 index 6d861c98..00000000 --- a/src/services/ProfileService.ts +++ /dev/null @@ -1,325 +0,0 @@ -/** - * ProfileService - Handles user profile operations and API calls - * Extracted from AccountViewView.vue to improve separation of concerns - * - * @author Matthew Raymer - * @since 2025-08-25 - */ - -import { AxiosInstance } from "axios"; -import { logger } from "../utils/logger"; -import { getServiceInitManager } from "./ServiceInitializationManager"; -import { - handleApiError, - createErrorContext, - createUserMessage, -} from "../utils/errorHandler"; -import { getHeaders } from "../libs/endorserServer"; - -/** - * Profile data structure - */ -export interface ProfileData { - description: string; - latitude: number; - longitude: number; - includeLocation: boolean; -} - -/** - * Profile service for managing user profile information - * - * @author Matthew Raymer - * @since 2025-08-25 - */ -export class ProfileService { - private axios: AxiosInstance; - private partnerApiServer: string; - - constructor(axios: AxiosInstance, partnerApiServer: string) { - this.axios = axios; - this.partnerApiServer = partnerApiServer; - - // Register with service initialization manager - const initManager = getServiceInitManager(); - initManager.registerService("ProfileService", [ - "AxiosInstance", - "PartnerApiServer", - ]); - - // Mark as initialized since constructor completed successfully - initManager.markInitialized("ProfileService"); - - logger.debug("[ProfileService] 🔧 Service initialized:", { - partnerApiServer, - hasAxios: !!axios, - timestamp: new Date().toISOString(), - }); - } - - /** - * Load user profile from the partner API - * - * @param did - User's DID - * @returns Profile data or null if not found - * @throws Error if API call fails - */ - async loadProfile(did: string): Promise { - const operation = "Load Profile"; - const context = createErrorContext("ProfileService", operation, { - did, - partnerApiServer: this.partnerApiServer, - endpoint: `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`, - }); - - try { - // Enhanced request tracking - const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - logger.info("[ProfileService] 🔍 Loading profile:", { - requestId, - ...context, - }); - - // Get authentication headers - const headers = await getHeaders(did); - - // FIXED: Use the original working endpoint that was working before recent changes - // The working endpoint is /api/partner/userProfileForIssuer/{did} for getting a specific user's profile - // NOT /api/partner/userProfile which returns a list of all profiles - const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`; - - logger.info("[ProfileService] 🔗 Making API request:", { - requestId, - did, - fullUrl, - partnerApiServer: this.partnerApiServer, - hasAuthHeader: !!headers.Authorization, - authHeaderLength: headers.Authorization?.length || 0, - }); - - const response = await this.axios.get(fullUrl, { headers }); - - logger.info("[ProfileService] ✅ Profile loaded successfully:", { - requestId, - ...context, - status: response.status, - hasData: !!response.data, - dataKeys: response.data ? Object.keys(response.data) : [], - responseData: response.data, - responseDataType: typeof response.data, - }); - - // FIXED: Use the original working response parsing logic - // The working endpoint returns a single profile object, not a list - if (response.data && response.data.data) { - const profileData = response.data.data; - logger.info("[ProfileService] 🔍 Parsing profile data:", { - requestId, - profileData, - profileDataKeys: Object.keys(profileData), - locLat: profileData.locLat, - locLon: profileData.locLon, - description: profileData.description, - issuerDid: profileData.issuerDid, - hasLocationFields: !!(profileData.locLat || profileData.locLon), - }); - - const result = { - description: profileData.description || "", - latitude: profileData.locLat || 0, - longitude: profileData.locLon || 0, - includeLocation: !!(profileData.locLat && profileData.locLon), - }; - - logger.info("[ProfileService] 📊 Parsed profile result:", { - requestId, - result, - hasLocation: result.includeLocation, - locationValues: { - original: { locLat: profileData.locLat, locLon: profileData.locLon }, - parsed: { latitude: result.latitude, longitude: result.longitude }, - }, - }); - - return result; - } else { - logger.warn("[ProfileService] âš ī¸ No profile data found in response:", { - requestId, - responseData: response.data, - hasData: !!response.data, - hasDataData: !!(response.data && response.data.data), - }); - } - - return null; - } catch (error: unknown) { - // Use standardized error handling - const errorInfo = handleApiError(error, context, operation); - - // Handle specific HTTP status codes - if (errorInfo.errorType === "AxiosError" && errorInfo.status === 404) { - logger.info( - "[ProfileService] â„šī¸ Profile not found (404) - this is normal for new users", - ); - return null; - } - - // Create user-friendly error message - const userMessage = createUserMessage( - errorInfo, - "Failed to load profile", - ); - throw new Error(userMessage); - } - } - - /** - * Save user profile to the partner API - * - * @param did - User's DID - * @param profileData - Profile data to save - * @returns Success status - * @throws Error if API call fails - */ - async saveProfile(did: string, profileData: ProfileData): Promise { - const operation = "Save Profile"; - const context = createErrorContext("ProfileService", operation, { - did, - partnerApiServer: this.partnerApiServer, - endpoint: `${this.partnerApiServer}/api/partner/userProfile`, - profileData, - }); - - try { - // Enhanced request tracking - const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - logger.info("[ProfileService] 💾 Saving profile:", { - requestId, - ...context, - }); - - // 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.info("[ProfileService] 📤 Sending payload to server:", { - requestId, - payload, - hasLocation: profileData.includeLocation, - latitude: profileData.latitude, - longitude: profileData.longitude, - payloadKeys: Object.keys(payload), - }); - - const response = await this.axios.post( - `${this.partnerApiServer}/api/partner/userProfile`, - payload, - { headers }, - ); - - logger.info("[ProfileService] ✅ Profile saved successfully:", { - requestId, - ...context, - status: response.status, - hasData: !!response.data, - responseData: response.data, - responseDataKeys: response.data ? Object.keys(response.data) : [], - }); - - return true; - } catch (error: unknown) { - // Use standardized error handling - const errorInfo = handleApiError(error, context, operation); - - // Create user-friendly error message - const userMessage = createUserMessage( - errorInfo, - "Failed to save profile", - ); - throw new Error(userMessage); - } - } - - /** - * Toggle profile location visibility - * - * @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, - }; - } -} - -/** - * Factory function to create a ProfileService instance - * - * @param axios - Axios instance for HTTP requests - * @param partnerApiServer - Partner API server URL - * @returns ProfileService instance - */ -export function createProfileService( - axios: AxiosInstance, - partnerApiServer: string, -): ProfileService { - // Register dependencies with service initialization manager - const initManager = getServiceInitManager(); - initManager.registerService("AxiosInstance", []); - initManager.registerService("PartnerApiServer", []); - - // Mark dependencies as initialized - initManager.markInitialized("AxiosInstance"); - initManager.markInitialized("PartnerApiServer"); - - return new ProfileService(axios, partnerApiServer); -} diff --git a/src/services/ServiceInitializationManager.ts b/src/services/ServiceInitializationManager.ts deleted file mode 100644 index 765c5e7a..00000000 --- a/src/services/ServiceInitializationManager.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Service Initialization Manager - * - * Manages the proper initialization order of services to prevent race conditions - * and ensure dependencies are available when services are created. - * - * @author Matthew Raymer - * @since 2025-08-25 - */ - -import { logger } from "../utils/logger"; - -/** - * Service initialization status tracking - */ -interface ServiceStatus { - name: string; - initialized: boolean; - dependencies: string[]; - error?: string; -} - -/** - * Service initialization manager to prevent race conditions - */ -export class ServiceInitializationManager { - private static instance: ServiceInitializationManager; - private serviceStatuses = new Map(); - private initializationPromise: Promise | null = null; - - private constructor() {} - - /** - * Get singleton instance - */ - static getInstance(): ServiceInitializationManager { - if (!ServiceInitializationManager.instance) { - ServiceInitializationManager.instance = - new ServiceInitializationManager(); - } - return ServiceInitializationManager.instance; - } - - /** - * Register a service that needs initialization - */ - registerService(name: string, dependencies: string[] = []): void { - this.serviceStatuses.set(name, { - name, - initialized: false, - dependencies, - }); - - logger.debug("[ServiceInit] 🔧 Service registered:", { - name, - dependencies, - totalServices: this.serviceStatuses.size, - }); - } - - /** - * Mark a service as initialized - */ - markInitialized(name: string): void { - const status = this.serviceStatuses.get(name); - if (status) { - status.initialized = true; - logger.debug("[ServiceInit] ✅ Service initialized:", { - name, - totalInitialized: this.getInitializedCount(), - totalServices: this.serviceStatuses.size, - }); - } - } - - /** - * Mark a service as failed - */ - markFailed(name: string, error: string): void { - const status = this.serviceStatuses.get(name); - if (status) { - status.error = error; - logger.error("[ServiceInit] ❌ Service failed:", { - name, - error, - totalFailed: this.getFailedCount(), - }); - } - } - - /** - * Get count of initialized services - */ - private getInitializedCount(): number { - return Array.from(this.serviceStatuses.values()).filter( - (s) => s.initialized, - ).length; - } - - /** - * Get count of failed services - */ - private getFailedCount(): number { - return Array.from(this.serviceStatuses.values()).filter((s) => s.error) - .length; - } - - /** - * Wait for all services to be initialized - */ - async waitForInitialization(): Promise { - if (this.initializationPromise) { - return this.initializationPromise; - } - - this.initializationPromise = new Promise((resolve, reject) => { - const checkInterval = setInterval(() => { - const totalServices = this.serviceStatuses.size; - const initializedCount = this.getInitializedCount(); - const failedCount = this.getFailedCount(); - - logger.debug("[ServiceInit] 🔍 Initialization progress:", { - totalServices, - initializedCount, - failedCount, - remaining: totalServices - initializedCount - failedCount, - }); - - if (failedCount > 0) { - clearInterval(checkInterval); - const failedServices = Array.from(this.serviceStatuses.values()) - .filter((s) => s.error) - .map((s) => `${s.name}: ${s.error}`); - - const error = new Error( - `Service initialization failed: ${failedServices.join(", ")}`, - ); - logger.error("[ServiceInit] ❌ Initialization failed:", error); - reject(error); - } else if (initializedCount === totalServices) { - clearInterval(checkInterval); - logger.info( - "[ServiceInit] 🎉 All services initialized successfully:", - { - totalServices, - initializedCount, - }, - ); - resolve(); - } - }, 100); - - // Timeout after 30 seconds - setTimeout(() => { - clearInterval(checkInterval); - const error = new Error( - "Service initialization timeout after 30 seconds", - ); - logger.error("[ServiceInit] ⏰ Initialization timeout:", error); - reject(error); - }, 30000); - }); - - return this.initializationPromise; - } - - /** - * Get initialization status summary - */ - getStatusSummary(): { - total: number; - initialized: number; - failed: number; - pending: number; - services: ServiceStatus[]; - } { - const services = Array.from(this.serviceStatuses.values()); - const total = services.length; - const initialized = services.filter((s) => s.initialized).length; - const failed = services.filter((s) => s.error).length; - const pending = total - initialized - failed; - - return { - total, - initialized, - failed, - pending, - services, - }; - } - - /** - * Reset the manager (useful for testing) - */ - reset(): void { - this.serviceStatuses.clear(); - this.initializationPromise = null; - logger.debug("[ServiceInit] 🔄 Manager reset"); - } -} - -/** - * Convenience function to get the service initialization manager - */ -export const getServiceInitManager = (): ServiceInitializationManager => { - return ServiceInitializationManager.getInstance(); -}; diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 669cf8e2..3ae1ccee 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -754,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"; @@ -815,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(); @@ -918,7 +921,6 @@ export default class AccountViewView extends Vue { imageLimits: ImageRateLimits | null = null; limitsMessage: string = ""; - private profileService!: ProfileService; private notify!: ReturnType; created() { @@ -957,24 +959,17 @@ export default class AccountViewView extends Vue { await this.initializeState(); await this.processIdentity(); - // FIXED: Create ProfileService AFTER settings are loaded to get correct partnerApiServer - this.profileService = createProfileService( - this.axios, - this.partnerApiServer, - ); - - logger.info( - "[AccountViewView] ✅ ProfileService created with correct partnerApiServer:", + // Profile service logic now inlined - no need for external service + logger.debug( + "[AccountViewView] Profile logic ready with partnerApiServer:", { partnerApiServer: this.partnerApiServer, - component: "AccountViewView", - timestamp: new Date().toISOString(), }, ); 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; @@ -1694,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, ); @@ -1713,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, @@ -1758,7 +1753,7 @@ export default class AccountViewView extends Vue { async deleteProfile(): Promise { try { - 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 = ""; @@ -1871,5 +1866,215 @@ 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 { + 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 + if (error && typeof error === "object" && "response" in error) { + const axiosError = error as { response?: { status?: number } }; + if (axiosError.response?.status === 404) { + logger.debug( + "[AccountViewView] Profile not found (404) - this is normal for new users", + ); + return null; + } + } + + logger.error("[AccountViewView] Failed to load profile:", error); + throw new Error("Failed to load profile"); + } + } + + /** + * Save user profile to the partner API + */ + private async saveProfileToServer( + did: string, + profileData: ProfileData, + ): Promise { + 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) { + logger.error("[AccountViewView] Failed to save profile:", error); + 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 { + 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) { + logger.error("[AccountViewView] Failed to delete profile:", error); + return false; + } + } } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 3a910119..d3cc9444 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -603,15 +603,12 @@ export default class HomeView extends Vue { /** * Ensures correct API server configuration * - * FIXED: Now respects user preferences instead of forcing production server - * * @internal * Called after loading settings to ensure correct API endpoint */ private async ensureCorrectApiServer() { const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app"); - // FIXED: Remove forced override - respect user preferences // Only set default if no user preference exists if (!this.apiServer) { // Set default API server for any platform if not already set diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue index 3dcc6972..4de52280 100644 --- a/src/views/IdentitySwitcherView.vue +++ b/src/views/IdentitySwitcherView.vue @@ -229,7 +229,7 @@ export default class IdentitySwitcherView extends Vue { if (did) { try { const newSettings = await this.$accountSettings(did); - logger.info( + logger.debug( "[IdentitySwitcher Settings Trace] ✅ New account settings loaded", { did, @@ -252,7 +252,7 @@ export default class IdentitySwitcherView extends Vue { } } - logger.info( + logger.debug( "[IdentitySwitcher Settings Trace] 🔄 Navigating to home to trigger watcher", { newDid: did, diff --git a/src/views/StartView.vue b/src/views/StartView.vue index 93c196fd..de21d2e9 100644 --- a/src/views/StartView.vue +++ b/src/views/StartView.vue @@ -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" }); } } From ceceabf7b54477b86e72a0e82dee934a54aa0d43 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 26 Aug 2025 08:14:12 +0000 Subject: [PATCH 16/23] 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" --- src/libs/endorserServer.ts | 195 ++++++++++++++++++++++++------------- 1 file changed, 129 insertions(+), 66 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 8ab98ac6..85216aa7 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -485,6 +485,15 @@ const planCache: LRUCache = 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 +>(); + /** * Retrieves plan data from cache or server * @param {string} handleId - Plan handle ID @@ -504,86 +513,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); - - // 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:", { + + // 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} 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 { + 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, - apiServer, - endpoint: url, - requesterDid, + status: resp.status, + hasData: !!resp.data?.data, + dataLength: resp.data?.data?.length || 0, timestamp: new Date().toISOString(), }); - try { - const resp = await axios.get(url, { headers }); + if (resp.status === 200 && resp.data?.data?.length > 0) { + const cred = resp.data.data[0]; + planCache.set(handleId, cred); - logger.debug("[Plan Loading] ✅ Plan loaded successfully:", { + logger.debug("[Plan Loading] 💾 Plan cached:", { requestId, handleId, - status: resp.status, - hasData: !!resp.data?.data, - dataLength: resp.data?.data?.length || 0, - timestamp: new Date().toISOString(), + planName: cred?.name, + planIssuer: cred?.issuerDid, }); - if (resp.status === 200 && resp.data?.data?.length > 0) { - cred = resp.data.data[0]; - planCache.set(handleId, cred); - - logger.debug("[Plan Loading] 💾 Plan cached:", { - requestId, - handleId, - planName: cred?.name, - planIssuer: cred?.issuerDid, - }); - } 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), - ); - } - } catch (error) { - // Enhanced error logging for plan loading failures - const axiosError = error as { - response?: { - data?: unknown; - status?: number; - statusText?: string; - }; - message?: string; - }; + 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; - logger.error("[Plan Loading] ❌ Failed to load plan:", { - requestId, + log( + "[Plan Loading] âš ī¸ Plan cache is empty for handle", 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(), - }); + " 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; } /** From 4f78bfe7440084e73e4ce0cce69655de17bdbc6a Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 26 Aug 2025 09:22:25 +0000 Subject: [PATCH 17/23] 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 --- src/views/AccountViewView.vue | 143 +++++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 10 deletions(-) diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 3ae1ccee..e68efca2 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1496,7 +1496,7 @@ export default class AccountViewView extends Vue { const previousApiServer = this.apiServer; const newApiServer = this.apiServerInput; - logger.info("[Server Switching] Claim URL change initiated:", { + logger.debug("[Server Switching] Claim URL change initiated:", { did: this.activeDid, previousServer: previousApiServer, newServer: newApiServer, @@ -1515,7 +1515,7 @@ export default class AccountViewView extends Vue { }); // Log successful server switch - logger.info("[Server Switching] Claim URL change completed:", { + logger.debug("[Server Switching] Claim URL change completed:", { did: this.activeDid, previousServer: previousApiServer, newServer: newApiServer, @@ -1530,7 +1530,7 @@ export default class AccountViewView extends Vue { const previousPartnerServer = this.partnerApiServer; const newPartnerServer = this.partnerApiServerInput; - logger.info("[Server Switching] Partner server change initiated:", { + logger.debug("[Server Switching] Partner server change initiated:", { did: this.activeDid, previousServer: previousPartnerServer, newServer: newPartnerServer, @@ -1548,7 +1548,7 @@ export default class AccountViewView extends Vue { }); // Log successful partner server switch - logger.info("[Server Switching] Partner server change completed:", { + logger.debug("[Server Switching] Partner server change completed:", { did: this.activeDid, previousServer: previousPartnerServer, newServer: newPartnerServer, @@ -1935,18 +1935,54 @@ export default class AccountViewView extends Vue { return null; } catch (error: unknown) { - // Handle specific HTTP status codes + // 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.debug( - "[AccountViewView] Profile not found (404) - this is normal for new users", + 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; + } } - logger.error("[AccountViewView] Failed to load profile:", error); + // 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"); } } @@ -2003,7 +2039,54 @@ export default class AccountViewView extends Vue { return true; } catch (error: unknown) { - logger.error("[AccountViewView] Failed to save profile:", error); + // 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"); } } @@ -2072,7 +2155,47 @@ export default class AccountViewView extends Vue { return true; } catch (error: unknown) { - logger.error("[AccountViewView] Failed to delete profile:", error); + // 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; } } From 98f97f2dc972091b4db30f750dbc3f5efaa7f01a Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 27 Aug 2025 09:45:42 +0000 Subject: [PATCH 18/23] 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. --- src/utils/PlatformServiceMixin.ts | 56 ++++++++----------------------- src/views/ContactAmountsView.vue | 4 +-- 2 files changed, 16 insertions(+), 44 deletions(-) diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index c36ede9a..010d79ec 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -437,30 +437,18 @@ 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 { try { - // FIXED: Use specific queries instead of ambiguous OR condition - let result; - - // Check if this is a master settings key (numeric or "1") - if (key === MASTER_SETTINGS_KEY || key === "1" || !isNaN(Number(key))) { - // Master settings: query by id - result = await this.$dbQuery("SELECT * FROM settings WHERE id = ?", [ - key, - ]); - } else { - // Account settings: query by accountDid - result = await this.$dbQuery( - "SELECT * FROM settings WHERE accountDid = ?", - [key], - ); - } + // Master settings: query by id + const result = await this.$dbQuery( + "SELECT * FROM settings WHERE id = ?", + [MASTER_SETTINGS_KEY], + ); if (!result?.values?.length) { return fallback; @@ -484,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; @@ -503,10 +490,7 @@ export const PlatformServiceMixin = { ): Promise { 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) { @@ -769,7 +753,7 @@ export const PlatformServiceMixin = { * @returns Fresh settings object from database */ async $settings(defaults: Settings = {}): Promise { - const settings = await this.$getSettings(MASTER_SETTINGS_KEY, defaults); + const settings = await this.$getMasterSettings(defaults); if (!settings) { return defaults; @@ -802,10 +786,7 @@ export const PlatformServiceMixin = { ): Promise { try { // Get default settings first - const defaultSettings = await this.$getSettings( - MASTER_SETTINGS_KEY, - defaults, - ); + const defaultSettings = await this.$getMasterSettings(defaults); if (!defaultSettings) { return defaults; @@ -1590,10 +1571,7 @@ export const PlatformServiceMixin = { async $debugMergedSettings(did: string): Promise { try { // Get default settings - const defaultSettings = await this.$getSettings( - MASTER_SETTINGS_KEY, - {}, - ); + const defaultSettings = await this.$getMasterSettings({}); logger.info( `[PlatformServiceMixin] Default settings:`, defaultSettings, @@ -1640,10 +1618,7 @@ export interface IPlatformServiceMixin { ): Promise; $dbExec(sql: string, params?: unknown[]): Promise; $dbGetOneRow(sql: string, params?: unknown[]): Promise; - $getSettings( - key: string, - fallback?: Settings | null, - ): Promise; + $getMasterSettings(fallback?: Settings | null): Promise; $getMergedSettings( defaultKey: string, accountDid?: string, @@ -1765,10 +1740,7 @@ declare module "@vue/runtime-core" { sql: string, params?: unknown[], ): Promise; - $getSettings( - key: string, - defaults?: Settings | null, - ): Promise; + $getMasterSettings(defaults?: Settings | null): Promise; $getMergedSettings( key: string, did?: string, diff --git a/src/views/ContactAmountsView.vue b/src/views/ContactAmountsView.vue index 233ddbce..56ee2061 100644 --- a/src/views/ContactAmountsView.vue +++ b/src/views/ContactAmountsView.vue @@ -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,7 +223,7 @@ export default class ContactAmountssView extends Vue { const contact = await this.$getContact(contactDid); this.contact = contact; - const settings = await this.$getSettings(MASTER_SETTINGS_KEY); + const settings = await this.$getMasterSettings(); this.activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; From 8827c4a973e4252d115161a39c4d589612295be2 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 27 Aug 2025 19:04:27 +0800 Subject: [PATCH 19/23] fix(electron): resolve TypeScript errors in Electron build configuration - Create separate Electron-specific capacitor config - Update build script to not copy main config to Electron directory - Fix TypeScript compilation by excluding main config from Electron tsconfig Resolves TypeScript compilation errors in npm run build:electron:dev --- BUILDING.md | 3 +- electron/capacitor.config.ts | 116 +++++++++++++++++++++++++++++++++++ electron/package-lock.json | 1 - electron/tsconfig.json | 2 +- scripts/build-electron.sh | 2 +- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 electron/capacitor.config.ts diff --git a/BUILDING.md b/BUILDING.md index d34702c0..955ab43b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -617,7 +617,8 @@ The Electron build process follows a multi-stage approach: #### **Stage 2: Capacitor Sync** - Copies web assets to Electron app directory -- Syncs Capacitor configuration and plugins +- Uses Electron-specific Capacitor configuration (not copied from main config) +- Syncs Capacitor plugins for Electron platform - Prepares native module bindings #### **Stage 3: TypeScript Compile** diff --git a/electron/capacitor.config.ts b/electron/capacitor.config.ts new file mode 100644 index 00000000..24ef38c6 --- /dev/null +++ b/electron/capacitor.config.ts @@ -0,0 +1,116 @@ +import { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'app.timesafari', + appName: 'TimeSafari', + webDir: 'dist', + server: { + cleartext: true + }, + plugins: { + App: { + appUrlOpen: { + handlers: [ + { + url: 'timesafari://*', + autoVerify: true + } + ] + } + }, + SplashScreen: { + launchShowDuration: 3000, + launchAutoHide: true, + backgroundColor: '#ffffff', + androidSplashResourceName: 'splash', + androidScaleType: 'CENTER_CROP', + showSpinner: false, + androidSpinnerStyle: 'large', + iosSpinnerStyle: 'small', + spinnerColor: '#999999', + splashFullScreen: true, + splashImmersive: true + }, + CapSQLite: { + iosDatabaseLocation: 'Library/CapacitorDatabase', + iosIsEncryption: false, + iosBiometric: { + biometricAuth: false, + biometricTitle: 'Biometric login for TimeSafari' + }, + androidIsEncryption: false, + androidBiometric: { + biometricAuth: false, + biometricTitle: 'Biometric login for TimeSafari' + }, + electronIsEncryption: false + } + }, + ios: { + contentInset: 'never', + allowsLinkPreview: true, + scrollEnabled: true, + limitsNavigationsToAppBoundDomains: true, + backgroundColor: '#ffffff', + allowNavigation: [ + '*.timesafari.app', + '*.jsdelivr.net', + 'api.endorser.ch' + ] + }, + android: { + allowMixedContent: true, + captureInput: true, + webContentsDebuggingEnabled: false, + allowNavigation: [ + '*.timesafari.app', + '*.jsdelivr.net', + 'api.endorser.ch', + '10.0.2.2:3000' + ] + }, + electron: { + deepLinking: { + schemes: ['timesafari'] + }, + buildOptions: { + appId: 'app.timesafari', + productName: 'TimeSafari', + directories: { + output: 'dist-electron-packages' + }, + files: [ + 'dist/**/*', + 'electron/**/*' + ], + mac: { + category: 'public.app-category.productivity', + target: [ + { + target: 'dmg', + arch: ['x64', 'arm64'] + } + ] + }, + win: { + target: [ + { + target: 'nsis', + arch: ['x64'] + } + ] + }, + linux: { + target: [ + { + target: 'AppImage', + arch: ['x64'] + } + ], + category: 'Utility' + } + } + } +}; + +export default config; diff --git a/electron/package-lock.json b/electron/package-lock.json index 98a7fbdd..9cf915f4 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -56,7 +56,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz", "integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==", - "license": "MIT", "dependencies": { "jeep-sqlite": "^2.7.2" }, diff --git a/electron/tsconfig.json b/electron/tsconfig.json index b590aebb..d6057ede 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -1,6 +1,6 @@ { "compileOnSave": true, - "include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"], + "include": ["./src/**/*"], "compilerOptions": { "outDir": "./build", "importHelpers": true, diff --git a/scripts/build-electron.sh b/scripts/build-electron.sh index 7e7756b8..96213afa 100755 --- a/scripts/build-electron.sh +++ b/scripts/build-electron.sh @@ -181,7 +181,7 @@ sync_capacitor() { copy_web_assets() { log_info "Copying web assets to Electron" safe_execute "Copying assets" "cp -r dist/* electron/app/" - safe_execute "Copying config" "cp capacitor.config.json electron/capacitor.config.json" + # Note: Electron has its own capacitor.config.ts file, so we don't copy the main config } # Compile TypeScript From 5780d96cdc8a60d5e07a98d5bd53d6d0e3872ec6 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 29 Aug 2025 06:50:16 +0000 Subject: [PATCH 20/23] chore: linting --- src/views/ProjectViewView.vue | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 01d0cdfd..361c822f 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -243,13 +243,19 @@ :project-name="name" /> -

Offered To This Idea

+

+ Offered To This Idea +

- (None yet. Wanna - offer something… especially if others join you?) + (None yet. + Wanna + offer something… especially if others join you?)
    @@ -325,7 +331,9 @@
-

Given To This Project

+

+ Given To This Project +

(None yet. If you've seen something, say something by clicking a @@ -498,7 +506,9 @@ Benefitted From This Project -
(None yet.)
+
+ (None yet.) +
  • Date: Fri, 29 Aug 2025 16:41:19 +0800 Subject: [PATCH 21/23] fix: persist identity names per user instead of globally Fixes issue where identity names were not saved when switching between multiple identities. Names were being saved to master settings instead of user-specific settings. Changes: - UserNameDialog: Load/save names from/to user-specific settings - NewEditAccountView: Save names to user-specific settings for active DID - Both components now use $accountSettings() and $saveUserSettings() instead of $settings() and $updateSettings() Each identity now properly retains their assigned name when switching between identities. Previously only "User Zero" would show their name due to using master settings instead of per-identity settings. Fixes: Identity name persistence across identity switches --- src/components/UserNameDialog.vue | 16 ++++++++++++++-- src/views/NewEditAccountView.vue | 22 ++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue index d71ee9fc..47607dac 100644 --- a/src/components/UserNameDialog.vue +++ b/src/components/UserNameDialog.vue @@ -84,7 +84,8 @@ export default class UserNameDialog extends Vue { */ async open(aCallback?: (name?: string) => void) { this.callback = aCallback || this.callback; - const settings = await this.$settings(); + // Load from account-specific settings instead of master settings + const settings = await this.$accountSettings(); this.givenName = settings.firstName || ""; this.visible = true; } @@ -95,7 +96,18 @@ export default class UserNameDialog extends Vue { */ async onClickSaveChanges() { try { - await this.$updateSettings({ firstName: this.givenName }); + // Get the current active DID to save to user-specific settings + const settings = await this.$accountSettings(); + const activeDid = settings.activeDid; + + if (activeDid) { + // Save to user-specific settings for the current identity + await this.$saveUserSettings(activeDid, { firstName: this.givenName }); + } else { + // Fallback to master settings if no active DID + await this.$saveSettings({ firstName: this.givenName }); + } + this.visible = false; this.callback(this.givenName); } catch (error) { diff --git a/src/views/NewEditAccountView.vue b/src/views/NewEditAccountView.vue index 78e709f2..98be3282 100644 --- a/src/views/NewEditAccountView.vue +++ b/src/views/NewEditAccountView.vue @@ -110,10 +110,24 @@ export default class NewEditAccountView extends Vue { * @async */ async onClickSaveChanges() { - await this.$updateSettings({ - firstName: this.givenName, - lastName: "", // deprecated, pre v 0.1.3 - }); + // Get the current active DID to save to user-specific settings + const settings = await this.$accountSettings(); + const activeDid = settings.activeDid; + + if (activeDid) { + // Save to user-specific settings for the current identity + await this.$saveUserSettings(activeDid, { + firstName: this.givenName, + lastName: "", // deprecated, pre v 0.1.3 + }); + } else { + // Fallback to master settings if no active DID + await this.$saveSettings({ + firstName: this.givenName, + lastName: "", // deprecated, pre v 0.1.3 + }); + } + this.$router.back(); } From dde37e73e1a7c9de98fdcab8eb1ae318b43a201b Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 29 Aug 2025 16:41:46 +0800 Subject: [PATCH 22/23] Lint fixes --- src/components/UserNameDialog.vue | 4 ++-- src/views/NewEditAccountView.vue | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue index 47607dac..7a426e7f 100644 --- a/src/components/UserNameDialog.vue +++ b/src/components/UserNameDialog.vue @@ -99,7 +99,7 @@ export default class UserNameDialog extends Vue { // Get the current active DID to save to user-specific settings const settings = await this.$accountSettings(); const activeDid = settings.activeDid; - + if (activeDid) { // Save to user-specific settings for the current identity await this.$saveUserSettings(activeDid, { firstName: this.givenName }); @@ -107,7 +107,7 @@ export default class UserNameDialog extends Vue { // Fallback to master settings if no active DID await this.$saveSettings({ firstName: this.givenName }); } - + this.visible = false; this.callback(this.givenName); } catch (error) { diff --git a/src/views/NewEditAccountView.vue b/src/views/NewEditAccountView.vue index 98be3282..7db96689 100644 --- a/src/views/NewEditAccountView.vue +++ b/src/views/NewEditAccountView.vue @@ -113,7 +113,7 @@ export default class NewEditAccountView extends Vue { // Get the current active DID to save to user-specific settings const settings = await this.$accountSettings(); const activeDid = settings.activeDid; - + if (activeDid) { // Save to user-specific settings for the current identity await this.$saveUserSettings(activeDid, { @@ -127,7 +127,7 @@ export default class NewEditAccountView extends Vue { lastName: "", // deprecated, pre v 0.1.3 }); } - + this.$router.back(); } From 5f8d1fc8c6529efae09abf69ca24950ae3374a14 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 1 Sep 2025 16:54:36 +0800 Subject: [PATCH 23/23] refactor: remove deprecated lastName field from user settings - Remove lastName field from $saveUserSettings and $saveSettings calls - Clean up deprecated pre v0.1.3 code --- src/views/NewEditAccountView.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/views/NewEditAccountView.vue b/src/views/NewEditAccountView.vue index 7db96689..d1349e5a 100644 --- a/src/views/NewEditAccountView.vue +++ b/src/views/NewEditAccountView.vue @@ -118,13 +118,11 @@ export default class NewEditAccountView extends Vue { // Save to user-specific settings for the current identity await this.$saveUserSettings(activeDid, { firstName: this.givenName, - lastName: "", // deprecated, pre v 0.1.3 }); } else { // Fallback to master settings if no active DID await this.$saveSettings({ firstName: this.givenName, - lastName: "", // deprecated, pre v 0.1.3 }); }