Compare commits
60 Commits
remove-ima
...
platformse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
243c3eea32 | ||
|
|
1d0c8ac3cf | ||
|
|
e733089bad | ||
|
|
3c44dc0921 | ||
|
|
1211b87f4e | ||
|
|
76c94bbe08 | ||
|
|
63e1738d87 | ||
|
|
1a06dea491 | ||
|
|
ab23d49145 | ||
|
|
86e9aa75c1 | ||
|
|
cdf5fbdfc6 | ||
| cf44ec1a1d | |||
|
|
f85c190557 | ||
|
|
bc9d3cdda5 | ||
| 1a03dbb24c | |||
| dc8a897004 | |||
| 404fa0e78f | |||
|
|
5f417aeabd | ||
|
|
1542c7bb75 | ||
|
|
3aa57c5ced | ||
|
|
38f3105946 | ||
| c1ae5cbfb8 | |||
|
|
215c37f00a | ||
| 799981d1cb | |||
|
|
37559e1bad | ||
|
|
379056aae1 | ||
|
|
ef4f845f74 | ||
|
|
bc618bb13b | ||
|
|
404f23c118 | ||
|
|
303f1bc565 | ||
|
|
68c0459533 | ||
|
|
b761088839 | ||
|
|
e15f540292 | ||
|
|
23b4460376 | ||
|
|
41c243e9f1 | ||
| 328ac0f14b | |||
| e07da3ffe1 | |||
|
|
949487629d | ||
|
|
79593f12b4 | ||
| 1ac60dc5ba | |||
|
|
495a94827a | ||
|
|
76749a097d | ||
| 31396dd555 | |||
|
|
a284067522 | ||
|
|
1a6b1e6151 | ||
|
|
c91ac135b3 | ||
|
|
45a8859a19 | ||
|
|
3926f9289d | ||
|
|
18fc31d45a | ||
|
|
ea6757c696 | ||
|
|
1dc534b61f | ||
|
|
9196081f34 | ||
|
|
7df52312ba | ||
|
|
d30597a921 | ||
|
|
c969c536bf | ||
|
|
49bf13021f | ||
|
|
6007bc34e4 | ||
|
|
2b6a2d3612 | ||
|
|
934e18f728 | ||
|
|
5cf1759653 |
63
.cursor/rules/adr_template.mdc
Normal file
@@ -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
Normal file
@@ -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?
|
||||
@@ -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.
|
||||
32
.cursor/rules/asset_configuration.mdc
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# Asset Configuration Directive
|
||||
*Scope: Assets Only (icons, splashes, image pipelines) — not overall build orchestration*
|
||||
|
||||
## Intent
|
||||
- Version **asset configuration files** (optionally dev-time generated).
|
||||
- **Do not** version platform asset outputs (Android/iOS/Electron); generate them **at build-time** with standard tools.
|
||||
- Keep existing per-platform build scripts unchanged.
|
||||
|
||||
## Source of Truth
|
||||
- **Preferred (Capacitor default):** `resources/` as the single master source.
|
||||
- **Alternative:** `assets/` is acceptable **only** if `capacitor-assets` is explicitly configured to read from it.
|
||||
- **Never** maintain both `resources/` and `assets/` as parallel sources. Migrate and delete the redundant folder.
|
||||
|
||||
## Config Files
|
||||
- Live under: `config/assets/` (committed).
|
||||
- Examples:
|
||||
- `config/assets/capacitor-assets.config.json` (or the path the tool expects)
|
||||
- `config/assets/android.assets.json`
|
||||
- `config/assets/ios.assets.json`
|
||||
- `config/assets/common.assets.yaml` (optional shared layer)
|
||||
- **Dev-time generation allowed** for these configs; **build-time generation is forbidden**.
|
||||
|
||||
## Build-Time Behavior
|
||||
- Build generates platform assets (not configs) using the standard chain:
|
||||
```bash
|
||||
npm run build:capacitor # web build via Vite (.mts)
|
||||
npx cap sync
|
||||
npx capacitor-assets generate # produces platform assets; not committed
|
||||
# then platform-specific build steps
|
||||
224
.cursor/rules/base_context.mdc
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
```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.
|
||||
```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.
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
globs: **/db/databaseUtil.ts, **/interfaces/absurd-sql.d.ts, **/src/registerSQLWorker.js, **/services/AbsurdSqlDatabaseService.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
# Absurd SQL - Cursor Development Guide
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
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.
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
description: rules used while developing
|
||||
globs:
|
||||
alwaysApply: true
|
||||
globs: **/src/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
✅ 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
|
||||
125
.cursor/rules/development/type_safety_guide.mdc
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
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]`.
|
||||
|
||||
## Type Safety Enforcement
|
||||
|
||||
### Core Type Safety Rules
|
||||
- **No `any` Types**: Use explicit types or `unknown` with proper type guards
|
||||
- **Error Handling Uses Guards**: Implement and reuse type guards from `src/interfaces/**`
|
||||
- **Dynamic Property Access**: Use `keyof` + `in` checks for type-safe property access
|
||||
|
||||
### Type Guard Patterns
|
||||
- **API Errors**: Use `isApiError(error)` guards for API error handling
|
||||
- **Database Errors**: Use `isDatabaseError(error)` guards for database operations
|
||||
- **Axios Errors**: Implement `isAxiosError(error)` guards for HTTP error handling
|
||||
|
||||
### Implementation Guidelines
|
||||
- **Avoid Type Assertions**: Replace `as any` with proper type guards and interfaces
|
||||
- **Narrow Types Properly**: Use type guards to narrow `unknown` types safely
|
||||
- **Document Type Decisions**: Explain complex type structures and their purpose
|
||||
|
||||
## Minimal Special Cases (document in PR when used)
|
||||
|
||||
- **Vue refs / instances**: Use `ComponentPublicInstance` or specific component
|
||||
types for dynamic refs.
|
||||
- **3rd‑party libs without types**: Narrow immediately to a **known interface**;
|
||||
do not leave `any` hanging.
|
||||
|
||||
## Patterns (short)
|
||||
|
||||
### Database errors
|
||||
|
||||
```ts
|
||||
try { await this.$addContact(contact); }
|
||||
catch (e: unknown) {
|
||||
if (isDatabaseError(e) && e.message.includes("Key already exists")) {
|
||||
/* handle duplicate */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API errors
|
||||
|
||||
```ts
|
||||
try { await apiCall(); }
|
||||
catch (e: unknown) {
|
||||
if (isApiError(e)) {
|
||||
const msg = e.response?.data?.error?.message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic keys
|
||||
|
||||
```ts
|
||||
const keys = Object.keys(newSettings).filter(
|
||||
k => k in newSettings && newSettings[k as keyof typeof newSettings] !== undefined
|
||||
);
|
||||
```
|
||||
|
||||
## Checklists
|
||||
|
||||
**Before commit**
|
||||
|
||||
- [ ] No `any` (except documented, justified cases)
|
||||
- [ ] Errors handled via guards
|
||||
- [ ] Dynamic access uses `keyof`/`in`
|
||||
- [ ] Imports point to correct interfaces/types
|
||||
|
||||
**Code review**
|
||||
|
||||
- [ ] Hunt hidden `as any`
|
||||
- [ ] Guard‑based error paths verified
|
||||
- [ ] Dynamic ops are type‑safe
|
||||
- [ ] Prefer existing types over re‑inventing
|
||||
|
||||
## Tools
|
||||
|
||||
- `npm run lint-fix` — lint & auto‑fix
|
||||
- `npm run type-check` — strict type compilation (CI + pre‑release)
|
||||
- IDE: enable strict TS, ESLint/TS ESLint, Volar (Vue 3)
|
||||
|
||||
## References
|
||||
|
||||
- TS Handbook — https://www.typescriptlang.org/docs/
|
||||
- TS‑ESLint — https://typescript-eslint.io/rules/
|
||||
- Vue 3 + TS — https://vuejs.org/guide/typescript/
|
||||
76
.cursor/rules/investigation_report_example.mdc
Normal file
@@ -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
|
||||
---
|
||||
225
.cursor/rules/logging_standards.mdc
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
globs: *.vue,*.ts,*.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
# Agent Contract — TimeSafari Logging (Unified, MANDATORY)
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-15
|
||||
**Status**: 🎯 **ACTIVE** - Mandatory logging standards
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines unified logging standards for the TimeSafari project,
|
||||
ensuring consistent, rest-parameter logging style using the project logger.
|
||||
No `console.*` methods are allowed in production code.
|
||||
|
||||
## Scope and Goals
|
||||
|
||||
**Scope**: Applies to all diffs and generated code in this workspace unless
|
||||
explicitly exempted below.
|
||||
|
||||
**Goal**: One consistent, rest-parameter logging style using the project
|
||||
logger; no `console.*` in production code.
|
||||
|
||||
## Non‑Negotiables (DO THIS)
|
||||
|
||||
- You **MUST** use the project logger; **DO NOT** use any `console.*`
|
||||
methods.
|
||||
- Import exactly as:
|
||||
- `import { logger } from '@/utils/logger'`
|
||||
- If `@` alias is unavailable, compute the correct relative path (do not
|
||||
fail).
|
||||
- Call signatures use **rest parameters**: `logger.info(message, ...args)`
|
||||
- Prefer primitives/IDs and small objects in `...args`; **never build a
|
||||
throwaway object** just to "wrap context".
|
||||
- Production defaults: Web = `warn+`, Electron = `error`, Dev/Capacitor =
|
||||
`info+` (override via `VITE_LOG_LEVEL`).
|
||||
- **Database persistence**: `info|warn|error` are persisted; `debug` is not.
|
||||
Use `logger.toDb(msg, level?)` for DB-only.
|
||||
|
||||
## Available Logger API (Authoritative)
|
||||
|
||||
- `logger.debug(message, ...args)` — verbose internals, timings, input/output
|
||||
shapes
|
||||
- `logger.log(message, ...args)` — synonym of `info` for general info
|
||||
- `logger.info(message, ...args)` — lifecycle, state changes, success paths
|
||||
- `logger.warn(message, ...args)` — recoverable issues, retries, degraded mode
|
||||
- `logger.error(message, ...args)` — failures, thrown exceptions, aborts
|
||||
- `logger.toDb(message, level?)` — DB-only entry (default level = `info`)
|
||||
- `logger.toConsoleAndDb(message, isError)` — console + DB (use sparingly)
|
||||
- `logger.withContext(componentName)` — returns a scoped logger
|
||||
|
||||
## Level Guidelines (Use These Heuristics)
|
||||
|
||||
### DEBUG
|
||||
|
||||
Use for method entry/exit, computed values, filters, loops, retries, and
|
||||
external call payload sizes.
|
||||
|
||||
```typescript
|
||||
logger.debug('[HomeView] reloadFeedOnChange() called');
|
||||
logger.debug('[HomeView] Current filter settings',
|
||||
settings.filterFeedByVisible,
|
||||
settings.filterFeedByNearby,
|
||||
settings.searchBoxes?.length ?? 0);
|
||||
logger.debug('[FeedFilters] Toggling nearby filter',
|
||||
this.isNearby, this.settingChanged, this.activeDid);
|
||||
```
|
||||
|
||||
**Avoid**: Vague messages (`'Processing data'`).
|
||||
|
||||
### INFO
|
||||
|
||||
Use for user-visible lifecycle and completed operations.
|
||||
|
||||
```typescript
|
||||
logger.info('[StartView] Component mounted', process.env.VITE_PLATFORM);
|
||||
logger.info('[StartView] User selected new seed generation');
|
||||
logger.info('[SearchAreaView] Search box stored',
|
||||
searchBox.name, searchBox.bbox);
|
||||
logger.info('[ContactQRScanShowView] Contact registration OK',
|
||||
contact.did);
|
||||
```
|
||||
|
||||
**Avoid**: Diagnostic details that belong in `debug`.
|
||||
|
||||
### WARN
|
||||
|
||||
Use for recoverable issues, fallbacks, unexpected-but-handled conditions.
|
||||
|
||||
```typescript
|
||||
logger.warn('[ContactQRScanShowView] Invalid scan result – no value',
|
||||
resultType);
|
||||
logger.warn('[ContactQRScanShowView] Invalid QR format – no JWT in URL');
|
||||
logger.warn('[ContactQRScanShowView] JWT missing "own" field');
|
||||
```
|
||||
|
||||
**Avoid**: Hard failures (those are `error`).
|
||||
|
||||
### ERROR
|
||||
|
||||
Use for unrecoverable failures, data integrity issues, and thrown
|
||||
exceptions.
|
||||
|
||||
```typescript
|
||||
logger.error('[HomeView Settings] initializeIdentity() failed', err);
|
||||
logger.error('[StartView] Failed to load initialization data', error);
|
||||
logger.error('[ContactQRScanShowView] Error processing contact QR',
|
||||
error, rawValue);
|
||||
```
|
||||
|
||||
**Avoid**: Expected user cancels (use `info`/`debug`).
|
||||
|
||||
## Context Hygiene (Consistent, Minimal, Helpful)
|
||||
|
||||
- **Component context**: Prefer scoped logger.
|
||||
|
||||
```typescript
|
||||
const log = logger.withContext('UserService');
|
||||
log.info('User created', userId);
|
||||
log.error('Failed to create user', error);
|
||||
```
|
||||
|
||||
If not using `withContext`, prefix message with `[ComponentName]`.
|
||||
|
||||
- **Emojis**: Optional and minimal for visual scanning. Recommended set:
|
||||
- Start/finish: 🚀 / ✅
|
||||
- Retry/loop: 🔄
|
||||
- External call: 📡
|
||||
- Data/metrics: 📊
|
||||
- Inspection: 🔍
|
||||
|
||||
- **Sensitive data**: Never log secrets (tokens, keys, passwords) or
|
||||
payloads >10KB. Prefer IDs over objects; redact/hash when needed.
|
||||
|
||||
## Migration — Auto‑Rewrites (Apply Every Time)
|
||||
|
||||
- Exact transforms:
|
||||
- `console.debug(...)` → `logger.debug(...)`
|
||||
- `console.log(...)` → `logger.log(...)` (or `logger.info(...)` when
|
||||
clearly stateful)
|
||||
- `console.info(...)` → `logger.info(...)`
|
||||
- `console.warn(...)` → `logger.warn(...)`
|
||||
- `console.error(...)` → `logger.error(...)`
|
||||
|
||||
- Multi-arg handling:
|
||||
- First arg becomes `message` (stringify safely if non-string).
|
||||
- Remaining args map 1:1 to `...args`:
|
||||
`console.info(msg, a, b)` → `logger.info(String(msg), a, b)`
|
||||
|
||||
- Sole `Error`:
|
||||
- `console.error(err)` → `logger.error(err.message, err)`
|
||||
|
||||
- **Object-wrapping cleanup**: Replace `{{ userId, meta }}` wrappers with
|
||||
separate args:
|
||||
`logger.info('User signed in', userId, meta)`
|
||||
|
||||
## DB Logging Rules
|
||||
|
||||
- `debug` **never** persists automatically.
|
||||
- `info|warn|error` persist automatically.
|
||||
- For DB-only events (no console), call `logger.toDb('Message',
|
||||
'info'|'warn'|'error')`.
|
||||
|
||||
## Exceptions (Tightly Scoped)
|
||||
|
||||
Allowed paths (still prefer logger):
|
||||
|
||||
- `**/*.test.*`, `**/*.spec.*`
|
||||
- `scripts/dev/**`, `scripts/migrate/**`
|
||||
|
||||
To intentionally keep `console.*`, add a pragma on the previous line:
|
||||
|
||||
```typescript
|
||||
// cursor:allow-console reason="short justification"
|
||||
console.log('temporary output');
|
||||
```
|
||||
|
||||
Without the pragma, rewrite to `logger.*`.
|
||||
|
||||
## CI & Diff Enforcement
|
||||
|
||||
- Do not introduce `console.*` anywhere outside allowed, pragma'd spots.
|
||||
- If an import is missing, insert it and resolve alias/relative path
|
||||
correctly.
|
||||
- Enforce rest-parameter call shape in reviews; replace object-wrapped
|
||||
context.
|
||||
- Ensure environment log level rules remain intact (`VITE_LOG_LEVEL`
|
||||
respected).
|
||||
|
||||
## Quick Before/After
|
||||
|
||||
### **Before**
|
||||
|
||||
```typescript
|
||||
console.log('User signed in', user.id, meta);
|
||||
console.error('Failed to update profile', err);
|
||||
console.info('Filter toggled', this.hasVisibleDid);
|
||||
```
|
||||
|
||||
### **After**
|
||||
|
||||
```typescript
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
logger.info('User signed in', user.id, meta);
|
||||
logger.error('Failed to update profile', err);
|
||||
logger.debug('[FeedFilters] Filter toggled', this.hasVisibleDid);
|
||||
```
|
||||
|
||||
## Checklist (for every PR)
|
||||
|
||||
- [ ] No `console.*` (or properly pragma'd in the allowed locations)
|
||||
- [ ] Correct import path for `logger`
|
||||
- [ ] Rest-parameter call shape (`message, ...args`)
|
||||
- [ ] Right level chosen (debug/info/warn/error)
|
||||
- [ ] No secrets / oversized payloads / throwaway context objects
|
||||
- [ ] Component context provided (scoped logger or `[Component]` prefix)
|
||||
|
||||
---
|
||||
|
||||
**Status**: Active and enforced
|
||||
**Last Updated**: 2025-08-15 08:11:46Z
|
||||
**Version**: 1.0
|
||||
**Maintainer**: Matthew Raymer
|
||||
170
.cursor/rules/research_diagnostic.mdc
Normal file
@@ -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`
|
||||
144
.cursor/rules/software_development.mdc
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
# 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
|
||||
- [ ] Dependencies validated and accessible
|
||||
- [ ] Environment impact assessed for team members
|
||||
- [ ] Pre-build validation implemented where appropriate
|
||||
|
||||
## Additional Core Principles
|
||||
|
||||
### 4. Dependency Management & Environment Validation
|
||||
- **Pre-build Validation**: Always validate critical dependencies before executing build scripts
|
||||
- **Environment Consistency**: Ensure team members have identical development environments
|
||||
- **Dependency Verification**: Check that required packages are installed and accessible
|
||||
- **Path Resolution**: Use `npx` for local dependencies to avoid PATH issues
|
||||
|
||||
## Additional Required Workflows
|
||||
|
||||
### Dependency Validation (Before Proposing Changes)
|
||||
- [ ] **Dependency Validation**: Verify all required dependencies are available and accessible
|
||||
|
||||
### Environment Impact Assessment (During Solution Design)
|
||||
- [ ] **Environment Impact**: Assess how changes affect team member setups
|
||||
|
||||
## Additional Competence Hooks
|
||||
|
||||
### Dependency & Environment Management
|
||||
- **"What dependencies does this feature require and are they properly declared?"**
|
||||
- **"How will this change affect team member development environments?"**
|
||||
- **"What validation can we add to catch dependency issues early?"**
|
||||
|
||||
## Dependency Management Best Practices
|
||||
|
||||
### Pre-build Validation
|
||||
- **Check Critical Dependencies**: Validate essential tools before executing build scripts
|
||||
- **Use npx for Local Dependencies**: Prefer `npx tsx` over direct `tsx` to avoid PATH issues
|
||||
- **Environment Consistency**: Ensure all team members have identical dependency versions
|
||||
|
||||
### Common Pitfalls
|
||||
- **Missing npm install**: Team members cloning without running `npm install`
|
||||
- **PATH Issues**: Direct command execution vs. npm script execution differences
|
||||
- **Version Mismatches**: Different Node.js/npm versions across team members
|
||||
|
||||
### Validation Strategies
|
||||
- **Dependency Check Scripts**: Implement pre-build validation for critical dependencies
|
||||
- **Environment Requirements**: Document and enforce minimum Node.js/npm versions
|
||||
- **Onboarding Checklist**: Standardize team member setup procedures
|
||||
|
||||
### Error Messages and Guidance
|
||||
- **Specific Error Context**: Provide clear guidance when dependency issues occur
|
||||
- **Actionable Solutions**: Direct users to specific commands (`npm install`, `npm run check:dependencies`)
|
||||
- **Environment Diagnostics**: Implement comprehensive environment validation tools
|
||||
|
||||
### Build Script Enhancements
|
||||
- **Early Validation**: Check dependencies before starting build processes
|
||||
- **Graceful Degradation**: Continue builds when possible but warn about issues
|
||||
- **Helpful Tips**: Remind users about dependency management best practices
|
||||
|
||||
- **Narrow Types Properly**: Use type guards to narrow `unknown` types safely
|
||||
- **Document Type Decisions**: Explain complex type structures and their purpose
|
||||
@@ -25,6 +25,37 @@ alwaysApply: true
|
||||
* a **draft commit message** (copy-paste ready),
|
||||
* nothing auto-applied.
|
||||
|
||||
## 4) Version Synchronization Requirements
|
||||
|
||||
* **MUST** check for version changes in `package.json` before committing
|
||||
* **MUST** ensure `CHANGELOG.md` is updated when `package.json` version changes
|
||||
* **MUST** validate version format consistency between both files
|
||||
* **MUST** include version bump commits in changelog with proper semantic versioning
|
||||
|
||||
### Version Sync Checklist (Before Commit)
|
||||
|
||||
- [ ] `package.json` version matches latest `CHANGELOG.md` entry
|
||||
- [ ] New version follows semantic versioning (MAJOR.MINOR.PATCH[-PRERELEASE])
|
||||
- [ ] Changelog entry includes all significant changes since last version
|
||||
- [ ] Version bump commit message follows `build(version): bump to X.Y.Z` format
|
||||
- [ ] Breaking changes properly documented with migration notes
|
||||
- [ ] Alert developer in chat message that version has been updated
|
||||
|
||||
### Version Change Detection
|
||||
|
||||
* **Check for version changes** in staged/unstaged `package.json`
|
||||
* **Alert developer** if version changed but changelog not updated
|
||||
* **Suggest changelog update** with proper format and content
|
||||
* **Validate semantic versioning** compliance
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
* **Version Detection**: Compare `package.json` version field with latest changelog entry
|
||||
* **Semantic Validation**: Ensure version follows `X.Y.Z[-PRERELEASE]` format
|
||||
* **Changelog Format**: Follow [Keep a Changelog](https://keepachangelog.com/) standards
|
||||
* **Breaking Changes**: Use `!` in commit message and `BREAKING CHANGE:` in changelog
|
||||
* **Pre-release Versions**: Include beta/alpha/rc suffixes in both files consistently
|
||||
|
||||
---
|
||||
|
||||
# Commit Message Format (Normative)
|
||||
@@ -1,10 +1,14 @@
|
||||
|
||||
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
|
||||
|
||||
# Logging Configuration - Development environment gets maximum visibility
|
||||
VITE_LOG_LEVEL=debug
|
||||
|
||||
# iOS doesn't like spaces in the app title.
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
|
||||
VITE_APP_SERVER=http://localhost:8080
|
||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not
|
||||
|
||||
|
||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||
# Using shared server by default to ease setup, which works for shared test users.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
|
||||
|
||||
|
||||
# Logging Configuration - Production environment gets minimal logging for performance
|
||||
VITE_LOG_LEVEL=warn
|
||||
|
||||
VITE_APP_SERVER=https://timesafari.app
|
||||
# This is the claim ID for actions in the BVC project.
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
|
||||
|
||||
# Logging Configuration - Test environment gets balanced logging for debugging
|
||||
VITE_LOG_LEVEL=info
|
||||
|
||||
# iOS doesn't like spaces in the app title.
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test"
|
||||
VITE_APP_SERVER=https://test.timesafari.app
|
||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not
|
||||
production).
|
||||
|
||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
||||
|
||||
|
||||
13
.eslintrc.js
@@ -30,9 +30,18 @@ module.exports = {
|
||||
}],
|
||||
"no-console": 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/no-unnecessary-type-constraint": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
// Prevent usage of deprecated $updateSettings method
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: '$',
|
||||
property: 'updateSettings',
|
||||
message: 'Use $saveSettings, $saveUserSettings, or $saveMySettings instead of the deprecated $updateSettings method.'
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
142
.github/workflows/asset-validation.yml
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
name: Asset Validation & CI Safeguards
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'resources/**'
|
||||
- 'config/assets/**'
|
||||
- 'capacitor-assets.config.json'
|
||||
- 'capacitor.config.ts'
|
||||
- 'capacitor.config.json'
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'resources/**'
|
||||
- 'config/assets/**'
|
||||
- 'capacitor-assets.config.json'
|
||||
- 'capacitor.config.ts'
|
||||
- 'capacitor.config.json'
|
||||
|
||||
jobs:
|
||||
asset-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Validate asset configuration
|
||||
run: npm run assets:validate
|
||||
|
||||
- name: Check for committed platform assets (Android)
|
||||
run: |
|
||||
if git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)' > /dev/null; then
|
||||
echo "❌ Android platform assets found in VCS - these should be generated at build-time"
|
||||
git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)'
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ No Android platform assets committed"
|
||||
|
||||
- name: Check for committed platform assets (iOS)
|
||||
run: |
|
||||
if git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)' > /dev/null; then
|
||||
echo "❌ iOS platform assets found in VCS - these should be generated at build-time"
|
||||
git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)'
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ No iOS platform assets committed"
|
||||
|
||||
- name: Test asset generation
|
||||
run: |
|
||||
echo "🧪 Testing asset generation workflow..."
|
||||
npm run build:capacitor
|
||||
npx cap sync
|
||||
npx capacitor-assets generate --dry-run || npx capacitor-assets generate
|
||||
echo "✅ Asset generation test completed"
|
||||
|
||||
- name: Verify clean tree after build
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "❌ Dirty tree after build - asset configs were modified"
|
||||
git status
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Build completed with clean tree"
|
||||
|
||||
schema-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Validate schema compliance
|
||||
run: |
|
||||
echo "🔍 Validating schema compliance..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8'));
|
||||
const schema = JSON.parse(fs.readFileSync('config/assets/schema.json', 'utf8'));
|
||||
|
||||
// Basic schema validation
|
||||
if (!config.icon || !config.splash) {
|
||||
throw new Error('Missing required sections: icon and splash');
|
||||
}
|
||||
|
||||
if (!config.icon.source || !config.splash.source) {
|
||||
throw new Error('Missing required source fields');
|
||||
}
|
||||
|
||||
if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) {
|
||||
throw new Error('Icon source must be in resources/ directory');
|
||||
}
|
||||
|
||||
if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) {
|
||||
throw new Error('Splash source must be in resources/ directory');
|
||||
}
|
||||
|
||||
console.log('✅ Schema validation passed');
|
||||
"
|
||||
|
||||
- name: Check source file existence
|
||||
run: |
|
||||
echo "📁 Checking source file existence..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8'));
|
||||
|
||||
const requiredFiles = [
|
||||
config.icon.source,
|
||||
config.splash.source
|
||||
];
|
||||
|
||||
if (config.splash.darkSource) {
|
||||
requiredFiles.push(config.splash.darkSource);
|
||||
}
|
||||
|
||||
const missingFiles = requiredFiles.filter(file => !fs.existsSync(file));
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
console.error('❌ Missing source files:', missingFiles);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ All source files exist');
|
||||
"
|
||||
12
.gitignore
vendored
@@ -56,6 +56,10 @@ icons
|
||||
|
||||
*.log
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Generated Android assets and resources (should be generated during build)
|
||||
android/app/src/main/assets/public/
|
||||
|
||||
@@ -64,6 +68,14 @@ android/app/src/main/res/drawable*/
|
||||
android/app/src/main/res/mipmap*/
|
||||
android/app/src/main/res/values/ic_launcher_background.xml
|
||||
|
||||
# Android generated assets (deny-listed in CI)
|
||||
android/app/src/main/res/mipmap-*/ic_launcher*.png
|
||||
android/app/src/main/res/drawable*/splash*.png
|
||||
|
||||
# iOS generated assets (deny-listed in CI)
|
||||
ios/App/App/Assets.xcassets/**/AppIcon*.png
|
||||
ios/App/App/Assets.xcassets/**/Splash*.png
|
||||
|
||||
# Keep these Android configuration files in version control:
|
||||
# - android/app/src/main/assets/capacitor.plugins.json
|
||||
# - android/app/src/main/res/values/strings.xml
|
||||
|
||||
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
18.19.0
|
||||
74
README.md
@@ -26,6 +26,35 @@ See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all plat
|
||||
|
||||
TimeSafari provides a simple script-based approach to clear the local database (not the claim server) for development purposes.
|
||||
|
||||
## Logging Configuration
|
||||
|
||||
TimeSafari supports configurable logging levels via the `VITE_LOG_LEVEL` environment variable. This allows developers to control console output verbosity without modifying code.
|
||||
|
||||
### Quick Usage
|
||||
|
||||
```bash
|
||||
# Show only errors
|
||||
VITE_LOG_LEVEL=error npm run dev
|
||||
|
||||
# Show warnings and errors
|
||||
VITE_LOG_LEVEL=warn npm run dev
|
||||
|
||||
# Show info, warnings, and errors (default)
|
||||
VITE_LOG_LEVEL=info npm run dev
|
||||
|
||||
# Show all log levels including debug
|
||||
VITE_LOG_LEVEL=debug npm run dev
|
||||
```
|
||||
|
||||
### Available Levels
|
||||
|
||||
- **`error`**: Critical errors only
|
||||
- **`warn`**: Warnings and errors (default for production web)
|
||||
- **`info`**: Info, warnings, and errors (default for development/capacitor)
|
||||
- **`debug`**: All log levels including verbose debugging
|
||||
|
||||
See [Logging Configuration Guide](doc/logging-configuration.md) for complete details.
|
||||
|
||||
### Quick Usage
|
||||
```bash
|
||||
# Run the database clearing script
|
||||
@@ -107,11 +136,50 @@ const apiUrl = `${APP_SERVER}/api/claim/123`;
|
||||
|
||||
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
||||
|
||||
## Icons
|
||||
## Asset Management
|
||||
|
||||
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
||||
TimeSafari uses a standardized asset configuration system for consistent
|
||||
icon and splash screen generation across all platforms.
|
||||
|
||||
To add a Font Awesome icon, add to fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
|
||||
### Asset Sources
|
||||
|
||||
- **Single source of truth**: `resources/` directory (Capacitor default)
|
||||
- **Source files**: `icon.png`, `splash.png`, `splash_dark.png`
|
||||
- **Format**: PNG or SVG files for optimal quality
|
||||
|
||||
### Asset Generation
|
||||
|
||||
- **Configuration**: `config/assets/capacitor-assets.config.json`
|
||||
- **Schema validation**: `config/assets/schema.json`
|
||||
- **Build-time generation**: Platform assets generated via `capacitor-assets`
|
||||
- **No VCS commits**: Generated assets are never committed to version control
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Generate/update asset configurations
|
||||
npm run assets:config
|
||||
|
||||
# Validate asset configurations
|
||||
npm run assets:validate
|
||||
|
||||
# Clean generated platform assets (local dev only)
|
||||
npm run assets:clean
|
||||
|
||||
# Build with asset generation
|
||||
npm run build:native
|
||||
```
|
||||
|
||||
### Platform Support
|
||||
|
||||
- **Android**: Adaptive icons with foreground/background, monochrome support
|
||||
- **iOS**: LaunchScreen storyboard preferred, splash assets when needed
|
||||
- **Web**: PWA icons generated during build to `dist/` (not committed)
|
||||
|
||||
### Font Awesome Icons
|
||||
|
||||
To add a Font Awesome icon, add to `fontawesome.ts` and reference with
|
||||
`font-awesome` element and `icon` attribute with the hyphenated name.
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"splashFullScreen": true,
|
||||
"splashImmersive": true
|
||||
},
|
||||
"CapacitorSQLite": {
|
||||
"CapSQLite": {
|
||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||
"iosIsEncryption": false,
|
||||
"iosBiometric": {
|
||||
|
||||
7
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#3F51B5</color>
|
||||
<color name="colorPrimaryDark">#303F9F</color>
|
||||
<color name="colorAccent">#FF4081</color>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
Application icons are here. They are processed for android & ios by the `capacitor-assets` command, as indicated in the BUILDING.md file.
|
||||
@@ -1,36 +1,32 @@
|
||||
{
|
||||
"icon": {
|
||||
"ios": {
|
||||
"source": "resources/ios/icon/icon.png",
|
||||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
|
||||
},
|
||||
"android": {
|
||||
"source": "resources/android/icon/icon.png",
|
||||
"adaptive": {
|
||||
"background": "#121212",
|
||||
"foreground": "resources/icon.png",
|
||||
"monochrome": "resources/icon.png"
|
||||
},
|
||||
"target": "android/app/src/main/res"
|
||||
},
|
||||
"ios": {
|
||||
"padding": 0,
|
||||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
|
||||
},
|
||||
"source": "resources/icon.png",
|
||||
"web": {
|
||||
"source": "resources/web/icon/icon.png",
|
||||
"target": "public/img/icons"
|
||||
}
|
||||
},
|
||||
"splash": {
|
||||
"ios": {
|
||||
"source": "resources/ios/splash/splash.png",
|
||||
"target": "ios/App/App/Assets.xcassets/Splash.imageset"
|
||||
},
|
||||
"android": {
|
||||
"source": "resources/android/splash/splash.png",
|
||||
"scale": "cover",
|
||||
"target": "android/app/src/main/res"
|
||||
}
|
||||
},
|
||||
"splashDark": {
|
||||
"ios": {
|
||||
"source": "resources/ios/splash/splash_dark.png",
|
||||
"target": "ios/App/App/Assets.xcassets/SplashDark.imageset"
|
||||
},
|
||||
"android": {
|
||||
"source": "resources/android/splash/splash_dark.png",
|
||||
"target": "android/app/src/main/res"
|
||||
}
|
||||
"darkSource": "resources/splash_dark.png",
|
||||
"ios": {
|
||||
"target": "ios/App/App/Assets.xcassets",
|
||||
"useStoryBoard": true
|
||||
},
|
||||
"source": "resources/splash.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
capacitor.config.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'app.timesafari',
|
||||
appName: 'TimeSafari',
|
||||
webDir: 'dist',
|
||||
server: {
|
||||
cleartext: true
|
||||
},
|
||||
plugins: {
|
||||
App: {
|
||||
appUrlOpen: {
|
||||
handlers: [
|
||||
{
|
||||
url: 'timesafari://*',
|
||||
autoVerify: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
SplashScreen: {
|
||||
launchShowDuration: 3000,
|
||||
launchAutoHide: true,
|
||||
backgroundColor: '#ffffff',
|
||||
androidSplashResourceName: 'splash',
|
||||
androidScaleType: 'CENTER_CROP',
|
||||
showSpinner: false,
|
||||
androidSpinnerStyle: 'large',
|
||||
iosSpinnerStyle: 'small',
|
||||
spinnerColor: '#999999',
|
||||
splashFullScreen: true,
|
||||
splashImmersive: true
|
||||
},
|
||||
CapSQLite: {
|
||||
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
||||
iosIsEncryption: false,
|
||||
iosBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle: 'Biometric login for TimeSafari'
|
||||
},
|
||||
androidIsEncryption: false,
|
||||
androidBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle: 'Biometric login for TimeSafari'
|
||||
},
|
||||
electronIsEncryption: false
|
||||
}
|
||||
},
|
||||
ios: {
|
||||
contentInset: 'never',
|
||||
allowsLinkPreview: true,
|
||||
scrollEnabled: true,
|
||||
limitsNavigationsToAppBoundDomains: true,
|
||||
backgroundColor: '#ffffff',
|
||||
allowNavigation: [
|
||||
'*.timesafari.app',
|
||||
'*.jsdelivr.net',
|
||||
'api.endorser.ch'
|
||||
]
|
||||
},
|
||||
android: {
|
||||
allowMixedContent: true,
|
||||
captureInput: true,
|
||||
webContentsDebuggingEnabled: false,
|
||||
allowNavigation: [
|
||||
'*.timesafari.app',
|
||||
'*.jsdelivr.net',
|
||||
'api.endorser.ch',
|
||||
'10.0.2.2:3000'
|
||||
]
|
||||
},
|
||||
electron: {
|
||||
deepLinking: {
|
||||
schemes: ['timesafari']
|
||||
},
|
||||
buildOptions: {
|
||||
appId: 'app.timesafari',
|
||||
productName: 'TimeSafari',
|
||||
directories: {
|
||||
output: 'dist-electron-packages'
|
||||
},
|
||||
files: [
|
||||
'dist/**/*',
|
||||
'electron/**/*'
|
||||
],
|
||||
mac: {
|
||||
category: 'public.app-category.productivity',
|
||||
target: [
|
||||
{
|
||||
target: 'dmg',
|
||||
arch: ['x64', 'arm64']
|
||||
}
|
||||
]
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: 'nsis',
|
||||
arch: ['x64']
|
||||
}
|
||||
]
|
||||
},
|
||||
linux: {
|
||||
target: [
|
||||
{
|
||||
target: 'AppImage',
|
||||
arch: ['x64']
|
||||
}
|
||||
],
|
||||
category: 'Utility'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
32
config/assets/capacitor-assets.config.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"icon": {
|
||||
"android": {
|
||||
"adaptive": {
|
||||
"background": "#121212",
|
||||
"foreground": "resources/icon.png",
|
||||
"monochrome": "resources/icon.png"
|
||||
},
|
||||
"target": "android/app/src/main/res"
|
||||
},
|
||||
"ios": {
|
||||
"padding": 0,
|
||||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
|
||||
},
|
||||
"source": "resources/icon.png",
|
||||
"web": {
|
||||
"target": "public/img/icons"
|
||||
}
|
||||
},
|
||||
"splash": {
|
||||
"android": {
|
||||
"scale": "cover",
|
||||
"target": "android/app/src/main/res"
|
||||
},
|
||||
"darkSource": "resources/splash_dark.png",
|
||||
"ios": {
|
||||
"target": "ios/App/App/Assets.xcassets",
|
||||
"useStoryBoard": true
|
||||
},
|
||||
"source": "resources/splash.png"
|
||||
}
|
||||
}
|
||||
119
config/assets/schema.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Capacitor Assets Configuration Schema",
|
||||
"description": "Schema for validating capacitor-assets configuration files",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Source icon file path relative to project root"
|
||||
},
|
||||
"android": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"adaptive": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foreground": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Foreground icon for Android adaptive icons"
|
||||
},
|
||||
"background": {
|
||||
"type": ["string", "object"],
|
||||
"description": "Background color or image for adaptive icons"
|
||||
},
|
||||
"monochrome": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Monochrome icon for Android 13+"
|
||||
}
|
||||
},
|
||||
"required": ["foreground", "background"]
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Android target directory for generated icons"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"padding": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Padding ratio for iOS icons"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "iOS target directory for generated icons"
|
||||
}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Web target directory for generated icons"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["source"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"splash": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Source splash screen file"
|
||||
},
|
||||
"darkSource": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Dark mode splash screen file"
|
||||
},
|
||||
"android": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scale": {
|
||||
"type": "string",
|
||||
"enum": ["cover", "contain", "fill"],
|
||||
"description": "Android splash screen scaling mode"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Android target directory for splash screens"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"useStoryBoard": {
|
||||
"type": "boolean",
|
||||
"description": "Use LaunchScreen storyboard instead of splash assets"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "iOS target directory for splash screens"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["source"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["icon", "splash"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
214
doc/asset-migration-plan.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# TimeSafari Asset Configuration Migration Plan
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-14
|
||||
**Status**: 🎯 **IMPLEMENTATION** - Ready for Execution
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the migration from the current mixed asset management
|
||||
system to a standardized, single-source asset configuration approach using
|
||||
`capacitor-assets` as the standard generator.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Asset Sources (Duplicated)
|
||||
|
||||
- **`assets/` directory**: Contains `icon.png`, `splash.png`, `splash_dark.png`
|
||||
- **`resources/` directory**: Contains identical files in platform-specific subdirectories
|
||||
- **Result**: Duplicate storage, confusion about source of truth
|
||||
|
||||
### Asset Generation (Manual)
|
||||
|
||||
- **Custom scripts**: `generate-icons.sh`, `generate-ios-assets.sh`, `generate-android-icons.sh`
|
||||
- **Bypass capacitor-assets**: Manual ImageMagick-based generation
|
||||
- **Inconsistent outputs**: Different generation methods for each platform
|
||||
|
||||
### Configuration (Scattered)
|
||||
|
||||
- **`capacitor-assets.config.json`**: Basic configuration at root
|
||||
- **Platform-specific configs**: Mixed in various build scripts
|
||||
- **No validation**: No schema or consistency checks
|
||||
|
||||
## Target State
|
||||
|
||||
### Single Source of Truth
|
||||
|
||||
- **`resources/` directory**: Capacitor default location for source assets
|
||||
- **Eliminate duplication**: Remove `assets/` directory after migration
|
||||
- **Standardized paths**: All tools read from `resources/`
|
||||
|
||||
### Standardized Generation
|
||||
|
||||
- **`capacitor-assets`**: Single tool for all platform asset generation
|
||||
- **Build-time generation**: Assets generated during build, not committed
|
||||
- **Deterministic outputs**: Same inputs → same outputs every time
|
||||
|
||||
### Centralized Configuration
|
||||
|
||||
- **`config/assets/`**: All asset-related configuration files
|
||||
- **Schema validation**: JSON schema for configuration validation
|
||||
- **CI safeguards**: Automated validation and compliance checks
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 1: Foundation Setup ✅
|
||||
|
||||
- [x] Create `config/assets/` directory structure
|
||||
- [x] Create asset configuration schema (`schema.json`)
|
||||
- [x] Create enhanced capacitor-assets configuration
|
||||
- [x] Convert `capacitor.config.json` to `capacitor.config.ts`
|
||||
- [x] Pin Node.js version (`.nvmrc`, `.node-version`)
|
||||
- [x] Create dev-time asset configuration generator
|
||||
- [x] Create asset configuration validator
|
||||
- [x] Add npm scripts for asset management
|
||||
- [x] Update `.gitignore` with proper asset exclusions
|
||||
- [x] Create CI workflow for asset validation
|
||||
|
||||
### Phase 2: Validation & Testing
|
||||
|
||||
- [ ] Run `npm run assets:config` to generate new configuration
|
||||
- [ ] Run `npm run assets:validate` to verify configuration
|
||||
- [ ] Test `npm run build:native` workflow
|
||||
- [ ] Verify CI workflow passes all checks
|
||||
- [ ] Confirm no platform assets are committed to VCS
|
||||
|
||||
### Phase 3: Cleanup & Removal
|
||||
|
||||
- [ ] Remove `assets/` directory (after validation)
|
||||
- [ ] Remove manual asset generation scripts
|
||||
- [ ] Remove asset checking scripts
|
||||
- [ ] Update documentation references
|
||||
- [ ] Final validation of clean state
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
resources/ # Image sources ONLY
|
||||
icon.png
|
||||
splash.png
|
||||
splash_dark.png
|
||||
|
||||
config/assets/ # Versioned config & schema
|
||||
capacitor-assets.config.json
|
||||
schema.json
|
||||
|
||||
scripts/
|
||||
assets-config.js # Dev-time config generator
|
||||
assets-validator.js # Schema validator
|
||||
```
|
||||
|
||||
### Configuration Schema
|
||||
|
||||
The schema enforces:
|
||||
- Source files must be in `resources/` directory
|
||||
- Required fields for icon and splash sections
|
||||
- Android adaptive icon support (foreground/background/monochrome)
|
||||
- iOS LaunchScreen preferences
|
||||
- Target directory validation
|
||||
|
||||
### CI Safeguards
|
||||
|
||||
- **Schema validation**: Configuration must comply with schema
|
||||
- **Source file validation**: All referenced files must exist
|
||||
- **Platform asset denial**: Reject commits with generated assets
|
||||
- **Clean tree enforcement**: Build must not modify committed configs
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Local Validation
|
||||
|
||||
```bash
|
||||
# Generate configuration
|
||||
npm run assets:config
|
||||
|
||||
# Validate configuration
|
||||
npm run assets:validate
|
||||
|
||||
# Test build workflow
|
||||
npm run build:native
|
||||
|
||||
# Clean generated assets
|
||||
npm run assets:clean
|
||||
```
|
||||
|
||||
### CI Validation
|
||||
|
||||
- **Asset validation workflow**: Runs on asset-related changes
|
||||
- **Schema compliance**: Ensures configuration follows schema
|
||||
- **Source file existence**: Verifies all referenced files exist
|
||||
- **Platform asset detection**: Prevents committed generated assets
|
||||
- **Build tree verification**: Ensures clean tree after build
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Data Loss Prevention
|
||||
|
||||
- **Backup branch**: Create backup before removing `assets/`
|
||||
- **Validation checks**: Multiple validation steps before removal
|
||||
- **Gradual migration**: Phase-by-phase approach with rollback capability
|
||||
|
||||
### Build Continuity
|
||||
|
||||
- **Per-platform scripts unchanged**: All existing build orchestration preserved
|
||||
- **Standard toolchain**: Uses capacitor-assets, not custom scripts
|
||||
- **Fallback support**: Manual scripts remain until migration complete
|
||||
|
||||
### Configuration Consistency
|
||||
|
||||
- **Schema enforcement**: JSON schema prevents invalid configurations
|
||||
- **CI validation**: Automated checks catch configuration issues
|
||||
- **Documentation updates**: Clear guidance for future changes
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Technical Requirements
|
||||
|
||||
- [ ] Single source of truth in `resources/` directory
|
||||
- [ ] All platform assets generated via `capacitor-assets`
|
||||
- [ ] No manual asset generation scripts
|
||||
- [ ] Configuration validation passes all checks
|
||||
- [ ] CI workflow enforces asset policies
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
- [ ] Zero duplicate asset sources
|
||||
- [ ] 100% configuration schema compliance
|
||||
- [ ] No platform assets committed to VCS
|
||||
- [ ] Clean build tree after asset generation
|
||||
- [ ] Deterministic asset outputs
|
||||
|
||||
### User Experience
|
||||
|
||||
- [ ] Clear asset management documentation
|
||||
- [ ] Simple development commands
|
||||
- [ ] Consistent asset generation across platforms
|
||||
- [ ] Reduced confusion about asset sources
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Execute Phase 2**: Run validation and testing steps
|
||||
2. **Verify CI workflow**: Ensure all checks pass
|
||||
3. **Execute Phase 3**: Remove duplicate assets and scripts
|
||||
4. **Update documentation**: Finalize README and BUILDING.md
|
||||
5. **Team training**: Ensure all developers understand new workflow
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise during migration:
|
||||
|
||||
1. **Restore backup branch**: `git checkout backup-before-asset-migration`
|
||||
2. **Revert configuration changes**: Remove new config files
|
||||
3. **Restore manual scripts**: Re-enable previous asset generation
|
||||
4. **Investigate issues**: Identify and resolve root causes
|
||||
5. **Plan revised migration**: Adjust approach based on lessons learned
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for Phase 2 execution
|
||||
**Priority**: High
|
||||
**Estimated Effort**: 2-3 hours
|
||||
**Dependencies**: CI workflow validation
|
||||
**Stakeholders**: Development team
|
||||
182
doc/debug-hook-guide.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# TimeSafari Debug Hook Guide
|
||||
|
||||
**Complete Guide for Team Members**
|
||||
|
||||
**Date**: 2025-01-27
|
||||
**Author**: Matthew Raymer
|
||||
**Status**: ✅ **ACTIVE** - Ready for production use
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
A pre-commit hook that automatically detects and prevents debug code from reaching protected branches (master, main, production, release, stable). This ensures production code remains clean while allowing free development on feature branches.
|
||||
|
||||
## 🚀 Quick Installation
|
||||
|
||||
**From within the TimeSafari repository:**
|
||||
|
||||
```bash
|
||||
./scripts/install-debug-hook.sh
|
||||
```
|
||||
|
||||
This automatically installs, updates, and verifies the hook in your current
|
||||
repository. **Note**: Hooks are not automatically installed - you must run this
|
||||
script deliberately to enable debug code checking.
|
||||
|
||||
## 🔧 Manual Installation
|
||||
|
||||
**Copy files manually:**
|
||||
|
||||
```bash
|
||||
cp scripts/git-hooks/pre-commit /path/to/your/repo/.git/hooks/
|
||||
cp scripts/git-hooks/debug-checker.config /path/to/your/repo/.git/hooks/
|
||||
chmod +x /path/to/your/repo/.git/hooks/pre-commit
|
||||
```
|
||||
|
||||
## 📋 What Gets Installed
|
||||
|
||||
- **`pre-commit`** - Main hook script (executable)
|
||||
- **`debug-checker.config`** - Configuration file
|
||||
- **`README.md`** - Documentation and troubleshooting
|
||||
|
||||
**Note**: Hooks are stored in `scripts/git-hooks/` and must be deliberately
|
||||
installed by each developer. They are not automatically active.
|
||||
|
||||
## 🎯 How It Works
|
||||
|
||||
1. **Deliberate Installation**: Hooks must be explicitly installed by each
|
||||
developer
|
||||
2. **Branch Detection**: Only runs on protected branches
|
||||
3. **File Filtering**: Automatically skips tests, scripts, and documentation
|
||||
4. **Pattern Matching**: Detects debug code using regex patterns
|
||||
5. **Commit Prevention**: Blocks commits containing debug code
|
||||
|
||||
## 🔒 Installation Philosophy
|
||||
|
||||
**Why deliberate installation?**
|
||||
|
||||
- **Developer choice**: Each developer decides whether to use the hook
|
||||
- **No forced behavior**: Hooks don't interfere with existing workflows
|
||||
- **Local control**: Hooks are installed locally, not globally
|
||||
- **Easy removal**: Can be uninstalled at any time
|
||||
- **Team flexibility**: Some developers may prefer different tools
|
||||
|
||||
## 🌿 Branch Behavior
|
||||
|
||||
- **Protected branches** (master, main, production, release, stable): Hook runs automatically
|
||||
- **Feature branches**: Hook is skipped, allowing free development with debug code
|
||||
|
||||
## 🔍 Debug Patterns Detected
|
||||
|
||||
- **Console statements**: `console.log`, `console.debug`, `console.error`
|
||||
- **Template debug**: `Debug:`, `debug:` in Vue templates
|
||||
- **Debug constants**: `DEBUG_`, `debug_` variables
|
||||
- **HTML debug**: `<!-- debug` comments
|
||||
- **Debug attributes**: `debug="true"` attributes
|
||||
- **Vue debug**: `v-if="debug"`, `v-show="debug"`
|
||||
- **Debug TODOs**: `TODO debug`, `FIXME debug`
|
||||
|
||||
## 📁 Files Automatically Skipped
|
||||
|
||||
- Test files: `*.test.js`, `*.spec.ts`, `*.test.vue`
|
||||
- Scripts: `scripts/` directory
|
||||
- Test directories: `test-*` directories
|
||||
- Documentation: `docs/`, `*.md`, `*.txt`
|
||||
- Config files: `*.json`, `*.yml`, `*.yaml`
|
||||
- IDE files: `.cursor/` directory
|
||||
|
||||
## ✅ Verification
|
||||
|
||||
**After installation, verify it's working:**
|
||||
|
||||
```bash
|
||||
# Check if files exist
|
||||
ls -la .git/hooks/pre-commit
|
||||
ls -la .git/hooks/debug-checker.config
|
||||
|
||||
# Test the hook manually
|
||||
.git/hooks/pre-commit
|
||||
|
||||
# Test with actual commit
|
||||
echo "console.log('test')" > test.vue
|
||||
git add test.vue
|
||||
git commit -m "test" # Should be blocked
|
||||
```
|
||||
|
||||
## 📊 Example Output
|
||||
|
||||
```
|
||||
❌ Debug code detected in staged files!
|
||||
Branch: master
|
||||
Files checked: 1
|
||||
Errors found: 3
|
||||
|
||||
🚨 AccountViewView.vue: Found debug pattern 'console\.'
|
||||
🚨 AccountViewView.vue: Found debug pattern 'Debug:'
|
||||
🚨 AccountViewView.vue: Found debug pattern 'DEBUG_'
|
||||
|
||||
💡 Please remove debug code before committing to master
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Edit `.git/hooks/debug-checker.config` to customize:
|
||||
- **Protected branches**: Add/remove branches as needed
|
||||
- **Debug patterns**: Customize what gets detected
|
||||
- **Skip patterns**: Adjust file filtering rules
|
||||
|
||||
## 🚨 Emergency Bypass
|
||||
|
||||
If you absolutely need to commit debug code to a protected branch:
|
||||
```bash
|
||||
git commit --no-verify -m "emergency: debug code needed"
|
||||
```
|
||||
⚠️ **Warning**: This bypasses all pre-commit hooks. Use sparingly.
|
||||
|
||||
## 🔄 Updates
|
||||
|
||||
When the hook is updated in the main repository:
|
||||
```bash
|
||||
./scripts/install-debug-hook.sh
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Hook not running | Check if on protected branch, verify permissions |
|
||||
| Permission denied | Run `chmod +x .git/hooks/pre-commit` |
|
||||
| Files not found | Ensure you're copying from TimeSafari repo |
|
||||
| False positives | Edit `debug-checker.config` to customize patterns |
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
A test script is available at `scripts/test-debug-hook.sh` to verify the hook works correctly.
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
1. **Use feature branches** for development with debug code
|
||||
2. **Use proper logging** instead of console statements (`logger.info`, `logger.debug`)
|
||||
3. **Test thoroughly** before merging to protected branches
|
||||
4. **Review commits** to ensure no debug code slips through
|
||||
5. **Keep hooks updated** across all repositories
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- **Hook documentation**: `scripts/git-hooks/README.md`
|
||||
- **Configuration**: `scripts/git-hooks/debug-checker.config`
|
||||
- **Test script**: `scripts/test-debug-hook.sh`
|
||||
- **Installation script**: `scripts/install-debug-hook.sh`
|
||||
|
||||
## 🎯 Team Workflow
|
||||
|
||||
**Recommended setup:**
|
||||
1. **Repository setup**: Include hook files in `.githooks/` directory
|
||||
2. **Team onboarding**: Run installation script in each repo
|
||||
3. **Updates**: Re-run installation script when hooks are updated
|
||||
4. **Documentation**: Keep this guide updated
|
||||
|
||||
---
|
||||
|
||||
**Status**: Active and enforced
|
||||
**Last Updated**: 2025-01-27
|
||||
**Maintainer**: Matthew Raymer
|
||||
117
doc/logging-configuration.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Logging Configuration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
TimeSafari now supports configurable logging levels via the `VITE_LOG_LEVEL` environment variable. This allows developers to control the verbosity of console output without modifying code.
|
||||
|
||||
## Available Log Levels
|
||||
|
||||
| Level | Value | Description | Console Output |
|
||||
|-------|-------|-------------|----------------|
|
||||
| `error` | 0 | Errors only | Critical errors only |
|
||||
| `warn` | 1 | Warnings and errors | Warnings and errors |
|
||||
| `info` | 2 | Info, warnings, and errors | General information, warnings, and errors |
|
||||
| `debug` | 3 | All log levels | Verbose debugging information |
|
||||
|
||||
## Environment Variable
|
||||
|
||||
Set the `VITE_LOG_LEVEL` environment variable to control logging:
|
||||
|
||||
```bash
|
||||
# Show only errors
|
||||
VITE_LOG_LEVEL=error
|
||||
|
||||
# Show warnings and errors (default for production web)
|
||||
VITE_LOG_LEVEL=warn
|
||||
|
||||
# Show info, warnings, and errors (default for development/capacitor)
|
||||
VITE_LOG_LEVEL=info
|
||||
|
||||
# Show all log levels including debug
|
||||
VITE_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
## Default Behavior by Platform
|
||||
|
||||
The logger automatically selects appropriate default log levels based on your platform and environment:
|
||||
|
||||
- **Production Web**: `warn` (warnings and errors only)
|
||||
- **Electron**: `error` (errors only - very quiet)
|
||||
- **Development/Capacitor**: `info` (info and above)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Setting Log Level in Development
|
||||
|
||||
```bash
|
||||
# In your terminal before running the app
|
||||
export VITE_LOG_LEVEL=debug
|
||||
npm run dev
|
||||
|
||||
# Or inline
|
||||
VITE_LOG_LEVEL=debug npm run dev
|
||||
```
|
||||
|
||||
### Setting Log Level in Production
|
||||
|
||||
```bash
|
||||
# For verbose production logging
|
||||
VITE_LOG_LEVEL=info npm run build:web
|
||||
|
||||
# For minimal production logging
|
||||
VITE_LOG_LEVEL=warn npm run build:web
|
||||
```
|
||||
|
||||
### Programmatic Access
|
||||
|
||||
The logger provides methods to check current configuration:
|
||||
|
||||
```typescript
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
// Get current log level
|
||||
const currentLevel = logger.getCurrentLevel(); // 'info'
|
||||
|
||||
// Check if a level is enabled
|
||||
const debugEnabled = logger.isLevelEnabled('debug'); // false if level < debug
|
||||
|
||||
// Get available levels
|
||||
const levels = logger.getAvailableLevels(); // ['error', 'warn', 'info', 'debug']
|
||||
```
|
||||
|
||||
## Database Logging
|
||||
|
||||
Database logging continues to work regardless of console log level settings. All log messages are still stored in the database for debugging and audit purposes.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- **Existing code**: No changes required - logging behavior remains the same
|
||||
- **New feature**: Use `VITE_LOG_LEVEL` to override default behavior
|
||||
- **Backward compatible**: All existing logging calls work as before
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Development**: Use `VITE_LOG_LEVEL=debug` for maximum visibility
|
||||
2. **Testing**: Use `VITE_LOG_LEVEL=info` for balanced output
|
||||
3. **Production**: Use `VITE_LOG_LEVEL=warn` for minimal noise
|
||||
4. **Debugging**: Temporarily set `VITE_LOG_LEVEL=debug` to troubleshoot issues
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Logs Appearing
|
||||
|
||||
Check your `VITE_LOG_LEVEL` setting:
|
||||
```bash
|
||||
echo $VITE_LOG_LEVEL
|
||||
```
|
||||
|
||||
### Too Many Logs
|
||||
|
||||
Reduce verbosity by setting a lower log level:
|
||||
```bash
|
||||
VITE_LOG_LEVEL=warn
|
||||
```
|
||||
|
||||
### Platform-Specific Issues
|
||||
|
||||
Remember that Electron is very quiet by default. Use `VITE_LOG_LEVEL=info` to see more output in Electron builds.
|
||||
474
docs/PlatformServiceMixin-Interface-Consolidation.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# PlatformServiceMixin Interface Consolidation
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-13
|
||||
**Status**: 🎯 **PLANNING** - Ready for Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the planned consolidation of PlatformServiceMixin interfaces to
|
||||
eliminate duplication and ensure consistency between `IPlatformServiceMixin` and
|
||||
`ComponentCustomProperties` interfaces. **IMPORTANT**: The planned consolidation will
|
||||
introduce a breaking change by removing the deprecated `$updateSettings` method, which
|
||||
will cause runtime failures in components that still use it.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current PlatformServiceMixin has two separate interfaces with overlapping methods:
|
||||
|
||||
1. **`IPlatformServiceMixin`** - Exported interface for component typing
|
||||
2. **`ComponentCustomProperties`** - Vue declaration merging interface
|
||||
|
||||
This causes:
|
||||
- Duplicate method definitions
|
||||
- Inconsistent interface maintenance
|
||||
- Confusion about which interface to use
|
||||
- Deprecated methods still appearing in interfaces
|
||||
|
||||
### ComponentCustomProperties Usage Analysis
|
||||
|
||||
**Important Discovery**: `ComponentCustomProperties` is **NOT actually used** anywhere in
|
||||
the codebase for runtime functionality. It exists solely for **TypeScript declaration
|
||||
merging** to provide:
|
||||
|
||||
- Method autocomplete when typing `this.$` in Vue components
|
||||
- Type checking for mixin methods
|
||||
- IntelliSense support in development environments
|
||||
- Compile-time validation that methods exist
|
||||
|
||||
All components use PlatformServiceMixin methods by explicitly importing the mixin and
|
||||
adding it to the `@Component` decorator.
|
||||
|
||||
## Planned Solution: Interface Consolidation
|
||||
|
||||
### Single Source of Truth
|
||||
|
||||
The `IPlatformServiceMixin` interface will serve as the single source of truth for all
|
||||
PlatformServiceMixin methods. The `ComponentCustomProperties` interface will extend this
|
||||
interface to ensure complete consistency.
|
||||
|
||||
```typescript
|
||||
// Single interface definition
|
||||
export interface IPlatformServiceMixin {
|
||||
// All methods defined here
|
||||
}
|
||||
|
||||
// Vue declaration merging extends the main interface
|
||||
declare module "@vue/runtime-core" {
|
||||
interface ComponentCustomProperties extends IPlatformServiceMixin {
|
||||
// All methods inherited from IPlatformServiceMixin
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deprecated Method Removal - PLANNED BREAKING CHANGE
|
||||
|
||||
**⚠️ CRITICAL**: The deprecated `$updateSettings` method will be completely removed
|
||||
from both interfaces AND the implementation. This is a **PLANNED BREAKING CHANGE** that
|
||||
will:
|
||||
|
||||
- **Prevent TypeScript compilation** (method not found in interfaces)
|
||||
- **Cause runtime crashes** (method not found in mixin implementation)
|
||||
- **Break existing functionality** in components that use it
|
||||
|
||||
**Methods to Remove**:
|
||||
- ❌ **`$updateSettings(changes, did?)`** - Will be completely removed from interfaces and implementation
|
||||
|
||||
**Required Replacement Methods**:
|
||||
- ✅ **`$saveSettings(changes)`** - for default settings
|
||||
- ✅ **`$saveUserSettings(did, changes)`** - for user-specific settings
|
||||
- ✅ **`$saveMySettings(changes)`** - for current user's settings
|
||||
|
||||
## Current Status: PLANNING PHASE
|
||||
|
||||
### What Will Happen
|
||||
|
||||
1. **Interface Consolidation**: Will eliminate duplication between interfaces
|
||||
2. **Deprecated Method Removal**: Will remove runtime functionality for `$updateSettings`
|
||||
3. **Component Migration**: Will be required for components using the removed method
|
||||
|
||||
### Impact Assessment
|
||||
|
||||
#### Immediate Issues After Implementation
|
||||
- **Build failures**: TypeScript compilation errors
|
||||
- **Runtime crashes**: `$updateSettings` method not found
|
||||
- **Broken functionality**: Settings updates will fail
|
||||
|
||||
#### Affected Components
|
||||
The following components actively use `$updateSettings` and will break after implementation:
|
||||
|
||||
- `NewActivityView.vue` - 6 method references
|
||||
- `SearchAreaView.vue` - 2 method references
|
||||
- `FeedFilters.vue` - 4 method references
|
||||
- `HelpView.vue` - 1 method reference
|
||||
- `HelpNotificationsView.vue` - 1 method reference
|
||||
- `ContactQRScanShowView.vue` - 2 method references
|
||||
- `NewEditAccountView.vue` - 1 method reference
|
||||
- `SharedPhotoView.vue` - 1 method reference
|
||||
- `UserNameDialog.vue` - 1 method reference
|
||||
- `OnboardingDialog.vue` - 2 method references
|
||||
|
||||
**Total**: ~20+ method references across multiple components
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Stabilization (Before Breaking Change)
|
||||
|
||||
1. **Plan migration strategy** for all affected components
|
||||
2. **Create migration script** to automate method replacement
|
||||
3. **Update test coverage** for new method patterns
|
||||
|
||||
### Phase 2: Interface Consolidation
|
||||
|
||||
1. **Consolidate interfaces** - Eliminate duplication between `IPlatformServiceMixin` and `ComponentCustomProperties`
|
||||
2. **Remove deprecated method** from interfaces
|
||||
3. **Remove deprecated method** from implementation
|
||||
|
||||
### Phase 3: Component Migration
|
||||
|
||||
1. **Audit all components** using `$updateSettings`
|
||||
2. **Categorize usage patterns** (default vs. user-specific settings)
|
||||
3. **Replace method calls** with appropriate new methods
|
||||
4. **Update tests** to use new method patterns
|
||||
5. **Validate functionality** after each change
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Preparation (Before Breaking Change)
|
||||
|
||||
1. **Audit all components** using `$updateSettings`
|
||||
2. **Categorize usage patterns** (default vs. user-specific settings)
|
||||
3. **Create migration plan** for each component
|
||||
4. **Update test coverage** for new method patterns
|
||||
|
||||
### Phase 2: Interface Consolidation
|
||||
|
||||
```typescript
|
||||
// Remove from IPlatformServiceMixin interface
|
||||
// Remove from ComponentCustomProperties interface
|
||||
// Remove implementation from PlatformServiceMixin
|
||||
```
|
||||
|
||||
### Phase 3: Component Migration (Systematic)
|
||||
|
||||
1. **Replace method calls** with appropriate new methods
|
||||
2. **Update tests** to use new method patterns
|
||||
3. **Validate functionality** after each change
|
||||
4. **Monitor for any missed usage**
|
||||
|
||||
## Changes to Be Made
|
||||
|
||||
### Interface Consolidation
|
||||
|
||||
- **Consolidate** `IPlatformServiceMixin` and `ComponentCustomProperties` interfaces
|
||||
- **Eliminate duplication** by making `ComponentCustomProperties` extend
|
||||
`IPlatformServiceMixin`
|
||||
- **Single source of truth** for all PlatformServiceMixin methods
|
||||
|
||||
### Deprecated Method Removal - PLANNED BREAKING CHANGE
|
||||
|
||||
- **Remove** deprecated `$updateSettings` method from `IPlatformServiceMixin` interface
|
||||
- **Remove** deprecated `$updateSettings` method from `ComponentCustomProperties`
|
||||
interface
|
||||
- **Remove** deprecated `$updateSettings` method implementation
|
||||
- **⚠️ This will break existing functionality**
|
||||
|
||||
### Code Cleanup
|
||||
|
||||
- **Eliminate** duplicate method definitions
|
||||
- **Remove** outdated comments about deprecated methods
|
||||
- **Consolidate** interface maintenance to single location
|
||||
|
||||
## Files to Be Modified
|
||||
|
||||
### `src/utils/PlatformServiceMixin.ts`
|
||||
|
||||
- Remove deprecated `$updateSettings` method from `IPlatformServiceMixin` interface
|
||||
- Remove deprecated `$updateSettings` method from `ComponentCustomProperties`
|
||||
interface
|
||||
- Remove deprecated `$updateSettings` method implementation
|
||||
- Make `ComponentCustomProperties` extend `IPlatformServiceMixin` for consistency
|
||||
- Add comments explaining deprecated method removal
|
||||
|
||||
## Proper Usage Patterns
|
||||
|
||||
### Settings Management
|
||||
|
||||
#### Default Settings (Global)
|
||||
|
||||
```typescript
|
||||
// Save to master settings table
|
||||
await this.$saveSettings({
|
||||
apiServer: 'https://api.example.com',
|
||||
defaultLanguage: 'en'
|
||||
});
|
||||
```
|
||||
|
||||
#### User-Specific Settings
|
||||
|
||||
```typescript
|
||||
// Save to user-specific settings table
|
||||
await this.$saveUserSettings(userDid, {
|
||||
firstName: 'John',
|
||||
isRegistered: true,
|
||||
profileImageUrl: 'https://example.com/avatar.jpg'
|
||||
});
|
||||
```
|
||||
|
||||
#### Current User Settings
|
||||
|
||||
```typescript
|
||||
// Automatically uses current activeDid
|
||||
await this.$saveMySettings({
|
||||
firstName: 'John',
|
||||
isRegistered: true
|
||||
});
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
|
||||
#### Ultra-Concise Methods
|
||||
|
||||
```typescript
|
||||
// Shortest possible names for frequent operations
|
||||
const contacts = await this.$contacts();
|
||||
const settings = await this.$settings();
|
||||
const result = await this.$db("SELECT * FROM table WHERE id = ?", [id]);
|
||||
await this.$exec("UPDATE table SET field = ? WHERE id = ?", [value, id]);
|
||||
const row = await this.$one("SELECT * FROM table WHERE id = ?", [id]);
|
||||
```
|
||||
|
||||
#### Query + Mapping Combo
|
||||
|
||||
```typescript
|
||||
// Automatic result mapping
|
||||
const users = await this.$query<User>("SELECT * FROM users WHERE active = ?", [true]);
|
||||
const firstUser = await this.$first<User>("SELECT * FROM users WHERE id = ?", [id]);
|
||||
```
|
||||
|
||||
#### Entity Operations
|
||||
|
||||
```typescript
|
||||
// High-level entity management
|
||||
await this.$insertContact({
|
||||
did: 'did:example:123',
|
||||
name: 'John Doe',
|
||||
publicKeyBase64: 'base64key'
|
||||
});
|
||||
|
||||
await this.$updateContact('did:example:123', {
|
||||
name: 'John Smith'
|
||||
});
|
||||
|
||||
const contact = await this.$getContact('did:example:123');
|
||||
await this.$deleteContact('did:example:123');
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From $updateSettings to Proper Methods
|
||||
|
||||
#### Before (Deprecated - Will Break After Implementation)
|
||||
|
||||
```typescript
|
||||
// ❌ DEPRECATED - This will cause runtime crashes after implementation
|
||||
await this.$updateSettings({ firstName: 'John' });
|
||||
await this.$updateSettings({ isRegistered: true }, userDid);
|
||||
```
|
||||
|
||||
#### After (Required - After Migration)
|
||||
|
||||
```typescript
|
||||
// ✅ For default/global settings
|
||||
await this.$saveSettings({ firstName: 'John' });
|
||||
|
||||
// ✅ For user-specific settings
|
||||
await this.$saveUserSettings(userDid, { isRegistered: true });
|
||||
|
||||
// ✅ For current user (automatically uses activeDid)
|
||||
await this.$saveMySettings({ firstName: 'John' });
|
||||
```
|
||||
|
||||
### Component Implementation
|
||||
|
||||
#### Class Component with Mixin
|
||||
|
||||
```typescript
|
||||
import { Component, Vue } from 'vue-facing-decorator';
|
||||
import { PlatformServiceMixin } from '@/utils/PlatformServiceMixin';
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin]
|
||||
})
|
||||
export default class MyComponent extends Vue {
|
||||
async saveUserProfile() {
|
||||
// Use the consolidated interface methods
|
||||
await this.$saveUserSettings(this.activeDid, {
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName
|
||||
});
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
// Use ultra-concise methods
|
||||
const contacts = await this.$contacts();
|
||||
const settings = await this.$settings();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Composition API with Mixin
|
||||
|
||||
```typescript
|
||||
import { defineComponent } from 'vue';
|
||||
import { PlatformServiceMixin } from '@/utils/PlatformServiceMixin';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [PlatformServiceMixin],
|
||||
async setup() {
|
||||
// Methods available through mixin
|
||||
const saveSettings = async (changes) => {
|
||||
return await this.$saveSettings(changes);
|
||||
};
|
||||
|
||||
return {
|
||||
saveSettings
|
||||
};
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Impact
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Eliminates interface duplication** - single source of truth
|
||||
- **Forces proper method usage** - no more deprecated `$updateSettings`
|
||||
- **Improves maintainability** - changes only needed in one place
|
||||
- **Enhances type safety** - consistent interfaces across all contexts
|
||||
- **Better developer experience** - clear method patterns and documentation
|
||||
|
||||
### Breaking Changes - CRITICAL
|
||||
|
||||
- **`$updateSettings` method no longer available** - will cause runtime crashes
|
||||
- **Interface consolidation** - ensures consistent method availability
|
||||
- **App will not work** until migration is complete
|
||||
|
||||
### Migration Required
|
||||
|
||||
- **All components using `$updateSettings`** must be updated to use proper settings
|
||||
methods
|
||||
- **Systematic migration** needed across multiple components
|
||||
- **Breaking change** will be introduced after implementation
|
||||
|
||||
## Security Audit Checklist
|
||||
|
||||
- ✅ **Input Validation**: All database methods include proper parameter validation
|
||||
- ✅ **SQL Injection Protection**: Parameterized queries used throughout
|
||||
- ✅ **Access Control**: User-specific settings properly isolated by DID
|
||||
- ✅ **Error Handling**: Comprehensive error logging and graceful fallbacks
|
||||
- ✅ **Type Safety**: Full TypeScript support prevents invalid data types
|
||||
- ✅ **Transaction Management**: Automatic rollback on database errors
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
- **NO CACHING**: Settings loaded fresh every time (no stale data)
|
||||
- **NO CACHING**: Contacts loaded fresh every time (no stale data)
|
||||
- **NO CACHING**: All database operations return fresh data
|
||||
- Memory-efficient data structures
|
||||
|
||||
### Database Operations
|
||||
|
||||
- Ultra-concise method names reduce boilerplate
|
||||
- Automatic transaction management
|
||||
- Optimized SQL queries with proper indexing
|
||||
|
||||
### Resource Management
|
||||
|
||||
- **NO WeakMap-based caching** - all caching code is commented out
|
||||
- **NO cache invalidation** - not needed since nothing is cached
|
||||
- **NO memory leaks from caching** - because there is no caching
|
||||
- Efficient component lifecycle management
|
||||
|
||||
### Caching Confusion Clarification
|
||||
|
||||
**Important Note**: There are several references to "caching" throughout the codebase
|
||||
that are **misleading and incorrect**:
|
||||
|
||||
#### What the Documentation Claims vs. Reality
|
||||
|
||||
| Claimed Feature | Actual Reality |
|
||||
|----------------|----------------|
|
||||
| "Smart caching layer with TTL" | ❌ **NO CACHING IMPLEMENTED** |
|
||||
| "WeakMap-based caching prevents memory leaks" | ❌ **ALL CACHING CODE COMMENTED OUT** |
|
||||
| "Cached database operations" | ❌ **EVERYTHING LOADED FRESH** |
|
||||
| "Settings shortcuts for ultra-frequent update patterns" | ❌ **NO CACHING, JUST CONVENIENCE METHODS** |
|
||||
|
||||
#### Evidence of No Caching
|
||||
|
||||
1. **All caching code is commented out** in `PlatformServiceMixin.ts`
|
||||
2. **Settings methods explicitly state** "WITHOUT caching" in comments
|
||||
3. **Contacts method explicitly states** "always fresh" in comments
|
||||
4. **No cache invalidation logic** exists
|
||||
5. **No TTL management** exists
|
||||
|
||||
#### Why This Confusion Exists
|
||||
|
||||
The caching system was **planned and designed** but **never implemented**. The
|
||||
documentation and comments reflect the original design intent, not the current
|
||||
reality. This is a case where the documentation is ahead of the implementation.
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
### Interface Testing
|
||||
|
||||
- All methods should be tested through the consolidated interface
|
||||
- Mock PlatformService for unit testing
|
||||
- Integration tests for database operations
|
||||
|
||||
### Migration Testing
|
||||
|
||||
- Verify deprecated methods are no longer accessible
|
||||
- Test new method signatures work correctly
|
||||
- Ensure backward compatibility for existing functionality
|
||||
|
||||
### Performance Testing
|
||||
|
||||
- Monitor database query performance
|
||||
- Verify caching behavior works as expected
|
||||
- Test memory usage patterns
|
||||
|
||||
## Next Steps - IMPLEMENTATION PLAN
|
||||
|
||||
1. **Plan migration strategy** - Systematic approach to updating components
|
||||
2. **Execute component migration** - Update all affected components
|
||||
3. **Implement interface consolidation** - Remove deprecated method and consolidate interfaces
|
||||
4. **Validate functionality** - Ensure all settings operations work correctly
|
||||
5. **Update documentation** - Reflect final state after implementation
|
||||
|
||||
## Conclusion
|
||||
|
||||
The planned PlatformServiceMixin interface consolidation will provide:
|
||||
|
||||
- **Single source of truth** for all mixin methods
|
||||
- **Elimination of deprecated methods** to prevent confusion
|
||||
- **Consistent interface** across all usage contexts
|
||||
- **Improved maintainability** and type safety
|
||||
- **Better developer experience** with clear method patterns
|
||||
|
||||
**⚠️ CRITICAL**: This consolidation will introduce a breaking change that requires
|
||||
careful planning and execution. The app will not work after implementation until:
|
||||
|
||||
1. **All components are migrated** to use the new methods, or
|
||||
2. **The deprecated method is restored** temporarily during migration
|
||||
|
||||
The fact that `ComponentCustomProperties` is only used for TypeScript support
|
||||
validates our approach - we're consolidating interfaces that serve different
|
||||
purposes (runtime vs. TypeScript support) while eliminating duplication.
|
||||
|
||||
**Status**: Planning phase - ready for implementation
|
||||
**Priority**: High - requires careful migration planning
|
||||
**Dependencies**: Component updates required before breaking change
|
||||
**Stakeholders**: Development team, QA team
|
||||
23
ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "splash@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "splash@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "splash@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 156 KiB |
530
package-lock.json
generated
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@playwright/test": "^1.45.2",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@types/dom-webcodecs": "^0.1.7",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
@@ -133,6 +133,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"tsx": "^4.20.4",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
@@ -3900,6 +3901,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
@@ -3917,6 +3935,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
@@ -3934,6 +3969,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
@@ -17406,6 +17458,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
|
||||
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/getenv": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
|
||||
@@ -26798,6 +26863,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-workspace-root": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.0.tgz",
|
||||
@@ -29272,6 +29347,459 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.4.tgz",
|
||||
"integrity": "sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
||||
"integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
|
||||
"integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
|
||||
"integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
|
||||
"integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
|
||||
"integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
|
||||
"integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
|
||||
"integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
|
||||
"integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
|
||||
"integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
|
||||
"integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
|
||||
"integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
|
||||
"integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
||||
"integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.9",
|
||||
"@esbuild/android-arm": "0.25.9",
|
||||
"@esbuild/android-arm64": "0.25.9",
|
||||
"@esbuild/android-x64": "0.25.9",
|
||||
"@esbuild/darwin-arm64": "0.25.9",
|
||||
"@esbuild/darwin-x64": "0.25.9",
|
||||
"@esbuild/freebsd-arm64": "0.25.9",
|
||||
"@esbuild/freebsd-x64": "0.25.9",
|
||||
"@esbuild/linux-arm": "0.25.9",
|
||||
"@esbuild/linux-arm64": "0.25.9",
|
||||
"@esbuild/linux-ia32": "0.25.9",
|
||||
"@esbuild/linux-loong64": "0.25.9",
|
||||
"@esbuild/linux-mips64el": "0.25.9",
|
||||
"@esbuild/linux-ppc64": "0.25.9",
|
||||
"@esbuild/linux-riscv64": "0.25.9",
|
||||
"@esbuild/linux-s390x": "0.25.9",
|
||||
"@esbuild/linux-x64": "0.25.9",
|
||||
"@esbuild/netbsd-arm64": "0.25.9",
|
||||
"@esbuild/netbsd-x64": "0.25.9",
|
||||
"@esbuild/openbsd-arm64": "0.25.9",
|
||||
"@esbuild/openbsd-x64": "0.25.9",
|
||||
"@esbuild/openharmony-arm64": "0.25.9",
|
||||
"@esbuild/sunos-x64": "0.25.9",
|
||||
"@esbuild/win32-arm64": "0.25.9",
|
||||
"@esbuild/win32-ia32": "0.25.9",
|
||||
"@esbuild/win32-x64": "0.25.9"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
||||
10
package.json
@@ -8,6 +8,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore 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",
|
||||
"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",
|
||||
@@ -25,6 +26,10 @@
|
||||
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
|
||||
"build:native": "vite build && npx cap sync && npx capacitor-assets generate",
|
||||
"assets:config": "tsx scripts/assets-config.ts",
|
||||
"assets:validate": "tsx scripts/assets-validator.ts",
|
||||
"assets:clean": "rimraf android/app/src/main/res/mipmap-* ios/App/App/Assets.xcassets/**/AppIcon*.png ios/App/App/Assets.xcassets/**/Splash*.png || true",
|
||||
"build:ios": "./scripts/build-ios.sh",
|
||||
"build:ios:dev": "./scripts/build-ios.sh --dev",
|
||||
"build:ios:test": "./scripts/build-ios.sh --test",
|
||||
@@ -53,6 +58,8 @@
|
||||
"build:web:docker:test": "./scripts/build-web.sh --docker:test",
|
||||
"build:web:docker:prod": "./scripts/build-web.sh --docker:prod",
|
||||
"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: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",
|
||||
@@ -204,7 +211,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@playwright/test": "^1.45.2",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@types/dom-webcodecs": "^0.1.7",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
@@ -241,6 +248,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"tsx": "^4.20.4",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: isLinuxEnvironment() ? 4 : undefined,
|
||||
workers: 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
['list'],
|
||||
|
||||
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
224
scripts/assets-config.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* TimeSafari Asset Configuration Generator
|
||||
* Generates capacitor-assets configuration files with deterministic outputs
|
||||
* Author: Matthew Raymer
|
||||
*
|
||||
* Usage: tsx scripts/assets-config.ts
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PROJECT_ROOT = path.dirname(__dirname);
|
||||
|
||||
// TypeScript interfaces for asset configuration
|
||||
interface AdaptiveIconConfig {
|
||||
foreground: string;
|
||||
background: string;
|
||||
monochrome: string;
|
||||
}
|
||||
|
||||
interface AndroidIconConfig {
|
||||
adaptive: AdaptiveIconConfig;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface IOSIconConfig {
|
||||
padding: number;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface WebIconConfig {
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface IconConfig {
|
||||
source: string;
|
||||
android: AndroidIconConfig;
|
||||
ios: IOSIconConfig;
|
||||
web: WebIconConfig;
|
||||
}
|
||||
|
||||
interface AndroidSplashConfig {
|
||||
scale: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface IOSSplashConfig {
|
||||
useStoryBoard: boolean;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface SplashConfig {
|
||||
source: string;
|
||||
darkSource: string;
|
||||
android: AndroidSplashConfig;
|
||||
ios: IOSSplashConfig;
|
||||
}
|
||||
|
||||
interface AssetConfig {
|
||||
icon: IconConfig;
|
||||
splash: SplashConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate deterministic capacitor-assets configuration
|
||||
* @returns Sorted, stable configuration object
|
||||
*/
|
||||
function generateAssetConfig(): AssetConfig {
|
||||
const config: AssetConfig = {
|
||||
icon: {
|
||||
source: "resources/icon.png",
|
||||
android: {
|
||||
adaptive: {
|
||||
foreground: "resources/icon.png",
|
||||
background: "#121212",
|
||||
monochrome: "resources/icon.png"
|
||||
},
|
||||
target: "android/app/src/main/res"
|
||||
},
|
||||
ios: {
|
||||
padding: 0,
|
||||
target: "ios/App/App/Assets.xcassets/AppIcon.appiconset"
|
||||
},
|
||||
web: {
|
||||
target: "public/img/icons"
|
||||
}
|
||||
},
|
||||
splash: {
|
||||
source: "resources/splash.png",
|
||||
darkSource: "resources/splash_dark.png",
|
||||
android: {
|
||||
scale: "cover",
|
||||
target: "android/app/src/main/res"
|
||||
},
|
||||
ios: {
|
||||
useStoryBoard: true,
|
||||
target: "ios/App/App/Assets.xcassets"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return sortObjectKeys(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort object keys recursively for deterministic output
|
||||
* @param obj - Object to sort
|
||||
* @returns Object with sorted keys
|
||||
*/
|
||||
function sortObjectKeys(obj: any): any {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(sortObjectKeys);
|
||||
}
|
||||
|
||||
const sorted: any = {};
|
||||
Object.keys(obj)
|
||||
.sort()
|
||||
.forEach(key => {
|
||||
sorted[key] = sortObjectKeys(obj[key]);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that required source files exist
|
||||
*/
|
||||
function validateSourceFiles(): void {
|
||||
const requiredFiles = [
|
||||
'resources/icon.png',
|
||||
'resources/splash.png',
|
||||
'resources/splash_dark.png'
|
||||
];
|
||||
|
||||
const missingFiles = requiredFiles.filter(file => {
|
||||
const filePath = path.join(PROJECT_ROOT, file);
|
||||
return !fs.existsSync(filePath);
|
||||
});
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
console.error('❌ Missing required source files:');
|
||||
missingFiles.forEach(file => console.error(` ${file}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ All required source files found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write configuration to file with consistent formatting
|
||||
* @param config - Configuration object
|
||||
* @param outputPath - Output file path
|
||||
*/
|
||||
function writeConfig(config: AssetConfig, outputPath: string): void {
|
||||
const jsonString = JSON.stringify(config, null, 2);
|
||||
|
||||
// Ensure consistent line endings and no trailing whitespace
|
||||
const cleanJson = jsonString
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n') + '\n';
|
||||
|
||||
fs.writeFileSync(outputPath, cleanJson, 'utf8');
|
||||
console.log(`✅ Configuration written to: ${outputPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution function
|
||||
*/
|
||||
function main(): void {
|
||||
console.log('🔄 Generating TimeSafari asset configuration...');
|
||||
console.log(`📁 Project root: ${PROJECT_ROOT}`);
|
||||
console.log(`📅 Generated: ${new Date().toISOString()}`);
|
||||
|
||||
try {
|
||||
// Validate source files exist
|
||||
validateSourceFiles();
|
||||
|
||||
// Generate configuration
|
||||
const config = generateAssetConfig();
|
||||
|
||||
// Ensure config directory exists
|
||||
const configDir = path.join(PROJECT_ROOT, 'config', 'assets');
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write configuration files
|
||||
const capacitorAssetsConfigPath = path.join(configDir, 'capacitor-assets.config.json');
|
||||
writeConfig(config, capacitorAssetsConfigPath);
|
||||
|
||||
// Copy to root for capacitor-assets discovery
|
||||
const rootConfigPath = path.join(PROJECT_ROOT, 'capacitor-assets.config.json');
|
||||
writeConfig(config, rootConfigPath);
|
||||
|
||||
console.log('🎉 Asset configuration generation completed successfully!');
|
||||
console.log('');
|
||||
console.log('📋 Next steps:');
|
||||
console.log(' 1. Review the generated configuration');
|
||||
console.log(' 2. Commit the configuration files');
|
||||
console.log(' 3. Run "npm run assets:validate" to verify');
|
||||
console.log(' 4. Use "npm run build:native" for builds');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Configuration generation failed:', error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { generateAssetConfig, sortObjectKeys, validateSourceFiles, AssetConfig };
|
||||
239
scripts/assets-validator.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* TimeSafari Asset Configuration Validator
|
||||
* Validates capacitor-assets configuration against schema and source files
|
||||
* Author: Matthew Raymer
|
||||
*
|
||||
* Usage: tsx scripts/assets-validator.ts [config-path]
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PROJECT_ROOT = path.dirname(__dirname);
|
||||
|
||||
// TypeScript interfaces for validation
|
||||
interface ValidationError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface AssetConfig {
|
||||
icon?: {
|
||||
source?: string;
|
||||
android?: {
|
||||
adaptive?: {
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
monochrome?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
splash?: {
|
||||
source?: string;
|
||||
darkSource?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse JSON file
|
||||
* @param filePath - Path to JSON file
|
||||
* @returns Parsed JSON object
|
||||
*/
|
||||
function loadJsonFile(filePath: string): AssetConfig {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration against schema
|
||||
* @param config - Configuration object to validate
|
||||
* @returns Array of validation errors
|
||||
*/
|
||||
function validateAgainstSchema(config: AssetConfig): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// Basic structure validation
|
||||
if (!config.icon || !config.splash) {
|
||||
errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' });
|
||||
}
|
||||
|
||||
// Icon validation
|
||||
if (config.icon) {
|
||||
if (!config.icon.source) {
|
||||
errors.push({ message: 'Icon section must contain "source" field' });
|
||||
} else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) {
|
||||
errors.push({ message: 'Icon source must be a PNG or SVG file in resources/ directory' });
|
||||
}
|
||||
|
||||
// Android adaptive icon validation
|
||||
if (config.icon.android?.adaptive) {
|
||||
const adaptive = config.icon.android.adaptive;
|
||||
if (!adaptive.foreground || !adaptive.background) {
|
||||
errors.push({ message: 'Android adaptive icon must have both foreground and background' });
|
||||
}
|
||||
if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) {
|
||||
errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Splash validation
|
||||
if (config.splash) {
|
||||
if (!config.splash.source) {
|
||||
errors.push({ message: 'Splash section must contain "source" field' });
|
||||
} else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) {
|
||||
errors.push({ message: 'Splash source must be a PNG or SVG file in resources/ directory' });
|
||||
}
|
||||
|
||||
if (config.splash.darkSource && !/^resources\/.*\.(png|svg)$/.test(config.splash.darkSource)) {
|
||||
errors.push({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' });
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that source files exist
|
||||
* @param config - Configuration object
|
||||
* @returns Array of missing file errors
|
||||
*/
|
||||
function validateSourceFiles(config: AssetConfig): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const requiredFiles = new Set<string>();
|
||||
|
||||
// Collect all required source files
|
||||
if (config.icon?.source) requiredFiles.add(config.icon.source);
|
||||
if (config.icon?.android?.adaptive?.foreground) requiredFiles.add(config.icon.android.adaptive.foreground);
|
||||
if (config.icon?.android?.adaptive?.monochrome) requiredFiles.add(config.icon.android.adaptive.monochrome);
|
||||
if (config.splash?.source) requiredFiles.add(config.splash.source);
|
||||
if (config.splash?.darkSource) requiredFiles.add(config.splash.darkSource);
|
||||
|
||||
// Check each file exists
|
||||
requiredFiles.forEach(file => {
|
||||
const filePath = path.join(PROJECT_ROOT, file);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
errors.push({ message: `Source file not found: ${file}` });
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate target directories are writable
|
||||
* @param config - Configuration object
|
||||
* @returns Array of directory validation errors
|
||||
*/
|
||||
function validateTargetDirectories(config: AssetConfig): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const targetDirs = new Set<string>();
|
||||
|
||||
// Collect all target directories
|
||||
if (config.icon?.android?.target) targetDirs.add(config.icon.android.target);
|
||||
if (config.icon?.ios?.target) targetDirs.add(config.icon.ios.target);
|
||||
if (config.icon?.web?.target) targetDirs.add(config.icon.web.target);
|
||||
if (config.splash?.android?.target) targetDirs.add(config.splash.android.target);
|
||||
if (config.splash?.ios?.target) targetDirs.add(config.splash.ios.target);
|
||||
|
||||
// Check each target directory
|
||||
targetDirs.forEach(dir => {
|
||||
const dirPath = path.join(PROJECT_ROOT, dir);
|
||||
const parentDir = path.dirname(dirPath);
|
||||
|
||||
if (!fs.existsSync(parentDir)) {
|
||||
errors.push({ message: `Parent directory does not exist: ${parentDir}` });
|
||||
} else if (!fs.statSync(parentDir).isDirectory()) {
|
||||
errors.push({ message: `Parent path is not a directory: ${parentDir}` });
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation function
|
||||
* @param configPath - Path to configuration file
|
||||
* @returns True if validation passes
|
||||
*/
|
||||
function validateConfiguration(configPath: string): boolean {
|
||||
console.log('🔍 Validating TimeSafari asset configuration...');
|
||||
console.log(`📁 Config file: ${configPath}`);
|
||||
console.log(`📁 Project root: ${PROJECT_ROOT}`);
|
||||
|
||||
try {
|
||||
// Load configuration
|
||||
const config = loadJsonFile(configPath);
|
||||
console.log('✅ Configuration file loaded successfully');
|
||||
|
||||
// Load schema
|
||||
const schemaPath = path.join(PROJECT_ROOT, 'config', 'assets', 'schema.json');
|
||||
const schema = loadJsonFile(schemaPath);
|
||||
console.log('✅ Schema file loaded successfully');
|
||||
|
||||
// Perform validations
|
||||
const schemaErrors = validateAgainstSchema(config);
|
||||
const fileErrors = validateSourceFiles(config);
|
||||
const dirErrors = validateTargetDirectories(config);
|
||||
|
||||
// Report results
|
||||
const allErrors = [...schemaErrors, ...fileErrors, ...dirErrors];
|
||||
|
||||
if (allErrors.length === 0) {
|
||||
console.log('🎉 All validations passed successfully!');
|
||||
console.log('');
|
||||
console.log('📋 Configuration summary:');
|
||||
console.log(` Icon source: ${config.icon?.source || 'NOT SET'}`);
|
||||
console.log(` Splash source: ${config.splash?.source || 'NOT SET'}`);
|
||||
console.log(` Dark splash: ${config.splash?.darkSource || 'NOT SET'}`);
|
||||
console.log(` Android adaptive: ${config.icon?.android?.adaptive ? 'ENABLED' : 'DISABLED'}`);
|
||||
console.log(` iOS LaunchScreen: ${config.splash?.ios?.useStoryBoard ? 'ENABLED' : 'DISABLED'}`);
|
||||
return true;
|
||||
} else {
|
||||
console.error('❌ Validation failed with the following errors:');
|
||||
allErrors.forEach((error, index) => {
|
||||
console.error(` ${index + 1}. ${error.message}`);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Validation failed:', error instanceof Error ? error.message : String(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution function
|
||||
*/
|
||||
function main(): void {
|
||||
const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(`❌ Configuration file not found: ${configPath}`);
|
||||
console.log('');
|
||||
console.log('💡 Available options:');
|
||||
console.log(' - Use default: capacitor-assets.config.json');
|
||||
console.log(' - Specify path: tsx scripts/assets-validator.ts path/to/config.json');
|
||||
console.log(' - Generate config: npm run assets:config');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const success = validateConfiguration(configPath);
|
||||
process.exit(success ? 0 : 1);
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories, AssetConfig };
|
||||
@@ -184,7 +184,7 @@ log_info "Build mode: $BUILD_MODE"
|
||||
log_info "Build type: $BUILD_TYPE"
|
||||
|
||||
# Setup environment for Capacitor build
|
||||
setup_build_env "capacitor"
|
||||
setup_build_env "capacitor" "$BUILD_MODE"
|
||||
|
||||
# Override API servers for Android development
|
||||
if [ "$BUILD_MODE" = "development" ]; then
|
||||
@@ -254,9 +254,9 @@ if [ "$DEPLOY_APP" = true ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Step 1: Check and fix Android resources
|
||||
safe_execute "Checking Android resources" "$(dirname "$0")/check-android-resources.sh" || {
|
||||
log_warning "Resource check found issues, but continuing with build..."
|
||||
# Step 1: Validate asset configuration
|
||||
safe_execute "Validating asset configuration" "npm run assets:validate" || {
|
||||
log_warn "Asset validation found issues, but continuing with build..."
|
||||
}
|
||||
|
||||
# Step 2: Clean Android app
|
||||
|
||||
@@ -339,7 +339,7 @@ main_electron_build() {
|
||||
fi
|
||||
|
||||
# Setup environment
|
||||
setup_build_env "electron"
|
||||
setup_build_env "electron" "$BUILD_MODE"
|
||||
setup_app_directories
|
||||
load_env_file ".env"
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ log_info "Build mode: $BUILD_MODE"
|
||||
log_info "Build type: $BUILD_TYPE"
|
||||
|
||||
# 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
|
||||
if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then
|
||||
|
||||
@@ -300,18 +300,20 @@ serve_build() {
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# Use a simple HTTP server to serve the build
|
||||
if command -v python3 &> /dev/null; then
|
||||
# Use a server that supports SPA routing (serves index.html for all routes)
|
||||
if command -v npx &> /dev/null; then
|
||||
log_info "Starting npx serve with SPA support on port 8080..."
|
||||
npx serve -s dist -l 8080
|
||||
elif command -v python3 &> /dev/null; then
|
||||
log_warn "Python HTTP server doesn't support SPA routing. Routes like /discover, /account will return 404."
|
||||
log_info "Starting Python HTTP server on port 8080..."
|
||||
cd dist && python3 -m http.server 8080
|
||||
elif command -v python &> /dev/null; then
|
||||
log_warn "Python HTTP server doesn't support SPA routing. Routes like /discover, /account will return 404."
|
||||
log_info "Starting Python HTTP server on port 8080..."
|
||||
cd dist && python -m SimpleHTTPServer 8080
|
||||
elif command -v npx &> /dev/null; then
|
||||
log_info "Starting npx serve on port 8080..."
|
||||
npx serve -s dist -l 8080
|
||||
else
|
||||
log_error "No suitable HTTP server found. Install Python or npx serve."
|
||||
log_error "No suitable HTTP server found. Install npx serve or Python."
|
||||
exit 5
|
||||
fi
|
||||
}
|
||||
@@ -330,7 +332,7 @@ log_info "Serve build: $SERVE_BUILD"
|
||||
validate_web_environment
|
||||
|
||||
# Setup environment for web build
|
||||
setup_build_env "web"
|
||||
setup_build_env "web" "$BUILD_MODE"
|
||||
|
||||
# Setup application directories
|
||||
setup_app_directories
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TimeSafari Android Resource Check Script
|
||||
# Checks for missing Android resources and automatically fixes common issues
|
||||
# Author: Matthew Raymer
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res"
|
||||
ASSETS_DIR="$PROJECT_ROOT/assets"
|
||||
|
||||
echo "=== TimeSafari Android Resource Check ==="
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Checking Android resources"
|
||||
|
||||
# Function to check if a file exists
|
||||
check_file() {
|
||||
local file="$1"
|
||||
local description="$2"
|
||||
if [ -f "$file" ]; then
|
||||
echo "[✓] $description: $file"
|
||||
return 0
|
||||
else
|
||||
echo "[✗] $description: $file (MISSING)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if a directory exists and has files
|
||||
check_directory() {
|
||||
local dir="$1"
|
||||
local description="$2"
|
||||
if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then
|
||||
echo "[✓] $description: $dir"
|
||||
return 0
|
||||
else
|
||||
echo "[✗] $description: $dir (MISSING OR EMPTY)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Track issues
|
||||
issues_found=0
|
||||
fixes_applied=0
|
||||
|
||||
echo "[INFO] Checking splash screen resources..."
|
||||
# Check splash screen resources
|
||||
if ! check_file "$ANDROID_RES_DIR/drawable/splash.png" "Splash screen (light)"; then
|
||||
if [ -f "$ASSETS_DIR/splash.png" ]; then
|
||||
echo "[FIX] Copying splash.png to Android resources..."
|
||||
cp "$ASSETS_DIR/splash.png" "$ANDROID_RES_DIR/drawable/splash.png"
|
||||
fixes_applied=$((fixes_applied + 1))
|
||||
else
|
||||
issues_found=$((issues_found + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! check_file "$ANDROID_RES_DIR/drawable/splash_dark.png" "Splash screen (dark)"; then
|
||||
if [ -f "$ASSETS_DIR/splash_dark.png" ]; then
|
||||
echo "[FIX] Copying splash_dark.png to Android resources..."
|
||||
cp "$ASSETS_DIR/splash_dark.png" "$ANDROID_RES_DIR/drawable/splash_dark.png"
|
||||
fixes_applied=$((fixes_applied + 1))
|
||||
else
|
||||
issues_found=$((issues_found + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[INFO] Checking launcher icon resources..."
|
||||
# Check launcher icon resources
|
||||
required_icons=(
|
||||
"mipmap-mdpi/ic_launcher.png"
|
||||
"mipmap-hdpi/ic_launcher.png"
|
||||
"mipmap-xhdpi/ic_launcher.png"
|
||||
"mipmap-xxhdpi/ic_launcher.png"
|
||||
"mipmap-xxxhdpi/ic_launcher.png"
|
||||
"mipmap-anydpi-v26/ic_launcher.xml"
|
||||
"mipmap-anydpi-v26/ic_launcher_round.xml"
|
||||
)
|
||||
|
||||
missing_icons=0
|
||||
for icon in "${required_icons[@]}"; do
|
||||
if ! check_file "$ANDROID_RES_DIR/$icon" "Launcher icon: $icon"; then
|
||||
missing_icons=$((missing_icons + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $missing_icons -gt 0 ]; then
|
||||
echo "[FIX] Missing launcher icons detected. Running icon generation script..."
|
||||
if [ -f "$SCRIPT_DIR/generate-android-icons.sh" ]; then
|
||||
"$SCRIPT_DIR/generate-android-icons.sh"
|
||||
fixes_applied=$((fixes_applied + 1))
|
||||
else
|
||||
echo "[ERROR] Icon generation script not found: $SCRIPT_DIR/generate-android-icons.sh"
|
||||
issues_found=$((issues_found + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[INFO] Checking Capacitor platform status..."
|
||||
# Check if Android platform is properly initialized
|
||||
if [ ! -d "$PROJECT_ROOT/android" ]; then
|
||||
echo "[ERROR] Android platform directory not found"
|
||||
issues_found=$((issues_found + 1))
|
||||
elif [ ! -f "$PROJECT_ROOT/android/app/src/main/AndroidManifest.xml" ]; then
|
||||
echo "[ERROR] AndroidManifest.xml not found - platform may be corrupted"
|
||||
issues_found=$((issues_found + 1))
|
||||
else
|
||||
echo "[✓] Android platform appears to be properly initialized"
|
||||
fi
|
||||
|
||||
# Check for common build issues
|
||||
echo "[INFO] Checking for common build issues..."
|
||||
|
||||
# Check for invalid resource names (dashes in filenames)
|
||||
invalid_resources=$(find "$ANDROID_RES_DIR" -name "*-*" -type f 2>/dev/null | grep -E '\.(png|jpg|jpeg|gif|xml)$' || true)
|
||||
if [ -n "$invalid_resources" ]; then
|
||||
echo "[WARNING] Found resources with invalid names (containing dashes):"
|
||||
echo "$invalid_resources" | while read -r file; do
|
||||
echo " - $file"
|
||||
done
|
||||
echo "[INFO] Android resource names must contain only lowercase a-z, 0-9, or underscore"
|
||||
issues_found=$((issues_found + 1))
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=== Resource Check Summary ==="
|
||||
if [ $issues_found -eq 0 ] && [ $fixes_applied -eq 0 ]; then
|
||||
echo "[SUCCESS] All Android resources are present and valid"
|
||||
exit 0
|
||||
elif [ $fixes_applied -gt 0 ]; then
|
||||
echo "[SUCCESS] Fixed $fixes_applied resource issues automatically"
|
||||
if [ $issues_found -gt 0 ]; then
|
||||
echo "[WARNING] $issues_found issues remain that require manual attention"
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "[ERROR] Found $issues_found resource issues that require manual attention"
|
||||
exit 1
|
||||
fi
|
||||
28
scripts/check-update-settings.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
# CI check script to ensure no new $updateSettings usage is introduced
|
||||
# This script will fail CI if any $updateSettings calls are found
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔍 Checking for deprecated \$updateSettings usage..."
|
||||
|
||||
# Search for $updateSettings usage in source files
|
||||
USAGE_COUNT=$(grep -r "\$updateSettings" src/ --include="*.vue" --include="*.ts" --include="*.js" | wc -l)
|
||||
|
||||
if [ "$USAGE_COUNT" -gt 0 ]; then
|
||||
echo "❌ Found $USAGE_COUNT usage(s) of deprecated \$updateSettings method:"
|
||||
echo ""
|
||||
grep -r "\$updateSettings" src/ --include="*.vue" --include="*.ts" --include="*.js" -n
|
||||
echo ""
|
||||
echo "⚠️ Migration required:"
|
||||
echo " - For global settings: use \$saveSettings(changes)"
|
||||
echo " - For user-specific settings: use \$saveUserSettings(did, changes)"
|
||||
echo " - For current user settings: use \$saveMySettings(changes)"
|
||||
echo ""
|
||||
echo "Run 'node scripts/migrate-update-settings.js' for migration guidance."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ No \$updateSettings usage found!"
|
||||
exit 0
|
||||
fi
|
||||
@@ -174,9 +174,9 @@ validate_env_vars() {
|
||||
# Function to set environment variables for different build types
|
||||
setup_build_env() {
|
||||
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
|
||||
local git_hash=$(get_git_hash)
|
||||
@@ -204,19 +204,19 @@ setup_build_env() {
|
||||
esac
|
||||
|
||||
# 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
|
||||
# Android builds will override this in build-android.sh
|
||||
export VITE_DEFAULT_ENDORSER_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"
|
||||
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_PARTNER_API_SERVER="https://test-partner-api.endorser.ch"
|
||||
log_debug "Test mode: Using test Endorser and Partner APIs"
|
||||
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_PARTNER_API_SERVER="https://partner-api.endorser.ch"
|
||||
log_debug "Production mode: Using production API servers"
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TimeSafari Android Icon Generation Script
|
||||
# Generates all required Android launcher icon sizes from assets/icon.png
|
||||
# Author: Matthew Raymer
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
ASSETS_DIR="$PROJECT_ROOT/assets"
|
||||
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res"
|
||||
|
||||
echo "=== TimeSafari Android Icon Generation ==="
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting Android icon generation"
|
||||
|
||||
# Check if source icon exists
|
||||
if [ ! -f "$ASSETS_DIR/icon.png" ]; then
|
||||
echo "[ERROR] Source icon not found: $ASSETS_DIR/icon.png"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if ImageMagick is available
|
||||
if ! command -v convert &> /dev/null; then
|
||||
echo "[ERROR] ImageMagick (convert) not found. Please install ImageMagick."
|
||||
echo " Arch: sudo pacman -S imagemagick"
|
||||
echo " Ubuntu: sudo apt-get install imagemagick"
|
||||
echo " macOS: brew install imagemagick"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create mipmap directories if they don't exist
|
||||
mkdir -p "$ANDROID_RES_DIR/mipmap-hdpi"
|
||||
mkdir -p "$ANDROID_RES_DIR/mipmap-mdpi"
|
||||
mkdir -p "$ANDROID_RES_DIR/mipmap-xhdpi"
|
||||
mkdir -p "$ANDROID_RES_DIR/mipmap-xxhdpi"
|
||||
mkdir -p "$ANDROID_RES_DIR/mipmap-xxxhdpi"
|
||||
|
||||
echo "[INFO] Generating launcher icons..."
|
||||
|
||||
# Generate launcher icons for different densities
|
||||
# Android launcher icon sizes: mdpi=48, hdpi=72, xhdpi=96, xxhdpi=144, xxxhdpi=192
|
||||
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png"
|
||||
|
||||
# Generate round launcher icons
|
||||
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png"
|
||||
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png"
|
||||
|
||||
# Create background and foreground mipmap files
|
||||
# These reference the existing drawable files
|
||||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_background.xml" << 'EOF'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
EOF
|
||||
|
||||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_foreground.xml" << 'EOF'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
EOF
|
||||
|
||||
# Update the existing launcher XML files to reference the correct resources
|
||||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher.xml" << 'EOF'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
EOF
|
||||
|
||||
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_round.xml" << 'EOF'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
EOF
|
||||
|
||||
echo "[SUCCESS] Generated Android launcher icons:"
|
||||
echo " - mipmap-mdpi/ic_launcher.png (48x48)"
|
||||
echo " - mipmap-hdpi/ic_launcher.png (72x72)"
|
||||
echo " - mipmap-xhdpi/ic_launcher.png (96x96)"
|
||||
echo " - mipmap-xxhdpi/ic_launcher.png (144x144)"
|
||||
echo " - mipmap-xxxhdpi/ic_launcher.png (192x192)"
|
||||
echo " - Updated mipmap-anydpi-v26 XML files"
|
||||
echo "[SUCCESS] Android icon generation completed successfully!"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create directories if they don't exist
|
||||
mkdir -p android/app/src/main/res/mipmap-mdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-hdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-xhdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-xxhdpi
|
||||
mkdir -p android/app/src/main/res/mipmap-xxxhdpi
|
||||
|
||||
# Generate placeholder icons using ImageMagick
|
||||
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png
|
||||
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png
|
||||
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
|
||||
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
|
||||
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
|
||||
|
||||
# Copy to round versions
|
||||
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
|
||||
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
|
||||
103
scripts/git-hooks/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# TimeSafari Git Hooks
|
||||
|
||||
This directory contains custom Git hooks for the TimeSafari project.
|
||||
|
||||
## Debug Code Checker Hook
|
||||
|
||||
### Overview
|
||||
The `pre-commit` hook automatically checks for debug code when committing to protected branches (master, main, production, release). This prevents debug statements from accidentally reaching production code.
|
||||
|
||||
### How It Works
|
||||
1. **Branch Detection**: Only runs on protected branches (configurable)
|
||||
2. **File Filtering**: Automatically skips test files, scripts, and documentation
|
||||
3. **Pattern Matching**: Detects common debug patterns using regex
|
||||
4. **Commit Prevention**: Blocks commits containing debug code
|
||||
|
||||
### Protected Branches (Default)
|
||||
- `master`
|
||||
- `main`
|
||||
- `production`
|
||||
- `release`
|
||||
- `stable`
|
||||
|
||||
### Debug Patterns Detected
|
||||
- **Console statements**: `console.log`, `console.debug`, `console.error`
|
||||
- **Template debug**: `Debug:`, `debug:` in Vue templates
|
||||
- **Debug constants**: `DEBUG_`, `debug_` variables
|
||||
- **HTML debug**: `<!-- debug` comments
|
||||
- **Debug attributes**: `debug="true"` attributes
|
||||
- **Vue debug**: `v-if="debug"`, `v-show="debug"`
|
||||
- **Debug TODOs**: `TODO debug`, `FIXME debug`
|
||||
|
||||
### Files Automatically Skipped
|
||||
- Test files: `*.test.js`, `*.spec.ts`, `*.test.vue`
|
||||
- Scripts: `scripts/` directory
|
||||
- Test directories: `test-*` directories
|
||||
- Documentation: `docs/`, `*.md`, `*.txt`
|
||||
- Config files: `*.json`, `*.yml`, `*.yaml`
|
||||
- IDE files: `.cursor/` directory
|
||||
|
||||
### Configuration
|
||||
Edit `.git/hooks/debug-checker.config` to customize:
|
||||
- Protected branches
|
||||
- Debug patterns
|
||||
- Skip patterns
|
||||
- Logging level
|
||||
|
||||
### Testing the Hook
|
||||
Run the test script to verify the hook works:
|
||||
```bash
|
||||
./scripts/test-debug-hook.sh
|
||||
```
|
||||
|
||||
### Manual Testing
|
||||
1. Make changes to a file with debug code
|
||||
2. Stage the file: `git add <filename>`
|
||||
3. Try to commit: `git commit -m 'test'`
|
||||
4. Hook should prevent commit if debug code is found
|
||||
|
||||
### Bypassing the Hook (Emergency)
|
||||
If you absolutely need to commit debug code to a protected branch:
|
||||
```bash
|
||||
git commit --no-verify -m "emergency: debug code needed"
|
||||
```
|
||||
⚠️ **Warning**: This bypasses all pre-commit hooks. Use sparingly and only in emergencies.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Hook not running
|
||||
- Ensure the hook is executable: `chmod +x .git/hooks/pre-commit`
|
||||
- Check if you're on a protected branch
|
||||
- Verify the hook file exists and has correct permissions
|
||||
|
||||
#### False positives
|
||||
- Add legitimate debug patterns to skip patterns in config
|
||||
- Use proper logging levels (`logger.info`, `logger.debug`) instead of console
|
||||
- Move debug code to feature branches first
|
||||
|
||||
#### Hook too strict
|
||||
- Modify debug patterns in config file
|
||||
- Add more file types to skip patterns
|
||||
- Adjust protected branch list
|
||||
|
||||
### Best Practices
|
||||
1. **Use feature branches** for development with debug code
|
||||
2. **Use proper logging** instead of console statements
|
||||
3. **Test thoroughly** before merging to protected branches
|
||||
4. **Review commits** to ensure no debug code slips through
|
||||
5. **Keep config updated** as project needs change
|
||||
|
||||
### Integration with CI/CD
|
||||
This hook works locally. For CI/CD pipelines, consider:
|
||||
- Running the same checks in your build process
|
||||
- Adding ESLint rules for console statements
|
||||
- Using TypeScript strict mode
|
||||
- Adding debug code detection to PR checks
|
||||
|
||||
### Support
|
||||
If you encounter issues:
|
||||
1. Check the hook output for specific error messages
|
||||
2. Verify your branch is in the protected list
|
||||
3. Review the configuration file
|
||||
4. Test with the provided test script
|
||||
5. Check file permissions and git setup
|
||||
86
scripts/git-hooks/debug-checker.config
Normal file
@@ -0,0 +1,86 @@
|
||||
# TimeSafari Debug Checker Configuration
|
||||
# Edit this file to customize protected branches and debug patterns
|
||||
|
||||
# Protected branches where debug code checking is enforced
|
||||
# Add or remove branches as needed
|
||||
PROTECTED_BRANCHES=(
|
||||
"master"
|
||||
"main"
|
||||
"production"
|
||||
"release"
|
||||
"stable"
|
||||
)
|
||||
|
||||
# Debug patterns to detect (regex patterns)
|
||||
# Add or remove patterns as needed
|
||||
DEBUG_PATTERNS=(
|
||||
# Console statements
|
||||
"console\."
|
||||
|
||||
# Template debug text
|
||||
"Debug:"
|
||||
"debug:"
|
||||
|
||||
# Debug constants and variables
|
||||
"DEBUG_"
|
||||
"debug_"
|
||||
|
||||
# HTML debug comments
|
||||
"<!-- debug"
|
||||
|
||||
# Debug attributes
|
||||
"debug.*="
|
||||
|
||||
# Vue debug patterns
|
||||
"v-if.*debug"
|
||||
"v-show.*debug"
|
||||
|
||||
# Common debug text
|
||||
"TODO.*debug"
|
||||
"FIXME.*debug"
|
||||
|
||||
# Debug imports (uncomment if you want to catch these)
|
||||
# "import.*debug"
|
||||
# "require.*debug"
|
||||
)
|
||||
|
||||
# Files and directories to skip during checking
|
||||
# Add patterns to exclude from debug checking
|
||||
SKIP_PATTERNS=(
|
||||
"\.(test|spec)\.(js|ts|vue)$" # Test files (must have .test. or .spec.)
|
||||
"^scripts/" # Scripts directory
|
||||
"^test-.*/" # Test directories (must end with /)
|
||||
"^\.git/" # Git directory
|
||||
"^node_modules/" # Dependencies
|
||||
"^docs/" # Documentation
|
||||
"^\.cursor/" # Cursor IDE files
|
||||
"\.md$" # Markdown files
|
||||
"\.txt$" # Text files
|
||||
"\.json$" # JSON config files
|
||||
"\.yml$" # YAML config files
|
||||
"\.yaml$" # YAML config files
|
||||
)
|
||||
|
||||
# Files that are whitelisted for console statements
|
||||
# These files may contain intentional console.log statements that are
|
||||
# properly whitelisted with eslint-disable-next-line no-console comments
|
||||
WHITELIST_FILES=(
|
||||
"src/services/platforms/WebPlatformService.ts" # Worker context logging
|
||||
"src/services/platforms/CapacitorPlatformService.ts" # Platform-specific logging
|
||||
"src/services/platforms/ElectronPlatformService.ts" # Electron-specific logging
|
||||
"src/services/QRScanner/.*" # QR Scanner services
|
||||
"src/utils/logger.ts" # Logger utility itself
|
||||
"src/utils/LogCollector.ts" # Log collection utilities
|
||||
"scripts/.*" # Build and utility scripts
|
||||
"test-.*/.*" # Test directories
|
||||
".*\.test\..*" # Test files
|
||||
".*\.spec\..*" # Spec files
|
||||
)
|
||||
|
||||
# Logging level (debug, info, warn, error)
|
||||
LOG_LEVEL="info"
|
||||
|
||||
# Exit codes
|
||||
EXIT_SUCCESS=0
|
||||
EXIT_DEBUG_FOUND=1
|
||||
EXIT_ERROR=2
|
||||
252
scripts/git-hooks/pre-commit
Executable file
@@ -0,0 +1,252 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TimeSafari Pre-commit Hook - Debug Code Checker
|
||||
# Only runs on master or specified branches to catch debug code before it reaches production
|
||||
|
||||
# Hook directory
|
||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$HOOK_DIR/debug-checker.config"
|
||||
|
||||
# Default configuration (fallback if config file is missing)
|
||||
DEFAULT_PROTECTED_BRANCHES=("master" "main" "production" "release")
|
||||
DEFAULT_DEBUG_PATTERNS=(
|
||||
"console\."
|
||||
"Debug:"
|
||||
"debug:"
|
||||
"DEBUG_"
|
||||
"debug_"
|
||||
"<!-- debug"
|
||||
"debug.*="
|
||||
)
|
||||
DEFAULT_WHITELIST_FILES=(
|
||||
"src/services/platforms/WebPlatformService.ts"
|
||||
"src/services/platforms/CapacitorPlatformService.ts"
|
||||
"src/services/platforms/ElectronPlatformService.ts"
|
||||
)
|
||||
|
||||
# Load configuration from file if it exists
|
||||
load_config() {
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
# Source the config file to load variables
|
||||
# We'll use a safer approach by reading and parsing
|
||||
PROTECTED_BRANCHES=()
|
||||
DEBUG_PATTERNS=()
|
||||
SKIP_PATTERNS=()
|
||||
WHITELIST_FILES=()
|
||||
|
||||
# Read protected branches
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^PROTECTED_BRANCHES=\( ]]; then
|
||||
# Start reading array
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^\)$ ]]; then
|
||||
break
|
||||
fi
|
||||
if [[ "$line" =~ \"([^\"]+)\" ]]; then
|
||||
PROTECTED_BRANCHES+=("${BASH_REMATCH[1]}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done < "$CONFIG_FILE"
|
||||
|
||||
# Read debug patterns
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^DEBUG_PATTERNS=\( ]]; then
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^\)$ ]]; then
|
||||
break
|
||||
fi
|
||||
if [[ "$line" =~ \"([^\"]+)\" ]]; then
|
||||
DEBUG_PATTERNS+=("${BASH_REMATCH[1]}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done < "$CONFIG_FILE"
|
||||
|
||||
# Read skip patterns
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^SKIP_PATTERNS=\( ]]; then
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^\)$ ]]; then
|
||||
break
|
||||
fi
|
||||
if [[ "$line" =~ \"([^\"]+)\" ]]; then
|
||||
SKIP_PATTERNS+=("${BASH_REMATCH[1]}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done < "$CONFIG_FILE"
|
||||
|
||||
# Read whitelist files
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^WHITELIST_FILES=\( ]]; then
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^\)$ ]]; then
|
||||
break
|
||||
fi
|
||||
if [[ "$line" =~ \"([^\"]+)\" ]]; then
|
||||
WHITELIST_FILES+=("${BASH_REMATCH[1]}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done < "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Use defaults if config loading failed
|
||||
if [[ ${#PROTECTED_BRANCHES[@]} -eq 0 ]]; then
|
||||
PROTECTED_BRANCHES=("${DEFAULT_PROTECTED_BRANCHES[@]}")
|
||||
fi
|
||||
|
||||
if [[ ${#DEBUG_PATTERNS[@]} -eq 0 ]]; then
|
||||
DEBUG_PATTERNS=("${DEFAULT_DEBUG_PATTERNS[@]}")
|
||||
fi
|
||||
|
||||
if [[ ${#SKIP_PATTERNS[@]} -eq 0 ]]; then
|
||||
SKIP_PATTERNS=("${DEFAULT_SKIP_PATTERNS[@]}")
|
||||
fi
|
||||
|
||||
if [[ ${#WHITELIST_FILES[@]} -eq 0 ]]; then
|
||||
WHITELIST_FILES=("${DEFAULT_WHITELIST_FILES[@]}")
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Check if current branch is protected
|
||||
is_protected_branch() {
|
||||
local branch="$1"
|
||||
for protected in "${PROTECTED_BRANCHES[@]}"; do
|
||||
if [[ "$branch" == "$protected" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if file should be skipped
|
||||
should_skip_file() {
|
||||
local file="$1"
|
||||
for pattern in "${SKIP_PATTERNS[@]}"; do
|
||||
if [[ "$file" =~ $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if file is whitelisted for console statements
|
||||
is_whitelisted_file() {
|
||||
local file="$1"
|
||||
for whitelisted in "${WHITELIST_FILES[@]}"; do
|
||||
if [[ "$file" =~ $whitelisted ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
# Load configuration
|
||||
load_config
|
||||
|
||||
# Get current branch name
|
||||
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
|
||||
if [[ -z "$CURRENT_BRANCH" ]]; then
|
||||
echo "⚠️ Could not determine current branch, skipping debug check"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if we should run the hook
|
||||
if ! is_protected_branch "$CURRENT_BRANCH"; then
|
||||
echo "🔒 Pre-commit hook skipped - not on protected branch ($CURRENT_BRANCH)"
|
||||
echo " Protected branches: ${PROTECTED_BRANCHES[*]}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔍 Running debug code check on protected branch: $CURRENT_BRANCH"
|
||||
echo " Using config: $CONFIG_FILE"
|
||||
|
||||
# Get all staged files (modified, added, copied, merged)
|
||||
ALL_STAGED_FILES=$(git diff --cached --name-only)
|
||||
|
||||
|
||||
|
||||
if [ -z "$ALL_STAGED_FILES" ]; then
|
||||
echo "✅ No staged files to check"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize error tracking
|
||||
ERRORS_FOUND=0
|
||||
ERROR_MESSAGES=()
|
||||
FILES_CHECKED=0
|
||||
|
||||
# Check each staged file for debug patterns
|
||||
for file in $ALL_STAGED_FILES; do
|
||||
# Skip files that should be ignored
|
||||
if should_skip_file "$file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
FILES_CHECKED=$((FILES_CHECKED + 1))
|
||||
|
||||
# Check for debug patterns in the file
|
||||
for pattern in "${DEBUG_PATTERNS[@]}"; do
|
||||
# Skip console pattern checks for whitelisted files
|
||||
if [[ "$pattern" == "console\." ]] && is_whitelisted_file "$file"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# For new files, check the file content directly
|
||||
# For modified files, check the staged diff
|
||||
if [[ -f "$file" ]]; then
|
||||
# New file - check content directly
|
||||
if grep -E "$pattern" "$file" > /dev/null; then
|
||||
ERRORS_FOUND=$((ERRORS_FOUND + 1))
|
||||
ERROR_MESSAGES+=("🚨 $file: Found debug pattern '$pattern'")
|
||||
fi
|
||||
else
|
||||
# Modified file - check staged diff
|
||||
if git diff --cached "$file" | grep -E "$pattern" > /dev/null; then
|
||||
ERRORS_FOUND=$((ERRORS_FOUND + 1))
|
||||
ERROR_MESSAGES+=("🚨 $file: Found debug pattern '$pattern'")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Report results
|
||||
if [ $ERRORS_FOUND -gt 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Debug code detected in staged files!"
|
||||
echo " Branch: $CURRENT_BRANCH"
|
||||
echo " Files checked: $FILES_CHECKED"
|
||||
echo " Errors found: $ERRORS_FOUND"
|
||||
echo ""
|
||||
for msg in "${ERROR_MESSAGES[@]}"; do
|
||||
echo " $msg"
|
||||
done
|
||||
echo ""
|
||||
echo "💡 Please remove debug code before committing to $CURRENT_BRANCH"
|
||||
echo " Common debug patterns to check:"
|
||||
echo " - console.log, console.debug, console.error"
|
||||
echo " - Debug: or debug: in templates"
|
||||
echo " - DEBUG_ constants"
|
||||
echo " - HTML comments with debug"
|
||||
echo ""
|
||||
echo " If debug code is intentional, consider:"
|
||||
echo " - Moving to a feature branch first"
|
||||
echo " - Using proper logging levels (logger.info, logger.debug)"
|
||||
echo " - Adding debug code to .gitignore or .debugignore"
|
||||
echo ""
|
||||
echo " Configuration file: $CONFIG_FILE"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ No debug code found in $FILES_CHECKED staged files"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
171
scripts/install-debug-hook.sh
Executable file
@@ -0,0 +1,171 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TimeSafari Debug Hook Installer
|
||||
# Run this script in any repository to install the debug pre-commit hook
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}🔧 TimeSafari Debug Hook Installer${NC}"
|
||||
echo "============================================="
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Error: Not in a git repository${NC}"
|
||||
echo "Please run this script from within a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get repository root
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
HOOKS_DIR="$REPO_ROOT/.git/hooks"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo -e "${BLUE}Repository:${NC} $REPO_ROOT"
|
||||
echo -e "${BLUE}Hooks directory:${NC} $HOOKS_DIR"
|
||||
echo -e "${BLUE}Script directory:${NC} $SCRIPT_DIR"
|
||||
|
||||
# Check if hooks directory exists
|
||||
if [[ ! -d "$HOOKS_DIR" ]]; then
|
||||
echo -e "${RED}❌ Error: Hooks directory not found${NC}"
|
||||
echo "This repository may not be properly initialized"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we have the hook files in the repository
|
||||
HOOK_SCRIPT="$SCRIPT_DIR/git-hooks/pre-commit"
|
||||
CONFIG_FILE="$SCRIPT_DIR/git-hooks/debug-checker.config"
|
||||
|
||||
if [[ ! -f "$HOOK_SCRIPT" ]]; then
|
||||
echo -e "${RED}❌ Error: Pre-commit hook script not found${NC}"
|
||||
echo "Expected location: $HOOK_SCRIPT"
|
||||
echo "Make sure you're running this from the TimeSafari repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
echo -e "${RED}❌ Error: Debug checker config not found${NC}"
|
||||
echo "Expected location: $CONFIG_FILE"
|
||||
echo "Make sure you're running this from the TimeSafari repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if already installed
|
||||
if [[ -f "$HOOKS_DIR/pre-commit" && -f "$HOOKS_DIR/debug-checker.config" ]]; then
|
||||
echo -e "${YELLOW}⚠️ Debug hook already appears to be installed${NC}"
|
||||
echo -e " Checking if update is needed..."
|
||||
|
||||
# Check if files are different
|
||||
if diff "$HOOK_SCRIPT" "$HOOKS_DIR/pre-commit" > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}✅${NC} Hook script is up to date"
|
||||
HOOK_UP_TO_DATE=true
|
||||
else
|
||||
echo -e " ${YELLOW}⚠️ Hook script differs - will update${NC}"
|
||||
HOOK_UP_TO_DATE=false
|
||||
fi
|
||||
|
||||
if diff "$CONFIG_FILE" "$HOOKS_DIR/debug-checker.config" > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}✅${NC} Config file is up to date"
|
||||
CONFIG_UP_TO_DATE=true
|
||||
else
|
||||
echo -e " ${YELLOW}⚠️ Config file differs - will update${NC}"
|
||||
CONFIG_UP_TO_DATE=false
|
||||
fi
|
||||
|
||||
if [[ "$HOOK_UP_TO_DATE" == true && "$CONFIG_UP_TO_DATE" == true ]]; then
|
||||
echo -e "\n${GREEN}✅ Debug hook is already up to date!${NC}"
|
||||
echo -e " No installation needed"
|
||||
else
|
||||
echo -e "\n${BLUE}Updating existing installation...${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "\n${BLUE}Installing debug hook...${NC}"
|
||||
fi
|
||||
|
||||
# Copy/update the hook script if needed
|
||||
if [[ "$HOOK_UP_TO_DATE" != true ]]; then
|
||||
cp "$HOOK_SCRIPT" "$HOOKS_DIR/pre-commit"
|
||||
chmod +x "$HOOKS_DIR/pre-commit"
|
||||
echo -e " ${GREEN}✅${NC} Pre-commit hook installed/updated"
|
||||
fi
|
||||
|
||||
# Copy/update the config file if needed
|
||||
if [[ "$CONFIG_UP_TO_DATE" != true ]]; then
|
||||
cp "$CONFIG_FILE" "$HOOKS_DIR/debug-checker.config"
|
||||
echo -e " ${GREEN}✅${NC} Configuration file installed/updated"
|
||||
fi
|
||||
|
||||
# Copy/update the README if needed
|
||||
README_FILE="$SCRIPT_DIR/git-hooks/README.md"
|
||||
if [[ -f "$README_FILE" ]]; then
|
||||
if [[ ! -f "$HOOKS_DIR/README.md" ]] || ! diff "$README_FILE" "$HOOKS_DIR/README.md" > /dev/null 2>&1; then
|
||||
cp "$README_FILE" "$HOOKS_DIR/README.md"
|
||||
echo -e " ${GREEN}✅${NC} Documentation installed/updated"
|
||||
else
|
||||
echo -e " ${GREEN}✅${NC} Documentation is up to date"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}🎉 Debug hook installation complete!${NC}"
|
||||
|
||||
# Test the installation
|
||||
echo -e "\n${BLUE}Testing installation...${NC}"
|
||||
if [[ -x "$HOOKS_DIR/pre-commit" ]]; then
|
||||
echo -e " ${GREEN}✅${NC} Hook is executable"
|
||||
else
|
||||
echo -e " ${RED}❌${NC} Hook is not executable"
|
||||
fi
|
||||
|
||||
if [[ -f "$HOOKS_DIR/debug-checker.config" ]]; then
|
||||
echo -e " ${GREEN}✅${NC} Config file exists"
|
||||
else
|
||||
echo -e " ${RED}❌${NC} Config file missing"
|
||||
fi
|
||||
|
||||
# Show current branch status
|
||||
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")
|
||||
echo -e "\n${BLUE}Current branch:${NC} $CURRENT_BRANCH"
|
||||
|
||||
# Check if this is a protected branch
|
||||
PROTECTED_BRANCHES=("master" "main" "production" "release" "stable")
|
||||
IS_PROTECTED=false
|
||||
|
||||
for branch in "${PROTECTED_BRANCHES[@]}"; do
|
||||
if [[ "$CURRENT_BRANCH" == "$branch" ]]; then
|
||||
IS_PROTECTED=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$IS_PROTECTED" == true ]]; then
|
||||
echo -e "${YELLOW}⚠️ You're on a protected branch ($CURRENT_BRANCH)${NC}"
|
||||
echo -e " The debug hook will now run on all commits to this branch"
|
||||
echo -e " Consider switching to a feature branch for development"
|
||||
else
|
||||
echo -e "${GREEN}✅ You're on a feature branch ($CURRENT_BRANCH)${NC}"
|
||||
echo -e " The debug hook will be skipped on this branch"
|
||||
echo -e " You can develop with debug code freely"
|
||||
fi
|
||||
|
||||
echo -e "\n${BLUE}Next steps:${NC}"
|
||||
echo "1. The hook will now run automatically on protected branches"
|
||||
echo "2. Test it by trying to commit a file with debug code"
|
||||
echo "3. Use feature branches for development with debug code"
|
||||
echo "4. Check the README.md in .git/hooks/ for more information"
|
||||
|
||||
echo -e "\n${BLUE}To test the hook:${NC}"
|
||||
echo "1. Create a test file with debug code (e.g., console.log('test'))"
|
||||
echo "2. Stage it: git add <filename>"
|
||||
echo "3. Try to commit: git commit -m 'test'"
|
||||
echo "4. The hook should prevent the commit if debug code is found"
|
||||
|
||||
echo -e "\n${BLUE}To uninstall:${NC}"
|
||||
echo "rm $HOOKS_DIR/pre-commit"
|
||||
echo "rm $HOOKS_DIR/debug-checker.config"
|
||||
echo "rm $HOOKS_DIR/README.md"
|
||||
110
scripts/migrate-update-settings.js
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migration script to replace deprecated $updateSettings calls
|
||||
* with the appropriate new methods ($saveSettings, $saveUserSettings, $saveMySettings)
|
||||
*
|
||||
* Usage: node scripts/migrate-update-settings.js
|
||||
*
|
||||
* This script will:
|
||||
* 1. Find all files containing $updateSettings calls
|
||||
* 2. Show the migration suggestions for each call
|
||||
* 3. Optionally perform the replacements
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
|
||||
// Migration patterns
|
||||
const MIGRATION_PATTERNS = [
|
||||
{
|
||||
pattern: /\$updateSettings\(\s*(\{[^}]*\})\s*\)/g,
|
||||
replacement: '$saveMySettings($1)',
|
||||
description: 'Single parameter (changes only) -> $saveMySettings'
|
||||
},
|
||||
{
|
||||
pattern: /\$updateSettings\(\s*(\{[^}]*\})\s*,\s*([^)]+)\s*\)/g,
|
||||
replacement: '$saveUserSettings($2, $1)',
|
||||
description: 'Two parameters (changes, did) -> $saveUserSettings(did, changes)'
|
||||
}
|
||||
];
|
||||
|
||||
// Find all Vue and TypeScript files
|
||||
function findFiles() {
|
||||
const patterns = [
|
||||
'src/**/*.vue',
|
||||
'src/**/*.ts',
|
||||
'src/**/*.js'
|
||||
];
|
||||
|
||||
let files = [];
|
||||
patterns.forEach(pattern => {
|
||||
files = files.concat(glob.sync(pattern, { ignore: ['node_modules/**', 'dist/**'] }));
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
// Analyze a file for $updateSettings usage
|
||||
function analyzeFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const usages = [];
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (line.includes('$updateSettings')) {
|
||||
usages.push({
|
||||
line: index + 1,
|
||||
content: line.trim(),
|
||||
file: filePath
|
||||
});
|
||||
console.log(`\n${filePath}:${index + 1}`);
|
||||
console.log(` ${line.trim()}`);
|
||||
|
||||
// Show migration suggestion
|
||||
MIGRATION_PATTERNS.forEach(pattern => {
|
||||
if (pattern.pattern.test(line)) {
|
||||
const replacement = line.replace(pattern.pattern, pattern.replacement);
|
||||
console.log(` → ${replacement.trim()}`);
|
||||
console.log(` ${pattern.description}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
// Main execution
|
||||
function main() {
|
||||
console.log('🔍 Finding files with $updateSettings usage...\n');
|
||||
|
||||
const files = findFiles();
|
||||
let totalUsages = 0;
|
||||
|
||||
files.forEach(file => {
|
||||
const usages = analyzeFile(file);
|
||||
totalUsages += usages.length;
|
||||
});
|
||||
|
||||
console.log(`\n📊 Summary:`);
|
||||
console.log(` Files scanned: ${files.length}`);
|
||||
console.log(` Total usages: ${totalUsages}`);
|
||||
|
||||
if (totalUsages > 0) {
|
||||
console.log(`\n📝 Migration Guide:`);
|
||||
console.log(` 1. For global/default settings: use $saveSettings(changes)`);
|
||||
console.log(` 2. For user-specific settings: use $saveUserSettings(did, changes)`);
|
||||
console.log(` 3. For current user settings: use $saveMySettings(changes)`);
|
||||
console.log(`\n⚠️ Note: $updateSettings is deprecated and will be removed in a future version.`);
|
||||
} else {
|
||||
console.log(`\n✅ No $updateSettings usage found!`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { findFiles, analyzeFile, MIGRATION_PATTERNS };
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TimeSafari Generated Assets Purge Script
|
||||
# Removes generated Android assets and resources from git history
|
||||
# Author: Matthew Raymer
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "=== TimeSafari Generated Assets Purge ==="
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting git history cleanup"
|
||||
|
||||
# Check if we're in a git repository
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "[ERROR] Not in a git repository. Please run this script from the project root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we have uncommitted changes
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "[ERROR] You have uncommitted changes. Please commit or stash them first."
|
||||
echo "Current status:"
|
||||
git status --short
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create backup branch
|
||||
BACKUP_BRANCH="backup-before-asset-purge-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "[INFO] Creating backup branch: $BACKUP_BRANCH"
|
||||
git branch "$BACKUP_BRANCH"
|
||||
|
||||
echo "[INFO] Starting git filter-branch to remove generated assets..."
|
||||
|
||||
# Use git filter-branch to remove the directories from history
|
||||
git filter-branch --force --index-filter '
|
||||
# Remove generated Android assets directory
|
||||
git rm -rf --cached --ignore-unmatch android/app/src/main/assets/public/ 2>/dev/null || true
|
||||
|
||||
# Remove generated Android resources (but keep config files)
|
||||
git rm -rf --cached --ignore-unmatch android/app/src/main/res/drawable*/ 2>/dev/null || true
|
||||
git rm -rf --cached --ignore-unmatch android/app/src/main/res/mipmap*/ 2>/dev/null || true
|
||||
git rm -rf --cached --ignore-unmatch android/app/src/main/res/values/ic_launcher_background.xml 2>/dev/null || true
|
||||
|
||||
# Keep configuration files
|
||||
git add android/app/src/main/res/values/strings.xml 2>/dev/null || true
|
||||
git add android/app/src/main/res/values/styles.xml 2>/dev/null || true
|
||||
git add android/app/src/main/res/layout/activity_main.xml 2>/dev/null || true
|
||||
git add android/app/src/main/res/xml/config.xml 2>/dev/null || true
|
||||
git add android/app/src/main/res/xml/file_paths.xml 2>/dev/null || true
|
||||
' --prune-empty --tag-name-filter cat -- --all
|
||||
|
||||
echo "[INFO] Cleaning up git filter-branch temporary files..."
|
||||
rm -rf .git/refs/original/
|
||||
git reflog expire --expire=now --all
|
||||
git gc --prune=now --aggressive
|
||||
|
||||
echo "[SUCCESS] Generated assets purged from git history!"
|
||||
echo "[INFO] Backup branch created: $BACKUP_BRANCH"
|
||||
echo "[INFO] Repository size should be significantly reduced"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Test that the repository works correctly"
|
||||
echo "2. Force push to remote: git push --force-with-lease origin <branch>"
|
||||
echo "3. Inform team members to re-clone or reset their local repositories"
|
||||
echo "4. Delete backup branch when confident: git branch -D $BACKUP_BRANCH"
|
||||
117
scripts/test-debug-hook.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for the debug pre-commit hook
|
||||
# This script helps verify that the hook is working correctly
|
||||
|
||||
set -e
|
||||
|
||||
echo "🧪 Testing TimeSafari Debug Pre-commit Hook"
|
||||
echo "============================================="
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test directory
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
echo -e "${BLUE}Created test directory: $TEST_DIR${NC}"
|
||||
|
||||
# Function to cleanup
|
||||
cleanup() {
|
||||
echo -e "${YELLOW}Cleaning up test directory...${NC}"
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
# Set trap to cleanup on exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Function to run test
|
||||
run_test() {
|
||||
local test_name="$1"
|
||||
local test_file="$2"
|
||||
local expected_exit="$3"
|
||||
|
||||
echo -e "\n${BLUE}Running test: $test_name${NC}"
|
||||
|
||||
# Create test file
|
||||
echo "$test_file" > "$TEST_DIR/test.vue"
|
||||
|
||||
# Stage the file
|
||||
cd "$TEST_DIR"
|
||||
git init > /dev/null 2>&1
|
||||
git add test.vue > /dev/null 2>&1
|
||||
|
||||
# Run the hook
|
||||
if bash ../../.git/hooks/pre-commit > hook_output.txt 2>&1; then
|
||||
exit_code=0
|
||||
else
|
||||
exit_code=$?
|
||||
fi
|
||||
|
||||
# Check result
|
||||
if [[ $exit_code -eq $expected_exit ]]; then
|
||||
echo -e " ${GREEN}✅ PASS${NC} - Exit code: $exit_code (expected: $expected_exit)"
|
||||
else
|
||||
echo -e " ${RED}❌ FAIL${NC} - Exit code: $exit_code (expected: $expected_exit)"
|
||||
echo -e " ${YELLOW}Hook output:${NC}"
|
||||
cat hook_output.txt
|
||||
fi
|
||||
|
||||
# Cleanup git
|
||||
rm -rf .git
|
||||
rm -f hook_output.txt
|
||||
}
|
||||
|
||||
# Test cases
|
||||
echo -e "\n${BLUE}Test Case 1: Clean file (should pass)${NC}"
|
||||
run_test "Clean file" "// No debug code here" 0
|
||||
|
||||
echo -e "\n${BLUE}Test Case 2: Console statement (should fail)${NC}"
|
||||
run_test "Console statement" "console.log('debug info')" 1
|
||||
|
||||
echo -e "\n${BLUE}Test Case 3: Debug template (should fail)${NC}"
|
||||
run_test "Debug template" "Debug: {{ isMapReady ? 'Map Ready' : 'Map Loading' }}" 1
|
||||
|
||||
echo -e "\n${BLUE}Test Case 4: Debug constant (should fail)${NC}"
|
||||
run_test "Debug constant" "const DEBUG_MODE = true" 1
|
||||
|
||||
echo -e "\n${BLUE}Test Case 5: Mixed content (should fail)${NC}"
|
||||
run_test "Mixed content" "// Some normal code\nconsole.debug('test')\n// More normal code" 1
|
||||
|
||||
echo -e "\n${BLUE}Test Case 6: HTML debug comment (should fail)${NC}"
|
||||
run_test "HTML debug comment" "<!-- debug: this is debug info -->" 1
|
||||
|
||||
echo -e "\n${BLUE}Test Case 7: Debug attribute (should fail)${NC}"
|
||||
run_test "Debug attribute" "<div debug='true'>content</div>" 1
|
||||
|
||||
echo -e "\n${BLUE}Test Case 8: Test file (should be skipped)${NC}"
|
||||
run_test "Test file" "console.log('this should be skipped')" 0
|
||||
|
||||
# Test branch detection
|
||||
echo -e "\n${BLUE}Testing branch detection...${NC}"
|
||||
cd "$TEST_DIR"
|
||||
git init > /dev/null 2>&1
|
||||
git checkout -b feature-branch > /dev/null 2>&1
|
||||
echo "console.log('debug')" > test.vue
|
||||
git add test.vue > /dev/null 2>&1
|
||||
|
||||
if bash ../../.git/hooks/pre-commit > hook_output.txt 2>&1; then
|
||||
echo -e " ${GREEN}✅ PASS${NC} - Hook skipped on feature branch"
|
||||
else
|
||||
echo -e " ${RED}❌ FAIL${NC} - Hook should have been skipped on feature branch"
|
||||
echo -e " ${YELLOW}Hook output:${NC}"
|
||||
cat hook_output.txt
|
||||
fi
|
||||
|
||||
rm -rf .git
|
||||
rm -f hook_output.txt
|
||||
|
||||
echo -e "\n${GREEN}🎉 All tests completed!${NC}"
|
||||
echo -e "\n${BLUE}To test manually:${NC}"
|
||||
echo "1. Make changes to a file with debug code"
|
||||
echo "2. Stage the file: git add <filename>"
|
||||
echo "3. Try to commit: git commit -m 'test'"
|
||||
echo "4. The hook should prevent the commit if debug code is found"
|
||||
@@ -17,34 +17,40 @@ parse_args "$@"
|
||||
print_header "Environment Variable Test"
|
||||
log_info "Testing environment variable handling at $(date)"
|
||||
|
||||
# Test 1: Capacitor environment
|
||||
log_info "Test 1: Setting up Capacitor environment..."
|
||||
setup_build_env "capacitor"
|
||||
# Test 1: Capacitor environment (development)
|
||||
log_info "Test 1: Setting up Capacitor environment (development mode)..."
|
||||
setup_build_env "capacitor" "development"
|
||||
print_env_vars "VITE_"
|
||||
echo ""
|
||||
|
||||
# Test 2: Web environment
|
||||
log_info "Test 2: Setting up Web environment..."
|
||||
setup_build_env "web"
|
||||
# Test 2: Web environment (development)
|
||||
log_info "Test 2: Setting up Web environment (development mode)..."
|
||||
setup_build_env "web" "development"
|
||||
print_env_vars "VITE_"
|
||||
echo ""
|
||||
|
||||
# Test 3: Production Capacitor environment
|
||||
log_info "Test 3: Setting up Production Capacitor environment..."
|
||||
setup_build_env "capacitor" "true"
|
||||
# Test 3: Capacitor test environment
|
||||
log_info "Test 3: Setting up Capacitor environment (test mode)..."
|
||||
setup_build_env "capacitor" "test"
|
||||
print_env_vars "VITE_"
|
||||
echo ""
|
||||
|
||||
# Test 4: Application directories
|
||||
log_info "Test 4: Setting up application directories..."
|
||||
# Test 4: Capacitor production environment
|
||||
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
|
||||
|
||||
# Test 5: Load .env file (if it exists)
|
||||
log_info "Test 5: Loading .env file..."
|
||||
# Test 6: Load .env file (if it exists)
|
||||
log_info "Test 6: Loading .env file..."
|
||||
load_env_file ".env"
|
||||
|
||||
# Test 6: Git hash
|
||||
log_info "Test 6: Getting git hash..."
|
||||
# Test 7: Git hash
|
||||
log_info "Test 7: Getting git hash..."
|
||||
GIT_HASH=$(get_git_hash)
|
||||
log_info "Git hash: $GIT_HASH"
|
||||
|
||||
|
||||
103
scripts/type-safety-check.sh
Executable file
@@ -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'"
|
||||
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">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ notification.text }}</p>
|
||||
<div class="w-full px-4 py-3 overflow-hidden">
|
||||
<h4 class="font-semibold text-ellipsis overflow-hidden">
|
||||
{{ notification.title }}
|
||||
</h4>
|
||||
<p class="text-sm text-ellipsis overflow-hidden">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +50,15 @@
|
||||
></font-awesome>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<div
|
||||
class="relative w-full pl-4 pr-8 py-2 text-slate-900 overflow-hidden"
|
||||
>
|
||||
<h4 class="font-semibold text-ellipsis overflow-hidden">
|
||||
{{ notification.title }}
|
||||
</h4>
|
||||
<p class="text-sm text-ellipsis overflow-hidden">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<div
|
||||
class="relative w-full pl-4 pr-8 py-2 text-emerald-900 overflow-hidden"
|
||||
>
|
||||
<h4 class="font-semibold text-ellipsis overflow-hidden">
|
||||
{{ notification.title }}
|
||||
</h4>
|
||||
<p class="text-sm text-ellipsis overflow-hidden">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<div
|
||||
class="relative w-full pl-4 pr-8 py-2 text-amber-900 overflow-hidden"
|
||||
>
|
||||
<h4 class="font-semibold text-ellipsis overflow-hidden">
|
||||
{{ notification.title }}
|
||||
</h4>
|
||||
<p class="text-sm text-ellipsis overflow-hidden">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<div
|
||||
class="relative w-full pl-4 pr-8 py-2 text-rose-900 overflow-hidden"
|
||||
>
|
||||
<h4 class="font-semibold text-ellipsis overflow-hidden">
|
||||
{{ notification.title }}
|
||||
</h4>
|
||||
<p class="text-sm text-ellipsis overflow-hidden">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
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;
|
||||
|
||||
truncateLongWords(sentence: string) {
|
||||
return sentence
|
||||
.split(" ")
|
||||
.map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
async turnOffNotifications(
|
||||
notification: NotificationIface,
|
||||
): Promise<boolean> {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"warning": {
|
||||
"fillRule": "evenodd",
|
||||
"d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z",
|
||||
"clipRule": "evenodd"
|
||||
},
|
||||
"spinner": {
|
||||
"d": "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
},
|
||||
"chart": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
},
|
||||
"plus": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M12 4v16m8-8H4"
|
||||
},
|
||||
"settings": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
},
|
||||
"settingsDot": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
},
|
||||
"lock": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
},
|
||||
"download": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
},
|
||||
"check": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
},
|
||||
"edit": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
},
|
||||
"trash": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
},
|
||||
"plusCircle": {
|
||||
"strokeLinecap": "round",
|
||||
"strokeLinejoin": "round",
|
||||
"strokeWidth": "2",
|
||||
"d": "M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
},
|
||||
"info": {
|
||||
"fillRule": "evenodd",
|
||||
"d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z",
|
||||
"clipRule": "evenodd"
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@ import { GiveRecordWithContactInfo } from "@/interfaces/give";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { isHiddenDid } from "../libs/endorserServer";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { createNotifyHelpers, NotifyFunction } from "@/utils/notify";
|
||||
import {
|
||||
NOTIFY_PERSON_HIDDEN,
|
||||
NOTIFY_UNKNOWN_PERSON,
|
||||
@@ -273,7 +273,7 @@ export default class ActivityListItem extends Vue {
|
||||
|
||||
isHiddenDid = isHiddenDid;
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
$notify!: (notification: any, timeout?: number) => void;
|
||||
$notify!: NotifyFunction;
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
@@ -288,8 +288,7 @@ export default class ActivityListItem extends Vue {
|
||||
}
|
||||
|
||||
get fetchAmount(): string {
|
||||
const claim =
|
||||
(this.record.fullClaim as any)?.claim || this.record.fullClaim;
|
||||
const claim = this.record.fullClaim;
|
||||
|
||||
const amount = claim.object?.amountOfThisGood
|
||||
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||
@@ -299,8 +298,7 @@ export default class ActivityListItem extends Vue {
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
const claim =
|
||||
(this.record.fullClaim as any)?.claim || this.record.fullClaim;
|
||||
const claim = this.record.fullClaim;
|
||||
|
||||
return `${claim?.description || ""}`;
|
||||
}
|
||||
|
||||
@@ -171,6 +171,8 @@ export default class DataExportSection extends Vue {
|
||||
* @throws {Error} If export fails
|
||||
*/
|
||||
public async exportDatabase(): Promise<void> {
|
||||
// Note that similar code is in ContactsView.vue exportContactData()
|
||||
|
||||
if (this.isExporting) {
|
||||
return; // Prevent multiple simultaneous exports
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ import {
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -119,11 +120,13 @@ export default class FeedFilters extends Vue {
|
||||
isNearby = false;
|
||||
settingChanged = false;
|
||||
visible = false;
|
||||
activeDid = "";
|
||||
|
||||
async open(onCloseIfChanged: () => void) {
|
||||
async open(onCloseIfChanged: () => void, activeDid: string) {
|
||||
this.onCloseIfChanged = onCloseIfChanged;
|
||||
this.activeDid = activeDid;
|
||||
|
||||
const settings = await this.$settings();
|
||||
const settings = await this.$accountSettings(activeDid);
|
||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
||||
this.isNearby = !!settings.filterFeedByNearby;
|
||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||
@@ -137,7 +140,8 @@ export default class FeedFilters extends Vue {
|
||||
async toggleHasVisibleDid() {
|
||||
this.settingChanged = true;
|
||||
this.hasVisibleDid = !this.hasVisibleDid;
|
||||
await this.$updateSettings({
|
||||
|
||||
await this.$saveMySettings({
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
}
|
||||
@@ -145,9 +149,18 @@ export default class FeedFilters extends Vue {
|
||||
async toggleNearby() {
|
||||
this.settingChanged = true;
|
||||
this.isNearby = !this.isNearby;
|
||||
await this.$updateSettings({
|
||||
|
||||
logger.debug("[FeedFilters] 🔄 Toggling nearby filter:", {
|
||||
newValue: this.isNearby,
|
||||
settingChanged: this.settingChanged,
|
||||
activeDid: this.activeDid,
|
||||
});
|
||||
|
||||
await this.$saveMySettings({
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
|
||||
logger.debug("[FeedFilters] ✅ Nearby filter updated in settings");
|
||||
}
|
||||
|
||||
async clearAll() {
|
||||
@@ -155,7 +168,7 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
await this.$updateSettings({
|
||||
await this.$saveMySettings({
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
@@ -169,7 +182,7 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
await this.$updateSettings({
|
||||
await this.$saveMySettings({
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
@@ -179,13 +192,20 @@ export default class FeedFilters extends Vue {
|
||||
}
|
||||
|
||||
close() {
|
||||
logger.debug("[FeedFilters] 🚪 Closing dialog:", {
|
||||
settingChanged: this.settingChanged,
|
||||
hasCallback: !!this.onCloseIfChanged,
|
||||
});
|
||||
|
||||
if (this.settingChanged) {
|
||||
logger.debug("[FeedFilters] 🔄 Settings changed, calling callback");
|
||||
this.onCloseIfChanged();
|
||||
}
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
done() {
|
||||
logger.debug("[FeedFilters] ✅ Done button clicked");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ import EntitySelectionStep from "../components/EntitySelectionStep.vue";
|
||||
import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
||||
import {
|
||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
||||
@@ -98,7 +98,7 @@ import {
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class GiftedDialog extends Vue {
|
||||
$notify!: (notification: any, timeout?: number) => void;
|
||||
$notify!: NotifyFunction;
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
/**
|
||||
@@ -622,7 +622,10 @@ export default class GiftedDialog extends Vue {
|
||||
* Handle edit entity request from GiftDetailsStep
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
v-if="iconData"
|
||||
:class="svgClass"
|
||||
:fill="fill"
|
||||
:stroke="stroke"
|
||||
:viewBox="viewBox"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path v-for="(path, index) in iconData.paths" :key="index" v-bind="path" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import icons from "../assets/icons.json";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Icon path interface
|
||||
*/
|
||||
interface IconPath {
|
||||
d: string;
|
||||
fillRule?: string;
|
||||
clipRule?: string;
|
||||
strokeLinecap?: string;
|
||||
strokeLinejoin?: string;
|
||||
strokeWidth?: string | number;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon data interface
|
||||
*/
|
||||
interface IconData {
|
||||
paths: IconPath[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Icons JSON structure
|
||||
*/
|
||||
interface IconsJson {
|
||||
[key: string]: IconPath | IconData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon Renderer Component
|
||||
*
|
||||
* This component loads SVG icon definitions from a JSON file and renders them
|
||||
* as SVG elements. It provides a clean way to use icons without cluttering
|
||||
* templates with long SVG path definitions.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
* @since 2024
|
||||
*/
|
||||
@Component({
|
||||
name: "IconRenderer",
|
||||
})
|
||||
export default class IconRenderer extends Vue {
|
||||
@Prop({ required: true }) readonly iconName!: string;
|
||||
@Prop({ default: "h-5 w-5" }) readonly svgClass!: string;
|
||||
@Prop({ default: "none" }) readonly fill!: string;
|
||||
@Prop({ default: "currentColor" }) readonly stroke!: string;
|
||||
@Prop({ default: "0 0 24 24" }) readonly viewBox!: string;
|
||||
|
||||
/**
|
||||
* Get the icon data for the specified icon name
|
||||
*
|
||||
* @returns {IconData | null} The icon data object or null if not found
|
||||
*/
|
||||
get iconData(): IconData | null {
|
||||
const icon = (icons as IconsJson)[this.iconName];
|
||||
if (!icon) {
|
||||
logger.warn(`Icon "${this.iconName}" not found in icons.json`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert single path to array format for consistency
|
||||
if ("d" in icon) {
|
||||
return {
|
||||
paths: [icon as IconPath],
|
||||
};
|
||||
}
|
||||
|
||||
return icon as IconData;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -282,7 +282,7 @@ import {
|
||||
NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT,
|
||||
createImageDialogCameraErrorMessage,
|
||||
} from "../constants/notifications";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
|
||||
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "../utils/notify";
|
||||
|
||||
const inputImageFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -291,7 +291,7 @@ const inputImageFileNameRef = ref<Blob>();
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class ImageMethodDialog extends Vue {
|
||||
$notify!: (notification: any, timeout?: number) => void;
|
||||
$notify!: NotifyFunction;
|
||||
$router!: Router;
|
||||
notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ export default class OnboardingDialog extends Vue {
|
||||
this.visible = true;
|
||||
if (this.page === OnboardPage.Create) {
|
||||
// we'll assume that they've been through all the other pages
|
||||
await this.$updateSettings({
|
||||
await this.$saveMySettings({
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
}
|
||||
@@ -297,7 +297,7 @@ export default class OnboardingDialog extends Vue {
|
||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||
this.visible = false;
|
||||
if (done) {
|
||||
await this.$updateSettings({
|
||||
await this.$saveMySettings({
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (goHome) {
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { EndorserRateLimits, ImageRateLimits } from "@/interfaces/limits";
|
||||
|
||||
@Component({
|
||||
name: "UsageLimitsSection",
|
||||
@@ -94,8 +95,8 @@ export default class UsageLimitsSection extends Vue {
|
||||
@Prop({ required: true }) loadingLimits!: boolean;
|
||||
@Prop({ required: true }) limitsMessage!: string;
|
||||
@Prop({ required: false }) activeDid?: string;
|
||||
@Prop({ required: false }) endorserLimits?: any;
|
||||
@Prop({ required: false }) imageLimits?: any;
|
||||
@Prop({ required: false }) endorserLimits?: EndorserRateLimits;
|
||||
@Prop({ required: false }) imageLimits?: ImageRateLimits;
|
||||
@Prop({ required: true }) onRecheckLimits!: () => void;
|
||||
|
||||
mounted() {
|
||||
|
||||
@@ -95,7 +95,7 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async onClickSaveChanges() {
|
||||
try {
|
||||
await this.$updateSettings({ firstName: this.givenName });
|
||||
await this.$saveMySettings({ firstName: this.givenName });
|
||||
this.visible = false;
|
||||
this.callback(this.givenName);
|
||||
} catch (error) {
|
||||
|
||||
@@ -846,6 +846,12 @@ export const NOTIFY_CONTACTS_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)
|
||||
export const NOTIFY_CONTACT_INFO_COPY = {
|
||||
title: "Info",
|
||||
@@ -1588,7 +1594,7 @@ export function createImageDialogCameraErrorMessage(error: Error): string {
|
||||
|
||||
// Helper function for dynamic upload error messages
|
||||
// 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)) {
|
||||
const status = error.response?.status;
|
||||
const data = error.response?.data;
|
||||
|
||||
@@ -60,9 +60,13 @@ export interface AxiosErrorResponse {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
config?: unknown;
|
||||
};
|
||||
config?: unknown;
|
||||
config?: {
|
||||
url?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -98,3 +102,81 @@ export interface VerifiableCredentialClaim {
|
||||
credentialSubject: ClaimObject;
|
||||
[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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
faCameraRotate,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faChartLine,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCirclePlus,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faCircleUser,
|
||||
@@ -49,6 +51,7 @@ import {
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGear,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
@@ -58,6 +61,7 @@ import {
|
||||
faHouseChimney,
|
||||
faImage,
|
||||
faImagePortrait,
|
||||
faInfo,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
@@ -72,8 +76,8 @@ import {
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faQrcode,
|
||||
faQuestion,
|
||||
faRightFromBracket,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
@@ -106,6 +110,7 @@ library.add(
|
||||
faCameraRotate,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faChartLine,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
@@ -114,6 +119,7 @@ library.add(
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCirclePlus,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faCircleUser,
|
||||
@@ -135,6 +141,7 @@ library.add(
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGear,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
@@ -144,6 +151,7 @@ library.add(
|
||||
faHouseChimney,
|
||||
faImage,
|
||||
faImagePortrait,
|
||||
faInfo,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getHeaders, errorStringForLog } from "@/libs/endorserServer";
|
||||
import { handleApiError } from "./api";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { AxiosErrorResponse } from "@/interfaces/common";
|
||||
|
||||
/**
|
||||
* Profile data interface
|
||||
@@ -129,12 +130,45 @@ export class ProfileService {
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
if (response.status === 204 || response.status === 200) {
|
||||
logger.info("Profile deleted successfully");
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_DELETED);
|
||||
throw new Error(
|
||||
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isApiError(error) && error.response) {
|
||||
const response = error.response;
|
||||
logger.error("API error deleting profile:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
url: this.getErrorUrl(error),
|
||||
});
|
||||
|
||||
// Handle specific HTTP status codes
|
||||
if (response.status === 204) {
|
||||
logger.debug("Profile deleted successfully (204 No Content)");
|
||||
return true; // 204 is success for DELETE operations
|
||||
} else if (response.status === 404) {
|
||||
logger.warn("Profile not found - may already be deleted");
|
||||
return true; // Consider this a success if profile doesn't exist
|
||||
} else if (response.status === 400) {
|
||||
logger.error("Bad request when deleting profile:", response.data);
|
||||
throw new Error(
|
||||
`Profile deletion failed: ${response.data?.error?.message || "Bad request"}`,
|
||||
);
|
||||
} else if (response.status === 401) {
|
||||
logger.error("Unauthorized to delete profile");
|
||||
throw new Error("You are not authorized to delete this profile");
|
||||
} else if (response.status === 403) {
|
||||
logger.error("Forbidden to delete profile");
|
||||
throw new Error("You are not allowed to delete this profile");
|
||||
}
|
||||
}
|
||||
|
||||
logger.error("Error deleting profile:", errorStringForLog(error));
|
||||
handleApiError(error as AxiosError, "/api/partner/userProfile");
|
||||
return false;
|
||||
@@ -204,13 +238,22 @@ export class ProfileService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for API errors
|
||||
* Type guard for API errors with proper typing
|
||||
*/
|
||||
private isApiError(
|
||||
error: unknown,
|
||||
): error is { response?: { status?: number } } {
|
||||
private isApiError(error: unknown): error is AxiosErrorResponse {
|
||||
return typeof error === "object" && error !== null && "response" in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error URL safely from error object
|
||||
*/
|
||||
private getErrorUrl(error: unknown): string | undefined {
|
||||
if (this.isApiError(error) && error.config) {
|
||||
const config = error.config as { url?: string };
|
||||
return config.url;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
99
src/services/QRNavigationService.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
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);
|
||||
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();
|
||||
await platformService.updateDidSpecificSettings(identity0.did, {
|
||||
isRegistered: true,
|
||||
@@ -35,7 +40,8 @@ export async function testBecomeUser0() {
|
||||
* @throws Error if registration fails or database access fails
|
||||
*/
|
||||
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);
|
||||
|
||||
|
||||
@@ -949,7 +949,7 @@ export const PlatformServiceMixin = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Save settings for current active user - $saveMySettings()
|
||||
* Save current user's settings - $saveMySettings()
|
||||
* Ultra-concise shortcut using activeDid from component
|
||||
* @param changes Settings changes to save
|
||||
* @returns Promise<boolean> Success status
|
||||
@@ -1022,7 +1022,7 @@ export const PlatformServiceMixin = {
|
||||
if (!record) {
|
||||
return [];
|
||||
}
|
||||
return this.$mapColumnsToValues(record.columns, record.values) as Array<
|
||||
return this._mapColumnsToValues(record.columns, record.values) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
},
|
||||
@@ -1293,41 +1293,6 @@ export const PlatformServiceMixin = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update settings with direct SQL - $updateSettings()
|
||||
* Eliminates verbose settings update patterns
|
||||
* @param changes Settings changes to apply
|
||||
* @param did Optional DID for user-specific settings
|
||||
* @returns Promise<boolean> Success status
|
||||
*/
|
||||
/**
|
||||
* Update settings - $updateSettings()
|
||||
* Ultra-concise shortcut for updating settings (default or user-specific)
|
||||
*
|
||||
* ⚠️ DEPRECATED: This method will be removed in favor of $saveSettings()
|
||||
* Use $saveSettings(changes, did?) instead for better consistency
|
||||
*
|
||||
* @param changes Settings changes to save
|
||||
* @param did Optional DID for user-specific settings
|
||||
* @returns Promise<boolean> Success status
|
||||
*/
|
||||
async $updateSettings(
|
||||
changes: Partial<Settings>,
|
||||
did?: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Use self-contained methods which handle the correct schema
|
||||
if (did) {
|
||||
return await this.$saveUserSettings(did, changes);
|
||||
} else {
|
||||
return await this.$saveSettings(changes);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[PlatformServiceMixin] Error updating settings:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get settings row as array - $getSettingsRow()
|
||||
* Eliminates verbose settings retrieval patterns
|
||||
@@ -1617,7 +1582,33 @@ export const PlatformServiceMixin = {
|
||||
* Enhanced interface with caching utility methods
|
||||
*/
|
||||
export interface IPlatformServiceMixin {
|
||||
// Core platform service access
|
||||
platformService: PlatformService;
|
||||
isCapacitor: boolean;
|
||||
isWeb: boolean;
|
||||
isElectron: boolean;
|
||||
capabilities: PlatformCapabilities;
|
||||
|
||||
// ActiveDid tracking
|
||||
currentActiveDid: string | null;
|
||||
$updateActiveDid(newDid: string | null): Promise<void>;
|
||||
|
||||
// Ultra-concise database methods (shortest possible names)
|
||||
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
|
||||
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
||||
$one(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
||||
|
||||
// Query + mapping combo methods
|
||||
$query<T = Record<string, unknown>>(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<T[]>;
|
||||
$first<T = Record<string, unknown>>(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<T | null>;
|
||||
|
||||
// Enhanced utility methods
|
||||
$dbQuery(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
@@ -1634,12 +1625,24 @@ export interface IPlatformServiceMixin {
|
||||
defaultFallback?: Settings,
|
||||
): Promise<Settings>;
|
||||
$withTransaction<T>(callback: () => Promise<T>): Promise<T>;
|
||||
isCapacitor: boolean;
|
||||
isWeb: boolean;
|
||||
isElectron: boolean;
|
||||
capabilities: PlatformCapabilities;
|
||||
|
||||
// High-level entity operations
|
||||
// Specialized shortcuts - contacts and settings always fresh (no caching)
|
||||
$contacts(): Promise<Contact[]>;
|
||||
$contactCount(): Promise<number>;
|
||||
$settings(defaults?: Settings): Promise<Settings>;
|
||||
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
||||
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
|
||||
|
||||
// Settings update shortcuts (eliminate 90% boilerplate)
|
||||
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
|
||||
$saveUserSettings(did: string, changes: Partial<Settings>): Promise<boolean>;
|
||||
$saveMySettings(changes: Partial<Settings>): Promise<boolean>;
|
||||
|
||||
// Cache management methods
|
||||
$refreshSettings(): Promise<Settings>;
|
||||
$refreshContacts(): Promise<Contact[]>;
|
||||
|
||||
// High-level entity operations (eliminate verbose SQL patterns)
|
||||
$mapResults<T>(
|
||||
results: QueryExecResult | undefined,
|
||||
mapper: (row: unknown[]) => T,
|
||||
@@ -1649,7 +1652,6 @@ export interface IPlatformServiceMixin {
|
||||
$getAllContacts(): Promise<Contact[]>;
|
||||
$getContact(did: string): Promise<Contact | null>;
|
||||
$deleteContact(did: string): Promise<boolean>;
|
||||
$contactCount(): Promise<number>;
|
||||
$getAllAccounts(): Promise<Account[]>;
|
||||
$getAllAccountDids(): Promise<string[]>;
|
||||
$insertEntity(
|
||||
@@ -1657,7 +1659,6 @@ export interface IPlatformServiceMixin {
|
||||
entity: Record<string, unknown>,
|
||||
fields: string[],
|
||||
): Promise<boolean>;
|
||||
$updateSettings(changes: Partial<Settings>, did?: string): Promise<boolean>;
|
||||
$getSettingsRow(
|
||||
fields: string[],
|
||||
did?: string,
|
||||
@@ -1712,141 +1713,8 @@ export interface IPlatformServiceMixin {
|
||||
|
||||
// TypeScript declaration merging to eliminate (this as any) type assertions
|
||||
declare module "@vue/runtime-core" {
|
||||
interface ComponentCustomProperties {
|
||||
// Core platform service access
|
||||
platformService: PlatformService;
|
||||
isCapacitor: boolean;
|
||||
isWeb: boolean;
|
||||
isElectron: boolean;
|
||||
capabilities: PlatformCapabilities;
|
||||
|
||||
// ActiveDid tracking
|
||||
currentActiveDid: string | null;
|
||||
$updateActiveDid(newDid: string | null): Promise<void>;
|
||||
|
||||
// Ultra-concise database methods (shortest possible names)
|
||||
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
|
||||
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
||||
$one(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
||||
|
||||
// Query + mapping combo methods
|
||||
$query<T = Record<string, unknown>>(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<T[]>;
|
||||
$first<T = Record<string, unknown>>(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<T | null>;
|
||||
|
||||
// Enhanced utility methods
|
||||
$dbQuery(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<QueryExecResult | undefined>;
|
||||
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
||||
$dbGetOneRow(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<unknown[] | undefined>;
|
||||
$getSettings(
|
||||
key: string,
|
||||
defaults?: Settings | null,
|
||||
): Promise<Settings | null>;
|
||||
$getMergedSettings(
|
||||
key: string,
|
||||
did?: string,
|
||||
defaults?: Settings,
|
||||
): Promise<Settings>;
|
||||
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||
|
||||
// Specialized shortcuts - contacts cached, settings fresh
|
||||
$contacts(): Promise<Contact[]>;
|
||||
$contactCount(): Promise<number>;
|
||||
$settings(defaults?: Settings): Promise<Settings>;
|
||||
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
||||
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
|
||||
|
||||
// Settings update shortcuts (eliminate 90% boilerplate)
|
||||
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
|
||||
$saveUserSettings(
|
||||
did: string,
|
||||
changes: Partial<Settings>,
|
||||
): Promise<boolean>;
|
||||
$saveMySettings(changes: Partial<Settings>): Promise<boolean>;
|
||||
|
||||
// Cache management methods
|
||||
$refreshSettings(): Promise<Settings>;
|
||||
$refreshContacts(): Promise<Contact[]>;
|
||||
// $clearAllCaches(): void;
|
||||
|
||||
// High-level entity operations (eliminate verbose SQL patterns)
|
||||
$mapResults<T>(
|
||||
results: QueryExecResult | undefined,
|
||||
mapper: (row: unknown[]) => T,
|
||||
): T[];
|
||||
$insertContact(contact: Partial<Contact>): Promise<boolean>;
|
||||
$updateContact(did: string, changes: Partial<Contact>): Promise<boolean>;
|
||||
$getAllContacts(): Promise<Contact[]>;
|
||||
$getContact(did: string): Promise<Contact | null>;
|
||||
$deleteContact(did: string): Promise<boolean>;
|
||||
$getAllAccounts(): Promise<Account[]>;
|
||||
$getAllAccountDids(): Promise<string[]>;
|
||||
$insertEntity(
|
||||
tableName: string,
|
||||
entity: Record<string, unknown>,
|
||||
fields: string[],
|
||||
): Promise<boolean>;
|
||||
$updateSettings(changes: Partial<Settings>, did?: string): Promise<boolean>;
|
||||
$getSettingsRow(
|
||||
fields: string[],
|
||||
did?: string,
|
||||
): Promise<unknown[] | undefined>;
|
||||
$updateEntity(
|
||||
tableName: string,
|
||||
entity: Record<string, unknown>,
|
||||
whereClause: string,
|
||||
whereParams: unknown[],
|
||||
): Promise<boolean>;
|
||||
$insertUserSettings(
|
||||
did: string,
|
||||
settings: Partial<Settings>,
|
||||
): Promise<boolean>;
|
||||
$getTemp(id: string): Promise<Temp | null>;
|
||||
$deleteTemp(id: string): Promise<boolean>;
|
||||
|
||||
// Logging methods
|
||||
$log(message: string, level?: string): Promise<void>;
|
||||
$logError(message: string): Promise<void>;
|
||||
$logAndConsole(message: string, isError?: boolean): Promise<void>;
|
||||
|
||||
// Memory logs access
|
||||
$memoryLogs: string[];
|
||||
|
||||
// New additions
|
||||
$logs(): Promise<Array<Record<string, unknown>>>;
|
||||
|
||||
// New additions
|
||||
$generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] };
|
||||
$generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
whereClause: string,
|
||||
whereParams?: unknown[],
|
||||
): { sql: string; params: unknown[] };
|
||||
$mapQueryResultToValues(
|
||||
record: QueryExecResult | undefined,
|
||||
): Array<Record<string, unknown>>;
|
||||
$mapColumnsToValues(
|
||||
columns: string[],
|
||||
values: unknown[][],
|
||||
): Array<Record<string, unknown>>;
|
||||
|
||||
// Debug methods
|
||||
$debugDidSettings(did: string): Promise<Settings | null>;
|
||||
$debugMergedSettings(did: string): Promise<void>;
|
||||
interface ComponentCustomProperties extends IPlatformServiceMixin {
|
||||
// All methods inherited from IPlatformServiceMixin
|
||||
// No additional methods needed - single source of truth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* Enhanced logger with self-contained database logging
|
||||
*
|
||||
* Provides comprehensive logging with console and database output.
|
||||
* Supports configurable log levels via VITE_LOG_LEVEL environment variable.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 2.0.0
|
||||
* @version 2.1.0
|
||||
* @since 2025-01-25
|
||||
*/
|
||||
|
||||
@@ -46,6 +47,42 @@ export function safeStringify(obj: unknown) {
|
||||
const isElectron = process.env.VITE_PLATFORM === "electron";
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
// Log level configuration via environment variable
|
||||
const LOG_LEVELS = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
debug: 3,
|
||||
} as const;
|
||||
|
||||
type LogLevel = keyof typeof LOG_LEVELS;
|
||||
|
||||
// Parse VITE_LOG_LEVEL environment variable
|
||||
const getLogLevel = (): LogLevel => {
|
||||
const envLogLevel = process.env.VITE_LOG_LEVEL?.toLowerCase();
|
||||
|
||||
if (envLogLevel && envLogLevel in LOG_LEVELS) {
|
||||
return envLogLevel as LogLevel;
|
||||
}
|
||||
|
||||
// Default log levels based on environment
|
||||
if (isProduction && !isElectron) {
|
||||
return "warn"; // Production web: warnings and errors only
|
||||
} else if (isElectron) {
|
||||
return "error"; // Electron: errors only
|
||||
} else {
|
||||
return "info"; // Development/Capacitor: info and above
|
||||
}
|
||||
};
|
||||
|
||||
const currentLogLevel = getLogLevel();
|
||||
const currentLevelValue = LOG_LEVELS[currentLogLevel];
|
||||
|
||||
// Helper function to check if a log level should be displayed
|
||||
const shouldLog = (level: LogLevel): boolean => {
|
||||
return LOG_LEVELS[level] <= currentLevelValue;
|
||||
};
|
||||
|
||||
// Track initialization state to prevent circular dependencies
|
||||
let isInitializing = true;
|
||||
|
||||
@@ -105,11 +142,11 @@ async function logToDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced logger with self-contained database methods
|
||||
// Enhanced logger with self-contained database methods and log level control
|
||||
export const logger = {
|
||||
debug: (message: string, ...args: unknown[]) => {
|
||||
// Debug logs are very verbose - only show in development mode for web
|
||||
if (!isProduction && !isElectron) {
|
||||
// Debug logs only show if VITE_LOG_LEVEL allows it
|
||||
if (shouldLog("debug")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(message, ...args);
|
||||
}
|
||||
@@ -117,11 +154,8 @@ export const logger = {
|
||||
},
|
||||
|
||||
log: (message: string, ...args: unknown[]) => {
|
||||
// Regular logs - show in development or for capacitor, but quiet for Electron
|
||||
if (
|
||||
(!isProduction && !isElectron) ||
|
||||
process.env.VITE_PLATFORM === "capacitor"
|
||||
) {
|
||||
// Regular logs - show if VITE_LOG_LEVEL allows info level
|
||||
if (shouldLog("info")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message, ...args);
|
||||
}
|
||||
@@ -132,11 +166,7 @@ export const logger = {
|
||||
},
|
||||
|
||||
info: (message: string, ...args: unknown[]) => {
|
||||
if (
|
||||
process.env.NODE_ENV !== "production" ||
|
||||
process.env.VITE_PLATFORM === "capacitor" ||
|
||||
process.env.VITE_PLATFORM === "electron"
|
||||
) {
|
||||
if (shouldLog("info")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(message, ...args);
|
||||
}
|
||||
@@ -147,8 +177,7 @@ export const logger = {
|
||||
},
|
||||
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
// Always show warnings, but for Electron, suppress routine database warnings
|
||||
if (!isElectron || !message.includes("[CapacitorPlatformService]")) {
|
||||
if (shouldLog("warn")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message, ...args);
|
||||
}
|
||||
@@ -159,9 +188,10 @@ export const logger = {
|
||||
},
|
||||
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
// Errors will always be logged to console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message, ...args);
|
||||
if (shouldLog("error")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message, ...args);
|
||||
}
|
||||
|
||||
// Database logging
|
||||
const messageString = safeStringify(message);
|
||||
@@ -175,11 +205,11 @@ export const logger = {
|
||||
},
|
||||
|
||||
toConsoleAndDb: async (message: string, isError = false): Promise<void> => {
|
||||
// Console output
|
||||
if (isError) {
|
||||
// Console output based on log level
|
||||
if (isError && shouldLog("error")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
} else {
|
||||
} else if (!isError && shouldLog("info")) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
@@ -194,6 +224,12 @@ export const logger = {
|
||||
error: (message: string) =>
|
||||
logToDatabase(`[${componentName}] ${message}`, "error"),
|
||||
}),
|
||||
|
||||
// Log level information methods
|
||||
getCurrentLevel: (): LogLevel => currentLogLevel,
|
||||
getCurrentLevelValue: (): number => currentLevelValue,
|
||||
isLevelEnabled: (level: LogLevel): boolean => shouldLog(level),
|
||||
getAvailableLevels: (): LogLevel[] => Object.keys(LOG_LEVELS) as LogLevel[],
|
||||
};
|
||||
|
||||
// Function to manually mark initialization as complete
|
||||
|
||||
@@ -174,11 +174,12 @@
|
||||
:aria-busy="loadingProfile || savingProfile"
|
||||
></textarea>
|
||||
|
||||
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
|
||||
<div class="flex items-center mb-4">
|
||||
<input
|
||||
v-model="includeUserProfileLocation"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="onLocationCheckboxChange"
|
||||
/>
|
||||
<label for="includeUserProfileLocation">Include Location</label>
|
||||
</div>
|
||||
@@ -194,6 +195,7 @@
|
||||
class="!z-40 rounded-md"
|
||||
@click="onProfileMapClick"
|
||||
@ready="onMapReady"
|
||||
@mounted="onMapMounted"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
@@ -751,6 +753,7 @@ import "dexie-export-import";
|
||||
// @ts-expect-error - they aren't exporting it but it's there
|
||||
import { ImportProgress } from "dexie-export-import";
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
import * as L from "leaflet";
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { ref } from "vue";
|
||||
@@ -902,6 +905,7 @@ export default class AccountViewView extends Vue {
|
||||
warnIfProdServer: boolean = false;
|
||||
warnIfTestServer: boolean = false;
|
||||
zoom: number = 2;
|
||||
isMapReady: boolean = false;
|
||||
|
||||
// Limits and validation properties
|
||||
endorserLimits: EndorserRateLimits | null = null;
|
||||
@@ -913,6 +917,23 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
// Fix Leaflet icon issues in modern bundlers
|
||||
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
|
||||
if (L.Icon.Default) {
|
||||
// Type-safe way to handle Leaflet icon prototype
|
||||
const iconDefault = L.Icon.Default.prototype as Record<string, unknown>;
|
||||
if ("_getIconUrl" in iconDefault) {
|
||||
delete iconDefault._getIconUrl;
|
||||
}
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl:
|
||||
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png",
|
||||
iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png",
|
||||
shadowUrl:
|
||||
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -939,10 +960,16 @@ export default class AccountViewView extends Vue {
|
||||
this.userProfileLatitude = profile.latitude;
|
||||
this.userProfileLongitude = profile.longitude;
|
||||
this.includeUserProfileLocation = profile.includeLocation;
|
||||
|
||||
// Initialize map ready state if location is included
|
||||
if (profile.includeLocation) {
|
||||
this.isMapReady = false; // Will be set to true when map is ready
|
||||
}
|
||||
} else {
|
||||
// Profile not created yet; leave defaults
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error loading profile:", error);
|
||||
this.notify.error(
|
||||
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE,
|
||||
);
|
||||
@@ -1518,9 +1545,51 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
onMapReady(map: L.Map): void {
|
||||
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
||||
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
|
||||
map.setView([this.userProfileLatitude, this.userProfileLongitude], zoom);
|
||||
try {
|
||||
logger.debug("Map ready event fired, map object:", map);
|
||||
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
||||
const zoom =
|
||||
this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
|
||||
const lat = this.userProfileLatitude || 0;
|
||||
const lng = this.userProfileLongitude || 0;
|
||||
map.setView([lat, lng], zoom);
|
||||
this.isMapReady = true;
|
||||
logger.debug(
|
||||
"Map ready state set to true, coordinates:",
|
||||
[lat, lng],
|
||||
"zoom:",
|
||||
zoom,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error in onMapReady:", error);
|
||||
this.isMapReady = true; // Set to true even on error to prevent infinite loading
|
||||
}
|
||||
}
|
||||
|
||||
onMapMounted(): void {
|
||||
logger.debug("Map component mounted");
|
||||
// Check if map ref is available
|
||||
const mapRef = this.$refs.profileMap;
|
||||
logger.debug("Map ref:", mapRef);
|
||||
|
||||
// Try to set map ready after component is mounted
|
||||
setTimeout(() => {
|
||||
this.isMapReady = true;
|
||||
logger.debug("Map ready set to true after mounted");
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Fallback method to handle map initialization failures
|
||||
private handleMapInitFailure(): void {
|
||||
logger.debug("Starting map initialization timeout (5 seconds)");
|
||||
setTimeout(() => {
|
||||
if (!this.isMapReady) {
|
||||
logger.warn("Map failed to initialize, forcing ready state");
|
||||
this.isMapReady = true;
|
||||
} else {
|
||||
logger.debug("Map initialized successfully, timeout not needed");
|
||||
}
|
||||
}, 5000); // 5 second timeout
|
||||
}
|
||||
|
||||
showProfileInfo(): void {
|
||||
@@ -1532,13 +1601,16 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async saveProfile(): Promise<void> {
|
||||
this.savingProfile = true;
|
||||
const profileData: ProfileData = {
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
};
|
||||
try {
|
||||
const profileData: ProfileData = {
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
};
|
||||
|
||||
logger.debug("Saving profile data:", profileData);
|
||||
|
||||
const success = await this.profileService.saveProfile(
|
||||
this.activeDid,
|
||||
profileData,
|
||||
@@ -1549,6 +1621,7 @@ export default class AccountViewView extends Vue {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error saving profile:", error);
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
||||
} finally {
|
||||
this.savingProfile = false;
|
||||
@@ -1556,15 +1629,25 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
toggleUserProfileLocation(): void {
|
||||
const updated = this.profileService.toggleProfileLocation({
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
});
|
||||
this.userProfileLatitude = updated.latitude;
|
||||
this.userProfileLongitude = updated.longitude;
|
||||
this.includeUserProfileLocation = updated.includeLocation;
|
||||
try {
|
||||
const updated = this.profileService.toggleProfileLocation({
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
});
|
||||
this.userProfileLatitude = updated.latitude;
|
||||
this.userProfileLongitude = updated.longitude;
|
||||
this.includeUserProfileLocation = updated.includeLocation;
|
||||
|
||||
// Reset map ready state when toggling location
|
||||
if (!updated.includeLocation) {
|
||||
this.isMapReady = false;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error in toggleUserProfileLocation:", error);
|
||||
this.notify.error("Failed to toggle location setting");
|
||||
}
|
||||
}
|
||||
|
||||
confirmEraseLatLong(): void {
|
||||
@@ -1592,6 +1675,7 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async deleteProfile(): Promise<void> {
|
||||
try {
|
||||
logger.debug("Attempting to delete profile for DID:", this.activeDid);
|
||||
const success = await this.profileService.deleteProfile(this.activeDid);
|
||||
if (success) {
|
||||
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
|
||||
@@ -1599,11 +1683,20 @@ export default class AccountViewView extends Vue {
|
||||
this.userProfileLatitude = 0;
|
||||
this.userProfileLongitude = 0;
|
||||
this.includeUserProfileLocation = false;
|
||||
this.isMapReady = false; // Reset map state
|
||||
logger.debug("Profile deleted successfully, UI state reset");
|
||||
} else {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
|
||||
}
|
||||
} catch (error) {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
|
||||
logger.error("Error in deleteProfile component method:", error);
|
||||
|
||||
// Show more specific error message if available
|
||||
if (error instanceof Error) {
|
||||
this.notify.error(error.message);
|
||||
} else {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1616,8 +1709,46 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
onProfileMapClick(event: LeafletMouseEvent) {
|
||||
this.userProfileLatitude = event.latlng.lat;
|
||||
this.userProfileLongitude = event.latlng.lng;
|
||||
try {
|
||||
if (event && event.latlng) {
|
||||
this.userProfileLatitude = event.latlng.lat;
|
||||
this.userProfileLongitude = event.latlng.lng;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error in onProfileMapClick:", error);
|
||||
}
|
||||
}
|
||||
|
||||
onLocationCheckboxChange(): void {
|
||||
try {
|
||||
logger.debug(
|
||||
"Location checkbox changed, new value:",
|
||||
this.includeUserProfileLocation,
|
||||
);
|
||||
if (!this.includeUserProfileLocation) {
|
||||
// Location checkbox was unchecked, clean up map state
|
||||
this.isMapReady = false;
|
||||
this.userProfileLatitude = 0;
|
||||
this.userProfileLongitude = 0;
|
||||
logger.debug("Location unchecked, map state reset");
|
||||
} else {
|
||||
// Location checkbox was checked, start map initialization timeout
|
||||
this.isMapReady = false;
|
||||
logger.debug("Location checked, starting map initialization timeout");
|
||||
|
||||
// Try to set map ready after a short delay to allow Vue to render
|
||||
setTimeout(() => {
|
||||
if (!this.isMapReady) {
|
||||
logger.debug("Setting map ready after timeout");
|
||||
this.isMapReady = true;
|
||||
}
|
||||
}, 1000); // 1 second delay
|
||||
|
||||
this.handleMapInitFailure();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error in onLocationCheckboxChange:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// IdentitySection event handlers
|
||||
|
||||
@@ -724,6 +724,8 @@ export default class ClaimView extends Vue {
|
||||
}
|
||||
|
||||
async created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
@@ -754,8 +756,6 @@ export default class ClaimView extends Vue {
|
||||
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
|
||||
|
||||
this.canShare = !!navigator.share;
|
||||
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
}
|
||||
|
||||
// insert a space before any capital letters except the initial letter
|
||||
|
||||
@@ -143,7 +143,7 @@ import {
|
||||
QR_TIMEOUT_STANDARD,
|
||||
QR_TIMEOUT_LONG,
|
||||
} from "@/constants/notifications";
|
||||
import { createNotifyHelpers } from "../utils/notify";
|
||||
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
|
||||
|
||||
interface QRScanResult {
|
||||
rawValue?: string;
|
||||
@@ -191,7 +191,7 @@ interface IUserNameDialog {
|
||||
* @since 2024
|
||||
*/
|
||||
export default class ContactQRScanFull extends Vue {
|
||||
$notify!: (notification: any, timeout?: number) => void;
|
||||
$notify!: NotifyFunction;
|
||||
$router!: Router;
|
||||
|
||||
// Notification helper system
|
||||
|
||||
@@ -714,8 +714,16 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
// Add new contact
|
||||
// @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(
|
||||
(this as any)._parseJsonField(contact.contactMethods, []),
|
||||
(
|
||||
this as {
|
||||
_parseJsonField: (
|
||||
value: unknown,
|
||||
defaultValue: unknown[],
|
||||
) => unknown[];
|
||||
}
|
||||
)._parseJsonField(contact.contactMethods, []),
|
||||
);
|
||||
await this.$insertContact(contact);
|
||||
|
||||
@@ -742,7 +750,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
{
|
||||
onCancel: async (stopAsking?: boolean) => {
|
||||
if (stopAsking) {
|
||||
await this.$updateSettings({
|
||||
await this.$saveMySettings({
|
||||
hideRegisterPromptOnNewContact: stopAsking,
|
||||
});
|
||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||
@@ -750,7 +758,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
},
|
||||
onNo: async (stopAsking?: boolean) => {
|
||||
if (stopAsking) {
|
||||
await this.$updateSettings({
|
||||
await this.$saveMySettings({
|
||||
hideRegisterPromptOnNewContact: stopAsking,
|
||||
});
|
||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||
|
||||
@@ -131,7 +131,7 @@ import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
// Capacitor import removed - using PlatformService instead
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
@@ -165,13 +165,18 @@ import { GiveSummaryRecord } from "@/interfaces/records";
|
||||
import { UserInfo } from "@/interfaces/common";
|
||||
import { VerifiableCredential } from "@/interfaces/claims-result";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||
import {
|
||||
generateSaveAndActivateIdentity,
|
||||
contactsToExportJson,
|
||||
} from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
// No longer needed - using PlatformServiceMixin methods
|
||||
// import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { isDatabaseError } from "@/interfaces/common";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { APP_SERVER } from "@/constants/app";
|
||||
import { QRNavigationService } from "@/services/QRNavigationService";
|
||||
import {
|
||||
NOTIFY_CONTACT_NO_INFO,
|
||||
NOTIFY_CONTACTS_ADD_ERROR,
|
||||
@@ -197,6 +202,7 @@ import {
|
||||
NOTIFY_REGISTRATION_ERROR_FALLBACK,
|
||||
NOTIFY_REGISTRATION_ERROR_GENERIC,
|
||||
NOTIFY_VISIBILITY_ERROR_FALLBACK,
|
||||
NOTIFY_EXPORT_DATA_PROMPT,
|
||||
getRegisterPersonSuccessMessage,
|
||||
getVisibilitySuccessMessage,
|
||||
getGivesRetrievalErrorMessage,
|
||||
@@ -376,7 +382,11 @@ export default class ContactsView extends Vue {
|
||||
"",
|
||||
async (name) => {
|
||||
await this.addContact({
|
||||
did: (registration.vc.credentialSubject.agent as any).identifier,
|
||||
did: (
|
||||
registration.vc.credentialSubject.agent as {
|
||||
identifier: string;
|
||||
}
|
||||
).identifier,
|
||||
name: name,
|
||||
registered: true,
|
||||
});
|
||||
@@ -387,7 +397,11 @@ export default class ContactsView extends Vue {
|
||||
async () => {
|
||||
// on cancel, will still add the contact
|
||||
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)",
|
||||
registered: true,
|
||||
});
|
||||
@@ -396,8 +410,7 @@ export default class ContactsView extends Vue {
|
||||
this.showOnboardingInfo();
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const fullError = "Error redeeming invite: " + errorStringForLog(error);
|
||||
this.$logAndConsole(fullError, true);
|
||||
let message = "Got an error sending the invite.";
|
||||
@@ -784,6 +797,9 @@ export default class ContactsView extends Vue {
|
||||
|
||||
// Show success notification
|
||||
this.notify.success(addedMessage);
|
||||
|
||||
// Show export data prompt after successful contact addition
|
||||
await this.showExportDataPrompt();
|
||||
} catch (err) {
|
||||
this.handleContactAddError(err);
|
||||
}
|
||||
@@ -881,20 +897,21 @@ export default class ContactsView extends Vue {
|
||||
/**
|
||||
* Handle errors during contact addition
|
||||
*/
|
||||
private handleContactAddError(err: any): void {
|
||||
private handleContactAddError(err: unknown): void {
|
||||
const fullError =
|
||||
"Error when adding contact to storage: " + errorStringForLog(err);
|
||||
this.$logAndConsole(fullError, true);
|
||||
|
||||
let message = NOTIFY_CONTACT_IMPORT_ERROR.message;
|
||||
if (
|
||||
(err as any).message?.indexOf("Key already exists in the object store.") >
|
||||
-1
|
||||
) {
|
||||
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
|
||||
}
|
||||
if ((err as any).name === "ConstraintError") {
|
||||
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
|
||||
|
||||
// Use type-safe error checking with our new type guards
|
||||
if (isDatabaseError(err)) {
|
||||
if (err.message.includes("Key already exists in the object store")) {
|
||||
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
|
||||
}
|
||||
if (err.name === "ConstraintError") {
|
||||
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* Uses native scanner on mobile platforms, web scanner otherwise
|
||||
* Uses QRNavigationService to determine scanner type and route
|
||||
*/
|
||||
|
||||
public handleQRCodeClick() {
|
||||
this.$logAndConsole(
|
||||
"[ContactsView] handleQRCodeClick method called",
|
||||
false,
|
||||
);
|
||||
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
} else {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
const qrNavigationService = QRNavigationService.getInstance();
|
||||
const route = qrNavigationService.getQRScannerRoute();
|
||||
|
||||
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"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,8 @@ export default class DIDView extends Vue {
|
||||
message +=
|
||||
" 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 () => {
|
||||
await this.deleteContact(contact);
|
||||
});
|
||||
|
||||
@@ -91,17 +91,12 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="downloadAccount"
|
||||
>
|
||||
<IconRenderer
|
||||
<font-awesome
|
||||
v-if="isLoading"
|
||||
icon-name="spinner"
|
||||
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<IconRenderer
|
||||
v-else
|
||||
icon-name="chart"
|
||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||
icon="spinner"
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
/>
|
||||
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Show Account Seed
|
||||
</button>
|
||||
|
||||
@@ -110,17 +105,12 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="downloadSettingsContacts"
|
||||
>
|
||||
<IconRenderer
|
||||
<font-awesome
|
||||
v-if="isLoading"
|
||||
icon-name="spinner"
|
||||
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<IconRenderer
|
||||
v-else
|
||||
icon-name="chart"
|
||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||
icon="spinner"
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
/>
|
||||
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Download Settings & Contacts
|
||||
</button>
|
||||
|
||||
@@ -143,17 +133,12 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="compareDatabases"
|
||||
>
|
||||
<IconRenderer
|
||||
<font-awesome
|
||||
v-if="isLoading"
|
||||
icon-name="spinner"
|
||||
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<IconRenderer
|
||||
v-else
|
||||
icon-name="chart"
|
||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||
icon="spinner"
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
/>
|
||||
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Compare Databases
|
||||
</button>
|
||||
|
||||
@@ -162,17 +147,12 @@
|
||||
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="migrateAll"
|
||||
>
|
||||
<IconRenderer
|
||||
<font-awesome
|
||||
v-if="isLoading"
|
||||
icon-name="spinner"
|
||||
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<IconRenderer
|
||||
v-else
|
||||
icon-name="check"
|
||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||
icon="spinner"
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
/>
|
||||
<font-awesome v-else icon="check" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Migrate All
|
||||
</button>
|
||||
|
||||
@@ -185,10 +165,9 @@
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="warning"
|
||||
svg-class="h-5 w-5 text-red-400"
|
||||
fill="currentColor"
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="h-5 w-5 text-red-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
@@ -207,10 +186,9 @@
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="warning"
|
||||
svg-class="h-5 w-5 text-red-400"
|
||||
fill="currentColor"
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="h-5 w-5 text-red-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
@@ -229,10 +207,7 @@
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="check"
|
||||
svg-class="h-5 w-5 text-green-400"
|
||||
/>
|
||||
<font-awesome icon="check" class="h-5 w-5 text-green-400" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-green-800">Success</h3>
|
||||
@@ -249,7 +224,7 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="exportComparison"
|
||||
>
|
||||
<IconRenderer icon-name="download" svg-class="-ml-1 mr-3 h-5 w-5" />
|
||||
<font-awesome icon="download" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Export Comparison
|
||||
</button>
|
||||
|
||||
@@ -258,17 +233,12 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="displayDatabases"
|
||||
>
|
||||
<IconRenderer
|
||||
<font-awesome
|
||||
v-if="isLoading"
|
||||
icon-name="spinner"
|
||||
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<IconRenderer
|
||||
v-else
|
||||
icon-name="chart"
|
||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||
icon="spinner"
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
/>
|
||||
<font-awesome v-else icon="chart-line" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Show Previous Data
|
||||
</button>
|
||||
|
||||
@@ -277,7 +247,7 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="migrateAccounts"
|
||||
>
|
||||
<IconRenderer icon-name="lock" svg-class="-ml-1 mr-3 h-5 w-5" />
|
||||
<font-awesome icon="lock" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Migrate Accounts
|
||||
</button>
|
||||
|
||||
@@ -286,7 +256,7 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="migrateSettings"
|
||||
>
|
||||
<IconRenderer icon-name="settings" svg-class="-ml-1 mr-3 h-5 w-5" />
|
||||
<font-awesome icon="gear" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Migrate Settings
|
||||
</button>
|
||||
|
||||
@@ -295,7 +265,7 @@
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="migrateContacts"
|
||||
>
|
||||
<IconRenderer icon-name="plus" svg-class="-ml-1 mr-3 h-5 w-5" />
|
||||
<font-awesome icon="plus" class="-ml-1 mr-3 h-5 w-5" />
|
||||
Migrate Contacts
|
||||
</button>
|
||||
</div>
|
||||
@@ -316,11 +286,7 @@
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="info"
|
||||
svg-class="h-5 w-5 text-blue-400"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<font-awesome icon="info" class="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">
|
||||
@@ -357,10 +323,9 @@
|
||||
<div
|
||||
class="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-white bg-blue-500 hover:bg-blue-400 transition ease-in-out duration-150 cursor-not-allowed"
|
||||
>
|
||||
<IconRenderer
|
||||
icon-name="spinner"
|
||||
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
fill="currentColor"
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
/>
|
||||
{{ loadingMessage }}
|
||||
</div>
|
||||
@@ -375,10 +340,7 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="lock"
|
||||
svg-class="h-6 w-6 text-orange-600"
|
||||
/>
|
||||
<font-awesome icon="lock" class="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
@@ -398,10 +360,7 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="check"
|
||||
svg-class="h-6 w-6 text-teal-600"
|
||||
/>
|
||||
<font-awesome icon="check" class="h-6 w-6 text-teal-600" />
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
@@ -422,10 +381,7 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="settings"
|
||||
svg-class="h-6 w-6 text-purple-600"
|
||||
/>
|
||||
<font-awesome icon="gear" class="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
@@ -445,10 +401,7 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="check"
|
||||
svg-class="h-6 w-6 text-indigo-600"
|
||||
/>
|
||||
<font-awesome icon="check" class="h-6 w-6 text-indigo-600" />
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
@@ -469,9 +422,9 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="chart"
|
||||
svg-class="h-6 w-6 text-blue-600"
|
||||
<font-awesome
|
||||
icon="chart-line"
|
||||
class="h-6 w-6 text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
@@ -492,10 +445,7 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<IconRenderer
|
||||
icon-name="check"
|
||||
svg-class="h-6 w-6 text-green-600"
|
||||
/>
|
||||
<font-awesome icon="check" class="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
@@ -526,9 +476,9 @@
|
||||
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="plusCircle"
|
||||
svg-class="h-5 w-5 text-blue-600 mr-2"
|
||||
<font-awesome
|
||||
icon="circle-plus"
|
||||
class="h-5 w-5 text-blue-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-blue-900">Add</span>
|
||||
</div>
|
||||
@@ -541,9 +491,9 @@
|
||||
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="check"
|
||||
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
||||
<font-awesome
|
||||
icon="check"
|
||||
class="h-5 w-5 text-yellow-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-yellow-900"
|
||||
>Unmodified</span
|
||||
@@ -558,9 +508,9 @@
|
||||
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="trash"
|
||||
svg-class="h-5 w-5 text-red-600 mr-2"
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
class="h-5 w-5 text-red-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-red-900">Keep</span>
|
||||
</div>
|
||||
@@ -677,9 +627,9 @@
|
||||
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="plusCircle"
|
||||
svg-class="h-5 w-5 text-blue-600 mr-2"
|
||||
<font-awesome
|
||||
icon="circle-plus"
|
||||
class="h-5 w-5 text-blue-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-blue-900">Add</span>
|
||||
</div>
|
||||
@@ -692,9 +642,9 @@
|
||||
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="edit"
|
||||
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
||||
<font-awesome
|
||||
icon="pen"
|
||||
class="h-5 w-5 text-yellow-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-yellow-900"
|
||||
>Modify</span
|
||||
@@ -709,9 +659,9 @@
|
||||
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="check"
|
||||
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
||||
<font-awesome
|
||||
icon="check"
|
||||
class="h-5 w-5 text-yellow-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-yellow-900"
|
||||
>Unmodified</span
|
||||
@@ -726,9 +676,9 @@
|
||||
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="trash"
|
||||
svg-class="h-5 w-5 text-red-600 mr-2"
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
class="h-5 w-5 text-red-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-red-900">Keep</span>
|
||||
</div>
|
||||
@@ -868,9 +818,9 @@
|
||||
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="plusCircle"
|
||||
svg-class="h-5 w-5 text-blue-600 mr-2"
|
||||
<font-awesome
|
||||
icon="circle-plus"
|
||||
class="h-5 w-5 text-blue-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-blue-900">Add</span>
|
||||
</div>
|
||||
@@ -883,9 +833,9 @@
|
||||
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="edit"
|
||||
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
||||
<font-awesome
|
||||
icon="pen"
|
||||
class="h-5 w-5 text-yellow-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-yellow-900"
|
||||
>Modify</span
|
||||
@@ -900,9 +850,9 @@
|
||||
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="check"
|
||||
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
||||
<font-awesome
|
||||
icon="check"
|
||||
class="h-5 w-5 text-yellow-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-yellow-900"
|
||||
>Unmodified</span
|
||||
@@ -917,9 +867,9 @@
|
||||
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<IconRenderer
|
||||
icon-name="trash"
|
||||
svg-class="h-5 w-5 text-red-600 mr-2"
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
class="h-5 w-5 text-red-600 mr-2"
|
||||
/>
|
||||
<span class="text-sm font-medium text-red-900">Keep</span>
|
||||
</div>
|
||||
@@ -1067,7 +1017,6 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import IconRenderer from "../components/IconRenderer.vue";
|
||||
import {
|
||||
compareDatabases,
|
||||
migrateSettings,
|
||||
@@ -1104,9 +1053,6 @@ import { logger } from "../utils/logger";
|
||||
*/
|
||||
@Component({
|
||||
name: "DatabaseMigration",
|
||||
components: {
|
||||
IconRenderer,
|
||||
},
|
||||
})
|
||||
export default class DatabaseMigration extends Vue {
|
||||
$router!: Router;
|
||||
|
||||
@@ -458,7 +458,9 @@ export default class DiscoverView extends Vue {
|
||||
if (this.isLocalActive) {
|
||||
await this.searchLocal();
|
||||
} 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
|
||||
} else {
|
||||
await this.searchAll();
|
||||
@@ -518,11 +520,11 @@ export default class DiscoverView extends Vue {
|
||||
throw JSON.stringify(results);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
logger.error("Error with search all: " + errorStringForLog(e));
|
||||
this.notify.error(
|
||||
e.userMessage || NOTIFY_DISCOVER_SEARCH_ERROR.message,
|
||||
(e as { userMessage?: string })?.userMessage ||
|
||||
NOTIFY_DISCOVER_SEARCH_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -524,9 +524,8 @@ export default class HelpNotificationsView extends Vue {
|
||||
DIRECT_PUSH_TITLE,
|
||||
async (success: boolean, timeText: string, message?: string) => {
|
||||
if (success) {
|
||||
await this.$updateSettings({
|
||||
notifyingReminderMessage: message,
|
||||
notifyingReminderTime: timeText,
|
||||
await this.$saveMySettings({
|
||||
hideHelpOnStart: true,
|
||||
});
|
||||
this.notifyingReminder = true;
|
||||
this.notifyingReminderMessage = message || "";
|
||||
|
||||
@@ -565,22 +565,22 @@
|
||||
<h2 class="text-xl font-semibold">What app version is this?</h2>
|
||||
<p>{{ package.version }} ({{ commitHash }})</p>
|
||||
|
||||
<div v-if="Capacitor.isNativePlatform()">
|
||||
<div v-if="isCapacitor">
|
||||
<h2 class="text-xl font-semibold">
|
||||
Do I have the latest version?
|
||||
</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">
|
||||
Check the App Store.
|
||||
</a>
|
||||
</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">
|
||||
Check the Play Store.
|
||||
</a>
|
||||
</p>
|
||||
<p v-else>
|
||||
Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized.
|
||||
Sorry, your platform is not recognized.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -592,12 +592,13 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
// Capacitor import removed - using QRNavigationService instead
|
||||
|
||||
import * as Package from "../../package.json";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { APP_SERVER } from "../constants/app";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { QRNavigationService } from "@/services/QRNavigationService";
|
||||
|
||||
/**
|
||||
* HelpView.vue - Comprehensive Help System Component
|
||||
@@ -643,7 +644,7 @@ export default class HelpView extends Vue {
|
||||
showVerifiable = false;
|
||||
|
||||
APP_SERVER = APP_SERVER;
|
||||
Capacitor = Capacitor;
|
||||
// Capacitor reference removed - using QRNavigationService instead
|
||||
|
||||
// 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.
|
||||
@@ -679,9 +680,8 @@ export default class HelpView extends Vue {
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
if (settings.activeDid) {
|
||||
await this.$updateSettings({
|
||||
...settings,
|
||||
finishedOnboarding: false,
|
||||
await this.$saveMySettings({
|
||||
hideHelpOnStart: true,
|
||||
});
|
||||
|
||||
this.$log(
|
||||
@@ -711,11 +711,10 @@ export default class HelpView extends Vue {
|
||||
* @private
|
||||
*/
|
||||
private handleQRCodeClick(): void {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
} else {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
}
|
||||
const qrNavigationService = QRNavigationService.getInstance();
|
||||
const route = qrNavigationService.getQRScannerRoute();
|
||||
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -476,7 +476,7 @@ export default class HomeView extends Vue {
|
||||
// Re-initialize identity with new settings (loads settings internally)
|
||||
await this.initializeIdentity();
|
||||
} else {
|
||||
logger.info(
|
||||
logger.debug(
|
||||
"[HomeView Settings Trace] 📍 DID unchanged, skipping re-initialization",
|
||||
);
|
||||
}
|
||||
@@ -756,17 +756,34 @@ export default class HomeView extends Vue {
|
||||
* Called by FeedFilters component when filters change
|
||||
*/
|
||||
async reloadFeedOnChange() {
|
||||
const settings = await this.$accountSettings(this.activeDid, {
|
||||
filterFeedByVisible: false,
|
||||
filterFeedByNearby: false,
|
||||
logger.debug("[HomeView] 🔄 reloadFeedOnChange() called - refreshing feed");
|
||||
|
||||
// Get current settings without overriding with defaults
|
||||
const settings = await this.$accountSettings(this.activeDid);
|
||||
|
||||
logger.debug("[HomeView] 📊 Current filter settings:", {
|
||||
filterFeedByVisible: settings.filterFeedByVisible,
|
||||
filterFeedByNearby: settings.filterFeedByNearby,
|
||||
searchBoxes: settings.searchBoxes?.length || 0,
|
||||
});
|
||||
|
||||
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
|
||||
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
|
||||
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
||||
|
||||
logger.debug("[HomeView] 🎯 Updated filter states:", {
|
||||
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
|
||||
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
|
||||
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
|
||||
});
|
||||
|
||||
this.feedData = [];
|
||||
this.feedPreviousOldestId = undefined;
|
||||
|
||||
logger.debug("[HomeView] 🧹 Cleared feed data, calling updateAllFeed()");
|
||||
await this.updateAllFeed();
|
||||
|
||||
logger.debug("[HomeView] ✅ Feed refresh completed");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -845,6 +862,14 @@ export default class HomeView extends Vue {
|
||||
* - this.feedLastViewedClaimId (via updateFeedLastViewedId)
|
||||
*/
|
||||
async updateAllFeed() {
|
||||
logger.debug("[HomeView] 🚀 updateAllFeed() called", {
|
||||
isFeedLoading: this.isFeedLoading,
|
||||
currentFeedDataLength: this.feedData.length,
|
||||
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
|
||||
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
|
||||
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
|
||||
});
|
||||
|
||||
this.isFeedLoading = true;
|
||||
let endOfResults = true;
|
||||
|
||||
@@ -853,21 +878,37 @@ export default class HomeView extends Vue {
|
||||
this.apiServer,
|
||||
this.feedPreviousOldestId,
|
||||
);
|
||||
|
||||
logger.debug("[HomeView] 📡 Retrieved gives from API", {
|
||||
resultsCount: results.data.length,
|
||||
endOfResults,
|
||||
});
|
||||
|
||||
if (results.data.length > 0) {
|
||||
endOfResults = false;
|
||||
// gather any contacts that user has blocked from view
|
||||
await this.processFeedResults(results.data);
|
||||
await this.updateFeedLastViewedId(results.data);
|
||||
|
||||
logger.debug("[HomeView] 📝 Processed feed results", {
|
||||
processedCount: this.feedData.length,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("[HomeView] ❌ Error in updateAllFeed:", e);
|
||||
this.handleFeedError(e);
|
||||
}
|
||||
|
||||
if (this.feedData.length === 0 && !endOfResults) {
|
||||
logger.debug("[HomeView] 🔄 No results after filtering, retrying...");
|
||||
await this.updateAllFeed();
|
||||
}
|
||||
|
||||
this.isFeedLoading = false;
|
||||
logger.debug("[HomeView] ✅ updateAllFeed() completed", {
|
||||
finalFeedDataLength: this.feedData.length,
|
||||
isFeedLoading: this.isFeedLoading,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -892,12 +933,35 @@ export default class HomeView extends Vue {
|
||||
* @param records Array of feed records to process
|
||||
*/
|
||||
private async processFeedResults(records: GiveSummaryRecord[]) {
|
||||
logger.debug("[HomeView] 📝 Processing feed results:", {
|
||||
inputRecords: records.length,
|
||||
currentFilters: {
|
||||
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
|
||||
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
|
||||
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
|
||||
},
|
||||
});
|
||||
|
||||
let processedCount = 0;
|
||||
let filteredCount = 0;
|
||||
|
||||
for (const record of records) {
|
||||
const processedRecord = await this.processRecord(record);
|
||||
if (processedRecord) {
|
||||
this.feedData.push(processedRecord);
|
||||
processedCount++;
|
||||
} else {
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("[HomeView] 📊 Feed processing results:", {
|
||||
processed: processedCount,
|
||||
filtered: filteredCount,
|
||||
total: records.length,
|
||||
finalFeedLength: this.feedData.length,
|
||||
});
|
||||
|
||||
this.feedPreviousOldestId = records[records.length - 1].jwtId;
|
||||
}
|
||||
|
||||
@@ -931,7 +995,7 @@ export default class HomeView extends Vue {
|
||||
* - this.feedData (via createFeedRecord)
|
||||
*
|
||||
* @param record The record to process
|
||||
* @returns Processed record with contact info if it passes filters, null otherwise
|
||||
* @returns Processed record if it passes filters, null otherwise
|
||||
*/
|
||||
private async processRecord(
|
||||
record: GiveSummaryRecord,
|
||||
@@ -941,13 +1005,28 @@ export default class HomeView extends Vue {
|
||||
const recipientDid = this.extractRecipientDid(claim);
|
||||
|
||||
const fulfillsPlan = await this.getFulfillsPlan(record);
|
||||
|
||||
// Log record details for debugging
|
||||
logger.debug("[HomeView] 🔍 Processing record:", {
|
||||
recordId: record.jwtId,
|
||||
hasFulfillsPlan: !!fulfillsPlan,
|
||||
fulfillsPlanHandleId: record.fulfillsPlanHandleId,
|
||||
filters: {
|
||||
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
|
||||
isFeedFilteredByVisible: this.isFeedFilteredByNearby,
|
||||
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
|
||||
},
|
||||
});
|
||||
|
||||
if (!this.shouldIncludeRecord(record, fulfillsPlan)) {
|
||||
logger.debug("[HomeView] ❌ Record filtered out:", record.jwtId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = this.extractProvider(claim);
|
||||
const providedByPlan = await this.getProvidedByPlan(provider);
|
||||
|
||||
logger.debug("[HomeView] ✅ Record included:", record.jwtId);
|
||||
return this.createFeedRecord(
|
||||
record,
|
||||
claim,
|
||||
@@ -1096,6 +1175,22 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// Add debug logging for nearby filter
|
||||
if (this.isFeedFilteredByNearby && record.fulfillsPlanHandleId) {
|
||||
logger.debug("[HomeView] 🔍 Nearby filter check:", {
|
||||
recordId: record.jwtId,
|
||||
hasFulfillsPlan: !!fulfillsPlan,
|
||||
hasLocation: !!(fulfillsPlan?.locLat && fulfillsPlan?.locLon),
|
||||
location: fulfillsPlan
|
||||
? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon }
|
||||
: null,
|
||||
inSearchBox: fulfillsPlan
|
||||
? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon)
|
||||
: null,
|
||||
finalResult: anyMatch,
|
||||
});
|
||||
}
|
||||
|
||||
return anyMatch;
|
||||
}
|
||||
|
||||
@@ -1531,7 +1626,10 @@ export default class HomeView extends Vue {
|
||||
* Called by template click handler
|
||||
*/
|
||||
openFeedFilters() {
|
||||
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
|
||||
(this.$refs.feedFilters as FeedFilters).open(
|
||||
this.reloadFeedOnChange,
|
||||
this.activeDid,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -234,7 +234,9 @@ export default class IdentitySwitcherView extends Vue {
|
||||
{
|
||||
did,
|
||||
settingsKeys: Object.keys(newSettings).filter(
|
||||
(k) => (newSettings as any)[k] !== undefined,
|
||||
(k) =>
|
||||
k in newSettings &&
|
||||
newSettings[k as keyof typeof newSettings] !== undefined,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -221,10 +221,11 @@ export default class ImportAccountView extends Vue {
|
||||
|
||||
this.notify.success("Account imported successfully!", TIMEOUTS.STANDARD);
|
||||
this.$router.push({ name: "account" });
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.$logError("Import failed: " + error);
|
||||
this.notify.error(
|
||||
error.message || "Failed to import account.",
|
||||
(error instanceof Error ? error.message : String(error)) ||
|
||||
"Failed to import account.",
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
|
||||