diff --git a/.cursor/rules/adr_template.mdc b/.cursor/rules/adr_template.mdc new file mode 100644 index 00000000..8faeaf1c --- /dev/null +++ b/.cursor/rules/adr_template.mdc @@ -0,0 +1,63 @@ +# ADR Template + +## ADR-XXXX-YY-ZZ: [Short Title] + +**Date:** YYYY-MM-DD +**Status:** [PROPOSED | ACCEPTED | REJECTED | DEPRECATED | SUPERSEDED] +**Deciders:** [List of decision makers] +**Technical Story:** [Link to issue/PR if applicable] + +## Context + +[Describe the forces at play, including technological, political, social, and project local. These forces are probably in tension, and should be called out as such. The language in this section is value-neutral. It is simply describing facts.] + +## Decision + +[Describe our response to these forces. We will use the past tense ("We will...").] + +## Consequences + +### Positive +- [List positive consequences] + +### Negative +- [List negative consequences or trade-offs] + +### Neutral +- [List neutral consequences or notes] + +## Alternatives Considered + +- **Alternative 1:** [Description] - [Why rejected] +- **Alternative 2:** [Description] - [Why rejected] +- **Alternative 3:** [Description] - [Why rejected] + +## Implementation Notes + +[Any specific implementation details, migration steps, or technical considerations] + +## References + +- [Link to relevant documentation] +- [Link to related ADRs] +- [Link to external resources] + +## Related Decisions + +- [List related ADRs or decisions] + +--- + +## Usage Guidelines + +1. **Copy this template** for new ADRs +2. **Number sequentially** (ADR-001, ADR-002, etc.) +3. **Use descriptive titles** that clearly indicate the decision +4. **Include all stakeholders** in the deciders list +5. **Link to related issues** and documentation +6. **Update status** as decisions evolve +7. **Store in** `doc/architecture-decisions/` directory +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/app/architectural_decision_record.mdc b/.cursor/rules/app/architectural_decision_record.mdc new file mode 100644 index 00000000..9772de1f --- /dev/null +++ b/.cursor/rules/app/architectural_decision_record.mdc @@ -0,0 +1,172 @@ +--- +description: +globs: +alwaysApply: true +--- +# TimeSafari Cross-Platform Architecture Guide + +## 1. Platform Support Matrix + +| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) | +|---------|-----------|--------------------|-------------------| +| QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented | +| Deep Linking | URL Parameters | App URL Open Events | Not Implemented | +| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs | +| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented | +| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks | + +--- + +## 2. Project Structure + +### Core Directories +``` +src/ +├── components/ # Vue components +├── services/ # Platform services and business logic +├── views/ # Page components +├── router/ # Vue router configuration +├── types/ # TypeScript type definitions +├── utils/ # Utility functions +├── lib/ # Core libraries +├── platforms/ # Platform-specific implementations +├── electron/ # Electron-specific code +├── constants/ # Application constants +├── db/ # Database related code +├── interfaces/ # TypeScript interfaces +└── assets/ # Static assets +``` + +### Entry Points +- `main.ts` → Base entry +- `main.common.ts` → Shared init +- `main.capacitor.ts` → Mobile entry +- `main.electron.ts` → Electron entry +- `main.web.ts` → Web entry + +--- + +## 3. Service Architecture + +### Service Organization + +```tree +services/ +├── QRScanner/ +│ ├── WebInlineQRScanner.ts +│ └── interfaces.ts +├── platforms/ +│ ├── WebPlatformService.ts +│ ├── CapacitorPlatformService.ts +│ └── ElectronPlatformService.ts +└── factory/ + └── PlatformServiceFactory.ts +``` + +### Factory Pattern +Use a **singleton factory** to select platform services via `process.env.VITE_PLATFORM`. + +--- + +## 4. Feature Guidelines + +### QR Code Scanning +- Define `QRScannerService` interface. +- Implement platform-specific classes (`WebInlineQRScanner`, Capacitor, etc). +- Provide `addListener` and `onStream` hooks for composability. + +### Deep Linking +- URL format: `timesafari://[/][?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? diff --git a/.cursor/rules/timesafari.mdc b/.cursor/rules/app/timesafari.mdc similarity index 100% rename from .cursor/rules/timesafari.mdc rename to .cursor/rules/app/timesafari.mdc diff --git a/.cursor/rules/architectural_decision_record.mdc b/.cursor/rules/architectural_decision_record.mdc deleted file mode 100644 index fdc53c60..00000000 --- a/.cursor/rules/architectural_decision_record.mdc +++ /dev/null @@ -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; - requestPermissions(): Promise; - isSupported(): Promise; - startScan(): Promise; - stopScan(): Promise; - addListener(listener: ScanListener): void; - onStream(callback: (stream: MediaStream | null) => void): void; - cleanup(): Promise; -} -``` - -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://[/][?queryParam1=value1] -interface DeepLinkParams { - route: string; - params?: Record; - query?: Record; -} -``` - -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 { - 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. diff --git a/.cursor/rules/asset_configuration.mdc b/.cursor/rules/asset_configuration.mdc new file mode 100644 index 00000000..916ecdd6 --- /dev/null +++ b/.cursor/rules/asset_configuration.mdc @@ -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 diff --git a/.cursor/rules/base_context.mdc b/.cursor/rules/base_context.mdc new file mode 100644 index 00000000..20ade9df --- /dev/null +++ b/.cursor/rules/base_context.mdc @@ -0,0 +1,110 @@ +```json +{ + "coaching_level": "standard", + "socratic_max_questions": 7, + "verbosity": "normal", + "timebox_minutes": null, + "format_enforcement": "strict" +} +``` + +# Base Context — Human Competence First + +## Purpose +All interactions must *increase the human's competence over time* while +completing the task efficiently. The model may handle menial work and memory +extension, but must also promote learning, autonomy, and healthy work habits. +The model should also **encourage human interaction and collaboration** rather +than replacing it — outputs should be designed to **facilitate human discussion, +decision-making, and creativity**, not to atomize tasks into isolated, purely +machine-driven steps. + +## Principles + +1) Competence over convenience: finish the task *and* leave the human more + capable next time. +2) Mentorship, not lectures: be concise, concrete, and immediately applicable. +3) Transparency: show assumptions, limits, and uncertainty; cite when non-obvious. +4) Optional scaffolding: include small, skimmable learning hooks that do not + bloat output. +5) Time respect: default to **lean output**; offer opt-in depth via toggles. +6) Psychological safety: encourage, never condescend; no medical/clinical advice. + No censorship! +7) Reusability: structure outputs so they can be saved, searched, reused, and repurposed. +8) **Collaborative Bias**: Favor solutions that invite human review, discussion, + and iteration. When in doubt, ask "Who should this be shown to?" or "Which human + input would improve this?" + +## Toggle Definitions + +### coaching_level + +Determines the depth of learning support: `light` (short hooks), `standard` +(balanced), `deep` (detailed). + +### socratic_max_questions + +The number of clarifying questions the model may ask before proceeding. +If >0, questions should be targeted, minimal, and followed by reasonable assumptions if unanswered. + +### verbosity +'terse' (just a sentence), `concise` (minimum commentary), `normal` (balanced explanation), or other project-defined levels. + +### timebox_minutes +*integer or null* — When set to a positive integer (e.g., `5`), this acts as a **time budget** guiding the model to prioritize delivering the most essential parts of the task within that constraint. +Behavior when set: +1. **Prioritize Core Output** — Deliver the minimum viable solution or result first. +2. **Limit Commentary** — Competence Hooks and Collaboration Hooks must be shorter than normal. +3. **Signal Skipped Depth** — Omitted details should be listed under *Deferred for depth*. +4. **Order by Value** — Start with blocking or high-value items, then proceed to nice-to-haves if budget allows. +If `null`, there is no timebox — the model can produce full-depth responses. + +### format_enforcement +`strict` (reject outputs with format drift) or `relaxed` (minor deviations acceptable). + +## Modes (select or combine) +- **Doer**: produce the artifact fast, minimal commentary. +- **Mentor**: add short "why/how" notes + next-step pointers. +- **Socratic**: ask up to N targeted questions when requirements are ambiguous. +- **Pair-Programmer/Pair-Writer**: explain tradeoffs as you implement. +- **Facilitator**: structure output to be reviewable, commentable, and ready for group discussion. + +Default: Doer + short Mentor notes. + +## Competence & Collaboration Levers (keep lightweight) +- "Why this works" (≤3 bullets) +- "Common pitfalls" (≤3 bullets) +- "Next skill unlock" (1 tiny action or reading) +- "Teach-back" (1 sentence prompt the human can answer to self-check) +- "Discussion prompts" (≤2 short questions for peers/stakeholders) + +## Output Contract (apply to every deliverable) +- Clear **Objective** (1 line) +- **Result** (artifact/code/answer) +- **Use/Run** (how to apply/test) +- **Competence Hooks** (the 4 learning levers above, kept terse) +- **Collaboration Hooks** (discussion prompts or group review steps) +- **Assumptions & Limits** +- **References** (if used; links or titles) + +## Do-Not +- No filler, hedging, or moralizing. +- No medical/mental-health advice; keep "healthy habits" to general work practices. +- No invented facts; mark uncertainty plainly. +- No censorship. +- Avoid outputs that bypass human review when such review is valuable. + +## Related Rulesets + +- **software_development.mdc**: For software-specific development practices +- **research_diagnostic.mdc**: For investigation and research workflows + +## Self-Check (model, before responding) +- [ ] Task done *and* at least one competence lever included (≤120 words total). +- [ ] At least one collaboration/discussion hook present. +- [ ] Output follows the **Output Contract** sections. +- [ ] Toggles respected; verbosity remains concise. +- [ ] Uncertainties/assumptions surfaced. +- [ ] No disallowed content. +- [ ] Uncertainties/assumptions surfaced. +- [ ] No disallowed content. diff --git a/.cursor/rules/absurd-sql.mdc b/.cursor/rules/database/absurd-sql.mdc similarity index 95% rename from .cursor/rules/absurd-sql.mdc rename to .cursor/rules/database/absurd-sql.mdc index 56729c2a..e8b66e79 100644 --- a/.cursor/rules/absurd-sql.mdc +++ b/.cursor/rules/database/absurd-sql.mdc @@ -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 diff --git a/.cursor/rules/database/legacy_dexie.mdc b/.cursor/rules/database/legacy_dexie.mdc new file mode 100644 index 00000000..5ef07221 --- /dev/null +++ b/.cursor/rules/database/legacy_dexie.mdc @@ -0,0 +1,5 @@ +--- +globs: **/databaseUtil.ts,**/AccountViewView.vue,**/ContactsView.vue,**/DatabaseMigration.vue,**/NewIdentifierView.vue +alwaysApply: false +--- +All references in the codebase to Dexie apply only to migration from IndexedDb to Sqlite and will be deprecated in future versions. \ No newline at end of file diff --git a/.cursor/rules/development_guide.mdc b/.cursor/rules/development/development_guide.mdc similarity index 82% rename from .cursor/rules/development_guide.mdc rename to .cursor/rules/development/development_guide.mdc index cc43b000..439c1f26 100644 --- a/.cursor/rules/development_guide.mdc +++ b/.cursor/rules/development/development_guide.mdc @@ -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 diff --git a/.cursor/rules/development/type_safety_guide.mdc b/.cursor/rules/development/type_safety_guide.mdc new file mode 100644 index 00000000..507e3f23 --- /dev/null +++ b/.cursor/rules/development/type_safety_guide.mdc @@ -0,0 +1,108 @@ +--- +globs: **/src/**/*,**/scripts/**/*,**/electron/**/* +alwaysApply: false +--- +```json +{ + "coaching_level": "light", + "socratic_max_questions": 7, + "verbosity": "concise", + "timebox_minutes": null, + "format_enforcement": "strict" +} +``` + +# TypeScript Type Safety Guidelines + +**Author**: Matthew Raymer +**Date**: 2025-08-16 +**Status**: 🎯 **ACTIVE** + +## Overview + +Practical rules to keep TypeScript strict and predictable. Minimize exceptions. + +## Core Rules + +1. **No `any`** + - Use explicit types. If unknown, use `unknown` and **narrow** via guards. + +2. **Error handling uses guards** + - Reuse guards from `src/interfaces/**` (e.g., `isDatabaseError`, `isApiError`). + - Catch with `unknown`; never cast to `any`. + +3. **Dynamic property access is type‑safe** + - Use `keyof` + `in` checks: + + ```ts + obj[k as keyof typeof obj] + ``` + + - Avoid `(obj as any)[k]`. + +## Minimal Special Cases (document in PR when used) + +- **Vue refs / instances**: Use `ComponentPublicInstance` or specific component + types for dynamic refs. +- **3rd‑party libs without types**: Narrow immediately to a **known interface**; + do not leave `any` hanging. + +## Patterns (short) + +### Database errors + +```ts +try { await this.$addContact(contact); } +catch (e: unknown) { + if (isDatabaseError(e) && e.message.includes("Key already exists")) { + /* handle duplicate */ + } +} +``` + +### API errors + +```ts +try { await apiCall(); } +catch (e: unknown) { + if (isApiError(e)) { + const msg = e.response?.data?.error?.message; + } +} +``` + +### Dynamic keys + +```ts +const keys = Object.keys(newSettings).filter( + k => k in newSettings && newSettings[k as keyof typeof newSettings] !== undefined +); +``` + +## Checklists + +**Before commit** + +- [ ] No `any` (except documented, justified cases) +- [ ] Errors handled via guards +- [ ] Dynamic access uses `keyof`/`in` +- [ ] Imports point to correct interfaces/types + +**Code review** + +- [ ] Hunt hidden `as any` +- [ ] Guard‑based error paths verified +- [ ] Dynamic ops are type‑safe +- [ ] Prefer existing types over re‑inventing + +## Tools + +- `npm run lint-fix` — lint & auto‑fix +- `npm run type-check` — strict type compilation (CI + pre‑release) +- IDE: enable strict TS, ESLint/TS ESLint, Volar (Vue 3) + +## References + +- TS Handbook — https://www.typescriptlang.org/docs/ +- TS‑ESLint — https://typescript-eslint.io/rules/ +- Vue 3 + TS — https://vuejs.org/guide/typescript/ diff --git a/.cursor/rules/documentation.mdc b/.cursor/rules/docs/documentation.mdc similarity index 100% rename from .cursor/rules/documentation.mdc rename to .cursor/rules/docs/documentation.mdc diff --git a/.cursor/rules/markdown.mdc b/.cursor/rules/docs/markdown.mdc similarity index 100% rename from .cursor/rules/markdown.mdc rename to .cursor/rules/docs/markdown.mdc diff --git a/.cursor/rules/camera-implementation.mdc b/.cursor/rules/features/camera-implementation.mdc similarity index 100% rename from .cursor/rules/camera-implementation.mdc rename to .cursor/rules/features/camera-implementation.mdc diff --git a/.cursor/rules/investigation_report_example.mdc b/.cursor/rules/investigation_report_example.mdc new file mode 100644 index 00000000..fcce2b2f --- /dev/null +++ b/.cursor/rules/investigation_report_example.mdc @@ -0,0 +1,76 @@ +# Investigation Report Example + +## Investigation — Registration Dialog Test Flakiness + +## Objective +Identify root cause of flaky tests related to registration dialogs in contact import scenarios. + +## System Map +- User action → ContactInputForm → ContactsView.addContact() → handleRegistrationPrompt() +- setTimeout(1000ms) → Modal dialog → User response → Registration API call +- Test execution → Wait for dialog → Assert dialog content → Click response button + +## Findings (Evidence) +- **1-second timeout causes flakiness** — evidence: `src/views/ContactsView.vue:971-1000`; setTimeout(..., 1000) in handleRegistrationPrompt() +- **Import flow bypasses dialogs** — evidence: `src/views/ContactImportView.vue:500-520`; importContacts() calls $insertContact() directly, no handleRegistrationPrompt() +- **Dialog only appears in direct add flow** — evidence: `src/views/ContactsView.vue:774-800`; addContact() calls handleRegistrationPrompt() after database insert + +## Hypotheses & Failure Modes +- H1: 1-second timeout makes dialog appearance unpredictable; would fail when tests run faster than 1000ms +- H2: Test environment timing differs from development; watch for CI vs local test differences + +## Corrections +- Updated: "Multiple dialogs interfere with imports" → "Import flow never triggers dialogs - they only appear in direct contact addition" +- Updated: "Complex batch registration needed" → "Simple timeout removal and test mode flag sufficient" + +## Diagnostics (Next Checks) +- [ ] Repro on CI environment vs local +- [ ] Measure actual dialog appearance timing +- [ ] Test with setTimeout removed +- [ ] Verify import flow doesn't call handleRegistrationPrompt + +## Risks & Scope +- Impacted: Contact addition tests, registration workflow tests; Data: None; Users: Test suite reliability + +## Decision / Next Steps +- Owner: Development Team; By: 2025-01-28 +- Action: Remove 1-second timeout + add test mode flag; Exit criteria: Tests pass consistently + +## References +- `src/views/ContactsView.vue:971-1000` +- `src/views/ContactImportView.vue:500-520` +- `src/views/ContactsView.vue:774-800` + +## Competence Hooks +- Why this works: Code path tracing revealed separate execution flows, evidence disproved initial assumptions +- Common pitfalls: Assuming related functionality without tracing execution paths, over-engineering solutions to imaginary problems +- Next skill: Learn to trace code execution before proposing architectural changes +- Teach-back: "What evidence shows that contact imports bypass registration dialogs?" + +--- + +## Key Learning Points + +### Evidence-First Approach +This investigation demonstrates the importance of: +1. **Tracing actual code execution** rather than making assumptions +2. **Citing specific evidence** with file:line references +3. **Validating problem scope** before proposing solutions +4. **Considering simpler alternatives** before complex architectural changes + +### Code Path Tracing Value +By tracing the execution paths, we discovered: +- Import flow and direct add flow are completely separate +- The "multiple dialog interference" problem didn't exist +- A simple timeout removal would solve the actual issue + +### Prevention of Over-Engineering +The investigation prevented: +- Unnecessary database schema changes +- Complex batch registration systems +- Migration scripts for non-existent problems +- Architectural changes based on assumptions +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/legacy_dexie.mdc b/.cursor/rules/legacy_dexie.mdc deleted file mode 100644 index 82de6cca..00000000 --- a/.cursor/rules/legacy_dexie.mdc +++ /dev/null @@ -1,6 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -All references in the codebase to Dexie apply only to migration from IndexedDb to Sqlite and will be deprecated in future versions. \ No newline at end of file diff --git a/.cursor/rules/research_diagnostic.mdc b/.cursor/rules/research_diagnostic.mdc new file mode 100644 index 00000000..d249a300 --- /dev/null +++ b/.cursor/rules/research_diagnostic.mdc @@ -0,0 +1,170 @@ +--- +description: Use this workflow when doing **pre-implementation research, defect investigations with uncertain repros, or clarifying system architecture and behaviors**. +alwaysApply: false +--- +```json +{ + "coaching_level": "light", + "socratic_max_questions": 2, + "verbosity": "concise", + "timebox_minutes": null, + "format_enforcement": "strict" +} +``` + +# Research & Diagnostic Workflow (R&D) + +## Purpose + +Provide a **repeatable, evidence-first** workflow to investigate features and +defects **before coding**. Outputs are concise reports, hypotheses, and next +steps—**not** code changes. + +## When to Use + +- Pre-implementation research for new features +- Defect investigations (repros uncertain, user-specific failures) +- Architecture/behavior clarifications (e.g., auth flows, merges, migrations) + +--- + +## Enhanced with Software Development Ruleset + +When investigating software issues, also apply: +- **Code Path Tracing**: Required for technical investigations +- **Evidence Validation**: Ensure claims are code-backed +- **Solution Complexity Assessment**: Justify architectural changes + +--- + +## Output Contract (strict) + +1) **Objective** — 1–2 lines +2) **System Map (if helpful)** — short diagram or bullet flow (≤8 bullets) +3) **Findings (Evidence-linked)** — bullets; each with file/function refs +4) **Hypotheses & Failure Modes** — short list, each testable +5) **Corrections** — explicit deltas from earlier assumptions (if any) +6) **Diagnostics** — what to check next (logs, DB, env, repro steps) +7) **Risks & Scope** — what could break; affected components +8) **Decision/Next Steps** — what we'll do, who's involved, by when +9) **References** — code paths, ADRs, docs +10) **Competence & Collaboration Hooks** — brief, skimmable + +> Keep total length lean. Prefer links and bullets over prose. + +--- + +## Quickstart Template + +Copy/paste and fill: + +```md +# Investigation — + +## Objective + + +## System Map +- +- + +## Findings (Evidence) +- — evidence: `src/path/file.ts:function` (lines X–Y); log snippet/trace id +- — evidence: `...` + +## Hypotheses & Failure Modes +- H1: ; would fail when +- H2: ; watch for + +## Corrections +- Updated: + +## Diagnostics (Next Checks) +- [ ] Repro on +- [ ] Inspect for +- [ ] Capture + +## Risks & Scope +- Impacted: ; Data: ; Users: + +## Decision / Next Steps +- Owner: ; By: (YYYY-MM-DD) +- Action: ; Exit criteria: + +## 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: "" +``` + +--- + +## 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` diff --git a/.cursor/rules/software_development.mdc b/.cursor/rules/software_development.mdc new file mode 100644 index 00000000..f84bd5a2 --- /dev/null +++ b/.cursor/rules/software_development.mdc @@ -0,0 +1,178 @@ +--- +alwaysApply: true +--- + +# Software Development Ruleset + +## Purpose +Specialized guidelines for software development tasks including code review, debugging, architecture decisions, and testing. + +## Core Principles + +### 1. Evidence-First Development +- **Code Citations Required**: Always cite specific file:line references when making claims +- **Execution Path Tracing**: Trace actual code execution before proposing architectural changes +- **Assumption Validation**: Flag assumptions as "assumed" vs "evidence-based" + +### 2. Code Review Standards +- **Trace Before Proposing**: Always trace execution paths before suggesting changes +- **Evidence Over Inference**: Prefer code citations over logical deductions +- **Scope Validation**: Confirm the actual scope of problems before proposing solutions + +### 3. Problem-Solution Validation +- **Problem Scope**: Does the solution address the actual problem? +- **Evidence Alignment**: Does the solution match the evidence? +- **Complexity Justification**: Is added complexity justified by real needs? +- **Alternative Analysis**: What simpler solutions were considered? + +## Required Workflows + +### Before Proposing Changes +- [ ] **Code Path Tracing**: Map execution flow from entry to exit +- [ ] **Evidence Collection**: Gather specific code citations and logs +- [ ] **Assumption Surfacing**: Identify what's proven vs. inferred +- [ ] **Scope Validation**: Confirm the actual extent of the problem + +### During Solution Design +- [ ] **Evidence Alignment**: Ensure solution addresses proven problems +- [ ] **Complexity Assessment**: Justify any added complexity +- [ ] **Alternative Evaluation**: Consider simpler approaches first +- [ ] **Impact Analysis**: Assess effects on existing systems + +## Software-Specific Competence Hooks + +### Evidence Validation +- **"What code path proves this claim?"** +- **"How does data actually flow through the system?"** +- **"What am I assuming vs. what can I prove?"** + +### Code Tracing +- **"What's the execution path from user action to system response?"** +- **"Which components actually interact in this scenario?"** +- **"Where does the data originate and where does it end up?"** + +### Architecture Decisions +- **"What evidence shows this change is necessary?"** +- **"What simpler solution could achieve the same goal?"** +- **"How does this change affect the existing system architecture?"** + +## Integration with Other Rulesets + +### With base_context.mdc +- Inherits generic competence principles +- Adds software-specific evidence requirements +- Maintains collaboration and learning focus + +### With research_diagnostic.mdc +- Enhances investigation with code path tracing +- Adds evidence validation to diagnostic workflow +- Strengthens problem identification accuracy + +## Usage Guidelines + +### When to Use This Ruleset +- Code reviews and architectural decisions +- Bug investigation and debugging +- Performance optimization +- Feature implementation planning +- Testing strategy development + +### When to Combine with Others +- **base_context + software_development**: General development tasks +- **research_diagnostic + software_development**: Technical investigations +- **All three**: Complex architectural decisions or major refactoring + +## Self-Check (model, before responding) +- [ ] Code path traced and documented +- [ ] Evidence cited with specific file:line references +- [ ] Assumptions clearly flagged as proven vs. inferred +- [ ] Solution complexity justified by evidence +- [ ] Simpler alternatives considered and documented +- [ ] Impact on existing systems assessed +# Software Development Ruleset + +## Purpose +Specialized guidelines for software development tasks including code review, debugging, architecture decisions, and testing. + +## Core Principles + +### 1. Evidence-First Development +- **Code Citations Required**: Always cite specific file:line references when making claims +- **Execution Path Tracing**: Trace actual code execution before proposing architectural changes +- **Assumption Validation**: Flag assumptions as "assumed" vs "evidence-based" + +### 2. Code Review Standards +- **Trace Before Proposing**: Always trace execution paths before suggesting changes +- **Evidence Over Inference**: Prefer code citations over logical deductions +- **Scope Validation**: Confirm the actual scope of problems before proposing solutions + +### 3. Problem-Solution Validation +- **Problem Scope**: Does the solution address the actual problem? +- **Evidence Alignment**: Does the solution match the evidence? +- **Complexity Justification**: Is added complexity justified by real needs? +- **Alternative Analysis**: What simpler solutions were considered? + +## Required Workflows + +### Before Proposing Changes +- [ ] **Code Path Tracing**: Map execution flow from entry to exit +- [ ] **Evidence Collection**: Gather specific code citations and logs +- [ ] **Assumption Surfacing**: Identify what's proven vs. inferred +- [ ] **Scope Validation**: Confirm the actual extent of the problem + +### During Solution Design +- [ ] **Evidence Alignment**: Ensure solution addresses proven problems +- [ ] **Complexity Assessment**: Justify any added complexity +- [ ] **Alternative Evaluation**: Consider simpler approaches first +- [ ] **Impact Analysis**: Assess effects on existing systems + +## Software-Specific Competence Hooks + +### Evidence Validation +- **"What code path proves this claim?"** +- **"How does data actually flow through the system?"** +- **"What am I assuming vs. what can I prove?"** + +### Code Tracing +- **"What's the execution path from user action to system response?"** +- **"Which components actually interact in this scenario?"** +- **"Where does the data originate and where does it end up?"** + +### Architecture Decisions +- **"What evidence shows this change is necessary?"** +- **"What simpler solution could achieve the same goal?"** +- **"How does this change affect the existing system architecture?"** + +## Integration with Other Rulesets + +### With base_context.mdc +- Inherits generic competence principles +- Adds software-specific evidence requirements +- Maintains collaboration and learning focus + +### With research_diagnostic.mdc +- Enhances investigation with code path tracing +- Adds evidence validation to diagnostic workflow +- Strengthens problem identification accuracy + +## Usage Guidelines + +### When to Use This Ruleset +- Code reviews and architectural decisions +- Bug investigation and debugging +- Performance optimization +- Feature implementation planning +- Testing strategy development + +### When to Combine with Others +- **base_context + software_development**: General development tasks +- **research_diagnostic + software_development**: Technical investigations +- **All three**: Complex architectural decisions or major refactoring + +## Self-Check (model, before responding) +- [ ] Code path traced and documented +- [ ] Evidence cited with specific file:line references +- [ ] Assumptions clearly flagged as proven vs. inferred +- [ ] Solution complexity justified by evidence +- [ ] Simpler alternatives considered and documented +- [ ] Impact on existing systems assessed diff --git a/.cursor/rules/version_control.mdc b/.cursor/rules/workflow/version_control.mdc similarity index 100% rename from .cursor/rules/version_control.mdc rename to .cursor/rules/workflow/version_control.mdc diff --git a/.eslintrc.js b/.eslintrc.js index fcd19ebe..43e7fbd5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,7 +30,7 @@ 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": "^_" }] diff --git a/.github/workflows/asset-validation.yml b/.github/workflows/asset-validation.yml new file mode 100644 index 00000000..72cd2be0 --- /dev/null +++ b/.github/workflows/asset-validation.yml @@ -0,0 +1,142 @@ +name: Asset Validation & CI Safeguards + +on: + pull_request: + paths: + - 'resources/**' + - 'config/assets/**' + - 'capacitor-assets.config.json' + - 'capacitor.config.ts' + - 'capacitor.config.json' + push: + branches: [main, develop] + paths: + - 'resources/**' + - 'config/assets/**' + - 'capacitor-assets.config.json' + - 'capacitor.config.ts' + - 'capacitor.config.json' + +jobs: + asset-validation: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Validate asset configuration + run: npm run assets:validate + + - name: Check for committed platform assets (Android) + run: | + if git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)' > /dev/null; then + echo "❌ Android platform assets found in VCS - these should be generated at build-time" + git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)' + exit 1 + fi + echo "✅ No Android platform assets committed" + + - name: Check for committed platform assets (iOS) + run: | + if git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)' > /dev/null; then + echo "❌ iOS platform assets found in VCS - these should be generated at build-time" + git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)' + exit 1 + fi + echo "✅ No iOS platform assets committed" + + - name: Test asset generation + run: | + echo "🧪 Testing asset generation workflow..." + npm run build:capacitor + npx cap sync + npx capacitor-assets generate --dry-run || npx capacitor-assets generate + echo "✅ Asset generation test completed" + + - name: Verify clean tree after build + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "❌ Dirty tree after build - asset configs were modified" + git status + git diff + exit 1 + fi + echo "✅ Build completed with clean tree" + + schema-validation: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Validate schema compliance + run: | + echo "🔍 Validating schema compliance..." + node -e " + const fs = require('fs'); + const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8')); + const schema = JSON.parse(fs.readFileSync('config/assets/schema.json', 'utf8')); + + // Basic schema validation + if (!config.icon || !config.splash) { + throw new Error('Missing required sections: icon and splash'); + } + + if (!config.icon.source || !config.splash.source) { + throw new Error('Missing required source fields'); + } + + if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { + throw new Error('Icon source must be in resources/ directory'); + } + + if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { + throw new Error('Splash source must be in resources/ directory'); + } + + console.log('✅ Schema validation passed'); + " + + - name: Check source file existence + run: | + echo "📁 Checking source file existence..." + node -e " + const fs = require('fs'); + const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8')); + + const requiredFiles = [ + config.icon.source, + config.splash.source + ]; + + if (config.splash.darkSource) { + requiredFiles.push(config.splash.darkSource); + } + + const missingFiles = requiredFiles.filter(file => !fs.existsSync(file)); + + if (missingFiles.length > 0) { + console.error('❌ Missing source files:', missingFiles); + process.exit(1); + } + + console.log('✅ All source files exist'); + " diff --git a/.gitignore b/.gitignore index a9f37e49..4202ef2a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..a9d08739 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.19.0 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a9d08739 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.19.0 diff --git a/README.md b/README.md index 6992ba09..efc9b1ad 100644 --- a/README.md +++ b/README.md @@ -136,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 diff --git a/android/app/src/main/assets/capacitor.config.json b/android/app/src/main/assets/capacitor.config.json index 594ebca3..04ab8d0c 100644 --- a/android/app/src/main/assets/capacitor.config.json +++ b/android/app/src/main/assets/capacitor.config.json @@ -29,7 +29,7 @@ "splashFullScreen": true, "splashImmersive": true }, - "CapacitorSQLite": { + "CapSQLite": { "iosDatabaseLocation": "Library/CapacitorDatabase", "iosIsEncryption": false, "iosBiometric": { diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..79f802e1 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #3F51B5 + #303F9F + #FF4081 + #FFFFFF + diff --git a/assets/README.md b/assets/README.md deleted file mode 100644 index b9272ff0..00000000 --- a/assets/README.md +++ /dev/null @@ -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. diff --git a/capacitor-assets.config.json b/capacitor-assets.config.json index d56533f4..92bd0414 100644 --- a/capacitor-assets.config.json +++ b/capacitor-assets.config.json @@ -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": { + }, + "darkSource": "resources/splash_dark.png", "ios": { - "source": "resources/ios/splash/splash_dark.png", - "target": "ios/App/App/Assets.xcassets/SplashDark.imageset" + "target": "ios/App/App/Assets.xcassets", + "useStoryBoard": true }, - "android": { - "source": "resources/android/splash/splash_dark.png", - "target": "android/app/src/main/res" - } + "source": "resources/splash.png" } -} \ No newline at end of file +} diff --git a/capacitor.config.ts b/capacitor.config.ts new file mode 100644 index 00000000..24ef38c6 --- /dev/null +++ b/capacitor.config.ts @@ -0,0 +1,116 @@ +import { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'app.timesafari', + appName: 'TimeSafari', + webDir: 'dist', + server: { + cleartext: true + }, + plugins: { + App: { + appUrlOpen: { + handlers: [ + { + url: 'timesafari://*', + autoVerify: true + } + ] + } + }, + SplashScreen: { + launchShowDuration: 3000, + launchAutoHide: true, + backgroundColor: '#ffffff', + androidSplashResourceName: 'splash', + androidScaleType: 'CENTER_CROP', + showSpinner: false, + androidSpinnerStyle: 'large', + iosSpinnerStyle: 'small', + spinnerColor: '#999999', + splashFullScreen: true, + splashImmersive: true + }, + CapSQLite: { + iosDatabaseLocation: 'Library/CapacitorDatabase', + iosIsEncryption: false, + iosBiometric: { + biometricAuth: false, + biometricTitle: 'Biometric login for TimeSafari' + }, + androidIsEncryption: false, + androidBiometric: { + biometricAuth: false, + biometricTitle: 'Biometric login for TimeSafari' + }, + electronIsEncryption: false + } + }, + ios: { + contentInset: 'never', + allowsLinkPreview: true, + scrollEnabled: true, + limitsNavigationsToAppBoundDomains: true, + backgroundColor: '#ffffff', + allowNavigation: [ + '*.timesafari.app', + '*.jsdelivr.net', + 'api.endorser.ch' + ] + }, + android: { + allowMixedContent: true, + captureInput: true, + webContentsDebuggingEnabled: false, + allowNavigation: [ + '*.timesafari.app', + '*.jsdelivr.net', + 'api.endorser.ch', + '10.0.2.2:3000' + ] + }, + electron: { + deepLinking: { + schemes: ['timesafari'] + }, + buildOptions: { + appId: 'app.timesafari', + productName: 'TimeSafari', + directories: { + output: 'dist-electron-packages' + }, + files: [ + 'dist/**/*', + 'electron/**/*' + ], + mac: { + category: 'public.app-category.productivity', + target: [ + { + target: 'dmg', + arch: ['x64', 'arm64'] + } + ] + }, + win: { + target: [ + { + target: 'nsis', + arch: ['x64'] + } + ] + }, + linux: { + target: [ + { + target: 'AppImage', + arch: ['x64'] + } + ], + category: 'Utility' + } + } + } +}; + +export default config; diff --git a/config/assets/capacitor-assets.config.json b/config/assets/capacitor-assets.config.json new file mode 100644 index 00000000..92bd0414 --- /dev/null +++ b/config/assets/capacitor-assets.config.json @@ -0,0 +1,32 @@ +{ + "icon": { + "android": { + "adaptive": { + "background": "#121212", + "foreground": "resources/icon.png", + "monochrome": "resources/icon.png" + }, + "target": "android/app/src/main/res" + }, + "ios": { + "padding": 0, + "target": "ios/App/App/Assets.xcassets/AppIcon.appiconset" + }, + "source": "resources/icon.png", + "web": { + "target": "public/img/icons" + } + }, + "splash": { + "android": { + "scale": "cover", + "target": "android/app/src/main/res" + }, + "darkSource": "resources/splash_dark.png", + "ios": { + "target": "ios/App/App/Assets.xcassets", + "useStoryBoard": true + }, + "source": "resources/splash.png" + } +} diff --git a/config/assets/schema.json b/config/assets/schema.json new file mode 100644 index 00000000..89e76c0c --- /dev/null +++ b/config/assets/schema.json @@ -0,0 +1,119 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Capacitor Assets Configuration Schema", + "description": "Schema for validating capacitor-assets configuration files", + "type": "object", + "properties": { + "icon": { + "type": "object", + "properties": { + "source": { + "type": "string", + "pattern": "^resources/.*\\.(png|svg)$", + "description": "Source icon file path relative to project root" + }, + "android": { + "type": "object", + "properties": { + "adaptive": { + "type": "object", + "properties": { + "foreground": { + "type": "string", + "pattern": "^resources/.*\\.(png|svg)$", + "description": "Foreground icon for Android adaptive icons" + }, + "background": { + "type": ["string", "object"], + "description": "Background color or image for adaptive icons" + }, + "monochrome": { + "type": "string", + "pattern": "^resources/.*\\.(png|svg)$", + "description": "Monochrome icon for Android 13+" + } + }, + "required": ["foreground", "background"] + }, + "target": { + "type": "string", + "description": "Android target directory for generated icons" + } + } + }, + "ios": { + "type": "object", + "properties": { + "padding": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Padding ratio for iOS icons" + }, + "target": { + "type": "string", + "description": "iOS target directory for generated icons" + } + } + }, + "web": { + "type": "object", + "properties": { + "target": { + "type": "string", + "description": "Web target directory for generated icons" + } + } + } + }, + "required": ["source"], + "additionalProperties": false + }, + "splash": { + "type": "object", + "properties": { + "source": { + "type": "string", + "pattern": "^resources/.*\\.(png|svg)$", + "description": "Source splash screen file" + }, + "darkSource": { + "type": "string", + "pattern": "^resources/.*\\.(png|svg)$", + "description": "Dark mode splash screen file" + }, + "android": { + "type": "object", + "properties": { + "scale": { + "type": "string", + "enum": ["cover", "contain", "fill"], + "description": "Android splash screen scaling mode" + }, + "target": { + "type": "string", + "description": "Android target directory for splash screens" + } + } + }, + "ios": { + "type": "object", + "properties": { + "useStoryBoard": { + "type": "boolean", + "description": "Use LaunchScreen storyboard instead of splash assets" + }, + "target": { + "type": "string", + "description": "iOS target directory for splash screens" + } + } + } + }, + "required": ["source"], + "additionalProperties": false + } + }, + "required": ["icon", "splash"], + "additionalProperties": false +} diff --git a/doc/asset-migration-plan.md b/doc/asset-migration-plan.md new file mode 100644 index 00000000..3a05353c --- /dev/null +++ b/doc/asset-migration-plan.md @@ -0,0 +1,214 @@ +# TimeSafari Asset Configuration Migration Plan + +**Author**: Matthew Raymer +**Date**: 2025-08-14 +**Status**: 🎯 **IMPLEMENTATION** - Ready for Execution + +## Overview + +This document outlines the migration from the current mixed asset management +system to a standardized, single-source asset configuration approach using +`capacitor-assets` as the standard generator. + +## Current State Analysis + +### Asset Sources (Duplicated) + +- **`assets/` directory**: Contains `icon.png`, `splash.png`, `splash_dark.png` +- **`resources/` directory**: Contains identical files in platform-specific subdirectories +- **Result**: Duplicate storage, confusion about source of truth + +### Asset Generation (Manual) + +- **Custom scripts**: `generate-icons.sh`, `generate-ios-assets.sh`, `generate-android-icons.sh` +- **Bypass capacitor-assets**: Manual ImageMagick-based generation +- **Inconsistent outputs**: Different generation methods for each platform + +### Configuration (Scattered) + +- **`capacitor-assets.config.json`**: Basic configuration at root +- **Platform-specific configs**: Mixed in various build scripts +- **No validation**: No schema or consistency checks + +## Target State + +### Single Source of Truth + +- **`resources/` directory**: Capacitor default location for source assets +- **Eliminate duplication**: Remove `assets/` directory after migration +- **Standardized paths**: All tools read from `resources/` + +### Standardized Generation + +- **`capacitor-assets`**: Single tool for all platform asset generation +- **Build-time generation**: Assets generated during build, not committed +- **Deterministic outputs**: Same inputs → same outputs every time + +### Centralized Configuration + +- **`config/assets/`**: All asset-related configuration files +- **Schema validation**: JSON schema for configuration validation +- **CI safeguards**: Automated validation and compliance checks + +## Migration Steps + +### Phase 1: Foundation Setup ✅ + +- [x] Create `config/assets/` directory structure +- [x] Create asset configuration schema (`schema.json`) +- [x] Create enhanced capacitor-assets configuration +- [x] Convert `capacitor.config.json` to `capacitor.config.ts` +- [x] Pin Node.js version (`.nvmrc`, `.node-version`) +- [x] Create dev-time asset configuration generator +- [x] Create asset configuration validator +- [x] Add npm scripts for asset management +- [x] Update `.gitignore` with proper asset exclusions +- [x] Create CI workflow for asset validation + +### Phase 2: Validation & Testing + +- [ ] Run `npm run assets:config` to generate new configuration +- [ ] Run `npm run assets:validate` to verify configuration +- [ ] Test `npm run build:native` workflow +- [ ] Verify CI workflow passes all checks +- [ ] Confirm no platform assets are committed to VCS + +### Phase 3: Cleanup & Removal + +- [ ] Remove `assets/` directory (after validation) +- [ ] Remove manual asset generation scripts +- [ ] Remove asset checking scripts +- [ ] Update documentation references +- [ ] Final validation of clean state + +## Implementation Details + +### File Structure + +``` +resources/ # Image sources ONLY + icon.png + splash.png + splash_dark.png + +config/assets/ # Versioned config & schema + capacitor-assets.config.json + schema.json + +scripts/ + assets-config.js # Dev-time config generator + assets-validator.js # Schema validator +``` + +### Configuration Schema + +The schema enforces: +- Source files must be in `resources/` directory +- Required fields for icon and splash sections +- Android adaptive icon support (foreground/background/monochrome) +- iOS LaunchScreen preferences +- Target directory validation + +### CI Safeguards + +- **Schema validation**: Configuration must comply with schema +- **Source file validation**: All referenced files must exist +- **Platform asset denial**: Reject commits with generated assets +- **Clean tree enforcement**: Build must not modify committed configs + +## Testing Strategy + +### Local Validation + +```bash +# Generate configuration +npm run assets:config + +# Validate configuration +npm run assets:validate + +# Test build workflow +npm run build:native + +# Clean generated assets +npm run assets:clean +``` + +### CI Validation + +- **Asset validation workflow**: Runs on asset-related changes +- **Schema compliance**: Ensures configuration follows schema +- **Source file existence**: Verifies all referenced files exist +- **Platform asset detection**: Prevents committed generated assets +- **Build tree verification**: Ensures clean tree after build + +## Risk Mitigation + +### Data Loss Prevention + +- **Backup branch**: Create backup before removing `assets/` +- **Validation checks**: Multiple validation steps before removal +- **Gradual migration**: Phase-by-phase approach with rollback capability + +### Build Continuity + +- **Per-platform scripts unchanged**: All existing build orchestration preserved +- **Standard toolchain**: Uses capacitor-assets, not custom scripts +- **Fallback support**: Manual scripts remain until migration complete + +### Configuration Consistency + +- **Schema enforcement**: JSON schema prevents invalid configurations +- **CI validation**: Automated checks catch configuration issues +- **Documentation updates**: Clear guidance for future changes + +## Success Criteria + +### Technical Requirements + +- [ ] Single source of truth in `resources/` directory +- [ ] All platform assets generated via `capacitor-assets` +- [ ] No manual asset generation scripts +- [ ] Configuration validation passes all checks +- [ ] CI workflow enforces asset policies + +### Quality Metrics + +- [ ] Zero duplicate asset sources +- [ ] 100% configuration schema compliance +- [ ] No platform assets committed to VCS +- [ ] Clean build tree after asset generation +- [ ] Deterministic asset outputs + +### User Experience + +- [ ] Clear asset management documentation +- [ ] Simple development commands +- [ ] Consistent asset generation across platforms +- [ ] Reduced confusion about asset sources + +## Next Steps + +1. **Execute Phase 2**: Run validation and testing steps +2. **Verify CI workflow**: Ensure all checks pass +3. **Execute Phase 3**: Remove duplicate assets and scripts +4. **Update documentation**: Finalize README and BUILDING.md +5. **Team training**: Ensure all developers understand new workflow + +## Rollback Plan + +If issues arise during migration: + +1. **Restore backup branch**: `git checkout backup-before-asset-migration` +2. **Revert configuration changes**: Remove new config files +3. **Restore manual scripts**: Re-enable previous asset generation +4. **Investigate issues**: Identify and resolve root causes +5. **Plan revised migration**: Adjust approach based on lessons learned + +--- + +**Status**: Ready for Phase 2 execution +**Priority**: High +**Estimated Effort**: 2-3 hours +**Dependencies**: CI workflow validation +**Stakeholders**: Development team diff --git a/ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json b/ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json new file mode 100644 index 00000000..0b268f6f --- /dev/null +++ b/ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "splash@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "splash@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "splash@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png b/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png new file mode 100644 index 00000000..79fc756e Binary files /dev/null and b/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png differ diff --git a/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png b/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png new file mode 100644 index 00000000..03f44f99 Binary files /dev/null and b/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png differ diff --git a/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png b/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png new file mode 100644 index 00000000..a1dbc775 Binary files /dev/null and b/ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png differ diff --git a/package-lock.json b/package-lock.json index 814a71ad..d6914554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1fcc2a0a..eb68f859 100644 --- a/package.json +++ b/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", @@ -243,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" } diff --git a/playwright.config-local.ts b/playwright.config-local.ts index c0e78047..32b7f023 100644 --- a/playwright.config-local.ts +++ b/playwright.config-local.ts @@ -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'], diff --git a/assets/icon.png b/resources/icon.png similarity index 100% rename from assets/icon.png rename to resources/icon.png diff --git a/assets/splash.png b/resources/splash.png similarity index 100% rename from assets/splash.png rename to resources/splash.png diff --git a/assets/splash_dark.png b/resources/splash_dark.png similarity index 100% rename from assets/splash_dark.png rename to resources/splash_dark.png diff --git a/scripts/assets-config.ts b/scripts/assets-config.ts new file mode 100644 index 00000000..baaadce5 --- /dev/null +++ b/scripts/assets-config.ts @@ -0,0 +1,224 @@ +#!/usr/bin/env tsx + +/** + * TimeSafari Asset Configuration Generator + * Generates capacitor-assets configuration files with deterministic outputs + * Author: Matthew Raymer + * + * Usage: tsx scripts/assets-config.ts + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PROJECT_ROOT = path.dirname(__dirname); + +// TypeScript interfaces for asset configuration +interface AdaptiveIconConfig { + foreground: string; + background: string; + monochrome: string; +} + +interface AndroidIconConfig { + adaptive: AdaptiveIconConfig; + target: string; +} + +interface IOSIconConfig { + padding: number; + target: string; +} + +interface WebIconConfig { + target: string; +} + +interface IconConfig { + source: string; + android: AndroidIconConfig; + ios: IOSIconConfig; + web: WebIconConfig; +} + +interface AndroidSplashConfig { + scale: string; + target: string; +} + +interface IOSSplashConfig { + useStoryBoard: boolean; + target: string; +} + +interface SplashConfig { + source: string; + darkSource: string; + android: AndroidSplashConfig; + ios: IOSSplashConfig; +} + +interface AssetConfig { + icon: IconConfig; + splash: SplashConfig; +} + +/** + * Generate deterministic capacitor-assets configuration + * @returns Sorted, stable configuration object + */ +function generateAssetConfig(): AssetConfig { + const config: AssetConfig = { + icon: { + source: "resources/icon.png", + android: { + adaptive: { + foreground: "resources/icon.png", + background: "#121212", + monochrome: "resources/icon.png" + }, + target: "android/app/src/main/res" + }, + ios: { + padding: 0, + target: "ios/App/App/Assets.xcassets/AppIcon.appiconset" + }, + web: { + target: "public/img/icons" + } + }, + splash: { + source: "resources/splash.png", + darkSource: "resources/splash_dark.png", + android: { + scale: "cover", + target: "android/app/src/main/res" + }, + ios: { + useStoryBoard: true, + target: "ios/App/App/Assets.xcassets" + } + } + }; + + return sortObjectKeys(config); +} + +/** + * Sort object keys recursively for deterministic output + * @param obj - Object to sort + * @returns Object with sorted keys + */ +function sortObjectKeys(obj: any): any { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(sortObjectKeys); + } + + const sorted: any = {}; + Object.keys(obj) + .sort() + .forEach(key => { + sorted[key] = sortObjectKeys(obj[key]); + }); + + return sorted; +} + +/** + * Validate that required source files exist + */ +function validateSourceFiles(): void { + const requiredFiles = [ + 'resources/icon.png', + 'resources/splash.png', + 'resources/splash_dark.png' + ]; + + const missingFiles = requiredFiles.filter(file => { + const filePath = path.join(PROJECT_ROOT, file); + return !fs.existsSync(filePath); + }); + + if (missingFiles.length > 0) { + console.error('❌ Missing required source files:'); + missingFiles.forEach(file => console.error(` ${file}`)); + process.exit(1); + } + + console.log('✅ All required source files found'); +} + +/** + * Write configuration to file with consistent formatting + * @param config - Configuration object + * @param outputPath - Output file path + */ +function writeConfig(config: AssetConfig, outputPath: string): void { + const jsonString = JSON.stringify(config, null, 2); + + // Ensure consistent line endings and no trailing whitespace + const cleanJson = jsonString + .split('\n') + .map(line => line.trimEnd()) + .join('\n') + '\n'; + + fs.writeFileSync(outputPath, cleanJson, 'utf8'); + console.log(`✅ Configuration written to: ${outputPath}`); +} + +/** + * Main execution function + */ +function main(): void { + console.log('🔄 Generating TimeSafari asset configuration...'); + console.log(`📁 Project root: ${PROJECT_ROOT}`); + console.log(`📅 Generated: ${new Date().toISOString()}`); + + try { + // Validate source files exist + validateSourceFiles(); + + // Generate configuration + const config = generateAssetConfig(); + + // Ensure config directory exists + const configDir = path.join(PROJECT_ROOT, 'config', 'assets'); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + + // Write configuration files + const capacitorAssetsConfigPath = path.join(configDir, 'capacitor-assets.config.json'); + writeConfig(config, capacitorAssetsConfigPath); + + // Copy to root for capacitor-assets discovery + const rootConfigPath = path.join(PROJECT_ROOT, 'capacitor-assets.config.json'); + writeConfig(config, rootConfigPath); + + console.log('🎉 Asset configuration generation completed successfully!'); + console.log(''); + console.log('📋 Next steps:'); + console.log(' 1. Review the generated configuration'); + console.log(' 2. Commit the configuration files'); + console.log(' 3. Run "npm run assets:validate" to verify'); + console.log(' 4. Use "npm run build:native" for builds'); + + } catch (error) { + console.error('❌ Configuration generation failed:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export { generateAssetConfig, sortObjectKeys, validateSourceFiles, AssetConfig }; diff --git a/scripts/assets-validator.ts b/scripts/assets-validator.ts new file mode 100644 index 00000000..587a25c7 --- /dev/null +++ b/scripts/assets-validator.ts @@ -0,0 +1,239 @@ +#!/usr/bin/env tsx + +/** + * TimeSafari Asset Configuration Validator + * Validates capacitor-assets configuration against schema and source files + * Author: Matthew Raymer + * + * Usage: tsx scripts/assets-validator.ts [config-path] + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PROJECT_ROOT = path.dirname(__dirname); + +// TypeScript interfaces for validation +interface ValidationError { + message: string; +} + +interface AssetConfig { + icon?: { + source?: string; + android?: { + adaptive?: { + foreground?: string; + background?: string; + monochrome?: string; + }; + }; + }; + splash?: { + source?: string; + darkSource?: string; + }; +} + +/** + * Load and parse JSON file + * @param filePath - Path to JSON file + * @returns Parsed JSON object + */ +function loadJsonFile(filePath: string): AssetConfig { + try { + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } catch (error) { + throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Validate configuration against schema + * @param config - Configuration object to validate + * @returns Array of validation errors + */ +function validateAgainstSchema(config: AssetConfig): ValidationError[] { + const errors: ValidationError[] = []; + + // Basic structure validation + if (!config.icon || !config.splash) { + errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' }); + } + + // Icon validation + if (config.icon) { + if (!config.icon.source) { + errors.push({ message: 'Icon section must contain "source" field' }); + } else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { + errors.push({ message: 'Icon source must be a PNG or SVG file in resources/ directory' }); + } + + // Android adaptive icon validation + if (config.icon.android?.adaptive) { + const adaptive = config.icon.android.adaptive; + if (!adaptive.foreground || !adaptive.background) { + errors.push({ message: 'Android adaptive icon must have both foreground and background' }); + } + if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { + errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' }); + } + } + } + + // Splash validation + if (config.splash) { + if (!config.splash.source) { + errors.push({ message: 'Splash section must contain "source" field' }); + } else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { + errors.push({ message: 'Splash source must be a PNG or SVG file in resources/ directory' }); + } + + if (config.splash.darkSource && !/^resources\/.*\.(png|svg)$/.test(config.splash.darkSource)) { + errors.push({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' }); + } + } + + return errors; +} + +/** + * Validate that source files exist + * @param config - Configuration object + * @returns Array of missing file errors + */ +function validateSourceFiles(config: AssetConfig): ValidationError[] { + const errors: ValidationError[] = []; + const requiredFiles = new Set(); + + // 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(); + + // 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 }; diff --git a/scripts/build-android.sh b/scripts/build-android.sh index 8cb3c1c2..c6c8ae88 100755 --- a/scripts/build-android.sh +++ b/scripts/build-android.sh @@ -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 diff --git a/scripts/check-android-resources.sh b/scripts/check-android-resources.sh deleted file mode 100755 index d328c051..00000000 --- a/scripts/check-android-resources.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/scripts/generate-android-icons.sh b/scripts/generate-android-icons.sh deleted file mode 100755 index 432f2428..00000000 --- a/scripts/generate-android-icons.sh +++ /dev/null @@ -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' - - - - -EOF - -cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_foreground.xml" << 'EOF' - - - - -EOF - -# Update the existing launcher XML files to reference the correct resources -cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher.xml" << 'EOF' - - - - - -EOF - -cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_round.xml" << 'EOF' - - - - - -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!" \ No newline at end of file diff --git a/scripts/generate-icons.sh b/scripts/generate-icons.sh deleted file mode 100755 index bed6fd14..00000000 --- a/scripts/generate-icons.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/scripts/purge-generated-assets.sh b/scripts/purge-generated-assets.sh deleted file mode 100755 index 3be21db6..00000000 --- a/scripts/purge-generated-assets.sh +++ /dev/null @@ -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 " -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" \ No newline at end of file diff --git a/scripts/type-safety-check.sh b/scripts/type-safety-check.sh new file mode 100755 index 00000000..399343df --- /dev/null +++ b/scripts/type-safety-check.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# Type Safety Pre-commit Check Script +# This script ensures type safety before commits by running linting and type checking + +set -e + +echo "🔍 Running Type Safety Pre-commit Checks..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +# Check if we're in the right directory +if [ ! -f "package.json" ]; then + print_error "Must run from project root directory" + exit 1 +fi + +# Step 1: Run ESLint with TypeScript rules +print_status "Running ESLint TypeScript checks..." +if npm run lint > /dev/null 2>&1; then + print_status "ESLint passed - no type safety issues found" +else + print_error "ESLint failed - type safety issues detected" + echo "" + echo "Running lint with details..." + npm run lint + echo "" + print_error "Please fix the above type safety issues before committing" + exit 1 +fi + +# Step 2: Run TypeScript type checking +print_status "Running TypeScript type checking..." +if npm run type-check > /dev/null 2>&1; then + print_status "TypeScript compilation passed" +else + print_error "TypeScript compilation failed" + echo "" + echo "Running type check with details..." + npm run type-check + echo "" + print_error "Please fix the above TypeScript errors before committing" + exit 1 +fi + +# Step 3: Check for any remaining 'any' types +print_status "Scanning for any remaining 'any' types..." +ANY_COUNT=$(grep -r "any" src/ --include="*.ts" --include="*.vue" | grep -v "// eslint-disable" | grep -v "eslint-disable-next-line" | wc -l) + +if [ "$ANY_COUNT" -eq 0 ]; then + print_status "No 'any' types found in source code" +else + print_warning "Found $ANY_COUNT instances of 'any' type usage" + echo "" + echo "Instances found:" + grep -r "any" src/ --include="*.ts" --include="*.vue" | grep -v "// eslint-disable" | grep -v "eslint-disable-next-line" || true + echo "" + print_error "Please replace 'any' types with proper TypeScript types before committing" + exit 1 +fi + +# Step 4: Verify database migration status +print_status "Checking database migration status..." +if grep -r "databaseUtil" src/ --include="*.ts" --include="*.vue" > /dev/null 2>&1; then + print_warning "Found databaseUtil imports - ensure migration is complete" + echo "" + echo "Files with databaseUtil imports:" + grep -r "databaseUtil" src/ --include="*.ts" --include="*.vue" | head -5 || true + echo "" + print_warning "Consider completing database migration to PlatformServiceMixin" +else + print_status "No databaseUtil imports found - migration appears complete" +fi + +# All checks passed +echo "" +print_status "All type safety checks passed! 🎉" +print_status "Your code is ready for commit" +echo "" +echo "📚 Remember to follow the Type Safety Guidelines:" +echo " - docs/typescript-type-safety-guidelines.md" +echo " - Use proper error handling patterns" +echo " - Leverage existing type definitions" +echo " - Run 'npm run lint-fix' for automatic fixes" + +exit 0 diff --git a/src/assets/icons.json b/src/assets/icons.json deleted file mode 100644 index 434421e6..00000000 --- a/src/assets/icons.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/src/components/ActivityListItem.vue b/src/components/ActivityListItem.vue index c68281da..39dfcffa 100644 --- a/src/components/ActivityListItem.vue +++ b/src/components/ActivityListItem.vue @@ -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 || ""}`; } diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index a21be7b2..61bf4753 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -171,6 +171,8 @@ export default class DataExportSection extends Vue { * @throws {Error} If export fails */ public async exportDatabase(): Promise { + // Note that similar code is in ContactsView.vue exportContactData() + if (this.isExporting) { return; // Prevent multiple simultaneous exports } diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 83a8b93d..0b9cd16a 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -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); } diff --git a/src/components/IconRenderer.vue b/src/components/IconRenderer.vue deleted file mode 100644 index 83a0b14c..00000000 --- a/src/components/IconRenderer.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - diff --git a/src/components/UsageLimitsSection.vue b/src/components/UsageLimitsSection.vue index 4eb9d8ef..ed53393d 100644 --- a/src/components/UsageLimitsSection.vue +++ b/src/components/UsageLimitsSection.vue @@ -83,6 +83,7 @@