Compare commits

...

71 Commits

Author SHA1 Message Date
Matthew Raymer
464a825a7b docs: update notification system for shared SQLite + clear T–lead
- Add SQLite Ownership & Concurrency section with WAL mode details
- Add DB Path & Adapter Configuration with shared DB setup
- Clarify T–lead governs prefetch attempts, not arming
- Add TTL-at-fire check callout for stale notification prevention
- Add DB Sharing acceptance criteria (visibility, WAL overlap, version safety)
- Update GLOSSARY.md with shared DB, WAL, and user_version definitions
- Remove plugin-owned DB references in favor of single shared database

Files modified:
- doc/notification-system.md
- doc/GLOSSARY.md

Compliance: Implements shared SQLite architecture with clear T–lead prefetch
semantics and WAL-based concurrency for app/plugin coordination.
2025-09-08 04:21:00 +00:00
Matthew Raymer
0dcb1d029e docs: merge notification system docs into single Native-First guide
- Consolidate 5 notification-system-* files into doc/notification-system.md
- Add web-push cleanup guide and Start-on-Login glossary entry
- Configure markdownlint for consistent formatting
- Remove web-push references, focus on native OS scheduling

Reduces maintenance overhead while preserving all essential information
in a single, well-formatted reference document.
2025-09-07 13:16:23 +00:00
Matthew Raymer
79b226e7d2 docs: apply documentation references model agents directive to notification system docs
- Remove duplicate sync checklists from notification-system-plan.md
- Fix markdown table formatting (remove double pipes) in executive summary
- Streamline cross-document references and eliminate redundant content
- Consolidate canonical ownership statements across all three docs
- Improve document structure and readability per directive guidelines

Files modified:
- doc/notification-system-executive-summary.md
- doc/notification-system-implementation.md
- doc/notification-system-plan.md

Compliance: Follows @docs/documentation_references_model_agents.mdc directive
for eliminating redundancy, centralizing context, and optimizing reference placement.
2025-09-07 10:51:27 +00:00
Matthew Raymer
74e4a20ef9 chore: update plan and rulesets 2025-09-05 13:09:30 +00:00
Matthew Raymer
cfeb920493 refactor(docs): split notification system docs into plan and implementation
Replace monolithic notification-system-implementation-plan.md with focused
strategic plan (notification-system-plan.md) and detailed implementation
guide (notification-system-implementation.md). Both documents now perfectly
aligned with TimeSafari codebase patterns including:

- Actual Settings type extension pattern (JSON strings for complex objects)
- Real useNotifications composable stub signatures with eslint-disable
- Verified logger exports and safeStringify usage
- Confirmed PlatformServiceMixin.$saveSettings integration
- Validated migration system registerMigration patterns

