Browse Source

Merge branch 'master' into imagemagick-anrdoid

pull/165/head
Matthew Raymer 10 hours ago
parent
commit
3aa57c5ced
  1. 63
      .cursor/rules/adr_template.mdc
  2. 172
      .cursor/rules/app/architectural_decision_record.mdc
  3. 0
      .cursor/rules/app/timesafari.mdc
  4. 287
      .cursor/rules/architectural_decision_record.mdc
  5. 110
      .cursor/rules/base_context.mdc
  6. 5
      .cursor/rules/database/absurd-sql.mdc
  7. 5
      .cursor/rules/database/legacy_dexie.mdc
  8. 5
      .cursor/rules/development/development_guide.mdc
  9. 108
      .cursor/rules/development/type_safety_guide.mdc
  10. 0
      .cursor/rules/docs/documentation.mdc
  11. 0
      .cursor/rules/docs/markdown.mdc
  12. 0
      .cursor/rules/features/camera-implementation.mdc
  13. 76
      .cursor/rules/investigation_report_example.mdc
  14. 6
      .cursor/rules/legacy_dexie.mdc
  15. 170
      .cursor/rules/research_diagnostic.mdc
  16. 178
      .cursor/rules/software_development.mdc
  17. 0
      .cursor/rules/workflow/version_control.mdc
  18. 2
      .eslintrc.js
  19. 3
      package.json
  20. 2
      scripts/build-android.sh
  21. 2
      scripts/build-electron.sh
  22. 2
      scripts/build-ios.sh
  23. 2
      scripts/build-web.sh
  24. 10
      scripts/common.sh
  25. 36
      scripts/test-env.sh
  26. 103
      scripts/type-safety-check.sh
  27. 65
      src/App.vue
  28. 10
      src/components/ActivityListItem.vue
  29. 2
      src/components/DataExportSection.vue
  30. 9
      src/components/GiftedDialog.vue
  31. 4
      src/components/ImageMethodDialog.vue
  32. 5
      src/components/UsageLimitsSection.vue
  33. 8
      src/constants/notifications.ts
  34. 78
      src/interfaces/common.ts
  35. 99
      src/services/QRNavigationService.ts
  36. 12
      src/test/index.ts
  37. 4
      src/views/ClaimView.vue
  38. 4
      src/views/ContactQRScanFullView.vue
  39. 10
      src/views/ContactQRScanShowView.vue
  40. 116
      src/views/ContactsView.vue
  41. 1
      src/views/DIDView.vue
  42. 10
      src/views/DiscoverView.vue
  43. 22
      src/views/HelpView.vue
  44. 4
      src/views/IdentitySwitcherView.vue
  45. 5
      src/views/ImportAccountView.vue
  46. 4
      src/views/ImportDerivedAccountView.vue
  47. 2
      src/views/OnboardMeetingListView.vue
  48. 6
      src/views/OnboardMeetingSetupView.vue
  49. 24
      src/views/ProjectsView.vue
  50. 2
      test-playwright/50-record-offer.spec.ts
  51. 4
      test-playwright/60-new-activity.spec.ts

63
.cursor/rules/adr_template.mdc

@ -0,0 +1,63 @@
# 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
---

172
.cursor/rules/app/architectural_decision_record.mdc

@ -0,0 +1,172 @@
---
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
### 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?

0
.cursor/rules/timesafari.mdc → .cursor/rules/app/timesafari.mdc

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.

110
.cursor/rules/base_context.mdc

@ -0,0 +1,110 @@
```json
{
"coaching_level": "standard",
"socratic_max_questions": 7,
"verbosity": "normal",
"timebox_minutes": null,
"format_enforcement": "strict"
}
```
# Base Context — Human Competence First
## 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.
- [ ] Uncertainties/assumptions surfaced.
- [ ] No disallowed content.

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

