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 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_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 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: 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 diff --git a/BUILDING.md b/BUILDING.md index d34702c0..1ac4ae9d 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** @@ -2768,6 +2769,45 @@ configuration files in the repository. --- +### 2025-08-26 - Capacitor Plugin Additions + +#### New Capacitor Plugins Added +- **Added**: `@capacitor/clipboard` v6.0.2 - Clipboard functionality for mobile platforms + - **Purpose**: Enable copy/paste operations on mobile devices + - **Platforms**: iOS and Android + - **Features**: Read/write clipboard content, text handling + - **Integration**: Automatically included in mobile builds + +- **Added**: `@capacitor/status-bar` v6.0.2 - Status bar management for mobile platforms + - **Purpose**: Control mobile device status bar appearance and behavior + - **Platforms**: iOS and Android + - **Features**: Status bar styling, visibility control, color management + - **Integration**: Automatically included in mobile builds + +#### Android Build System Updates +- **Modified**: `android/capacitor.settings.gradle` - Added new plugin project includes + - **Added**: `:capacitor-clipboard` project directory mapping + - **Added**: `:capacitor-status-bar` project directory mapping + - **Impact**: New plugins now properly integrated into Android build process + +#### Package Dependencies +- **Updated**: `package.json` - Added new Capacitor plugin dependencies +- **Updated**: `package-lock.json` - Locked dependency versions for consistency +- **Version**: All new plugins use Capacitor 6.x compatible versions + +#### Build Process Impact +- **No Breaking Changes**: Existing build commands continue to work unchanged +- **Enhanced Mobile Features**: New clipboard and status bar capabilities available +- **Automatic Integration**: Plugins automatically included in mobile builds +- **Platform Support**: Both iOS and Android builds now include new functionality + +#### Testing Requirements +- **Mobile Builds**: Verify new plugins integrate correctly in iOS and Android builds +- **Functionality**: Test clipboard operations and status bar management on devices +- **Fallback**: Ensure graceful degradation when plugins are unavailable + +--- + **Note**: This documentation is maintained alongside the build system. For the most up-to-date information, refer to the actual script files and Vite configuration files in the repository. 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 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 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" /> diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue index 930284f6..a884af74 100644 --- a/src/components/TopMessage.vue +++ b/src/components/TopMessage.vue @@ -20,6 +20,7 @@ import { Component, Vue, Prop } from "vue-facing-decorator"; import { AppString, NotificationIface } from "../constants/app"; import { PlatformServiceMixin } from "../utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "../utils/notify"; +import { logger } from "../utils/logger"; @Component({ mixins: [PlatformServiceMixin], @@ -44,26 +45,49 @@ export default class TopMessage extends Vue { this.notify = createNotifyHelpers(this.$notify); try { - // Ultra-concise cached settings loading - replaces 50+ lines of logic! - const settings = await this.$accountSettings(undefined, { - activeDid: undefined, - apiServer: AppString.PROD_ENDORSER_API_SERVER, + // Load settings without overriding database values - fixes settings inconsistency + logger.debug("[TopMessage] 📥 Loading settings without overrides..."); + const settings = await this.$accountSettings(); + + logger.debug("[TopMessage] 📊 Settings loaded:", { + activeDid: settings.activeDid, + apiServer: settings.apiServer, + warnIfTestServer: settings.warnIfTestServer, + warnIfProdServer: settings.warnIfProdServer, + component: "TopMessage", + timestamp: new Date().toISOString(), }); + // Only show warnings if the user has explicitly enabled them if ( settings.warnIfTestServer && + settings.apiServer && settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER ) { const didPrefix = settings.activeDid?.slice(11, 15); this.message = "You're not using prod, user " + didPrefix; + logger.debug("[TopMessage] ⚠️ Test server warning displayed:", { + apiServer: settings.apiServer, + didPrefix: didPrefix, + }); } else if ( settings.warnIfProdServer && + settings.apiServer && settings.apiServer === AppString.PROD_ENDORSER_API_SERVER ) { const didPrefix = settings.activeDid?.slice(11, 15); this.message = "You are using prod, user " + didPrefix; + logger.debug("[TopMessage] ⚠️ Production server warning displayed:", { + apiServer: settings.apiServer, + didPrefix: didPrefix, + }); + } else { + logger.debug( + "[TopMessage] ℹ️ No warnings displayed - conditions not met", + ); } } catch (err: unknown) { + logger.error("[TopMessage] ❌ Error loading settings:", err); this.notify.error(JSON.stringify(err), TIMEOUTS.MODAL); } } diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue index d71ee9fc..7a426e7f 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/libs/endorserServer.ts b/src/libs/endorserServer.ts index 6b0d2792..667083bf 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -486,6 +486,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 @@ -505,40 +514,140 @@ export async function getPlanFromCache( if (!handleId) { return undefined; } - let cred = planCache.get(handleId); - if (!cred) { - const url = - apiServer + - "/api/v2/report/plans?handleId=" + - encodeURIComponent(handleId); - const headers = await getHeaders(requesterDid); - try { - const resp = await axios.get(url, { headers }); - if (resp.status === 200 && resp.data?.data?.length > 0) { - cred = resp.data.data[0]; - planCache.set(handleId, cred); - } else { - // Use debug level for development to reduce console noise - const isDevelopment = process.env.VITE_PLATFORM === "development"; - const log = isDevelopment ? logger.debug : logger.log; - - log( - "[EndorserServer] Plan cache is empty for handle", - handleId, - " Got data:", - JSON.stringify(resp.data), - ); - } - } catch (error) { - logger.error( - "[EndorserServer] Failed to load plan with handle", + + // 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, + status: resp.status, + hasData: !!resp.data?.data, + dataLength: resp.data?.data?.length || 0, + timestamp: new Date().toISOString(), + }); + + if (resp.status === 200 && resp.data?.data?.length > 0) { + const cred = resp.data.data[0]; + planCache.set(handleId, cred); + + logger.debug("[Plan Loading] 💾 Plan cached:", { + requestId, + handleId, + planName: cred?.name, + planIssuer: cred?.issuerDid, + }); + + return cred; + } else { + // Use debug level for development to reduce console noise + const isDevelopment = process.env.VITE_PLATFORM === "development"; + const log = isDevelopment ? logger.debug : logger.log; + + log( + "[Plan Loading] ⚠️ Plan cache is empty for handle", handleId, - " Got error:", - JSON.stringify(error), + " Got data:", + JSON.stringify(resp.data), ); + + return undefined; } + } catch (error) { + // Enhanced error logging for plan loading failures + const axiosError = error as { + response?: { + data?: unknown; + status?: number; + statusText?: string; + }; + message?: string; + }; + + logger.error("[Plan Loading] ❌ Failed to load plan:", { + requestId, + handleId, + apiServer, + endpoint: url, + requesterDid, + errorStatus: axiosError.response?.status, + errorStatusText: axiosError.response?.statusText, + errorData: axiosError.response?.data, + errorMessage: axiosError.message || String(error), + timestamp: new Date().toISOString(), + }); + + throw error; } - return cred; } /** @@ -1019,19 +1128,82 @@ export async function createAndSubmitClaim( const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload); + // Enhanced diagnostic logging for claim submission + const requestId = `claim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + logger.info("[Claim Submission] 🚀 Starting claim submission:", { + requestId, + apiServer, + requesterDid: issuerDid, + endpoint: `${apiServer}/api/v2/claim`, + timestamp: new Date().toISOString(), + jwtLength: vcJwt.length, + }); + // Make the xhr request payload const payload = JSON.stringify({ jwtEncoded: vcJwt }); const url = `${apiServer}/api/v2/claim`; + logger.debug("[Claim Submission] 📡 Making API request:", { + requestId, + url, + payloadSize: payload.length, + headers: { "Content-Type": "application/json" }, + }); + const response = await axios.post(url, payload, { headers: { "Content-Type": "application/json", }, }); + logger.info("[Claim Submission] ✅ Claim submitted successfully:", { + requestId, + status: response.status, + handleId: response.data?.handleId, + responseSize: JSON.stringify(response.data).length, + timestamp: new Date().toISOString(), + }); + return { success: true, handleId: response.data?.handleId }; } catch (error: unknown) { - logger.error("Error submitting claim:", error); + // Enhanced error logging with comprehensive context + const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + const axiosError = error as { + response?: { + data?: { error?: { code?: string; message?: string } }; + status?: number; + statusText?: string; + headers?: Record; + }; + 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 @@ -1494,14 +1666,56 @@ export async function fetchEndorserRateLimits( ) { const url = `${apiServer}/api/report/rateLimits`; const headers = await getHeaders(issuerDid); + + // Enhanced diagnostic logging for user registration tracking + logger.debug("[User Registration] Checking user status on server:", { + did: issuerDid, + server: apiServer, + endpoint: url, + timestamp: new Date().toISOString(), + }); + try { const response = await axios.get(url, { headers } as AxiosRequestConfig); + + // Log successful registration check + logger.debug("[User Registration] User registration check successful:", { + did: issuerDid, + server: apiServer, + status: response.status, + isRegistered: true, + timestamp: new Date().toISOString(), + }); + return response; } catch (error) { + // Enhanced error logging with user registration context + const axiosError = error as { + response?: { + data?: { error?: { code?: string; message?: string } }; + status?: number; + }; + }; + const errorCode = axiosError.response?.data?.error?.code; + const errorMessage = axiosError.response?.data?.error?.message; + const httpStatus = axiosError.response?.status; + + logger.warn("[User Registration] User not registered on server:", { + did: issuerDid, + server: apiServer, + errorCode: errorCode, + errorMessage: errorMessage, + httpStatus: httpStatus, + needsRegistration: true, + timestamp: new Date().toISOString(), + }); + + // Log the original error for debugging logger.error( `[fetchEndorserRateLimits] Error for DID ${issuerDid}:`, errorStringForLog(error), ); + throw error; } } @@ -1514,8 +1728,53 @@ export async function fetchEndorserRateLimits( * @param {string} issuerDid - The DID for which to check rate limits. * @returns {Promise} The Axios response object. */ -export async function fetchImageRateLimits(axios: Axios, issuerDid: string) { - const url = DEFAULT_IMAGE_API_SERVER + "/image-limits"; +export async function fetchImageRateLimits( + axios: Axios, + issuerDid: string, + imageServer?: string, +) { + const server = imageServer || DEFAULT_IMAGE_API_SERVER; + const url = server + "/image-limits"; const headers = await getHeaders(issuerDid); - return await axios.get(url, { headers } as AxiosRequestConfig); + + // Enhanced diagnostic logging for image server calls + logger.debug("[Image Server] Checking image rate limits:", { + did: issuerDid, + server: server, + endpoint: url, + timestamp: new Date().toISOString(), + }); + + try { + const response = await axios.get(url, { headers } as AxiosRequestConfig); + + // Log successful image server call + logger.debug("[Image Server] Image rate limits check successful:", { + did: issuerDid, + server: server, + status: response.status, + timestamp: new Date().toISOString(), + }); + + return response; + } catch (error) { + // Enhanced error logging for image server failures + const axiosError = error as { + response?: { + data?: { error?: { code?: string; message?: string } }; + status?: number; + }; + }; + + logger.warn("[Image Server] Image rate limits check failed:", { + did: issuerDid, + server: server, + errorCode: axiosError.response?.data?.error?.code, + errorMessage: axiosError.response?.data?.error?.message, + httpStatus: axiosError.response?.status, + timestamp: new Date().toISOString(), + }); + + throw error; + } } diff --git a/src/libs/util.ts b/src/libs/util.ts index c5ab0d42..83004078 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -984,13 +984,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) { @@ -1016,7 +1019,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/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..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, @@ -337,6 +337,22 @@ router.beforeEach(async (to, _from, next) => { }); try { + // Log boot-time configuration on first navigation + if (!_from) { + logger.info( + "[Router] 🚀 First navigation detected - logging boot-time configuration:", + { + platform: process.env.VITE_PLATFORM, + defaultEndorserApiServer: + process.env.VITE_DEFAULT_ENDORSER_API_SERVER, + defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER, + nodeEnv: process.env.NODE_ENV, + targetRoute: to.path, + timestamp: new Date().toISOString(), + }, + ); + } + // Skip identity check for routes that handle identity creation manually const skipIdentityRoutes = [ "/start", @@ -352,11 +368,11 @@ router.beforeEach(async (to, _from, next) => { return next(); } - logger.info(`[Router] 🔍 Checking user identity for route: ${to.path}`); + logger.debug(`[Router] 🔍 Checking user identity for route: ${to.path}`); // Check if user has any identities const allMyDids = await retrieveAccountDids(); - logger.info(`[Router] 📋 Found ${allMyDids.length} user identities`); + logger.debug(`[Router] 📋 Found ${allMyDids.length} user identities`); if (allMyDids.length === 0) { logger.info("[Router] ⚠️ No identities found, creating default identity"); @@ -366,7 +382,7 @@ router.beforeEach(async (to, _from, next) => { logger.info("[Router] ✅ Default identity created successfully"); } else { - logger.info( + logger.debug( `[Router] ✅ User has ${allMyDids.length} identities, proceeding`, ); } @@ -392,7 +408,7 @@ router.beforeEach(async (to, _from, next) => { // Add navigation success logging router.afterEach((to, from) => { - logger.info(`[Router] ✅ Navigation completed:`, { + logger.debug(`[Router] ✅ Navigation completed:`, { from: from?.path || "none", to: to.path, name: to.name, diff --git a/src/services/ProfileService.ts b/src/services/ProfileService.ts deleted file mode 100644 index 3ec64edd..00000000 --- a/src/services/ProfileService.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * ProfileService - Handles user profile operations and API calls - * Extracted from AccountViewView.vue to improve separation of concerns - */ - -import { AxiosInstance, AxiosError } from "axios"; -import { UserProfile } from "@/libs/partnerServer"; -import { UserProfileResponse } from "@/interfaces/accountView"; -import { getHeaders, errorStringForLog } from "@/libs/endorserServer"; -import { handleApiError } from "./api"; -import { logger } from "@/utils/logger"; -import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView"; - -/** - * Profile data interface - */ -export interface ProfileData { - description: string; - latitude: number; - longitude: number; - includeLocation: boolean; -} - -/** - * Profile service class - */ -export class ProfileService { - private axios: AxiosInstance; - private partnerApiServer: string; - - constructor(axios: AxiosInstance, partnerApiServer: string) { - this.axios = axios; - this.partnerApiServer = partnerApiServer; - } - - /** - * Load user profile from the server - * @param activeDid - The user's DID - * @returns ProfileData or null if profile doesn't exist - */ - async loadProfile(activeDid: string): Promise { - try { - const headers = await getHeaders(activeDid); - const response = await this.axios.get( - `${this.partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`, - { headers }, - ); - - if (response.status === 200) { - const data = response.data.data; - const profileData: ProfileData = { - description: data.description || "", - latitude: data.locLat || 0, - longitude: data.locLon || 0, - includeLocation: !!(data.locLat && data.locLon), - }; - return profileData; - } else { - throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.UNABLE_TO_LOAD_PROFILE); - } - } catch (error) { - if (this.isApiError(error) && error.response?.status === 404) { - // Profile doesn't exist yet - this is normal - return null; - } - - logger.error("Error loading profile:", errorStringForLog(error)); - handleApiError(error as AxiosError, "/api/partner/userProfileForIssuer"); - return null; - } - } - - /** - * Save user profile to the server - * @param activeDid - The user's DID - * @param profileData - The profile data to save - * @returns true if successful, false otherwise - */ - async saveProfile( - activeDid: string, - profileData: ProfileData, - ): Promise { - try { - const headers = await getHeaders(activeDid); - const payload: UserProfile = { - description: profileData.description, - issuerDid: activeDid, - }; - - // Add location data if location is included - if ( - profileData.includeLocation && - profileData.latitude && - profileData.longitude - ) { - payload.locLat = profileData.latitude; - payload.locLon = profileData.longitude; - } - - const response = await this.axios.post( - `${this.partnerApiServer}/api/partner/userProfile`, - payload, - { headers }, - ); - - if (response.status === 201) { - return true; - } else { - logger.error("Error saving profile:", response); - throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED); - } - } catch (error) { - logger.error("Error saving profile:", errorStringForLog(error)); - handleApiError(error as AxiosError, "/api/partner/userProfile"); - return false; - } - } - - /** - * Delete user profile from the server - * @param activeDid - The user's DID - * @returns true if successful, false otherwise - */ - async deleteProfile(activeDid: string): Promise { - try { - const headers = await getHeaders(activeDid); - const url = `${this.partnerApiServer}/api/partner/userProfile`; - const response = await this.axios.delete(url, { headers }); - - if (response.status === 204 || response.status === 200) { - logger.info("Profile deleted successfully"); - return true; - } else { - logger.error("Unexpected response status when deleting profile:", { - status: response.status, - statusText: response.statusText, - data: response.data, - }); - throw new Error( - `Profile not deleted - HTTP ${response.status}: ${response.statusText}`, - ); - } - } catch (error) { - if (this.isApiError(error) && error.response) { - const response = error.response; - logger.error("API error deleting profile:", { - status: response.status, - statusText: response.statusText, - data: response.data, - url: this.getErrorUrl(error), - }); - - // Handle specific HTTP status codes - if (response.status === 204) { - logger.debug("Profile deleted successfully (204 No Content)"); - return true; // 204 is success for DELETE operations - } else if (response.status === 404) { - logger.warn("Profile not found - may already be deleted"); - return true; // Consider this a success if profile doesn't exist - } else if (response.status === 400) { - logger.error("Bad request when deleting profile:", response.data); - const errorMessage = - typeof response.data === "string" - ? response.data - : response.data?.message || "Bad request"; - throw new Error(`Profile deletion failed: ${errorMessage}`); - } else if (response.status === 401) { - logger.error("Unauthorized to delete profile"); - throw new Error("You are not authorized to delete this profile"); - } else if (response.status === 403) { - logger.error("Forbidden to delete profile"); - throw new Error("You are not allowed to delete this profile"); - } - } - - logger.error("Error deleting profile:", errorStringForLog(error)); - handleApiError(error as AxiosError, "/api/partner/userProfile"); - return false; - } - } - - /** - * Update profile location - * @param profileData - Current profile data - * @param latitude - New latitude - * @param longitude - New longitude - * @returns Updated profile data - */ - updateProfileLocation( - profileData: ProfileData, - latitude: number, - longitude: number, - ): ProfileData { - return { - ...profileData, - latitude, - longitude, - includeLocation: true, - }; - } - - /** - * Toggle location inclusion in profile - * @param profileData - Current profile data - * @returns Updated profile data - */ - toggleProfileLocation(profileData: ProfileData): ProfileData { - const includeLocation = !profileData.includeLocation; - return { - ...profileData, - latitude: includeLocation ? profileData.latitude : 0, - longitude: includeLocation ? profileData.longitude : 0, - includeLocation, - }; - } - - /** - * Clear profile location - * @param profileData - Current profile data - * @returns Updated profile data - */ - clearProfileLocation(profileData: ProfileData): ProfileData { - return { - ...profileData, - latitude: 0, - longitude: 0, - includeLocation: false, - }; - } - - /** - * Reset profile to default state - * @returns Default profile data - */ - getDefaultProfile(): ProfileData { - return { - description: "", - latitude: 0, - longitude: 0, - includeLocation: false, - }; - } - - /** - * Type guard for API errors with proper typing - */ - private isApiError(error: unknown): error is { - response?: { - status?: number; - statusText?: string; - data?: { message?: string } | string; - }; - } { - return typeof error === "object" && error !== null && "response" in error; - } - - /** - * Extract error URL safely from error object - */ - private getErrorUrl(error: unknown): string | undefined { - if (this.isAxiosError(error)) { - return error.config?.url; - } - if (this.isApiError(error) && this.hasConfigProperty(error)) { - const config = this.getConfigProperty(error); - return config?.url; - } - return undefined; - } - - /** - * Type guard to check if error has config property - */ - private hasConfigProperty( - error: unknown, - ): error is { config?: { url?: string } } { - return typeof error === "object" && error !== null && "config" in error; - } - - /** - * Safely extract config property from error - */ - private getConfigProperty(error: { - config?: { url?: string }; - }): { url?: string } | undefined { - return error.config; - } - - /** - * Type guard for AxiosError - */ - private isAxiosError(error: unknown): error is AxiosError { - return error instanceof AxiosError; - } -} - -/** - * Factory function to create a ProfileService instance - */ -export function createProfileService( - axios: AxiosInstance, - partnerApiServer: string, -): ProfileService { - return new ProfileService(axios, partnerApiServer); -} diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 68c09720..010d79ec 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -437,17 +437,17 @@ export const PlatformServiceMixin = { }, /** - * Utility method for retrieving and parsing settings + * Utility method for retrieving master settings * Common pattern used across many components */ - async $getSettings( - key: string, + async $getMasterSettings( fallback: Settings | null = null, ): Promise { try { + // Master settings: query by id const result = await this.$dbQuery( - "SELECT * FROM settings WHERE id = ? OR accountDid = ?", - [key, key], + "SELECT * FROM settings WHERE id = ?", + [MASTER_SETTINGS_KEY], ); if (!result?.values?.length) { @@ -472,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; @@ -491,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) { @@ -757,19 +753,20 @@ 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; } - // **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; } @@ -789,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; @@ -813,14 +807,17 @@ export const PlatformServiceMixin = { defaultSettings, ); - // **ELECTRON-SPECIFIC FIX**: Force production API endpoints for Electron - // This ensures Electron doesn't use localhost development servers that might be saved in user settings - if (process.env.VITE_PLATFORM === "electron") { + // FIXED: Remove forced override - respect user preferences + // Only set default if no user preference exists + if ( + !mergedSettings.apiServer && + process.env.VITE_PLATFORM === "electron" + ) { // Import constants dynamically to get platform-specific values const { DEFAULT_ENDORSER_API_SERVER } = await import( "../constants/app" ); - + // Only set if user hasn't specified a preference mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER; } @@ -1574,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, @@ -1624,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, @@ -1749,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/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(); +}; diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 8ff7902f..e68efca2 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -58,8 +58,10 @@ v-if="!isRegistered" :passkeys-enabled="PASSKEYS_ENABLED" :given-name="givenName" - message="Before you can publicly announce a new project or time commitment, - a friend needs to register you." + :message=" + `Before you can publicly announce a new project or time commitment, ` + + `a friend needs to register you.` + " /> @@ -752,6 +754,7 @@ import "leaflet/dist/leaflet.css"; import { Buffer } from "buffer/"; import "dexie-export-import"; + // @ts-expect-error - they aren't exporting it but it's there import { ImportProgress } from "dexie-export-import"; import { LeafletMouseEvent } from "leaflet"; @@ -813,11 +816,13 @@ import { isApiError, ImportContent, } from "@/interfaces/accountView"; -import { - ProfileService, - createProfileService, - ProfileData, -} from "@/services/ProfileService"; +// Profile data interface (inlined from ProfileService) +interface ProfileData { + description: string; + latitude: number; + longitude: number; + includeLocation: boolean; +} const inputImportFileNameRef = ref(); @@ -916,7 +921,6 @@ export default class AccountViewView extends Vue { imageLimits: ImageRateLimits | null = null; limitsMessage: string = ""; - private profileService!: ProfileService; private notify!: ReturnType; created() { @@ -926,7 +930,10 @@ export default class AccountViewView extends Vue { // This prevents the "Cannot read properties of undefined (reading 'Default')" error if (L.Icon.Default) { // Type-safe way to handle Leaflet icon prototype - const iconDefault = L.Icon.Default.prototype as Record; + const iconDefault = L.Icon.Default.prototype as unknown as Record< + string, + unknown + >; if ("_getIconUrl" in iconDefault) { delete iconDefault._getIconUrl; } @@ -948,17 +955,21 @@ export default class AccountViewView extends Vue { * @throws Will display specific messages to the user based on different errors. */ async mounted(): Promise { - this.profileService = createProfileService( - this.axios, - this.partnerApiServer, - ); try { await this.initializeState(); await this.processIdentity(); + // Profile service logic now inlined - no need for external service + logger.debug( + "[AccountViewView] Profile logic ready with partnerApiServer:", + { + partnerApiServer: this.partnerApiServer, + }, + ); + if (this.isRegistered) { try { - const profile = await this.profileService.loadProfile(this.activeDid); + const profile = await this.loadProfile(this.activeDid); if (profile) { this.userProfileDesc = profile.description; this.userProfileLatitude = profile.latitude; @@ -1411,21 +1422,24 @@ 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, 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; @@ -1451,7 +1465,26 @@ export default class AccountViewView extends Vue { } catch (error) { this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS; - logger.error("Error retrieving limits: ", error); + + // Enhanced error logging with server context + const axiosError = error as { + response?: { + data?: { error?: { code?: string; message?: string } }; + status?: number; + }; + }; + logger.error("[Server Limits] Error retrieving limits:", { + error: error instanceof Error ? error.message : String(error), + did: did, + apiServer: this.apiServer, + partnerApiServer: this.partnerApiServer, + errorCode: axiosError?.response?.data?.error?.code, + errorMessage: axiosError?.response?.data?.error?.message, + httpStatus: axiosError?.response?.status, + needsUserMigration: true, + timestamp: new Date().toISOString(), + }); + // this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD); } finally { this.loadingLimits = false; @@ -1459,24 +1492,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.debug("[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.debug("[Server Switching] Claim URL change completed:", { + did: this.activeDid, + previousServer: previousApiServer, + newServer: newApiServer, + changeType: "apiServer", + settingsSaved: true, + timestamp: new Date().toISOString(), + }); } async onClickSavePartnerServer(): Promise { + // Enhanced diagnostic logging for partner server changes + const previousPartnerServer = this.partnerApiServer; + const newPartnerServer = this.partnerApiServerInput; + + logger.debug("[Server Switching] Partner server change initiated:", { + did: this.activeDid, + previousServer: previousPartnerServer, + newServer: newPartnerServer, + changeType: "partnerApiServer", + timestamp: new Date().toISOString(), + }); + 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.debug("[Server Switching] Partner server change completed:", { + did: this.activeDid, + previousServer: previousPartnerServer, + newServer: newPartnerServer, + changeType: "partnerApiServer", + settingsSaved: true, + timestamp: new Date().toISOString(), + }); } async onClickSavePushServer(): Promise { @@ -1550,7 +1629,6 @@ export default class AccountViewView extends Vue { onMapReady(map: L.Map): void { try { - logger.debug("Map ready event fired, map object:", map); // doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2; @@ -1579,19 +1657,15 @@ export default class AccountViewView extends Vue { // Try to set map ready after component is mounted setTimeout(() => { this.isMapReady = true; - logger.debug("Map ready set to true after mounted"); }, 500); } // Fallback method to handle map initialization failures private handleMapInitFailure(): void { - logger.debug("Starting map initialization timeout (5 seconds)"); setTimeout(() => { if (!this.isMapReady) { logger.warn("Map failed to initialize, forcing ready state"); this.isMapReady = true; - } else { - logger.debug("Map initialized successfully, timeout not needed"); } }, 5000); // 5 second timeout } @@ -1615,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, ); @@ -1634,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, @@ -1679,8 +1753,7 @@ 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); + const success = await this.deleteProfileFromServer(this.activeDid); if (success) { this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED); this.userProfileDesc = ""; @@ -1688,7 +1761,6 @@ export default class AccountViewView extends Vue { this.userProfileLongitude = 0; this.includeUserProfileLocation = false; this.isMapReady = false; // Reset map state - logger.debug("Profile deleted successfully, UI state reset"); } else { this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR); } @@ -1734,7 +1806,6 @@ export default class AccountViewView extends Vue { this.isMapReady = false; this.userProfileLatitude = 0; this.userProfileLongitude = 0; - logger.debug("Location unchecked, map state reset"); } else { // Location checkbox was checked, start map initialization timeout this.isMapReady = false; @@ -1743,7 +1814,6 @@ export default class AccountViewView extends Vue { // Try to set map ready after a short delay to allow Vue to render setTimeout(() => { if (!this.isMapReady) { - logger.debug("Setting map ready after timeout"); this.isMapReady = true; } }, 1000); // 1 second delay @@ -1796,5 +1866,338 @@ export default class AccountViewView extends Vue { onRecheckLimits() { this.checkLimits(); } + + // Inlined profile methods (previously in ProfileService) + + /** + * Load user profile from the partner API + */ + private async loadProfile(did: string): Promise { + try { + const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + logger.debug("[AccountViewView] Loading profile:", { + requestId, + did, + partnerApiServer: this.partnerApiServer, + }); + + // Get authentication headers + const headers = await getHeaders(did); + + const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`; + + logger.debug("[AccountViewView] Making API request:", { + requestId, + did, + fullUrl, + hasAuthHeader: !!headers.Authorization, + }); + + const response = await this.axios.get(fullUrl, { headers }); + + logger.debug("[AccountViewView] Profile loaded successfully:", { + requestId, + status: response.status, + hasData: !!response.data, + }); + + if (response.data && response.data.data) { + const profileData = response.data.data; + logger.debug("[AccountViewView] Parsing profile data:", { + requestId, + locLat: profileData.locLat, + locLon: profileData.locLon, + description: profileData.description, + }); + + const result = { + description: profileData.description || "", + latitude: profileData.locLat || 0, + longitude: profileData.locLon || 0, + includeLocation: !!(profileData.locLat && profileData.locLon), + }; + + logger.debug("[AccountViewView] Parsed profile result:", { + requestId, + result, + hasLocation: result.includeLocation, + }); + + return result; + } else { + logger.debug("[AccountViewView] No profile data found in response:", { + requestId, + hasData: !!response.data, + hasDataData: !!(response.data && response.data.data), + }); + } + + return null; + } catch (error: unknown) { + // Handle specific HTTP status codes cleanly to suppress console spam + if (error && typeof error === "object" && "response" in error) { + const axiosError = error as { response?: { status?: number } }; + + if (axiosError.response?.status === 404) { + logger.info( + "[Profile] No profile found - this is normal for new users", + { + did, + server: this.partnerApiServer, + status: 404, + timestamp: new Date().toISOString(), + }, + ); + return null; + } + + if (axiosError.response?.status === 400) { + logger.warn("[Profile] Bad request - user may not be registered", { + did, + server: this.partnerApiServer, + status: 400, + timestamp: new Date().toISOString(), + }); + return null; + } + + if ( + axiosError.response?.status === 401 || + axiosError.response?.status === 403 + ) { + logger.warn("[Profile] Authentication/authorization issue", { + did, + server: this.partnerApiServer, + status: axiosError.response.status, + timestamp: new Date().toISOString(), + }); + return null; + } + } + + // Only log full errors for unexpected issues (5xx, network errors, etc.) + logger.error("[Profile] Unexpected error loading profile:", { + did, + server: this.partnerApiServer, + error: error instanceof Error ? error.message : String(error), + timestamp: new Date().toISOString(), + }); + throw new Error("Failed to load profile"); + } + } + + /** + * Save user profile to the partner API + */ + private async saveProfileToServer( + did: string, + profileData: ProfileData, + ): Promise { + try { + const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + logger.debug("[AccountViewView] Saving profile:", { + requestId, + did, + profileData, + }); + + // Get authentication headers + const headers = await getHeaders(did); + + // Prepare payload in the format expected by the partner API + const payload = { + description: profileData.description, + issuerDid: did, + ...(profileData.includeLocation && + profileData.latitude && + profileData.longitude + ? { + locLat: profileData.latitude, + locLon: profileData.longitude, + } + : {}), + }; + + logger.debug("[AccountViewView] Sending payload to server:", { + requestId, + payload, + hasLocation: profileData.includeLocation, + }); + + const response = await this.axios.post( + `${this.partnerApiServer}/api/partner/userProfile`, + payload, + { headers }, + ); + + logger.debug("[AccountViewView] Profile saved successfully:", { + requestId, + status: response.status, + }); + + return true; + } catch (error: unknown) { + // Handle specific HTTP status codes cleanly to suppress console spam + if (error && typeof error === "object" && "response" in error) { + const axiosError = error as { response?: { status?: number } }; + + if (axiosError.response?.status === 400) { + logger.warn("[Profile] Bad request saving profile", { + did, + server: this.partnerApiServer, + status: 400, + timestamp: new Date().toISOString(), + }); + throw new Error("Invalid profile data"); + } + + if ( + axiosError.response?.status === 401 || + axiosError.response?.status === 403 + ) { + logger.warn( + "[Profile] Authentication/authorization issue saving profile", + { + did, + server: this.partnerApiServer, + status: axiosError.response.status, + timestamp: new Date().toISOString(), + }, + ); + throw new Error("Authentication required"); + } + + if (axiosError.response?.status === 409) { + logger.warn("[Profile] Profile conflict - may already exist", { + did, + server: this.partnerApiServer, + status: 409, + timestamp: new Date().toISOString(), + }); + throw new Error("Profile already exists"); + } + } + + // Only log full errors for unexpected issues + logger.error("[Profile] Unexpected error saving profile:", { + did, + server: this.partnerApiServer, + error: error instanceof Error ? error.message : String(error), + timestamp: new Date().toISOString(), + }); + throw new Error("Failed to save profile"); + } + } + + /** + * Toggle profile location visibility + */ + private toggleProfileLocation(profileData: ProfileData): ProfileData { + const includeLocation = !profileData.includeLocation; + return { + ...profileData, + latitude: includeLocation ? profileData.latitude : 0, + longitude: includeLocation ? profileData.longitude : 0, + includeLocation, + }; + } + + /** + * Clear profile location + */ + private clearProfileLocation(profileData: ProfileData): ProfileData { + return { + ...profileData, + latitude: 0, + longitude: 0, + includeLocation: false, + }; + } + + /** + * Get default profile data + */ + private getDefaultProfile(): ProfileData { + return { + description: "", + latitude: 0, + longitude: 0, + includeLocation: false, + }; + } + + /** + * Delete user profile from the partner API + */ + private async deleteProfileFromServer(did: string): Promise { + try { + const requestId = `profile_delete_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + logger.debug("[AccountViewView] Deleting profile:", { + requestId, + did, + }); + + // Get authentication headers + const headers = await getHeaders(did); + + const response = await this.axios.delete( + `${this.partnerApiServer}/api/partner/userProfile/${did}`, + { headers }, + ); + + logger.debug("[AccountViewView] Profile deleted successfully:", { + requestId, + status: response.status, + }); + + return true; + } catch (error: unknown) { + // Handle specific HTTP status codes cleanly to suppress console spam + if (error && typeof error === "object" && "response" in error) { + const axiosError = error as { response?: { status?: number } }; + + if (axiosError.response?.status === 404) { + logger.info( + "[Profile] Profile not found for deletion - may already be deleted", + { + did, + server: this.partnerApiServer, + status: 404, + timestamp: new Date().toISOString(), + }, + ); + return true; // Consider it successful if already deleted + } + + if ( + axiosError.response?.status === 401 || + axiosError.response?.status === 403 + ) { + logger.warn( + "[Profile] Authentication/authorization issue deleting profile", + { + did, + server: this.partnerApiServer, + status: axiosError.response.status, + timestamp: new Date().toISOString(), + }, + ); + return false; + } + } + + // Only log full errors for unexpected issues + logger.error("[Profile] Unexpected error deleting profile:", { + did, + server: this.partnerApiServer, + error: error instanceof Error ? error.message : String(error), + timestamp: new Date().toISOString(), + }); + return false; + } + } } 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 || ""; diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index b9dbf017..45a5d5bb 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -568,10 +568,27 @@ export default class HomeView extends Vue { this.isRegistered = true; } } catch (error) { + // Enhanced error logging with server context + const errorMessage = + error instanceof Error ? error.message : String(error); + const axiosError = error as { + response?: { + data?: { error?: { code?: string; message?: string } }; + status?: number; + }; + }; + logger.warn( "[HomeView Settings Trace] ⚠️ Registration check failed", { - error: error instanceof Error ? error.message : String(error), + error: errorMessage, + did: this.activeDid, + server: this.apiServer, + errorCode: axiosError?.response?.data?.error?.code, + errorMessage: axiosError?.response?.data?.error?.message, + httpStatus: axiosError?.response?.status, + needsUserMigration: true, + timestamp: new Date().toISOString(), }, ); } @@ -585,8 +602,7 @@ export default class HomeView extends Vue { } /** - * Ensures API server is correctly set for the current platform - * For Electron, always use production endpoint regardless of saved settings + * Ensures correct API server configuration * * @internal * Called after loading settings to ensure correct API endpoint @@ -594,12 +610,9 @@ export default class HomeView extends Vue { private async ensureCorrectApiServer() { const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app"); - if (process.env.VITE_PLATFORM === "electron") { - // **CRITICAL FIX**: Always use production API server for Electron - // This prevents the capacitor-electron:// protocol from being used for API calls - this.apiServer = DEFAULT_ENDORSER_API_SERVER; - } else if (!this.apiServer) { - // **FIX**: Set default API server for web/development if not already set + // Only set default if no user preference exists + if (!this.apiServer) { + // Set default API server for any platform if not already set this.apiServer = DEFAULT_ENDORSER_API_SERVER; } } @@ -1161,9 +1174,13 @@ export default class HomeView extends Vue { location: fulfillsPlan ? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon } : null, - inSearchBox: fulfillsPlan - ? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon) - : null, + inSearchBox: + fulfillsPlan?.locLat && fulfillsPlan?.locLon + ? this.latLongInAnySearchBox( + fulfillsPlan.locLat, + fulfillsPlan.locLon, + ) + : null, finalResult: anyMatch, }); } 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/NewEditAccountView.vue b/src/views/NewEditAccountView.vue index 78e709f2..d1349e5a 100644 --- a/src/views/NewEditAccountView.vue +++ b/src/views/NewEditAccountView.vue @@ -110,10 +110,22 @@ 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, + }); + } else { + // Fallback to master settings if no active DID + await this.$saveSettings({ + firstName: this.givenName, + }); + } + this.$router.back(); } 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" }); } } 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"; + } + } }