Browse Source

Merge branch 'master' into notification-section

pull/150/head
Matthew Raymer 1 day ago
parent
commit
cf34952c1f
  1. 65
      .cursor/rules/adr_template.mdc
  2. 352
      .cursor/rules/app/architectural_decision_record.mdc
  3. 181
      .cursor/rules/app/timesafari.mdc
  4. 287
      .cursor/rules/architectural_decision_record.mdc
  5. 61
      .cursor/rules/asset_configuration.mdc
  6. 154
      .cursor/rules/base_context.mdc
  7. 48
      .cursor/rules/database/absurd-sql.mdc
  8. 8
      .cursor/rules/database/legacy_dexie.mdc
  9. 5
      .cursor/rules/development/development_guide.mdc
  10. 139
      .cursor/rules/development/type_safety_guide.mdc
  11. 0
      .cursor/rules/docs/documentation.mdc
  12. 0
      .cursor/rules/docs/markdown.mdc
  13. 6
      .cursor/rules/features/camera-implementation.mdc
  14. 117
      .cursor/rules/investigation_report_example.mdc
  15. 6
      .cursor/rules/legacy_dexie.mdc
  16. 222
      .cursor/rules/logging_standards.mdc
  17. 174
      .cursor/rules/research_diagnostic.mdc
  18. 225
      .cursor/rules/software_development.mdc
  19. 329
      .cursor/rules/time.mdc
  20. 316
      .cursor/rules/timesafari.mdc
  21. 122
      .cursor/rules/version_control.mdc
  22. 335
      .cursor/rules/workflow/version_control.mdc
  23. 2
      .env.test
  24. 2
      .eslintrc.js
  25. 142
      .github/workflows/asset-validation.yml
  26. 12
      .gitignore
  27. 1
      .node-version
  28. 1
      .nvmrc
  29. 63
      BUILDING.md
  30. 8
      CHANGELOG.md
  31. 72
      README.md
  32. 4
      android/app/build.gradle
  33. 2
      android/app/src/main/assets/capacitor.config.json
  34. 7
      android/app/src/main/res/values/colors.xml
  35. 2
      android/build.gradle
  36. 2
      assets/README.md
  37. 34
      capacitor-assets.config.json
  38. 116
      capacitor.config.ts
  39. 32
      config/assets/capacitor-assets.config.json
  40. 119
      config/assets/schema.json
  41. 214
      doc/asset-migration-plan.md
  42. 182
      doc/debug-hook-guide.md
  43. 2
      index.html
  44. 8
      ios/App/App.xcodeproj/project.pbxproj
  45. 23
      ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json
  46. BIN
      ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png
  47. BIN
      ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png
  48. BIN
      ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png
  49. 2017
      package-lock.json
  50. 16
      package.json
  51. 2
      playwright.config-local.ts
  52. 0
      resources/icon.png
  53. 0
      resources/splash.png
  54. 0
      resources/splash_dark.png
  55. 224
      scripts/assets-config.ts
  56. 239
      scripts/assets-validator.ts
  57. 40
      scripts/build-android.sh
  58. 8
      scripts/build-ios.sh
  59. 142
      scripts/check-android-resources.sh
  60. 110
      scripts/check-dependencies.sh
  61. 62
      scripts/clean-android.sh
  62. 96
      scripts/generate-android-icons.sh
  63. 22
      scripts/generate-icons.sh
  64. 103
      scripts/git-hooks/README.md
  65. 86
      scripts/git-hooks/debug-checker.config
  66. 252
      scripts/git-hooks/pre-commit
  67. 171
      scripts/install-debug-hook.sh
  68. 67
      scripts/purge-generated-assets.sh
  69. 117
      scripts/test-debug-hook.sh
  70. 103
      scripts/type-safety-check.sh
  71. 75
      src/assets/icons.json
  72. 6
      src/components/ActivityListItem.vue
  73. 2
      src/components/DataExportSection.vue
  74. 24
      src/components/FeedFilters.vue
  75. 5
      src/components/GiftedDialog.vue
  76. 90
      src/components/IconRenderer.vue
  77. 5
      src/components/UsageLimitsSection.vue
  78. 8
      src/constants/notifications.ts
  79. 84
      src/interfaces/common.ts
  80. 28
      src/interfaces/deepLinks.ts
  81. 10
      src/libs/fontawesome.ts
  82. 120
      src/main.capacitor.ts
  83. 26
      src/main.ts
  84. 72
      src/router/index.ts
  85. 101
      src/services/ProfileService.ts
  86. 99
      src/services/QRNavigationService.ts
  87. 334
      src/services/deepLinks.ts
  88. 139
      src/views/AccountViewView.vue
  89. 10
      src/views/ContactQRScanShowView.vue
  90. 110
      src/views/ContactsView.vue
  91. 2
      src/views/DIDView.vue
  92. 206
      src/views/DatabaseMigration.vue
  93. 4
      src/views/DeepLinkErrorView.vue
  94. 10
      src/views/DiscoverView.vue
  95. 22
      src/views/HelpView.vue
  96. 110
      src/views/HomeView.vue
  97. 4
      src/views/IdentitySwitcherView.vue
  98. 5
      src/views/ImportAccountView.vue
  99. 2
      src/views/OnboardMeetingListView.vue
  100. 2
      src/views/OnboardMeetingMembersView.vue

65
.cursor/rules/adr_template.mdc

@ -0,0 +1,65 @@
# ADR Template
## ADR-XXXX-YY-ZZ: [Short Title]
**Date:** YYYY-MM-DD
**Status:** [PROPOSED | ACCEPTED | REJECTED | DEPRECATED | SUPERSEDED]
**Deciders:** [List of decision makers]
**Technical Story:** [Link to issue/PR if applicable]
## Context
[Describe the forces at play, including technological, political, social, and
project local. These forces are probably in tension, and should be called out as
such. The language in this section is value-neutral. It is simply describing facts.]
## Decision
[Describe our response to these forces. We will use the past tense ("We will...").]
## Consequences
### Positive
- [List positive consequences]
### Negative
- [List negative consequences or trade-offs]
### Neutral
- [List neutral consequences or notes]
## Alternatives Considered
- **Alternative 1:** [Description] - [Why rejected]
- **Alternative 2:** [Description] - [Why rejected]
- **Alternative 3:** [Description] - [Why rejected]
## Implementation Notes
[Any specific implementation details, migration steps, or technical considerations]
## References
- [Link to relevant documentation]
- [Link to related ADRs]
- [Link to external resources]
## Related Decisions
- [List related ADRs or decisions]
---
## Usage Guidelines
1. **Copy this template** for new ADRs
2. **Number sequentially** (ADR-001, ADR-002, etc.)
3. **Use descriptive titles** that clearly indicate the decision
4. **Include all stakeholders** in the deciders list
5. **Link to related issues** and documentation
6. **Update status** as decisions evolve
7. **Store in** `doc/architecture-decisions/` directory
description:
globs:
alwaysApply: false
---

352
.cursor/rules/app/architectural_decision_record.mdc

@ -0,0 +1,352 @@
---
description: when you need to understand the system architecture or make changes that impact the system architecture
alwaysApply: false
---
# TimeSafari Cross-Platform Architecture Guide
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Architecture guidelines
## 1. Platform Support Matrix
| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) |
|---------|-----------|--------------------|-------------------|
| QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented |
| Deep Linking | URL Parameters | App URL Open Events | Not Implemented |
| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs |
| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented |
| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks |
## 2. Project Structure
### Core Directories
```
src/
├── components/ # Vue components
├── services/ # Platform services and business logic
├── views/ # Page components
├── router/ # Vue router configuration
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── lib/ # Core libraries
├── platforms/ # Platform-specific implementations
├── electron/ # Electron-specific code
├── constants/ # Application constants
├── db/ # Database related code
├── interfaces/ # TypeScript interfaces
└── assets/ # Static assets
```
### Entry Points
- `main.ts` → Base entry
- `main.common.ts` → Shared init
- `main.capacitor.ts` → Mobile entry
- `main.electron.ts` → Electron entry
- `main.web.ts` → Web entry
## 3. Service Architecture
### Service Organization
```tree
services/
├── QRScanner/
│ ├── WebInlineQRScanner.ts
│ └── interfaces.ts
├── platforms/
│ ├── WebPlatformService.ts
│ ├── CapacitorPlatformService.ts
│ └── ElectronPlatformService.ts
└── factory/
└── PlatformServiceFactory.ts
```
### Factory Pattern
Use a **singleton factory** to select platform services via
`process.env.VITE_PLATFORM`.
## 4. Feature Guidelines
### QR Code Scanning
- Define `QRScannerService` interface.
- Implement platform-specific classes (`WebInlineQRScanner`, Capacitor,
etc).
- Provide `addListener` and `onStream` hooks for composability.
### Deep Linking
- URL format: `timesafari://<route>[/<param>][?query=value]`
- Web: `router.beforeEach` → parse query
- Capacitor: `App.addListener("appUrlOpen", …)`
## 5. Build Process
- `vite.config.common.mts` → shared config
- Platform configs: `vite.config.web.mts`, `.capacitor.mts`,
`.electron.mts`
- Use `process.env.VITE_PLATFORM` for conditional loading.
```bash
npm run build:web
npm run build:capacitor
npm run build:electron
```
## 6. Testing Strategy
- **Unit tests** for services.
- **Playwright** for Web + Capacitor:
- `playwright.config-local.ts` includes web + Pixel 5.
- **Electron tests**: add `spectron` or Playwright-Electron.
- Mark tests with platform tags:
```ts
test.skip(!process.env.MOBILE_TEST, "Mobile-only test");
```
> 🔗 **Human Hook:** Before merging new tests, hold a short sync (≤15
> min) with QA to align on coverage and flaky test risks.
## 7. Error Handling
- Global Vue error handler → logs with component name.
- Platform-specific wrappers log API errors with platform prefix
(`[Capacitor API Error]`, etc).
- Use structured logging (not `console.log`).
## 8. Best Practices
- Keep platform code **isolated** in `platforms/`.
- Always define a **shared interface** first.
- Use feature detection, not platform detection, when possible.
- Dependency injection for services → improves testability.
- Maintain **Competence Hooks** in PRs (2–3 prompts for dev
discussion).
## 9. Dependency Management
- Key deps: `@capacitor/core`, `electron`, `vue`.
- Use conditional `import()` for platform-specific libs.
## 10. Security Considerations
- **Permissions**: Always check + request gracefully.
- **Storage**: Secure storage for sensitive data; encrypt when possible.
- **Audits**: Schedule quarterly security reviews.
## 11. ADR Process
- All major architecture choices → log in `doc/adr/`.
- Use ADR template with Context, Decision, Consequences, Status.
- Link related ADRs in PR descriptions.
> 🔗 **Human Hook:** When proposing a new ADR, schedule a 30-min
> design sync for discussion, not just async review.
## 12. Collaboration Hooks
- **QR features**: Sync with Security before merging → permissions &
privacy.
- **New platform builds**: Demo in team meeting → confirm UX
differences.
- **Critical ADRs**: Present in guild or architecture review.
## Self-Check
- [ ] Does this feature implement a shared interface?
- [ ] Are fallbacks + errors handled gracefully?
- [ ] Have relevant ADRs been updated/linked?
- [ ] Did I add competence hooks or prompts for the team?
- [ ] Was human interaction (sync/review/demo) scheduled?
---
**Status**: Active architecture guidelines
**Priority**: High
**Estimated Effort**: Ongoing reference
**Dependencies**: Vue 3, Capacitor, Electron, Vite
**Stakeholders**: Development team, Architecture team
- [ ] Are fallbacks + errors handled gracefully?
- [ ] Have relevant ADRs been updated/linked?
- [ ] Did I add competence hooks or prompts for the team?
- [ ] Was human interaction (sync/review/demo) scheduled?
# TimeSafari Cross-Platform Architecture Guide
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Architecture guidelines
## 1. Platform Support Matrix
| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) |
|---------|-----------|--------------------|-------------------|
| QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented |
| Deep Linking | URL Parameters | App URL Open Events | Not Implemented |
| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs |
| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented |
| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks |
## 2. Project Structure
### Core Directories
```
src/
├── components/ # Vue components
├── services/ # Platform services and business logic
├── views/ # Page components
├── router/ # Vue router configuration
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── lib/ # Core libraries
├── platforms/ # Platform-specific implementations
├── electron/ # Electron-specific code
├── constants/ # Application constants
├── db/ # Database related code
├── interfaces/ # TypeScript interfaces
└── assets/ # Static assets
```
### Entry Points
- `main.ts` → Base entry
- `main.common.ts` → Shared init
- `main.capacitor.ts` → Mobile entry
- `main.electron.ts` → Electron entry
- `main.web.ts` → Web entry
## 3. Service Architecture
### Service Organization
```tree
services/
├── QRScanner/
│ ├── WebInlineQRScanner.ts
│ └── interfaces.ts
├── platforms/
│ ├── WebPlatformService.ts
│ ├── CapacitorPlatformService.ts
│ └── ElectronPlatformService.ts
└── factory/
└── PlatformServiceFactory.ts
```
### Factory Pattern
Use a **singleton factory** to select platform services via
`process.env.VITE_PLATFORM`.
## 4. Feature Guidelines
### QR Code Scanning
- Define `QRScannerService` interface.
- Implement platform-specific classes (`WebInlineQRScanner`, Capacitor,
etc).
- Provide `addListener` and `onStream` hooks for composability.
### Deep Linking
- URL format: `timesafari://<route>[/<param>][?query=value]`
- Web: `router.beforeEach` → parse query
- Capacitor: `App.addListener("appUrlOpen", …)`
## 5. Build Process
- `vite.config.common.mts` → shared config
- Platform configs: `vite.config.web.mts`, `.capacitor.mts`,
`.electron.mts`
- Use `process.env.VITE_PLATFORM` for conditional loading.
```bash
npm run build:web
npm run build:capacitor
npm run build:electron
```
## 6. Testing Strategy
- **Unit tests** for services.
- **Playwright** for Web + Capacitor:
- `playwright.config-local.ts` includes web + Pixel 5.
- **Electron tests**: add `spectron` or Playwright-Electron.
- Mark tests with platform tags:
```ts
test.skip(!process.env.MOBILE_TEST, "Mobile-only test");
```
> 🔗 **Human Hook:** Before merging new tests, hold a short sync (≤15
> min) with QA to align on coverage and flaky test risks.
## 7. Error Handling
- Global Vue error handler → logs with component name.
- Platform-specific wrappers log API errors with platform prefix
(`[Capacitor API Error]`, etc).
- Use structured logging (not `console.log`).
## 8. Best Practices
- Keep platform code **isolated** in `platforms/`.
- Always define a **shared interface** first.
- Use feature detection, not platform detection, when possible.
- Dependency injection for services → improves testability.
- Maintain **Competence Hooks** in PRs (2–3 prompts for dev
discussion).
## 9. Dependency Management
- Key deps: `@capacitor/core`, `electron`, `vue`.
- Use conditional `import()` for platform-specific libs.
## 10. Security Considerations
- **Permissions**: Always check + request gracefully.
- **Storage**: Secure storage for sensitive data; encrypt when possible.
- **Audits**: Schedule quarterly security reviews.
## 11. ADR Process
- All major architecture choices → log in `doc/adr/`.
- Use ADR template with Context, Decision, Consequences, Status.
- Link related ADRs in PR descriptions.
> 🔗 **Human Hook:** When proposing a new ADR, schedule a 30-min
> design sync for discussion, not just async review.
## 12. Collaboration Hooks
- **QR features**: Sync with Security before merging → permissions &
privacy.
- **New platform builds**: Demo in team meeting → confirm UX
differences.
- **Critical ADRs**: Present in guild or architecture review.
## Self-Check
- [ ] Does this feature implement a shared interface?
- [ ] Are fallbacks + errors handled gracefully?
- [ ] Have relevant ADRs been updated/linked?
- [ ] Did I add competence hooks or prompts for the team?
- [ ] Was human interaction (sync/review/demo) scheduled?
---
**Status**: Active architecture guidelines
**Priority**: High
**Estimated Effort**: Ongoing reference
**Dependencies**: Vue 3, Capacitor, Electron, Vite
**Stakeholders**: Development team, Architecture team
- [ ] Are fallbacks + errors handled gracefully?
- [ ] Have relevant ADRs been updated/linked?
- [ ] Did I add competence hooks or prompts for the team?
- [ ] Was human interaction (sync/review/demo) scheduled?

181
.cursor/rules/app/timesafari.mdc

@ -0,0 +1,181 @@
# Time Safari Context
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Core application context
## Project Overview
Time Safari is an application designed to foster community building through
gifts, gratitude, and collaborative projects. The app makes it easy and
intuitive for users of any age and capability to recognize contributions,
build trust networks, and organize collective action. It is built on services
that preserve privacy and data sovereignty.
## Core Goals
1. **Connect**: Make it easy, rewarding, and non-threatening for people to
connect with others who have similar interests, and to initiate activities
together.
2. **Reveal**: Widely advertise the great support and rewards that are being
given and accepted freely, especially non-monetary ones, showing the impact
gifts make in people's lives.
## Technical Foundation
### Architecture
- **Privacy-preserving claims architecture** via endorser.ch
- **Decentralized Identifiers (DIDs)**: User identities based on
public/private key pairs stored on devices
- **Cryptographic Verification**: All claims and confirmations are
cryptographically signed
- **User-Controlled Visibility**: Users explicitly control who can see their
identifiers and data
- **Cross-Platform**: Web (PWA), Mobile (Capacitor), Desktop (Electron)
### Current Database State
- **Database**: SQLite via Absurd SQL (browser) and native SQLite
(mobile/desktop)
- **Legacy Support**: IndexedDB (Dexie) for backward compatibility
- **Status**: Modern database architecture fully implemented
### Core Technologies
- **Frontend**: Vue 3 + TypeScript + vue-facing-decorator
- **Styling**: TailwindCSS
- **Build**: Vite with platform-specific configs
- **Testing**: Playwright E2E, Jest unit tests
- **Database**: SQLite (Absurd SQL in browser), IndexedDB (legacy)
- **State**: Pinia stores
- **Platform Services**: Abstracted behind interfaces with factory pattern
## Development Principles
### Code Organization
- **Platform Services**: Abstract platform-specific code behind interfaces
- **Service Factory**: Use `PlatformServiceFactory` for platform selection
- **Type Safety**: Strict TypeScript, no `any` types, use type guards
- **Modern Architecture**: Use current platform service patterns
### Architecture Patterns
- **Dependency Injection**: Services injected via mixins and factory pattern
- **Interface Segregation**: Small, focused interfaces over large ones
- **Composition over Inheritance**: Prefer mixins and composition
- **Single Responsibility**: Each component/service has one clear purpose
### Testing Strategy
- **E2E**: Playwright for critical user journeys
- **Unit**: Jest with F.I.R.S.T. principles
- **Platform Coverage**: Web + Capacitor (Pixel 5) in CI
- **Quality Assurance**: Comprehensive testing and validation
## Current Development Focus
### Active Development
- **Feature Development**: Build new functionality using modern platform
services
- **Performance Optimization**: Improve app performance and user experience
- **Platform Enhancement**: Leverage platform-specific capabilities
- **Code Quality**: Maintain high standards and best practices
### Development Metrics
- **Code Quality**: High standards maintained across all platforms
- **Performance**: Optimized for all target devices
- **Testing**: Comprehensive coverage maintained
- **User Experience**: Focus on intuitive, accessible interfaces
## Platform-Specific Considerations
### Web (PWA)
- **QR Scanning**: WebInlineQRScanner
- **Deep Linking**: URL parameters
- **File System**: Limited browser APIs
- **Build**: `npm run build:web` (development build)
### Mobile (Capacitor)
- **QR Scanning**: @capacitor-mlkit/barcode-scanning
- **Deep Linking**: App URL open events
- **File System**: Capacitor Filesystem
- **Build**: `npm run build:capacitor`
### Desktop (Electron)
- **File System**: Node.js fs
- **Build**: `npm run build:electron`
- **Distribution**: AppImage, DEB, DMG packages
## Development Workflow
### Build Commands
```bash
# Web (development)
npm run build:web
# Mobile
npm run build:capacitor
npm run build:native
# Desktop
npm run build:electron
npm run build:electron:appimage
npm run build:electron:deb
npm run build:electron:dmg
```
### Testing Commands
```bash
# Web E2E
npm run test:web
# Mobile
npm run test:mobile
npm run test:android
npm run test:ios
# Type checking
npm run type-check
npm run lint-fix
```
## Key Constraints
1. **Privacy First**: User identifiers remain private except when explicitly
shared
2. **Platform Compatibility**: Features must work across all target platforms
3. **Performance**: Must remain performant on older/simpler devices
4. **Modern Architecture**: New features should use current platform services
5. **Offline Capability**: Key functionality should work offline when feasible
## Use Cases to Support
1. **Community Building**: Tools for finding others with shared interests
2. **Project Coordination**: Easy proposal and collaboration on projects
3. **Reputation Building**: Showcasing contributions and reliability
4. **Governance**: Facilitating decision-making and collective governance
## Resources
- **Testing**: `docs/migration-testing/`
- **Architecture**: `docs/architecture-decisions.md`
- **Build Context**: `docs/build-modernization-context.md`
---
## Status: Active application context
- **Priority**: Critical
- **Estimated Effort**: Ongoing reference
- **Dependencies**: Vue 3, TypeScript, SQLite, Capacitor, Electron
- **Stakeholders**: Development team, Product team

287
.cursor/rules/architectural_decision_record.mdc

@ -1,287 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# TimeSafari Cross-Platform Architecture Guide
## 1. Platform Support Matrix
| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) |
|---------|-----------|-------------------|-------------------|
| QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented |
| Deep Linking | URL Parameters | App URL Open Events | Not Implemented |
| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs |
| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented |
| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks |
## 2. Project Structure
### 2.1 Core Directories
```
src/
├── components/ # Vue components
├── services/ # Platform services and business logic
├── views/ # Page components
├── router/ # Vue router configuration
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── lib/ # Core libraries
├── platforms/ # Platform-specific implementations
├── electron/ # Electron-specific code
├── constants/ # Application constants
├── db/ # Database related code
├── interfaces/ # TypeScript interfaces and type definitions
└── assets/ # Static assets
```
### 2.2 Entry Points
```
src/
├── main.ts # Base entry
├── main.common.ts # Shared initialization
├── main.capacitor.ts # Mobile entry
├── main.electron.ts # Electron entry
└── main.web.ts # Web/PWA entry
```
### 2.3 Build Configurations
```
root/
├── vite.config.common.mts # Shared config
├── vite.config.capacitor.mts # Mobile build
├── vite.config.electron.mts # Electron build
└── vite.config.web.mts # Web/PWA build
```
## 3. Service Architecture
### 3.1 Service Organization
```
services/
├── QRScanner/ # QR code scanning service
│ ├── WebInlineQRScanner.ts
│ └── interfaces.ts
├── platforms/ # Platform-specific services
│ ├── WebPlatformService.ts
│ ├── CapacitorPlatformService.ts
│ └── ElectronPlatformService.ts
└── factory/ # Service factories
└── PlatformServiceFactory.ts
```
### 3.2 Service Factory Pattern
```typescript
// PlatformServiceFactory.ts
export class PlatformServiceFactory {
private static instance: PlatformService | null = null;
public static getInstance(): PlatformService {
if (!PlatformServiceFactory.instance) {
const platform = process.env.VITE_PLATFORM || "web";
PlatformServiceFactory.instance = createPlatformService(platform);
}
return PlatformServiceFactory.instance;
}
}
```
## 4. Feature Implementation Guidelines
### 4.1 QR Code Scanning
1. **Service Interface**
```typescript
interface QRScannerService {
checkPermissions(): Promise<boolean>;
requestPermissions(): Promise<boolean>;
isSupported(): Promise<boolean>;
startScan(): Promise<void>;
stopScan(): Promise<void>;
addListener(listener: ScanListener): void;
onStream(callback: (stream: MediaStream | null) => void): void;
cleanup(): Promise<void>;
}
```
2. **Platform-Specific Implementation**
```typescript
// WebInlineQRScanner.ts
export class WebInlineQRScanner implements QRScannerService {
private scanListener: ScanListener | null = null;
private isScanning = false;
private stream: MediaStream | null = null;
private events = new EventEmitter();
// Implementation of interface methods
}
```
### 4.2 Deep Linking
1. **URL Structure**
```typescript
// Format: timesafari://<route>[/<param>][?queryParam1=value1]
interface DeepLinkParams {
route: string;
params?: Record<string, string>;
query?: Record<string, string>;
}
```
2. **Platform Handlers**
```typescript
// Capacitor
App.addListener("appUrlOpen", handleDeepLink);
// Web
router.beforeEach((to, from, next) => {
handleWebDeepLink(to.query);
});
```
## 5. Build Process
### 5.1 Environment Configuration
```typescript
// vite.config.common.mts
export function createBuildConfig(mode: string) {
return {
define: {
'process.env.VITE_PLATFORM': JSON.stringify(mode),
// PWA is automatically enabled for web platforms via build configuration
__IS_MOBILE__: JSON.stringify(isCapacitor),
__USE_QR_READER__: JSON.stringify(!isCapacitor)
}
};
}
```
### 5.2 Platform-Specific Builds
```bash
# Build commands from package.json
"build:web": "vite build --config vite.config.web.mts",
"build:capacitor": "vite build --config vite.config.capacitor.mts",
"build:electron": "vite build --config vite.config.electron.mts"
```
## 6. Testing Strategy
### 6.1 Test Configuration
```typescript
// playwright.config-local.ts
const config: PlaywrightTestConfig = {
projects: [
{
name: 'web',
use: { browserName: 'chromium' }
},
{
name: 'mobile',
use: { ...devices['Pixel 5'] }
}
]
};
```
### 6.2 Platform-Specific Tests
```typescript
test('QR scanning works on mobile', async ({ page }) => {
test.skip(!process.env.MOBILE_TEST, 'Mobile-only test');
// Test implementation
});
```
## 7. Error Handling
### 7.1 Global Error Handler
```typescript
function setupGlobalErrorHandler(app: VueApp) {
app.config.errorHandler = (err, instance, info) => {
logger.error("[App Error]", {
error: err,
info,
component: instance?.$options.name
});
};
}
```
### 7.2 Platform-Specific Error Handling
```typescript
// API error handling for Capacitor
if (process.env.VITE_PLATFORM === 'capacitor') {
logger.error(`[Capacitor API Error] ${endpoint}:`, {
message: error.message,
status: error.response?.status
});
}
```
## 8. Best Practices
### 8.1 Code Organization
- Use platform-specific directories for unique implementations
- Share common code through service interfaces
- Implement feature detection before using platform capabilities
- Keep platform-specific code isolated in dedicated directories
- Use TypeScript interfaces for cross-platform compatibility
### 8.2 Platform Detection
```typescript
const platformService = PlatformServiceFactory.getInstance();
const capabilities = platformService.getCapabilities();
if (capabilities.hasCamera) {
// Implement camera features
}
```
### 8.3 Feature Implementation
1. Define platform-agnostic interface
2. Create platform-specific implementations
3. Use factory pattern for instantiation
4. Implement graceful fallbacks
5. Add comprehensive error handling
6. Use dependency injection for better testability
## 9. Dependency Management
### 9.1 Platform-Specific Dependencies
```json
{
"dependencies": {
"@capacitor/core": "^6.2.0",
"electron": "^33.2.1",
"vue": "^3.4.0"
}
}
```
### 9.2 Conditional Loading
```typescript
if (process.env.VITE_PLATFORM === 'capacitor') {
await import('@capacitor/core');
}
```
## 10. Security Considerations
### 10.1 Permission Handling
```typescript
async checkPermissions(): Promise<boolean> {
if (platformService.isCapacitor()) {
return await checkNativePermissions();
}
return await checkWebPermissions();
}
```
### 10.2 Data Storage
- Use secure storage mechanisms for sensitive data
- Implement proper encryption for stored data
- Follow platform-specific security guidelines
- Regular security audits and updates
This document should be updated as new features are added or platform-specific implementations change. Regular reviews ensure it remains current with the codebase.

61
.cursor/rules/asset_configuration.mdc

@ -0,0 +1,61 @@
---
description: when doing anything with capacitor assets
alwaysApply: false
---
# Asset Configuration Directive
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Asset management guidelines
*Scope: Assets Only (icons, splashes, image pipelines) — not overall build
orchestration*
## Intent
- Version **asset configuration files** (optionally dev-time generated).
- **Do not** version platform asset outputs (Android/iOS/Electron); generate
them **at build-time** with standard tools.
- Keep existing per-platform build scripts unchanged.
## Source of Truth
- **Preferred (Capacitor default):** `resources/` as the single master source.
- **Alternative:** `assets/` is acceptable **only** if `capacitor-assets` is
explicitly configured to read from it.
- **Never** maintain both `resources/` and `assets/` as parallel sources.
Migrate and delete the redundant folder.
## Config Files
- Live under: `config/assets/` (committed).
- Examples:
- `config/assets/capacitor-assets.config.json` (or the path the tool
expects)
- `config/assets/android.assets.json`
- `config/assets/ios.assets.json`
- `config/assets/common.assets.yaml` (optional shared layer)
- **Dev-time generation allowed** for these configs; **build-time
generation is forbidden**.
## Build-Time Behavior
- Build generates platform assets (not configs) using the standard chain:
```bash
npm run build:capacitor # web build via Vite (.mts)
npx cap sync
npx capacitor-assets generate # produces platform assets; not committed
# then platform-specific build steps
```
---
**Status**: Active asset management directive
**Priority**: Medium
**Estimated Effort**: Ongoing reference
**Dependencies**: capacitor-assets toolchain
**Stakeholders**: Development team, Build team
npx capacitor-assets generate # produces platform assets; not committed
# then platform-specific build steps

154
.cursor/rules/base_context.mdc

@ -0,0 +1,154 @@
---
alwaysApply: true
---
```json
{
"coaching_level": "standard",
"socratic_max_questions": 7,
"verbosity": "normal",
"timebox_minutes": null,
"format_enforcement": "strict"
}
```
# Base Context — Human Competence First
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Core interaction guidelines
## Purpose
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
than replacing it — outputs should be designed to **facilitate human discussion,
decision-making, and creativity**, not to atomize tasks into isolated, purely
machine-driven steps.
## Principles
1. Competence over convenience: finish the task *and* leave the human more
capable next time.
2. Mentorship, not lectures: be concise, concrete, and immediately applicable.
3. Transparency: show assumptions, limits, and uncertainty; cite when
non-obvious.
4. Optional scaffolding: include small, skimmable learning hooks that do not
bloat output.
5. Time respect: default to **lean output**; offer opt-in depth via toggles.
6. Psychological safety: encourage, never condescend; no medical/clinical
advice. No censorship!
7. Reusability: structure outputs so they can be saved, searched, reused, and
repurposed.
8. **Collaborative Bias**: Favor solutions that invite human review,
discussion, and iteration. When in doubt, ask "Who should this be shown
to?" or "Which human input would improve this?"
## Toggle Definitions
### coaching_level
Determines the depth of learning support: `light` (short hooks),
`standard` (balanced), `deep` (detailed).
### socratic_max_questions
The number of clarifying questions the model may ask before proceeding.
If >0, questions should be targeted, minimal, and followed by reasonable
assumptions if unanswered.
### verbosity
'terse' (just a sentence), `concise` (minimum commentary), `normal`
(balanced explanation), or other project-defined levels.
### timebox_minutes
*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.
Behavior when set:
1. **Prioritize Core Output** — Deliver the minimum viable solution or
result first.
2. **Limit Commentary** — Competence Hooks and Collaboration Hooks must be
shorter than normal.
3. **Signal Skipped Depth** — Omitted details should be listed under
*Deferred for depth*.
4. **Order by Value** — Start with blocking or high-value items, then
proceed to nice-to-haves if budget allows.
If `null`, there is no timebox — the model can produce full-depth
responses.
### format_enforcement
`strict` (reject outputs with format drift) or `relaxed` (minor deviations
acceptable).
## Modes (select or combine)
- **Doer**: produce the artifact fast, minimal commentary.
- **Mentor**: add short "why/how" notes + next-step pointers.
- **Socratic**: ask up to N targeted questions when requirements are
ambiguous.
- **Pair-Programmer/Pair-Writer**: explain tradeoffs as you implement.
- **Facilitator**: structure output to be reviewable, commentable, and
ready for group discussion.
Default: Doer + short Mentor notes.
## Competence & Collaboration Levers (keep lightweight)
- "Why this works" (≤3 bullets)
- "Common pitfalls" (≤3 bullets)
- "Next skill unlock" (1 tiny action or reading)
- "Teach-back" (1 sentence prompt the human can answer to self-check)
- "Discussion prompts" (≤2 short questions for peers/stakeholders)
## Output Contract (apply to every deliverable)
- Clear **Objective** (1 line)
- **Result** (artifact/code/answer)
- **Use/Run** (how to apply/test)
- **Competence Hooks** (the 4 learning levers above, kept terse)
- **Collaboration Hooks** (discussion prompts or group review steps)
- **Assumptions & Limits**
- **References** (if used; links or titles)
## Do-Not
- No filler, hedging, or moralizing.
- No medical/mental-health advice; keep "healthy habits" to general work
practices.
- No invented facts; mark uncertainty plainly.
- No censorship.
- Avoid outputs that bypass human review when such review is valuable.
## Related Rulesets
- **software_development.mdc**: For software-specific development practices
- **research_diagnostic.mdc**: For investigation and research workflows
## Self-Check (model, before responding)
- [ ] 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.
- [ ] Toggles respected; verbosity remains concise.
- [ ] Uncertainties/assumptions surfaced.
- [ ] No disallowed content.
---
**Status**: Active core guidelines
**Priority**: Critical
**Estimated Effort**: Ongoing reference
**Dependencies**: None (base ruleset)
**Stakeholders**: All AI interactions
- [ ] Uncertainties/assumptions surfaced.
- [ ] No disallowed content.