@ -1,7 +1,6 @@
--- ---
description: globs: **/db/databaseUtil.ts, **/interfaces/absurd-sql.d.ts, **/src/registerSQLWorker.js, **/services/AbsurdSqlDatabaseService.ts
globs: alwaysApply: false
alwaysApply: true
--- ---
# Absurd SQL - Cursor Development Guide # Absurd SQL - Cursor Development Guide

5
.cursor/rules/database/legacy_dexie.mdc

@ -0,0 +1,5 @@
---
globs: **/databaseUtil.ts,**/AccountViewView.vue,**/ContactsView.vue,**/DatabaseMigration.vue,**/NewIdentifierView.vue
alwaysApply: false
---
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

108
.cursor/rules/development/type_safety_guide.mdc

@ -0,0 +1,108 @@
---
globs: **/src/**/*,**/scripts/**/*,**/electron/**/*
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-16
**Status**: 🎯 **ACTIVE**
## 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]`.
## 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/

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

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

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

76
.cursor/rules/investigation_report_example.mdc

@ -0,0 +1,76 @@
# Investigation Report 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
description:
globs:
alwaysApply: false
---

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.

170
.cursor/rules/research_diagnostic.mdc

@ -0,0 +1,170 @@
---
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`

178
.cursor/rules/software_development.mdc

@ -0,0 +1,178 @@
---
alwaysApply: true
---
# Software Development Ruleset
## 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?
## 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
### 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
## 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?"**
## 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
# Software Development Ruleset
## 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?
## 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
### 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
## 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?"**
## 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

0
.cursor/rules/version_control.mdc → .cursor/rules/workflow/version_control.mdc

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": "^_" }]

3
package.json

@ -8,6 +8,7 @@
"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",
@ -57,6 +58,8 @@
"build:web:docker:test": "./scripts/build-web.sh --docker:test", "build:web:docker:test": "./scripts/build-web.sh --docker:test",
"build:web:docker:prod": "./scripts/build-web.sh --docker:prod", "build:web:docker:prod": "./scripts/build-web.sh --docker:prod",
"build:web:serve": "./scripts/build-web.sh --serve", "build:web:serve": "./scripts/build-web.sh --serve",
"build:web:serve:test": "./scripts/build-web.sh --serve --test",
"build:web:serve:prod": "./scripts/build-web.sh --serve --prod",
"docker:up": "docker-compose up", "docker:up": "docker-compose up",
"docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test", "docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test",
"docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production", "docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production",

2
scripts/build-android.sh

@ -184,7 +184,7 @@ log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE" log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build # Setup environment for Capacitor build
setup_build_env "capacitor" setup_build_env "capacitor" "$BUILD_MODE"
# Override API servers for Android development # Override API servers for Android development
if [ "$BUILD_MODE" = "development" ]; then if [ "$BUILD_MODE" = "development" ]; then

2
scripts/build-electron.sh

@ -339,7 +339,7 @@ main_electron_build() {
fi fi
# Setup environment # Setup environment
setup_build_env "electron" setup_build_env "electron" "$BUILD_MODE"
setup_app_directories setup_app_directories
load_env_file ".env" load_env_file ".env"

2
scripts/build-ios.sh

@ -311,7 +311,7 @@ log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE" log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build # Setup environment for Capacitor build
setup_build_env "capacitor" setup_build_env "capacitor" "$BUILD_MODE"
# Override API servers for iOS development when custom IP is specified # Override API servers for iOS development when custom IP is specified
if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then

2
scripts/build-web.sh

@ -332,7 +332,7 @@ log_info "Serve build: $SERVE_BUILD"
validate_web_environment validate_web_environment
# Setup environment for web build # Setup environment for web build
setup_build_env "web" setup_build_env "web" "$BUILD_MODE"
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories

10
scripts/common.sh

@ -174,9 +174,9 @@ validate_env_vars() {
# Function to set environment variables for different build types # Function to set environment variables for different build types
setup_build_env() { setup_build_env() {
local build_type="$1" local build_type="$1"
local production="${2:-false}" local build_mode="${2:-development}"
log_info "Setting up environment for $build_type build" log_info "Setting up environment for $build_type build (mode: $build_mode)"
# Get git hash for versioning # Get git hash for versioning
local git_hash=$(get_git_hash) local git_hash=$(get_git_hash)
@ -204,19 +204,19 @@ setup_build_env() {
esac esac
# Set API server environment variables based on build mode # Set API server environment variables based on build mode
if [ "$BUILD_MODE" = "development" ]; then if [ "$build_mode" = "development" ]; then
# For Capacitor development, use localhost by default # For Capacitor development, use localhost by default
# Android builds will override this in build-android.sh # Android builds will override this in build-android.sh
export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000" export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000" export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000"
log_debug "Development mode: Using localhost for Endorser and Partner APIs" log_debug "Development mode: Using localhost for Endorser and Partner APIs"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app" export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
elif [ "$BUILD_MODE" = "test" ]; then elif [ "$build_mode" = "test" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="https://test-api.endorser.ch" export VITE_DEFAULT_ENDORSER_API_SERVER="https://test-api.endorser.ch"
export VITE_DEFAULT_PARTNER_API_SERVER="https://test-partner-api.endorser.ch" export VITE_DEFAULT_PARTNER_API_SERVER="https://test-partner-api.endorser.ch"
log_debug "Test mode: Using test Endorser and Partner APIs" log_debug "Test mode: Using test Endorser and Partner APIs"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app" export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
elif [ "$BUILD_MODE" = "production" ]; then elif [ "$build_mode" = "production" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="https://api.endorser.ch" export VITE_DEFAULT_ENDORSER_API_SERVER="https://api.endorser.ch"
export VITE_DEFAULT_PARTNER_API_SERVER="https://partner-api.endorser.ch" export VITE_DEFAULT_PARTNER_API_SERVER="https://partner-api.endorser.ch"
log_debug "Production mode: Using production API servers" log_debug "Production mode: Using production API servers"

36
scripts/test-env.sh

@ -17,34 +17,40 @@ parse_args "$@"
print_header "Environment Variable Test" print_header "Environment Variable Test"
log_info "Testing environment variable handling at $(date)" log_info "Testing environment variable handling at $(date)"
# Test 1: Capacitor environment # Test 1: Capacitor environment (development)
log_info "Test 1: Setting up Capacitor environment..." log_info "Test 1: Setting up Capacitor environment (development mode)..."
setup_build_env "capacitor" setup_build_env "capacitor" "development"
print_env_vars "VITE_" print_env_vars "VITE_"
echo "" echo ""
# Test 2: Web environment # Test 2: Web environment (development)
log_info "Test 2: Setting up Web environment..." log_info "Test 2: Setting up Web environment (development mode)..."
setup_build_env "web" setup_build_env "web" "development"
print_env_vars "VITE_" print_env_vars "VITE_"
echo "" echo ""
# Test 3: Production Capacitor environment # Test 3: Capacitor test environment
log_info "Test 3: Setting up Production Capacitor environment..." log_info "Test 3: Setting up Capacitor environment (test mode)..."
setup_build_env "capacitor" "true" setup_build_env "capacitor" "test"
print_env_vars "VITE_" print_env_vars "VITE_"
echo "" echo ""
# Test 4: Application directories # Test 4: Capacitor production environment
log_info "Test 4: Setting up application directories..." log_info "Test 4: Setting up Capacitor environment (production mode)..."
setup_build_env "capacitor" "production"
print_env_vars "VITE_"
echo ""
# Test 5: Application directories
log_info "Test 5: Setting up application directories..."
setup_app_directories setup_app_directories
# Test 5: Load .env file (if it exists) # Test 6: Load .env file (if it exists)
log_info "Test 5: Loading .env file..." log_info "Test 6: Loading .env file..."
load_env_file ".env" load_env_file ".env"
# Test 6: Git hash # Test 7: Git hash
log_info "Test 6: Getting git hash..." log_info "Test 7: Getting git hash..."
GIT_HASH=$(get_git_hash) GIT_HASH=$(get_git_hash)
log_info "Git hash: $GIT_HASH" log_info "Git hash: $GIT_HASH"

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

65
src/App.vue

@ -27,9 +27,13 @@
v-if="notification.type === 'toast'" v-if="notification.type === 'toast'"
class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md" class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
> >
<div class="w-full px-4 py-3"> <div class="w-full px-4 py-3 overflow-hidden">
<span class="font-semibold">{{ notification.title }}</span> <h4 class="font-semibold text-ellipsis overflow-hidden">
<p class="text-sm">{{ notification.text }}</p> {{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
</div> </div>
</div> </div>
@ -46,9 +50,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-slate-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
@ -72,9 +82,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-emerald-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
@ -98,9 +114,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-amber-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
@ -124,9 +146,15 @@
></font-awesome> ></font-awesome>
</div> </div>
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900"> <div
<span class="font-semibold">{{ notification.title }}</span> class="relative w-full pl-4 pr-8 py-2 text-rose-900 overflow-hidden"
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p> >
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button <button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600" class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
@ -349,13 +377,6 @@ export default class App extends Vue {
stopAsking = false; stopAsking = false;
truncateLongWords(sentence: string) {
return sentence
.split(" ")
.map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word))
.join(" ");
}
async turnOffNotifications( async turnOffNotifications(
notification: NotificationIface, notification: NotificationIface,
): Promise<boolean> { ): Promise<boolean> {

10
src/components/ActivityListItem.vue

@ -252,7 +252,7 @@ import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import { isHiddenDid } from "../libs/endorserServer"; import { isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers } from "@/utils/notify"; import { createNotifyHelpers, NotifyFunction } from "@/utils/notify";
import { import {
NOTIFY_PERSON_HIDDEN, NOTIFY_PERSON_HIDDEN,
NOTIFY_UNKNOWN_PERSON, NOTIFY_UNKNOWN_PERSON,
@ -273,7 +273,7 @@ export default class ActivityListItem extends Vue {
isHiddenDid = isHiddenDid; isHiddenDid = isHiddenDid;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
@ -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
} }

9
src/components/GiftedDialog.vue

@ -80,7 +80,7 @@ import EntitySelectionStep from "../components/EntitySelectionStep.vue";
import GiftDetailsStep from "../components/GiftDetailsStep.vue"; import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records"; import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import { import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT, NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION, NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
@ -98,7 +98,7 @@ import {
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
/** /**
@ -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);
} }

4
src/components/ImageMethodDialog.vue

@ -282,7 +282,7 @@ import {
NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT, NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT,
createImageDialogCameraErrorMessage, createImageDialogCameraErrorMessage,
} from "../constants/notifications"; } from "../constants/notifications";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify"; import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "../utils/notify";
const inputImageFileNameRef = ref<Blob>(); const inputImageFileNameRef = ref<Blob>();
@ -291,7 +291,7 @@ const inputImageFileNameRef = ref<Blob>();
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class ImageMethodDialog extends Vue { export default class ImageMethodDialog extends Vue {
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
$router!: Router; $router!: Router;
notify = createNotifyHelpers(this.$notify); notify = createNotifyHelpers(this.$notify);

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;

78
src/interfaces/common.ts

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

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

12
src/test/index.ts

@ -15,10 +15,15 @@ const TEST_USER_0_MNEMONIC =
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage"; "rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage";
export async function testBecomeUser0() { export async function testBecomeUser0() {
const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC); const [addr, privateHex, publicHex, deriPath] =
deriveAddress(TEST_USER_0_MNEMONIC);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
await saveNewIdentity(identity0, TEST_USER_0_MNEMONIC, DEFAULT_ROOT_DERIVATION_PATH); await saveNewIdentity(
identity0,
TEST_USER_0_MNEMONIC,
DEFAULT_ROOT_DERIVATION_PATH,
);
const platformService = await PlatformServiceFactory.getInstance(); const platformService = await PlatformServiceFactory.getInstance();
await platformService.updateDidSpecificSettings(identity0.did, { await platformService.updateDidSpecificSettings(identity0.did, {
isRegistered: true, isRegistered: true,
@ -35,7 +40,8 @@ export async function testBecomeUser0() {
* @throws Error if registration fails or database access fails * @throws Error if registration fails or database access fails
*/ */
export async function testServerRegisterUser() { export async function testServerRegisterUser() {
const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC); const [addr, privateHex, publicHex, deriPath] =
deriveAddress(TEST_USER_0_MNEMONIC);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);

4
src/views/ClaimView.vue

@ -724,6 +724,8 @@ export default class ClaimView extends Vue {
} }
async created() { async created() {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
@ -754,8 +756,6 @@ export default class ClaimView extends Vue {
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
this.canShare = !!navigator.share; this.canShare = !!navigator.share;
this.notify = createNotifyHelpers(this.$notify);
} }
// insert a space before any capital letters except the initial letter // insert a space before any capital letters except the initial letter

4
src/views/ContactQRScanFullView.vue

@ -143,7 +143,7 @@ import {
QR_TIMEOUT_STANDARD, QR_TIMEOUT_STANDARD,
QR_TIMEOUT_LONG, QR_TIMEOUT_LONG,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { createNotifyHelpers } from "../utils/notify"; import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
interface QRScanResult { interface QRScanResult {
rawValue?: string; rawValue?: string;
@ -191,7 +191,7 @@ interface IUserNameDialog {
* @since 2024 * @since 2024
*/ */
export default class ContactQRScanFull extends Vue { export default class ContactQRScanFull extends Vue {
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
$router!: Router; $router!: Router;
// Notification helper system // Notification helper system

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);

116
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,20 +897,21 @@ 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"}`,
);
} }
} }
} }