Documents are production-ready with accurate code examples verified
against actual TimeSafari infrastructure.
2025-09-05 09:01:15 +00:00
Matthew Raymer
a427a9e66f chore: still tweaking plan 2025-09-04 13:09:40 +00:00
Matthew Raymer
5110c17fba chore: create new notification plan 2025-09-04 13:09:00 +00:00
de45e83ffb Merge pull request 'test: add a check that the newly invited person can indeed log a claim' (#194) from invited-check into master
Reviewed-on: #194

https://app.clickup.com/t/86b6gc7ag
2025-09-03 22:06:36 -04:00
ba587471f9 doc: update the in-app help doc 2025-09-02 19:11:50 -06:00
2f05d27b51 Merge pull request 'fix: clean up "register random person" test' (#190) from playwright-test-00-fix into master
Reviewed-on: #190
2025-09-01 10:00:15 -04:00
40c8189c51 Merge pull request 'feat: add duplicate account import prevention' (#189) from account-import-duplicate-prevention into master
Reviewed-on: #189
2025-09-01 09:58:49 -04:00
cd7755979f Merge pull request 'enhance the verbiage & display for bulk confirmations & visibility' (#183) from better-confirms into master
Reviewed-on: #183
2025-09-01 09:56:14 -04:00
4fa8c8f4cb test: add a check that the newly invited person can indeed log a claim 2025-09-01 07:51:02 -06:00
2c7cb9333e chore: remove error logging for errors that are propagated 2025-09-01 06:59:36 -06:00
fa8956fb38 chore: explicitly share error message used for logic 2025-09-01 06:42:00 -06:00
Jose Olarte III
1499211018 refactor: simplify duplicate account error detection
Replace dual string check with single unique identifier for more precise error handling
2025-09-01 20:03:17 +08:00
Jose Olarte III
25e37cc415 refactor: consolidate duplicate account checking logic into unified utility
- Extract checkForDuplicateAccount methods from ImportAccountView and ImportDerivedAccountView
- Create unified utility function in src/libs/util.ts with TypeScript overloads
- Support both direct DID checking and mnemonic+derivation path checking
- Improve error handling with centralized logging via PlatformServiceFactory
- Add comprehensive JSDoc documentation for both function overloads
- Remove unused imports (deriveAddress, newIdentifier) from ImportAccountView

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

Both components maintain identical functionality while using centralized logic.
2025-09-01 19:36:01 +08:00
Jose Olarte III
d339f1a274 chore: remove generated doc
- Generated document reads more like a log, and does not contribute to actual documentation of app
2025-09-01 19:33:18 +08:00
Jose Olarte III
c2e7531554 Merge branch 'master' into account-import-duplicate-prevention 2025-09-01 18:06:36 +08:00
aa64f426f3 Merge pull request 'feat(electron): add editMenu to enable copy/paste keyboard shortcuts' (#192) from electron-copy-paste-keyboard-shortcuts into master
Reviewed-on: #192
2025-09-01 05:05:27 -04:00
Jose Olarte III
e6f0c7a079 Merge branch 'master' into electron-copy-paste-keyboard-shortcuts 2025-09-01 17:06:05 +08:00
2b9b43d08f Merge pull request 'fix: persist identity names per user instead of globally' (#191) from switching-identities-change-name into master
Reviewed-on: #191
2025-09-01 04:54:40 -04:00
Jose Olarte III
5f8d1fc8c6 refactor: remove deprecated lastName field from user settings
- Remove lastName field from $saveUserSettings and $saveSettings calls
- Clean up deprecated pre v0.1.3 code
2025-09-01 16:54:36 +08:00
Jose Olarte III
c9082fa57b refactor: remove single-use notification constant
- Replace constant usage with direct message string in ImportDerivedAccountView.vue
- Clean up import statement to remove unused import
- Remove unused constant from notifications.ts
2025-09-01 16:02:48 +08:00
a7608429be Merge pull request 'fix(electron): resolve TypeScript errors in Electron build configuration' (#187) from electron-build-config-overwrite into master
Reviewed-on: #187
2025-09-01 03:49:44 -04:00
Jose Olarte III
4a1249d166 feat(electron): add editMenu to enable copy/paste keyboard shortcuts
- Add 'editMenu' role to AppMenuBarMenuTemplate in setup.ts and index.ts
- Enables standard keyboard shortcuts (Cmd+C, Cmd+V, etc.) in Electron app
- Fixes issue where copy/paste shortcuts were not working in text inputs
- Maintains existing clipboard service functionality for programmatic operations

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

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

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

Fixes: Identity name persistence across identity switches
2025-08-29 16:41:19 +08:00
Matthew Raymer
5780d96cdc chore: linting 2025-08-29 06:50:16 +00:00
Jose Olarte III
e67c97821a fix: change import User Zero function
- Use the ./account route to mimic real-world use
2025-08-28 21:06:26 +08:00
Jose Olarte III
40fa38a9ce fix: clean up "register random person" test
- Remove redundant "import User Zero" action
- Remove out-of-scope actions from test (sending a gift to an unrelated entity, deleting the contact)
- Update imports based on changes
2025-08-28 20:50:57 +08:00
ff864adbe5 Merge branch 'master' into profile_include_location 2025-08-28 06:37:51 -04:00
Jose Olarte III
96e4d3c394 chore - reorder duplication test
- Rename the test to run it earlier in the test suite
2025-08-28 18:34:38 +08:00
Jose Olarte III
c4f2bb5e3a refactor: move duplicate account import warnings to notification constants
- Add NOTIFY_DUPLICATE_ACCOUNT_IMPORT constant for import warnings
- Add NOTIFY_DUPLICATE_DERIVED_ACCOUNT constant for derived account warnings
- Update ImportAccountView.vue to use notification constants
- Update ImportDerivedAccountView.vue to use notification constants
- Update test file to use notification constants for assertions

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

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

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

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

Resolves: Prevent duplicate account imports in IdentitySwitcherView
2025-08-28 16:35:04 +08:00
Jose Olarte III
8827c4a973 fix(electron): resolve TypeScript errors in Electron build configuration
- Create separate Electron-specific capacitor config
- Update build script to not copy main config to Electron directory
- Fix TypeScript compilation by excluding main config from Electron tsconfig

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes: Workflow state rigidity issue
Enables: Commits on demand during investigation phases
2025-08-24 05:45:37 +00:00
Matthew Raymer
be4ab16b00 Merge branch 'master' into profile_include_location 2025-08-24 02:47:14 +00:00
Matthew Raymer
1305eed9bc Merge branch 'master' into profile_include_location 2025-08-23 04:03:19 +00:00
Jose Olarte III
aa55588cbb UI: spacing and text styling consistencies 2025-08-21 16:27:00 +08:00
Jose Olarte III
5f63e05090 Fix: hide offer link from unregistered users 2025-08-21 16:25:57 +08:00
Matthew Raymer
3be7001d1b chore: linting 2025-08-20 10:04:40 +00:00
Matthew Raymer
95a8f5ebe1 Merge branch 'master' into profile_include_location 2025-08-20 09:48:26 +00:00
Matthew Raymer
f2026bb921 chore: clean up debug logging 2025-08-18 07:44:26 +00:00
76 changed files with 5209 additions and 962 deletions

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ alwaysApply: false
## Purpose
All interactions must *increase the human's competence over time* while
All interactions must _increase the human's competence over time_ while
completing the task efficiently. The model may handle menial work and memory
extension, but must also promote learning, autonomy, and healthy work habits.
The model should also **encourage human interaction and collaboration** rather
@@ -31,7 +31,7 @@ machine-driven steps.
## Principles
1. Competence over convenience: finish the task *and* leave the human more
1. Competence over convenience: finish the task _and_ leave the human more
capable next time.
@@ -75,7 +75,7 @@ assumptions if unanswered.
### timebox_minutes
*integer or null* — When set to a positive integer (e.g., `5`), this acts
_integer or null_ — When set to a positive integer (e.g., `5`), this acts
as a **time budget** guiding the model to prioritize delivering the most
essential parts of the task within that constraint.
@@ -91,7 +91,7 @@ Behavior when set:
3. **Signal Skipped Depth** — Omitted details should be listed under
*Deferred for depth*.
_Deferred for depth_.
4. **Order by Value** — Start with blocking or high-value items, then
@@ -198,7 +198,7 @@ Default: Doer + short Mentor notes.
## Self-Check (model, before responding)
- [ ] Task done *and* at least one competence lever included (≤120 words
- [ ] Task done _and_ at least one competence lever included (≤120 words
total)
- [ ] At least one collaboration/discussion hook present
- [ ] Output follows the **Output Contract** sections

View File

@@ -53,7 +53,7 @@ evidence-backed steps**.
- **Verifiable Outputs**: Include expected results, status codes, or
error messages
- **Cite evidence** for *Works/Doesn't* items (timestamps, filenames,
- **Cite evidence** for _Works/Doesn't_ items (timestamps, filenames,
line numbers, IDs/status codes, or logs).
## Required Sections
@@ -181,8 +181,8 @@ Before publishing, verify:
---
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human
Competence First*
**Status**: 🚢 ACTIVE — General ruleset extending _Base Context — Human
Competence First_
**Priority**: Critical
**Estimated Effort**: Ongoing reference

View File

@@ -16,7 +16,7 @@ language: Match repository languages and conventions
where it occurs; avoid new layers, indirection, or patterns unless
strictly necessary.
2. **Keep scope tight.** Implement only what is needed to satisfy the
acceptance criteria and tests for *this* issue.
acceptance criteria and tests for _this_ issue.
3. **Avoid speculative abstractions.** Use the **Rule of Three**:
don't extract helpers/patterns until the third concrete usage proves
the shape.
@@ -29,7 +29,7 @@ language: Match repository languages and conventions
7. **Targeted tests only.** Add the smallest set of tests that prove
the fix and guard against regression; don't rewrite suites.
8. **Document the "why enough."** Include a one-paragraph note
explaining why this minimal solution is sufficient *now*.
explaining why this minimal solution is sufficient _now_.
## Future-Proofing Requires Evidence + Discussion

View File

@@ -9,8 +9,8 @@ alwaysApply: false
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Asset management guidelines
*Scope: Assets Only (icons, splashes, image pipelines) — not overall build
orchestration*
_Scope: Assets Only (icons, splashes, image pipelines) — not overall build
orchestration_
## Intent

View File

@@ -40,7 +40,7 @@ feature development, issue investigations, ADRs, and documentation**.
`2025-08-17`).
- Avoid ambiguous terms like *recently*, *last month*, or *soon*.
- Avoid ambiguous terms like _recently_, _last month_, or _soon_.
- For time-based experiments (e.g., A/B tests), always include:

View File

@@ -19,7 +19,7 @@
- Optionally provide UTC alongside if context requires cross-team clarity.
- When interpreting relative terms like *now*, *today*, *last week*:
- When interpreting relative terms like _now_, _today_, _last week_:
- Resolve them against the **developer's current time**.

View File

@@ -0,0 +1,96 @@
---
title: Documentation, References, and Model Agent Use
version: 1.1
alwaysApply: true
scope: code, project-plans
---
# Directive on Documentation, References, and Model Agent Use in Code and Project Plans
To ensure clarity, efficiency, and high-value documentation within code and project plans—and to leverage **model agents** (AI- or automation-based assistants) effectively—contributors must follow these rules:
---
## 1. Documentation and References Must Add Clear Value
- Only include documentation, comments, or reference links when they provide _new, meaningful information_ that assists understanding or decision-making.
- Avoid duplicating content already obvious in the codebase, version history, or linked project documents.
---
## 2. Eliminate Redundant or Noisy References
- Remove references that serve no purpose beyond filling space.
- Model agents may automatically flag and suggest removal of trivial references (e.g., links to unchanged boilerplate or self-evident context).
---
## 3. Explicit Role of Model Agents
Model agents are **active participants** in documentation quality control. Their tasks include:
- **Relevance Evaluation**: Automatically analyze references for their substantive contribution before inclusion.
- **Redundancy Detection**: Flag duplicate or trivial references across commits, files, or tasks.
- **Context Linking**: Suggest appropriate higher-level docs (designs, ADRs, meeting notes) when a code change touches multi-stage or cross-team items.
- **Placement Optimization**: Recommend centralization of references (e.g., in plan overviews, ADRs, or merge commit messages) rather than scattered low-value inline references.
- **Consistency Monitoring**: Ensure references align with team standards (e.g., ADR template, architecture repo, or external policy documents).
Contributors must treat agent recommendations as **first-pass reviews** but remain accountable for final human judgment.
---
## 4. Contextual References for Complex Items
- Use **centralized references** for multi-stage features (e.g., architectural docs, research threads).
- Keep inline code comments light; push broader context into centralized documents.
- Model agents may auto-summarize complex chains of discussion and attach them as a single reference point.
---
## 5. Centralization of Broader Context
- Store overarching context (design docs, proposals, workflows) in accessible, well-indexed places.
- Model agents should assist by **generating reference maps** that track where docs are cited across the codebase.
---
## 6. Focused Documentation
- Documentation should explain **why** and **how** decisions are made, not just what was changed.
- Model agents can auto-generate first-pass explanations from commit metadata, diffs, and linked issues—but humans must refine them for accuracy and intent.
---
## 7. Review and Accountability
- Reviewers and team leads must reject submissions containing unnecessary or low-quality documentation.
- Model agent outputs are aids, not replacements—contributors remain responsible for **final clarity and relevance**.
---
## 8. Continuous Improvement and Agent Feedback Loops
- Encourage iterative development of model agents so their evaluations become more precise over time.
- Contributions should include **feedback on agent suggestions** (e.g., accepted, rejected, or corrected) to train better future outputs.
- Agents should log patterns of “rejected” suggestions for refinement.
---
## 9. Workflow Overview (Mermaid Diagram)
```mermaid
flowchart TD
A[Contributor] -->|Writes Code & Draft Docs| B[Model Agent]
B -->|Evaluates References| C{Relevant?}
C -->|Yes| D[Suggest Placement & Context Links]
C -->|No| E[Flag Redundancy / Noise]
D --> F[Contributor Refines Docs]
E --> F
F --> G[Reviewer]
G -->|Approves / Requests Revisions| H[Final Documentation]
G -->|Feedback on Agent Suggestions| B
```
---
✅ **Outcome:** By integrating disciplined contributor standards with **model agent augmentation**, the team achieves documentation that is consistently _relevant, concise, centralized, and decision-focused_. AI ensures coverage and noise reduction, while humans ensure precision and judgment.

View File

@@ -192,6 +192,7 @@ Summary of key concepts and skills.
Where to apply this knowledge next.
```
- [ ] Integration tests
- [ ] E2E tests

View File

@@ -16,9 +16,10 @@ inherits: base_context.mdc
**Author**: System/Shared
**Date**: 2025-08-21 (UTC)
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human Competence First*
**Status**: 🚢 ACTIVE — General ruleset extending _Base Context — Human Competence First_
> **Alignment with Base Context**
>
> - **Purpose fit**: Prioritizes human competence and collaboration while delivering reproducible artifacts.
> - **Output Contract**: This directive **adds universal constraints** for any technical topic while **inheriting** the Base Context contract sections.
> - **Toggles honored**: Uses the same toggle semantics; defaults above can be overridden by the caller.
@@ -26,9 +27,11 @@ inherits: base_context.mdc
---
## Objective
Produce a **developer-grade, reproducible guide** for any technical topic that onboards a competent practitioner **without meta narration** and **with evidence-backed steps**.
## Scope & Constraints
- **One Markdown document** as the deliverable.
- Use **absolute dates** in **UTC** (e.g., `2025-08-21T14:22Z`) — avoid “today/yesterday”.
- Include at least **one diagram** (Mermaid preferred). Choose the most fitting type:
@@ -37,10 +40,11 @@ Produce a **developer-grade, reproducible guide** for any technical topic that o
- **APIs**: `curl` + one client library (e.g., `httpx` for Python).
- **CLIs**: literal command blocks and expected output snippets.
- **Code**: minimal, self-contained samples (language appropriate).
- Cite **evidence** for *Works/Doesnt* items (timestamps, filenames, line numbers, IDs/status codes, or logs).
- Cite **evidence** for _Works/Doesnt_ items (timestamps, filenames, line numbers, IDs/status codes, or logs).
- If something is unknown, output `TODO:<missing>` — **never invent**.
## Required Sections (extends Base Output Contract)
Follow this exact order **after** the Base Contracts **Objective → Result → Use/Run** headers:
1. **Context & Scope**
@@ -52,9 +56,9 @@ Follow this exact order **after** the Base Contracts **Objective → Result
4. **Architecture / Process Overview**
- Short prose + **one diagram** selected from the list above.
5. **Interfaces & Contracts (choose one)**
- **API-based**: Endpoint table (*Step, Method, Path/URL, Auth, Key Headers/Params, Sample Req/Resp ref*).
- **Data/Files**: I/O contract table (*Source, Format, Schema/Columns, Size, Validation rules*).
- **Systems/Hardware**: Interfaces table (*Port/Bus, Protocol, Voltage/Timing, Constraints*).
- **API-based**: Endpoint table (_Step, Method, Path/URL, Auth, Key Headers/Params, Sample Req/Resp ref_).
- **Data/Files**: I/O contract table (_Source, Format, Schema/Columns, Size, Validation rules_).
- **Systems/Hardware**: Interfaces table (_Port/Bus, Protocol, Voltage/Timing, Constraints_).
6. **Repro: End-to-End Procedure**
- Minimal copy-paste steps with code/commands and **expected outputs**.
7. **What Works (with Evidence)**
@@ -69,16 +73,19 @@ Follow this exact order **after** the Base Contracts **Objective → Result
- Canonical docs, specs, tickets, prior analyses.
> **Competence Hooks (per Base Context; keep lightweight):**
> - *Why this works* (≤3 bullets) — core invariants or guarantees.
> - *Common pitfalls* (≤3 bullets) — the traps we saw in evidence.
> - *Next skill unlock* (1 line) — the next capability to implement/learn.
> - *Teach-back* (1 line) — prompt the reader to restate the flow/architecture.
>
> - _Why this works_ (≤3 bullets) — core invariants or guarantees.
> - _Common pitfalls_ (≤3 bullets) — the traps we saw in evidence.
> - _Next skill unlock_ (1 line) — the next capability to implement/learn.
> - _Teach-back_ (1 line) — prompt the reader to restate the flow/architecture.
> **Collaboration Hooks (per Base Context):**
>
> - Name reviewers for **Interfaces & Contracts** and the **diagram**.
> - Short **sign-off checklist** before merging/publishing the guide.
## Do / Dont (Base-aligned)
- **Do** quantify progress only against a defined scope with acceptance criteria.
- **Do** include minimal sample payloads/headers or I/O schemas; redact sensitive values.
- **Do** keep commentary lean; if timeboxed, move depth to **Deferred for depth**.
@@ -86,6 +93,7 @@ Follow this exact order **after** the Base Contracts **Objective → Result
- **Dont** include IDE-specific chatter or internal rules unrelated to the task.
## Validation Checklist (self-check before returning)
- [ ] All Required Sections present and ordered.
- [ ] Diagram compiles (basic Mermaid syntax) and fits the problem.
- [ ] If API-based, **Auth** and **Key Headers/Params** are listed for each endpoint.
@@ -96,6 +104,7 @@ Follow this exact order **after** the Base Contracts **Objective → Result
- [ ] Base **Output Contract** sections satisfied (Objective/Result/Use/Run/Competence/Collaboration/Assumptions/References).
## Universal Template (fill-in)
```markdown
# <Title> — Working Notes (As of YYYY-MM-DDTHH:MMZ)
@@ -132,37 +141,46 @@ Follow this exact order **after** the Base Contracts **Objective → Result
```
## Interfaces & Contracts
### If API-based
| Step | Method | Path/URL | Auth | Key Headers/Params | Sample |
|---|---|---|---|---|---|
| <…> | <…> | <…> | <…> | <…> | below |
### If Data/Files
| Source | Format | Schema/Columns | Size | Validation |
|---|---|---|---|---|
| <…> | <…> | <…> | <…> | <…> |
### If Systems/Hardware
| Interface | Protocol | Timing/Voltage | Constraints | Notes |
|---|---|---|---|---|
| <…> | <…> | <…> | <…> | <…> |
## Repro: End-to-End Procedure
```bash
# commands / curl examples (redacted where necessary)
```
```python
# minimal client library example (language appropriate)
```
> Expected output: <snippet/checks>
## What Works (Evidence)
- ✅ <short statement>
- **Time**: <YYYY-MM-DDTHH:MMZ>
- **Evidence**: file/line/log or request id/status
- **Verify at**: <where>
## What Doesnt (Evidence & Hypotheses)
- ❌ <short failure> at `<component/endpoint/file>`
- **Time**: <YYYY-MM-DDTHH:MMZ>
- **Evidence**: <snippet/id/status>
@@ -170,38 +188,46 @@ Follow this exact order **after** the Base Contracts **Objective → Result
- **Next probe**: <short>
## Risks, Limits, Assumptions
<bullets: limits, security boundaries, retries/backoff, idempotency, SLOs>
## Next Steps
| Owner | Task | Exit Criteria | Target Date (UTC) |
|---|---|---|---|
| <name> | <action> | <measurable outcome> | <YYYY-MM-DD> |
## References
<links/titles>
## Competence Hooks
- *Why this works*: <≤3 bullets>
- *Common pitfalls*: <≤3 bullets>
- *Next skill unlock*: <1 line>
- *Teach-back*: <1 line>
- _Why this works_: <≤3 bullets>
- _Common pitfalls_: <≤3 bullets>
- _Next skill unlock_: <1 line>
- _Teach-back_: <1 line>
## Collaboration Hooks
- Reviewers: <names/roles>
- Sign-off checklist: <≤5 checks>
## Assumptions & Limits
<bullets>
## Deferred for depth
<park deeper material here to respect timeboxing>
```
---
**Notes for Implementers:**
- Respect Base *Do-Not* (no filler, no invented facts, no censorship).
- Respect Base _Do-Not_ (no filler, no invented facts, no censorship).
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)

View File

@@ -1,169 +1,287 @@
# 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:**
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
- `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
### **Phase 3: Documentation & Reporting**
❌ **Never use during diagnosis:**
1. **Investigation Report** - Use `investigation_report_example.mdc`
for comprehensive documentation
2. **Root Cause Analysis** - Synthesize findings into actionable
insights
- `npm run build:web` - Blocks chat
- `npm run build:electron` - Blocks chat
- `npm run build:capacitor` - Blocks chat
- Any long-running build processes
## Success Criteria
## Investigation Workflow
- [ ] **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
### **Phase 1: Problem Definition**
1. **Gather Evidence**
- Error messages and stack traces
- User-reported symptoms
- System logs and timestamps
- Reproduction steps
2. **Context Analysis**
- When did the problem start?
- What changed recently?
- Which platform/environment?
- User actions leading to the issue
### **Phase 2: Systematic Investigation**
1. **Code Inspection**
- Relevant file examination
- Import and dependency analysis
- Syntax and type checking
- Logic flow analysis
2. **Environment Analysis**
- Platform-specific considerations
- Configuration and settings
- Database and storage state
- Network and API connectivity
### **Phase 3: Root Cause Identification**
1. **Pattern Recognition**
- Similar issues in codebase
- Common failure modes
- Platform-specific behaviors
- Recent changes impact
2. **Hypothesis Testing**
- Targeted code changes
- Configuration modifications
- Environment adjustments
- Systematic elimination
## Investigation Techniques
### **Safe Code Analysis**
- **File Reading**: Use `read_file` tool for targeted inspection
- **Pattern Searching**: Use `grep_search` for code patterns
- **Semantic Search**: Use `codebase_search` for related functionality
- **Import Tracing**: Follow dependency chains systematically
### **Error Analysis**
- **Stack Trace Analysis**: Identify error origin and propagation
- **Log Correlation**: Match errors with system events
- **Timeline Reconstruction**: Build sequence of events
- **Context Preservation**: Maintain investigation state
### **Platform Considerations**
- **Web Platform**: Browser-specific behaviors and limitations
- **Electron Platform**: Desktop app considerations
- **Capacitor Platform**: Mobile app behaviors
- **Cross-Platform**: Shared vs. platform-specific code
## Evidence Collection Standards
### **Timestamps**
- **UTC Format**: All timestamps in UTC for consistency
- **Precision**: Include milliseconds for precise correlation
- **Context**: Include relevant system state information
- **Correlation**: Link events across different components
### **Error Context**
- **Full Error Objects**: Capture complete error information
- **Stack Traces**: Preserve call stack for analysis
- **User Actions**: Document steps leading to error
- **System State**: Capture relevant configuration and state
### **Reproduction Steps**
- **Clear Sequence**: Step-by-step reproduction instructions
- **Environment Details**: Platform, version, configuration
- **Data Requirements**: Required data or state
- **Expected vs. Actual**: Clear behavior comparison
## Investigation Documentation
### **Problem Summary**
- **Issue Description**: Clear, concise problem statement
- **Impact Assessment**: Severity and user impact
- **Scope Definition**: Affected components and users
- **Priority Level**: Based on impact and frequency
### **Investigation Log**
- **Timeline**: Chronological investigation steps
- **Evidence**: Collected information and findings
- **Hypotheses**: Tested theories and results
- **Conclusions**: Root cause identification
### **Solution Requirements**
- **Fix Description**: Required changes and approach
- **Testing Strategy**: Validation and verification steps
- **Rollback Plan**: Reversion strategy if needed
- **Prevention Measures**: Future issue prevention
## Quality Standards
### **Investigation Completeness**
- **Evidence Sufficiency**: Adequate information for root cause
- **Alternative Theories**: Considered and eliminated
- **Platform Coverage**: All relevant platforms investigated
- **Edge Cases**: Unusual scenarios considered
### **Documentation Quality**
- **Clear Communication**: Understandable to all stakeholders
- **Technical Accuracy**: Precise technical details
- **Actionable Insights**: Clear next steps and recommendations
- **Knowledge Transfer**: Lessons learned for future reference
## Common Pitfalls
- **Don't skip evidence collection** - leads to speculation
- **Don't ignore platform differences** - misses platform-specific issues
- **Don't skip documentation** - loses investigation insights
- **Don't assume root cause** - verify with evidence
- **Don't ignore time context** - misses temporal factors
- **Don't skip reproduction steps** - makes verification impossible
### **Investigation Mistakes**
## Integration Points
- **Jumping to Solutions**: Implementing fixes before understanding
- **Insufficient Evidence**: Making assumptions without data
- **Platform Blindness**: Ignoring platform-specific behaviors
- **Scope Creep**: Expanding investigation beyond original problem
### **With Other Meta-Rules**
### **Communication Issues**
- **Feature Planning**: Use complexity assessment for investigation planning
- **Bug Fixing**: Investigation results feed directly into fix implementation
- **Feature Implementation**: Investigation insights inform future development
- **Technical Jargon**: Using unclear terminology
- **Missing Context**: Insufficient background information
- **Unclear Recommendations**: Vague or ambiguous next steps
- **Poor Documentation**: Incomplete or unclear investigation records
### **With Development Workflow**
## Success Criteria
- Investigation findings inform testing strategy
- Root cause analysis drives preventive measures
- Evidence collection improves logging standards
- [ ] **Problem clearly defined** with sufficient evidence
- [ ] **Root cause identified** through systematic investigation
- [ ] **Solution approach determined** with clear requirements
- [ ] **Documentation complete** for knowledge transfer
- [ ] **No chat-blocking commands** used during investigation
- [ ] **Platform considerations** properly addressed
- [ ] **Timeline and context** properly documented
## Feedback & Improvement
## Integration with Other Meta-Rules
### **Sub-Rule Ratings (1-5 scale)**
### **Bug Fixing**
- **Research Diagnostic**: ___/5 - Comments: _______________
- **Investigation Report**: ___/5 - Comments: _______________
- **Technical Guide Creation**: ___/5 - Comments: _______________
- **Logging Standards**: ___/5 - Comments: _______________
- **Time Standards**: ___/5 - Comments: _______________
- **Investigation Results**: Provide foundation for fix implementation
- **Solution Requirements**: Define what needs to be built
- **Testing Strategy**: Inform validation approach
- **Documentation**: Support implementation guidance
### **Workflow Feedback**
### **Feature Planning**
- **Investigation Effectiveness**: How well did the process help find root cause?
- **Missing Steps**: What investigation steps should be added?
- **Process Gaps**: Where did the workflow break down?
- **Root Cause Analysis**: Identify systemic issues
- **Prevention Measures**: Plan future issue avoidance
- **Architecture Improvements**: Identify structural enhancements
- **Process Refinements**: Improve development workflows
### **Sub-Rule Improvements**
### **Research and Documentation**
- **Clarity Issues**: Which rules were unclear or confusing?
- **Missing Examples**: What examples would make rules more useful?
- **Template Improvements**: How could investigation templates be better?
### **Overall Experience**
- **Time Saved**: How much time did this meta-rule save you?
- **Quality Improvement**: Did following these rules improve your investigation?
- **Recommendation**: Would you recommend this meta-rule to others?
## Model Implementation Checklist
### Before Bug Investigation
- [ ] **Problem Definition**: Clearly define what needs to be investigated
- [ ] **Scope Definition**: Determine investigation scope and boundaries
- [ ] **Evidence Planning**: Plan evidence collection strategy
- [ ] **Stakeholder Identification**: Identify who needs to be involved
### During Bug Investigation
- [ ] **Rule Application**: Apply bundled rules in recommended sequence
- [ ] **Evidence Collection**: Collect evidence systematically with timestamps
- [ ] **Documentation**: Document investigation process and findings
- [ ] **Validation**: Verify findings with reproduction steps
### After Bug Investigation
- [ ] **Report Creation**: Create comprehensive investigation report
- [ ] **Root Cause Analysis**: Document root cause with evidence
- [ ] **Feedback Collection**: Collect feedback on meta-rule effectiveness
- [ ] **Process Improvement**: Identify improvements for future investigations
- **Knowledge Base**: Contribute to troubleshooting guides
- **Pattern Recognition**: Identify common failure modes
- **Best Practices**: Develop investigation methodologies
- **Team Training**: Improve investigation capabilities
---
**See also**:
- `.cursor/rules/meta_feature_planning.mdc` for planning investigation work
- `.cursor/rules/meta_bug_fixing.mdc` for implementing fixes
- `.cursor/rules/meta_feature_implementation.mdc` for preventive measures
- `.cursor/rules/meta_feature_planning.mdc` for planning improvements
- `.cursor/rules/meta_documentation.mdc` for documentation standards
**Status**: Active meta-rule for bug diagnosis
**Priority**: High

View File

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

View File

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

View File

@@ -14,6 +14,115 @@ 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 +274,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 +283,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 +292,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 +309,9 @@ or context. They form the foundation for all AI assistant behavior.
**Estimated Effort**: Ongoing reference
**Dependencies**: All bundled sub-rules
**Stakeholders**: All AI interactions, Development team
**Dependencies**: All bundled sub-rules
**Stakeholders**: All AI interactions, Development team
**Dependencies**: All bundled sub-rules
**Stakeholders**: All AI interactions, Development team

View File

@@ -10,9 +10,48 @@ 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**:
- Writing new documentation
- Updating existing documentation
- Creating technical guides
@@ -69,6 +108,7 @@ providing technical descriptions.
### **Document Structure**
**Mandatory Sections**:
- **Overview**: Clear purpose and scope with educational context
- **Why This Matters**: Business value and user benefit explanation
- **Core Concepts**: Fundamental understanding before implementation
@@ -78,6 +118,7 @@ providing technical descriptions.
- **Next Steps**: Where to go from here
**Optional Sections**:
- **Background**: Historical context and evolution
- **Alternatives**: Other approaches and trade-offs
- **Advanced Topics**: Deep dive into complex scenarios
@@ -86,6 +127,7 @@ providing technical descriptions.
### **Writing Style**
**Educational Approach**:
- **Conversational tone**: Write as if explaining to a colleague
- **Progressive disclosure**: Start simple, add complexity gradually
- **Active voice**: "You can do this" not "This can be done"
@@ -93,6 +135,7 @@ providing technical descriptions.
- **Analogies**: Use familiar concepts to explain complex ideas
**Technical Accuracy**:
- **Precise language**: Use exact technical terms consistently
- **Code examples**: Working, tested code snippets
- **Version information**: Specify applicable versions and platforms
@@ -101,6 +144,7 @@ providing technical descriptions.
### **Content Quality Standards**
**Educational Value**:
- [ ] **Concept clarity**: Reader understands the fundamental idea
- [ ] **Context relevance**: Reader knows when to apply the knowledge
- [ ] **Practical application**: Reader can implement the solution
@@ -108,6 +152,7 @@ providing technical descriptions.
- [ ] **Next steps**: Reader knows where to continue learning
**Technical Accuracy**:
- [ ] **Fact verification**: All technical details are correct
- [ ] **Code validation**: Examples compile and run correctly
- [ ] **Version compatibility**: Platform and version requirements clear
@@ -145,6 +190,7 @@ providing technical descriptions.
### **Review Checklist**
**Educational Quality**:
- [ ] **Clear learning objective**: What will the reader learn?
- [ ] **Appropriate complexity**: Matches target audience knowledge
- [ ] **Progressive disclosure**: Information builds logically
@@ -152,6 +198,7 @@ providing technical descriptions.
- [ ] **Common questions**: Anticipates and answers reader questions
**Technical Quality**:
- [ ] **Accuracy**: All technical details verified
- [ ] **Completeness**: Covers all necessary information
- [ ] **Consistency**: Terminology and formatting consistent

View File

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

View File

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

View File

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

View File

@@ -9,26 +9,31 @@ alwaysApply: false
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
## Objective
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
## Context & Scope
- **Audience**: Developers debugging Playwright test failures
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
- **Out of scope**: Test writing best practices, CI/CD configuration
## Artifacts & Links
- Test results: `test-results/` directory
- Error context: `error-context.md` files with page snapshots
- Trace files: `trace.zip` files for failed tests
- HTML reports: Interactive test reports with screenshots
## Environment & Preconditions
- OS/Runtime: Linux/Windows/macOS with Node.js
- Versions: Playwright test framework, browser drivers
- Services: Local test server (localhost:8080), test data setup
- Auth mode: None required for test investigation
## Architecture / Process Overview
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
```mermaid
@@ -57,6 +62,7 @@ flowchart TD
## Interfaces & Contracts
### Test Results Structure
| Component | Format | Content | Validation |
|---|---|---|---|
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
@@ -65,6 +71,7 @@ flowchart TD
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
### Investigation Commands
| Step | Command | Expected Output | Notes |
|---|---|---|---|
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
@@ -74,6 +81,7 @@ flowchart TD
## Repro: End-to-End Investigation Procedure
### 1. Locate Failed Test Results
```bash
# Find all results for a specific test
find test-results -name "*test-name*" -type d
@@ -83,6 +91,7 @@ find test-results -name "error-context.md" | head -5
```
### 2. Analyze Error Context
```bash
# Read error context for specific test
cat test-results/test-name-test-description-browser/error-context.md
@@ -92,6 +101,7 @@ grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
```
### 3. Check Trace Files
```bash
# List available trace files
find test-results -name "*.zip" | grep trace
@@ -101,6 +111,7 @@ npx playwright show-trace test-results/test-name/trace.zip
```
### 4. Investigate Selector Issues
```typescript
// Check for multiple elements with same text
await page.locator('button:has-text("Yes")').count(); // Should be 1
@@ -110,6 +121,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
```
## What Works (Evidence)
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
- **Time**: 2025-08-21T14:22Z
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
@@ -126,6 +138,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Verify at**: Error context markdown files
## What Doesn't (Evidence & Hypotheses)
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
- **Time**: 2025-08-21T14:22Z
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
@@ -139,12 +152,14 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Next probe**: Implement alert queuing or prevent overlapping alerts
## Risks, Limits, Assumptions
- **Trace file size**: Large trace files may impact storage and analysis time
- **Browser compatibility**: Trace viewer requires specific browser support
- **Test isolation**: Shared state between tests may affect investigation results
- **Timing sensitivity**: Tests may pass/fail based on system performance
## Next Steps
| Owner | Task | Exit Criteria | Target Date (UTC) |
|---|---|---|---|
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
@@ -152,21 +167,25 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
## References
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
- [Test Investigation Workflow](./research_diagnostic.mdc)
## Competence Hooks
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
## Collaboration Hooks
- **Reviewers**: QA team, test automation engineers
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
## Assumptions & Limits
- Test results directory structure follows Playwright conventions
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
- Error context files contain valid YAML page snapshots
@@ -178,6 +197,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
**Priority**: High
**Maintainer**: Development team
**Next Review**: 2025-09-21
# Playwright Test Investigation — Harbor Pilot Directive
**Author**: Matthew Raymer
@@ -185,26 +205,31 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
## Objective
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
## Context & Scope
- **Audience**: Developers debugging Playwright test failures
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
- **Out of scope**: Test writing best practices, CI/CD configuration
## Artifacts & Links
- Test results: `test-results/` directory
- Error context: `error-context.md` files with page snapshots
- Trace files: `trace.zip` files for failed tests
- HTML reports: Interactive test reports with screenshots
## Environment & Preconditions
- OS/Runtime: Linux/Windows/macOS with Node.js
- Versions: Playwright test framework, browser drivers
- Services: Local test server (localhost:8080), test data setup
- Auth mode: None required for test investigation
## Architecture / Process Overview
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
```mermaid
@@ -233,6 +258,7 @@ flowchart TD
## Interfaces & Contracts
### Test Results Structure
| Component | Format | Content | Validation |
|---|---|---|---|
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
@@ -241,6 +267,7 @@ flowchart TD
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
### Investigation Commands
| Step | Command | Expected Output | Notes |
|---|---|---|---|
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
@@ -250,6 +277,7 @@ flowchart TD
## Repro: End-to-End Investigation Procedure
### 1. Locate Failed Test Results
```bash
# Find all results for a specific test
find test-results -name "*test-name*" -type d
@@ -259,6 +287,7 @@ find test-results -name "error-context.md" | head -5
```
### 2. Analyze Error Context
```bash
# Read error context for specific test
cat test-results/test-name-test-description-browser/error-context.md
@@ -268,6 +297,7 @@ grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
```
### 3. Check Trace Files
```bash
# List available trace files
find test-results -name "*.zip" | grep trace
@@ -277,6 +307,7 @@ npx playwright show-trace test-results/test-name/trace.zip
```
### 4. Investigate Selector Issues
```typescript
// Check for multiple elements with same text
await page.locator('button:has-text("Yes")').count(); // Should be 1
@@ -286,6 +317,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
```
## What Works (Evidence)
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
- **Time**: 2025-08-21T14:22Z
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
@@ -302,6 +334,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Verify at**: Error context markdown files
## What Doesn't (Evidence & Hypotheses)
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
- **Time**: 2025-08-21T14:22Z
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
@@ -315,12 +348,14 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Next probe**: Implement alert queuing or prevent overlapping alerts
## Risks, Limits, Assumptions
- **Trace file size**: Large trace files may impact storage and analysis time
- **Browser compatibility**: Trace viewer requires specific browser support
- **Test isolation**: Shared state between tests may affect investigation results
- **Timing sensitivity**: Tests may pass/fail based on system performance
## Next Steps
| Owner | Task | Exit Criteria | Target Date (UTC) |
|---|---|---|---|
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
@@ -328,21 +363,25 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
## References
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
- [Test Investigation Workflow](./research_diagnostic.mdc)
## Competence Hooks
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
## Collaboration Hooks
- **Reviewers**: QA team, test automation engineers
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
## Assumptions & Limits
- Test results directory structure follows Playwright conventions
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
- Error context files contain valid YAML page snapshots

View File

@@ -73,7 +73,7 @@
### Avoid
- Vague: *improved, enhanced, better*
- Vague: _improved, enhanced, better_
- Trivialities: tiny docs, one-liners, pure lint cleanups (separate,

3
.gitignore vendored
View File

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

View File

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

View File

@@ -5,23 +5,28 @@
#
. "$(dirname -- "$0")/_/husky.sh"
echo "🔍 Running Build Architecture Guard (pre-push)..."
echo "🔍 Pre-push checks..."
# Get the remote branch we're pushing to
REMOTE_BRANCH="origin/$(git rev-parse --abbrev-ref HEAD)"
# Build Architecture Guard - DISABLED
# echo "🔍 Running Build Architecture Guard (pre-push)..."
#
# # Get the remote branch we're pushing to
# REMOTE_BRANCH="origin/$(git rev-parse --abbrev-ref HEAD)"
#
# # Check if remote branch exists
# if git show-ref --verify --quiet "refs/remotes/$REMOTE_BRANCH"; then
# RANGE="$REMOTE_BRANCH...HEAD"
# else
# # If remote branch doesn't exist, check last commit
# RANGE="HEAD~1..HEAD"
# fi
#
# bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
# echo
# echo "💡 To bypass this check for emergency pushes, use:"
# echo " git push --no-verify"
# echo
# exit 1
# }
# Check if remote branch exists
if git show-ref --verify --quiet "refs/remotes/$REMOTE_BRANCH"; then
RANGE="$REMOTE_BRANCH...HEAD"
else
# If remote branch doesn't exist, check last commit
RANGE="HEAD~1..HEAD"
fi
bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
echo
echo "💡 To bypass this check for emergency pushes, use:"
echo " git push --no-verify"
echo
exit 1
}
echo "✅ Pre-push checks passed!"

View File

@@ -1,27 +1,56 @@
{
"MD013": {
"line_length": 80,
"code_blocks": false,
"tables": false,
"headings": false
"MD013": false,
"MD033": false,
"MD041": false,
"MD024": {
"siblings_only": true
},
"MD029": {
"style": "ordered"
},
"MD007": {
"indent": 2
},
"MD012": {
"maximum": 1
},
"MD012": true,
"MD022": true,
"MD025": true,
"MD026": {
"punctuation": ".,;:!"
},
"MD030": {
"ul_single": 1,
"ol_single": 1,
"ul_multi": 1,
"ol_multi": 1
},
"MD031": true,
"MD032": true,
"MD047": true,
"MD009": true,
"MD010": true,
"MD004": { "style": "dash" },
"MD029": { "style": "ordered" },
"MD041": false,
"MD025": false,
"MD024": false,
"MD034": true,
"MD035": {
"style": "---"
},
"MD036": false,
"MD003": false,
"MD040": false,
"MD055": false,
"MD056": false,
"MD034": false,
"MD023": false
"MD037": true,
"MD038": true,
"MD039": true,
"MD040": true,
"MD042": true,
"MD043": false,
"MD044": false,
"MD045": true,
"MD046": {
"style": "fenced"
},
"MD047": true,
"MD048": {
"style": "backtick"
},
"MD049": {
"style": "underscore"
},
"MD050": {
"style": "asterisk"
}
}

View File

@@ -93,6 +93,7 @@ The Build Architecture Guard protects your build system by enforcing documentati
#### Protected File Patterns
The guard monitors these sensitive paths:
- `vite.config.*` - Build configuration
- `scripts/**` - Build and utility scripts
- `electron/**` - Desktop application code
@@ -132,6 +133,7 @@ npm run guard:setup
#### Troubleshooting
If you encounter `mapfile: command not found` errors:
```bash
# Ensure script is executable
chmod +x scripts/build-arch-guard.sh
@@ -270,6 +272,7 @@ Start the development server using `npm run build:web:dev` or `npm run build:web
3. To test the production build locally, use `npm run build:web:serve` (builds then serves)
**Why Use `serve`?**
- **Production Testing**: Test your optimized production build locally before deployment
- **SPA Routing Validation**: Verify deep linking and navigation work correctly (handles routes like `/discover`, `/account`)
- **Performance Testing**: Test the minified and optimized build locally
@@ -335,15 +338,18 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
The `serve` functionality provides a local HTTP server for testing production builds:
**What It Does:**
1. **Builds** the application using Vite
2. **Serves** the built files from the `dist/` directory
3. **Handles SPA Routing** - serves `index.html` for all routes (fixes 404s on `/discover`, `/account`, etc.)
**Server Options:**
- **Primary**: `npx serve -s dist -l 8080` (recommended - full SPA support)
- **Fallback**: Python HTTP server (limited SPA routing support)
**Use Cases:**
- Testing production builds before deployment
- Validating SPA routing behavior
- Performance testing of optimized builds
@@ -365,8 +371,8 @@ current version to test DB migrations.
- Put the commit hash in the changelog (which will help you remember to bump the
version in the step later).
- Tag with the new version,
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
- Tag with the new version,
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
`git tag 1.0.2 && git push origin 1.0.2`.
- For test, build the app:
@@ -617,7 +623,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**
@@ -1184,14 +1191,14 @@ If you need to build manually or want to understand the individual steps:
- Choose Product -> Archive
- This will trigger a build and take time, needing user's "login" keychain
password (user's login password), repeatedly.
- If it fails with `building for 'iOS', but linking in dylib
- If it fails with `building for 'iOS', but linking in dylib
(.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run
XCode outside that terminal (ie. not with `npx cap open ios`).
- Click Distribute -> App Store Connect
- In AppStoreConnect, add the build to the distribution. You may have to remove
the current build with the "-" when you hover over it, then "Add Build" with the
new build.
- May have to go to App Review, click Submission, then hover over the build
- May have to go to App Review, click Submission, then hover over the build
and click "-".
- It can take 15 minutes for the build to show up in the list of builds.
- You'll probably have to "Manage" something about encryption, disallowed in France.
@@ -1256,11 +1263,13 @@ npm run assets:validate
##### What Gets Validated
**Source Assets (Required):**
- `resources/icon.png` - App icon source
- `resources/splash.png` - Splash screen source
- `resources/splash_dark.png` - Dark mode splash source
**Android Resources (Generated):**
- `android/app/src/main/res/drawable/splash.png` - Splash screen drawable
- `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities
- `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities
@@ -2705,6 +2714,7 @@ configuration files in the repository.
### 2025-08-21 - Cursor Rules Refactoring and Build System Updates
#### Package Dependencies Updated
- **Added**: `markdownlint-cli2` v0.18.1 - Modern markdown linting with improved performance
- **Added**: `@commitlint/cli` v18.6.1 - Conventional commit message validation
- **Added**: `@commitlint/config-conventional` v18.6.2 - Conventional commit standards
@@ -2712,28 +2722,33 @@ configuration files in the repository.
- **Updated**: `lint-staged` v15.2.2 - Pre-commit linting automation
#### Build Script Improvements
- **Markdown Linting**: Replaced custom markdown scripts with `markdownlint-cli2`
- **Before**: `./scripts/fix-markdown.sh` and `./scripts/validate-markdown.sh`
- **After**: `markdownlint-cli2 --fix` and `markdownlint-cli2`
- **Benefits**: Faster execution, better error reporting, modern markdown standards
#### Lint-Staged Configuration Enhanced
- **Added**: Markdown file linting to pre-commit hooks
- **Pattern**: `*.{md,markdown,mdc}` files now automatically formatted
- **Command**: `markdownlint-cli2 --fix` runs before each commit
- **Coverage**: All markdown files including `.mdc` cursor rules
#### Commit Message Standards
- **Added**: Conventional commit validation via commitlint
- **Configuration**: Extends `@commitlint/config-conventional`
- **Enforcement**: Ensures consistent commit message format across the project
#### Node.js Version Requirements
- **Updated**: Minimum Node.js version requirements for new dependencies
- **markdownlint-cli2**: Requires Node.js >=20
- **Various utilities**: Require Node.js >=18 for modern ES features
#### Build Process Impact
- **No Breaking Changes**: All existing build commands continue to work
- **Improved Quality**: Better markdown formatting and commit message standards
- **Enhanced Automation**: More comprehensive pre-commit validation
@@ -2744,6 +2759,7 @@ configuration files in the repository.
### 2025-08-21 - Commitlint Configuration Refinement
#### Commit Message Validation Improvements
- **Modified**: Commitlint configuration moved from `package.json` to dedicated `commitlint.config.js`
- **Enhanced**: Strict validation rules downgraded from errors to warnings
- **Before**: `subject-case` and `subject-full-stop` rules caused red error messages
@@ -2751,16 +2767,18 @@ configuration files in the repository.
- **Benefit**: Eliminates confusing red error messages while maintaining commit quality guidance
#### Configuration Structure
- **File**: `commitlint.config.js` - Dedicated commitlint configuration
- **Extends**: `@commitlint/config-conventional` - Standard conventional commit rules
- **Custom Rules**:
- **Custom Rules**:
- `subject-case: [1, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']]`
- `subject-full-stop: [1, 'never', '.']`
- **Levels**:
- **Levels**:
- `0` = Disabled, `1` = Warning, `2` = Error
- Current: Problematic rules set to warning level (1)
#### User Experience Impact
- **Before**: Red error messages on every push with strict commit rules
- **After**: Yellow warning messages that provide guidance without disruption
- **Workflow**: Commits and pushes continue to work while maintaining quality standards
@@ -2768,6 +2786,50 @@ configuration files in the repository.
---
### 2025-08-26 - Capacitor Plugin Additions
#### New Capacitor Plugins Added
- **Added**: `@capacitor/clipboard` v6.0.2 - Clipboard functionality for mobile platforms
- **Purpose**: Enable copy/paste operations on mobile devices
- **Platforms**: iOS and Android
- **Features**: Read/write clipboard content, text handling
- **Integration**: Automatically included in mobile builds
- **Added**: `@capacitor/status-bar` v6.0.2 - Status bar management for mobile platforms
- **Purpose**: Control mobile device status bar appearance and behavior
- **Platforms**: iOS and Android
- **Features**: Status bar styling, visibility control, color management
- **Integration**: Automatically included in mobile builds
#### Android Build System Updates
- **Modified**: `android/capacitor.settings.gradle` - Added new plugin project includes
- **Added**: `:capacitor-clipboard` project directory mapping
- **Added**: `:capacitor-status-bar` project directory mapping
- **Impact**: New plugins now properly integrated into Android build process
#### Package Dependencies
- **Updated**: `package.json` - Added new Capacitor plugin dependencies
- **Updated**: `package-lock.json` - Locked dependency versions for consistency
- **Version**: All new plugins use Capacitor 6.x compatible versions
#### Build Process Impact
- **No Breaking Changes**: Existing build commands continue to work unchanged
- **Enhanced Mobile Features**: New clipboard and status bar capabilities available
- **Automatic Integration**: Plugins automatically included in mobile builds
- **Platform Support**: Both iOS and Android builds now include new functionality
#### Testing Requirements
- **Mobile Builds**: Verify new plugins integrate correctly in iOS and Android builds
- **Functionality**: Test clipboard operations and status bar management on devices
- **Fallback**: Ensure graceful degradation when plugins are unavailable
---
**Note**: This documentation is maintained alongside the build system. For the
most up-to-date information, refer to the actual script files and Vite
configuration files in the repository.

View File

@@ -66,14 +66,17 @@ test-image.tar a1b2c3d4e5f6...
```
### Docs
- [x] **BUILDING.md** updated (sections): Docker deployment
- [x] Troubleshooting updated: Added Docker troubleshooting section
### Rollback
- [x] Verified steps to restore previous behavior:
1. `git revert HEAD`
2. `docker rmi test-image`
3. Restore previous BUILDING.md
```
---

View File

@@ -11,7 +11,7 @@ See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e
Quick start:
* For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files.
- For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files.
```bash
npm install
@@ -90,6 +90,7 @@ VITE_LOG_LEVEL=debug npm run dev
See [Logging Configuration Guide](doc/logging-configuration.md) for complete details.
### Quick Usage
```bash
# Run the database clearing script
./scripts/clear-database.sh
@@ -102,16 +103,19 @@ npm run build:web:dev # For Web
### What It Does
#### **Electron (Desktop App)**
- Automatically finds and clears the SQLite database files
- Works on Linux, macOS, and Windows
- Clears all data and forces fresh migrations on next startup
#### **Web Browser**
- Provides instructions for using custom browser data directories
- Shows manual clearing via browser DevTools
- Ensures reliable database clearing without browser complications
### Safety Features
-**Interactive Script**: Guides you through the process
-**Platform Detection**: Automatically detects your OS
-**Clear Instructions**: Step-by-step guidance for each platform
@@ -120,6 +124,7 @@ npm run build:web:dev # For Web
### Manual Commands (if needed)
#### **Electron Database Location**
```bash
# Linux
rm -rf ~/.config/TimeSafari/*
@@ -132,6 +137,7 @@ rmdir /s /q %APPDATA%\TimeSafari
```
#### **Web Browser (Custom Data Directory)**
```bash
# Create isolated browser profile
mkdir ~/timesafari-dev-data
@@ -144,6 +150,7 @@ URL generation across all environments. This prevents localhost URLs from
appearing in shared links during development.
### Key Features
-**Production URLs for Sharing**: All copy link buttons use production domain
-**Environment-Specific Internal URLs**: Internal operations use appropriate
environment URLs
@@ -227,6 +234,7 @@ npm run test:prerequisites
- **Build failures**: Run `npm run check:dependencies` to diagnose environment issues
**Required Versions**:
- Node.js: 18+ (LTS recommended)
- npm: 8+ (comes with Node.js)
- Platform-specific tools: Android Studio, Xcode (for mobile builds)
@@ -246,25 +254,26 @@ To add a Font Awesome icon, add to `fontawesome.ts` and reference with
### Reference Material
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
- Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
* [Customize Vue configuration](https://cli.vuejs.org/config/).
- [Customize Vue configuration](https://cli.vuejs.org/config/).
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
- If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
### Code Organization
The project uses a centralized approach to type definitions and interfaces:
* `src/interfaces/` - Contains all TypeScript interfaces and type definitions
* `deepLinks.ts` - Deep linking type system and Zod validation schemas
* `give.ts` - Give-related interfaces and type definitions
* `claims.ts` - Claim-related interfaces and verifiable credentials
* `common.ts` - Shared interfaces and utility types
* Other domain-specific interface files
- `src/interfaces/` - Contains all TypeScript interfaces and type definitions
- `deepLinks.ts` - Deep linking type system and Zod validation schemas
- `give.ts` - Give-related interfaces and type definitions
- `claims.ts` - Claim-related interfaces and verifiable credentials
- `common.ts` - Shared interfaces and utility types
- Other domain-specific interface files
Key principles:
- All interfaces and types are defined in the interfaces folder
- Zod schemas are used for runtime validation and type generation
- Domain-specific interfaces are separated into their own files
@@ -275,11 +284,11 @@ Key principles:
The application uses a platform-agnostic database layer with Vue mixins for service access:
* `src/services/PlatformService.ts` - Database interface definition
* `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
* `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
* `src/utils/PlatformServiceMixin.ts` - Vue mixin for database access with caching
* `src/db/` - Legacy Dexie database (migration in progress)
- `src/services/PlatformService.ts` - Database interface definition
- `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
- `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
- `src/utils/PlatformServiceMixin.ts` - Vue mixin for database access with caching
- `src/db/` - Legacy Dexie database (migration in progress)
**Development Guidelines**:
@@ -316,11 +325,11 @@ timesafari/
Gifts make the world go 'round!
* [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
* [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
* [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons
* Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/)
- [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license
- [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
- [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
- [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
- [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
- Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
- [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons
- Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/)

View File

@@ -5,33 +5,33 @@
We can't trust iOS IndexedDB to persist. I want to start delivering an app to people now, in preparation for presentations mid-June: Rotary on June 12 and Porcfest on June 17.
* Apple WebKit puts a [7-day cap on IndexedDB](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/).
- Apple WebKit puts a [7-day cap on IndexedDB](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/).
* The web standards expose a `persist` method to mark memory as persistent, and [supposedly WebView supports it](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persisted), but too many other things indicate it's not reliable. I've talked with [ChatGPT](https://chatgpt.com/share/68322f40-84c8-8007-b213-855f7962989a) & Venice & Claude (in Cursor); [this answer from Perplexity](https://www.perplexity.ai/search/which-platforms-prompt-the-use-HUQLqy4qQD2cRbkmO4CgHg) says that most platforms don't prompt and Safari doesn't support it; I don't know if that means WebKit as well.
- The web standards expose a `persist` method to mark memory as persistent, and [supposedly WebView supports it](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persisted), but too many other things indicate it's not reliable. I've talked with [ChatGPT](https://chatgpt.com/share/68322f40-84c8-8007-b213-855f7962989a) & Venice & Claude (in Cursor); [this answer from Perplexity](https://www.perplexity.ai/search/which-platforms-prompt-the-use-HUQLqy4qQD2cRbkmO4CgHg) says that most platforms don't prompt and Safari doesn't support it; I don't know if that means WebKit as well.
* Capacitor says [not to trust it on iOS](https://capacitorjs.com/docs/v6/guides/storage).
- Capacitor says [not to trust it on iOS](https://capacitorjs.com/docs/v6/guides/storage).
Also, with sensitive data, the accounts info should be encrypted.
# Options
* There is a community [SQLite plugin for Capacitor](https://github.com/capacitor-community/sqlite) with encryption by [SQLCipher](https://github.com/sqlcipher/sqlcipher).
- There is a community [SQLite plugin for Capacitor](https://github.com/capacitor-community/sqlite) with encryption by [SQLCipher](https://github.com/sqlcipher/sqlcipher).
* [This tutorial](https://jepiqueau.github.io/2023/09/05/Ionic7Vue-SQLite-CRUD-App.html#part-1---web---table-of-contents) shows how that plugin works for web as well as native.
- [This tutorial](https://jepiqueau.github.io/2023/09/05/Ionic7Vue-SQLite-CRUD-App.html#part-1---web---table-of-contents) shows how that plugin works for web as well as native.
* Capacitor abstracts [user preferences in an API](https://capacitorjs.com/docs/apis/preferences), which uses different underlying libraries on iOS & Android. Unfortunately, it won't do any filtering or searching, and is only meant for small amounts of data. (It could be used for settings and for identifiers, but contacts will grow and image blobs won't work.)
- Capacitor abstracts [user preferences in an API](https://capacitorjs.com/docs/apis/preferences), which uses different underlying libraries on iOS & Android. Unfortunately, it won't do any filtering or searching, and is only meant for small amounts of data. (It could be used for settings and for identifiers, but contacts will grow and image blobs won't work.)
* There are hints that Capacitor offers another custom storage API but all I could find was that Preferences API.
- There are hints that Capacitor offers another custom storage API but all I could find was that Preferences API.
* [Ionic Storage](https://ionic.io/docs/secure-storage) is an enterprise solution, which also supports encryption.
- [Ionic Storage](https://ionic.io/docs/secure-storage) is an enterprise solution, which also supports encryption.
* Not an option yet: Dexie may support SQLite in [a future version](https://dexie.org/roadmap/dexie5.0).
- Not an option yet: Dexie may support SQLite in [a future version](https://dexie.org/roadmap/dexie5.0).
# Current Plan
* Implement SQLite for Capacitor & web, with encryption. That will allow us to test quickly and keep the same interface for native & web, but we don't deal with migrations for current web users.
- Implement SQLite for Capacitor & web, with encryption. That will allow us to test quickly and keep the same interface for native & web, but we don't deal with migrations for current web users.
* After that is delivered, write a migration for current web users from IndexedDB to SQLite.
- After that is delivered, write a migration for current web users from IndexedDB to SQLite.
# Current method calls

21
doc/GLOSSARY.md Normal file
View File

@@ -0,0 +1,21 @@
# Glossary
**T (slot time)** — The local wall-clock time a notification should fire (e.g., 08:00).
**Tlead** — The moment **`prefetchLeadMinutes`** before **T** when the system *attempts* a **single** background prefetch. Tlead **controls prefetch attempts, not arming**; locals are pre-armed earlier to guarantee closed-app delivery.
**Rolling window** — Always keep **today's remaining** (and tomorrow if iOS pending caps allow) locals **armed** so the OS can deliver while the app is closed.
**TTL (time-to-live)** — Maximum allowed payload age **at fire time**. If `T fetchedAt > ttlSeconds`, we **skip** arming for that T.
**Shared DB (default)** — The app and plugin open the **same SQLite file**; the app owns schema/migrations, the plugin performs short writes with WAL.
**WAL (Write-Ahead Logging)** — SQLite journaling mode that permits concurrent reads during writes; recommended for foreground-read + background-write.
**`PRAGMA user_version`** — An integer the app increments on each migration; the plugin **checks** (does not migrate) to ensure compatibility.
**Exact alarm (Android)** — Minute-precise alarm via `AlarmManager.setExactAndAllowWhileIdle`, subject to policy and permission.
**Windowed alarm (Android)** — Batched/inexact alarm via `setWindow(start,len)`; we target **±10 minutes** when exact alarms are unavailable.
**Start-on-Login** — Electron feature that automatically launches the application when the user logs into their system, enabling background notification scheduling and delivery after system reboot.

View File

@@ -330,6 +330,7 @@ Track the effectiveness of your Build Architecture Guard:
## 📝 **Changelog**
### 2025-08-22 - Shell Compatibility Fix
- **Fixed**: Replaced `mapfile` command with portable alternative for cross-shell compatibility
- **Impact**: Resolves "mapfile: command not found" errors in pre-commit hooks
- **Files**: `scripts/build-arch-guard.sh`

View File

@@ -1,77 +1,11 @@
# TimeSafari Docs
# TimeSafari — Native-First Notification System (Clean Pack) — 2025-09-07
## Generating PDF from Markdown on OSx
This pack contains a single-version **Native-First** documentation set with a clear definition of **Tlead** and aligned terminology.
This uses Pandoc and BasicTex (LaTeX) Installed through Homebrew.
**Native-First =** OS-scheduled **background prefetch at Tlead** + **pre-armed one-shot local notifications**. Web-push is retired.
### Set Up
**Included files**
```bash
brew install pandoc
brew install basictex
# Setting up LaTex packages
# First update tlmgr
sudo tlmgr update --self
# Then install LaTex packages
sudo tlmgr install bbding
sudo tlmgr install enumitem
sudo tlmgr install environ
sudo tlmgr install fancyhdr
sudo tlmgr install framed
sudo tlmgr install import
sudo tlmgr install lastpage # Enables Page X of Y
sudo tlmgr install mdframed
sudo tlmgr install multirow
sudo tlmgr install needspace
sudo tlmgr install ntheorem
sudo tlmgr install tabu
sudo tlmgr install tcolorbox
sudo tlmgr install textpos
sudo tlmgr install titlesec
sudo tlmgr install titling # Required for the fancy headers used
sudo tlmgr install threeparttable
sudo tlmgr install trimspaces
sudo tlmgr install tocloft # Required for \tableofcontents generation
sudo tlmgr install varwidth
sudo tlmgr install wrapfig
# Install fonts
sudo tlmgr install cmbright
sudo tlmgr install collection-fontsrecommended # And set up fonts
sudo tlmgr install fira
sudo tlmgr install fontaxes
sudo tlmgr install libertine # The main font the doc uses
sudo tlmgr install opensans
sudo tlmgr install sourceserifpro
```
#### References
The following guide was adapted to this project except that we install with Brew and have a few more packages.
Guide: <https://daniel.feldroy.com/posts/setting-up-latex-on-mac-os-x>
### Usage
Use the `pandoc` command to generate a PDF.
```bash
pandoc usage-guide.md -o usage-guide.pdf
```
And you can open the PDF with the `open` command.
```bash
open usage-guide.pdf
```
Or use this one-liner
```bash
pandoc usage-guide.md -o usage-guide.pdf && open usage-guide.pdf
```
- `notification-system.md` (merged comprehensive guide)
- `web-push-cleanup-guide.md` (cleanup instructions)
- `GLOSSARY.md` (definitions incl. **T** and **Tlead**)

View File

@@ -117,25 +117,25 @@ async function getDatabaseService() {
## Files Modified
1. **src/interfaces/worker-messages.ts** *(NEW)*
1. **src/interfaces/worker-messages.ts** _(NEW)_
- Type definitions for worker communication
- Request and response message interfaces
2. **src/registerSQLWorker.js** *(MAJOR REWRITE)*
2. **src/registerSQLWorker.js** _(MAJOR REWRITE)_
- Message-based operation handling
- **Fixed circular dependency with lazy loading**
- Proper error handling and response formatting
3. **src/services/platforms/WebPlatformService.ts** *(MAJOR REWRITE)*
3. **src/services/platforms/WebPlatformService.ts** _(MAJOR REWRITE)_
- Worker-only database access
- Message sending and response handling
- Timeout and error management
4. **src/main.web.ts** *(SIMPLIFIED)*
4. **src/main.web.ts** _(SIMPLIFIED)_
- Removed duplicate worker creation
- Simplified initialization flow
5. **WORKER_ONLY_DATABASE_IMPLEMENTATION.md** *(NEW)*
5. **WORKER_ONLY_DATABASE_IMPLEMENTATION.md** _(NEW)_
- Complete documentation of changes
## Benefits

View File

@@ -11,12 +11,14 @@ The Android Asset Validation System automatically detects and fixes missing Andr
## Problem Solved
Previously, Android builds would fail with errors like:
```
error: resource drawable/splash (aka app.timesafari.app:drawable/splash) not found.
error: resource mipmap/ic_launcher (aka app.timesafari.app:mipmap/ic_launcher) not found.
```
This happened when:
- Source assets existed but weren't generated into Android resources
- Android resource directories were missing
- Asset generation tools weren't run before building
@@ -45,16 +47,19 @@ npm run build:android:studio
### What Gets Validated
#### Source Assets (Required)
- `resources/icon.png` - App icon source
- `resources/splash.png` - Splash screen source
- `resources/splash_dark.png` - Dark mode splash source
#### Android Resources (Generated)
- `android/app/src/main/res/drawable/splash.png` - Splash screen drawable
- `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities
- `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities
### Density Levels Checked
- `mipmap-mdpi` (1x)
- `mipmap-hdpi` (1.5x)
- `mipmap-xhdpi` (2x)
@@ -64,6 +69,7 @@ npm run build:android:studio
## Usage
### Automatic Validation (Recommended)
The validation runs automatically during all Android builds:
```bash
@@ -78,6 +84,7 @@ npm run build:android:debug
```
### Manual Validation
Run validation only to check/fix assets:
```bash
@@ -89,6 +96,7 @@ npm run assets:validate:android
```
### Validation Only (No Regeneration)
Check configuration without fixing:
```bash
@@ -98,6 +106,7 @@ npm run assets:validate
## Error Handling
### Missing Source Assets
If source assets are missing, the build fails with clear error messages:
```
@@ -108,6 +117,7 @@ If source assets are missing, the build fails with clear error messages:
```
### Missing Generated Resources
If generated resources are missing, they're automatically regenerated:
```
@@ -119,6 +129,7 @@ If generated resources are missing, they're automatically regenerated:
```
### Generation Failure
If regeneration fails, helpful guidance is provided:
```
@@ -131,6 +142,7 @@ If regeneration fails, helpful guidance is provided:
## Integration Points
### Build Script Integration
The validation is integrated into the main build process:
```bash
@@ -143,6 +155,7 @@ validate_android_assets || {
```
### NPM Scripts
New npm scripts for asset management:
```json
@@ -156,17 +169,20 @@ New npm scripts for asset management:
## Benefits
### For Developers
- **No More Build Failures**: Automatic detection and fixing of missing resources
- **Faster Development**: No need to manually run asset generation tools
- **Clear Error Messages**: Helpful guidance when issues occur
- **Consistent Results**: Same validation on all development machines
### For CI/CD
- **Reliable Builds**: Consistent asset validation across environments
- **Early Detection**: Catches issues before they reach production
- **Automated Fixes**: Self-healing builds when possible
### For Project Maintenance
- **Reduced Support**: Fewer "build doesn't work" issues
- **Documentation**: Clear requirements for required assets
- **Standardization**: Consistent asset structure across the project
@@ -176,21 +192,27 @@ New npm scripts for asset management:
### Common Issues
#### "No assets found in the asset path"
This occurs when the `assets/` directory is empty. The validation system automatically copies source assets and regenerates them.
#### "Failed to generate Android assets"
Check that:
- Source assets exist in `resources/`
- `@capacitor/assets` is installed
- You have write permissions to the Android directories
#### "Asset generation completed but some resources are still missing"
This indicates a problem with the asset generation tool. Try:
1. Running `npm install` to ensure dependencies are up to date
2. Manually running `npx @capacitor/assets generate`
3. Checking the asset generation logs for specific errors
### Manual Recovery
If automatic regeneration fails, you can manually create the missing resources:
```bash
@@ -213,12 +235,14 @@ rm assets/icon.png assets/splash.png assets/splash_dark.png
## Future Enhancements
### Planned Improvements
- **iOS Asset Validation**: Extend validation to iOS assets
- **Asset Quality Checks**: Validate image dimensions and formats
- **Performance Optimization**: Cache validation results
- **CI/CD Integration**: Add validation to GitHub Actions
### Configuration Options
- **Custom Asset Paths**: Support for different asset directory structures
- **Validation Rules**: Configurable validation requirements
- **Skip Options**: Ability to skip validation for specific scenarios

View File

@@ -122,4 +122,4 @@ export default class HomeView extends Vue {
---
*This decision was made based on the current codebase architecture and team expertise. The mixin approach provides the best balance of performance, developer experience, and architectural consistency for the TimeSafari application.*
_This decision was made based on the current codebase architecture and team expertise. The mixin approach provides the best balance of performance, developer experience, and architectural consistency for the TimeSafari application._

View File

@@ -92,5 +92,5 @@ Multiple stack traces showing Vue router navigation and component mounting cycle
3. Address API/server issues in separate debugging session
---
*Log Entry by: Migration Assistant*
*Session: ProjectsView.vue Triple Migration Pattern*
_Log Entry by: Migration Assistant_
_Session: ProjectsView.vue Triple Migration Pattern_

View File

@@ -32,6 +32,7 @@ you apply 1-3 meta-rules that automatically include everything you need.
### **Step 1: Always Start with Core Always-On**
**Every single interaction** starts with:
```
meta_core_always_on.mdc
```
@@ -60,64 +61,121 @@ 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
```
**What This Gives You**:
- **Core Always-On**: Human competence focus, time standards, context
- **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
**Meta-Rule Selection**:
```
meta_core_always_on + meta_feature_planning + meta_feature_implementation
```
**What This Gives You**:
- **Core Always-On**: Foundation principles and context
- **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
**Meta-Rule Selection**:
```
meta_core_always_on + meta_documentation
```
**What This Gives You**:
- **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
@@ -154,27 +212,35 @@ Each meta-rule includes success criteria. Use these to validate your work:
## Common Meta-Rule Combinations
### **Research + Diagnosis**
```
meta_core_always_on + meta_research + meta_bug_diagnosis
```
**Use for**: Complex bug investigations requiring systematic analysis
### **Planning + Implementation**
```
meta_core_always_on + meta_feature_planning + meta_feature_implementation
```
**Use for**: End-to-end feature development from concept to deployment
### **Research + Planning**
```
meta_core_always_on + meta_research + meta_feature_planning
```
**Use for**: Feasibility research and solution design
### **Documentation + Context**
```
meta_core_always_on + meta_documentation + [context-specific]
```
**Use for**: Creating comprehensive, educational documentation
## Best Practices

231
doc/notification-system.md Normal file
View File

@@ -0,0 +1,231 @@
# TimeSafari — Native-First Notification System
**Status:** Ready for implementation
**Date:** 2025-09-07
**Author:** Matthew Raymer
---
## Executive Summary
Ship a **single, Native-First** notification system: OS-scheduled **background prefetch at Tlead** + **pre-armed** local notifications. Web-push is retired.
### What we deliver
- **Closed-app delivery:** Pre-armed locals fire even if the app is closed.
- **Freshness:** One prefetch attempt per slot at **Tlead**; ETag/TTL controls; skip when stale.
- **Android precision:** Exact alarms with permission; windowed fallback (±10m) otherwise.
- **Resilience:** Re-arm after reboot/time-change (Android receivers; iOS on next wake/silent push).
- **Cross-platform:** Same TS API (iOS/Android/Electron). Electron is best-effort while running.
### Success signals
- High delivery reliability, minute-precision on Android with permission.
- Prefetch budget hit rate at **Tlead**; zero stale deliveries beyond TTL.
---
## Strategic Plan
### Goal
Deliver 1..M daily notifications with **OS background prefetch at Tlead** and **rolling-window safety** so messages display with fresh content even when the app is closed.
### Tenets
- **Reliability first:** OS delivers once scheduled; no JS at delivery time.
- **Freshness with guardrails:** Prefetch at **Tlead**; enforce **TTL-at-fire**; ETag-aware.
- **Single system:** One TS API; native adapters swap under the hood.
- **Platform honesty:** Android exactness via permission; iOS best-effort budget.
### Architecture (high level)
App (Vue/TS) → Orchestrator (policy) → Native Adapters:
- **SchedulerNative** — AlarmManager (Android) / UNUserNotificationCenter (iOS)
- **BackgroundPrefetchNative** — WorkManager (Android) / BGTaskScheduler (+ silent push) (iOS)
- **DataStore** — SQLite
**Storage (single shared DB):** The app and the native plugin will use **the same SQLite database file**. The app owns schema/migrations; the plugin opens the same file with WAL enabled and performs short, serialized writes. This keeps one source of truth for payloads, delivery logs, and config.
### SQLite Ownership & Concurrency
* **One DB file:** The plugin opens the **same path** the app uses (no second DB).
* **Migrations owned by app:** The app executes schema migrations and bumps `PRAGMA user_version`. The plugin **never** migrates; it **asserts** the expected version.
* **WAL mode:** Open DB with `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000`, `foreign_keys=ON`. WAL allows foreground reads while a background job commits quickly.
* **Single-writer discipline:** Background jobs write in **short transactions** (UPSERT per slot), then return.
* **Encryption (optional):** If using SQLCipher, the **same key** is used by both app and plugin. Do not mix encrypted and unencrypted openings.
### Scheduling & Tlead
- **Arm** a rolling window (today + tomorrow within iOS cap).
- **Attempt** a single **online-first** fetch per slot at **Tlead = T prefetchLeadMinutes**.
- If prefetch is skipped, the armed local **still fires** using cached content.
### Policies
- **TTL-at-fire:** If (T fetchedAt) > `ttlSeconds`**skip** arming.
- **Android exactness:** Request `SCHEDULE_EXACT_ALARM`; fallback **±10m** window.
- **Reboot/time change:** Android receivers re-arm next 24h; iOS on next wake/silent push.
- **No delivery-time mutation:** iOS locals cannot be mutated by NSE; render before scheduling.
---
## Implementation Guide
### 1) Interfaces (TS stable)
- **SchedulerNative**: `scheduleExact({slotId, whenMs, title, body, extra})`, `scheduleWindow(..., windowLenMs)`, `cancelBySlot`, `rescheduleAll`, `capabilities()`
- **BackgroundPrefetchNative**: `schedulePrefetch(slotId, atMs)`, `cancelPrefetch(slotId)`
- **DataStore**: SQLite adapters (notif_contents, notif_deliveries, notif_config)
- **Public API**: `configure`, `requestPermissions`, `runFullPipelineNow`, `reschedule`, `getState`
### DB Path & Adapter Configuration
* **Configure option:** `dbPath: string` (absolute path or platform alias) is passed from JS to the plugin during `configure()`.
* **Shared tables:**
* `notif_contents(slot_id, payload_json, fetched_at, etag, …)`
* `notif_deliveries(slot_id, fire_at, delivered_at, status, error_code, …)`
* `notif_config(k, v)`
* **Open settings:**
* `journal_mode=WAL`
* `synchronous=NORMAL`
* `busy_timeout=5000`
* `foreign_keys=ON`
**Type (TS) extension**
```ts
export type ConfigureOptions = {
// …existing fields…
dbPath: string; // shared DB file the plugin will open
storage: 'shared'; // canonical value; plugin-owned DB is not used
};
```
**Plugin side (pseudo)**
```kotlin
// Android open
val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READWRITE)
db.execSQL("PRAGMA journal_mode=WAL")
db.execSQL("PRAGMA synchronous=NORMAL")
db.execSQL("PRAGMA foreign_keys=ON")
db.execSQL("PRAGMA busy_timeout=5000")
// Verify schema version
val uv = rawQuery("PRAGMA user_version").use { it.moveToFirst(); it.getInt(0) }
require(uv >= MIN_EXPECTED_VERSION) { "Schema version too old" }
```
```swift
// iOS open (FMDB / SQLite3)
// Set WAL via PRAGMA after open; check user_version the same way.
```
### 2) Templating & Arming
- Render `title/body` **before** scheduling; pass via **SchedulerNative**.
- Route all arming through **SchedulerNative** to centralize Android exact/window semantics.
### 3) Tlead (single attempt)
**Tlead governs prefetch, not arming.** We **arm** one-shot locals as part of the rolling window so closed-app delivery is guaranteed. At **Tlead = T prefetchLeadMinutes**, the **native background job** attempts **one** 12s ETag-aware fetch. If fresh content arrives and will not violate **TTL-at-fire**, we (re)arm the upcoming slot; if the OS skips the wake, the pre-armed local still fires with cached content.
- Compute Tlead = `whenMs - prefetchLeadMinutes*60_000`.
- `BackgroundPrefetchNative.schedulePrefetch(slotId, atMs=Tlead)`.
- On wake: **ETag** fetch (timeout **12s**), persist, optionally cancel & re-arm if within TTL.
- Never fetch at delivery time.
### 4) TTL-at-fire
**TTL-at-fire:** Before arming for time **T**, compute `T fetchedAt`. If that exceeds `ttlSeconds`, **do not arm** (skip). This prevents posting stale notifications when the app has been closed for a long time.
`if (whenMs - fetchedAt) > ttlSeconds*1000 → skip`
### 5) Android specifics
- Request `SCHEDULE_EXACT_ALARM`; deep-link if denied; fallback to `setWindow(start,len)` (±10m).
- Receivers: `BOOT_COMPLETED`, `TIMEZONE_CHANGED`, `TIME_SET` → recompute & re-arm for next 24h and schedule Tlead prefetch.
### 6) iOS specifics
- `BGTaskScheduler` for Tlead prefetch (best-effort). Optional silent push nudge.
- Locals: `UNCalendarNotificationTrigger` (one-shots); no NSE mutation for locals.
### 7) Network & Timeouts
- Content fetch: **12s** timeout; single attempt at Tlead; ETag/304 respected.
- ACK/Error: **8s** timeout, fire-and-forget.
### 8) Electron
- Notifications while app is running; recommend **Start-on-Login**. No true background scheduling when fully closed.
### 9) Telemetry
- Record `scheduled|shown|error`; ACK deliveries (8s timeout); include slot/times/TZ/app version.
---
## Capability Matrix
| Capability | Android (Native) | iOS (Native) | Electron | Web |
|---|---|---|---|---|
| Multi-daily locals (closed app) | ✅ | ✅ | ✅ (app running) | — |
| Prefetch at Tlead (app closed) | ✅ WorkManager | ⚠️ BGTask (best-effort) | ✅ (app running) | — |
| Re-arm after reboot/time-change | ✅ Receivers | ⚠️ On next wake/silent push | ✅ Start-on-Login | — |
| Minute-precision alarms | ✅ with exact permission | ❌ not guaranteed | ✅ timer best-effort | — |
| Delivery-time mutation for locals | ❌ | ❌ | — | — |
| ETag/TTL enforcement | ✅ | ✅ | ✅ | — |
| Rolling-window safety | ✅ | ✅ | ✅ | — |
---
## Acceptance Criteria
### Core
- **Closed-app delivery:** Armed locals fire at T with last rendered content. No delivery-time network.
- **Tlead prefetch:** Single background attempt at **Tlead**; if skipped, delivery still occurs from cache.
- **TTL-at-fire:** No armed local violates TTL at T.
### Android
- **Exact permission path:** With `SCHEDULE_EXACT_ALARM` → within ±1m; else **±10m** window.
- **Reboot recovery:** After reboot, receivers re-arm next 24h and schedule Tlead prefetch.
- **TZ/DST change:** Recompute & re-arm; future slots align to new wall-clock.
### iOS
- **BGTask budget respected:** Prefetch often runs but may be skipped; delivery still occurs via rolling window.
- **Force-quit caveat:** No background execution after user terminate; delivery still occurs if pre-armed.
### Electron
- **Running-app rule:** Delivery only while app runs; with Start-on-Login, after reboot the orchestrator re-arms and subsequent slots deliver.
### Network
- Content fetch timeout **12s**; ACK/Error **8s**; no retries inside lead; ETag honored.
### Observability
- Log/telemetry for `scheduled|shown|error`; ACK payload includes slot, times, device TZ, app version.
### DB Sharing
* **Shared DB visibility:** A background prefetch writes `notif_contents`; the foreground UI **immediately** reads the same row.
* **WAL overlap:** With the app reading while the plugin commits, no user-visible blocking occurs.
* **Version safety:** If `user_version` is behind, the plugin emits an error and does not write (protects against partial installs).
---
## Web-Push Cleanup
Web-push functionality has been retired due to unreliability. All web-push related code paths and documentation sections should be removed or marked as deprecated. See `web-push-cleanup-guide.md` for detailed cleanup steps.
---
_This document consolidates the Native-First notification system strategy, implementation details, capabilities, and acceptance criteria into a single comprehensive reference._

View File

@@ -0,0 +1,551 @@
# TimeSafari Web-Push Cleanup Guide
**Status:** 🚀 Native-First Implementation
**Date:** 2025-01-27T14:30Z (UTC)
**Author:** Matthew Raymer
**Scope:** Web-push code cleanup and deprecation
**Goal:** Remove or quarantine all web-push code paths and mark as deprecated.
---
## Executive Summary
This document provides a comprehensive cleanup guide for removing web-push code
paths from TimeSafari. Web-push has been retired for unreliability, and the
system now focuses on native mobile reliability with Electron best-effort support.
---
## Cleanup Strategy
### Phase 1: Identify Web-Push Code Paths
#### Service Worker Files
- [ ] `sw_scripts/notification-click.js` - Mark as deprecated
- [ ] `sw_scripts/` directory - Review for web-push dependencies
- [ ] Service worker registration code - Remove or quarantine
#### Web-Specific Code
- [ ] Web push notification handlers
- [ ] Service worker event listeners
- [ ] Web notification API usage
- [ ] Push subscription management
#### Configuration Files
- [ ] VitePWA plugin configuration
- [ ] Service worker build configuration
- [ ] Web push manifest files
### Phase 2: Mark as Deprecated
#### Code Comments
```javascript
// DEPRECATED: Web-push notification handling
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
```
#### Documentation Updates
- [ ] Mark web-push sections as deprecated
- [ ] Add deprecation notices
- [ ] Update README files
- [ ] Update API documentation
### Phase 3: Remove or Quarantine
#### Complete Removal
- [ ] Web push subscription code
- [ ] Service worker notification handlers
- [ ] Web-specific notification APIs
- [ ] Push message handling
#### Quarantine (Keep for Reference)
- [ ] Service worker registration code
- [ ] Web push configuration
- [ ] Historical web-push tests
---
## Detailed Cleanup Tasks
### 1. Service Worker Cleanup
#### Files to Deprecate
**`sw_scripts/notification-click.js`**
```javascript
// DEPRECATED: Service worker notification handling
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
// Original web-push notification click handler
self.addEventListener('notificationclick', (event) => {
// DEPRECATED: Web-push only
event.notification.close();
const slotId = event.notification.data?.slotId;
const route = slotId ? '/#/daily' : '/#/notifications';
event.waitUntil(
clients.openWindow(route).catch(() => {
return clients.openWindow('/');
})
);
});
```
**Service Worker Registration**
```javascript
// DEPRECATED: Service worker registration
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
if ('serviceWorker' in navigator && process.env.VITE_PLATFORM === 'web') {
// DEPRECATED: Web-push only
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker registered:', registration);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}
```
### 2. Web Push API Cleanup
#### Push Subscription Management
```javascript
// DEPRECATED: Web push subscription management
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
class WebPushManager {
// DEPRECATED: Web-push only
async subscribeToPush() {
// Implementation kept for reference
}
// DEPRECATED: Web-push only
async unsubscribeFromPush() {
// Implementation kept for reference
}
}
```
#### Push Message Handling
```javascript
// DEPRECATED: Push message handling
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
self.addEventListener('push', (event) => {
// DEPRECATED: Web-push only
const data = event.data ? event.data.json() : {};
const options = {
body: data.body,
icon: '/icon-192x192.png',
badge: '/badge-72x72.png',
data: data
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
```
### 3. Configuration Cleanup
#### VitePWA Plugin Configuration
```javascript
// DEPRECATED: VitePWA plugin configuration
// This configuration is kept for reference but not used in production
// Replaced by Native-First notification system
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
// DEPRECATED: Web-push only
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
},
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
manifest: {
name: 'TimeSafari',
short_name: 'TimeSafari',
description: 'TimeSafari App',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
}
]
}
})
]
})
```
#### Service Worker Build Configuration
```javascript
// DEPRECATED: Service worker build configuration
// This configuration is kept for reference but not used in production
// Replaced by Native-First notification system
export default defineConfig({
build: {
rollupOptions: {
input: {
// DEPRECATED: Web-push only
sw: 'sw_scripts/notification-click.js'
}
}
}
})
```
### 4. Test Cleanup
#### Web Push Tests
```javascript
// DEPRECATED: Web push tests
// These tests are kept for reference but not used in production
// Replaced by Native-First notification system
describe('Web Push Notifications (DEPRECATED)', () => {
// DEPRECATED: Web-push only
it('should handle push notifications', async () => {
// Test implementation kept for reference
});
// DEPRECATED: Web-push only
it('should handle notification clicks', async () => {
// Test implementation kept for reference
});
});
```
#### Service Worker Tests
```javascript
// DEPRECATED: Service worker tests
// These tests are kept for reference but not used in production
// Replaced by Native-First notification system
describe('Service Worker (DEPRECATED)', () => {
// DEPRECATED: Web-push only
it('should register service worker', async () => {
// Test implementation kept for reference
});
// DEPRECATED: Web-push only
it('should handle push events', async () => {
// Test implementation kept for reference
});
});
```
### 5. Documentation Cleanup
#### README Updates
```markdown
# TimeSafari Native-First Notification System
## Web-Push Status: DEPRECATED
Web-push has been retired for unreliability. The system now focuses on native mobile reliability with Electron best-effort support.
### Deprecated Features
- ❌ Web push notifications
- ❌ Service worker notification handling
- ❌ Web notification API
### Active Features
- ✅ Native mobile notifications (Android/iOS)
- ✅ Electron notifications (best-effort)
- ✅ OS-scheduled background prefetch
- ✅ Rolling window safety
```
#### API Documentation Updates
```markdown
## Notification API (Native-First)
### Deprecated Methods
- `subscribeToPush()` - DEPRECATED: Web-push only
- `unsubscribeFromPush()` - DEPRECATED: Web-push only
- `handlePushMessage()` - DEPRECATED: Web-push only
### Active Methods
- `scheduleExact()` - Native exact scheduling
- `scheduleWindow()` - Native windowed scheduling
- `schedulePrefetch()` - Native background prefetch
```
---
## File-by-File Cleanup Checklist
### Service Worker Files
- [ ] `sw_scripts/notification-click.js` - Mark as deprecated
- [ ] `sw_scripts/` directory - Review for web-push dependencies
- [ ] Service worker build configuration - Remove or quarantine
### Web-Specific Code
- [ ] `src/main.web.ts` - Remove service worker registration
- [ ] `src/services/webPush.ts` - Mark as deprecated
- [ ] `src/utils/serviceWorker.ts` - Mark as deprecated
- [ ] Web notification API usage - Remove or quarantine
### Configuration Files
- [ ] `vite.config.web.mts` - Remove VitePWA plugin
- [ ] `package.json` - Remove web-push dependencies
- [ ] `public/manifest.json` - Mark as deprecated
- [ ] Service worker build scripts - Remove or quarantine
### Test Files
- [ ] `test-playwright/web-push.spec.ts` - Mark as deprecated
- [ ] `test/services/webPush.test.ts` - Mark as deprecated
- [ ] Service worker tests - Mark as deprecated
### Documentation Files
- [ ] `README.md` - Update to reflect native-first approach
- [ ] `doc/web-push.md` - Mark as deprecated
- [ ] API documentation - Remove web-push references
---
## Dependencies to Remove
### NPM Packages
```json
{
"dependencies": {
// DEPRECATED: Web-push only
"web-push": "^7.4.0",
"vite-plugin-pwa": "^0.17.0"
},
"devDependencies": {
// DEPRECATED: Web-push only
"workbox-webpack-plugin": "^6.5.0"
}
}
```
### Build Scripts
```json
{
"scripts": {
// DEPRECATED: Web-push only
"build:sw": "workbox generateSW",
"test:sw": "jest --testPathPattern=serviceWorker"
}
}
```
---
## Migration Guide
### From Web-Push to Native-First
#### Step 1: Remove Web-Push Dependencies
```bash
# Remove web-push packages
npm uninstall web-push vite-plugin-pwa workbox-webpack-plugin
# Remove service worker files
rm -rf sw_scripts/
rm -f public/sw.js
rm -f public/workbox-*.js
```
#### Step 2: Update Configuration
```javascript
// Remove VitePWA plugin from vite.config.web.mts
export default defineConfig({
plugins: [
// Remove VitePWA plugin
// VitePWA({ ... })
]
})
```
#### Step 3: Update Service Registration
```javascript
// Remove service worker registration from main.web.ts
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/sw.js')
// }
```
#### Step 4: Update Tests
```javascript
// Remove web-push tests
// describe('Web Push Notifications', () => { ... })
```
---
## Verification Checklist
### Code Removal Verification
- [ ] No web-push imports remain
- [ ] No service worker registration code
- [ ] No push subscription management
- [ ] No web notification API usage
- [ ] No VitePWA plugin configuration
### Documentation Verification
- [ ] All web-push references marked as deprecated
- [ ] README updated to reflect native-first approach
- [ ] API documentation updated
- [ ] Test documentation updated
### Build Verification
- [ ] Web build succeeds without service worker
- [ ] No service worker files generated
- [ ] No web-push dependencies in bundle
- [ ] Native builds work correctly
### Test Verification
- [ ] Web-push tests are marked as deprecated
- [ ] Native notification tests pass
- [ ] No web-push test failures
- [ ] Test suite runs successfully
---
## Rollback Plan
### Emergency Rollback
If native-first implementation fails, web-push code can be restored:
#### 1. **Restore Dependencies**
```bash
npm install web-push vite-plugin-pwa workbox-webpack-plugin
```
#### 2. **Restore Service Worker Files**
```bash
git checkout HEAD~1 -- sw_scripts/
git checkout HEAD~1 -- public/sw.js
```
#### 3. **Restore Configuration**
```bash
git checkout HEAD~1 -- vite.config.web.mts
git checkout HEAD~1 -- package.json
```
#### 4. **Restore Tests**
```bash
git checkout HEAD~1 -- test-playwright/web-push.spec.ts
git checkout HEAD~1 -- test/services/webPush.test.ts
```
### Rollback Verification
- [ ] Web-push functionality restored
- [ ] Service worker registration works
- [ ] Push notifications work
- [ ] Tests pass
---
## Post-Cleanup Tasks
### Code Review
- [ ] Review all changes for completeness
- [ ] Verify no web-push code remains
- [ ] Check for orphaned references
- [ ] Validate native-first implementation
### Testing
- [ ] Run full test suite
- [ ] Verify native notifications work
- [ ] Check Electron functionality
- [ ] Validate mobile builds
### Documentation
- [ ] Update all documentation
- [ ] Remove web-push references
- [ ] Update API documentation
- [ ] Update user guides
---
## Success Criteria
### Complete Web-Push Removal
- [ ] All web-push code marked as deprecated
- [ ] Service worker files quarantined
- [ ] Dependencies removed
- [ ] Configuration updated
### Native-First Implementation
- [ ] Native notifications work on Android
- [ ] Native notifications work on iOS
- [ ] Electron notifications work
- [ ] Background prefetch works
### Documentation Updated
- [ ] All docs reflect native-first approach
- [ ] Web-push marked as deprecated
- [ ] Migration guide provided
- [ ] Rollback plan documented
---
_This cleanup guide provides comprehensive instructions for removing web-push
code paths from TimeSafari. Web-push has been retired for unreliability, and the
system now focuses on native mobile reliability with Electron best-effort support._

View File

@@ -5,12 +5,15 @@
**Status**: 🎯 **ACTIVE** - Z-index layering standards
## Objective
Establish consistent z-index values across the TimeSafari application to ensure proper layering of UI elements.
## Result
This document defines the z-index hierarchy for all UI components.
## Use/Run
Reference these values when implementing new components or modifying existing ones to maintain consistent layering.
## Z-Index Hierarchy
@@ -51,7 +54,7 @@ Reference these values when implementing new components or modifying existing on
## Collaboration Hooks
- **Reviewers**: Frontend team, UI/UX designers
- **Sign-off checklist**:
- **Sign-off checklist**:
- [ ] All new components follow z-index guidelines
- [ ] Existing components updated to use defined values
- [ ] Cross-browser testing completed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -136,7 +136,6 @@
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
},
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2",

View File

@@ -181,7 +181,7 @@ sync_capacitor() {
copy_web_assets() {
log_info "Copying web assets to Electron"
safe_execute "Copying assets" "cp -r dist/* electron/app/"
safe_execute "Copying config" "cp capacitor.config.json electron/capacitor.config.json"
# Note: Electron has its own capacitor.config.ts file, so we don't copy the main config
}
# Compile TypeScript

View File

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

View File

@@ -20,6 +20,7 @@ import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import { logger } from "../utils/logger";
@Component({
mixins: [PlatformServiceMixin],
@@ -44,26 +45,49 @@ export default class TopMessage extends Vue {
this.notify = createNotifyHelpers(this.$notify);
try {
// Ultra-concise cached settings loading - replaces 50+ lines of logic!
const settings = await this.$accountSettings(undefined, {
activeDid: undefined,
apiServer: AppString.PROD_ENDORSER_API_SERVER,
// Load settings without overriding database values - fixes settings inconsistency
logger.debug("[TopMessage] 📥 Loading settings without overrides...");
const settings = await this.$accountSettings();
logger.debug("[TopMessage] 📊 Settings loaded:", {
activeDid: settings.activeDid,
apiServer: settings.apiServer,
warnIfTestServer: settings.warnIfTestServer,
warnIfProdServer: settings.warnIfProdServer,
component: "TopMessage",
timestamp: new Date().toISOString(),
});
// Only show warnings if the user has explicitly enabled them
if (
settings.warnIfTestServer &&
settings.apiServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You're not using prod, user " + didPrefix;
logger.debug("[TopMessage] ⚠️ Test server warning displayed:", {
apiServer: settings.apiServer,
didPrefix: didPrefix,
});
} else if (
settings.warnIfProdServer &&
settings.apiServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You are using prod, user " + didPrefix;
logger.debug("[TopMessage] ⚠️ Production server warning displayed:", {
apiServer: settings.apiServer,
didPrefix: didPrefix,
});
} else {
logger.debug(
"[TopMessage] No warnings displayed - conditions not met",
);
}
} catch (err: unknown) {
logger.error("[TopMessage] ❌ Error loading settings:", err);
this.notify.error(JSON.stringify(err), TIMEOUTS.MODAL);
}
}

View File

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

View File

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

View File

@@ -486,6 +486,15 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
/**
* Tracks in-flight requests to prevent duplicate API calls for the same plan
* @constant {Map}
*/
const inFlightRequests = new Map<
string,
Promise<PlanSummaryRecord | undefined>
>();
/**
* Retrieves plan data from cache or server
* @param {string} handleId - Plan handle ID
@@ -505,40 +514,140 @@ export async function getPlanFromCache(
if (!handleId) {
return undefined;
}
let cred = planCache.get(handleId);
if (!cred) {
const url =
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(requesterDid);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
// Use debug level for development to reduce console noise
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log(
"[EndorserServer] Plan cache is empty for handle",
handleId,
" Got data:",
JSON.stringify(resp.data),
);
}
} catch (error) {
logger.error(
"[EndorserServer] Failed to load plan with handle",
handleId,
" Got error:",
JSON.stringify(error),
);
}
// Check cache first (existing behavior)
const cred = planCache.get(handleId);
if (cred) {
return cred;
}
// Check if request is already in flight (NEW: request deduplication)
if (inFlightRequests.has(handleId)) {
logger.debug(
"[Plan Loading] 🔄 Request already in flight, reusing promise:",
{
handleId,
requesterDid,
timestamp: new Date().toISOString(),
},
);
return inFlightRequests.get(handleId);
}
// Create new request promise (NEW: request coordination)
const requestPromise = performPlanRequest(
handleId,
axios,
apiServer,
requesterDid,
);
inFlightRequests.set(handleId, requestPromise);
try {
const result = await requestPromise;
return result;
} finally {
// Clean up in-flight request tracking (NEW: cleanup)
inFlightRequests.delete(handleId);
}
}
/**
* Performs the actual plan request to the server
* @param {string} handleId - Plan handle ID
* @param {Axios} axios - Axios instance
* @param {string} apiServer - API server URL
* @param {string} [requesterDid] - Optional requester DID for private info
* @returns {Promise<PlanSummaryRecord|undefined>} Plan data or undefined if not found
*
* @throws {Error} If server request fails
*/
async function performPlanRequest(
handleId: string,
axios: Axios,
apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> {
const url =
apiServer + "/api/v2/report/plans?handleId=" + encodeURIComponent(handleId);
const headers = await getHeaders(requesterDid);
// Enhanced diagnostic logging for plan loading
const requestId = `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[Plan Loading] 🔍 Loading plan from server:", {
requestId,
handleId,
apiServer,
endpoint: url,
requesterDid,
timestamp: new Date().toISOString(),
});
try {
const resp = await axios.get(url, { headers });
logger.debug("[Plan Loading] ✅ Plan loaded successfully:", {
requestId,
handleId,
status: resp.status,
hasData: !!resp.data?.data,
dataLength: resp.data?.data?.length || 0,
timestamp: new Date().toISOString(),
});
if (resp.status === 200 && resp.data?.data?.length > 0) {
const cred = resp.data.data[0];
planCache.set(handleId, cred);
logger.debug("[Plan Loading] 💾 Plan cached:", {
requestId,
handleId,
planName: cred?.name,
planIssuer: cred?.issuerDid,
});
return cred;
} else {
// Use debug level for development to reduce console noise
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log(
"[Plan Loading] ⚠️ Plan cache is empty for handle",
handleId,
" Got data:",
JSON.stringify(resp.data),
);
return undefined;
}
} catch (error) {
// Enhanced error logging for plan loading failures
const axiosError = error as {
response?: {
data?: unknown;
status?: number;
statusText?: string;
};
message?: string;
};
logger.error("[Plan Loading] ❌ Failed to load plan:", {
requestId,
handleId,
apiServer,
endpoint: url,
requesterDid,
errorStatus: axiosError.response?.status,
errorStatusText: axiosError.response?.statusText,
errorData: axiosError.response?.data,
errorMessage: axiosError.message || String(error),
timestamp: new Date().toISOString(),
});
throw error;
}
return cred;
}
/**
@@ -1019,19 +1128,82 @@ export async function createAndSubmitClaim(
const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload);
// Enhanced diagnostic logging for claim submission
const requestId = `claim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.info("[Claim Submission] 🚀 Starting claim submission:", {
requestId,
apiServer,
requesterDid: issuerDid,
endpoint: `${apiServer}/api/v2/claim`,
timestamp: new Date().toISOString(),
jwtLength: vcJwt.length,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = `${apiServer}/api/v2/claim`;
logger.debug("[Claim Submission] 📡 Making API request:", {
requestId,
url,
payloadSize: payload.length,
headers: { "Content-Type": "application/json" },
});
const response = await axios.post(url, payload, {
headers: {
"Content-Type": "application/json",
},
});
logger.info("[Claim Submission] ✅ Claim submitted successfully:", {
requestId,
status: response.status,
handleId: response.data?.handleId,
responseSize: JSON.stringify(response.data).length,
timestamp: new Date().toISOString(),
});
return { success: true, handleId: response.data?.handleId };
} catch (error: unknown) {
logger.error("Error submitting claim:", error);
// Enhanced error logging with comprehensive context
const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
statusText?: string;
headers?: Record<string, string>;
};
config?: {
url?: string;
method?: string;
headers?: Record<string, string>;
};
message?: string;
};
logger.error("[Claim Submission] ❌ Claim submission failed:", {
requestId,
apiServer,
requesterDid: issuerDid,
endpoint: `${apiServer}/api/v2/claim`,
errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status,
httpStatusText: axiosError.response?.statusText,
responseHeaders: axiosError.response?.headers,
requestConfig: {
url: axiosError.config?.url,
method: axiosError.config?.method,
headers: axiosError.config?.headers,
},
originalError: axiosError.message || String(error),
timestamp: new Date().toISOString(),
});
const errorMessage: string =
serverMessageForUser(error) ||
(error && typeof error === "object" && "message" in error
@@ -1494,14 +1666,56 @@ export async function fetchEndorserRateLimits(
) {
const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(issuerDid);
// Enhanced diagnostic logging for user registration tracking
logger.debug("[User Registration] Checking user status on server:", {
did: issuerDid,
server: apiServer,
endpoint: url,
timestamp: new Date().toISOString(),
});
try {
const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful registration check
logger.debug("[User Registration] User registration check successful:", {
did: issuerDid,
server: apiServer,
status: response.status,
isRegistered: true,
timestamp: new Date().toISOString(),
});
return response;
} catch (error) {
// Enhanced error logging with user registration context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
const errorCode = axiosError.response?.data?.error?.code;
const errorMessage = axiosError.response?.data?.error?.message;
const httpStatus = axiosError.response?.status;
logger.warn("[User Registration] User not registered on server:", {
did: issuerDid,
server: apiServer,
errorCode: errorCode,
errorMessage: errorMessage,
httpStatus: httpStatus,
needsRegistration: true,
timestamp: new Date().toISOString(),
});
// Log the original error for debugging
logger.error(
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
errorStringForLog(error),
);
throw error;
}
}
@@ -1514,8 +1728,53 @@ export async function fetchEndorserRateLimits(
* @param {string} issuerDid - The DID for which to check rate limits.
* @returns {Promise<AxiosResponse>} The Axios response object.
*/
export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
export async function fetchImageRateLimits(
axios: Axios,
issuerDid: string,
imageServer?: string,
) {
const server = imageServer || DEFAULT_IMAGE_API_SERVER;
const url = server + "/image-limits";
const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig);
// Enhanced diagnostic logging for image server calls
logger.debug("[Image Server] Checking image rate limits:", {
did: issuerDid,
server: server,
endpoint: url,
timestamp: new Date().toISOString(),
});
try {
const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful image server call
logger.debug("[Image Server] Image rate limits check successful:", {
did: issuerDid,
server: server,
status: response.status,
timestamp: new Date().toISOString(),
});
return response;
} catch (error) {
// Enhanced error logging for image server failures
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
logger.warn("[Image Server] Image rate limits check failed:", {
did: issuerDid,
server: server,
errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(),
});
throw error;
}
}

View File

@@ -614,57 +614,64 @@ export const retrieveAllAccountsMetadata = async (): Promise<
return result;
};
export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account.";
/**
* Saves a new identity to both SQL and Dexie databases
* Saves a new identity to SQL database
*/
export async function saveNewIdentity(
identity: IIdentifier,
mnemonic: string,
derivationPath: string,
): Promise<void> {
try {
// add to the new sql db
const platformService = await getPlatformService();
// add to the new sql db
const platformService = await getPlatformService();
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
);
}
// Check if account already exists before attempting to save
const existingAccount = await platformService.dbQuery(
"SELECT did FROM accounts WHERE did = ?",
[identity.did],
);
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const identityStr = JSON.stringify(identity);
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`;
const params = [
new Date().toISOString(),
derivationPath,
identity.did,
encryptedIdentityBase64,
encryptedMnemonicBase64,
identity.keys[0].publicKeyHex,
];
await platformService.dbExec(sql, params);
await platformService.updateDefaultSettings({ activeDid: identity.did });
await platformService.insertNewDidIntoSettings(identity.did);
} catch (error) {
logger.error("Failed to update default settings:", error);
if (existingAccount?.values?.length) {
throw new Error(
"Failed to set default settings. Please try again or restart the app.",
`Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
);
}
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
);
}
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const identityStr = JSON.stringify(identity);
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`;
const params = [
new Date().toISOString(),
derivationPath,
identity.did,
encryptedIdentityBase64,
encryptedMnemonicBase64,
identity.keys[0].publicKeyHex,
];
await platformService.dbExec(sql, params);
await platformService.updateDefaultSettings({ activeDid: identity.did });
await platformService.insertNewDidIntoSettings(identity.did);
}
/**
@@ -974,13 +981,16 @@ export async function importFromMnemonic(
const firstName = settings[0];
const isRegistered = settings[1];
logger.info("[importFromMnemonic] Test User #0 settings verification", {
did: newId.did,
firstName,
isRegistered,
expectedFirstName: "User Zero",
expectedIsRegistered: true,
});
logger.debug(
"[importFromMnemonic] Test User #0 settings verification",
{
did: newId.did,
firstName,
isRegistered,
expectedFirstName: "User Zero",
expectedIsRegistered: true,
},
);
// If settings weren't saved correctly, try individual updates
if (firstName !== "User Zero" || isRegistered !== 1) {
@@ -1006,7 +1016,7 @@ export async function importFromMnemonic(
if (retryResult?.values?.length) {
const retrySettings = retryResult.values[0];
logger.info(
logger.debug(
"[importFromMnemonic] Test User #0 settings after retry",
{
firstName: retrySettings[0],
@@ -1029,3 +1039,58 @@ export async function importFromMnemonic(
}
}
}
/**
* Checks if an account with the given DID already exists in the database
*
* @param did - The DID to check for duplicates
* @returns Promise<boolean> - True if account already exists, false otherwise
* @throws Error if database query fails
*/
export async function checkForDuplicateAccount(did: string): Promise<boolean>;
/**
* Checks if an account with the given DID already exists in the database
*
* @param mnemonic - The mnemonic phrase to derive DID from
* @param derivationPath - The derivation path to use
* @returns Promise<boolean> - True if account already exists, false otherwise
* @throws Error if database query fails
*/
export async function checkForDuplicateAccount(
mnemonic: string,
derivationPath: string,
): Promise<boolean>;
/**
* Implementation of checkForDuplicateAccount with overloaded signatures
*/
export async function checkForDuplicateAccount(
didOrMnemonic: string,
derivationPath?: string,
): Promise<boolean> {
let didToCheck: string;
if (derivationPath) {
// Derive the DID from mnemonic and derivation path
const [address, privateHex, publicHex] = deriveAddress(
didOrMnemonic.trim().toLowerCase(),
derivationPath,
);
const newId = newIdentifier(address, privateHex, publicHex, derivationPath);
didToCheck = newId.did;
} else {
// Use the provided DID directly
didToCheck = didOrMnemonic;
}
// Check if an account with this DID already exists
const platformService = await getPlatformService();
const existingAccount = await platformService.dbQuery(
"SELECT did FROM accounts WHERE did = ?",
[didToCheck],
);
return (existingAccount?.values?.length ?? 0) > 0;
}

View File

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

View File

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

View File

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

View File

@@ -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<Settings | null> {
try {
// Master settings: query by id
const result = await this.$dbQuery(
"SELECT * FROM settings WHERE id = ? OR accountDid = ?",
[key, key],
"SELECT * FROM settings WHERE id = ?",
[MASTER_SETTINGS_KEY],
);
if (!result?.values?.length) {
@@ -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<Settings> {
try {
// Get default settings
const defaultSettings = await this.$getSettings(
defaultKey,
defaultFallback,
);
const defaultSettings = await this.$getMasterSettings(defaultFallback);
// If no account DID, return defaults
if (!accountDid) {
@@ -757,19 +753,20 @@ export const PlatformServiceMixin = {
* @returns Fresh settings object from database
*/
async $settings(defaults: Settings = {}): Promise<Settings> {
const settings = await this.$getSettings(MASTER_SETTINGS_KEY, defaults);
const settings = await this.$getMasterSettings(defaults);
if (!settings) {
return defaults;
}
// **ELECTRON-SPECIFIC FIX**: Apply platform-specific API server override
// This ensures Electron always uses production endpoints regardless of cached settings
if (process.env.VITE_PLATFORM === "electron") {
// FIXED: Remove forced override - respect user preferences
// Only set default if no user preference exists
if (!settings.apiServer && process.env.VITE_PLATFORM === "electron") {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
// Only set if user hasn't specified a preference
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
@@ -789,10 +786,7 @@ export const PlatformServiceMixin = {
): Promise<Settings> {
try {
// Get default settings first
const defaultSettings = await this.$getSettings(
MASTER_SETTINGS_KEY,
defaults,
);
const defaultSettings = await this.$getMasterSettings(defaults);
if (!defaultSettings) {
return defaults;
@@ -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<void> {
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<QueryExecResult | undefined>;
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
$getSettings(
key: string,
fallback?: Settings | null,
): Promise<Settings | null>;
$getMasterSettings(fallback?: Settings | null): Promise<Settings | null>;
$getMergedSettings(
defaultKey: string,
accountDid?: string,
@@ -1749,10 +1740,7 @@ declare module "@vue/runtime-core" {
sql: string,
params?: unknown[],
): Promise<unknown[] | undefined>;
$getSettings(
key: string,
defaults?: Settings | null,
): Promise<Settings | null>;
$getMasterSettings(defaults?: Settings | null): Promise<Settings | null>;
$getMergedSettings(
key: string,
did?: string,

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

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

View File

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

View File

@@ -58,8 +58,10 @@
v-if="!isRegistered"
:passkeys-enabled="PASSKEYS_ENABLED"
:given-name="givenName"
message="Before you can publicly announce a new project or time commitment,
a friend needs to register you."
:message="
`Before you can publicly announce a new project or time commitment, ` +
`a friend needs to register you.`
"
/>
<!-- Notifications -->
@@ -752,6 +754,7 @@ import "leaflet/dist/leaflet.css";
import { Buffer } from "buffer/";
import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
@@ -813,11 +816,13 @@ import {
isApiError,
ImportContent,
} from "@/interfaces/accountView";
import {
ProfileService,
createProfileService,
ProfileData,
} from "@/services/ProfileService";
// Profile data interface (inlined from ProfileService)
interface ProfileData {
description: string;
latitude: number;
longitude: number;
includeLocation: boolean;
}
const inputImportFileNameRef = ref<Blob>();
@@ -916,7 +921,6 @@ export default class AccountViewView extends Vue {
imageLimits: ImageRateLimits | null = null;
limitsMessage: string = "";
private profileService!: ProfileService;
private notify!: ReturnType<typeof createNotifyHelpers>;
created() {
@@ -926,7 +930,10 @@ export default class AccountViewView extends Vue {
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
if (L.Icon.Default) {
// Type-safe way to handle Leaflet icon prototype
const iconDefault = L.Icon.Default.prototype as Record<string, unknown>;
const iconDefault = L.Icon.Default.prototype as unknown as Record<
string,
unknown
>;
if ("_getIconUrl" in iconDefault) {
delete iconDefault._getIconUrl;
}
@@ -948,17 +955,21 @@ export default class AccountViewView extends Vue {
* @throws Will display specific messages to the user based on different errors.
*/
async mounted(): Promise<void> {
this.profileService = createProfileService(
this.axios,
this.partnerApiServer,
);
try {
await this.initializeState();
await this.processIdentity();
// Profile service logic now inlined - no need for external service
logger.debug(
"[AccountViewView] Profile logic ready with partnerApiServer:",
{
partnerApiServer: this.partnerApiServer,
},
);
if (this.isRegistered) {
try {
const profile = await this.profileService.loadProfile(this.activeDid);
const profile = await this.loadProfile(this.activeDid);
if (profile) {
this.userProfileDesc = profile.description;
this.userProfileLatitude = profile.latitude;
@@ -1411,21 +1422,24 @@ export default class AccountViewView extends Vue {
async checkLimits(): Promise<void> {
this.loadingLimits = true;
const did = this.activeDid;
if (!did) {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
return;
}
try {
const did = this.activeDid;
if (!did) {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
return;
}
await this.$saveUserSettings(did, {
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
webPushServer: this.webPushServer,
});
const imageResp = await fetchImageRateLimits(this.axios, did);
const imageResp = await fetchImageRateLimits(
this.axios,
did,
this.DEFAULT_IMAGE_API_SERVER,
);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
@@ -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<void> {
await this.$saveSettings({
apiServer: this.apiServerInput,
// Enhanced diagnostic logging for claim URL changes
const previousApiServer = this.apiServer;
const newApiServer = this.apiServerInput;
logger.debug("[Server Switching] Claim URL change initiated:", {
did: this.activeDid,
previousServer: previousApiServer,
newServer: newApiServer,
changeType: "apiServer",
timestamp: new Date().toISOString(),
});
this.apiServer = this.apiServerInput;
await this.$saveSettings({
apiServer: newApiServer,
});
this.apiServer = newApiServer;
// Add this line to save to user-specific settings
await this.$saveUserSettings(this.activeDid, {
apiServer: this.apiServer,
});
// Log successful server switch
logger.debug("[Server Switching] Claim URL change completed:", {
did: this.activeDid,
previousServer: previousApiServer,
newServer: newApiServer,
changeType: "apiServer",
settingsSaved: true,
timestamp: new Date().toISOString(),
});
}
async onClickSavePartnerServer(): Promise<void> {
await this.$saveSettings({
partnerApiServer: this.partnerApiServerInput,
// Enhanced diagnostic logging for partner server changes
const previousPartnerServer = this.partnerApiServer;
const newPartnerServer = this.partnerApiServerInput;
logger.debug("[Server Switching] Partner server change initiated:", {
did: this.activeDid,
previousServer: previousPartnerServer,
newServer: newPartnerServer,
changeType: "partnerApiServer",
timestamp: new Date().toISOString(),
});
this.partnerApiServer = this.partnerApiServerInput;
await this.$saveSettings({
partnerApiServer: newPartnerServer,
});
this.partnerApiServer = newPartnerServer;
await this.$saveUserSettings(this.activeDid, {
partnerApiServer: this.partnerApiServer,
});
// Log successful partner server switch
logger.debug("[Server Switching] Partner server change completed:", {
did: this.activeDid,
previousServer: previousPartnerServer,
newServer: newPartnerServer,
changeType: "partnerApiServer",
settingsSaved: true,
timestamp: new Date().toISOString(),
});
}
async onClickSavePushServer(): Promise<void> {
@@ -1550,7 +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<void> {
try {
logger.debug("Attempting to delete profile for DID:", this.activeDid);
const success = await this.profileService.deleteProfile(this.activeDid);
const success = await this.deleteProfileFromServer(this.activeDid);
if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
this.userProfileDesc = "";
@@ -1688,7 +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<ProfileData | null> {
try {
const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Loading profile:", {
requestId,
did,
partnerApiServer: this.partnerApiServer,
});
// Get authentication headers
const headers = await getHeaders(did);
const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`;
logger.debug("[AccountViewView] Making API request:", {
requestId,
did,
fullUrl,
hasAuthHeader: !!headers.Authorization,
});
const response = await this.axios.get(fullUrl, { headers });
logger.debug("[AccountViewView] Profile loaded successfully:", {
requestId,
status: response.status,
hasData: !!response.data,
});
if (response.data && response.data.data) {
const profileData = response.data.data;
logger.debug("[AccountViewView] Parsing profile data:", {
requestId,
locLat: profileData.locLat,
locLon: profileData.locLon,
description: profileData.description,
});
const result = {
description: profileData.description || "",
latitude: profileData.locLat || 0,
longitude: profileData.locLon || 0,
includeLocation: !!(profileData.locLat && profileData.locLon),
};
logger.debug("[AccountViewView] Parsed profile result:", {
requestId,
result,
hasLocation: result.includeLocation,
});
return result;
} else {
logger.debug("[AccountViewView] No profile data found in response:", {
requestId,
hasData: !!response.data,
hasDataData: !!(response.data && response.data.data),
});
}
return null;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
logger.info(
"[Profile] No profile found - this is normal for new users",
{
did,
server: this.partnerApiServer,
status: 404,
timestamp: new Date().toISOString(),
},
);
return null;
}
if (axiosError.response?.status === 400) {
logger.warn("[Profile] Bad request - user may not be registered", {
did,
server: this.partnerApiServer,
status: 400,
timestamp: new Date().toISOString(),
});
return null;
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn("[Profile] Authentication/authorization issue", {
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
});
return null;
}
}
// Only log full errors for unexpected issues (5xx, network errors, etc.)
logger.error("[Profile] Unexpected error loading profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
throw new Error("Failed to load profile");
}
}
/**
* Save user profile to the partner API
*/
private async saveProfileToServer(
did: string,
profileData: ProfileData,
): Promise<boolean> {
try {
const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Saving profile:", {
requestId,
did,
profileData,
});
// Get authentication headers
const headers = await getHeaders(did);
// Prepare payload in the format expected by the partner API
const payload = {
description: profileData.description,
issuerDid: did,
...(profileData.includeLocation &&
profileData.latitude &&
profileData.longitude
? {
locLat: profileData.latitude,
locLon: profileData.longitude,
}
: {}),
};
logger.debug("[AccountViewView] Sending payload to server:", {
requestId,
payload,
hasLocation: profileData.includeLocation,
});
const response = await this.axios.post(
`${this.partnerApiServer}/api/partner/userProfile`,
payload,
{ headers },
);
logger.debug("[AccountViewView] Profile saved successfully:", {
requestId,
status: response.status,
});
return true;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 400) {
logger.warn("[Profile] Bad request saving profile", {
did,
server: this.partnerApiServer,
status: 400,
timestamp: new Date().toISOString(),
});
throw new Error("Invalid profile data");
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn(
"[Profile] Authentication/authorization issue saving profile",
{
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
},
);
throw new Error("Authentication required");
}
if (axiosError.response?.status === 409) {
logger.warn("[Profile] Profile conflict - may already exist", {
did,
server: this.partnerApiServer,
status: 409,
timestamp: new Date().toISOString(),
});
throw new Error("Profile already exists");
}
}
// Only log full errors for unexpected issues
logger.error("[Profile] Unexpected error saving profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
throw new Error("Failed to save profile");
}
}
/**
* Toggle profile location visibility
*/
private toggleProfileLocation(profileData: ProfileData): ProfileData {
const includeLocation = !profileData.includeLocation;
return {
...profileData,
latitude: includeLocation ? profileData.latitude : 0,
longitude: includeLocation ? profileData.longitude : 0,
includeLocation,
};
}
/**
* Clear profile location
*/
private clearProfileLocation(profileData: ProfileData): ProfileData {
return {
...profileData,
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Get default profile data
*/
private getDefaultProfile(): ProfileData {
return {
description: "",
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Delete user profile from the partner API
*/
private async deleteProfileFromServer(did: string): Promise<boolean> {
try {
const requestId = `profile_delete_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Deleting profile:", {
requestId,
did,
});
// Get authentication headers
const headers = await getHeaders(did);
const response = await this.axios.delete(
`${this.partnerApiServer}/api/partner/userProfile/${did}`,
{ headers },
);
logger.debug("[AccountViewView] Profile deleted successfully:", {
requestId,
status: response.status,
});
return true;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
logger.info(
"[Profile] Profile not found for deletion - may already be deleted",
{
did,
server: this.partnerApiServer,
status: 404,
timestamp: new Date().toISOString(),
},
);
return true; // Consider it successful if already deleted
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn(
"[Profile] Authentication/authorization issue deleting profile",
{
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
},
);
return false;
}
}
// Only log full errors for unexpected issues
logger.error("[Profile] Unexpected error deleting profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
return false;
}
}
}
</script>

View File

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

View File

@@ -71,22 +71,22 @@
contactFromDid?.seesMe && contactFromDid.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They can see you"
title="They can see your activity"
@click="confirmSetVisibility(contactFromDid, false)"
>
<font-awesome icon="eye" class="fa-fw" />
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They cannot see you"
title="They cannot see your activity"
@click="confirmSetVisibility(contactFromDid, true)"
>
<font-awesome icon="eye-slash" class="fa-fw" />
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<button
@@ -95,11 +95,11 @@
contactFromDid.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="I view their content"
title="You watch their activity"
@click="confirmViewContent(contactFromDid, false)"
>
<font-awesome icon="eye" class="fa-fw" />
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
@@ -107,11 +107,11 @@
contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="I do not view their content"
title="You do not watch their activity"
@click="confirmViewContent(contactFromDid, true)"
>
<font-awesome icon="eye-slash" class="fa-fw" />
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<button

View File

@@ -319,8 +319,9 @@
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
Beware that this will erase your existing contact & settings.
click Advanced, and follow the instructions to "Import Contacts".
(There is currently no way to import other settings, so you'll have to recreate
by hand your search area, filters, etc.)
</li>
</ul>
</div>
@@ -336,14 +337,18 @@
<h2 class="text-xl font-semibold">How do I erase my data from my device?</h2>
<p>
Before doing this, you may want to back up your data with the instructions above.
Before doing this, you should back up your data with the instructions above.
Note that this does not erase data sent to our servers (see contact info below)
</p>
<ul>
<li class="list-disc list-outside ml-4">
Mobile
<ul>
<li class="list-disc list-outside ml-4">
Home Screen: hold down on the icon, and choose to delete it
App Store app: hold down on the icon, then uninstall it
</li>
<li class="list-disc list-outside ml-4">
Home Screen PWA: hold down on the icon, and delete it
</li>
<li class="list-disc list-outside ml-4">
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
@@ -415,15 +420,6 @@
different page.
</p>
<h2 class="text-xl font-semibold">
Where do I get help with notifications?
</h2>
<p>
<router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link
>
</p>
<h2 class="text-xl font-semibold">
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
What can I do?
@@ -434,10 +430,13 @@
</p>
<ul class="list-disc list-outside ml-4">
<li>
Drag down on the screen to refresh it; do that multiple times, because
For mobile apps, make sure you're connected to the internet.
</li>
<li>
For PWAs, drag down on the screen to refresh it; do that multiple times, because
it sometimes takes multiple tries for the app to refresh to the latest version.
You can see the version information at the bottom of this page; the best
way to determine the latest version is to open this page in an incognito/private
way to determine the latest version is to open TimeSafari.app in an incognito/private
browser window and look at the version there.
</li>
<li>
@@ -468,9 +467,6 @@
</ul>
Then reload Time Safari.
</li>
<li>
Restart your device.
</li>
</ul>
<p>
If you still have problems, you can clear the cache (see "erase my data" above)
@@ -508,16 +504,12 @@
</p>
<ul class="list-disc list-outside ml-4">
<li>
If using notifications, a server stores push token data. That can be revoked at any time
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
</li>
<li>
If sending images, a server stores them, too. They can be removed by editing the claim
and deleting them.
If sending images, a server stores them. They can be removed by editing each claim
and deleting the image.
</li>
<li>
If sending other partner system data (eg. to Trustroots) a public key and message
data are stored on a server. Those can be removed via direct personal request.
data are stored on a server. Those can be removed via direct personal request (via contact below).
</li>
<li>
For all other claim data,

View File

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

View File

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

View File

@@ -88,9 +88,15 @@ import { Router } from "vue-router";
import { AppString, NotificationIface } from "../constants/app";
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
import { retrieveAccountCount, importFromMnemonic } from "../libs/util";
import {
retrieveAccountCount,
importFromMnemonic,
checkForDuplicateAccount,
DUPLICATE_ACCOUNT_ERROR,
} from "../libs/util";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications";
/**
* Import Account View Component
@@ -198,6 +204,19 @@ export default class ImportAccountView extends Vue {
}
try {
// Check for duplicate account before importing
const isDuplicate = await checkForDuplicateAccount(
this.mnemonic,
this.derivationPath,
);
if (isDuplicate) {
this.notify.warning(
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
TIMEOUTS.LONG,
);
return;
}
await importFromMnemonic(
this.mnemonic,
this.derivationPath,
@@ -223,9 +242,20 @@ export default class ImportAccountView extends Vue {
this.$router.push({ name: "account" });
} catch (error: unknown) {
this.$logError("Import failed: " + error);
// Check if this is a duplicate account error from saveNewIdentity
const errorMessage =
error instanceof Error ? error.message : String(error);
if (errorMessage.includes(DUPLICATE_ACCOUNT_ERROR)) {
this.notify.warning(
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
TIMEOUTS.LONG,
);
return;
}
this.notify.error(
(error instanceof Error ? error.message : String(error)) ||
"Failed to import account.",
errorMessage || "Failed to import account.",
TIMEOUTS.LONG,
);
}

View File

@@ -83,6 +83,7 @@ import {
retrieveAllAccountsMetadata,
retrieveFullyDecryptedAccount,
saveNewIdentity,
checkForDuplicateAccount,
} from "../libs/util";
import { logger } from "../utils/logger";
import { Account, AccountEncrypted } from "../db/tables/accounts";
@@ -171,6 +172,16 @@ export default class ImportAccountView extends Vue {
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
try {
// Check for duplicate account before creating
const isDuplicate = await checkForDuplicateAccount(newId.did);
if (isDuplicate) {
this.notify.warning(
"This derived account already exists. Please try a different derivation path.",
TIMEOUTS.LONG,
);
return;
}
await saveNewIdentity(newId, mne, newDerivPath);
// record that as the active DID

View File

@@ -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();
}

View File

@@ -226,7 +226,7 @@
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<!-- First, offers on the left-->
<div class="bg-slate-100 px-4 py-3 rounded-md">
<div v-if="activeDid && isRegistered">
<div v-if="activeDid && isRegistered" class="mb-4">
<div class="text-center">
<button
data-testId="offerButton"
@@ -243,13 +243,19 @@
:project-name="name"
/>
<h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3>
<h3 class="text-lg font-bold leading-tight mb-3">
Offered To This Idea
</h3>
<div v-if="offersToThis.length === 0">
(None yet. Wanna
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
>offer something... especially if others join you</span
>?)
<div v-if="offersToThis.length === 0" class="text-sm">
(None yet.<span v-if="activeDid && isRegistered">
Wanna
<span
class="cursor-pointer text-blue-500"
@click="openOfferDialog()"
>offer something&hellip; especially if others join you</span
>?</span
>)
</div>
<ul v-else class="text-sm border-t border-slate-300">
@@ -314,7 +320,7 @@
<!-- Now, gives TO this project in the middle -->
<!-- (similar to "FROM" gift display below) -->
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-to">
<div v-if="activeDid && isRegistered">
<div v-if="activeDid && isRegistered" class="mb-4">
<div class="text-center">
<button
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 text-sm leading-tight rounded-md"
@@ -325,7 +331,9 @@
</div>
</div>
<h3 class="text-lg font-bold mt-4">Given To This Project</h3>
<h3 class="text-lg font-bold leading-tight mb-3">
Given To This Project
</h3>
<div v-if="givesToThis.length === 0" class="text-sm">
(None yet. If you've seen something, say something by clicking a
@@ -476,7 +484,7 @@
<!-- Finally, gives FROM this project on the right -->
<!-- (similar to "TO" gift display above) -->
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-from">
<div v-if="activeDid && isRegistered">
<div v-if="activeDid && isRegistered" class="mb-4">
<div class="text-center">
<button
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 text-sm leading-tight rounded-md"
@@ -494,11 +502,13 @@
:is-from-project-view="true"
/>
<h3 class="text-lg font-bold mb-3 mt-4">
<h3 class="text-lg font-bold leading-tight mb-3">
Benefitted From This Project
</h3>
<div v-if="givesProvidedByThis.length === 0">(None yet.)</div>
<div v-if="givesProvidedByThis.length === 0" class="text-sm">
(None yet.)
</div>
<ul v-else class="text-sm border-t border-slate-300">
<li

View File

@@ -69,10 +69,17 @@
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
<span>
{{ claimCountWithHiddenText }}
so if you expected but do not see details from someone then ask them to
check that their activity is visible to you on their Contacts
<font-awesome icon="users" class="text-slate-500" />
page.
If you don't see expected info above for someone, ask them to check that
their activity is visible to you (
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
) on
<a
class="text-blue-500 underline cursor-pointer"
@click="copyContactsLinkToClipboard"
>
this page </a
>.
</span>
</div>
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
@@ -120,10 +127,11 @@ import { DateTime } from "luxon";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, APP_SERVER } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import {
GenericCredWrapper,
@@ -148,6 +156,7 @@ import {
NOTIFY_ALL_CONFIRMATIONS_ERROR,
NOTIFY_GIVE_SEND_ERROR,
NOTIFY_CLAIMS_SEND_ERROR,
NOTIFY_COPIED_TO_CLIPBOARD,
createConfirmationSuccessMessage,
createCombinedSuccessMessage,
} from "@/constants/notifications";
@@ -195,8 +204,8 @@ export default class QuickActionBvcEndView extends Vue {
get claimCountWithHiddenText() {
if (this.claimCountWithHidden === 0) return "";
return this.claimCountWithHidden === 1
? "There is 1 other claim with hidden details,"
: `There are ${this.claimCountWithHidden} other claims with hidden details,`;
? "There is 1 other claim with hidden details."
: `There are ${this.claimCountWithHidden} other claims with hidden details.`;
}
get claimCountByUserText() {
@@ -296,6 +305,25 @@ export default class QuickActionBvcEndView extends Vue {
(this.$router as Router).push(route);
}
copyContactsLinkToClipboard() {
const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`;
useClipboard()
.copy(deepLinkUrl)
.then(() => {
this.notify.success(
NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"),
TIMEOUTS.SHORT,
);
})
.catch((error) => {
logger.error("Failed to copy to clipboard:", error);
this.notify.error(
"Failed to copy link to clipboard. Please try again.",
TIMEOUTS.SHORT,
);
});
}
async record() {
try {
if (this.claimsToConfirmSelected.length > 0) {

View File

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

View File

@@ -91,13 +91,95 @@
name: 'shared-photo',
query: { fileName },
}"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
data-testId="fileUploadButton"
>
Go to Shared Page
</router-link>
</div>
<!-- URL Flow Testing Section -->
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">URL Flow Testing</h2>
<p class="text-sm text-gray-600 mb-3">
Test claim and partner server URL flow from initialization to change
propagation.
</p>
<div class="space-y-4">
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">Current URL State</h3>
<div class="space-y-2 text-sm">
<div>
<strong>API Server:</strong>
<span class="font-mono">{{ apiServer || "Not Set" }}</span>
</div>
<div>
<strong>Partner API Server:</strong>
<span class="font-mono">{{ partnerApiServer || "Not Set" }}</span>
</div>
<div>
<strong>Active DID:</strong>
<span class="font-mono">{{ activeDid || "Not Set" }}</span>
</div>
<div>
<strong>Platform:</strong>
<span class="font-mono">{{ getCurrentPlatform() }}</span>
</div>
</div>
</div>
<div class="space-y-2">
<button
:class="primaryButtonClasses"
:disabled="isUrlTestRunning"
@click="testUrlFlow()"
>
{{ isUrlTestRunning ? "Testing..." : "Test URL Flow" }}
</button>
<button :class="secondaryButtonClasses" @click="changeApiServer()">
Change API Server (Test Prod)
</button>
<button
:class="secondaryButtonClasses"
@click="changePartnerApiServer()"
>
Change Partner API Server (Test Prod)
</button>
<button :class="warningButtonClasses" @click="resetToDefaults()">
Reset to Defaults
</button>
<button :class="secondaryButtonClasses" @click="refreshSettings()">
Refresh Settings
</button>
<button
:class="secondaryButtonClasses"
@click="logEnvironmentState()"
>
Log Environment State
</button>
</div>
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">URL Flow Test Results</h3>
<div class="max-h-64 overflow-y-auto space-y-2">
<div
v-for="(result, index) in urlTestResults"
:key="index"
class="p-2 border border-gray-200 rounded text-xs font-mono bg-white"
>
{{ result }}
</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results.
@@ -326,6 +408,11 @@ export default class Help extends Vue {
showEntityGridTest = false;
showPlatformServiceTest = false;
// for URL flow testing
isUrlTestRunning = false;
urlTestResults: string[] = [];
partnerApiServer: string | undefined;
/**
* Computed properties for template streamlining
* Eliminates repeated classes and logic in template
@@ -534,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;
logger.info(
"[TestView] 🚀 Component mounting - starting URL flow tracking",
);
const account = await retrieveAccountMetadata(this.activeDid);
if (this.activeDid) {
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
} else {
alert("No account found for DID " + this.activeDid);
// Boot-time logging for initial configuration
logger.info("[TestView] 🌍 Boot-time configuration detected:", {
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
nodeEnv: process.env.NODE_ENV,
timestamp: new Date().toISOString(),
});
try {
// Track settings loading
logger.info("[TestView] 📥 Loading account settings...");
const settings = await this.$accountSettings();
logger.info("[TestView] 📊 Settings loaded:", {
activeDid: settings.activeDid,
apiServer: settings.apiServer,
partnerApiServer: settings.partnerApiServer,
isRegistered: settings.isRegistered,
firstName: settings.firstName,
});
// Update component state
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
this.userName = settings.firstName;
logger.info("[TestView] ✅ Component state updated:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
// Load account metadata
if (this.activeDid) {
logger.info(
"[TestView] 🔍 Loading account metadata for DID:",
this.activeDid,
);
const account = await retrieveAccountMetadata(this.activeDid);
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
logger.info("[TestView] ✅ Account metadata loaded:", {
did: account.did,
hasPasskey: !!account.passkeyCredIdHex,
passkeyId: account.passkeyCredIdHex,
});
} else {
logger.warn(
"[TestView] ⚠️ No account found for DID:",
this.activeDid,
);
alert("No account found for DID " + this.activeDid);
}
}
logger.info("[TestView] 🎯 Component initialization complete:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
hasPasskey: !!this.credIdHex,
platform: this.getCurrentPlatform(),
});
} catch (error) {
logger.error(
"[TestView] ❌ Error during component initialization:",
error,
);
this.$notify(
{
group: "error",
type: "error",
title: "Initialization Error",
text: `Failed to initialize component: ${error instanceof Error ? error.message : String(error)}`,
},
5000,
);
}
}
@@ -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";
}
}
}
</script>

View File

@@ -69,8 +69,7 @@
*/
import { test, expect } from '@playwright/test';
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
import { createContactName, generateNewEthrUser, importUser, importUserFromAccount } from './testUtils';
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
test('Check activity feed - check that server is running', async ({ page }) => {
@@ -185,35 +184,20 @@ test('Check invalid DID shows error and redirects', async ({ page }) => {
});
test('Check User 0 can register a random person', async ({ page }) => {
await importUser(page, '00');
const newDid = await generateAndRegisterEthrUser(page);
expect(newDid).toContain('did:ethr:');
const newDid = await generateNewEthrUser(page); // generate a new user
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
await page.getByPlaceholder('What was given').fill('Gave me access!');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible();
// now ensure that alert goes away
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
await expect(page.getByText('That gift was recorded.')).toBeHidden();
await importUserFromAccount(page, "00"); // switch to User Zero
// now delete the contact to test that pages still do reasonable things
await deleteContact(page, newDid);
// go the activity page for this new person
await page.goto('./did/' + encodeURIComponent(newDid));
// maybe replace by: const popupPromise = page.waitForEvent('popup');
let error;
try {
await page.waitForSelector('div[role="alert"]', { timeout: 2000 });
error = new Error('Error alert should not show.');
} catch (error) {
// success
} finally {
if (error) {
throw error;
}
}
// As User Zero, add the new user as a contact
await page.goto('./contacts');
const contactName = createContactName(newDid);
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`);
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible…
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
await page.locator('div[role="alert"] button:text-is("Yes")').click(); // Register new contact
await page.locator('div[role="alert"] button:text-is("No, Not Now")').click(); // Dismiss export data prompt
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
});

View File

@@ -0,0 +1,63 @@
import { test, expect } from '@playwright/test';
import { importUserFromAccount, getTestUserData } from './testUtils';
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from '../src/constants/notifications';
/**
* Test duplicate account import functionality
*
* This test verifies that:
* 1. A user can successfully import an account the first time
* 2. Attempting to import the same account again shows a warning message
* 3. The duplicate import is prevented
*/
test.describe('Duplicate Account Import', () => {
test('should prevent importing the same account twice', async ({ page }) => {
const userData = getTestUserData("00");
// First import - should succeed
await page.goto("./start");
await page.getByText("You have a seed").click();
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
// Verify first import was successful
await expect(page.getByRole("code")).toContainText(userData.did);
// Navigate back to start page for second import attempt
await page.goto("./start");
await page.getByText("You have a seed").click();
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
// Verify duplicate import shows warning message
// The warning can appear either from the pre-check or from the saveNewIdentity error handling
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).toBeVisible();
// Verify we're still on the import page (not redirected to account)
await expect(page.getByPlaceholder("Seed Phrase")).toBeVisible();
});
test('should allow importing different accounts', async ({ page }) => {
const userZeroData = getTestUserData("00");
const userOneData = getTestUserData("01");
// Import first user
await page.goto("./start");
await page.getByText("You have a seed").click();
await page.getByPlaceholder("Seed Phrase").fill(userZeroData.seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
// Verify first import was successful
await expect(page.getByRole("code")).toContainText(userZeroData.did);
// Navigate back to start page for second user import
await page.goto("./start");
await page.getByText("You have a seed").click();
await page.getByPlaceholder("Seed Phrase").fill(userOneData.seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
// Verify second import was successful (should not show duplicate warning)
await expect(page.getByRole("code")).toContainText(userOneData.did);
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).not.toBeVisible();
});
});

View File

@@ -8,6 +8,7 @@
* - Custom expiration date
* 2. The invitation appears in the list after creation
* 3. A new user can accept the invitation and become connected
* 4. The new user can create gift records from the front page
*
* Test Flow:
* 1. Imports User 0 (test account)
@@ -19,6 +20,8 @@
* 4. Creates a new user with Ethr DID
* 5. Accepts the invitation as the new user
* 6. Verifies the connection is established
* 7. Tests that the new user can create gift records from the front page
* 8. Verifies the gift appears in the home view
*
* Related Files:
* - Frontend invite handling: src/libs/endorserServer.ts
@@ -29,7 +32,7 @@
* @requires ./testUtils - For user management utilities
*/
import { test, expect } from '@playwright/test';
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
import { createGiftFromFrontPageForNewUser, deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
test('Check User 0 can invite someone', async ({ page }) => {
await importUser(page, '00');
@@ -58,4 +61,7 @@ test('Check User 0 can invite someone', async ({ page }) => {
await page.locator('button:has-text("Save")').click();
await expect(page.locator('button:has-text("Save")')).toBeHidden();
await expect(page.locator(`li:has-text("My pal User #0")`)).toBeVisible();
// Verify the new user can create a gift record from the front page
const giftTitle = await createGiftFromFrontPageForNewUser(page, `Gift from new user ${neighborNum}`);
});

View File

@@ -1,4 +1,5 @@
import { expect, Page } from "@playwright/test";
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
// Get test user data based on the ID.
// '01' -> user 111
@@ -109,7 +110,7 @@ export async function switchToUser(page: Page, did: string): Promise<void> {
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
}
function createContactName(did: string): string {
export function createContactName(did: string): string {
return "User " + did.slice(11, 14);
}
@@ -144,30 +145,6 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
return newDid;
}
// Generate a new random user and register them.
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
const newDid = await generateNewEthrUser(page);
await importUser(page, "000"); // switch to user 000
await page.goto("./contacts");
const contactName = createContactName(newDid);
await page
.getByPlaceholder("URL or DID, Name, Public Key")
.fill(`${newDid}, ${contactName}`);
await page.locator("button > svg.fa-plus").click();
// register them
await page.locator('div[role="alert"] button:text-is("Yes")').click();
// wait for it to disappear because the next steps may depend on alerts being gone
await expect(
page.locator('div[role="alert"] button:text-is("Yes")')
).toBeHidden();
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
return newDid;
}
// Function to generate a random string of specified length
export async function generateRandomString(length: number): Promise<string> {
return Math.random()
@@ -239,3 +216,44 @@ export function isResourceIntensiveTest(testPath: string): boolean {
testPath.includes("40-add-contact")
);
}
/**
* Create a gift record from the front page
* @param page - Playwright page object
* @param giftTitle - Optional custom title, defaults to "Gift " + random string
* @param amount - Optional amount, defaults to random 1-99
* @returns Promise resolving to the created gift title
*/
export async function createGiftFromFrontPageForNewUser(
page: Page,
giftTitle?: string,
amount?: number
): Promise<void> {
// Generate random values if not provided
const randomString = Math.random().toString(36).substring(2, 6);
const finalTitle = giftTitle || `Gift ${randomString}`;
const finalAmount = amount || Math.floor(Math.random() * 99) + 1;
// Navigate to home page and close onboarding
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
// Start gift creation flow
await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
// Fill gift details
await page.getByPlaceholder('What was given').fill(finalTitle);
await page.getByRole('spinbutton').fill(finalAmount.toString());
// Submit gift
await page.getByRole('button', { name: 'Sign & Send' }).click();
// Verify success
await expect(page.getByText('That gift was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
// Verify the gift appears in the home view
await page.goto('./');
await expect(page.locator('ul#listLatestActivity li').filter({ hasText: giftTitle })).toBeVisible();
}