48
.cursor/rules/absurd-sql.mdc → .cursor/rules/database/absurd-sql.mdc

@ -1,14 +1,23 @@
--- ---
description: globs: **/db/databaseUtil.ts, **/interfaces/absurd-sql.d.ts, **/src/registerSQLWorker.js, **/
globs: services/AbsurdSqlDatabaseService.ts
alwaysApply: true alwaysApply: false
--- ---
# Absurd SQL - Cursor Development Guide # Absurd SQL - Cursor Development Guide
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Database development guidelines
## Project Overview ## Project Overview
Absurd SQL is a backend implementation for sql.js that enables persistent SQLite databases in the browser by using IndexedDB as a block storage system. This guide provides rules and best practices for developing with this project in Cursor.
Absurd SQL is a backend implementation for sql.js that enables persistent
SQLite databases in the browser by using IndexedDB as a block storage system.
This guide provides rules and best practices for developing with this project
in Cursor.
## Project Structure ## Project Structure
``` ```
absurd-sql/ absurd-sql/
├── src/ # Source code ├── src/ # Source code
@ -21,36 +30,45 @@ absurd-sql/
## Development Rules ## Development Rules
### 1. Worker Thread Requirements ### 1. Worker Thread Requirements
- All SQL operations MUST be performed in a worker thread - All SQL operations MUST be performed in a worker thread
- Main thread should only handle worker initialization and communication - Main thread should only handle worker initialization and communication
- Never block the main thread with database operations - Never block the main thread with database operations
### 2. Code Organization ### 2. Code Organization
- Keep worker code in separate files (e.g., `*.worker.js`) - Keep worker code in separate files (e.g., `*.worker.js`)
- Use ES modules for imports/exports - Use ES modules for imports/exports
- Follow the project's existing module structure - Follow the project's existing module structure
### 3. Required Headers ### 3. Required Headers
When developing locally or deploying, ensure these headers are set: When developing locally or deploying, ensure these headers are set:
``` ```
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Embedder-Policy: require-corp
``` ```
### 4. Browser Compatibility ### 4. Browser Compatibility
- Primary target: Modern browsers with SharedArrayBuffer support - Primary target: Modern browsers with SharedArrayBuffer support
- Fallback mode: Safari (with limitations) - Fallback mode: Safari (with limitations)
- Always test in both modes - Always test in both modes
### 5. Database Configuration ### 5. Database Configuration
Recommended database settings: Recommended database settings:
```sql ```sql
PRAGMA journal_mode=MEMORY; PRAGMA journal_mode=MEMORY;
PRAGMA page_size=8192; -- Optional, but recommended PRAGMA page_size=8192; -- Optional, but recommended
``` ```
### 6. Development Workflow ### 6. Development Workflow
1. Install dependencies: 1. Install dependencies:
```bash ```bash
yarn add @jlongster/sql.js absurd-sql yarn add @jlongster/sql.js absurd-sql
``` ```
@ -61,17 +79,20 @@ PRAGMA page_size=8192; -- Optional, but recommended
- `yarn serve` - Start development server - `yarn serve` - Start development server
### 7. Testing Guidelines ### 7. Testing Guidelines
- Write tests for both SharedArrayBuffer and fallback modes - Write tests for both SharedArrayBuffer and fallback modes
- Use Jest for testing - Use Jest for testing
- Include performance benchmarks for critical operations - Include performance benchmarks for critical operations
### 8. Performance Considerations ### 8. Performance Considerations
- Use bulk operations when possible - Use bulk operations when possible
- Monitor read/write performance - Monitor read/write performance
- Consider using transactions for multiple operations - Consider using transactions for multiple operations
- Avoid unnecessary database connections - Avoid unnecessary database connections
### 9. Error Handling ### 9. Error Handling
- Implement proper error handling for: - Implement proper error handling for:
- Worker initialization failures - Worker initialization failures
- Database connection issues - Database connection issues
@ -79,18 +100,21 @@ PRAGMA page_size=8192; -- Optional, but recommended
- Storage quota exceeded scenarios - Storage quota exceeded scenarios
### 10. Security Best Practices ### 10. Security Best Practices
- Never expose database operations directly to the client - Never expose database operations directly to the client
- Validate all SQL queries - Validate all SQL queries
- Implement proper access controls - Implement proper access controls
- Handle sensitive data appropriately - Handle sensitive data appropriately
### 11. Code Style ### 11. Code Style
- Follow ESLint configuration - Follow ESLint configuration
- Use async/await for asynchronous operations - Use async/await for asynchronous operations
- Document complex database operations - Document complex database operations
- Include comments for non-obvious optimizations - Include comments for non-obvious optimizations
### 12. Debugging ### 12. Debugging
- Use `jest-debug` for debugging tests - Use `jest-debug` for debugging tests
- Monitor IndexedDB usage in browser dev tools - Monitor IndexedDB usage in browser dev tools
- Check worker communication in console - Check worker communication in console
@ -99,6 +123,7 @@ PRAGMA page_size=8192; -- Optional, but recommended
## Common Patterns ## Common Patterns
### Worker Initialization ### Worker Initialization
```javascript ```javascript
// Main thread // Main thread
import { initBackend } from 'absurd-sql/dist/indexeddb-main-thread'; import { initBackend } from 'absurd-sql/dist/indexeddb-main-thread';
@ -110,6 +135,7 @@ function init() {
``` ```
### Database Setup ### Database Setup
```javascript ```javascript
// Worker thread // Worker thread
import initSqlJs from '@jlongster/sql.js'; import initSqlJs from '@jlongster/sql.js';
@ -131,6 +157,7 @@ async function setupDatabase() {
## Troubleshooting ## Troubleshooting
### Common Issues ### Common Issues
1. SharedArrayBuffer not available 1. SharedArrayBuffer not available
- Check COOP/COEP headers - Check COOP/COEP headers
- Verify browser support - Verify browser support
@ -147,6 +174,19 @@ async function setupDatabase() {
- Verify transaction usage - Verify transaction usage
## Resources ## Resources
- [Project Demo](https://priceless-keller-d097e5.netlify.app/)
- [Example Project](https://github.com/jlongster/absurd-example-project)
- [Blog Post](https://jlongster.com/future-sql-web)
- [SQL.js Documentation](https://github.com/sql-js/sql.js/)
---
**Status**: Active database development guidelines
**Priority**: High
**Estimated Effort**: Ongoing reference
**Dependencies**: Absurd SQL, SQL.js, IndexedDB
**Stakeholders**: Development team, Database team
- [Project Demo](https://priceless-keller-d097e5.netlify.app/) - [Project Demo](https://priceless-keller-d097e5.netlify.app/)
- [Example Project](https://github.com/jlongster/absurd-example-project) - [Example Project](https://github.com/jlongster/absurd-example-project)
- [Blog Post](https://jlongster.com/future-sql-web) - [Blog Post](https://jlongster.com/future-sql-web)

8
.cursor/rules/database/legacy_dexie.mdc

@ -0,0 +1,8 @@
---
globs: **/databaseUtil.ts,**/AccountViewView.vue,**/ContactsView.vue,**/DatabaseMigration.vue,**/NewIdentifierView.vue
alwaysApply: false
---
# What to do with Dexie
All references in the codebase to Dexie apply only to migration from IndexedDb to
Sqlite and will be deprecated in future versions.

5
.cursor/rules/development_guide.mdc → .cursor/rules/development/development_guide.mdc

@ -1,7 +1,6 @@
--- ---
description: rules used while developing globs: **/src/**/*
globs: alwaysApply: false
alwaysApply: true
--- ---
✅ use system date command to timestamp all interactions with accurate date and time ✅ use system date command to timestamp all interactions with accurate date and time
✅ python script files must always have a blank line at their end ✅ python script files must always have a blank line at their end

139
.cursor/rules/development/type_safety_guide.mdc

@ -0,0 +1,139 @@
---
description: when dealing with types and Typesript
alwaysApply: false
---
```json
{
"coaching_level": "light",
"socratic_max_questions": 7,
"verbosity": "concise",
"timebox_minutes": null,
"format_enforcement": "strict"
}
```
# TypeScript Type Safety Guidelines
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Type safety enforcement
## Overview
Practical rules to keep TypeScript strict and predictable. Minimize exceptions.
## Core Rules
1. **No `any`**
- Use explicit types. If unknown, use `unknown` and **narrow** via guards.
2. **Error handling uses guards**
- Reuse guards from `src/interfaces/**` (e.g., `isDatabaseError`,
`isApiError`).
- Catch with `unknown`; never cast to `any`.
3. **Dynamic property access is type‑safe**
- Use `keyof` + `in` checks:
```ts
obj[k as keyof typeof obj]
```
- Avoid `(obj as any)[k]`.
## Type Safety Enforcement
### Core Type Safety Rules
- **No `any` Types**: Use explicit types or `unknown` with proper type guards
- **Error Handling Uses Guards**: Implement and reuse type guards from `src/interfaces/**`
- **Dynamic Property Access**: Use `keyof` + `in` checks for type-safe property access
### Type Guard Patterns
- **API Errors**: Use `isApiError(error)` guards for API error handling
- **Database Errors**: Use `isDatabaseError(error)` guards for database operations
- **Axios Errors**: Implement `isAxiosError(error)` guards for HTTP error handling
### Implementation Guidelines
- **Avoid Type Assertions**: Replace `as any` with proper type guards and interfaces
- **Narrow Types Properly**: Use type guards to narrow `unknown` types safely
- **Document Type Decisions**: Explain complex type structures and their purpose
## Minimal Special Cases (document in PR when used)
- **Vue refs / instances**: Use `ComponentPublicInstance` or specific
component types for dynamic refs.
- **3rd‑party libs without types**: Narrow immediately to a **known
interface**; do not leave `any` hanging.
## Patterns (short)
### Database errors
```ts
try { await this.$addContact(contact); }
catch (e: unknown) {
if (isDatabaseError(e) && e.message.includes("Key already exists")) {
/* handle duplicate */
}
}
```
### API errors
```ts
try { await apiCall(); }
catch (e: unknown) {
if (isApiError(e)) {
const msg = e.response?.data?.error?.message;
}
}
```
### Dynamic keys
```ts
const keys = Object.keys(newSettings).filter(
k => k in newSettings && newSettings[k as keyof typeof newSettings] !== undefined
);
```
## Checklists
**Before commit**
- [ ] No `any` (except documented, justified cases)
- [ ] Errors handled via guards
- [ ] Dynamic access uses `keyof`/`in`
- [ ] Imports point to correct interfaces/types
**Code review**
- [ ] Hunt hidden `as any`
- [ ] Guard‑based error paths verified
- [ ] Dynamic ops are type‑safe
- [ ] Prefer existing types over re‑inventing
## Tools
- `npm run lint-fix` — lint & auto‑fix
- `npm run type-check` — strict type compilation (CI + pre‑release)
- IDE: enable strict TS, ESLint/TS ESLint, Volar (Vue 3)
## References
- TS Handbook — https://www.typescriptlang.org/docs/
- TS‑ESLint — https://typescript-eslint.io/rules/
- Vue 3 + TS — https://vuejs.org/guide/typescript/
---
**Status**: Active type safety guidelines
**Priority**: High
**Estimated Effort**: Ongoing reference
**Dependencies**: TypeScript, ESLint, Vue 3
**Stakeholders**: Development team
- TS Handbook — https://www.typescriptlang.org/docs/
- TS‑ESLint — https://typescript-eslint.io/rules/
- Vue 3 + TS — https://vuejs.org/guide/typescript/

0
.cursor/rules/documentation.mdc → .cursor/rules/docs/documentation.mdc

0
.cursor/rules/markdown.mdc → .cursor/rules/docs/markdown.mdc

6
.cursor/rules/camera-implementation.mdc → .cursor/rules/features/camera-implementation.mdc

@ -1,13 +1,13 @@
--- ---
description: description: when dealing with cameras in the application
globs:
alwaysApply: false alwaysApply: false
--- ---
# Camera Implementation Documentation # Camera Implementation Documentation
## Overview ## Overview
This document describes how camera functionality is implemented across the TimeSafari application. The application uses cameras for two main purposes: This document describes how camera functionality is implemented across the
TimeSafari application. The application uses cameras for two main purposes:
1. QR Code scanning 1. QR Code scanning
2. Photo capture 2. Photo capture

117
.cursor/rules/investigation_report_example.mdc

@ -0,0 +1,117 @@
# Investigation Report Example
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Investigation methodology example
## Investigation — Registration Dialog Test Flakiness
## Objective
Identify root cause of flaky tests related to registration dialogs in contact
import scenarios.
## System Map
- User action → ContactInputForm → ContactsView.addContact() →
handleRegistrationPrompt()
- setTimeout(1000ms) → Modal dialog → User response → Registration API call
- Test execution → Wait for dialog → Assert dialog content → Click response
button
## Findings (Evidence)
- **1-second timeout causes flakiness** — evidence:
`src/views/ContactsView.vue:971-1000`; setTimeout(..., 1000) in
handleRegistrationPrompt()
- **Import flow bypasses dialogs** — evidence:
`src/views/ContactImportView.vue:500-520`; importContacts() calls
$insertContact() directly, no handleRegistrationPrompt()
- **Dialog only appears in direct add flow** — evidence:
`src/views/ContactsView.vue:774-800`; addContact() calls
handleRegistrationPrompt() after database insert
## Hypotheses & Failure Modes
- H1: 1-second timeout makes dialog appearance unpredictable; would fail when
tests run faster than 1000ms
- H2: Test environment timing differs from development; watch for CI vs local
test differences
## Corrections
- Updated: "Multiple dialogs interfere with imports" → "Import flow never
triggers dialogs - they only appear in direct contact addition"
- Updated: "Complex batch registration needed" → "Simple timeout removal and
test mode flag sufficient"
## Diagnostics (Next Checks)
- [ ] Repro on CI environment vs local
- [ ] Measure actual dialog appearance timing
- [ ] Test with setTimeout removed
- [ ] Verify import flow doesn't call handleRegistrationPrompt
## Risks & Scope
- Impacted: Contact addition tests, registration workflow tests; Data: None;
Users: Test suite reliability
## Decision / Next Steps
- Owner: Development Team; By: 2025-01-28
- Action: Remove 1-second timeout + add test mode flag; Exit criteria: Tests
pass consistently
## References
- `src/views/ContactsView.vue:971-1000`
- `src/views/ContactImportView.vue:500-520`
- `src/views/ContactsView.vue:774-800`
## Competence Hooks
- Why this works: Code path tracing revealed separate execution flows,
evidence disproved initial assumptions
- Common pitfalls: Assuming related functionality without tracing execution
paths, over-engineering solutions to imaginary problems
- Next skill: Learn to trace code execution before proposing architectural
changes
- Teach-back: "What evidence shows that contact imports bypass registration
dialogs?"
## Key Learning Points
### Evidence-First Approach
This investigation demonstrates the importance of:
1. **Tracing actual code execution** rather than making assumptions
2. **Citing specific evidence** with file:line references
3. **Validating problem scope** before proposing solutions
4. **Considering simpler alternatives** before complex architectural changes
### Code Path Tracing Value
By tracing the execution paths, we discovered:
- Import flow and direct add flow are completely separate
- The "multiple dialog interference" problem didn't exist
- A simple timeout removal would solve the actual issue
### Prevention of Over-Engineering
The investigation prevented:
- Unnecessary database schema changes
- Complex batch registration systems
- Migration scripts for non-existent problems
- Architectural changes based on assumptions
---
**Status**: Active investigation methodology
**Priority**: High
**Estimated Effort**: Ongoing reference
**Dependencies**: software_development.mdc
**Stakeholders**: Development team, QA team

6
.cursor/rules/legacy_dexie.mdc

@ -1,6 +0,0 @@
---
description:
globs:
alwaysApply: true
---
All references in the codebase to Dexie apply only to migration from IndexedDb to Sqlite and will be deprecated in future versions.

222
.cursor/rules/logging_standards.mdc

@ -0,0 +1,222 @@
# Agent Contract — TimeSafari Logging (Unified, MANDATORY)
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Mandatory logging standards
## Overview
This document defines unified logging standards for the TimeSafari project,
ensuring consistent, rest-parameter logging style using the project logger.
No `console.*` methods are allowed in production code.
## Scope and Goals
**Scope**: Applies to all diffs and generated code in this workspace unless
explicitly exempted below.
**Goal**: One consistent, rest-parameter logging style using the project
logger; no `console.*` in production code.
## Non‑Negotiables (DO THIS)
- You **MUST** use the project logger; **DO NOT** use any `console.*`
methods.
- Import exactly as:
- `import { logger } from '@/utils/logger'`
- If `@` alias is unavailable, compute the correct relative path (do not
fail).
- Call signatures use **rest parameters**: `logger.info(message, ...args)`
- Prefer primitives/IDs and small objects in `...args`; **never build a
throwaway object** just to "wrap context".
- Production defaults: Web = `warn+`, Electron = `error`, Dev/Capacitor =
`info+` (override via `VITE_LOG_LEVEL`).
- **Database persistence**: `info|warn|error` are persisted; `debug` is not.
Use `logger.toDb(msg, level?)` for DB-only.
## Available Logger API (Authoritative)
- `logger.debug(message, ...args)` — verbose internals, timings, input/output
shapes
- `logger.log(message, ...args)` — synonym of `info` for general info
- `logger.info(message, ...args)` — lifecycle, state changes, success paths
- `logger.warn(message, ...args)` — recoverable issues, retries, degraded mode
- `logger.error(message, ...args)` — failures, thrown exceptions, aborts
- `logger.toDb(message, level?)` — DB-only entry (default level = `info`)
- `logger.toConsoleAndDb(message, isError)` — console + DB (use sparingly)
- `logger.withContext(componentName)` — returns a scoped logger
## Level Guidelines (Use These Heuristics)
### DEBUG
Use for method entry/exit, computed values, filters, loops, retries, and
external call payload sizes.
```typescript
logger.debug('[HomeView] reloadFeedOnChange() called');
logger.debug('[HomeView] Current filter settings',
settings.filterFeedByVisible,
settings.filterFeedByNearby,
settings.searchBoxes?.length ?? 0);
logger.debug('[FeedFilters] Toggling nearby filter',
this.isNearby, this.settingChanged, this.activeDid);
```
**Avoid**: Vague messages (`'Processing data'`).
### INFO
Use for user-visible lifecycle and completed operations.
```typescript
logger.info('[StartView] Component mounted', process.env.VITE_PLATFORM);
logger.info('[StartView] User selected new seed generation');
logger.info('[SearchAreaView] Search box stored',
searchBox.name, searchBox.bbox);
logger.info('[ContactQRScanShowView] Contact registration OK',
contact.did);
```
**Avoid**: Diagnostic details that belong in `debug`.
### WARN
Use for recoverable issues, fallbacks, unexpected-but-handled conditions.
```typescript
logger.warn('[ContactQRScanShowView] Invalid scan result – no value',
resultType);
logger.warn('[ContactQRScanShowView] Invalid QR format – no JWT in URL');
logger.warn('[ContactQRScanShowView] JWT missing "own" field');
```
**Avoid**: Hard failures (those are `error`).
### ERROR
Use for unrecoverable failures, data integrity issues, and thrown
exceptions.
```typescript
logger.error('[HomeView Settings] initializeIdentity() failed', err);
logger.error('[StartView] Failed to load initialization data', error);
logger.error('[ContactQRScanShowView] Error processing contact QR',
error, rawValue);
```
**Avoid**: Expected user cancels (use `info`/`debug`).
## Context Hygiene (Consistent, Minimal, Helpful)
- **Component context**: Prefer scoped logger.
```typescript
const log = logger.withContext('UserService');
log.info('User created', userId);
log.error('Failed to create user', error);
```
If not using `withContext`, prefix message with `[ComponentName]`.
- **Emojis**: Optional and minimal for visual scanning. Recommended set:
- Start/finish: 🚀 / ✅
- Retry/loop: 🔄
- External call: 📡
- Data/metrics: 📊
- Inspection: 🔍
- **Sensitive data**: Never log secrets (tokens, keys, passwords) or
payloads >10KB. Prefer IDs over objects; redact/hash when needed.
## Migration — Auto‑Rewrites (Apply Every Time)
- Exact transforms:
- `console.debug(...)` → `logger.debug(...)`
- `console.log(...)` → `logger.log(...)` (or `logger.info(...)` when
clearly stateful)
- `console.info(...)` → `logger.info(...)`
- `console.warn(...)` → `logger.warn(...)`
- `console.error(...)` → `logger.error(...)`
- Multi-arg handling:
- First arg becomes `message` (stringify safely if non-string).
- Remaining args map 1:1 to `...args`:
`console.info(msg, a, b)` → `logger.info(String(msg), a, b)`
- Sole `Error`:
- `console.error(err)` → `logger.error(err.message, err)`
- **Object-wrapping cleanup**: Replace `{{ userId, meta }}` wrappers with
separate args:
`logger.info('User signed in', userId, meta)`
## DB Logging Rules
- `debug` **never** persists automatically.
- `info|warn|error` persist automatically.
- For DB-only events (no console), call `logger.toDb('Message',
'info'|'warn'|'error')`.
## Exceptions (Tightly Scoped)
Allowed paths (still prefer logger):
- `**/*.test.*`, `**/*.spec.*`
- `scripts/dev/**`, `scripts/migrate/**`
To intentionally keep `console.*`, add a pragma on the previous line:
```typescript
// cursor:allow-console reason="short justification"
console.log('temporary output');
```
Without the pragma, rewrite to `logger.*`.
## CI & Diff Enforcement
- Do not introduce `console.*` anywhere outside allowed, pragma'd spots.
- If an import is missing, insert it and resolve alias/relative path
correctly.
- Enforce rest-parameter call shape in reviews; replace object-wrapped
context.
- Ensure environment log level rules remain intact (`VITE_LOG_LEVEL`
respected).
## Quick Before/After
### **Before**
```typescript
console.log('User signed in', user.id, meta);
console.error('Failed to update profile', err);
console.info('Filter toggled', this.hasVisibleDid);
```
### **After**
```typescript
import { logger } from '@/utils/logger';
logger.info('User signed in', user.id, meta);
logger.error('Failed to update profile', err);
logger.debug('[FeedFilters] Filter toggled', this.hasVisibleDid);
```
## Checklist (for every PR)
- [ ] No `console.*` (or properly pragma'd in the allowed locations)
- [ ] Correct import path for `logger`
- [ ] Rest-parameter call shape (`message, ...args`)
- [ ] Right level chosen (debug/info/warn/error)
- [ ] No secrets / oversized payloads / throwaway context objects
- [ ] Component context provided (scoped logger or `[Component]` prefix)
---
**Status**: Active and enforced
**Priority**: Critical
**Estimated Effort**: Ongoing reference
**Dependencies**: TimeSafari logger utility
**Stakeholders**: Development team, Code review team

174
.cursor/rules/research_diagnostic.mdc

@ -0,0 +1,174 @@
---
description: Use this workflow when doing **pre-implementation research, defect investigations with uncertain repros, or clarifying system architecture and behaviors**.
alwaysApply: false
---
```json
{
"coaching_level": "light",
"socratic_max_questions": 2,
"verbosity": "concise",
"timebox_minutes": null,
"format_enforcement": "strict"
}
```
# Research & Diagnostic Workflow (R&D)
## Purpose
Provide a **repeatable, evidence-first** workflow to investigate features and
defects **before coding**. Outputs are concise reports, hypotheses, and next
steps—**not** code changes.
## When to Use
- Pre-implementation research for new features
- Defect investigations (repros uncertain, user-specific failures)
- Architecture/behavior clarifications (e.g., auth flows, merges, migrations)
---
## Enhanced with Software Development Ruleset
When investigating software issues, also apply:
- **Code Path Tracing**: Required for technical investigations
- **Evidence Validation**: Ensure claims are code-backed
- **Solution Complexity Assessment**: Justify architectural changes
---
## Output Contract (strict)
1) **Objective** — 1–2 lines
2) **System Map (if helpful)** — short diagram or bullet flow (≤8 bullets)
3) **Findings (Evidence-linked)** — bullets; each with file/function refs
4) **Hypotheses & Failure Modes** — short list, each testable
5) **Corrections** — explicit deltas from earlier assumptions (if any)
6) **Diagnostics** — what to check next (logs, DB, env, repro steps)
7) **Risks & Scope** — what could break; affected components
8) **Decision/Next Steps** — what we'll do, who's involved, by when
9) **References** — code paths, ADRs, docs
10) **Competence & Collaboration Hooks** — brief, skimmable
> Keep total length lean. Prefer links and bullets over prose.
---
## Quickstart Template
Copy/paste and fill:
```md
# Investigation — <short title>
## Objective
<one or two lines>
## System Map
- <module> → <function> → <downstream>
- <data path> → <db table> → <api>
## Findings (Evidence)
- <claim> — evidence: `src/path/file.ts:function` (lines X–Y); log snippet/trace id
- <claim> — evidence: `...`
## Hypotheses & Failure Modes
- H1: <hypothesis>; would fail when <condition>
- H2: <hypothesis>; watch for <signal>
## Corrections
- Updated: <old statement> → <new statement with evidence>
## Diagnostics (Next Checks)
- [ ] Repro on <platform/version>
- [ ] Inspect <table/store> for <record>
- [ ] Capture <log/trace>
## Risks & Scope
- Impacted: <areas/components>; Data: <tables/keys>; Users: <segments>
## Decision / Next Steps
- Owner: <name>; By: <date> (YYYY-MM-DD)
- Action: <spike/bugfix/ADR>; Exit criteria: <binary checks>
## References
- `src/...`
- ADR: `docs/adr/xxxx-yy-zz-something.md`
- Design: `docs/...`
## Competence Hooks
- Why this works: <≤3 bullets>
- Common pitfalls: <≤3 bullets>
- Next skill: <≤1 item>
- Teach-back: "<one question>"
```
---
## Evidence Quality Bar
- **Cite the source** (file:func, line range if possible).
- **Prefer primary evidence** (code, logs) over inference.
- **Disambiguate platform** (Web/Capacitor/Electron) and **state** (migration, auth).
- **Note uncertainty** explicitly.
---
## Code Path Tracing (Required for Software Investigations)
Before proposing solutions, trace the actual execution path:
- [ ] **Entry Points**: Identify where the flow begins (user action, API call, etc.)
- [ ] **Component Flow**: Map which components/methods are involved
- [ ] **Data Path**: Track how data moves through the system
- [ ] **Exit Points**: Confirm where the flow ends and what results
- [ ] **Evidence Collection**: Gather specific code citations for each step
---
## Collaboration Hooks
- **Syncs:** 10–15m with QA/Security/Platform owners for high-risk areas.
- **ADR:** Record major decisions; link here.
- **Review:** Share repro + diagnostics checklist in PR/issue.
---
## Integration with Other Rulesets
### With software_development.mdc
- **Enhanced Evidence Validation**: Use code path tracing for technical investigations
- **Architecture Assessment**: Apply complexity justification to proposed solutions
- **Impact Analysis**: Assess effects on existing systems before recommendations
### With base_context.mdc
- **Competence Building**: Focus on technical investigation skills
- **Collaboration**: Structure outputs for team review and discussion
---
## Self-Check (model, before responding)
- [ ] Output matches the **Output Contract** sections.
- [ ] Each claim has **evidence** or **uncertainty** is flagged.
- [ ] Hypotheses are testable; diagnostics are actionable.
- [ ] Competence + collaboration hooks present (≤120 words total).
- [ ] Respect toggles; keep it concise.
- [ ] **Code path traced** (for software investigations).
- [ ] **Evidence validated** against actual code execution.
---
## Optional Globs (examples)
> Uncomment `globs` in the header if you want auto-attach behavior.
- `src/platforms/**`, `src/services/**` — attach during service/feature investigations
- `docs/adr/**` — attach when editing ADRs
## Referenced Files
- Consider including templates as context: `@adr_template.mdc`, `@investigation_report_example.mdc`

225
.cursor/rules/software_development.mdc

@ -0,0 +1,225 @@
# Software Development Ruleset
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Core development guidelines
## Purpose
Specialized guidelines for software development tasks including code review,
debugging, architecture decisions, and testing.
## Core Principles
### 1. Evidence-First Development
- **Code Citations Required**: Always cite specific file:line references when
making claims
- **Execution Path Tracing**: Trace actual code execution before proposing
architectural changes
- **Assumption Validation**: Flag assumptions as "assumed" vs "evidence-based"
### 2. Code Review Standards
- **Trace Before Proposing**: Always trace execution paths before suggesting
changes
- **Evidence Over Inference**: Prefer code citations over logical deductions
- **Scope Validation**: Confirm the actual scope of problems before proposing
solutions
### 3. Problem-Solution Validation
- **Problem Scope**: Does the solution address the actual problem?
- **Evidence Alignment**: Does the solution match the evidence?
- **Complexity Justification**: Is added complexity justified by real needs?
- **Alternative Analysis**: What simpler solutions were considered?
### 4. Dependency Management & Environment Validation
- **Pre-build Validation**: Always validate critical dependencies before executing
build scripts
- **Environment Consistency**: Ensure team members have identical development
environments
- **Dependency Verification**: Check that required packages are installed and
accessible
- **Path Resolution**: Use `npx` for local dependencies to avoid PATH issues
## Required Workflows
### Before Proposing Changes
- [ ] **Code Path Tracing**: Map execution flow from entry to exit
- [ ] **Evidence Collection**: Gather specific code citations and logs
- [ ] **Assumption Surfacing**: Identify what's proven vs. inferred
- [ ] **Scope Validation**: Confirm the actual extent of the problem
- [ ] **Dependency Validation**: Verify all required dependencies are available
and accessible
### During Solution Design
- [ ] **Evidence Alignment**: Ensure solution addresses proven problems
- [ ] **Complexity Assessment**: Justify any added complexity
- [ ] **Alternative Evaluation**: Consider simpler approaches first
- [ ] **Impact Analysis**: Assess effects on existing systems
- [ ] **Environment Impact**: Assess how changes affect team member setups
## Software-Specific Competence Hooks
### Evidence Validation
- **"What code path proves this claim?"**
- **"How does data actually flow through the system?"**
- **"What am I assuming vs. what can I prove?"**
### Code Tracing
- **"What's the execution path from user action to system response?"**
- **"Which components actually interact in this scenario?"**
- **"Where does the data originate and where does it end up?"**
### Architecture Decisions
- **"What evidence shows this change is necessary?"**
- **"What simpler solution could achieve the same goal?"**
- **"How does this change affect the existing system architecture?"**
### Dependency & Environment Management
- **"What dependencies does this feature require and are they properly
declared?"**
- **"How will this change affect team member development environments?"**
- **"What validation can we add to catch dependency issues early?"**
## Dependency Management Best Practices
### Pre-build Validation
- **Check Critical Dependencies**: Validate essential tools before executing build
scripts
- **Use npx for Local Dependencies**: Prefer `npx tsx` over direct `tsx` to
avoid PATH issues
- **Environment Consistency**: Ensure all team members have identical dependency
versions
### Common Pitfalls
- **Missing npm install**: Team members cloning without running `npm install`
- **PATH Issues**: Direct command execution vs. npm script execution differences
- **Version Mismatches**: Different Node.js/npm versions across team members
### Validation Strategies
- **Dependency Check Scripts**: Implement pre-build validation for critical
dependencies
- **Environment Requirements**: Document and enforce minimum Node.js/npm versions
- **Onboarding Checklist**: Standardize team member setup procedures
### Error Messages and Guidance
- **Specific Error Context**: Provide clear guidance when dependency issues occur
- **Actionable Solutions**: Direct users to specific commands (`npm install`,
`npm run check:dependencies`)
- **Environment Diagnostics**: Implement comprehensive environment validation
tools
### Build Script Enhancements
- **Early Validation**: Check dependencies before starting build processes
- **Graceful Degradation**: Continue builds when possible but warn about issues
- **Helpful Tips**: Remind users about dependency management best practices
## Integration with Other Rulesets
### With base_context.mdc
- Inherits generic competence principles
- Adds software-specific evidence requirements
- Maintains collaboration and learning focus
### With research_diagnostic.mdc
- Enhances investigation with code path tracing
- Adds evidence validation to diagnostic workflow
- Strengthens problem identification accuracy
## Usage Guidelines
### When to Use This Ruleset
- Code reviews and architectural decisions
- Bug investigation and debugging
- Performance optimization
- Feature implementation planning
- Testing strategy development
### When to Combine with Others
- **base_context + software_development**: General development tasks
- **research_diagnostic + software_development**: Technical investigations
- **All three**: Complex architectural decisions or major refactoring
## Self-Check (model, before responding)
- [ ] Code path traced and documented
- [ ] Evidence cited with specific file:line references
- [ ] Assumptions clearly flagged as proven vs. inferred
- [ ] Solution complexity justified by evidence
- [ ] Simpler alternatives considered and documented
- [ ] Impact on existing systems assessed
- [ ] Dependencies validated and accessible
- [ ] Environment impact assessed for team members
- [ ] Pre-build validation implemented where appropriate
## Additional Core Principles
### 4. Dependency Management & Environment Validation
- **Pre-build Validation**: Always validate critical dependencies before executing build scripts
- **Environment Consistency**: Ensure team members have identical development environments
- **Dependency Verification**: Check that required packages are installed and accessible
- **Path Resolution**: Use `npx` for local dependencies to avoid PATH issues
## Additional Required Workflows
### Dependency Validation (Before Proposing Changes)
- [ ] **Dependency Validation**: Verify all required dependencies are available and accessible
### Environment Impact Assessment (During Solution Design)
- [ ] **Environment Impact**: Assess how changes affect team member setups
## Additional Competence Hooks
### Dependency & Environment Management
- **"What dependencies does this feature require and are they properly declared?"**
- **"How will this change affect team member development environments?"**
- **"What validation can we add to catch dependency issues early?"**
## Dependency Management Best Practices
### Pre-build Validation
- **Check Critical Dependencies**: Validate essential tools before executing build scripts
- **Use npx for Local Dependencies**: Prefer `npx tsx` over direct `tsx` to avoid PATH issues
- **Environment Consistency**: Ensure all team members have identical dependency versions
### Common Pitfalls
- **Missing npm install**: Team members cloning without running `npm install`
- **PATH Issues**: Direct command execution vs. npm script execution differences
- **Version Mismatches**: Different Node.js/npm versions across team members
### Validation Strategies
- **Dependency Check Scripts**: Implement pre-build validation for critical dependencies
- **Environment Requirements**: Document and enforce minimum Node.js/npm versions
- **Onboarding Checklist**: Standardize team member setup procedures
### Error Messages and Guidance
- **Specific Error Context**: Provide clear guidance when dependency issues occur
- **Actionable Solutions**: Direct users to specific commands (`npm install`, `npm run check:dependencies`)
- **Environment Diagnostics**: Implement comprehensive environment validation tools
### Build Script Enhancements
- **Early Validation**: Check dependencies before starting build processes
- **Graceful Degradation**: Continue builds when possible but warn about issues
- **Helpful Tips**: Remind users about dependency management best practices
- **Narrow Types Properly**: Use type guards to narrow `unknown` types safely
- **Document Type Decisions**: Explain complex type structures and their purpose

329
.cursor/rules/time.mdc

@ -0,0 +1,329 @@
---
alwaysApply: true
---
# Time Handling in Development Workflow
**Author**: Matthew Raymer
**Date**: 2025-08-17
**Status**: 🎯 **ACTIVE** - Production Ready
## Overview
This guide establishes **how time should be referenced and used** across the
development workflow. It is not tied to any one project, but applies to **all
feature development, issue investigations, ADRs, and documentation**.
## General Principles
- **Explicit over relative**: Always prefer absolute dates (`2025-08-17`) over
relative references like "last week."
- **ISO 8601 Standard**: Use `YYYY-MM-DD` format for all date references in
docs, issues, ADRs, and commits.
- **Time zones**: Default to **UTC** unless explicitly tied to user-facing
behavior.
- **Precision**: Only specify as much precision as needed (date vs. datetime vs.
timestamp).
- **Consistency**: Align time references across ADRs, commits, and investigation
reports.
## In Documentation & ADRs
- Record decision dates using **absolute ISO dates**.
- For ongoing timelines, state start and end explicitly (e.g., `2025-08-01` →
`2025-08-17`).
- Avoid ambiguous terms like *recently*, *last month*, or *soon*.
- For time-based experiments (e.g., A/B tests), always include:
- Start date
- Expected duration
- Review date checkpoint
## In Code & Commits
- Use **UTC timestamps** in logs, DB migrations, and serialized formats.
- In commits, link changes to **date-bound ADRs or investigation docs**.
- For migrations, include both **applied date** and **intended version window**.
- Use constants for known fixed dates; avoid hardcoding arbitrary strings.
## In Investigations & Research
- Capture **when** an issue occurred (absolute time or version tag).
- When describing failures: note whether they are **time-sensitive** (e.g., after
migrations, cache expirations).
- Record diagnostic timelines in ISO format (not relative).
- For performance regressions, annotate both **baseline timeframe** and
**measurement timeframe**.
## Collaboration Hooks
- During reviews, verify **time references are clear, absolute, and
standardized**.
- In syncs, reframe relative terms ("this week") into shared absolute
references.
- Tag ADRs with both **date created** and **review by** checkpoints.
## Self-Check Before Submitting
- [ ] Did I check the time using the **developer's actual system time and
timezone**?
- [ ] Am I using absolute ISO dates?
- [ ] Is UTC assumed unless specified otherwise?
- [ ] Did I avoid ambiguous relative terms?
- [ ] If duration matters, did I specify both start and end?
- [ ] For future work, did I include a review/revisit date?
## Real-Time Context in Developer Interactions
- The model must always resolve **"current time"** using the **developer's
actual system time and timezone**.
- When generating timestamps (e.g., in investigation logs, ADRs, or examples),
the model should:
- Use the **developer's current local time** by default.
- Indicate the timezone explicitly (e.g., `2025-08-17T10:32-05:00`).
- Optionally provide UTC alongside if context requires cross-team clarity.
- When interpreting relative terms like *now*, *today*, *last week*:
- Resolve them against the **developer's current time**.
- Convert them into **absolute ISO-8601 values** in the output.
## LLM Time Checking Instructions
**CRITICAL**: The LLM must actively query the system for current time rather
than assuming or inventing times.
### How to Check Current Time
#### 1. **Query System Time (Required)**
- **Always start** by querying the current system time using available tools
- **Never assume** what the current time is
- **Never use** placeholder values like "current time" or "now"
#### 2. **Available Time Query Methods**
- **System Clock**: Use `date` command or equivalent system time function
- **Programming Language**: Use language-specific time functions (e.g.,
`Date.now()`, `datetime.now()`)
- **Environment Variables**: Check for time-related environment variables
- **API Calls**: Use time service APIs if available
#### 3. **Required Time Information**
When querying time, always obtain:
- **Current Date**: YYYY-MM-DD format
- **Current Time**: HH:MM:SS format (24-hour)
- **Timezone**: Current system timezone or UTC offset
- **UTC Equivalent**: Convert local time to UTC for cross-team clarity
#### 4. **Time Query Examples**
```bash
# Example: Query system time
$ date
# Expected output: Mon Aug 17 10:32:45 EDT 2025
# Example: Query UTC time
$ date -u
# Expected output: Mon Aug 17 14:32:45 UTC 2025
```
```python
# Example: Python time query
import datetime
current_time = datetime.datetime.now()
utc_time = datetime.datetime.utcnow()
print(f"Local: {current_time}")
print(f"UTC: {utc_time}")
```
```javascript
// Example: JavaScript time query
const now = new Date();
const utc = new Date().toISOString();
console.log(`Local: ${now}`);
console.log(`UTC: ${utc}`);
```
#### 5. **LLM Time Checking Workflow**
1. **Query**: Actively query system for current time
2. **Validate**: Confirm time data is reasonable and current
3. **Format**: Convert to ISO 8601 format
4. **Context**: Provide both local and UTC times when helpful
5. **Document**: Show the source of time information
#### 6. **Error Handling for Time Queries**
- **If time query fails**: Ask user for current time or use "unknown time"
with explanation
- **If timezone unclear**: Default to UTC and ask for clarification
- **If time seems wrong**: Verify with user before proceeding
- **Always log**: Record when and how time was obtained
#### 7. **Time Query Verification**
Before using queried time, verify:
- [ ] Time is recent (within last few minutes)
- [ ] Timezone information is available
- [ ] UTC conversion is accurate
- [ ] Format follows ISO 8601 standard
## Model Behavior Rules
- **Never invent a "fake now"**: All "current time" references must come from
the real system clock available at runtime.
- **Check developer time zone**: If ambiguous, ask for clarification (e.g.,
"Should I use UTC or your local timezone?").
- **Format for clarity**:
- Local time: `YYYY-MM-DDTHH:mm±hh:mm`
- UTC equivalent (if needed): `YYYY-MM-DDTHH:mmZ`
## Examples
### Good
- "Feature flag rollout started on `2025-08-01` and will be reviewed on
`2025-08-21`."
- "Migration applied on `2025-07-15T14:00Z`."
- "Issue reproduced on `2025-08-17T09:00-05:00 (local)` /
`2025-08-17T14:00Z (UTC)`."
### Bad
- "Feature flag rolled out last week."
- "Migration applied recently."
- "Now is August, so we assume this was last month."
### More Examples
#### Issue Reports
- ✅ **Good**: "User reported login failure at `2025-08-17T14:30:00Z`. Issue
persisted until `2025-08-17T15:45:00Z`."
- ❌ **Bad**: "User reported login failure earlier today. Issue lasted for a
while."
#### Release Planning
- ✅ **Good**: "Feature X scheduled for release on `2025-08-25`. Testing
window: `2025-08-20` to `2025-08-24`."
- ❌ **Bad**: "Feature X will be released next week after testing."
#### Performance Monitoring
- ✅ **Good**: "Baseline performance measured on `2025-08-10T09:00:00Z`.
Regression detected on `2025-08-15T14:00:00Z`."
- ❌ **Bad**: "Performance was good last week but got worse this week."
## Technical Implementation Notes
### UTC Storage Principle
- **Store all timestamps in UTC** in databases, logs, and serialized formats
- **Convert to local time only for user display**
- **Use ISO 8601 format** for all storage: `YYYY-MM-DDTHH:mm:ss.sssZ`
### Common Implementation Patterns
#### Database Storage
```sql
-- ✅ Good: Store in UTC
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
-- ❌ Bad: Store in local time
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```
#### API Responses
```json
// ✅ Good: Include both UTC and local time
{
"eventTime": "2025-08-17T14:00:00Z",
"localTime": "2025-08-17T10:00:00-04:00",
"timezone": "America/New_York"
}
// ❌ Bad: Only local time
{
"eventTime": "2025-08-17T10:00:00-04:00"
}
```
#### Logging
```python
# ✅ Good: Log in UTC with timezone info
logger.info(f"User action at {datetime.utcnow().isoformat()}Z (UTC)")
# ❌ Bad: Log in local time
logger.info(f"User action at {datetime.now()}")
```
### Timezone Handling Best Practices
#### 1. Always Store Timezone Information
- Include IANA timezone identifier (e.g., `America/New_York`)
- Store UTC offset at time of creation
- Handle daylight saving time transitions automatically
#### 2. User Display Considerations
- Convert UTC to user's preferred timezone
- Show timezone abbreviation when helpful
- Use relative time for recent events ("2 hours ago")
#### 3. Edge Case Handling
- **Daylight Saving Time**: Use timezone-aware libraries
- **Leap Seconds**: Handle gracefully (rare but important)
- **Invalid Times**: Validate before processing
### Common Mistakes to Avoid
#### 1. Timezone Confusion
- ❌ **Don't**: Assume server timezone is user timezone
- ✅ **Do**: Always convert UTC to user's local time for display
#### 2. Format Inconsistency
- ❌ **Don't**: Mix different time formats in the same system
- ✅ **Do**: Standardize on ISO 8601 for all storage
#### 3. Relative Time References
- ❌ **Don't**: Use relative terms in persistent storage
- ✅ **Do**: Convert relative terms to absolute timestamps immediately
## References
- [ISO 8601 Date and Time Standard](https://en.wikipedia.org/wiki/ISO_8601)
- [IANA Timezone Database](https://www.iana.org/time-zones)
- [ADR Template](./adr_template.md)
- [Research & Diagnostic Workflow](./research_diagnostic.mdc)
---
**Rule of Thumb**: Every time reference in development artifacts should be
**clear in 6 months without context**, and aligned to the **developer's actual
current time**.
**Technical Rule of Thumb**: **Store in UTC, display in local time, always
include timezone context.**
---
**Status**: Active
**Version**: 1.0
**Maintainer**: Matthew Raymer
**Next Review**: 2025-09-17

316
.cursor/rules/timesafari.mdc

@ -1,316 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# Time Safari Context
## Project Overview
Time Safari is an application designed to foster community building through gifts,
gratitude, and collaborative projects. The app should make it extremely easy and
intuitive for users of any age and capability to recognize contributions, build
trust networks, and organize collective action. It is built on services that
preserve privacy and data sovereignty.
The ultimate goals of Time Safari are two-fold:
1. **Connect** Make it easy, rewarding, and non-threatening for people to
connect with others who have similar interests, and to initiate activities
together. This helps people accomplish and learn from other individuals in
less-structured environments; moreover, it helps them discover who they want
to continue to support and with whom they want to maintain relationships.
2. **Reveal** Widely advertise the great support and rewards that are being
given and accepted freely, especially non-monetary ones. Using visuals and text,
display the kind of impact that gifts are making in the lives of others. Also
show useful and engaging reports of project statistics and personal accomplishments.
## Core Approaches
Time Safari should help everyday users build meaningful connections and organize
collective efforts by:
1. **Recognizing Contributions**: Creating permanent, verifiable records of gifts
and contributions people give to each other and their communities.
2. **Facilitating Collaboration**: Making it ridiculously easy for people to ask
for or propose help on projects and interests that matter to them.
3. **Building Trust Networks**: Enabling users to maintain their network and activity
visibility. Developing reputation through verified contributions and references,
which can be selectively shown to others outside the network.
4. **Preserving Privacy**: Ensuring personal identifiers are only shared with
explicitly authorized contacts, allowing private individuals including children
to participate safely.
5. **Engaging Content**: Displaying people's records in compelling stories, and
highlighting those projects that are lifting people's lives long-term, both in
physical support and in emotional-spiritual-creative thriving.
## Technical Foundation
This application is built on a privacy-preserving claims architecture (via
endorser.ch) with these key characteristics:
- **Decentralized Identifiers (DIDs)**: User identities are based on public/private
key pairs stored on their devices
- **Cryptographic Verification**: All claims and confirmations are
cryptographically signed
- **User-Controlled Visibility**: Users explicitly control who can see their
identifiers and data
- **Merkle-Chained Claims**: Claims are cryptographically chained for verification
and integrity
- **Native and Web App**: Works on Capacitor (iOS, Android), Desktop (Electron
and CEFPython), and web browsers
## User Journey
The typical progression of usage follows these stages:
1. **Gratitude & Recognition**: Users begin by expressing and recording gratitude
for gifts received, building a foundation of acknowledgment.
2. **Project Proposals**: Users propose projects and ideas, reaching out to connect
with others who share similar interests.
3. **Action Triggers**: Offers of help serve as triggers and motivations to execute
proposed projects, moving from ideas to action.
## Context for LLM Development
When developing new functionality for Time Safari, consider these design principles:
1. **Accessibility First**: Features should be usable by non-technical users with
minimal learning curve.
2. **Privacy by Design**: All features must respect user privacy and data sovereignty.
3. **Progressive Enhancement**: Core functionality should work across all devices,
with richer experiences where supported.
4. **Voluntary Collaboration**: The system should enable but never coerce participation.
5. **Trust Building**: Features should help build verifiable trust between users.
6. **Network Effects**: Consider how features scale as more users join the platform.
7. **Low Resource Requirements**: The system should be lightweight enough to run
on inexpensive devices users already own.
## Use Cases to Support
LLM development should focus on enhancing these key use cases:
1. **Community Building**: Tools that help people find others with shared
interests and values.
2. **Project Coordination**: Features that make it easy to propose collaborative
projects and to submit suggestions and offers to existing ones.
3. **Reputation Building**: Methods for users to showcase their contributions
and reliability, in contexts where they explicitly reveal that information.
4. **Governance Experimentation**: Features that facilitate decision-making and
collective governance.
## Constraints
When developing new features, be mindful of these constraints:
1. **Privacy Preservation**: User identifiers must remain private except when
explicitly shared.
2. **Platform Limitations**: Features must work within the constraints of the target
app platforms, while aiming to leverage the best platform technology available.
3. **Endorser API Limitations**: Backend features are constrained by the endorser.ch
API capabilities.
4. **Performance on Low-End Devices**: The application should remain performant
on older/simpler devices.
5. **Offline-First When Possible**: Key functionality should work offline when feasible.
## Project Technologies
- Typescript using ES6 classes using vue-facing-decorator
- TailwindCSS
- Vite Build Tool
- Playwright E2E testing
- IndexDB
- Camera, Image uploads, QR Code reader, ...
## Mobile Features
- Deep Linking
- Local Notifications via a custom Capacitor plugin
## Project Architecture
- The application must work on web browser, PWA (Progressive Web Application),
desktop via Electron, and mobile via Capacitor
- Building for each platform is managed via Vite
## Core Development Principles
### DRY development
- **Code Reuse**
- Extract common functionality into utility functions
- Create reusable components for UI patterns
- Implement service classes for shared business logic
- Use mixins for cross-cutting concerns
- Leverage TypeScript interfaces for shared type definitions
- **Component Patterns**
- Create base components for common UI elements
- Implement higher-order components for shared behavior
- Use slot patterns for flexible component composition
- Create composable services for business logic
- Implement factory patterns for component creation
- **State Management**
- Centralize state in Pinia stores
- Use computed properties for derived state
- Implement shared state selectors
- Create reusable state mutations
- Use action creators for common operations
- **Error Handling**
- Implement centralized error handling
- Create reusable error components
- Use error boundary components
- Implement consistent error logging
- Create error type definitions
- **Type Definitions**
- Create shared interfaces for common data structures
- Use type aliases for complex types
- Implement generic types for reusable components
- Create utility types for common patterns
- Use discriminated unions for state management
- **API Integration**
- Create reusable API client classes
- Implement request/response interceptors
- Use consistent error handling patterns
- Create type-safe API endpoints
- Implement caching strategies
- **Platform Services**
- Abstract platform-specific code behind interfaces
- Create platform-agnostic service layers
- Implement feature detection
- Use dependency injection for services
- Create service factories
- **Testing**
- Create reusable test utilities
- Implement test factories
- Use shared test configurations
- Create reusable test helpers
- Implement consistent test patterns
- F.I.R.S.T. (for Unit Tests)
F – Fast
I – Independent
R – Repeatable
S – Self-validating
T – Timely
### SOLID Principles
- **Single Responsibility**: Each class/component should have only one reason to
change
- Components should focus on one specific feature (e.g., QR scanning, DID management)
- Services should handle one type of functionality (e.g., platform services,
crypto services)
- Utilities should provide focused helper functions
- **Open/Closed**: Software entities should be open for extension but closed for
modification
- Use interfaces for service definitions
- Implement plugin architecture for platform-specific features
- Allow component behavior extension through props and events
- **Liskov Substitution**: Objects should be replaceable with their subtypes
- Platform services should work consistently across web/mobile
- Authentication providers should be interchangeable
- Storage implementations should be swappable
- **Interface Segregation**: Clients shouldn't depend on interfaces they don't use
- Break down large service interfaces into smaller, focused ones
- Component props should be minimal and purposeful
- Event emissions should be specific and targeted
- **Dependency Inversion**: High-level modules shouldn't depend on low-level modules
- Use dependency injection for services
- Abstract platform-specific code behind interfaces
- Implement factory patterns for component creation
### Law of Demeter
- Components should only communicate with immediate dependencies
- Avoid chaining method calls (e.g., `this.service.getUser().getProfile().getName()`)
- Use mediator patterns for complex component interactions
- Implement facade patterns for subsystem access
- Keep component communication through defined events and props
### Composition over Inheritance
- Prefer building components through composition
- Use mixins for shared functionality
- Implement feature toggles through props
- Create higher-order components for common patterns
- Use service composition for complex features
### Interface Segregation
- Define clear interfaces for services
- Keep component APIs minimal and focused
- Split large interfaces into smaller, specific ones
- Use TypeScript interfaces for type definitions
- Implement role-based interfaces for different use cases
### Fail Fast
- Validate inputs early in the process
- Use TypeScript strict mode
- Implement comprehensive error handling
- Add runtime checks for critical operations
- Use assertions for development-time validation
### Principle of Least Astonishment
- Follow Vue.js conventions consistently
- Use familiar naming patterns
- Implement predictable component behaviors
- Maintain consistent error handling
- Keep UI interactions intuitive
### Information Hiding
- Encapsulate implementation details
- Use private class members
- Implement proper access modifiers
- Hide complex logic behind simple interfaces
- Use TypeScript's access modifiers effectively
### Single Source of Truth
- Use Pinia for state management
- Maintain one source for user data
- Centralize configuration management
- Use computed properties for derived state
- Implement proper state synchronization
### Principle of Least Privilege
- Implement proper access control
- Use minimal required permissions
- Follow privacy-by-design principles
- Restrict component access to necessary data
- Implement proper authentication/authorization

122
.cursor/rules/version_control.mdc

@ -1,122 +0,0 @@
---
alwaysApply: true
---
# Directive: Peaceful Co-Existence with Developers
## 1) Version-Control Ownership
* **MUST NOT** run `git add`, `git commit`, or any write action.
* **MUST** leave staging/committing to the developer.
## 2) Source of Truth for Commit Text
* **MUST** derive messages **only** from:
* files **staged** for commit (primary), and
* files **awaiting staging** (context).
* **MUST** use the **diffs** to inform content.
* **MUST NOT** invent changes or imply work not present in diffs.
## 3) Mandatory Preview Flow
* **ALWAYS** present, before any real commit:
* file list + brief per-file notes,
* a **draft commit message** (copy-paste ready),
* nothing auto-applied.
---
# Commit Message Format (Normative)
## A. Subject Line (required)
```
<type>(<scope>)<!>: <summary>
```
* **type** (lowercase, Conventional Commits): `feat|fix|refactor|perf|docs|test|build|chore|ci|revert`
* **scope**: optional module/package/area (e.g., `api`, `ui/login`, `db`)
* **!**: include when a breaking change is introduced
* **summary**: imperative mood, ≤ 72 chars, no trailing period
**Examples**
* `fix(api): handle null token in refresh path`
* `feat(ui/login)!: require OTP after 3 failed attempts`
## B. Body (optional, when it adds non-obvious value)
* One blank line after subject.
* Wrap at \~72 chars.
* Explain **what** and **why**, not line-by-line “how”.
* Include brief notes like tests passing or TS/lint issues resolved **only if material**.
**Body checklist**
* [ ] Problem/symptom being addressed
* [ ] High-level approach or rationale
* [ ] Risks, tradeoffs, or follow-ups (if any)
## C. Footer (optional)
* Issue refs: `Closes #123`, `Refs #456`
* Breaking change (alternative to `!`):
`BREAKING CHANGE: <impact + migration note>`
* Authors: `Co-authored-by: Name <email>`
* Security: `CVE-XXXX-YYYY: <short note>` (if applicable)
---
## Content Guidance
### Include (when relevant)
* Specific fixes/features delivered
* Symptoms/problems fixed
* Brief note that tests passed or TS/lint errors resolved
### Avoid
* Vague: *improved, enhanced, better*
* Trivialities: tiny docs, one-liners, pure lint cleanups (separate, focused commits if needed)
* Redundancy: generic blurbs repeated across files
* Multi-purpose dumps: keep commits **narrow and focused**
* Long explanations that good inline code comments already cover
**Guiding Principle:** Let code and inline docs speak. Use commits to highlight what isn’t obvious.
---
# Copy-Paste Templates
## Minimal (no body)
```text
<type>(<scope>): <summary>
```
## Standard (with body & footer)
```text
<type>(<scope>)<!>: <summary>
<why-this-change?>
<what-it-does?>
<risks-or-follow-ups?>
Closes #<id>
BREAKING CHANGE: <impact + migration>
Co-authored-by: <Name> <email>
```
---
# Assistant Output Checklist (before showing the draft)
* [ ] List changed files + 1–2 line notes per file
* [ ] Provide **one** focused draft message (subject/body/footer)
* [ ] Subject ≤ 72 chars, imperative mood, correct `type(scope)!` syntax
* [ ] Body only if it adds non-obvious value
* [ ] No invented changes; aligns strictly with diffs
* [ ] Render as a single copy-paste block for the developer

335
.cursor/rules/workflow/version_control.mdc

@ -0,0 +1,335 @@
---
description: interacting with git
alwaysApply: false
---
# Directive: Peaceful Co-Existence with Developers
**Author**: Matthew Raymer
**Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Version control guidelines
## 1) Version-Control Ownership
- **MUST NOT** run `git add`, `git commit`, or any write action.
- **MUST** leave staging/committing to the developer.
## 2) Source of Truth for Commit Text
- **MUST** derive messages **only** from:
- files **staged** for commit (primary), and
- files **awaiting staging** (context).
- **MUST** use the **diffs** to inform content.
- **MUST NOT** invent changes or imply work not present in diffs.
## 3) Mandatory Preview Flow
- **ALWAYS** present, before any real commit:
- file list + brief per-file notes,
- a **draft commit message** (copy-paste ready),
- nothing auto-applied.
## 4) Version Synchronization Requirements
- **MUST** check for version changes in `package.json` before committing
- **MUST** ensure `CHANGELOG.md` is updated when `package.json` version
changes
- **MUST** validate version format consistency between both files
- **MUST** include version bump commits in changelog with proper semantic
versioning
### Version Sync Checklist (Before Commit)
- [ ] `package.json` version matches latest `CHANGELOG.md` entry
- [ ] New version follows semantic versioning
(MAJOR.MINOR.PATCH[-PRERELEASE])
- [ ] Changelog entry includes all significant changes since last version
- [ ] Version bump commit message follows `build(version): bump to X.Y.Z`
format
- [ ] Breaking changes properly documented with migration notes
- [ ] Alert developer in chat message that version has been updated
### Version Change Detection
- **Check for version changes** in staged/unstaged `package.json`
- **Alert developer** if version changed but changelog not updated
- **Suggest changelog update** with proper format and content
- **Validate semantic versioning** compliance
### Implementation Notes
- **Version Detection**: Compare `package.json` version field with latest
changelog entry
- **Semantic Validation**: Ensure version follows `X.Y.Z[-PRERELEASE]`
format
- **Changelog Format**: Follow [Keep a Changelog](https://keepachangelog.com/)
standards
- **Breaking Changes**: Use `!` in commit message and `BREAKING CHANGE:`
in changelog
- **Pre-release Versions**: Include beta/alpha/rc suffixes in both files
consistently
## Commit Message Format (Normative)
### A. Subject Line (required)
```
<type>(<scope>)<!>: <summary>
```
- **type** (lowercase, Conventional Commits):
`feat|fix|refactor|perf|docs|test|build|chore|ci|revert`
- **scope**: optional module/package/area (e.g., `api`, `ui/login`, `db`)
- **!**: include when a breaking change is introduced
- **summary**: imperative mood, ≤ 72 chars, no trailing period
**Examples**
- `fix(api): handle null token in refresh path`
- `feat(ui/login)!: require OTP after 3 failed attempts`
### B. Body (optional, when it adds non-obvious value)
- One blank line after subject.
- Wrap at ~72 chars.
- Explain **what** and **why**, not line-by-line "how".
- Include brief notes like tests passing or TS/lint issues resolved
**only if material**.
**Body checklist**
- [ ] Problem/symptom being addressed
- [ ] High-level approach or rationale
- [ ] Risks, tradeoffs, or follow-ups (if any)
### C. Footer (optional)
- Issue refs: `Closes #123`, `Refs #456`
- Breaking change (alternative to `!`):
`BREAKING CHANGE: <impact + migration note>`
- Authors: `Co-authored-by: Name <email>`
- Security: `CVE-XXXX-YYYY: <short note>` (if applicable)
## Content Guidance
### Include (when relevant)
- Specific fixes/features delivered
- Symptoms/problems fixed
- Brief note that tests passed or TS/lint errors resolved
### Avoid
- Vague: *improved, enhanced, better*
- Trivialities: tiny docs, one-liners, pure lint cleanups (separate,
focused commits if needed)
- Redundancy: generic blurbs repeated across files
- Multi-purpose dumps: keep commits **narrow and focused**
- Long explanations that good inline code comments already cover
**Guiding Principle:** Let code and inline docs speak. Use commits to
highlight what isn't obvious.
## Copy-Paste Templates
### Minimal (no body)
```text
<type>(<scope>): <summary>
```
### Standard (with body & footer)
```text
<type>(<scope>)<!>: <summary>
<why-this-change?>
<what-it-does?>
<risks-or-follow-ups?>
Closes #<id>
BREAKING CHANGE: <impact + migration>
Co-authored-by: <Name> <email>
```
## Assistant Output Checklist (before showing the draft)
- [ ] List changed files + 1–2 line notes per file
- [ ] Provide **one** focused draft message (subject/body/footer)
- [ ] Subject ≤ 72 chars, imperative mood, correct `type(scope)!` syntax
- [ ] Body only if it adds non-obvious value
- [ ] No invented changes; aligns strictly with diffs
- [ ] Render as a single copy-paste block for the developer
---
**Status**: Active version control guidelines
**Priority**: High
**Estimated Effort**: Ongoing reference
**Dependencies**: git, package.json, CHANGELOG.md
**Stakeholders**: Development team, AI assistants
- [ ] No invented changes; aligns strictly with diffs
- [ ] Render as a single copy-paste block for the developer
## 1) Version-Control Ownership
- **MUST NOT** run `git add`, `git commit`, or any write action.
- **MUST** leave staging/committing to the developer.
## 2) Source of Truth for Commit Text
- **MUST** derive messages **only** from:
- files **staged** for commit (primary), and
- files **awaiting staging** (context).
- **MUST** use the **diffs** to inform content.
- **MUST NOT** invent changes or imply work not present in diffs.
## 3) Mandatory Preview Flow
- **ALWAYS** present, before any real commit:
- file list + brief per-file notes,
- a **draft commit message** (copy-paste ready),
- nothing auto-applied.
## 4) Version Synchronization Requirements
- **MUST** check for version changes in `package.json` before committing
- **MUST** ensure `CHANGELOG.md` is updated when `package.json` version
changes
- **MUST** validate version format consistency between both files
- **MUST** include version bump commits in changelog with proper semantic
versioning
### Version Sync Checklist (Before Commit)
- [ ] `package.json` version matches latest `CHANGELOG.md` entry
- [ ] New version follows semantic versioning
(MAJOR.MINOR.PATCH[-PRERELEASE])
- [ ] Changelog entry includes all significant changes since last version
- [ ] Version bump commit message follows `build(version): bump to X.Y.Z`
format
- [ ] Breaking changes properly documented with migration notes
- [ ] Alert developer in chat message that version has been updated
### Version Change Detection
- **Check for version changes** in staged/unstaged `package.json`
- **Alert developer** if version changed but changelog not updated
- **Suggest changelog update** with proper format and content
- **Validate semantic versioning** compliance
### Implementation Notes
- **Version Detection**: Compare `package.json` version field with latest
changelog entry
- **Semantic Validation**: Ensure version follows `X.Y.Z[-PRERELEASE]`
format
- **Changelog Format**: Follow [Keep a Changelog](https://keepachangelog.com/)
standards
- **Breaking Changes**: Use `!` in commit message and `BREAKING CHANGE:`
in changelog
- **Pre-release Versions**: Include beta/alpha/rc suffixes in both files
consistently
## Commit Message Format (Normative)
### A. Subject Line (required)
```
<type>(<scope>)<!>: <summary>
```
- **type** (lowercase, Conventional Commits):
`feat|fix|refactor|perf|docs|test|build|chore|ci|revert`
- **scope**: optional module/package/area (e.g., `api`, `ui/login`, `db`)
- **!**: include when a breaking change is introduced
- **summary**: imperative mood, ≤ 72 chars, no trailing period
**Examples**
- `fix(api): handle null token in refresh path`
- `feat(ui/login)!: require OTP after 3 failed attempts`
### B. Body (optional, when it adds non-obvious value)
- One blank line after subject.
- Wrap at ~72 chars.
- Explain **what** and **why**, not line-by-line "how".
- Include brief notes like tests passing or TS/lint issues resolved
**only if material**.
**Body checklist**
- [ ] Problem/symptom being addressed
- [ ] High-level approach or rationale
- [ ] Risks, tradeoffs, or follow-ups (if any)
### C. Footer (optional)
- Issue refs: `Closes #123`, `Refs #456`
- Breaking change (alternative to `!`):
`BREAKING CHANGE: <impact + migration note>`
- Authors: `Co-authored-by: Name <email>`
- Security: `CVE-XXXX-YYYY: <short note>` (if applicable)
## Content Guidance
### Include (when relevant)
- Specific fixes/features delivered
- Symptoms/problems fixed
- Brief note that tests passed or TS/lint errors resolved
### Avoid
- Vague: *improved, enhanced, better*
- Trivialities: tiny docs, one-liners, pure lint cleanups (separate,
focused commits if needed)
- Redundancy: generic blurbs repeated across files
- Multi-purpose dumps: keep commits **narrow and focused**
- Long explanations that good inline code comments already cover
**Guiding Principle:** Let code and inline docs speak. Use commits to
highlight what isn't obvious.
## Copy-Paste Templates
### Minimal (no body)
```text
<type>(<scope>): <summary>
```
### Standard (with body & footer)
```text
<type>(<scope>)<!>: <summary>
<why-this-change?>
<what-it-does?>
<risks-or-follow-ups?>
Closes #<id>
BREAKING CHANGE: <impact + migration>
Co-authored-by: <Name> <email>
```
## Assistant Output Checklist (before showing the draft)
- [ ] List changed files + 1–2 line notes per file
- [ ] Provide **one** focused draft message (subject/body/footer)
- [ ] Subject ≤ 72 chars, imperative mood, correct `type(scope)!` syntax
- [ ] Body only if it adds non-obvious value
- [ ] No invented changes; aligns strictly with diffs
- [ ] Render as a single copy-paste block for the developer
---
**Status**: Active version control guidelines
**Priority**: High
**Estimated Effort**: Ongoing reference
**Dependencies**: git, package.json, CHANGELOG.md
**Stakeholders**: Development team, AI assistants
* [ ] No invented changes; aligns strictly with diffs
* [ ] Render as a single copy-paste block for the developer

2
.env.test

@ -7,7 +7,7 @@ VITE_LOG_LEVEL=info
TIME_SAFARI_APP_TITLE="TimeSafari_Test" TIME_SAFARI_APP_TITLE="TimeSafari_Test"
VITE_APP_SERVER=https://test.timesafari.app VITE_APP_SERVER=https://test.timesafari.app
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not # This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not
production). # This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch

2
.eslintrc.js

@ -30,7 +30,7 @@ module.exports = {
}], }],
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn", "no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn", "no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off", "@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]