1
src/views/DIDView.vue

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

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);
}
} }
/** /**

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

4
src/views/ImportDerivedAccountView.vue

@ -87,7 +87,7 @@ import {
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { Account, AccountEncrypted } from "../db/tables/accounts"; import { Account, AccountEncrypted } from "../db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import { import {
NOTIFY_ACCOUNT_DERIVATION_SUCCESS, NOTIFY_ACCOUNT_DERIVATION_SUCCESS,
NOTIFY_ACCOUNT_DERIVATION_ERROR, NOTIFY_ACCOUNT_DERIVATION_ERROR,
@ -100,7 +100,7 @@ import {
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
$route!: RouteLocationNormalizedLoaded; $route!: RouteLocationNormalizedLoaded;
$router!: Router; $router!: Router;
$notify!: (notification: any, timeout?: number) => void; $notify!: NotifyFunction;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;

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,

6
src/views/OnboardMeetingSetupView.vue

@ -345,7 +345,9 @@ export default class OnboardMeetingView extends Vue {
} }
async created() { async created() {
this.notify = createNotifyHelpers(this.$notify as any); this.notify = createNotifyHelpers(
this.$notify as Parameters<typeof createNotifyHelpers>[0],
);
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
@ -419,7 +421,7 @@ export default class OnboardMeetingView extends Vue {
} else { } else {
this.newOrUpdatedMeetingInputs = this.blankMeeting(); this.newOrUpdatedMeetingInputs = this.blankMeeting();
} }
} catch (error: any) { } catch (error: unknown) {
this.newOrUpdatedMeetingInputs = this.blankMeeting(); this.newOrUpdatedMeetingInputs = this.blankMeeting();
} }
} }

24
src/views/ProjectsView.vue

@ -264,7 +264,7 @@
import { AxiosRequestConfig } from "axios"; import { AxiosRequestConfig } from "axios";
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 { Capacitor } from "@capacitor/core"; // Capacitor import removed - using QRNavigationService instead
import { NotificationIface } from "../constants/app"; import { NotificationIface } from "../constants/app";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
@ -281,6 +281,7 @@ import { OnboardPage, iconForUnitCode } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { QRNavigationService } from "@/services/QRNavigationService";
import { import {
NOTIFY_NO_ACCOUNT_ERROR, NOTIFY_NO_ACCOUNT_ERROR,
NOTIFY_PROJECT_LOAD_ERROR, NOTIFY_PROJECT_LOAD_ERROR,
@ -464,8 +465,10 @@ export default class ProjectsView extends Vue {
); );
this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG); this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG);
} }
} catch (error: any) { } catch (error: unknown) {
logger.error("Got error loading plans:", error.message || error); const errorMessage =
error instanceof Error ? error.message : String(error);
logger.error("Got error loading plans:", errorMessage);
this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG); this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG);
} finally { } finally {
this.isLoading = false; this.isLoading = false;
@ -578,8 +581,10 @@ export default class ProjectsView extends Vue {
); );
this.notify.error(NOTIFY_OFFERS_LOAD_ERROR.message, TIMEOUTS.LONG); this.notify.error(NOTIFY_OFFERS_LOAD_ERROR.message, TIMEOUTS.LONG);
} }
} catch (error: any) { } catch (error: unknown) {
logger.error("Got error loading offers:", error.message || error); const errorMessage =
error instanceof Error ? error.message : String(error);
logger.error("Got error loading offers:", errorMessage);
this.notify.error(NOTIFY_OFFERS_FETCH_ERROR.message, TIMEOUTS.LONG); this.notify.error(NOTIFY_OFFERS_FETCH_ERROR.message, TIMEOUTS.LONG);
} finally { } finally {
this.isLoading = false; this.isLoading = false;
@ -757,11 +762,10 @@ export default class ProjectsView extends Vue {
* - Web-based QR interface for browser environments * - Web-based QR interface for browser environments
*/ */
private handleQRCodeClick() { private handleQRCodeClick() {
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);
}
} }
/** /**

2
test-playwright/50-record-offer.spec.ts

@ -23,7 +23,7 @@ test('Record an offer', async ({ page }) => {
await page.locator('button', { hasText: 'Edit' }).isVisible(); // since the 'edit' takes longer to show, wait for that (lest the click miss) await page.locator('button', { hasText: 'Edit' }).isVisible(); // since the 'edit' takes longer to show, wait for that (lest the click miss)
await page.getByTestId('offerButton').click(); await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(description); await page.getByTestId('inputDescription').fill(description);
await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString()); await page.getByTestId('inputOfferAmount').locator('input').fill(randomNonZeroNumber.toString());
expect(page.getByRole('button', { name: 'Sign & Send' })); expect(page.getByRole('button', { name: 'Sign & Send' }));
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible(); await expect(page.getByText('That offer was recorded.')).toBeVisible();

4
test-playwright/60-new-activity.spec.ts

@ -36,7 +36,7 @@ test('New offers for another user', async ({ page }) => {
const randomString1 = Math.random().toString(36).substring(2, 5); const randomString1 = Math.random().toString(36).substring(2, 5);
await page.getByTestId('offerButton').click(); await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(`help of ${randomString1} from #000`); await page.getByTestId('inputDescription').fill(`help of ${randomString1} from #000`);
await page.getByTestId('inputOfferAmount').fill('1'); await page.getByTestId('inputOfferAmount').locator('input').fill('1');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible(); await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
@ -46,7 +46,7 @@ test('New offers for another user', async ({ page }) => {
const randomString2 = Math.random().toString(36).substring(2, 5); const randomString2 = Math.random().toString(36).substring(2, 5);
await page.getByTestId('offerButton').click(); await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(`help of ${randomString2} from #000`); await page.getByTestId('inputDescription').fill(`help of ${randomString2} from #000`);
await page.getByTestId('inputOfferAmount').fill('3'); await page.getByTestId('inputOfferAmount').locator('input').fill('3');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible(); await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert

Loading…
Cancel
Save