142
.github/workflows/asset-validation.yml

@ -0,0 +1,142 @@
name: Asset Validation & CI Safeguards
on:
pull_request:
paths:
- 'resources/**'
- 'config/assets/**'
- 'capacitor-assets.config.json'
- 'capacitor.config.ts'
- 'capacitor.config.json'
push:
branches: [main, develop]
paths:
- 'resources/**'
- 'config/assets/**'
- 'capacitor-assets.config.json'
- 'capacitor.config.ts'
- 'capacitor.config.json'
jobs:
asset-validation:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Validate asset configuration
run: npm run assets:validate
- name: Check for committed platform assets (Android)
run: |
if git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)' > /dev/null; then
echo "❌ Android platform assets found in VCS - these should be generated at build-time"
git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)'
exit 1
fi
echo "✅ No Android platform assets committed"
- name: Check for committed platform assets (iOS)
run: |
if git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)' > /dev/null; then
echo "❌ iOS platform assets found in VCS - these should be generated at build-time"
git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)'
exit 1
fi
echo "✅ No iOS platform assets committed"
- name: Test asset generation
run: |
echo "🧪 Testing asset generation workflow..."
npm run build:capacitor
npx cap sync
npx capacitor-assets generate --dry-run || npx capacitor-assets generate
echo "✅ Asset generation test completed"
- name: Verify clean tree after build
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "❌ Dirty tree after build - asset configs were modified"
git status
git diff
exit 1
fi
echo "✅ Build completed with clean tree"
schema-validation:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Validate schema compliance
run: |
echo "🔍 Validating schema compliance..."
node -e "
const fs = require('fs');
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8'));
const schema = JSON.parse(fs.readFileSync('config/assets/schema.json', 'utf8'));
// Basic schema validation
if (!config.icon || !config.splash) {
throw new Error('Missing required sections: icon and splash');
}
if (!config.icon.source || !config.splash.source) {
throw new Error('Missing required source fields');
}
if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) {
throw new Error('Icon source must be in resources/ directory');
}
if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) {
throw new Error('Splash source must be in resources/ directory');
}
console.log('✅ Schema validation passed');
"
- name: Check source file existence
run: |
echo "📁 Checking source file existence..."
node -e "
const fs = require('fs');
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8'));
const requiredFiles = [
config.icon.source,
config.splash.source
];
if (config.splash.darkSource) {
requiredFiles.push(config.splash.darkSource);
}
const missingFiles = requiredFiles.filter(file => !fs.existsSync(file));
if (missingFiles.length > 0) {
console.error('❌ Missing source files:', missingFiles);
process.exit(1);
}
console.log('✅ All source files exist');
"

12
.gitignore

@ -56,6 +56,10 @@ icons
*.log *.log
# Build outputs
dist/
build/
# Generated Android assets and resources (should be generated during build) # Generated Android assets and resources (should be generated during build)
android/app/src/main/assets/public/ android/app/src/main/assets/public/
@ -64,6 +68,14 @@ android/app/src/main/res/drawable*/
android/app/src/main/res/mipmap*/ android/app/src/main/res/mipmap*/
android/app/src/main/res/values/ic_launcher_background.xml android/app/src/main/res/values/ic_launcher_background.xml
# Android generated assets (deny-listed in CI)
android/app/src/main/res/mipmap-*/ic_launcher*.png
android/app/src/main/res/drawable*/splash*.png
# iOS generated assets (deny-listed in CI)
ios/App/App/Assets.xcassets/**/AppIcon*.png
ios/App/App/Assets.xcassets/**/Splash*.png
# Keep these Android configuration files in version control: # Keep these Android configuration files in version control:
# - android/app/src/main/assets/capacitor.plugins.json # - android/app/src/main/assets/capacitor.plugins.json
# - android/app/src/main/res/values/strings.xml # - android/app/src/main/res/values/strings.xml

1
.node-version

@ -0,0 +1 @@
18.19.0

1
.nvmrc

@ -0,0 +1 @@
18.19.0

63
BUILDING.md

@ -1017,47 +1017,27 @@ If you need to build manually or want to understand the individual steps:
export GEM_PATH=$shortened_path export GEM_PATH=$shortened_path
``` ```
1. Build the web assets & update ios: 1. Bump the version in package.json, then here.
```bash
rm -rf dist
npm run build:web
npm run build:capacitor
npx cap sync ios
```
- If that fails with "Could not find..." then look at the "gem_path" instructions above.
3. Copy the assets:
```bash
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
npx capacitor-assets generate --ios
```
4. Bump the version to match Android & package.json:
``` ```
cd ios/App && xcrun agvtool new-version 39 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.6;/g" App.xcodeproj/project.pbxproj && cd - cd ios/App && xcrun agvtool new-version 40 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.7;/g" App.xcodeproj/project.pbxproj && cd -
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #xcrun agvtool new-marketing-version 0.4.5
``` ```
5. Open the project in Xcode: 2. Build.
Here's prod. Also available: test, dev
```bash ```bash
npx cap open ios npm run build:ios:prod
``` ```
6. Use Xcode to build and run on simulator or device. 3.1. Use Xcode to build and run on simulator or device.
* Select Product -> Destination with some Simulator version. Then click the run arrow. * Select Product -> Destination with some Simulator version. Then click the run arrow.
7. Release 3.2. Use Xcode to release.
* Someday: Under "General" we want to rename a bunch of things to "Time Safari" * Someday: Under "General" we want to rename a bunch of things to "Time Safari"
* Choose Product -> Destination -> Any iOS Device * Choose Product -> Destination -> Any iOS Device
@ -1125,35 +1105,28 @@ The recommended way to build for Android is using the automated build script:
#### Manual Build Process #### Manual Build Process
1. Build the web assets: 1. Bump the version in package.json, then here: android/app/build.gradle
```bash ```bash
rm -rf dist perl -p -i -e 's/versionCode .*/versionCode 40/g' android/app/build.gradle
npm run build:web perl -p -i -e 's/versionName .*/versionName "1.0.7"/g' android/app/build.gradle
npm run build:capacitor
``` ```
2. Update Android project with latest build: 2. Build.
```bash Here's prod. Also available: test, dev
npx cap sync android
```
3. Copy the assets
```bash ```bash
npx capacitor-assets generate --android npm run build:android:prod
``` ```
4. Bump version to match iOS & package.json: android/app/build.gradle 3. Open the project in Android Studio:
5. Open the project in Android Studio:
```bash ```bash
npx cap open android npx cap open android
``` ```
6. Use Android Studio to build and run on emulator or device. 4. Use Android Studio to build and run on emulator or device.
## Android Build from the console ## Android Build from the console
@ -1186,9 +1159,9 @@ cd -
At play.google.com/console: At play.google.com/console:
- Go to the Testing Track (eg. Closed). - Go to Production or the Closed Testing and either Create Track or Manage Track.
- Click "Create new release". - Click "Create new release".
- Upload the `aab` file. - Upload the `aab` file from: app/build/outputs/bundle/release/app-release.aab
- Hit "Next". - Hit "Next".
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review". - Save, go to "Publishing Overview" as prompted, and click "Send changes for review".

8
CHANGELOG.md

@ -5,12 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.3] - 2025.07.12 ## [1.0.7] - 2025.08.18
### Changed
- Photo is pinned to profile mode
### Fixed ### Fixed
- Deep link URLs (and other prod settings) - Deep link for onboard-meeting-members
- Error in BVC begin view
## [1.0.6] - 2025.08.09 ## [1.0.6] - 2025.08.09
### Fixed ### Fixed

72
README.md

@ -136,11 +136,77 @@ const apiUrl = `${APP_SERVER}/api/claim/123`;
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions. See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
## Icons ## Asset Management
Application icons are in the `assets` directory, processed by the `capacitor-assets` command. TimeSafari uses a standardized asset configuration system for consistent
icon and splash screen generation across all platforms.
To add a Font Awesome icon, add to fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name. ### Asset Sources
- **Single source of truth**: `resources/` directory (Capacitor default)
- **Source files**: `icon.png`, `splash.png`, `splash_dark.png`
- **Format**: PNG or SVG files for optimal quality
### Asset Generation
- **Configuration**: `config/assets/capacitor-assets.config.json`
- **Schema validation**: `config/assets/schema.json`
- **Build-time generation**: Platform assets generated via `capacitor-assets`
- **No VCS commits**: Generated assets are never committed to version control
### Development Commands
```bash
# Generate/update asset configurations
npm run assets:config
# Validate asset configurations
npm run assets:validate
# Clean generated platform assets (local dev only)
npm run assets:clean
# Build with asset generation
npm run build:native
```
### Environment Setup & Dependencies
Before building the application, ensure your development environment is properly
configured:
```bash
# Install all dependencies (required first time and after updates)
npm install
# Validate your development environment
npm run check:dependencies
# Check prerequisites for testing
npm run test:prerequisites
```
**Common Issues & Solutions**:
- **"tsx: command not found"**: Run `npm install` to install devDependencies
- **"capacitor-assets: command not found"**: Ensure `@capacitor/assets` is installed
- **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)
### Platform Support
- **Android**: Adaptive icons with foreground/background, monochrome support
- **iOS**: LaunchScreen storyboard preferred, splash assets when needed
- **Web**: PWA icons generated during build to `dist/` (not committed)
### Font Awesome Icons
To add a Font Awesome icon, add to `fontawesome.ts` and reference with
`font-awesome` element and `icon` attribute with the hyphenated name.
## Other ## Other

4
android/app/build.gradle

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

2
android/app/src/main/assets/capacitor.config.json

@ -29,7 +29,7 @@
"splashFullScreen": true, "splashFullScreen": true,
"splashImmersive": true "splashImmersive": true
}, },
"CapacitorSQLite": { "CapSQLite": {
"iosDatabaseLocation": "Library/CapacitorDatabase", "iosDatabaseLocation": "Library/CapacitorDatabase",
"iosIsEncryption": false, "iosIsEncryption": false,
"iosBiometric": { "iosBiometric": {

7
android/app/src/main/res/values/colors.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

2
android/build.gradle

@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.12.0' classpath 'com.android.tools.build:gradle:8.12.1'
classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

2
assets/README.md

@ -1,2 +0,0 @@
Application icons are here. They are processed for android & ios by the `capacitor-assets` command, as indicated in the BUILDING.md file.

34
capacitor-assets.config.json

@ -1,36 +1,32 @@
{ {
"icon": { "icon": {
"ios": {
"source": "resources/ios/icon/icon.png",
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
},
"android": { "android": {
"source": "resources/android/icon/icon.png", "adaptive": {
"background": "#121212",
"foreground": "resources/icon.png",
"monochrome": "resources/icon.png"
},
"target": "android/app/src/main/res" "target": "android/app/src/main/res"
}, },
"ios": {
"padding": 0,
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
},
"source": "resources/icon.png",
"web": { "web": {
"source": "resources/web/icon/icon.png",
"target": "public/img/icons" "target": "public/img/icons"
} }
}, },
"splash": { "splash": {
"ios": {
"source": "resources/ios/splash/splash.png",
"target": "ios/App/App/Assets.xcassets/Splash.imageset"
},
"android": { "android": {
"source": "resources/android/splash/splash.png", "scale": "cover",
"target": "android/app/src/main/res" "target": "android/app/src/main/res"
}
}, },
"splashDark": { "darkSource": "resources/splash_dark.png",
"ios": { "ios": {
"source": "resources/ios/splash/splash_dark.png", "target": "ios/App/App/Assets.xcassets",
"target": "ios/App/App/Assets.xcassets/SplashDark.imageset" "useStoryBoard": true
}, },
"android": { "source": "resources/splash.png"
"source": "resources/android/splash/splash_dark.png",
"target": "android/app/src/main/res"
}
} }
} }

116
capacitor.config.ts

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

32
config/assets/capacitor-assets.config.json

@ -0,0 +1,32 @@
{
"icon": {
"android": {
"adaptive": {
"background": "#121212",
"foreground": "resources/icon.png",
"monochrome": "resources/icon.png"
},
"target": "android/app/src/main/res"
},
"ios": {
"padding": 0,
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
},
"source": "resources/icon.png",
"web": {
"target": "public/img/icons"
}
},
"splash": {
"android": {
"scale": "cover",
"target": "android/app/src/main/res"
},
"darkSource": "resources/splash_dark.png",
"ios": {
"target": "ios/App/App/Assets.xcassets",
"useStoryBoard": true
},
"source": "resources/splash.png"
}
}

119
config/assets/schema.json

@ -0,0 +1,119 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Capacitor Assets Configuration Schema",
"description": "Schema for validating capacitor-assets configuration files",
"type": "object",
"properties": {
"icon": {
"type": "object",
"properties": {
"source": {
"type": "string",
"pattern": "^resources/.*\\.(png|svg)$",
"description": "Source icon file path relative to project root"
},
"android": {
"type": "object",
"properties": {
"adaptive": {
"type": "object",
"properties": {
"foreground": {
"type": "string",
"pattern": "^resources/.*\\.(png|svg)$",
"description": "Foreground icon for Android adaptive icons"
},
"background": {
"type": ["string", "object"],
"description": "Background color or image for adaptive icons"
},
"monochrome": {
"type": "string",
"pattern": "^resources/.*\\.(png|svg)$",
"description": "Monochrome icon for Android 13+"
}
},
"required": ["foreground", "background"]
},
"target": {
"type": "string",
"description": "Android target directory for generated icons"
}
}
},
"ios": {
"type": "object",
"properties": {
"padding": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Padding ratio for iOS icons"
},
"target": {
"type": "string",
"description": "iOS target directory for generated icons"
}
}
},
"web": {
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "Web target directory for generated icons"
}
}
}
},
"required": ["source"],
"additionalProperties": false
},
"splash": {
"type": "object",
"properties": {
"source": {
"type": "string",
"pattern": "^resources/.*\\.(png|svg)$",
"description": "Source splash screen file"
},
"darkSource": {
"type": "string",
"pattern": "^resources/.*\\.(png|svg)$",
"description": "Dark mode splash screen file"
},
"android": {
"type": "object",
"properties": {
"scale": {
"type": "string",
"enum": ["cover", "contain", "fill"],
"description": "Android splash screen scaling mode"
},
"target": {
"type": "string",
"description": "Android target directory for splash screens"
}
}
},
"ios": {
"type": "object",
"properties": {
"useStoryBoard": {
"type": "boolean",
"description": "Use LaunchScreen storyboard instead of splash assets"
},
"target": {
"type": "string",
"description": "iOS target directory for splash screens"
}
}
}
},
"required": ["source"],
"additionalProperties": false
}
},
"required": ["icon", "splash"],
"additionalProperties": false
}

214
doc/asset-migration-plan.md

@ -0,0 +1,214 @@
# TimeSafari Asset Configuration Migration Plan
**Author**: Matthew Raymer
**Date**: 2025-08-14
**Status**: 🎯 **IMPLEMENTATION** - Ready for Execution
## Overview
This document outlines the migration from the current mixed asset management
system to a standardized, single-source asset configuration approach using
`capacitor-assets` as the standard generator.
## Current State Analysis
### Asset Sources (Duplicated)
- **`assets/` directory**: Contains `icon.png`, `splash.png`, `splash_dark.png`
- **`resources/` directory**: Contains identical files in platform-specific subdirectories
- **Result**: Duplicate storage, confusion about source of truth
### Asset Generation (Manual)
- **Custom scripts**: `generate-icons.sh`, `generate-ios-assets.sh`, `generate-android-icons.sh`
- **Bypass capacitor-assets**: Manual ImageMagick-based generation
- **Inconsistent outputs**: Different generation methods for each platform
### Configuration (Scattered)
- **`capacitor-assets.config.json`**: Basic configuration at root
- **Platform-specific configs**: Mixed in various build scripts
- **No validation**: No schema or consistency checks
## Target State
### Single Source of Truth
- **`resources/` directory**: Capacitor default location for source assets
- **Eliminate duplication**: Remove `assets/` directory after migration
- **Standardized paths**: All tools read from `resources/`
### Standardized Generation
- **`capacitor-assets`**: Single tool for all platform asset generation
- **Build-time generation**: Assets generated during build, not committed
- **Deterministic outputs**: Same inputs → same outputs every time
### Centralized Configuration
- **`config/assets/`**: All asset-related configuration files
- **Schema validation**: JSON schema for configuration validation
- **CI safeguards**: Automated validation and compliance checks
## Migration Steps
### Phase 1: Foundation Setup ✅
- [x] Create `config/assets/` directory structure
- [x] Create asset configuration schema (`schema.json`)
- [x] Create enhanced capacitor-assets configuration
- [x] Convert `capacitor.config.json` to `capacitor.config.ts`
- [x] Pin Node.js version (`.nvmrc`, `.node-version`)
- [x] Create dev-time asset configuration generator
- [x] Create asset configuration validator
- [x] Add npm scripts for asset management
- [x] Update `.gitignore` with proper asset exclusions
- [x] Create CI workflow for asset validation
### Phase 2: Validation & Testing
- [ ] Run `npm run assets:config` to generate new configuration
- [ ] Run `npm run assets:validate` to verify configuration
- [ ] Test `npm run build:native` workflow
- [ ] Verify CI workflow passes all checks
- [ ] Confirm no platform assets are committed to VCS
### Phase 3: Cleanup & Removal
- [ ] Remove `assets/` directory (after validation)
- [ ] Remove manual asset generation scripts
- [ ] Remove asset checking scripts
- [ ] Update documentation references
- [ ] Final validation of clean state
## Implementation Details
### File Structure
```
resources/ # Image sources ONLY
icon.png
splash.png
splash_dark.png
config/assets/ # Versioned config & schema
capacitor-assets.config.json
schema.json
scripts/
assets-config.js # Dev-time config generator
assets-validator.js # Schema validator
```
### Configuration Schema
The schema enforces:
- Source files must be in `resources/` directory
- Required fields for icon and splash sections
- Android adaptive icon support (foreground/background/monochrome)
- iOS LaunchScreen preferences
- Target directory validation
### CI Safeguards
- **Schema validation**: Configuration must comply with schema
- **Source file validation**: All referenced files must exist
- **Platform asset denial**: Reject commits with generated assets
- **Clean tree enforcement**: Build must not modify committed configs
## Testing Strategy
### Local Validation
```bash
# Generate configuration
npm run assets:config
# Validate configuration
npm run assets:validate
# Test build workflow
npm run build:native
# Clean generated assets
npm run assets:clean
```
### CI Validation
- **Asset validation workflow**: Runs on asset-related changes
- **Schema compliance**: Ensures configuration follows schema
- **Source file existence**: Verifies all referenced files exist
- **Platform asset detection**: Prevents committed generated assets
- **Build tree verification**: Ensures clean tree after build
## Risk Mitigation
### Data Loss Prevention
- **Backup branch**: Create backup before removing `assets/`
- **Validation checks**: Multiple validation steps before removal
- **Gradual migration**: Phase-by-phase approach with rollback capability
### Build Continuity
- **Per-platform scripts unchanged**: All existing build orchestration preserved
- **Standard toolchain**: Uses capacitor-assets, not custom scripts
- **Fallback support**: Manual scripts remain until migration complete
### Configuration Consistency
- **Schema enforcement**: JSON schema prevents invalid configurations
- **CI validation**: Automated checks catch configuration issues
- **Documentation updates**: Clear guidance for future changes
## Success Criteria
### Technical Requirements
- [ ] Single source of truth in `resources/` directory
- [ ] All platform assets generated via `capacitor-assets`
- [ ] No manual asset generation scripts
- [ ] Configuration validation passes all checks
- [ ] CI workflow enforces asset policies
### Quality Metrics
- [ ] Zero duplicate asset sources
- [ ] 100% configuration schema compliance
- [ ] No platform assets committed to VCS
- [ ] Clean build tree after asset generation
- [ ] Deterministic asset outputs
### User Experience
- [ ] Clear asset management documentation
- [ ] Simple development commands
- [ ] Consistent asset generation across platforms
- [ ] Reduced confusion about asset sources
## Next Steps
1. **Execute Phase 2**: Run validation and testing steps
2. **Verify CI workflow**: Ensure all checks pass
3. **Execute Phase 3**: Remove duplicate assets and scripts
4. **Update documentation**: Finalize README and BUILDING.md
5. **Team training**: Ensure all developers understand new workflow
## Rollback Plan
If issues arise during migration:
1. **Restore backup branch**: `git checkout backup-before-asset-migration`
2. **Revert configuration changes**: Remove new config files
3. **Restore manual scripts**: Re-enable previous asset generation
4. **Investigate issues**: Identify and resolve root causes
5. **Plan revised migration**: Adjust approach based on lessons learned
---
**Status**: Ready for Phase 2 execution
**Priority**: High
**Estimated Effort**: 2-3 hours
**Dependencies**: CI workflow validation
**Stakeholders**: Development team

182
doc/debug-hook-guide.md

@ -0,0 +1,182 @@
# TimeSafari Debug Hook Guide
**Complete Guide for Team Members**
**Date**: 2025-01-27
**Author**: Matthew Raymer
**Status**: ✅ **ACTIVE** - Ready for production use
## 🎯 Overview
A pre-commit hook that automatically detects and prevents debug code from reaching protected branches (master, main, production, release, stable). This ensures production code remains clean while allowing free development on feature branches.
## 🚀 Quick Installation
**From within the TimeSafari repository:**
```bash
./scripts/install-debug-hook.sh
```
This automatically installs, updates, and verifies the hook in your current
repository. **Note**: Hooks are not automatically installed - you must run this
script deliberately to enable debug code checking.
## 🔧 Manual Installation
**Copy files manually:**
```bash
cp scripts/git-hooks/pre-commit /path/to/your/repo/.git/hooks/
cp scripts/git-hooks/debug-checker.config /path/to/your/repo/.git/hooks/
chmod +x /path/to/your/repo/.git/hooks/pre-commit
```
## 📋 What Gets Installed
- **`pre-commit`** - Main hook script (executable)
- **`debug-checker.config`** - Configuration file
- **`README.md`** - Documentation and troubleshooting
**Note**: Hooks are stored in `scripts/git-hooks/` and must be deliberately
installed by each developer. They are not automatically active.
## 🎯 How It Works
1. **Deliberate Installation**: Hooks must be explicitly installed by each
developer
2. **Branch Detection**: Only runs on protected branches
3. **File Filtering**: Automatically skips tests, scripts, and documentation
4. **Pattern Matching**: Detects debug code using regex patterns
5. **Commit Prevention**: Blocks commits containing debug code
## 🔒 Installation Philosophy
**Why deliberate installation?**
- **Developer choice**: Each developer decides whether to use the hook
- **No forced behavior**: Hooks don't interfere with existing workflows
- **Local control**: Hooks are installed locally, not globally
- **Easy removal**: Can be uninstalled at any time
- **Team flexibility**: Some developers may prefer different tools
## 🌿 Branch Behavior
- **Protected branches** (master, main, production, release, stable): Hook runs automatically
- **Feature branches**: Hook is skipped, allowing free development with debug code
## 🔍 Debug Patterns Detected
- **Console statements**: `console.log`, `console.debug`, `console.error`
- **Template debug**: `Debug:`, `debug:` in Vue templates
- **Debug constants**: `DEBUG_`, `debug_` variables
- **HTML debug**: `<!-- debug` comments
- **Debug attributes**: `debug="true"` attributes
- **Vue debug**: `v-if="debug"`, `v-show="debug"`
- **Debug TODOs**: `TODO debug`, `FIXME debug`
## 📁 Files Automatically Skipped
- Test files: `*.test.js`, `*.spec.ts`, `*.test.vue`
- Scripts: `scripts/` directory
- Test directories: `test-*` directories
- Documentation: `docs/`, `*.md`, `*.txt`
- Config files: `*.json`, `*.yml`, `*.yaml`
- IDE files: `.cursor/` directory
## ✅ Verification
**After installation, verify it's working:**
```bash
# Check if files exist
ls -la .git/hooks/pre-commit
ls -la .git/hooks/debug-checker.config
# Test the hook manually
.git/hooks/pre-commit
# Test with actual commit
echo "console.log('test')" > test.vue
git add test.vue
git commit -m "test" # Should be blocked
```
## 📊 Example Output
```
❌ Debug code detected in staged files!
Branch: master
Files checked: 1
Errors found: 3
🚨 AccountViewView.vue: Found debug pattern 'console\.'
🚨 AccountViewView.vue: Found debug pattern 'Debug:'
🚨 AccountViewView.vue: Found debug pattern 'DEBUG_'
💡 Please remove debug code before committing to master
```
## ⚙️ Configuration
Edit `.git/hooks/debug-checker.config` to customize:
- **Protected branches**: Add/remove branches as needed
- **Debug patterns**: Customize what gets detected
- **Skip patterns**: Adjust file filtering rules
## 🚨 Emergency Bypass
If you absolutely need to commit debug code to a protected branch:
```bash
git commit --no-verify -m "emergency: debug code needed"
```
⚠️ **Warning**: This bypasses all pre-commit hooks. Use sparingly.
## 🔄 Updates
When the hook is updated in the main repository:
```bash
./scripts/install-debug-hook.sh
```
## 🚨 Troubleshooting
| Issue | Solution |
|-------|----------|
| Hook not running | Check if on protected branch, verify permissions |
| Permission denied | Run `chmod +x .git/hooks/pre-commit` |
| Files not found | Ensure you're copying from TimeSafari repo |
| False positives | Edit `debug-checker.config` to customize patterns |
## 🧪 Testing
A test script is available at `scripts/test-debug-hook.sh` to verify the hook works correctly.
## 💡 Best Practices
1. **Use feature branches** for development with debug code
2. **Use proper logging** instead of console statements (`logger.info`, `logger.debug`)
3. **Test thoroughly** before merging to protected branches
4. **Review commits** to ensure no debug code slips through
5. **Keep hooks updated** across all repositories
## 📚 Additional Resources
- **Hook documentation**: `scripts/git-hooks/README.md`
- **Configuration**: `scripts/git-hooks/debug-checker.config`
- **Test script**: `scripts/test-debug-hook.sh`
- **Installation script**: `scripts/install-debug-hook.sh`
## 🎯 Team Workflow
**Recommended setup:**
1. **Repository setup**: Include hook files in `.githooks/` directory
2. **Team onboarding**: Run installation script in each repo
3. **Updates**: Re-run installation script when hooks are updated
4. **Documentation**: Keep this guide updated
---
**Status**: Active and enforced
**Last Updated**: 2025-01-27
**Maintainer**: Matthew Raymer

2
index.html

@ -11,6 +11,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.web.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

8
ios/App/App.xcodeproj/project.pbxproj

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

23
ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "splash@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "splash@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "splash@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

2017
package-lock.json

File diff suppressed because it is too large

16
package.json

@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.7-beta", "version": "1.0.8-beta",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@ -8,9 +8,12 @@
"scripts": { "scripts": {
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src", "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src", "lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"type-safety-check": "./scripts/type-safety-check.sh",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js", "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
"test:prerequisites": "node scripts/check-prerequisites.js", "test:prerequisites": "node scripts/check-prerequisites.js",
"check:dependencies": "./scripts/check-dependencies.sh",
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on", "test:web": "npx playwright test -c playwright.config-local.ts --trace on",
"test:mobile": "./scripts/test-mobile.sh", "test:mobile": "./scripts/test-mobile.sh",
"test:android": "node scripts/test-android.js", "test:android": "node scripts/test-android.js",
@ -25,6 +28,10 @@
"auto-run:electron": "./scripts/auto-run.sh --platform=electron", "auto-run:electron": "./scripts/auto-run.sh --platform=electron",
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts", "build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor:sync": "npm run build:capacitor && npx cap sync", "build:capacitor:sync": "npm run build:capacitor && npx cap sync",
"build:native": "vite build && npx cap sync && npx capacitor-assets generate",
"assets:config": "npx tsx scripts/assets-config.ts",
"assets:validate": "npx tsx scripts/assets-validator.ts",
"assets:clean": "rimraf android/app/src/main/res/mipmap-* ios/App/App/Assets.xcassets/**/AppIcon*.png ios/App/App/Assets.xcassets/**/Splash*.png || true",
"build:ios": "./scripts/build-ios.sh", "build:ios": "./scripts/build-ios.sh",
"build:ios:dev": "./scripts/build-ios.sh --dev", "build:ios:dev": "./scripts/build-ios.sh --dev",
"build:ios:test": "./scripts/build-ios.sh --test", "build:ios:test": "./scripts/build-ios.sh --test",
@ -91,7 +98,7 @@
"build:electron:dmg:dev": "./scripts/build-electron.sh --dev --dmg", "build:electron:dmg:dev": "./scripts/build-electron.sh --dev --dmg",
"build:electron:dmg:test": "./scripts/build-electron.sh --test --dmg", "build:electron:dmg:test": "./scripts/build-electron.sh --test --dmg",
"build:electron:dmg:prod": "./scripts/build-electron.sh --prod --dmg", "build:electron:dmg:prod": "./scripts/build-electron.sh --prod --dmg",
"clean:android": "adb uninstall app.timesafari.app || true", "clean:android": "./scripts/clean-android.sh",
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true", "clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
"clean:electron": "./scripts/build-electron.sh --clean", "clean:electron": "./scripts/build-electron.sh --clean",
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron", "clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
@ -195,9 +202,9 @@
"three": "^0.156.1", "three": "^0.156.1",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"uint8arrays": "^5.0.0", "uint8arrays": "^5.0.0",
"vue": "^3.5.13", "vue": "3.5.13",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4", "vue-facing-decorator": "3.0.4",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@ -243,6 +250,7 @@
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"tsx": "^4.20.4",
"typescript": "~5.2.2", "typescript": "~5.2.2",
"vite": "^5.2.0" "vite": "^5.2.0"
} }

2
playwright.config-local.ts

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

0
assets/icon.png → resources/icon.png

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

0
assets/splash.png → resources/splash.png

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

0
assets/splash_dark.png → resources/splash_dark.png

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

224
scripts/assets-config.ts

@ -0,0 +1,224 @@
#!/usr/bin/env tsx
/**
* TimeSafari Asset Configuration Generator
* Generates capacitor-assets configuration files with deterministic outputs
* Author: Matthew Raymer
*
* Usage: tsx scripts/assets-config.ts
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.dirname(__dirname);
// TypeScript interfaces for asset configuration
interface AdaptiveIconConfig {
foreground: string;
background: string;
monochrome: string;
}
interface AndroidIconConfig {
adaptive: AdaptiveIconConfig;
target: string;
}
interface IOSIconConfig {
padding: number;
target: string;
}
interface WebIconConfig {
target: string;
}
interface IconConfig {
source: string;
android: AndroidIconConfig;
ios: IOSIconConfig;
web: WebIconConfig;
}
interface AndroidSplashConfig {
scale: string;
target: string;
}
interface IOSSplashConfig {
useStoryBoard: boolean;
target: string;
}
interface SplashConfig {
source: string;
darkSource: string;
android: AndroidSplashConfig;
ios: IOSSplashConfig;
}
interface AssetConfig {
icon: IconConfig;
splash: SplashConfig;
}
/**
* Generate deterministic capacitor-assets configuration
* @returns Sorted, stable configuration object
*/
function generateAssetConfig(): AssetConfig {
const config: AssetConfig = {
icon: {
source: "resources/icon.png",
android: {
adaptive: {
foreground: "resources/icon.png",
background: "#121212",
monochrome: "resources/icon.png"
},
target: "android/app/src/main/res"
},
ios: {
padding: 0,
target: "ios/App/App/Assets.xcassets/AppIcon.appiconset"
},
web: {
target: "public/img/icons"
}
},
splash: {
source: "resources/splash.png",
darkSource: "resources/splash_dark.png",
android: {
scale: "cover",
target: "android/app/src/main/res"
},
ios: {
useStoryBoard: true,
target: "ios/App/App/Assets.xcassets"
}
}
};
return sortObjectKeys(config);
}
/**
* Sort object keys recursively for deterministic output
* @param obj - Object to sort
* @returns Object with sorted keys
*/
function sortObjectKeys(obj: any): any {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(sortObjectKeys);
}
const sorted: any = {};
Object.keys(obj)
.sort()
.forEach(key => {
sorted[key] = sortObjectKeys(obj[key]);
});
return sorted;
}
/**
* Validate that required source files exist
*/
function validateSourceFiles(): void {
const requiredFiles = [
'resources/icon.png',
'resources/splash.png',
'resources/splash_dark.png'
];
const missingFiles = requiredFiles.filter(file => {
const filePath = path.join(PROJECT_ROOT, file);
return !fs.existsSync(filePath);
});
if (missingFiles.length > 0) {
console.error('❌ Missing required source files:');
missingFiles.forEach(file => console.error(` ${file}`));
process.exit(1);
}
console.log('✅ All required source files found');
}
/**
* Write configuration to file with consistent formatting
* @param config - Configuration object
* @param outputPath - Output file path
*/
function writeConfig(config: AssetConfig, outputPath: string): void {
const jsonString = JSON.stringify(config, null, 2);
// Ensure consistent line endings and no trailing whitespace
const cleanJson = jsonString
.split('\n')
.map(line => line.trimEnd())
.join('\n') + '\n';
fs.writeFileSync(outputPath, cleanJson, 'utf8');
console.log(`✅ Configuration written to: ${outputPath}`);
}
/**
* Main execution function
*/
function main(): void {
console.log('🔄 Generating TimeSafari asset configuration...');
console.log(`📁 Project root: ${PROJECT_ROOT}`);
console.log(`📅 Generated: ${new Date().toISOString()}`);
try {
// Validate source files exist
validateSourceFiles();
// Generate configuration
const config = generateAssetConfig();
// Ensure config directory exists
const configDir = path.join(PROJECT_ROOT, 'config', 'assets');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// Write configuration files
const capacitorAssetsConfigPath = path.join(configDir, 'capacitor-assets.config.json');
writeConfig(config, capacitorAssetsConfigPath);
// Copy to root for capacitor-assets discovery
const rootConfigPath = path.join(PROJECT_ROOT, 'capacitor-assets.config.json');
writeConfig(config, rootConfigPath);
console.log('🎉 Asset configuration generation completed successfully!');
console.log('');
console.log('📋 Next steps:');
console.log(' 1. Review the generated configuration');
console.log(' 2. Commit the configuration files');
console.log(' 3. Run "npm run assets:validate" to verify');
console.log(' 4. Use "npm run build:native" for builds');
} catch (error) {
console.error('❌ Configuration generation failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { generateAssetConfig, sortObjectKeys, validateSourceFiles, AssetConfig };

239
scripts/assets-validator.ts

@ -0,0 +1,239 @@
#!/usr/bin/env tsx
/**
* TimeSafari Asset Configuration Validator
* Validates capacitor-assets configuration against schema and source files
* Author: Matthew Raymer
*
* Usage: tsx scripts/assets-validator.ts [config-path]
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.dirname(__dirname);
// TypeScript interfaces for validation
interface ValidationError {
message: string;
}
interface AssetConfig {
icon?: {
source?: string;
android?: {
adaptive?: {
foreground?: string;
background?: string;
monochrome?: string;
};
};
};
splash?: {
source?: string;
darkSource?: string;
};
}
/**
* Load and parse JSON file
* @param filePath - Path to JSON file
* @returns Parsed JSON object
*/
function loadJsonFile(filePath: string): AssetConfig {
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Validate configuration against schema
* @param config - Configuration object to validate
* @returns Array of validation errors
*/
function validateAgainstSchema(config: AssetConfig): ValidationError[] {
const errors: ValidationError[] = [];
// Basic structure validation
if (!config.icon || !config.splash) {
errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' });
}
// Icon validation
if (config.icon) {
if (!config.icon.source) {
errors.push({ message: 'Icon section must contain "source" field' });
} else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) {
errors.push({ message: 'Icon source must be a PNG or SVG file in resources/ directory' });
}
// Android adaptive icon validation
if (config.icon.android?.adaptive) {
const adaptive = config.icon.android.adaptive;
if (!adaptive.foreground || !adaptive.background) {
errors.push({ message: 'Android adaptive icon must have both foreground and background' });
}
if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) {
errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' });
}
}
}
// Splash validation
if (config.splash) {
if (!config.splash.source) {
errors.push({ message: 'Splash section must contain "source" field' });
} else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) {
errors.push({ message: 'Splash source must be a PNG or SVG file in resources/ directory' });
}
if (config.splash.darkSource && !/^resources\/.*\.(png|svg)$/.test(config.splash.darkSource)) {
errors.push({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' });
}
}
return errors;
}
/**
* Validate that source files exist
* @param config - Configuration object
* @returns Array of missing file errors
*/
function validateSourceFiles(config: AssetConfig): ValidationError[] {
const errors: ValidationError[] = [];
const requiredFiles = new Set<string>();
// Collect all required source files
if (config.icon?.source) requiredFiles.add(config.icon.source);
if (config.icon?.android?.adaptive?.foreground) requiredFiles.add(config.icon.android.adaptive.foreground);
if (config.icon?.android?.adaptive?.monochrome) requiredFiles.add(config.icon.android.adaptive.monochrome);
if (config.splash?.source) requiredFiles.add(config.splash.source);
if (config.splash?.darkSource) requiredFiles.add(config.splash.darkSource);
// Check each file exists
requiredFiles.forEach(file => {
const filePath = path.join(PROJECT_ROOT, file);
if (!fs.existsSync(filePath)) {
errors.push({ message: `Source file not found: ${file}` });
}
});
return errors;
}
/**
* Validate target directories are writable
* @param config - Configuration object
* @returns Array of directory validation errors
*/
function validateTargetDirectories(config: AssetConfig): ValidationError[] {
const errors: ValidationError[] = [];
const targetDirs = new Set<string>();
// Collect all target directories
if (config.icon?.android?.target) targetDirs.add(config.icon.android.target);
if (config.icon?.ios?.target) targetDirs.add(config.icon.ios.target);
if (config.icon?.web?.target) targetDirs.add(config.icon.web.target);
if (config.splash?.android?.target) targetDirs.add(config.splash.android.target);
if (config.splash?.ios?.target) targetDirs.add(config.splash.ios.target);
// Check each target directory
targetDirs.forEach(dir => {
const dirPath = path.join(PROJECT_ROOT, dir);
const parentDir = path.dirname(dirPath);
if (!fs.existsSync(parentDir)) {
errors.push({ message: `Parent directory does not exist: ${parentDir}` });
} else if (!fs.statSync(parentDir).isDirectory()) {
errors.push({ message: `Parent path is not a directory: ${parentDir}` });
}
});
return errors;
}
/**
* Main validation function
* @param configPath - Path to configuration file
* @returns True if validation passes
*/
function validateConfiguration(configPath: string): boolean {
console.log('🔍 Validating TimeSafari asset configuration...');
console.log(`📁 Config file: ${configPath}`);
console.log(`📁 Project root: ${PROJECT_ROOT}`);
try {
// Load configuration
const config = loadJsonFile(configPath);
console.log('✅ Configuration file loaded successfully');
// Load schema
const schemaPath = path.join(PROJECT_ROOT, 'config', 'assets', 'schema.json');
const schema = loadJsonFile(schemaPath);
console.log('✅ Schema file loaded successfully');
// Perform validations
const schemaErrors = validateAgainstSchema(config);
const fileErrors = validateSourceFiles(config);
const dirErrors = validateTargetDirectories(config);
// Report results
const allErrors = [...schemaErrors, ...fileErrors, ...dirErrors];
if (allErrors.length === 0) {
console.log('🎉 All validations passed successfully!');
console.log('');
console.log('📋 Configuration summary:');
console.log(` Icon source: ${config.icon?.source || 'NOT SET'}`);
console.log(` Splash source: ${config.splash?.source || 'NOT SET'}`);
console.log(` Dark splash: ${config.splash?.darkSource || 'NOT SET'}`);
console.log(` Android adaptive: ${config.icon?.android?.adaptive ? 'ENABLED' : 'DISABLED'}`);
console.log(` iOS LaunchScreen: ${config.splash?.ios?.useStoryBoard ? 'ENABLED' : 'DISABLED'}`);
return true;
} else {
console.error('❌ Validation failed with the following errors:');
allErrors.forEach((error, index) => {
console.error(` ${index + 1}. ${error.message}`);
});
return false;
}
} catch (error) {
console.error('❌ Validation failed:', error instanceof Error ? error.message : String(error));
return false;
}
}
/**
* Main execution function
*/
function main(): void {
const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json');
if (!fs.existsSync(configPath)) {
console.error(`❌ Configuration file not found: ${configPath}`);
console.log('');
console.log('💡 Available options:');
console.log(' - Use default: capacitor-assets.config.json');
console.log(' - Specify path: tsx scripts/assets-validator.ts path/to/config.json');
console.log(' - Generate config: npm run assets:config');
process.exit(1);
}
const success = validateConfiguration(configPath);
process.exit(success ? 0 : 1);
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories, AssetConfig };

40
scripts/build-android.sh

@ -49,6 +49,31 @@ set -e
# Source common utilities # Source common utilities
source "$(dirname "$0")/common.sh" source "$(dirname "$0")/common.sh"
# Function to validate critical dependencies
validate_dependencies() {
log_info "Validating critical dependencies..."
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
log_error "node_modules directory not found. Please run 'npm install' first."
exit 1
fi
# Check if tsx is available
if [ ! -f "node_modules/.bin/tsx" ]; then
log_error "tsx dependency not found. Please run 'npm install' first."
exit 1
fi
# Check if capacitor-assets is available
if [ ! -f "node_modules/.bin/capacitor-assets" ]; then
log_error "capacitor-assets dependency not found. Please run 'npm install' first."
exit 1
fi
log_success "All critical dependencies validated successfully"
}
# Default values # Default values
BUILD_MODE="development" BUILD_MODE="development"
BUILD_TYPE="debug" BUILD_TYPE="debug"
@ -179,6 +204,11 @@ parse_android_args "$@"
# Print build header # Print build header
print_header "TimeSafari Android Build Process" print_header "TimeSafari Android Build Process"
# Validate dependencies before proceeding
validate_dependencies
# Log build start
log_info "Starting Android build process at $(date)" log_info "Starting Android build process at $(date)"
log_info "Build mode: $BUILD_MODE" log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE" log_info "Build type: $BUILD_TYPE"
@ -254,9 +284,10 @@ if [ "$DEPLOY_APP" = true ]; then
exit 0 exit 0
fi fi
# Step 1: Check and fix Android resources # Step 1: Validate asset configuration
safe_execute "Checking Android resources" "$(dirname "$0")/check-android-resources.sh" || { safe_execute "Validating asset configuration" "npm run assets:validate" || {
log_warning "Resource check found issues, but continuing with build..." log_warn "Asset validation found issues, but continuing with build..."
log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available."
} }
# Step 2: Clean Android app # Step 2: Clean Android app
@ -337,6 +368,9 @@ if [ "$OPEN_STUDIO" = true ]; then
log_info "Android Studio: opened" log_info "Android Studio: opened"
fi fi
# Reminder about dependency management
log_info "💡 Tip: If you encounter dependency issues, run 'npm install' to ensure all packages are up to date."
print_footer "Android Build" print_footer "Android Build"
# Exit with success # Exit with success

8
scripts/build-ios.sh

@ -173,20 +173,20 @@ check_ios_resources() {
# Check for required assets # Check for required assets
if [ ! -f "assets/icon.png" ]; then if [ ! -f "assets/icon.png" ]; then
log_warning "App icon not found at assets/icon.png" log_warn "App icon not found at assets/icon.png"
fi fi
if [ ! -f "assets/splash.png" ]; then if [ ! -f "assets/splash.png" ]; then
log_warning "Splash screen not found at assets/splash.png" log_warn "Splash screen not found at assets/splash.png"
fi fi
# Check for iOS-specific files # Check for iOS-specific files
if [ ! -f "ios/App/App/Info.plist" ]; then if [ ! -f "ios/App/App/Info.plist" ]; then
log_warning "Info.plist not found" log_warn "Info.plist not found"
fi fi
if [ ! -f "ios/App/App/AppDelegate.swift" ]; then if [ ! -f "ios/App/App/AppDelegate.swift" ]; then
log_warning "AppDelegate.swift not found" log_warn "AppDelegate.swift not found"
fi fi
log_success "iOS resource check completed" log_success "iOS resource check completed"

142
scripts/check-android-resources.sh

@ -1,142 +0,0 @@
#!/bin/bash
# TimeSafari Android Resource Check Script
# Checks for missing Android resources and automatically fixes common issues
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res"
ASSETS_DIR="$PROJECT_ROOT/assets"
echo "=== TimeSafari Android Resource Check ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Checking Android resources"
# Function to check if a file exists
check_file() {
local file="$1"
local description="$2"
if [ -f "$file" ]; then
echo "[✓] $description: $file"
return 0
else
echo "[✗] $description: $file (MISSING)"
return 1
fi
}
# Function to check if a directory exists and has files
check_directory() {
local dir="$1"
local description="$2"
if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then
echo "[✓] $description: $dir"
return 0
else
echo "[✗] $description: $dir (MISSING OR EMPTY)"
return 1
fi
}
# Track issues
issues_found=0
fixes_applied=0
echo "[INFO] Checking splash screen resources..."
# Check splash screen resources
if ! check_file "$ANDROID_RES_DIR/drawable/splash.png" "Splash screen (light)"; then
if [ -f "$ASSETS_DIR/splash.png" ]; then
echo "[FIX] Copying splash.png to Android resources..."
cp "$ASSETS_DIR/splash.png" "$ANDROID_RES_DIR/drawable/splash.png"
fixes_applied=$((fixes_applied + 1))
else
issues_found=$((issues_found + 1))
fi
fi
if ! check_file "$ANDROID_RES_DIR/drawable/splash_dark.png" "Splash screen (dark)"; then
if [ -f "$ASSETS_DIR/splash_dark.png" ]; then
echo "[FIX] Copying splash_dark.png to Android resources..."
cp "$ASSETS_DIR/splash_dark.png" "$ANDROID_RES_DIR/drawable/splash_dark.png"
fixes_applied=$((fixes_applied + 1))
else
issues_found=$((issues_found + 1))
fi
fi
echo "[INFO] Checking launcher icon resources..."
# Check launcher icon resources
required_icons=(
"mipmap-mdpi/ic_launcher.png"
"mipmap-hdpi/ic_launcher.png"
"mipmap-xhdpi/ic_launcher.png"
"mipmap-xxhdpi/ic_launcher.png"
"mipmap-xxxhdpi/ic_launcher.png"
"mipmap-anydpi-v26/ic_launcher.xml"
"mipmap-anydpi-v26/ic_launcher_round.xml"
)
missing_icons=0
for icon in "${required_icons[@]}"; do
if ! check_file "$ANDROID_RES_DIR/$icon" "Launcher icon: $icon"; then
missing_icons=$((missing_icons + 1))
fi
done
if [ $missing_icons -gt 0 ]; then
echo "[FIX] Missing launcher icons detected. Running icon generation script..."
if [ -f "$SCRIPT_DIR/generate-android-icons.sh" ]; then
"$SCRIPT_DIR/generate-android-icons.sh"
fixes_applied=$((fixes_applied + 1))
else
echo "[ERROR] Icon generation script not found: $SCRIPT_DIR/generate-android-icons.sh"
issues_found=$((issues_found + 1))
fi
fi
echo "[INFO] Checking Capacitor platform status..."
# Check if Android platform is properly initialized
if [ ! -d "$PROJECT_ROOT/android" ]; then
echo "[ERROR] Android platform directory not found"
issues_found=$((issues_found + 1))
elif [ ! -f "$PROJECT_ROOT/android/app/src/main/AndroidManifest.xml" ]; then
echo "[ERROR] AndroidManifest.xml not found - platform may be corrupted"
issues_found=$((issues_found + 1))
else
echo "[✓] Android platform appears to be properly initialized"
fi
# Check for common build issues
echo "[INFO] Checking for common build issues..."
# Check for invalid resource names (dashes in filenames)
invalid_resources=$(find "$ANDROID_RES_DIR" -name "*-*" -type f 2>/dev/null | grep -E '\.(png|jpg|jpeg|gif|xml)$' || true)
if [ -n "$invalid_resources" ]; then
echo "[WARNING] Found resources with invalid names (containing dashes):"
echo "$invalid_resources" | while read -r file; do
echo " - $file"
done
echo "[INFO] Android resource names must contain only lowercase a-z, 0-9, or underscore"
issues_found=$((issues_found + 1))
fi
# Summary
echo ""
echo "=== Resource Check Summary ==="
if [ $issues_found -eq 0 ] && [ $fixes_applied -eq 0 ]; then
echo "[SUCCESS] All Android resources are present and valid"
exit 0
elif [ $fixes_applied -gt 0 ]; then
echo "[SUCCESS] Fixed $fixes_applied resource issues automatically"
if [ $issues_found -gt 0 ]; then
echo "[WARNING] $issues_found issues remain that require manual attention"
exit 1
else
exit 0
fi
else
echo "[ERROR] Found $issues_found resource issues that require manual attention"
exit 1
fi

110
scripts/check-dependencies.sh

@ -0,0 +1,110 @@
#!/bin/bash
# check-dependencies.sh
# Author: Matthew Raymer
# Date: 2025-08-19
# Description: Dependency validation script for TimeSafari development environment
# This script checks for critical dependencies required for building the application.
# Exit on any error
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
print_header "TimeSafari Dependency Validation"
log_info "Checking development environment dependencies..."
# Check Node.js version
if command -v node &> /dev/null; then
NODE_VERSION=$(node --version)
log_info "Node.js version: $NODE_VERSION"
# Extract major version number
MAJOR_VERSION=$(echo $NODE_VERSION | sed 's/v\([0-9]*\)\..*/\1/')
if [ "$MAJOR_VERSION" -lt 18 ]; then
log_error "Node.js version $NODE_VERSION is too old. Please upgrade to Node.js 18 or later."
exit 1
fi
else
log_error "Node.js is not installed. Please install Node.js 18 or later."
exit 1
fi
# Check npm version
if command -v npm &> /dev/null; then
NPM_VERSION=$(npm --version)
log_info "npm version: $NPM_VERSION"
else
log_error "npm is not installed. Please install npm."
exit 1
fi
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
log_error "node_modules directory not found."
log_info "Please run: npm install"
exit 1
fi
# Check critical dependencies
log_info "Validating critical packages..."
CRITICAL_DEPS=("tsx" "capacitor-assets" "vite")
for dep in "${CRITICAL_DEPS[@]}"; do
if [ -f "node_modules/.bin/$dep" ]; then
log_success "$dep found"
else
log_error "$dep not found in node_modules/.bin"
log_info "This usually means the package wasn't installed properly."
log_info "Try running: npm install"
exit 1
fi
done
# Check TypeScript via npx
if npx tsc --version &> /dev/null; then
TSC_VERSION=$(npx tsc --version)
log_success "✓ TypeScript found: $TSC_VERSION"
else
log_error "✗ TypeScript not accessible via npx"
log_info "Try running: npm install"
exit 1
fi
# Check Capacitor CLI
if command -v npx &> /dev/null; then
if npx cap --version &> /dev/null; then
CAP_VERSION=$(npx cap --version)
log_success "✓ Capacitor CLI version: $CAP_VERSION"
else
log_error "✗ Capacitor CLI not accessible via npx"
log_info "Try running: npm install @capacitor/cli"
exit 1
fi
else
log_error "npx is not available. Please ensure npm is properly installed."
exit 1
fi
# Check Android development tools
if command -v adb &> /dev/null; then
log_success "✓ Android Debug Bridge (adb) found"
else
log_warn "⚠ Android Debug Bridge (adb) not found"
log_info "This is only needed for Android development and testing."
fi
if command -v gradle &> /dev/null; then
GRADLE_VERSION=$(gradle --version | head -n 1)
log_success "✓ Gradle found: $GRADLE_VERSION"
else
log_warn "⚠ Gradle not found in PATH"
log_info "This is only needed if building outside of Android Studio."
fi
log_success "Dependency validation completed successfully!"
log_info "Your development environment is ready for TimeSafari development."
print_footer "Dependency Validation"

62
scripts/clean-android.sh

@ -0,0 +1,62 @@
#!/bin/bash
# clean-android.sh
# Author: Matthew Raymer
# Date: 2025-08-19
# Description: Clean Android app with timeout protection to prevent hanging
# This script safely uninstalls the TimeSafari app from connected Android devices
# with a 30-second timeout to prevent indefinite hanging.
# Exit on any error
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Function to implement timeout for systems without timeout command
timeout_command() {
local timeout_seconds="$1"
shift
# Check if timeout command exists
if command -v timeout &> /dev/null; then
timeout "$timeout_seconds" "$@"
else
# Fallback for systems without timeout (like macOS)
# Use perl to implement timeout
perl -e '
eval {
local $SIG{ALRM} = sub { die "timeout" };
alarm shift;
system @ARGV;
alarm 0;
};
if ($@) { exit 1; }
' "$timeout_seconds" "$@"
fi
}
log_info "Starting Android cleanup process..."
# Check if adb is available
if ! command -v adb &> /dev/null; then
log_error "adb command not found. Please install Android SDK Platform Tools."
exit 1
fi
# Check for connected devices
log_info "Checking for connected Android devices..."
if adb devices | grep -q 'device$'; then
log_info "Android device(s) found. Attempting to uninstall app..."
# Try to uninstall with timeout
if timeout_command 30 adb uninstall app.timesafari.app; then
log_success "Successfully uninstalled TimeSafari app"
else
log_warn "Uninstall failed or timed out after 30 seconds"
log_info "This is normal if the app wasn't installed or device is unresponsive"
fi
else
log_info "No Android devices connected. Skipping uninstall."
fi
log_success "Android cleanup process completed"

96
scripts/generate-android-icons.sh

@ -1,96 +0,0 @@
#!/bin/bash
# TimeSafari Android Icon Generation Script
# Generates all required Android launcher icon sizes from assets/icon.png
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ASSETS_DIR="$PROJECT_ROOT/assets"
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res"
echo "=== TimeSafari Android Icon Generation ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting Android icon generation"
# Check if source icon exists
if [ ! -f "$ASSETS_DIR/icon.png" ]; then
echo "[ERROR] Source icon not found: $ASSETS_DIR/icon.png"
exit 1
fi
# Check if ImageMagick is available
if ! command -v convert &> /dev/null; then
echo "[ERROR] ImageMagick (convert) not found. Please install ImageMagick."
echo " Arch: sudo pacman -S imagemagick"
echo " Ubuntu: sudo apt-get install imagemagick"
echo " macOS: brew install imagemagick"
exit 1
fi
# Create mipmap directories if they don't exist
mkdir -p "$ANDROID_RES_DIR/mipmap-hdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-mdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xhdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xxhdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xxxhdpi"
echo "[INFO] Generating launcher icons..."
# Generate launcher icons for different densities
# Android launcher icon sizes: mdpi=48, hdpi=72, xhdpi=96, xxhdpi=144, xxxhdpi=192
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png"
# Generate round launcher icons
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png"
# Create background and foreground mipmap files
# These reference the existing drawable files
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_background.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
</adaptive-icon>
EOF
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_foreground.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
EOF
# Update the existing launcher XML files to reference the correct resources
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
EOF
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_round.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
EOF
echo "[SUCCESS] Generated Android launcher icons:"
echo " - mipmap-mdpi/ic_launcher.png (48x48)"
echo " - mipmap-hdpi/ic_launcher.png (72x72)"
echo " - mipmap-xhdpi/ic_launcher.png (96x96)"
echo " - mipmap-xxhdpi/ic_launcher.png (144x144)"
echo " - mipmap-xxxhdpi/ic_launcher.png (192x192)"
echo " - Updated mipmap-anydpi-v26 XML files"
echo "[SUCCESS] Android icon generation completed successfully!"

22
scripts/generate-icons.sh

@ -1,22 +0,0 @@
#!/bin/bash
# Create directories if they don't exist
mkdir -p android/app/src/main/res/mipmap-mdpi
mkdir -p android/app/src/main/res/mipmap-hdpi
mkdir -p android/app/src/main/res/mipmap-xhdpi
mkdir -p android/app/src/main/res/mipmap-xxhdpi
mkdir -p android/app/src/main/res/mipmap-xxxhdpi
# Generate placeholder icons using ImageMagick
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
# Copy to round versions
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

103
scripts/git-hooks/README.md

@ -0,0 +1,103 @@
# TimeSafari Git Hooks
This directory contains custom Git hooks for the TimeSafari project.
## Debug Code Checker Hook
### Overview
The `pre-commit` hook automatically checks for debug code when committing to protected branches (master, main, production, release). This prevents debug statements from accidentally reaching production code.
### How It Works
1. **Branch Detection**: Only runs on protected branches (configurable)
2. **File Filtering**: Automatically skips test files, scripts, and documentation
3. **Pattern Matching**: Detects common debug patterns using regex
4. **Commit Prevention**: Blocks commits containing debug code
### Protected Branches (Default)
- `master`
- `main`
- `production`
- `release`
- `stable`
### Debug Patterns Detected
- **Console statements**: `console.log`, `console.debug`, `console.error`
- **Template debug**: `Debug:`, `debug:` in Vue templates
- **Debug constants**: `DEBUG_`, `debug_` variables
- **HTML debug**: `<!-- debug` comments
- **Debug attributes**: `debug="true"` attributes
- **Vue debug**: `v-if="debug"`, `v-show="debug"`
- **Debug TODOs**: `TODO debug`, `FIXME debug`
### Files Automatically Skipped
- Test files: `*.test.js`, `*.spec.ts`, `*.test.vue`
- Scripts: `scripts/` directory
- Test directories: `test-*` directories
- Documentation: `docs/`, `*.md`, `*.txt`
- Config files: `*.json`, `*.yml`, `*.yaml`
- IDE files: `.cursor/` directory
### Configuration
Edit `.git/hooks/debug-checker.config` to customize:
- Protected branches
- Debug patterns
- Skip patterns
- Logging level
### Testing the Hook
Run the test script to verify the hook works:
```bash
./scripts/test-debug-hook.sh
```
### Manual Testing
1. Make changes to a file with debug code
2. Stage the file: `git add <filename>`
3. Try to commit: `git commit -m 'test'`
4. Hook should prevent commit if debug code is found
### Bypassing the Hook (Emergency)
If you absolutely need to commit debug code to a protected branch:
```bash
git commit --no-verify -m "emergency: debug code needed"
```
⚠️ **Warning**: This bypasses all pre-commit hooks. Use sparingly and only in emergencies.
### Troubleshooting
#### Hook not running
- Ensure the hook is executable: `chmod +x .git/hooks/pre-commit`
- Check if you're on a protected branch
- Verify the hook file exists and has correct permissions
#### False positives
- Add legitimate debug patterns to skip patterns in config
- Use proper logging levels (`logger.info`, `logger.debug`) instead of console
- Move debug code to feature branches first
#### Hook too strict
- Modify debug patterns in config file
- Add more file types to skip patterns
- Adjust protected branch list
### Best Practices
1. **Use feature branches** for development with debug code
2. **Use proper logging** instead of console statements
3. **Test thoroughly** before merging to protected branches
4. **Review commits** to ensure no debug code slips through
5. **Keep config updated** as project needs change
### Integration with CI/CD
This hook works locally. For CI/CD pipelines, consider:
- Running the same checks in your build process
- Adding ESLint rules for console statements
- Using TypeScript strict mode
- Adding debug code detection to PR checks
### Support
If you encounter issues:
1. Check the hook output for specific error messages
2. Verify your branch is in the protected list
3. Review the configuration file
4. Test with the provided test script
5. Check file permissions and git setup

86
scripts/git-hooks/debug-checker.config

@ -0,0 +1,86 @@
# TimeSafari Debug Checker Configuration
# Edit this file to customize protected branches and debug patterns
# Protected branches where debug code checking is enforced
# Add or remove branches as needed
PROTECTED_BRANCHES=(
"master"
"main"
"production"
"release"
"stable"
)
# Debug patterns to detect (regex patterns)
# Add or remove patterns as needed
DEBUG_PATTERNS=(
# Console statements
"console\."
# Template debug text
"Debug:"
"debug:"
# Debug constants and variables
"DEBUG_"
"debug_"
# HTML debug comments
"<!-- debug"
# Debug attributes
"debug.*="
# Vue debug patterns
"v-if.*debug"
"v-show.*debug"
# Common debug text
"TODO.*debug"
"FIXME.*debug"
# Debug imports (uncomment if you want to catch these)
# "import.*debug"
# "require.*debug"
)
# Files and directories to skip during checking
# Add patterns to exclude from debug checking
SKIP_PATTERNS=(
"\.(test|spec)\.(js|ts|vue)$" # Test files (must have .test. or .spec.)
"^scripts/" # Scripts directory
"^test-.*/" # Test directories (must end with /)
"^\.git/" # Git directory
"^node_modules/" # Dependencies
"^docs/" # Documentation
"^\.cursor/" # Cursor IDE files
"\.md$" # Markdown files
"\.txt$" # Text files
"\.json$" # JSON config files
"\.yml$" # YAML config files
"\.yaml$" # YAML config files
)
# Files that are whitelisted for console statements
# These files may contain intentional console.log statements that are
# properly whitelisted with eslint-disable-next-line no-console comments
WHITELIST_FILES=(
"src/services/platforms/WebPlatformService.ts" # Worker context logging
"src/services/platforms/CapacitorPlatformService.ts" # Platform-specific logging
"src/services/platforms/ElectronPlatformService.ts" # Electron-specific logging
"src/services/QRScanner/.*" # QR Scanner services
"src/utils/logger.ts" # Logger utility itself
"src/utils/LogCollector.ts" # Log collection utilities
"scripts/.*" # Build and utility scripts
"test-.*/.*" # Test directories
".*\.test\..*" # Test files
".*\.spec\..*" # Spec files
)
# Logging level (debug, info, warn, error)
LOG_LEVEL="info"
# Exit codes
EXIT_SUCCESS=0
EXIT_DEBUG_FOUND=1
EXIT_ERROR=2

252
scripts/git-hooks/pre-commit

@ -0,0 +1,252 @@
#!/bin/bash
# TimeSafari Pre-commit Hook - Debug Code Checker
# Only runs on master or specified branches to catch debug code before it reaches production
# Hook directory
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$HOOK_DIR/debug-checker.config"
# Default configuration (fallback if config file is missing)
DEFAULT_PROTECTED_BRANCHES=("master" "main" "production" "release")
DEFAULT_DEBUG_PATTERNS=(
"console\."
"Debug:"
"debug:"
"DEBUG_"
"debug_"
"<!-- debug"
"debug.*="
)
DEFAULT_WHITELIST_FILES=(
"src/services/platforms/WebPlatformService.ts"
"src/services/platforms/CapacitorPlatformService.ts"
"src/services/platforms/ElectronPlatformService.ts"
)
# Load configuration from file if it exists
load_config() {
if [[ -f "$CONFIG_FILE" ]]; then
# Source the config file to load variables
# We'll use a safer approach by reading and parsing
PROTECTED_BRANCHES=()
DEBUG_PATTERNS=()
SKIP_PATTERNS=()
WHITELIST_FILES=()
# Read protected branches
while IFS= read -r line; do
if [[ "$line" =~ ^PROTECTED_BRANCHES=\( ]]; then
# Start reading array
while IFS= read -r line; do
if [[ "$line" =~ ^\)$ ]]; then
break
fi
if [[ "$line" =~ \"([^\"]+)\" ]]; then
PROTECTED_BRANCHES+=("${BASH_REMATCH[1]}")
fi
done
fi
done < "$CONFIG_FILE"
# Read debug patterns
while IFS= read -r line; do
if [[ "$line" =~ ^DEBUG_PATTERNS=\( ]]; then
while IFS= read -r line; do
if [[ "$line" =~ ^\)$ ]]; then
break
fi
if [[ "$line" =~ \"([^\"]+)\" ]]; then
DEBUG_PATTERNS+=("${BASH_REMATCH[1]}")
fi
done
fi
done < "$CONFIG_FILE"
# Read skip patterns
while IFS= read -r line; do
if [[ "$line" =~ ^SKIP_PATTERNS=\( ]]; then
while IFS= read -r line; do
if [[ "$line" =~ ^\)$ ]]; then
break
fi
if [[ "$line" =~ \"([^\"]+)\" ]]; then
SKIP_PATTERNS+=("${BASH_REMATCH[1]}")
fi
done
fi
done < "$CONFIG_FILE"
# Read whitelist files
while IFS= read -r line; do
if [[ "$line" =~ ^WHITELIST_FILES=\( ]]; then
while IFS= read -r line; do
if [[ "$line" =~ ^\)$ ]]; then
break
fi
if [[ "$line" =~ \"([^\"]+)\" ]]; then
WHITELIST_FILES+=("${BASH_REMATCH[1]}")
fi
done
fi
done < "$CONFIG_FILE"
fi
# Use defaults if config loading failed
if [[ ${#PROTECTED_BRANCHES[@]} -eq 0 ]]; then
PROTECTED_BRANCHES=("${DEFAULT_PROTECTED_BRANCHES[@]}")
fi
if [[ ${#DEBUG_PATTERNS[@]} -eq 0 ]]; then
DEBUG_PATTERNS=("${DEFAULT_DEBUG_PATTERNS[@]}")
fi
if [[ ${#SKIP_PATTERNS[@]} -eq 0 ]]; then
SKIP_PATTERNS=("${DEFAULT_SKIP_PATTERNS[@]}")
fi
if [[ ${#WHITELIST_FILES[@]} -eq 0 ]]; then
WHITELIST_FILES=("${DEFAULT_WHITELIST_FILES[@]}")
fi
}
# Check if current branch is protected
is_protected_branch() {
local branch="$1"
for protected in "${PROTECTED_BRANCHES[@]}"; do
if [[ "$branch" == "$protected" ]]; then
return 0
fi
done
return 1
}
# Check if file should be skipped
should_skip_file() {
local file="$1"
for pattern in "${SKIP_PATTERNS[@]}"; do
if [[ "$file" =~ $pattern ]]; then
return 0
fi
done
return 1
}
# Check if file is whitelisted for console statements
is_whitelisted_file() {
local file="$1"
for whitelisted in "${WHITELIST_FILES[@]}"; do
if [[ "$file" =~ $whitelisted ]]; then
return 0
fi
done
return 1
}
# Main execution
main() {
# Load configuration
load_config
# Get current branch name
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
if [[ -z "$CURRENT_BRANCH" ]]; then
echo "⚠️ Could not determine current branch, skipping debug check"
exit 0
fi
# Check if we should run the hook
if ! is_protected_branch "$CURRENT_BRANCH"; then
echo "🔒 Pre-commit hook skipped - not on protected branch ($CURRENT_BRANCH)"
echo " Protected branches: ${PROTECTED_BRANCHES[*]}"
exit 0
fi
echo "🔍 Running debug code check on protected branch: $CURRENT_BRANCH"
echo " Using config: $CONFIG_FILE"
# Get all staged files (modified, added, copied, merged)
ALL_STAGED_FILES=$(git diff --cached --name-only)
if [ -z "$ALL_STAGED_FILES" ]; then
echo "✅ No staged files to check"
exit 0
fi
# Initialize error tracking
ERRORS_FOUND=0
ERROR_MESSAGES=()
FILES_CHECKED=0
# Check each staged file for debug patterns
for file in $ALL_STAGED_FILES; do
# Skip files that should be ignored
if should_skip_file "$file"; then
continue
fi
FILES_CHECKED=$((FILES_CHECKED + 1))
# Check for debug patterns in the file
for pattern in "${DEBUG_PATTERNS[@]}"; do
# Skip console pattern checks for whitelisted files
if [[ "$pattern" == "console\." ]] && is_whitelisted_file "$file"; then
continue
fi
# For new files, check the file content directly
# For modified files, check the staged diff
if [[ -f "$file" ]]; then
# New file - check content directly
if grep -E "$pattern" "$file" > /dev/null; then
ERRORS_FOUND=$((ERRORS_FOUND + 1))
ERROR_MESSAGES+=("🚨 $file: Found debug pattern '$pattern'")
fi
else
# Modified file - check staged diff
if git diff --cached "$file" | grep -E "$pattern" > /dev/null; then
ERRORS_FOUND=$((ERRORS_FOUND + 1))
ERROR_MESSAGES+=("🚨 $file: Found debug pattern '$pattern'")
fi
fi
done
done
# Report results
if [ $ERRORS_FOUND -gt 0 ]; then
echo ""
echo "❌ Debug code detected in staged files!"
echo " Branch: $CURRENT_BRANCH"
echo " Files checked: $FILES_CHECKED"
echo " Errors found: $ERRORS_FOUND"
echo ""
for msg in "${ERROR_MESSAGES[@]}"; do
echo " $msg"
done
echo ""
echo "💡 Please remove debug code before committing to $CURRENT_BRANCH"
echo " Common debug patterns to check:"
echo " - console.log, console.debug, console.error"
echo " - Debug: or debug: in templates"
echo " - DEBUG_ constants"
echo " - HTML comments with debug"
echo ""
echo " If debug code is intentional, consider:"
echo " - Moving to a feature branch first"
echo " - Using proper logging levels (logger.info, logger.debug)"
echo " - Adding debug code to .gitignore or .debugignore"
echo ""
echo " Configuration file: $CONFIG_FILE"
exit 1
else
echo "✅ No debug code found in $FILES_CHECKED staged files"
exit 0
fi
}
# Run main function
main "$@"

171
scripts/install-debug-hook.sh

@ -0,0 +1,171 @@
#!/bin/bash
# TimeSafari Debug Hook Installer
# Run this script in any repository to install the debug pre-commit hook
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}🔧 TimeSafari Debug Hook Installer${NC}"
echo "============================================="
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo -e "${RED}❌ Error: Not in a git repository${NC}"
echo "Please run this script from within a git repository"
exit 1
fi
# Get repository root
REPO_ROOT=$(git rev-parse --show-toplevel)
HOOKS_DIR="$REPO_ROOT/.git/hooks"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo -e "${BLUE}Repository:${NC} $REPO_ROOT"
echo -e "${BLUE}Hooks directory:${NC} $HOOKS_DIR"
echo -e "${BLUE}Script directory:${NC} $SCRIPT_DIR"
# Check if hooks directory exists
if [[ ! -d "$HOOKS_DIR" ]]; then
echo -e "${RED}❌ Error: Hooks directory not found${NC}"
echo "This repository may not be properly initialized"
exit 1
fi
# Check if we have the hook files in the repository
HOOK_SCRIPT="$SCRIPT_DIR/git-hooks/pre-commit"
CONFIG_FILE="$SCRIPT_DIR/git-hooks/debug-checker.config"
if [[ ! -f "$HOOK_SCRIPT" ]]; then
echo -e "${RED}❌ Error: Pre-commit hook script not found${NC}"
echo "Expected location: $HOOK_SCRIPT"
echo "Make sure you're running this from the TimeSafari repository"
exit 1
fi
if [[ ! -f "$CONFIG_FILE" ]]; then
echo -e "${RED}❌ Error: Debug checker config not found${NC}"
echo "Expected location: $CONFIG_FILE"
echo "Make sure you're running this from the TimeSafari repository"
exit 1
fi
# Check if already installed
if [[ -f "$HOOKS_DIR/pre-commit" && -f "$HOOKS_DIR/debug-checker.config" ]]; then
echo -e "${YELLOW}⚠️ Debug hook already appears to be installed${NC}"
echo -e " Checking if update is needed..."
# Check if files are different
if diff "$HOOK_SCRIPT" "$HOOKS_DIR/pre-commit" > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} Hook script is up to date"
HOOK_UP_TO_DATE=true
else
echo -e " ${YELLOW}⚠️ Hook script differs - will update${NC}"
HOOK_UP_TO_DATE=false
fi
if diff "$CONFIG_FILE" "$HOOKS_DIR/debug-checker.config" > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} Config file is up to date"
CONFIG_UP_TO_DATE=true
else
echo -e " ${YELLOW}⚠️ Config file differs - will update${NC}"
CONFIG_UP_TO_DATE=false
fi
if [[ "$HOOK_UP_TO_DATE" == true && "$CONFIG_UP_TO_DATE" == true ]]; then
echo -e "\n${GREEN}✅ Debug hook is already up to date!${NC}"
echo -e " No installation needed"
else
echo -e "\n${BLUE}Updating existing installation...${NC}"
fi
else
echo -e "\n${BLUE}Installing debug hook...${NC}"
fi
# Copy/update the hook script if needed
if [[ "$HOOK_UP_TO_DATE" != true ]]; then
cp "$HOOK_SCRIPT" "$HOOKS_DIR/pre-commit"
chmod +x "$HOOKS_DIR/pre-commit"
echo -e " ${GREEN}${NC} Pre-commit hook installed/updated"
fi
# Copy/update the config file if needed
if [[ "$CONFIG_UP_TO_DATE" != true ]]; then
cp "$CONFIG_FILE" "$HOOKS_DIR/debug-checker.config"
echo -e " ${GREEN}${NC} Configuration file installed/updated"
fi
# Copy/update the README if needed
README_FILE="$SCRIPT_DIR/git-hooks/README.md"
if [[ -f "$README_FILE" ]]; then
if [[ ! -f "$HOOKS_DIR/README.md" ]] || ! diff "$README_FILE" "$HOOKS_DIR/README.md" > /dev/null 2>&1; then
cp "$README_FILE" "$HOOKS_DIR/README.md"
echo -e " ${GREEN}${NC} Documentation installed/updated"
else
echo -e " ${GREEN}${NC} Documentation is up to date"
fi
fi
echo -e "\n${GREEN}🎉 Debug hook installation complete!${NC}"
# Test the installation
echo -e "\n${BLUE}Testing installation...${NC}"
if [[ -x "$HOOKS_DIR/pre-commit" ]]; then
echo -e " ${GREEN}${NC} Hook is executable"
else
echo -e " ${RED}${NC} Hook is not executable"
fi
if [[ -f "$HOOKS_DIR/debug-checker.config" ]]; then
echo -e " ${GREEN}${NC} Config file exists"
else
echo -e " ${RED}${NC} Config file missing"
fi
# Show current branch status
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")
echo -e "\n${BLUE}Current branch:${NC} $CURRENT_BRANCH"
# Check if this is a protected branch
PROTECTED_BRANCHES=("master" "main" "production" "release" "stable")
IS_PROTECTED=false
for branch in "${PROTECTED_BRANCHES[@]}"; do
if [[ "$CURRENT_BRANCH" == "$branch" ]]; then
IS_PROTECTED=true
break
fi
done
if [[ "$IS_PROTECTED" == true ]]; then
echo -e "${YELLOW}⚠️ You're on a protected branch ($CURRENT_BRANCH)${NC}"
echo -e " The debug hook will now run on all commits to this branch"
echo -e " Consider switching to a feature branch for development"
else
echo -e "${GREEN}✅ You're on a feature branch ($CURRENT_BRANCH)${NC}"
echo -e " The debug hook will be skipped on this branch"
echo -e " You can develop with debug code freely"
fi
echo -e "\n${BLUE}Next steps:${NC}"
echo "1. The hook will now run automatically on protected branches"
echo "2. Test it by trying to commit a file with debug code"
echo "3. Use feature branches for development with debug code"
echo "4. Check the README.md in .git/hooks/ for more information"
echo -e "\n${BLUE}To test the hook:${NC}"
echo "1. Create a test file with debug code (e.g., console.log('test'))"
echo "2. Stage it: git add <filename>"
echo "3. Try to commit: git commit -m 'test'"
echo "4. The hook should prevent the commit if debug code is found"
echo -e "\n${BLUE}To uninstall:${NC}"
echo "rm $HOOKS_DIR/pre-commit"
echo "rm $HOOKS_DIR/debug-checker.config"
echo "rm $HOOKS_DIR/README.md"

67
scripts/purge-generated-assets.sh

@ -1,67 +0,0 @@
#!/bin/bash
# TimeSafari Generated Assets Purge Script
# Removes generated Android assets and resources from git history
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
echo "=== TimeSafari Generated Assets Purge ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting git history cleanup"
# Check if we're in a git repository
if [ ! -d ".git" ]; then
echo "[ERROR] Not in a git repository. Please run this script from the project root."
exit 1
fi
# Check if we have uncommitted changes
if [ -n "$(git status --porcelain)" ]; then
echo "[ERROR] You have uncommitted changes. Please commit or stash them first."
echo "Current status:"
git status --short
exit 1
fi
# Create backup branch
BACKUP_BRANCH="backup-before-asset-purge-$(date +%Y%m%d-%H%M%S)"
echo "[INFO] Creating backup branch: $BACKUP_BRANCH"
git branch "$BACKUP_BRANCH"
echo "[INFO] Starting git filter-branch to remove generated assets..."
# Use git filter-branch to remove the directories from history
git filter-branch --force --index-filter '
# Remove generated Android assets directory
git rm -rf --cached --ignore-unmatch android/app/src/main/assets/public/ 2>/dev/null || true
# Remove generated Android resources (but keep config files)
git rm -rf --cached --ignore-unmatch android/app/src/main/res/drawable*/ 2>/dev/null || true
git rm -rf --cached --ignore-unmatch android/app/src/main/res/mipmap*/ 2>/dev/null || true
git rm -rf --cached --ignore-unmatch android/app/src/main/res/values/ic_launcher_background.xml 2>/dev/null || true
# Keep configuration files
git add android/app/src/main/res/values/strings.xml 2>/dev/null || true
git add android/app/src/main/res/values/styles.xml 2>/dev/null || true
git add android/app/src/main/res/layout/activity_main.xml 2>/dev/null || true
git add android/app/src/main/res/xml/config.xml 2>/dev/null || true
git add android/app/src/main/res/xml/file_paths.xml 2>/dev/null || true
' --prune-empty --tag-name-filter cat -- --all
echo "[INFO] Cleaning up git filter-branch temporary files..."
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now --aggressive
echo "[SUCCESS] Generated assets purged from git history!"
echo "[INFO] Backup branch created: $BACKUP_BRANCH"
echo "[INFO] Repository size should be significantly reduced"
echo ""
echo "Next steps:"
echo "1. Test that the repository works correctly"
echo "2. Force push to remote: git push --force-with-lease origin <branch>"
echo "3. Inform team members to re-clone or reset their local repositories"
echo "4. Delete backup branch when confident: git branch -D $BACKUP_BRANCH"

117
scripts/test-debug-hook.sh

@ -0,0 +1,117 @@
#!/bin/bash
# Test script for the debug pre-commit hook
# This script helps verify that the hook is working correctly
set -e
echo "🧪 Testing TimeSafari Debug Pre-commit Hook"
echo "============================================="
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Test directory
TEST_DIR="$(mktemp -d)"
echo -e "${BLUE}Created test directory: $TEST_DIR${NC}"
# Function to cleanup
cleanup() {
echo -e "${YELLOW}Cleaning up test directory...${NC}"
rm -rf "$TEST_DIR"
}
# Set trap to cleanup on exit
trap cleanup EXIT
# Function to run test
run_test() {
local test_name="$1"
local test_file="$2"
local expected_exit="$3"
echo -e "\n${BLUE}Running test: $test_name${NC}"
# Create test file
echo "$test_file" > "$TEST_DIR/test.vue"
# Stage the file
cd "$TEST_DIR"
git init > /dev/null 2>&1
git add test.vue > /dev/null 2>&1
# Run the hook
if bash ../../.git/hooks/pre-commit > hook_output.txt 2>&1; then
exit_code=0
else
exit_code=$?
fi
# Check result
if [[ $exit_code -eq $expected_exit ]]; then
echo -e " ${GREEN}✅ PASS${NC} - Exit code: $exit_code (expected: $expected_exit)"
else
echo -e " ${RED}❌ FAIL${NC} - Exit code: $exit_code (expected: $expected_exit)"
echo -e " ${YELLOW}Hook output:${NC}"
cat hook_output.txt
fi
# Cleanup git
rm -rf .git
rm -f hook_output.txt
}
# Test cases
echo -e "\n${BLUE}Test Case 1: Clean file (should pass)${NC}"
run_test "Clean file" "// No debug code here" 0
echo -e "\n${BLUE}Test Case 2: Console statement (should fail)${NC}"
run_test "Console statement" "console.log('debug info')" 1
echo -e "\n${BLUE}Test Case 3: Debug template (should fail)${NC}"
run_test "Debug template" "Debug: {{ isMapReady ? 'Map Ready' : 'Map Loading' }}" 1
echo -e "\n${BLUE}Test Case 4: Debug constant (should fail)${NC}"
run_test "Debug constant" "const DEBUG_MODE = true" 1
echo -e "\n${BLUE}Test Case 5: Mixed content (should fail)${NC}"
run_test "Mixed content" "// Some normal code\nconsole.debug('test')\n// More normal code" 1
echo -e "\n${BLUE}Test Case 6: HTML debug comment (should fail)${NC}"
run_test "HTML debug comment" "<!-- debug: this is debug info -->" 1
echo -e "\n${BLUE}Test Case 7: Debug attribute (should fail)${NC}"
run_test "Debug attribute" "<div debug='true'>content</div>" 1
echo -e "\n${BLUE}Test Case 8: Test file (should be skipped)${NC}"
run_test "Test file" "console.log('this should be skipped')" 0
# Test branch detection
echo -e "\n${BLUE}Testing branch detection...${NC}"
cd "$TEST_DIR"
git init > /dev/null 2>&1
git checkout -b feature-branch > /dev/null 2>&1
echo "console.log('debug')" > test.vue
git add test.vue > /dev/null 2>&1
if bash ../../.git/hooks/pre-commit > hook_output.txt 2>&1; then
echo -e " ${GREEN}✅ PASS${NC} - Hook skipped on feature branch"
else
echo -e " ${RED}❌ FAIL${NC} - Hook should have been skipped on feature branch"
echo -e " ${YELLOW}Hook output:${NC}"
cat hook_output.txt
fi
rm -rf .git
rm -f hook_output.txt
echo -e "\n${GREEN}🎉 All tests completed!${NC}"
echo -e "\n${BLUE}To test manually:${NC}"
echo "1. Make changes to a file with debug code"
echo "2. Stage the file: git add <filename>"
echo "3. Try to commit: git commit -m 'test'"
echo "4. The hook should prevent the commit if debug code is found"

103
scripts/type-safety-check.sh

@ -0,0 +1,103 @@
#!/bin/bash
# Type Safety Pre-commit Check Script
# This script ensures type safety before commits by running linting and type checking
set -e
echo "🔍 Running Type Safety Pre-commit Checks..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
# Check if we're in the right directory
if [ ! -f "package.json" ]; then
print_error "Must run from project root directory"
exit 1
fi
# Step 1: Run ESLint with TypeScript rules
print_status "Running ESLint TypeScript checks..."
if npm run lint > /dev/null 2>&1; then
print_status "ESLint passed - no type safety issues found"
else
print_error "ESLint failed - type safety issues detected"
echo ""
echo "Running lint with details..."
npm run lint
echo ""
print_error "Please fix the above type safety issues before committing"
exit 1
fi
# Step 2: Run TypeScript type checking
print_status "Running TypeScript type checking..."
if npm run type-check > /dev/null 2>&1; then
print_status "TypeScript compilation passed"
else
print_error "TypeScript compilation failed"
echo ""
echo "Running type check with details..."
npm run type-check
echo ""
print_error "Please fix the above TypeScript errors before committing"
exit 1
fi
# Step 3: Check for any remaining 'any' types
print_status "Scanning for any remaining 'any' types..."
ANY_COUNT=$(grep -r "any" src/ --include="*.ts" --include="*.vue" | grep -v "// eslint-disable" | grep -v "eslint-disable-next-line" | wc -l)
if [ "$ANY_COUNT" -eq 0 ]; then
print_status "No 'any' types found in source code"
else
print_warning "Found $ANY_COUNT instances of 'any' type usage"
echo ""
echo "Instances found:"
grep -r "any" src/ --include="*.ts" --include="*.vue" | grep -v "// eslint-disable" | grep -v "eslint-disable-next-line" || true
echo ""
print_error "Please replace 'any' types with proper TypeScript types before committing"
exit 1
fi
# Step 4: Verify database migration status
print_status "Checking database migration status..."
if grep -r "databaseUtil" src/ --include="*.ts" --include="*.vue" > /dev/null 2>&1; then
print_warning "Found databaseUtil imports - ensure migration is complete"
echo ""
echo "Files with databaseUtil imports:"
grep -r "databaseUtil" src/ --include="*.ts" --include="*.vue" | head -5 || true
echo ""
print_warning "Consider completing database migration to PlatformServiceMixin"
else
print_status "No databaseUtil imports found - migration appears complete"
fi
# All checks passed
echo ""
print_status "All type safety checks passed! 🎉"
print_status "Your code is ready for commit"
echo ""
echo "📚 Remember to follow the Type Safety Guidelines:"
echo " - docs/typescript-type-safety-guidelines.md"
echo " - Use proper error handling patterns"
echo " - Leverage existing type definitions"
echo " - Run 'npm run lint-fix' for automatic fixes"
exit 0

75
src/assets/icons.json

@ -1,75 +0,0 @@
{
"warning": {
"fillRule": "evenodd",
"d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z",
"clipRule": "evenodd"
},
"spinner": {
"d": "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
},
"chart": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
},
"plus": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M12 4v16m8-8H4"
},
"settings": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
},
"settingsDot": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M15 12a3 3 0 11-6 0 3 3 0 016 0z"
},
"lock": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
},
"download": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
},
"check": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
},
"edit": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
},
"trash": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
},
"plusCircle": {
"strokeLinecap": "round",
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M12 6v6m0 0v6m0-6h6m-6 0H6"
},
"info": {
"fillRule": "evenodd",
"d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z",
"clipRule": "evenodd"
}
}

6
src/components/ActivityListItem.vue

@ -288,8 +288,7 @@ export default class ActivityListItem extends Vue {
} }
get fetchAmount(): string { get fetchAmount(): string {
const claim = const claim = this.record.fullClaim;
(this.record.fullClaim as any)?.claim || this.record.fullClaim;
const amount = claim.object?.amountOfThisGood const amount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
@ -299,8 +298,7 @@ export default class ActivityListItem extends Vue {
} }
get description(): string { get description(): string {
const claim = const claim = this.record.fullClaim;
(this.record.fullClaim as any)?.claim || this.record.fullClaim;
return `${claim?.description || ""}`; return `${claim?.description || ""}`;
} }

2
src/components/DataExportSection.vue

@ -171,6 +171,8 @@ export default class DataExportSection extends Vue {
* @throws {Error} If export fails * @throws {Error} If export fails
*/ */
public async exportDatabase(): Promise<void> { public async exportDatabase(): Promise<void> {
// Note that similar code is in ContactsView.vue exportContactData()
if (this.isExporting) { if (this.isExporting) {
return; // Prevent multiple simultaneous exports return; // Prevent multiple simultaneous exports
} }

24
src/components/FeedFilters.vue

@ -101,6 +101,7 @@ import {
import { Router } from "vue-router"; import { Router } from "vue-router";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { logger } from "@/utils/logger";
@Component({ @Component({
components: { components: {
@ -119,11 +120,13 @@ export default class FeedFilters extends Vue {
isNearby = false; isNearby = false;
settingChanged = false; settingChanged = false;
visible = false; visible = false;
activeDid = "";
async open(onCloseIfChanged: () => void) { async open(onCloseIfChanged: () => void, activeDid: string) {
this.onCloseIfChanged = onCloseIfChanged; this.onCloseIfChanged = onCloseIfChanged;
this.activeDid = activeDid;
const settings = await this.$settings(); const settings = await this.$accountSettings(activeDid);
this.hasVisibleDid = !!settings.filterFeedByVisible; this.hasVisibleDid = !!settings.filterFeedByVisible;
this.isNearby = !!settings.filterFeedByNearby; this.isNearby = !!settings.filterFeedByNearby;
if (settings.searchBoxes && settings.searchBoxes.length > 0) { if (settings.searchBoxes && settings.searchBoxes.length > 0) {
@ -137,6 +140,7 @@ export default class FeedFilters extends Vue {
async toggleHasVisibleDid() { async toggleHasVisibleDid() {
this.settingChanged = true; this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid; this.hasVisibleDid = !this.hasVisibleDid;
await this.$updateSettings({ await this.$updateSettings({
filterFeedByVisible: this.hasVisibleDid, filterFeedByVisible: this.hasVisibleDid,
}); });
@ -145,9 +149,18 @@ export default class FeedFilters extends Vue {
async toggleNearby() { async toggleNearby() {
this.settingChanged = true; this.settingChanged = true;
this.isNearby = !this.isNearby; this.isNearby = !this.isNearby;
logger.debug("[FeedFilters] 🔄 Toggling nearby filter:", {
newValue: this.isNearby,
settingChanged: this.settingChanged,
activeDid: this.activeDid,
});
await this.$updateSettings({ await this.$updateSettings({
filterFeedByNearby: this.isNearby, filterFeedByNearby: this.isNearby,
}); });
logger.debug("[FeedFilters] ✅ Nearby filter updated in settings");
} }
async clearAll() { async clearAll() {
@ -179,13 +192,20 @@ export default class FeedFilters extends Vue {
} }
close() { close() {
logger.debug("[FeedFilters] 🚪 Closing dialog:", {
settingChanged: this.settingChanged,
hasCallback: !!this.onCloseIfChanged,
});
if (this.settingChanged) { if (this.settingChanged) {
logger.debug("[FeedFilters] 🔄 Settings changed, calling callback");
this.onCloseIfChanged(); this.onCloseIfChanged();
} }
this.visible = false; this.visible = false;
} }
done() { done() {
logger.debug("[FeedFilters] ✅ Done button clicked");
this.close(); this.close();
} }
} }

5
src/components/GiftedDialog.vue

@ -622,7 +622,10 @@ export default class GiftedDialog extends Vue {
* Handle edit entity request from GiftDetailsStep * Handle edit entity request from GiftDetailsStep
* @param data - Object containing entityType and currentEntity * @param data - Object containing entityType and currentEntity
*/ */
handleEditEntity(data: { entityType: string; currentEntity: any }) { handleEditEntity(data: {
entityType: string;
currentEntity: { did: string; name: string };
}) {
this.goBackToStep1(data.entityType); this.goBackToStep1(data.entityType);
} }

90
src/components/IconRenderer.vue

@ -1,90 +0,0 @@
<template>
<svg
v-if="iconData"
:class="svgClass"
:fill="fill"
:stroke="stroke"
:viewBox="viewBox"
xmlns="http://www.w3.org/2000/svg"
>
<path v-for="(path, index) in iconData.paths" :key="index" v-bind="path" />
</svg>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import icons from "../assets/icons.json";
import { logger } from "../utils/logger";
/**
* Icon path interface
*/
interface IconPath {
d: string;
fillRule?: string;
clipRule?: string;
strokeLinecap?: string;
strokeLinejoin?: string;
strokeWidth?: string | number;
fill?: string;
stroke?: string;
}
/**
* Icon data interface
*/
interface IconData {
paths: IconPath[];
}
/**
* Icons JSON structure
*/
interface IconsJson {
[key: string]: IconPath | IconData;
}
/**
* Icon Renderer Component
*
* This component loads SVG icon definitions from a JSON file and renders them
* as SVG elements. It provides a clean way to use icons without cluttering
* templates with long SVG path definitions.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2024
*/
@Component({
name: "IconRenderer",
})
export default class IconRenderer extends Vue {
@Prop({ required: true }) readonly iconName!: string;
@Prop({ default: "h-5 w-5" }) readonly svgClass!: string;
@Prop({ default: "none" }) readonly fill!: string;
@Prop({ default: "currentColor" }) readonly stroke!: string;
@Prop({ default: "0 0 24 24" }) readonly viewBox!: string;
/**
* Get the icon data for the specified icon name
*
* @returns {IconData | null} The icon data object or null if not found
*/
get iconData(): IconData | null {
const icon = (icons as IconsJson)[this.iconName];
if (!icon) {
logger.warn(`Icon "${this.iconName}" not found in icons.json`);
return null;
}
// Convert single path to array format for consistency
if ("d" in icon) {
return {
paths: [icon as IconPath],
};
}
return icon as IconData;
}
}
</script>

5
src/components/UsageLimitsSection.vue

@ -83,6 +83,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator"; import { Component, Vue, Prop } from "vue-facing-decorator";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { EndorserRateLimits, ImageRateLimits } from "@/interfaces/limits";
@Component({ @Component({
name: "UsageLimitsSection", name: "UsageLimitsSection",
@ -94,8 +95,8 @@ export default class UsageLimitsSection extends Vue {
@Prop({ required: true }) loadingLimits!: boolean; @Prop({ required: true }) loadingLimits!: boolean;
@Prop({ required: true }) limitsMessage!: string; @Prop({ required: true }) limitsMessage!: string;
@Prop({ required: false }) activeDid?: string; @Prop({ required: false }) activeDid?: string;
@Prop({ required: false }) endorserLimits?: any; @Prop({ required: false }) endorserLimits?: EndorserRateLimits;
@Prop({ required: false }) imageLimits?: any; @Prop({ required: false }) imageLimits?: ImageRateLimits;
@Prop({ required: true }) onRecheckLimits!: () => void; @Prop({ required: true }) onRecheckLimits!: () => void;
mounted() { mounted() {

8
src/constants/notifications.ts

@ -846,6 +846,12 @@ export const NOTIFY_CONTACTS_ADDED = {
message: "They were added.", message: "They were added.",
}; };
// Used in: ContactsView.vue (addContact method - export data prompt after contact addition)
export const NOTIFY_EXPORT_DATA_PROMPT = {
title: "Export Your Data",
message: "Would you like to export your contact data as a backup?",
};
// Used in: ContactsView.vue (showCopySelectionsInfo method - info about copying contacts) // Used in: ContactsView.vue (showCopySelectionsInfo method - info about copying contacts)
export const NOTIFY_CONTACT_INFO_COPY = { export const NOTIFY_CONTACT_INFO_COPY = {
title: "Info", title: "Info",
@ -1588,7 +1594,7 @@ export function createImageDialogCameraErrorMessage(error: Error): string {
// Helper function for dynamic upload error messages // Helper function for dynamic upload error messages
// Used in: ImageMethodDialog.vue (uploadImage method - dynamic upload error message) // Used in: ImageMethodDialog.vue (uploadImage method - dynamic upload error message)
export function createImageDialogUploadErrorMessage(error: any): string { export function createImageDialogUploadErrorMessage(error: unknown): string {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
const status = error.response?.status; const status = error.response?.status;
const data = error.response?.data; const data = error.response?.data;

84
src/interfaces/common.ts

@ -60,9 +60,13 @@ export interface AxiosErrorResponse {
[key: string]: unknown; [key: string]: unknown;
}; };
status?: number; status?: number;
statusText?: string;
config?: unknown; config?: unknown;
}; };
config?: unknown; config?: {
url?: string;
[key: string]: unknown;
};
[key: string]: unknown; [key: string]: unknown;
} }
@ -98,3 +102,81 @@ export interface VerifiableCredentialClaim {
credentialSubject: ClaimObject; credentialSubject: ClaimObject;
[key: string]: unknown; [key: string]: unknown;
} }
/**
* Database constraint error types for consistent error handling
*/
export interface DatabaseConstraintError extends Error {
name: "ConstraintError";
message: string;
constraint?: string;
}
/**
* Database storage error types for IndexedDB/SQLite operations
*/
export interface DatabaseStorageError extends Error {
name: "StorageError";
message: string;
code?: string;
constraint?: string;
}
/**
* Legacy Dexie error types for migration compatibility
*/
export interface DexieError extends Error {
name: string;
message: string;
inner?: unknown;
stack?: string;
}
/**
* Type guard for database constraint errors
*/
export function isDatabaseConstraintError(
error: unknown,
): error is DatabaseConstraintError {
return error instanceof Error && error.name === "ConstraintError";
}
/**
* Type guard for database storage errors
*/
export function isDatabaseStorageError(
error: unknown,
): error is DatabaseStorageError {
return error instanceof Error && error.name === "StorageError";
}
/**
* Type guard for legacy Dexie errors
*/
export function isDexieError(error: unknown): error is DexieError {
return (
error instanceof Error &&
(error.name === "DexieError" ||
error.message.includes("Key already exists in the object store") ||
error.message.includes("ConstraintError"))
);
}
/**
* Unified error type for database operations
*/
export type DatabaseError =
| DatabaseConstraintError
| DatabaseStorageError
| DexieError;
/**
* Type guard for any database error
*/
export function isDatabaseError(error: unknown): error is DatabaseError {
return (
isDatabaseConstraintError(error) ||
isDatabaseStorageError(error) ||
isDexieError(error)
);
}

28
src/interfaces/deepLinks.ts

@ -28,7 +28,7 @@
import { z } from "zod"; import { z } from "zod";
// Parameter validation schemas for each route type // Parameter validation schemas for each route type
export const deepLinkSchemas = { export const deepLinkPathSchemas = {
claim: z.object({ claim: z.object({
id: z.string(), id: z.string(),
}), }),
@ -60,7 +60,7 @@ export const deepLinkSchemas = {
jwt: z.string().optional(), jwt: z.string().optional(),
}), }),
"onboard-meeting-members": z.object({ "onboard-meeting-members": z.object({
id: z.string(), groupId: z.string(),
}), }),
project: z.object({ project: z.object({
id: z.string(), id: z.string(),
@ -70,6 +70,17 @@ export const deepLinkSchemas = {
}), }),
}; };
export const deepLinkQuerySchemas = {
"onboard-meeting-members": z.object({
password: z.string(),
}),
};
// Add a union type of all valid route paths
export const VALID_DEEP_LINK_ROUTES = Object.keys(
deepLinkPathSchemas,
) as readonly (keyof typeof deepLinkPathSchemas)[];
// Create a type from the array // Create a type from the array
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
@ -80,14 +91,13 @@ export const baseUrlSchema = z.object({
queryParams: z.record(z.string()).optional(), queryParams: z.record(z.string()).optional(),
}); });
// Add a union type of all valid route paths // export type DeepLinkPathParams = {
export const VALID_DEEP_LINK_ROUTES = Object.keys( // [K in keyof typeof deepLinkPathSchemas]: z.infer<(typeof deepLinkPathSchemas)[K]>;
deepLinkSchemas, // };
) as readonly (keyof typeof deepLinkSchemas)[];
export type DeepLinkParams = { // export type DeepLinkQueryParams = {
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; // [K in keyof typeof deepLinkQuerySchemas]: z.infer<(typeof deepLinkQuerySchemas)[K]>;
}; // };
export interface DeepLinkError extends Error { export interface DeepLinkError extends Error {
code: string; code: string;

10
src/libs/fontawesome.ts

@ -20,6 +20,7 @@ import {
faCameraRotate, faCameraRotate,
faCaretDown, faCaretDown,
faChair, faChair,
faChartLine,
faCheck, faCheck,
faChevronDown, faChevronDown,
faChevronLeft, faChevronLeft,
@ -28,6 +29,7 @@ import {
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
faCirclePlus,
faCircleQuestion, faCircleQuestion,
faCircleRight, faCircleRight,
faCircleUser, faCircleUser,
@ -49,6 +51,7 @@ import {
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faForward, faForward,
faGear,
faGift, faGift,
faGlobe, faGlobe,
faHammer, faHammer,
@ -58,6 +61,7 @@ import {
faHouseChimney, faHouseChimney,
faImage, faImage,
faImagePortrait, faImagePortrait,
faInfo,
faLeftRight, faLeftRight,
faLightbulb, faLightbulb,
faLink, faLink,
@ -72,8 +76,8 @@ import {
faPersonCircleCheck, faPersonCircleCheck,
faPersonCircleQuestion, faPersonCircleQuestion,
faPlus, faPlus,
faQuestion,
faQrcode, faQrcode,
faQuestion,
faRightFromBracket, faRightFromBracket,
faRotate, faRotate,
faShareNodes, faShareNodes,
@ -106,6 +110,7 @@ library.add(
faCameraRotate, faCameraRotate,
faCaretDown, faCaretDown,
faChair, faChair,
faChartLine,
faCheck, faCheck,
faChevronDown, faChevronDown,
faChevronLeft, faChevronLeft,
@ -114,6 +119,7 @@ library.add(
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
faCirclePlus,
faCircleQuestion, faCircleQuestion,
faCircleRight, faCircleRight,
faCircleUser, faCircleUser,
@ -135,6 +141,7 @@ library.add(
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faForward, faForward,
faGear,
faGift, faGift,
faGlobe, faGlobe,
faHammer, faHammer,
@ -144,6 +151,7 @@ library.add(
faHouseChimney, faHouseChimney,
faImage, faImage,
faImagePortrait, faImagePortrait,
faInfo,
faLeftRight, faLeftRight,
faLightbulb, faLightbulb,
faLink, faLink,

120
src/main.capacitor.ts

@ -29,14 +29,14 @@
*/ */
import { initializeApp } from "./main.common"; import { initializeApp } from "./main.common";
import { App } from "./libs/capacitor/app"; import { App as CapacitorApp } from "@capacitor/app";
import router from "./router"; import router from "./router";
import { handleApiError } from "./services/api"; import { handleApiError } from "./services/api";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks"; import { DeepLinkHandler } from "./services/deepLinks";
import { logger, safeStringify } from "./utils/logger"; import { logger, safeStringify } from "./utils/logger";
logger.log("[Capacitor] Starting initialization"); logger.log("[Capacitor] 🚀 Starting initialization");
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM); logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
const app = initializeApp(); const app = initializeApp();
@ -67,23 +67,123 @@ const deepLinkHandler = new DeepLinkHandler(router);
* @throws {Error} If URL format is invalid * @throws {Error} If URL format is invalid
*/ */
const handleDeepLink = async (data: { url: string }) => { const handleDeepLink = async (data: { url: string }) => {
const { url } = data;
logger.info(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
try { try {
// Wait for router to be ready
logger.info(`[Main] ⏳ Waiting for router to be ready...`);
await router.isReady(); await router.isReady();
await deepLinkHandler.handleDeepLink(data.url); logger.info(`[Main] ✅ Router is ready, processing deeplink`);
// Process the deeplink
logger.info(`[Main] 🚀 Starting deeplink processing`);
await deepLinkHandler.handleDeepLink(url);
logger.info(`[Main] ✅ Deeplink processed successfully`);
} catch (error) { } catch (error) {
logger.error("[DeepLink] Error handling deep link: ", error); logger.error(`[Main] ❌ Deeplink processing failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
// Log additional context for debugging
logger.error(`[Main] 🔍 Debug context:`, {
routerReady: router.isReady(),
currentRoute: router.currentRoute.value,
appMounted: app._instance?.isMounted,
timestamp: new Date().toISOString(),
});
// Fallback to original error handling
let message: string = let message: string =
error instanceof Error ? error.message : safeStringify(error); error instanceof Error ? error.message : safeStringify(error);
if (data.url) { if (url) {
message += `\nURL: ${data.url}`; message += `\nURL: ${url}`;
} }
handleApiError({ message } as AxiosError, "deep-link"); handleApiError({ message } as AxiosError, "deep-link");
} }
}; };
// Register deep link handler with Capacitor // Function to register the deeplink listener
App.addListener("appUrlOpen", handleDeepLink); const registerDeepLinkListener = async () => {
try {
logger.info(
`[Main] 🔗 Attempting to register deeplink handler with Capacitor`,
);
// Check if Capacitor App plugin is available
logger.info(`[Main] 🔍 Checking Capacitor App plugin availability...`);
if (!CapacitorApp) {
throw new Error("Capacitor App plugin not available");
}
logger.info(`[Main] ✅ Capacitor App plugin is available`);
// Check available methods on CapacitorApp
logger.info(
`[Main] 🔍 Capacitor App plugin methods:`,
Object.getOwnPropertyNames(CapacitorApp),
);
logger.info(
`[Main] 🔍 Capacitor App plugin addListener method:`,
typeof CapacitorApp.addListener,
);
// Wait for router to be ready first
await router.isReady();
logger.info(
`[Main] ✅ Router is ready, proceeding with listener registration`,
);
// Try to register the listener
logger.info(`[Main] 🧪 Attempting to register appUrlOpen listener...`);
const listenerHandle = await CapacitorApp.addListener(
"appUrlOpen",
handleDeepLink,
);
logger.info(
`[Main] ✅ appUrlOpen listener registered successfully with handle:`,
listenerHandle,
);
logger.log("[Capacitor] Mounting app"); // Test the listener registration by checking if it's actually registered
logger.info(`[Main] 🧪 Verifying listener registration...`);
return listenerHandle;
} catch (error) {
logger.error(`[Main] ❌ Failed to register deeplink listener:`, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
throw error;
}
};
logger.log("[Capacitor] 🚀 Mounting app");
app.mount("#app"); app.mount("#app");
logger.log("[Capacitor] App mounted"); logger.info(`[Main] ✅ App mounted successfully`);
// Register deeplink listener after app is mounted
setTimeout(async () => {
try {
logger.info(
`[Main] ⏳ Delaying listener registration to ensure Capacitor is ready...`,
);
await registerDeepLinkListener();
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
} catch (error) {
logger.error(`[Main] ❌ Deep link system initialization failed:`, error);
}
}, 2000); // 2 second delay to ensure Capacitor is fully ready
// Log app initialization status
setTimeout(() => {
logger.info(`[Main] 📊 App initialization status:`, {
routerReady: router.isReady(),
currentRoute: router.currentRoute.value,
appMounted: app._instance?.isMounted,
timestamp: new Date().toISOString(),
});
}, 1000);

26
src/main.ts

@ -0,0 +1,26 @@
/**
* @file Dynamic Main Entry Point
* @author Matthew Raymer
*
* This file dynamically loads the appropriate platform-specific main entry point
* based on the current environment and build configuration.
*/
import { logger } from "./utils/logger";
// Check the platform from environment variables
const platform = process.env.VITE_PLATFORM || "web";
logger.info(`[Main] 🚀 Loading TimeSafari for platform: ${platform}`);
// Dynamically import the appropriate main entry point
if (platform === "capacitor") {
logger.info(`[Main] 📱 Loading Capacitor-specific entry point`);
import("./main.capacitor");
} else if (platform === "electron") {
logger.info(`[Main] 💻 Loading Electron-specific entry point`);
import("./main.electron");
} else {
logger.info(`[Main] 🌐 Loading Web-specific entry point`);
import("./main.web");
}

72
src/router/index.ts

@ -321,24 +321,21 @@ const errorHandler = (
router.onError(errorHandler); // Assign the error handler to the router instance router.onError(errorHandler); // Assign the error handler to the router instance
/** /**
* Global navigation guard to ensure user identity exists * Navigation guard to ensure user has an identity before accessing protected routes
*
* This guard checks if the user has any identities before navigating to most routes.
* If no identity exists, it automatically creates one using the default seed-based method.
*
* Routes that are excluded from this check:
* - /start - Manual identity creation selection
* - /new-identifier - Manual seed-based creation
* - /import-account - Manual import flow
* - /import-derive - Manual derivation flow
* - /database-migration - Migration utilities
* - /deep-link-error - Error page
*
* @param to - Target route * @param to - Target route
* @param from - Source route * @param _from - Source route (unused)
* @param next - Navigation function * @param next - Navigation function
*/ */
router.beforeEach(async (to, _from, next) => { router.beforeEach(async (to, _from, next) => {
logger.info(`[Router] 🧭 Navigation guard triggered:`, {
from: _from?.path || "none",
to: to.path,
name: to.name,
params: to.params,
query: to.query,
timestamp: new Date().toISOString(),
});
try { try {
// Skip identity check for routes that handle identity creation manually // Skip identity check for routes that handle identity creation manually
const skipIdentityRoutes = [ const skipIdentityRoutes = [
@ -351,32 +348,67 @@ router.beforeEach(async (to, _from, next) => {
]; ];
if (skipIdentityRoutes.includes(to.path)) { if (skipIdentityRoutes.includes(to.path)) {
logger.debug(`[Router] ⏭️ Skipping identity check for route: ${to.path}`);
return next(); return next();
} }
logger.info(`[Router] 🔍 Checking user identity for route: ${to.path}`);
// Check if user has any identities // Check if user has any identities
const allMyDids = await retrieveAccountDids(); const allMyDids = await retrieveAccountDids();
logger.info(`[Router] 📋 Found ${allMyDids.length} user identities`);
if (allMyDids.length === 0) { if (allMyDids.length === 0) {
logger.info("[Router] No identities found, creating default identity"); logger.info("[Router] ⚠️ No identities found, creating default identity");
// Create identity automatically using seed-based method // Create identity automatically using seed-based method
await generateSaveAndActivateIdentity(); await generateSaveAndActivateIdentity();
logger.info("[Router] Default identity created successfully"); logger.info("[Router] ✅ Default identity created successfully");
} else {
logger.info(
`[Router] ✅ User has ${allMyDids.length} identities, proceeding`,
);
} }
logger.info(`[Router] ✅ Navigation guard passed for: ${to.path}`);
next(); next();
} catch (error) { } catch (error) {
logger.error( logger.error("[Router] ❌ Identity creation failed in navigation guard:", {
"[Router] Identity creation failed in navigation guard:", error: error instanceof Error ? error.message : String(error),
error, stack: error instanceof Error ? error.stack : undefined,
); route: to.path,
timestamp: new Date().toISOString(),
});
// Redirect to start page if identity creation fails // Redirect to start page if identity creation fails
// This allows users to manually create an identity or troubleshoot // This allows users to manually create an identity or troubleshoot
logger.info(
`[Router] 🔄 Redirecting to /start due to identity creation failure`,
);
next("/start"); next("/start");
} }
}); });
// Add navigation success logging
router.afterEach((to, from) => {
logger.info(`[Router] ✅ Navigation completed:`, {
from: from?.path || "none",
to: to.path,
name: to.name,
params: to.params,
query: to.query,
timestamp: new Date().toISOString(),
});
});
// Add error logging
router.onError((error) => {
logger.error(`[Router] ❌ Navigation error:`, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
});
export default router; export default router;

101
src/services/ProfileService.ts

@ -124,17 +124,55 @@ export class ProfileService {
async deleteProfile(activeDid: string): Promise<boolean> { async deleteProfile(activeDid: string): Promise<boolean> {
try { try {
const headers = await getHeaders(activeDid); const headers = await getHeaders(activeDid);
const response = await this.axios.delete( const url = `${this.partnerApiServer}/api/partner/userProfile`;
`${this.partnerApiServer}/api/partner/userProfile`, const response = await this.axios.delete(url, { headers });
{ headers },
);
if (response.status === 200) { if (response.status === 204 || response.status === 200) {
logger.info("Profile deleted successfully");
return true; return true;
} else { } else {
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_DELETED); 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) { } 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)); logger.error("Error deleting profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfile"); handleApiError(error as AxiosError, "/api/partner/userProfile");
return false; return false;
@ -204,13 +242,56 @@ export class ProfileService {
} }
/** /**
* Type guard for API errors * Type guard for API errors with proper typing
*/ */
private isApiError( private isApiError(error: unknown): error is {
error: unknown, response?: {
): error is { response?: { status?: number } } { status?: number;
statusText?: string;
data?: { message?: string } | string;
};
} {
return typeof error === "object" && error !== null && "response" in error; 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;
}
} }
/** /**

99
src/services/QRNavigationService.ts

@ -0,0 +1,99 @@
import { PlatformServiceFactory } from "./PlatformServiceFactory";
import { PlatformService } from "./PlatformService";
import { logger } from "@/utils/logger";
/**
* QR Navigation Service
*
* Handles platform-specific routing logic for QR scanning operations.
* Removes coupling between views and routing logic by centralizing
* navigation decisions based on platform capabilities.
*
* @author Matthew Raymer
*/
export class QRNavigationService {
private static instance: QRNavigationService | null = null;
private platformService: PlatformService;
private constructor() {
this.platformService = PlatformServiceFactory.getInstance();
}
/**
* Get singleton instance of QRNavigationService
*/
public static getInstance(): QRNavigationService {
if (!QRNavigationService.instance) {
QRNavigationService.instance = new QRNavigationService();
}
return QRNavigationService.instance;
}
/**
* Get the appropriate QR scanner route based on platform
*
* @returns Object with route name and parameters for QR scanning
*/
public getQRScannerRoute(): {
name: string;
params?: Record<string, string | number>;
} {
const isCapacitor = this.platformService.isCapacitor();
logger.debug("QR Navigation - Platform detection:", {
isCapacitor,
platform: this.platformService.getCapabilities(),
});
if (isCapacitor) {
// Use native scanner on mobile platforms
return { name: "contact-qr-scan-full" };
} else {
// Use web scanner on other platforms
return { name: "contact-qr" };
}
}
/**
* Get the appropriate QR display route based on platform
*
* @returns Object with route name and parameters for QR display
*/
public getQRDisplayRoute(): {
name: string;
params?: Record<string, string | number>;
} {
const isCapacitor = this.platformService.isCapacitor();
logger.debug("QR Navigation - Display route detection:", {
isCapacitor,
platform: this.platformService.getCapabilities(),
});
if (isCapacitor) {
// Use dedicated display view on mobile
return { name: "contact-qr-scan-show" };
} else {
// Use combined view on web
return { name: "contact-qr" };
}
}
/**
* Check if native QR scanning is available on current platform
*
* @returns true if native scanning is available, false otherwise
*/
public isNativeScanningAvailable(): boolean {
return this.platformService.isCapacitor();
}
/**
* Get platform capabilities for QR operations
*
* @returns Platform capabilities object
*/
public getPlatformCapabilities() {
return this.platformService.getCapabilities();
}
}

334
src/services/deepLinks.ts

@ -1,56 +1,22 @@
/** /**
* @file Deep Link Handler Service * DeepLinks Service
* @author Matthew Raymer
*
* This service handles the processing and routing of deep links in the TimeSafari app.
* It provides a type-safe interface between the raw deep links and the application router.
*
* Architecture:
* 1. DeepLinkHandler class encapsulates all deep link processing logic
* 2. Uses Zod schemas from interfaces/deepLinks for parameter validation
* 3. Provides consistent error handling and logging
* 4. Maps validated parameters to Vue router calls
*
* Error Handling Strategy:
* - All errors are wrapped in DeepLinkError interface
* - Errors include error codes for systematic handling
* - Detailed error information is logged for debugging
* - Errors are propagated to the global error handler
* *
* Validation Strategy: * Handles deep link processing and routing for the TimeSafari application.
* - URL structure validation * Supports both path parameters and query parameters with comprehensive validation.
* - Route-specific parameter validation using Zod schemas
* - Query parameter validation and sanitization
* - Type-safe parameter passing to router
* *
* Deep Link Format: * @author Matthew Raymer
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2] * @version 2.0.0
* * @since 2025-01-25
* Supported Routes:
* - claim: View claim
* - claim-add-raw: Add raw claim
* - claim-cert: View claim certificate
* - confirm-gift
* - contact-import: Import contacts
* - did: View DID
* - invite-one-accept: Accept invitation
* - onboard-meeting-members
* - project: View project details
* - user-profile: View user profile
*
* @example
* const handler = new DeepLinkHandler(router);
* await handler.handleDeepLink("timesafari://claim/123?view=details");
*/ */
import { Router } from "vue-router"; import { Router } from "vue-router";
import { z } from "zod"; import { z } from "zod";
import { import {
deepLinkSchemas, deepLinkPathSchemas,
baseUrlSchema,
routeSchema, routeSchema,
DeepLinkRoute, DeepLinkRoute,
deepLinkQuerySchemas,
} from "../interfaces/deepLinks"; } from "../interfaces/deepLinks";
import type { DeepLinkError } from "../interfaces/deepLinks"; import type { DeepLinkError } from "../interfaces/deepLinks";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@ -74,7 +40,7 @@ function getFirstKeyFromZodObject(
* because "router.replace" expects the right parameter name for the route. * because "router.replace" expects the right parameter name for the route.
*/ */
export const ROUTE_MAP: Record<string, { name: string; paramKey?: string }> = export const ROUTE_MAP: Record<string, { name: string; paramKey?: string }> =
Object.entries(deepLinkSchemas).reduce( Object.entries(deepLinkPathSchemas).reduce(
(acc, [routeName, schema]) => { (acc, [routeName, schema]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>); const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>);
@ -103,83 +69,152 @@ export class DeepLinkHandler {
} }
/** /**
* Main entry point for processing deep links
* @param url - The deep link URL to process
* @throws {DeepLinkError} If validation fails or route is invalid
*/
async handleDeepLink(url: string): Promise<void> {
logger.info(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
* Parses deep link URL into path, params and query components. try {
* Validates URL structure using Zod schemas. logger.info(`[DeepLink] 📍 Parsing URL: ${url}`);
* const { path, params, query } = this.parseDeepLink(url);
* @param url - The deep link URL to parse (format: scheme://path[?query])
* @throws {DeepLinkError} If URL format is invalid logger.info(`[DeepLink] ✅ URL parsed successfully:`, {
* @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string}) path,
params: Object.keys(params),
query: Object.keys(query),
fullParams: params,
fullQuery: query,
});
// Sanitize parameters (remove undefined values)
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
await this.validateAndRoute(path, sanitizedParams, query);
logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`);
} catch (error) {
logger.error(`[DeepLink] ❌ Deeplink processing failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
const deepLinkError = error as DeepLinkError;
throw deepLinkError;
}
}
/**
* Parse a deep link URL into its components
* @param url - The deep link URL
* @returns Parsed components
*/ */
private parseDeepLink(url: string) { private parseDeepLink(url: string): {
path: string;
params: Record<string, string>;
query: Record<string, string>;
} {
logger.debug(`[DeepLink] 🔍 Parsing deep link: ${url}`);
try {
const parts = url.split("://"); const parts = url.split("://");
if (parts.length !== 2) { if (parts.length !== 2) {
throw { code: "INVALID_URL", message: "Invalid URL format" }; throw new Error("Invalid URL format");
} }
// Validate base URL structure
baseUrlSchema.parse({
scheme: parts[0],
path: parts[1],
queryParams: {}, // Will be populated below
});
const [path, queryString] = parts[1].split("?"); const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/"); const [routePath, ...pathParams] = path.split("/");
// Validate route exists before proceeding // Parse path parameters using route-specific configuration
if (!ROUTE_MAP[routePath]) { const params: Record<string, string> = {};
throw { if (pathParams.length > 0) {
code: "INVALID_ROUTE", // Get the correct parameter key for this route
message: `Invalid route path: ${routePath}`, const routeConfig = ROUTE_MAP[routePath];
details: { routePath }, if (routeConfig?.paramKey) {
}; params[routeConfig.paramKey] = pathParams[0];
logger.debug(
`[DeepLink] 📍 Path parameter extracted: ${routeConfig.paramKey}=${pathParams[0]}`,
);
} else {
// Fallback to 'id' for backward compatibility
params.id = pathParams[0];
logger.debug(
`[DeepLink] 📍 Path parameter extracted: id=${pathParams[0]} (fallback)`,
);
}
} }
// Parse query parameters
const query: Record<string, string> = {}; const query: Record<string, string> = {};
if (queryString) { if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => { const queryParams = new URLSearchParams(queryString);
for (const [key, value] of queryParams.entries()) {
query[key] = value; query[key] = value;
});
} }
logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query);
const params: Record<string, string> = {};
if (pathParams) {
// Now we know routePath exists in ROUTE_MAP
const routeConfig = ROUTE_MAP[routePath];
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
} }
// logConsoleAndDb( logger.info(`[DeepLink] ✅ Parse completed:`, {
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`, routePath,
// false, pathParams: pathParams.length,
// ); queryParams: Object.keys(query).length,
});
return { path: routePath, params, query }; return { path: routePath, params, query };
} catch (error) {
logger.error(`[DeepLink] ❌ Parse failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
} }
/** /**
* Routes the deep link to appropriate view with validated parameters. * Validate and route the deep link
* Validates route and parameters using Zod schemas before routing. * @param path - The route path
* * @param params - Path parameters
* @param path - The route path from the deep link * @param query - Query parameters
* @param params - URL parameters
* @param query - Query string parameters
* @throws {DeepLinkError} If validation fails or route is invalid
*/ */
private async validateAndRoute( private async validateAndRoute(
path: string, path: string,
params: Record<string, string>, params: Record<string, string>,
query: Record<string, string>, query: Record<string, string>,
): Promise<void> { ): Promise<void> {
logger.info(
`[DeepLink] 🎯 Starting validation and routing for path: ${path}`,
);
// First try to validate the route path // First try to validate the route path
let routeName: string; let routeName: string;
try { try {
logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`);
// Validate route exists // Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute; const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = ROUTE_MAP[validRoute].name; logger.info(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
// Get route configuration
const routeConfig = ROUTE_MAP[validRoute];
logger.info(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
if (!routeConfig) {
logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`);
throw new Error(`Route configuration missing for: ${validRoute}`);
}
routeName = routeConfig.name;
logger.info(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
} catch (error) { } catch (error) {
logger.error(`[DeepLink] Invalid route path: ${path}`); logger.error(`[DeepLink] ❌ Route validation failed:`, {
path,
error: error instanceof Error ? error.message : String(error),
});
// Redirect to error page with information about the invalid link // Redirect to error page with information about the invalid link
await this.router.replace({ await this.router.replace({
@ -193,21 +228,66 @@ export class DeepLinkHandler {
}, },
}); });
// This previously threw an error but we're redirecting so there's no need. logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`,
);
return; return;
} }
// Continue with parameter validation as before... // Continue with parameter validation
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; logger.info(
`[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`,
);
const pathSchema =
deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
const querySchema =
deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
logger.debug(`[DeepLink] 📋 Schemas found:`, {
hasPathSchema: !!pathSchema,
hasQuerySchema: !!querySchema,
pathSchemaType: pathSchema ? typeof pathSchema : "none",
querySchemaType: querySchema ? typeof querySchema : "none",
});
let validatedPathParams: Record<string, string> = {};
let validatedQueryParams: Record<string, string> = {};
let validatedParams;
try { try {
validatedParams = await schema.parseAsync(params); if (pathSchema) {
} catch (error) { logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params);
// For parameter validation errors, provide specific error feedback validatedPathParams = await pathSchema.parseAsync(params);
logger.error( logger.info(
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`, `[DeepLink] ✅ Path parameters validated:`,
validatedPathParams,
);
} else {
logger.debug(`[DeepLink] ⚠️ No path schema found for: ${path}`);
validatedPathParams = params;
}
if (querySchema) {
logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query);
validatedQueryParams = await querySchema.parseAsync(query);
logger.info(
`[DeepLink] ✅ Query parameters validated:`,
validatedQueryParams,
); );
} else {
logger.debug(`[DeepLink] ⚠️ No query schema found for: ${path}`);
validatedQueryParams = query;
}
} catch (error) {
logger.error(`[DeepLink] ❌ Parameter validation failed:`, {
routeName,
path,
params,
query,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params, params,
@ -219,58 +299,52 @@ export class DeepLinkHandler {
}, },
}); });
// This previously threw an error but we're redirecting so there's no need. logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid parameters`,
);
return; return;
} }
// Attempt navigation
try { try {
logger.info(`[DeepLink] 🚀 Attempting navigation:`, {
routeName,
pathParams: validatedPathParams,
queryParams: validatedQueryParams,
});
await this.router.replace({ await this.router.replace({
name: routeName, name: routeName,
params: validatedParams, params: validatedPathParams,
query: validatedQueryParams,
}); });
logger.info(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
} catch (error) { } catch (error) {
logger.error( logger.error(`[DeepLink] ❌ Navigation failed:`, {
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`, routeName,
); path,
// For parameter validation errors, provide specific error feedback validatedPathParams,
validatedQueryParams,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
// Redirect to error page for navigation failures
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params: validatedParams, params: validatedPathParams,
query: { query: {
originalPath: path, originalPath: path,
errorCode: "ROUTING_ERROR", errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`, errorMessage: `Error routing to ${routeName}: ${(error as Error).message}`,
...validatedQueryParams,
}, },
}); });
}
}
/** logger.info(
* Processes incoming deep links and routes them appropriately. `[DeepLink] 🔄 Redirected to error page for navigation failure`,
* Handles validation, error handling, and routing to the correct view.
*
* @param url - The deep link URL to process
* @throws {DeepLinkError} If URL processing fails
*/
async handleDeepLink(url: string): Promise<void> {
try {
const { path, params, query } = this.parseDeepLink(url);
// Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
); );
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logger.error(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
);
throw {
code: deepLinkError.code || "UNKNOWN_ERROR",
message: deepLinkError.message,
details: deepLinkError.details,
};
} }
} }
} }

139
src/views/AccountViewView.vue

@ -90,11 +90,12 @@
:aria-busy="loadingProfile || savingProfile" :aria-busy="loadingProfile || savingProfile"
></textarea> ></textarea>
<div class="flex items-center mb-4" @click="toggleUserProfileLocation"> <div class="flex items-center mb-4">
<input <input
v-model="includeUserProfileLocation" v-model="includeUserProfileLocation"
type="checkbox" type="checkbox"
class="mr-2" class="mr-2"
@change="onLocationCheckboxChange"
/> />
<label for="includeUserProfileLocation">Include Location</label> <label for="includeUserProfileLocation">Include Location</label>
</div> </div>
@ -110,6 +111,7 @@
class="!z-40 rounded-md" class="!z-40 rounded-md"
@click="onProfileMapClick" @click="onProfileMapClick"
@ready="onMapReady" @ready="onMapReady"
@mounted="onMapMounted"
> >
<l-tile-layer <l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@ -667,6 +669,7 @@ import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there // @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import"; import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet"; import { LeafletMouseEvent } from "leaflet";
import * as L from "leaflet";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ref } from "vue"; import { ref } from "vue";
@ -810,6 +813,7 @@ export default class AccountViewView extends Vue {
warnIfProdServer: boolean = false; warnIfProdServer: boolean = false;
warnIfTestServer: boolean = false; warnIfTestServer: boolean = false;
zoom: number = 2; zoom: number = 2;
isMapReady: boolean = false;
// Limits and validation properties // Limits and validation properties
endorserLimits: EndorserRateLimits | null = null; endorserLimits: EndorserRateLimits | null = null;
@ -821,6 +825,23 @@ export default class AccountViewView extends Vue {
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
// Fix Leaflet icon issues in modern bundlers
// 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>;
if ("_getIconUrl" in iconDefault) {
delete iconDefault._getIconUrl;
}
L.Icon.Default.mergeOptions({
iconRetinaUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png",
iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png",
shadowUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png",
});
}
} }
/** /**
@ -847,10 +868,16 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = profile.latitude; this.userProfileLatitude = profile.latitude;
this.userProfileLongitude = profile.longitude; this.userProfileLongitude = profile.longitude;
this.includeUserProfileLocation = profile.includeLocation; this.includeUserProfileLocation = profile.includeLocation;
// Initialize map ready state if location is included
if (profile.includeLocation) {
this.isMapReady = false; // Will be set to true when map is ready
}
} else { } else {
// Profile not created yet; leave defaults // Profile not created yet; leave defaults
} }
} catch (error) { } catch (error) {
logger.error("Error loading profile:", error);
this.notify.error( this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE, ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE,
); );
@ -1327,9 +1354,51 @@ export default class AccountViewView extends Vue {
} }
onMapReady(map: L.Map): void { 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 // doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2; const zoom =
map.setView([this.userProfileLatitude, this.userProfileLongitude], zoom); this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
const lat = this.userProfileLatitude || 0;
const lng = this.userProfileLongitude || 0;
map.setView([lat, lng], zoom);
this.isMapReady = true;
logger.debug(
"Map ready state set to true, coordinates:",
[lat, lng],
"zoom:",
zoom,
);
} catch (error) {
logger.error("Error in onMapReady:", error);
this.isMapReady = true; // Set to true even on error to prevent infinite loading
}
}
onMapMounted(): void {
logger.debug("Map component mounted");
// Check if map ref is available
const mapRef = this.$refs.profileMap;
logger.debug("Map ref:", mapRef);
// Try to set map ready after component is mounted
setTimeout(() => {
this.isMapReady = true;
logger.debug("Map ready set to true after mounted");
}, 500);
}
// Fallback method to handle map initialization failures
private handleMapInitFailure(): void {
logger.debug("Starting map initialization timeout (5 seconds)");
setTimeout(() => {
if (!this.isMapReady) {
logger.warn("Map failed to initialize, forcing ready state");
this.isMapReady = true;
} else {
logger.debug("Map initialized successfully, timeout not needed");
}
}, 5000); // 5 second timeout
} }
showProfileInfo(): void { showProfileInfo(): void {
@ -1341,13 +1410,16 @@ export default class AccountViewView extends Vue {
async saveProfile(): Promise<void> { async saveProfile(): Promise<void> {
this.savingProfile = true; this.savingProfile = true;
try {
const profileData: ProfileData = { const profileData: ProfileData = {
description: this.userProfileDesc, description: this.userProfileDesc,
latitude: this.userProfileLatitude, latitude: this.userProfileLatitude,
longitude: this.userProfileLongitude, longitude: this.userProfileLongitude,
includeLocation: this.includeUserProfileLocation, includeLocation: this.includeUserProfileLocation,
}; };
try {
logger.debug("Saving profile data:", profileData);
const success = await this.profileService.saveProfile( const success = await this.profileService.saveProfile(
this.activeDid, this.activeDid,
profileData, profileData,
@ -1358,6 +1430,7 @@ export default class AccountViewView extends Vue {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR); this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
} }
} catch (error) { } catch (error) {
logger.error("Error saving profile:", error);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR); this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
} finally { } finally {
this.savingProfile = false; this.savingProfile = false;
@ -1365,6 +1438,7 @@ export default class AccountViewView extends Vue {
} }
toggleUserProfileLocation(): void { toggleUserProfileLocation(): void {
try {
const updated = this.profileService.toggleProfileLocation({ const updated = this.profileService.toggleProfileLocation({
description: this.userProfileDesc, description: this.userProfileDesc,
latitude: this.userProfileLatitude, latitude: this.userProfileLatitude,
@ -1374,6 +1448,15 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = updated.latitude; this.userProfileLatitude = updated.latitude;
this.userProfileLongitude = updated.longitude; this.userProfileLongitude = updated.longitude;
this.includeUserProfileLocation = updated.includeLocation; this.includeUserProfileLocation = updated.includeLocation;
// Reset map ready state when toggling location
if (!updated.includeLocation) {
this.isMapReady = false;
}
} catch (error) {
logger.error("Error in toggleUserProfileLocation:", error);
this.notify.error("Failed to toggle location setting");
}
} }
confirmEraseLatLong(): void { confirmEraseLatLong(): void {
@ -1401,6 +1484,7 @@ export default class AccountViewView extends Vue {
async deleteProfile(): Promise<void> { async deleteProfile(): Promise<void> {
try { try {
logger.debug("Attempting to delete profile for DID:", this.activeDid);
const success = await this.profileService.deleteProfile(this.activeDid); const success = await this.profileService.deleteProfile(this.activeDid);
if (success) { if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED); this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
@ -1408,13 +1492,22 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = 0; this.userProfileLatitude = 0;
this.userProfileLongitude = 0; this.userProfileLongitude = 0;
this.includeUserProfileLocation = false; this.includeUserProfileLocation = false;
this.isMapReady = false; // Reset map state
logger.debug("Profile deleted successfully, UI state reset");
} else { } else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR); this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
} }
} catch (error) { } catch (error) {
logger.error("Error in deleteProfile component method:", error);
// Show more specific error message if available
if (error instanceof Error) {
this.notify.error(error.message);
} else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR); this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
} }
} }
}
private handleQRCodeClick() { private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) { if (Capacitor.isNativePlatform()) {
@ -1425,9 +1518,47 @@ export default class AccountViewView extends Vue {
} }
onProfileMapClick(event: LeafletMouseEvent) { onProfileMapClick(event: LeafletMouseEvent) {
try {
if (event && event.latlng) {
this.userProfileLatitude = event.latlng.lat; this.userProfileLatitude = event.latlng.lat;
this.userProfileLongitude = event.latlng.lng; this.userProfileLongitude = event.latlng.lng;
} }
} catch (error) {
logger.error("Error in onProfileMapClick:", error);
}
}
onLocationCheckboxChange(): void {
try {
logger.debug(
"Location checkbox changed, new value:",
this.includeUserProfileLocation,
);
if (!this.includeUserProfileLocation) {
// Location checkbox was unchecked, clean up map state
this.isMapReady = false;
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
logger.debug("Location unchecked, map state reset");
} else {
// Location checkbox was checked, start map initialization timeout
this.isMapReady = false;
logger.debug("Location checked, starting map initialization timeout");
// Try to set map ready after a short delay to allow Vue to render
setTimeout(() => {
if (!this.isMapReady) {
logger.debug("Setting map ready after timeout");
this.isMapReady = true;
}
}, 1000); // 1 second delay
this.handleMapInitFailure();
}
} catch (error) {
logger.error("Error in onLocationCheckboxChange:", error);
}
}
// IdentitySection event handlers // IdentitySection event handlers
onEditName() { onEditName() {

10
src/views/ContactQRScanShowView.vue

@ -714,8 +714,16 @@ export default class ContactQRScanShow extends Vue {
// Add new contact // Add new contact
// @ts-expect-error because we're just using the value to store to the DB // @ts-expect-error because we're just using the value to store to the DB
// eslint-disable-next-line @typescript-eslint/no-explicit-any
contact.contactMethods = JSON.stringify( contact.contactMethods = JSON.stringify(
(this as any)._parseJsonField(contact.contactMethods, []), (
this as {
_parseJsonField: (
value: unknown,
defaultValue: unknown[],
) => unknown[];
}
)._parseJsonField(contact.contactMethods, []),
); );
await this.$insertContact(contact); await this.$insertContact(contact);

110
src/views/ContactsView.vue

@ -131,7 +131,7 @@ import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { Capacitor } from "@capacitor/core"; // Capacitor import removed - using PlatformService instead
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
@ -165,13 +165,18 @@ import { GiveSummaryRecord } from "@/interfaces/records";
import { UserInfo } from "@/interfaces/common"; import { UserInfo } from "@/interfaces/common";
import { VerifiableCredential } from "@/interfaces/claims-result"; import { VerifiableCredential } from "@/interfaces/claims-result";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { generateSaveAndActivateIdentity } from "../libs/util"; import {
generateSaveAndActivateIdentity,
contactsToExportJson,
} from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
// No longer needed - using PlatformServiceMixin methods // No longer needed - using PlatformServiceMixin methods
// import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; // import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { isDatabaseError } from "@/interfaces/common";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { APP_SERVER } from "@/constants/app"; import { APP_SERVER } from "@/constants/app";
import { QRNavigationService } from "@/services/QRNavigationService";
import { import {
NOTIFY_CONTACT_NO_INFO, NOTIFY_CONTACT_NO_INFO,
NOTIFY_CONTACTS_ADD_ERROR, NOTIFY_CONTACTS_ADD_ERROR,
@ -197,6 +202,7 @@ import {
NOTIFY_REGISTRATION_ERROR_FALLBACK, NOTIFY_REGISTRATION_ERROR_FALLBACK,
NOTIFY_REGISTRATION_ERROR_GENERIC, NOTIFY_REGISTRATION_ERROR_GENERIC,
NOTIFY_VISIBILITY_ERROR_FALLBACK, NOTIFY_VISIBILITY_ERROR_FALLBACK,
NOTIFY_EXPORT_DATA_PROMPT,
getRegisterPersonSuccessMessage, getRegisterPersonSuccessMessage,
getVisibilitySuccessMessage, getVisibilitySuccessMessage,
getGivesRetrievalErrorMessage, getGivesRetrievalErrorMessage,
@ -376,7 +382,11 @@ export default class ContactsView extends Vue {
"", "",
async (name) => { async (name) => {
await this.addContact({ await this.addContact({
did: (registration.vc.credentialSubject.agent as any).identifier, did: (
registration.vc.credentialSubject.agent as {
identifier: string;
}
).identifier,
name: name, name: name,
registered: true, registered: true,
}); });
@ -387,7 +397,11 @@ export default class ContactsView extends Vue {
async () => { async () => {
// on cancel, will still add the contact // on cancel, will still add the contact
await this.addContact({ await this.addContact({
did: (registration.vc.credentialSubject.agent as any).identifier, did: (
registration.vc.credentialSubject.agent as {
identifier: string;
}
).identifier,
name: "(person who invited you)", name: "(person who invited you)",
registered: true, registered: true,
}); });
@ -396,8 +410,7 @@ export default class ContactsView extends Vue {
this.showOnboardingInfo(); this.showOnboardingInfo();
}, },
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: unknown) {
} catch (error: any) {
const fullError = "Error redeeming invite: " + errorStringForLog(error); const fullError = "Error redeeming invite: " + errorStringForLog(error);
this.$logAndConsole(fullError, true); this.$logAndConsole(fullError, true);
let message = "Got an error sending the invite."; let message = "Got an error sending the invite.";
@ -784,6 +797,9 @@ export default class ContactsView extends Vue {
// Show success notification // Show success notification
this.notify.success(addedMessage); this.notify.success(addedMessage);
// Show export data prompt after successful contact addition
await this.showExportDataPrompt();
} catch (err) { } catch (err) {
this.handleContactAddError(err); this.handleContactAddError(err);
} }
@ -881,21 +897,22 @@ export default class ContactsView extends Vue {
/** /**
* Handle errors during contact addition * Handle errors during contact addition
*/ */
private handleContactAddError(err: any): void { private handleContactAddError(err: unknown): void {
const fullError = const fullError =
"Error when adding contact to storage: " + errorStringForLog(err); "Error when adding contact to storage: " + errorStringForLog(err);
this.$logAndConsole(fullError, true); this.$logAndConsole(fullError, true);
let message = NOTIFY_CONTACT_IMPORT_ERROR.message; let message = NOTIFY_CONTACT_IMPORT_ERROR.message;
if (
(err as any).message?.indexOf("Key already exists in the object store.") > // Use type-safe error checking with our new type guards
-1 if (isDatabaseError(err)) {
) { if (err.message.includes("Key already exists in the object store")) {
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message; message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
} }
if ((err as any).name === "ConstraintError") { if (err.name === "ConstraintError") {
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message; message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
} }
}
this.notify.error(message, TIMEOUTS.LONG); this.notify.error(message, TIMEOUTS.LONG);
} }
@ -1246,19 +1263,76 @@ export default class ContactsView extends Vue {
/** /**
* Handle QR code button click - route to appropriate scanner * Handle QR code button click - route to appropriate scanner
* Uses native scanner on mobile platforms, web scanner otherwise * Uses QRNavigationService to determine scanner type and route
*/ */
public handleQRCodeClick() { public handleQRCodeClick() {
this.$logAndConsole( this.$logAndConsole(
"[ContactsView] handleQRCodeClick method called", "[ContactsView] handleQRCodeClick method called",
false, false,
); );
if (Capacitor.isNativePlatform()) { const qrNavigationService = QRNavigationService.getInstance();
this.$router.push({ name: "contact-qr-scan-full" }); const route = qrNavigationService.getQRScannerRoute();
} else {
this.$router.push({ name: "contact-qr" }); this.$router.push(route);
}
/**
* Show export data prompt after adding a contact
* Prompts user to export their contact data as a backup
*/
private async showExportDataPrompt(): Promise<void> {
setTimeout(() => {
this.$notify(
{
group: "modal",
type: "confirm",
title: NOTIFY_EXPORT_DATA_PROMPT.title,
text: NOTIFY_EXPORT_DATA_PROMPT.message,
onYes: async () => {
await this.exportContactData();
},
yesText: "Export Data",
onNo: async () => {
// User chose not to export - no action needed
},
noText: "Not Now",
},
-1,
);
}, 1000); // Small delay to ensure success notification is shown first
}
/**
* Export contact data to JSON file
* Uses platform service to handle platform-specific export logic
*/
private async exportContactData(): Promise<void> {
// Note that similar code is in DataExportSection.vue exportDatabase()
try {
// Fetch all contacts from database
const allContacts = await this.$contacts();
// Convert contacts to export format
const exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
// Generate filename with current date
const today = new Date();
const dateString = today.toISOString().split("T")[0]; // YYYY-MM-DD format
const fileName = `timesafari-backup-contacts-${dateString}.json`;
// Use platform service to handle export
await this.platformService.writeAndShareFile(fileName, jsonStr);
this.notify.success(
"Contact export completed successfully. Check your downloads or share dialog.",
);
} catch (error) {
logger.error("Export Error:", error);
this.notify.error(
`There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} }
} }
} }

2
src/views/DIDView.vue

@ -490,6 +490,8 @@ export default class DIDView extends Vue {
message += message +=
" Note that they can see your activity, so if you want to hide your activity from them then you should do that first."; " Note that they can see your activity, so if you want to hide your activity from them then you should do that first.";
} }
message +=
" Note that this will also remove anyone with the same DID underneath.";
this.notify.confirm(message, async () => { this.notify.confirm(message, async () => {
await this.deleteContact(contact); await this.deleteContact(contact);
}); });

206
src/views/DatabaseMigration.vue

@ -91,17 +91,12 @@
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="downloadAccount" @click="downloadAccount"
> >
<IconRenderer <font-awesome
v-if="isLoading" v-if="isLoading"
icon-name="spinner" icon="spinner"
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
fill="currentColor"
/>
<IconRenderer
v-else
icon-name="chart"
svg-class="-ml-1 mr-3 h-5 w-5"
/> />
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
Show Account Seed Show Account Seed
</button> </button>
@ -110,17 +105,12 @@
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="downloadSettingsContacts" @click="downloadSettingsContacts"
> >
<IconRenderer <font-awesome
v-if="isLoading" v-if="isLoading"
icon-name="spinner" icon="spinner"
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
fill="currentColor"
/>
<IconRenderer
v-else
icon-name="chart"
svg-class="-ml-1 mr-3 h-5 w-5"
/> />
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
Download Settings &amp; Contacts Download Settings &amp; Contacts
</button> </button>
@ -143,17 +133,12 @@
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="compareDatabases" @click="compareDatabases"
> >
<IconRenderer <font-awesome
v-if="isLoading" v-if="isLoading"
icon-name="spinner" icon="spinner"
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
fill="currentColor"
/>
<IconRenderer
v-else
icon-name="chart"
svg-class="-ml-1 mr-3 h-5 w-5"
/> />
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
Compare Databases Compare Databases
</button> </button>
@ -162,17 +147,12 @@
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="migrateAll" @click="migrateAll"
> >
<IconRenderer <font-awesome
v-if="isLoading" v-if="isLoading"
icon-name="spinner" icon="spinner"
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
fill="currentColor"
/>
<IconRenderer
v-else
icon-name="check"
svg-class="-ml-1 mr-3 h-5 w-5"
/> />
<font-awesome v-else icon="check" class="-ml-1 mr-3 h-5 w-5" />
Migrate All Migrate All
</button> </button>
@ -185,10 +165,9 @@
> >
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome
icon-name="warning" icon="triangle-exclamation"
svg-class="h-5 w-5 text-red-400" class="h-5 w-5 text-red-400"
fill="currentColor"
/> />
</div> </div>
<div class="ml-3"> <div class="ml-3">
@ -207,10 +186,9 @@
> >
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome
icon-name="warning" icon="triangle-exclamation"
svg-class="h-5 w-5 text-red-400" class="h-5 w-5 text-red-400"
fill="currentColor"
/> />
</div> </div>
<div class="ml-3"> <div class="ml-3">
@ -229,10 +207,7 @@
> >
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome icon="check" class="h-5 w-5 text-green-400" />
icon-name="check"
svg-class="h-5 w-5 text-green-400"
/>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-green-800">Success</h3> <h3 class="text-sm font-medium text-green-800">Success</h3>
@ -249,7 +224,7 @@
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="exportComparison" @click="exportComparison"
> >
<IconRenderer icon-name="download" svg-class="-ml-1 mr-3 h-5 w-5" /> <font-awesome icon="download" class="-ml-1 mr-3 h-5 w-5" />
Export Comparison Export Comparison
</button> </button>
@ -258,17 +233,12 @@
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="displayDatabases" @click="displayDatabases"
> >
<IconRenderer <font-awesome
v-if="isLoading" v-if="isLoading"
icon-name="spinner" icon="spinner"
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
fill="currentColor"
/>
<IconRenderer
v-else
icon-name="chart"
svg-class="-ml-1 mr-3 h-5 w-5"
/> />
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
Show Previous Data Show Previous Data
</button> </button>
@ -277,7 +247,7 @@
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="migrateAccounts" @click="migrateAccounts"
> >
<IconRenderer icon-name="lock" svg-class="-ml-1 mr-3 h-5 w-5" /> <font-awesome icon="lock" class="-ml-1 mr-3 h-5 w-5" />
Migrate Accounts Migrate Accounts
</button> </button>
@ -286,7 +256,7 @@
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="migrateSettings" @click="migrateSettings"
> >
<IconRenderer icon-name="settings" svg-class="-ml-1 mr-3 h-5 w-5" /> <font-awesome icon="gear" class="-ml-1 mr-3 h-5 w-5" />
Migrate Settings Migrate Settings
</button> </button>
@ -295,7 +265,7 @@
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="migrateContacts" @click="migrateContacts"
> >
<IconRenderer icon-name="plus" svg-class="-ml-1 mr-3 h-5 w-5" /> <font-awesome icon="plus" class="-ml-1 mr-3 h-5 w-5" />
Migrate Contacts Migrate Contacts
</button> </button>
</div> </div>
@ -316,11 +286,7 @@
> >
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome icon="info" class="h-5 w-5 text-blue-400" />
icon-name="info"
svg-class="h-5 w-5 text-blue-400"
fill="currentColor"
/>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-blue-800"> <h3 class="text-sm font-medium text-blue-800">
@ -357,10 +323,9 @@
<div <div
class="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-white bg-blue-500 hover:bg-blue-400 transition ease-in-out duration-150 cursor-not-allowed" class="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-white bg-blue-500 hover:bg-blue-400 transition ease-in-out duration-150 cursor-not-allowed"
> >
<IconRenderer <font-awesome
icon-name="spinner" icon="spinner"
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
fill="currentColor"
/> />
{{ loadingMessage }} {{ loadingMessage }}
</div> </div>
@ -375,10 +340,7 @@
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome icon="lock" class="h-6 w-6 text-orange-600" />
icon-name="lock"
svg-class="h-6 w-6 text-orange-600"
/>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
@ -398,10 +360,7 @@
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome icon="check" class="h-6 w-6 text-teal-600" />
icon-name="check"
svg-class="h-6 w-6 text-teal-600"
/>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
@ -422,10 +381,7 @@
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome icon="gear" class="h-6 w-6 text-purple-600" />
icon-name="settings"
svg-class="h-6 w-6 text-purple-600"
/>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
@ -445,10 +401,7 @@
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome icon="check" class="h-6 w-6 text-indigo-600" />
icon-name="check"
svg-class="h-6 w-6 text-indigo-600"
/>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
@ -469,9 +422,9 @@
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome
icon-name="chart" icon="chart-line"
svg-class="h-6 w-6 text-blue-600" class="h-6 w-6 text-blue-600"
/> />
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
@ -492,10 +445,7 @@
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<IconRenderer <font-awesome icon="check" class="h-6 w-6 text-green-600" />
icon-name="check"
svg-class="h-6 w-6 text-green-600"
/>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
@ -526,9 +476,9 @@
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="plusCircle" icon="circle-plus"
svg-class="h-5 w-5 text-blue-600 mr-2" class="h-5 w-5 text-blue-600 mr-2"
/> />
<span class="text-sm font-medium text-blue-900">Add</span> <span class="text-sm font-medium text-blue-900">Add</span>
</div> </div>
@ -541,9 +491,9 @@
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="check" icon="check"
svg-class="h-5 w-5 text-yellow-600 mr-2" class="h-5 w-5 text-yellow-600 mr-2"
/> />
<span class="text-sm font-medium text-yellow-900" <span class="text-sm font-medium text-yellow-900"
>Unmodified</span >Unmodified</span
@ -558,9 +508,9 @@
class="flex items-center justify-between p-3 bg-red-50 rounded-lg" class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="trash" icon="trash-can"
svg-class="h-5 w-5 text-red-600 mr-2" class="h-5 w-5 text-red-600 mr-2"
/> />
<span class="text-sm font-medium text-red-900">Keep</span> <span class="text-sm font-medium text-red-900">Keep</span>
</div> </div>
@ -677,9 +627,9 @@
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="plusCircle" icon="circle-plus"
svg-class="h-5 w-5 text-blue-600 mr-2" class="h-5 w-5 text-blue-600 mr-2"
/> />
<span class="text-sm font-medium text-blue-900">Add</span> <span class="text-sm font-medium text-blue-900">Add</span>
</div> </div>
@ -692,9 +642,9 @@
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="edit" icon="pen"
svg-class="h-5 w-5 text-yellow-600 mr-2" class="h-5 w-5 text-yellow-600 mr-2"
/> />
<span class="text-sm font-medium text-yellow-900" <span class="text-sm font-medium text-yellow-900"
>Modify</span >Modify</span
@ -709,9 +659,9 @@
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="check" icon="check"
svg-class="h-5 w-5 text-yellow-600 mr-2" class="h-5 w-5 text-yellow-600 mr-2"
/> />
<span class="text-sm font-medium text-yellow-900" <span class="text-sm font-medium text-yellow-900"
>Unmodified</span >Unmodified</span
@ -726,9 +676,9 @@
class="flex items-center justify-between p-3 bg-red-50 rounded-lg" class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="trash" icon="trash-can"
svg-class="h-5 w-5 text-red-600 mr-2" class="h-5 w-5 text-red-600 mr-2"
/> />
<span class="text-sm font-medium text-red-900">Keep</span> <span class="text-sm font-medium text-red-900">Keep</span>
</div> </div>
@ -868,9 +818,9 @@
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="plusCircle" icon="circle-plus"
svg-class="h-5 w-5 text-blue-600 mr-2" class="h-5 w-5 text-blue-600 mr-2"
/> />
<span class="text-sm font-medium text-blue-900">Add</span> <span class="text-sm font-medium text-blue-900">Add</span>
</div> </div>
@ -883,9 +833,9 @@
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="edit" icon="pen"
svg-class="h-5 w-5 text-yellow-600 mr-2" class="h-5 w-5 text-yellow-600 mr-2"
/> />
<span class="text-sm font-medium text-yellow-900" <span class="text-sm font-medium text-yellow-900"
>Modify</span >Modify</span
@ -900,9 +850,9 @@
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="check" icon="check"
svg-class="h-5 w-5 text-yellow-600 mr-2" class="h-5 w-5 text-yellow-600 mr-2"
/> />
<span class="text-sm font-medium text-yellow-900" <span class="text-sm font-medium text-yellow-900"
>Unmodified</span >Unmodified</span
@ -917,9 +867,9 @@
class="flex items-center justify-between p-3 bg-red-50 rounded-lg" class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
> >
<div class="flex items-center"> <div class="flex items-center">
<IconRenderer <font-awesome
icon-name="trash" icon="trash-can"
svg-class="h-5 w-5 text-red-600 mr-2" class="h-5 w-5 text-red-600 mr-2"
/> />
<span class="text-sm font-medium text-red-900">Keep</span> <span class="text-sm font-medium text-red-900">Keep</span>
</div> </div>
@ -1067,7 +1017,6 @@ import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { Router } from "vue-router"; import { Router } from "vue-router";
import IconRenderer from "../components/IconRenderer.vue";
import { import {
compareDatabases, compareDatabases,
migrateSettings, migrateSettings,
@ -1104,9 +1053,6 @@ import { logger } from "../utils/logger";
*/ */
@Component({ @Component({
name: "DatabaseMigration", name: "DatabaseMigration",
components: {
IconRenderer,
},
}) })
export default class DatabaseMigration extends Vue { export default class DatabaseMigration extends Vue {
$router!: Router; $router!: Router;

4
src/views/DeepLinkErrorView.vue

@ -47,7 +47,7 @@ import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { import {
VALID_DEEP_LINK_ROUTES, VALID_DEEP_LINK_ROUTES,
deepLinkSchemas, deepLinkPathSchemas,
} from "../interfaces/deepLinks"; } from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db/databaseUtil"; import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@ -56,7 +56,7 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
// an object with the route as the key and the first param name as the value // an object with the route as the key and the first param name as the value
const deepLinkSchemaKeys = Object.fromEntries( const deepLinkSchemaKeys = Object.fromEntries(
Object.entries(deepLinkSchemas).map(([route, schema]) => { Object.entries(deepLinkPathSchemas).map(([route, schema]) => {
const param = Object.keys(schema.shape)[0]; const param = Object.keys(schema.shape)[0];
return [route, param]; return [route, param];
}), }),

10
src/views/DiscoverView.vue

@ -458,7 +458,9 @@ export default class DiscoverView extends Vue {
if (this.isLocalActive) { if (this.isLocalActive) {
await this.searchLocal(); await this.searchLocal();
} else if (this.isMappedActive) { } else if (this.isMappedActive) {
const mapRef = this.$refs.projectMap as any; const mapRef = this.$refs.projectMap as {
leafletObject: L.Map;
};
this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
} else { } else {
await this.searchAll(); await this.searchAll();
@ -518,11 +520,11 @@ export default class DiscoverView extends Vue {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: unknown) {
} catch (e: any) {
logger.error("Error with search all: " + errorStringForLog(e)); logger.error("Error with search all: " + errorStringForLog(e));
this.notify.error( this.notify.error(
e.userMessage || NOTIFY_DISCOVER_SEARCH_ERROR.message, (e as { userMessage?: string })?.userMessage ||
NOTIFY_DISCOVER_SEARCH_ERROR.message,
TIMEOUTS.LONG, TIMEOUTS.LONG,
); );
} finally { } finally {

22
src/views/HelpView.vue

@ -565,22 +565,22 @@
<h2 class="text-xl font-semibold">What app version is this?</h2> <h2 class="text-xl font-semibold">What app version is this?</h2>
<p>{{ package.version }} ({{ commitHash }})</p> <p>{{ package.version }} ({{ commitHash }})</p>
<div v-if="Capacitor.isNativePlatform()"> <div v-if="isCapacitor">
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
Do I have the latest version? Do I have the latest version?
</h2> </h2>
<p v-if="Capacitor.getPlatform() === 'ios'"> <p v-if="capabilities.isIOS">
<a href="https://apps.apple.com/us/app/time-safari/id6742664907" target="_blank" class="text-blue-500"> <a href="https://apps.apple.com/us/app/time-safari/id6742664907" target="_blank" class="text-blue-500">
Check the App Store. Check the App Store.
</a> </a>
</p> </p>
<p v-else-if="Capacitor.getPlatform() === 'android'"> <p v-else-if="!capabilities.isIOS && capabilities.isMobile">
<a href="https://play.google.com/store/apps/details?id=app.timesafari.app" target="_blank" class="text-blue-500"> <a href="https://play.google.com/store/apps/details?id=app.timesafari.app" target="_blank" class="text-blue-500">
Check the Play Store. Check the Play Store.
</a> </a>
</p> </p>
<p v-else> <p v-else>
Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized. Sorry, your platform is not recognized.
</p> </p>
</div> </div>
</div> </div>
@ -592,12 +592,13 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { Capacitor } from "@capacitor/core"; // Capacitor import removed - using QRNavigationService instead
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER } from "../constants/app"; import { APP_SERVER } from "../constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { QRNavigationService } from "@/services/QRNavigationService";
/** /**
* HelpView.vue - Comprehensive Help System Component * HelpView.vue - Comprehensive Help System Component
@ -643,7 +644,7 @@ export default class HelpView extends Vue {
showVerifiable = false; showVerifiable = false;
APP_SERVER = APP_SERVER; APP_SERVER = APP_SERVER;
Capacitor = Capacitor; // Capacitor reference removed - using QRNavigationService instead
// Ideally, we put no functionality in here, especially in the setup, // Ideally, we put no functionality in here, especially in the setup,
// because we never want this page to have a chance of throwing an error. // because we never want this page to have a chance of throwing an error.
@ -711,11 +712,10 @@ export default class HelpView extends Vue {
* @private * @private
*/ */
private handleQRCodeClick(): void { private handleQRCodeClick(): void {
if (Capacitor.isNativePlatform()) { const qrNavigationService = QRNavigationService.getInstance();
this.$router.push({ name: "contact-qr-scan-full" }); const route = qrNavigationService.getQRScannerRoute();
} else {
this.$router.push({ name: "contact-qr" }); this.$router.push(route);
}
} }
/** /**

110
src/views/HomeView.vue

@ -476,7 +476,7 @@ export default class HomeView extends Vue {
// Re-initialize identity with new settings (loads settings internally) // Re-initialize identity with new settings (loads settings internally)
await this.initializeIdentity(); await this.initializeIdentity();
} else { } else {
logger.info( logger.debug(
"[HomeView Settings Trace] 📍 DID unchanged, skipping re-initialization", "[HomeView Settings Trace] 📍 DID unchanged, skipping re-initialization",
); );
} }
@ -756,17 +756,34 @@ export default class HomeView extends Vue {
* Called by FeedFilters component when filters change * Called by FeedFilters component when filters change
*/ */
async reloadFeedOnChange() { async reloadFeedOnChange() {
const settings = await this.$accountSettings(this.activeDid, { logger.debug("[HomeView] 🔄 reloadFeedOnChange() called - refreshing feed");
filterFeedByVisible: false,
filterFeedByNearby: false, // Get current settings without overriding with defaults
const settings = await this.$accountSettings(this.activeDid);
logger.debug("[HomeView] 📊 Current filter settings:", {
filterFeedByVisible: settings.filterFeedByVisible,
filterFeedByNearby: settings.filterFeedByNearby,
searchBoxes: settings.searchBoxes?.length || 0,
}); });
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
logger.debug("[HomeView] 🎯 Updated filter states:", {
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
});
this.feedData = []; this.feedData = [];
this.feedPreviousOldestId = undefined; this.feedPreviousOldestId = undefined;
logger.debug("[HomeView] 🧹 Cleared feed data, calling updateAllFeed()");
await this.updateAllFeed(); await this.updateAllFeed();
logger.debug("[HomeView] ✅ Feed refresh completed");
} }
/** /**
@ -845,6 +862,14 @@ export default class HomeView extends Vue {
* - this.feedLastViewedClaimId (via updateFeedLastViewedId) * - this.feedLastViewedClaimId (via updateFeedLastViewedId)
*/ */
async updateAllFeed() { async updateAllFeed() {
logger.debug("[HomeView] 🚀 updateAllFeed() called", {
isFeedLoading: this.isFeedLoading,
currentFeedDataLength: this.feedData.length,
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
});
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true; let endOfResults = true;
@ -853,21 +878,37 @@ export default class HomeView extends Vue {
this.apiServer, this.apiServer,
this.feedPreviousOldestId, this.feedPreviousOldestId,
); );
logger.debug("[HomeView] 📡 Retrieved gives from API", {
resultsCount: results.data.length,
endOfResults,
});
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false; endOfResults = false;
// gather any contacts that user has blocked from view // gather any contacts that user has blocked from view
await this.processFeedResults(results.data); await this.processFeedResults(results.data);
await this.updateFeedLastViewedId(results.data); await this.updateFeedLastViewedId(results.data);
logger.debug("[HomeView] 📝 Processed feed results", {
processedCount: this.feedData.length,
});
} }
} catch (e) { } catch (e) {
logger.error("[HomeView] ❌ Error in updateAllFeed:", e);
this.handleFeedError(e); this.handleFeedError(e);
} }
if (this.feedData.length === 0 && !endOfResults) { if (this.feedData.length === 0 && !endOfResults) {
logger.debug("[HomeView] 🔄 No results after filtering, retrying...");
await this.updateAllFeed(); await this.updateAllFeed();
} }
this.isFeedLoading = false; this.isFeedLoading = false;
logger.debug("[HomeView] ✅ updateAllFeed() completed", {
finalFeedDataLength: this.feedData.length,
isFeedLoading: this.isFeedLoading,
});
} }
/** /**
@ -892,12 +933,35 @@ export default class HomeView extends Vue {
* @param records Array of feed records to process * @param records Array of feed records to process
*/ */
private async processFeedResults(records: GiveSummaryRecord[]) { private async processFeedResults(records: GiveSummaryRecord[]) {
logger.debug("[HomeView] 📝 Processing feed results:", {
inputRecords: records.length,
currentFilters: {
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
},
});
let processedCount = 0;
let filteredCount = 0;
for (const record of records) { for (const record of records) {
const processedRecord = await this.processRecord(record); const processedRecord = await this.processRecord(record);
if (processedRecord) { if (processedRecord) {
this.feedData.push(processedRecord); this.feedData.push(processedRecord);
processedCount++;
} else {
filteredCount++;
} }
} }
logger.debug("[HomeView] 📊 Feed processing results:", {
processed: processedCount,
filtered: filteredCount,
total: records.length,
finalFeedLength: this.feedData.length,
});
this.feedPreviousOldestId = records[records.length - 1].jwtId; this.feedPreviousOldestId = records[records.length - 1].jwtId;
} }
@ -931,7 +995,7 @@ export default class HomeView extends Vue {
* - this.feedData (via createFeedRecord) * - this.feedData (via createFeedRecord)
* *
* @param record The record to process * @param record The record to process
* @returns Processed record with contact info if it passes filters, null otherwise * @returns Processed record if it passes filters, null otherwise
*/ */
private async processRecord( private async processRecord(
record: GiveSummaryRecord, record: GiveSummaryRecord,
@ -941,13 +1005,28 @@ export default class HomeView extends Vue {
const recipientDid = this.extractRecipientDid(claim); const recipientDid = this.extractRecipientDid(claim);
const fulfillsPlan = await this.getFulfillsPlan(record); const fulfillsPlan = await this.getFulfillsPlan(record);
// Log record details for debugging
logger.debug("[HomeView] 🔍 Processing record:", {
recordId: record.jwtId,
hasFulfillsPlan: !!fulfillsPlan,
fulfillsPlanHandleId: record.fulfillsPlanHandleId,
filters: {
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
isFeedFilteredByVisible: this.isFeedFilteredByNearby,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
},
});
if (!this.shouldIncludeRecord(record, fulfillsPlan)) { if (!this.shouldIncludeRecord(record, fulfillsPlan)) {
logger.debug("[HomeView] ❌ Record filtered out:", record.jwtId);
return null; return null;
} }
const provider = this.extractProvider(claim); const provider = this.extractProvider(claim);
const providedByPlan = await this.getProvidedByPlan(provider); const providedByPlan = await this.getProvidedByPlan(provider);
logger.debug("[HomeView] ✅ Record included:", record.jwtId);
return this.createFeedRecord( return this.createFeedRecord(
record, record,
claim, claim,
@ -1096,6 +1175,22 @@ export default class HomeView extends Vue {
} }
} }
// Add debug logging for nearby filter
if (this.isFeedFilteredByNearby && record.fulfillsPlanHandleId) {
logger.debug("[HomeView] 🔍 Nearby filter check:", {
recordId: record.jwtId,
hasFulfillsPlan: !!fulfillsPlan,
hasLocation: !!(fulfillsPlan?.locLat && fulfillsPlan?.locLon),
location: fulfillsPlan
? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon }
: null,
inSearchBox: fulfillsPlan
? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon)
: null,
finalResult: anyMatch,
});
}
return anyMatch; return anyMatch;
} }
@ -1531,7 +1626,10 @@ export default class HomeView extends Vue {
* Called by template click handler * Called by template click handler
*/ */
openFeedFilters() { openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange); (this.$refs.feedFilters as FeedFilters).open(
this.reloadFeedOnChange,
this.activeDid,
);
} }
/** /**

4
src/views/IdentitySwitcherView.vue

@ -234,7 +234,9 @@ export default class IdentitySwitcherView extends Vue {
{ {
did, did,
settingsKeys: Object.keys(newSettings).filter( settingsKeys: Object.keys(newSettings).filter(
(k) => (newSettings as any)[k] !== undefined, (k) =>
k in newSettings &&
newSettings[k as keyof typeof newSettings] !== undefined,
), ),
}, },
); );

5
src/views/ImportAccountView.vue

@ -221,10 +221,11 @@ export default class ImportAccountView extends Vue {
this.notify.success("Account imported successfully!", TIMEOUTS.STANDARD); this.notify.success("Account imported successfully!", TIMEOUTS.STANDARD);
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
} catch (error: any) { } catch (error: unknown) {
this.$logError("Import failed: " + error); this.$logError("Import failed: " + error);
this.notify.error( this.notify.error(
error.message || "Failed to import account.", (error instanceof Error ? error.message : String(error)) ||
"Failed to import account.",
TIMEOUTS.LONG, TIMEOUTS.LONG,
); );
} }

2
src/views/OnboardMeetingListView.vue

@ -251,7 +251,7 @@ export default class OnboardMeetingListView extends Vue {
if (response2.data?.data) { if (response2.data?.data) {
this.meetings = response2.data.data; this.meetings = response2.data.data;
} }
} catch (error: any) { } catch (error: unknown) {
this.$logAndConsole( this.$logAndConsole(
"Error fetching meetings: " + errorStringForLog(error), "Error fetching meetings: " + errorStringForLog(error),
true, true,

2
src/views/OnboardMeetingMembersView.vue

@ -113,7 +113,7 @@ export default class OnboardMeetingMembersView extends Vue {
try { try {
// Identity creation should be handled by router guard, but keep as fallback for meeting setup // Identity creation should be handled by router guard, but keep as fallback for meeting setup
if (!this.activeDid) { if (!this.activeDid) {
logger.info( this.$logAndConsole(
"[OnboardMeetingMembersView] No active DID found, creating identity as fallback for meeting setup", "[OnboardMeetingMembersView] No active DID found, creating identity as fallback for meeting setup",
); );
this.activeDid = await generateSaveAndActivateIdentity(); this.activeDid = await generateSaveAndActivateIdentity();

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

Loading…
Cancel
Save