From 5cf17596533e2db449a1c1e29df1d9bf79badff7 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 29 Jul 2025 19:34:40 +0800 Subject: [PATCH 01/48] Fix: switch to CSS-based text-truncate - Eliminate dependence on arbitrary maxlength for truncation - Ensure truncation is purely visual, and does not touch content --- src/App.vue | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/App.vue b/src/App.vue index 76acc710..0b190521 100644 --- a/src/App.vue +++ b/src/App.vue @@ -27,9 +27,9 @@ v-if="notification.type === 'toast'" class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md" > -
- {{ notification.title }} -

{{ notification.text }}

+
+

{{ notification.title }}

+

{{ notification.text }}

@@ -46,9 +46,9 @@ > -
- {{ notification.title }} -

{{ truncateLongWords(notification.text) }}

+
+

{{ notification.title }}

+

{{ notification.text }}

-
- {{ notification.title }} -

{{ truncateLongWords(notification.text) }}

+
+

{{ notification.title }}

+

{{ notification.text }}

-
- {{ notification.title }} -

{{ truncateLongWords(notification.text) }}

+
+

{{ notification.title }}

+

{{ notification.text }}

-
- {{ notification.title }} -

{{ truncateLongWords(notification.text) }}

+
+

{{ notification.title }}

+

{{ notification.text }}

@@ -110,17 +105,12 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="downloadSettingsContacts" > - - + Download Settings & Contacts @@ -143,17 +133,12 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="compareDatabases" > - - + Compare Databases @@ -162,17 +147,12 @@ class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateAll" > - - + Migrate All @@ -185,10 +165,9 @@ >
-
@@ -207,10 +186,9 @@ >
-
@@ -229,10 +207,7 @@ >
- +

Success

@@ -249,7 +224,7 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="exportComparison" > - + Export Comparison @@ -258,17 +233,12 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="displayDatabases" > - - + Show Previous Data @@ -277,7 +247,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateAccounts" > - + Migrate Accounts @@ -286,7 +256,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateSettings" > - + Migrate Settings @@ -295,7 +265,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateContacts" > - + Migrate Contacts
@@ -316,11 +286,7 @@ >
- +

@@ -357,10 +323,9 @@
- {{ loadingMessage }}
@@ -375,10 +340,7 @@
- +
@@ -398,10 +360,7 @@
- +
@@ -422,10 +381,7 @@
- +
@@ -445,10 +401,7 @@
- +
@@ -469,9 +422,9 @@
-
@@ -492,10 +445,7 @@
- +
@@ -526,9 +476,9 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- Add
@@ -541,9 +491,9 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- Unmodified
- Keep
@@ -677,9 +627,9 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- Add
@@ -692,9 +642,9 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- Modify
- Unmodified
- Keep
@@ -868,9 +818,9 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- Add
@@ -883,9 +833,9 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- Modify
- Unmodified
- Keep
@@ -1067,7 +1017,6 @@ import { Component, Vue } from "vue-facing-decorator"; import { useClipboard } from "@vueuse/core"; import { Router } from "vue-router"; -import IconRenderer from "../components/IconRenderer.vue"; import { compareDatabases, migrateSettings, @@ -1104,9 +1053,6 @@ import { logger } from "../utils/logger"; */ @Component({ name: "DatabaseMigration", - components: { - IconRenderer, - }, }) export default class DatabaseMigration extends Vue { $router!: Router; From 2b6a2d3612ffb9daacd3a518bd9b8c680634f610 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 30 Jul 2025 20:13:09 +0800 Subject: [PATCH 03/48] Delete IconRenderer component --- src/components/IconRenderer.vue | 90 --------------------------------- 1 file changed, 90 deletions(-) delete mode 100644 src/components/IconRenderer.vue 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 @@ - - - From 6007bc34e435b1f0db8fb13f074c8fbe5c812804 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 30 Jul 2025 12:47:55 +0000 Subject: [PATCH 04/48] refactor: centralize QR navigation logic and add export prompt after contact addition - Create QRNavigationService to handle platform-specific QR routing - Remove direct Capacitor imports from ContactsView, ProjectsView, HelpView - Replace duplicated QR routing logic with centralized service calls - Update HelpView template to use platform service methods (isCapacitor, capabilities) - Add export data prompt after successfully adding a contact - Add NOTIFY_EXPORT_DATA_PROMPT notification constant - Implement exportContactData() method with platform service integration - Fix TypeScript compatibility for Vue Router route parameters - Maintain consistent QR navigation behavior across all views Eliminates code duplication and improves platform abstraction by using PlatformService instead of direct Capacitor references. Enhances user experience with automatic export prompts for data backup. --- src/constants/notifications.ts | 6 ++ src/services/QRNavigationService.ts | 99 +++++++++++++++++++++++++++++ src/views/ContactsView.vue | 80 ++++++++++++++++++++--- src/views/HelpView.vue | 22 +++---- src/views/ProjectsView.vue | 12 ++-- 5 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 src/services/QRNavigationService.ts diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 9ffeb31f..8eb1c06f 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -846,6 +846,12 @@ export const NOTIFY_CONTACTS_ADDED = { message: "They were added.", }; +// Used in: ContactsView.vue (addContact method - export data prompt after contact addition) +export const NOTIFY_EXPORT_DATA_PROMPT = { + title: "Export Your Data", + message: "Would you like to export your contact data as a backup?", +}; + // Used in: ContactsView.vue (showCopySelectionsInfo method - info about copying contacts) export const NOTIFY_CONTACT_INFO_COPY = { title: "Info", diff --git a/src/services/QRNavigationService.ts b/src/services/QRNavigationService.ts new file mode 100644 index 00000000..33716c39 --- /dev/null +++ b/src/services/QRNavigationService.ts @@ -0,0 +1,99 @@ +import { PlatformServiceFactory } from "./PlatformServiceFactory"; +import { PlatformService } from "./PlatformService"; +import { logger } from "@/utils/logger"; + +/** + * QR Navigation Service + * + * Handles platform-specific routing logic for QR scanning operations. + * Removes coupling between views and routing logic by centralizing + * navigation decisions based on platform capabilities. + * + * @author Matthew Raymer + */ +export class QRNavigationService { + private static instance: QRNavigationService | null = null; + private platformService: PlatformService; + + private constructor() { + this.platformService = PlatformServiceFactory.getInstance(); + } + + /** + * Get singleton instance of QRNavigationService + */ + public static getInstance(): QRNavigationService { + if (!QRNavigationService.instance) { + QRNavigationService.instance = new QRNavigationService(); + } + return QRNavigationService.instance; + } + + /** + * Get the appropriate QR scanner route based on platform + * + * @returns Object with route name and parameters for QR scanning + */ + public getQRScannerRoute(): { + name: string; + params?: Record; + } { + const isCapacitor = this.platformService.isCapacitor(); + + logger.debug("QR Navigation - Platform detection:", { + isCapacitor, + platform: this.platformService.getCapabilities(), + }); + + if (isCapacitor) { + // Use native scanner on mobile platforms + return { name: "contact-qr-scan-full" }; + } else { + // Use web scanner on other platforms + return { name: "contact-qr" }; + } + } + + /** + * Get the appropriate QR display route based on platform + * + * @returns Object with route name and parameters for QR display + */ + public getQRDisplayRoute(): { + name: string; + params?: Record; + } { + const isCapacitor = this.platformService.isCapacitor(); + + logger.debug("QR Navigation - Display route detection:", { + isCapacitor, + platform: this.platformService.getCapabilities(), + }); + + if (isCapacitor) { + // Use dedicated display view on mobile + return { name: "contact-qr-scan-show" }; + } else { + // Use combined view on web + return { name: "contact-qr" }; + } + } + + /** + * Check if native QR scanning is available on current platform + * + * @returns true if native scanning is available, false otherwise + */ + public isNativeScanningAvailable(): boolean { + return this.platformService.isCapacitor(); + } + + /** + * Get platform capabilities for QR operations + * + * @returns Platform capabilities object + */ + public getPlatformCapabilities() { + return this.platformService.getCapabilities(); + } +} diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 3d5576e7..a6533201 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -127,7 +127,7 @@ import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { useClipboard } from "@vueuse/core"; -import { Capacitor } from "@capacitor/core"; +// Capacitor import removed - using PlatformService instead import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.vue"; @@ -161,13 +161,17 @@ import { GiveSummaryRecord } from "@/interfaces/records"; import { UserInfo } from "@/interfaces/common"; import { VerifiableCredential } from "@/interfaces/claims-result"; import * as libsUtil from "../libs/util"; -import { generateSaveAndActivateIdentity } from "../libs/util"; +import { + generateSaveAndActivateIdentity, + contactsToExportJson, +} from "../libs/util"; import { logger } from "../utils/logger"; // No longer needed - using PlatformServiceMixin methods // import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { APP_SERVER } from "@/constants/app"; +import { QRNavigationService } from "@/services/QRNavigationService"; import { NOTIFY_CONTACT_NO_INFO, NOTIFY_CONTACTS_ADD_ERROR, @@ -193,6 +197,7 @@ import { NOTIFY_REGISTRATION_ERROR_FALLBACK, NOTIFY_REGISTRATION_ERROR_GENERIC, NOTIFY_VISIBILITY_ERROR_FALLBACK, + NOTIFY_EXPORT_DATA_PROMPT, getRegisterPersonSuccessMessage, getVisibilitySuccessMessage, getGivesRetrievalErrorMessage, @@ -780,6 +785,9 @@ export default class ContactsView extends Vue { // Show success notification this.notify.success(addedMessage); + + // Show export data prompt after successful contact addition + await this.showExportDataPrompt(); } catch (err) { this.handleContactAddError(err); } @@ -1243,19 +1251,75 @@ export default class ContactsView extends Vue { /** * Handle QR code button click - route to appropriate scanner - * Uses native scanner on mobile platforms, web scanner otherwise + * Uses QRNavigationService to determine scanner type and route */ - public handleQRCodeClick() { this.$logAndConsole( "[ContactsView] handleQRCodeClick method called", false, ); - if (Capacitor.isNativePlatform()) { - this.$router.push({ name: "contact-qr-scan-full" }); - } else { - this.$router.push({ name: "contact-qr" }); + const qrNavigationService = QRNavigationService.getInstance(); + const route = qrNavigationService.getQRScannerRoute(); + + this.$router.push(route); + } + + /** + * Show export data prompt after adding a contact + * Prompts user to export their contact data as a backup + */ + private async showExportDataPrompt(): Promise { + setTimeout(() => { + this.$notify( + { + group: "modal", + type: "confirm", + title: NOTIFY_EXPORT_DATA_PROMPT.title, + text: NOTIFY_EXPORT_DATA_PROMPT.message, + onYes: async () => { + await this.exportContactData(); + }, + yesText: "Export Data", + onNo: async () => { + // User chose not to export - no action needed + }, + noText: "Not Now", + }, + -1, + ); + }, 1000); // Small delay to ensure success notification is shown first + } + + /** + * Export contact data to JSON file + * Uses platform service to handle platform-specific export logic + */ + private async exportContactData(): Promise { + try { + // Fetch all contacts from database + const allContacts = await this.$contacts(); + + // Convert contacts to export format + const exportData = contactsToExportJson(allContacts); + const jsonStr = JSON.stringify(exportData, null, 2); + + // Generate filename with current date + const today = new Date(); + const dateString = today.toISOString().split("T")[0]; // YYYY-MM-DD format + const fileName = `timesafari-backup-contacts-${dateString}.json`; + + // Use platform service to handle export + await this.platformService.writeAndShareFile(fileName, jsonStr); + + this.notify.success( + "Contact export completed successfully. Check your downloads or share dialog.", + ); + } catch (error) { + logger.error("Export Error:", error); + this.notify.error( + `There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`, + ); } } } diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue index 11505ba3..9b05b7aa 100644 --- a/src/views/HelpView.vue +++ b/src/views/HelpView.vue @@ -565,22 +565,22 @@

What app version is this?

{{ package.version }} ({{ commitHash }})

-
+

Do I have the latest version?

-

+

Check the App Store.

-

+

Check the Play Store.

- Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized. + Sorry, your platform is not recognized.

@@ -592,12 +592,13 @@ import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import { useClipboard } from "@vueuse/core"; -import { Capacitor } from "@capacitor/core"; +// Capacitor import removed - using QRNavigationService instead import * as Package from "../../package.json"; import QuickNav from "../components/QuickNav.vue"; import { APP_SERVER } from "../constants/app"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; +import { QRNavigationService } from "@/services/QRNavigationService"; /** * HelpView.vue - Comprehensive Help System Component @@ -643,7 +644,7 @@ export default class HelpView extends Vue { showVerifiable = false; APP_SERVER = APP_SERVER; - Capacitor = Capacitor; + // Capacitor reference removed - using QRNavigationService instead // Ideally, we put no functionality in here, especially in the setup, // because we never want this page to have a chance of throwing an error. @@ -711,11 +712,10 @@ export default class HelpView extends Vue { * @private */ private handleQRCodeClick(): void { - if (Capacitor.isNativePlatform()) { - this.$router.push({ name: "contact-qr-scan-full" }); - } else { - this.$router.push({ name: "contact-qr" }); - } + const qrNavigationService = QRNavigationService.getInstance(); + const route = qrNavigationService.getQRScannerRoute(); + + this.$router.push(route); } /** diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue index 84db4f02..b2f2b7f4 100644 --- a/src/views/ProjectsView.vue +++ b/src/views/ProjectsView.vue @@ -264,7 +264,7 @@ import { AxiosRequestConfig } from "axios"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; -import { Capacitor } from "@capacitor/core"; +// Capacitor import removed - using QRNavigationService instead import { NotificationIface } from "../constants/app"; import EntityIcon from "../components/EntityIcon.vue"; @@ -281,6 +281,7 @@ import { OnboardPage, iconForUnitCode } from "../libs/util"; import { logger } from "../utils/logger"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; +import { QRNavigationService } from "@/services/QRNavigationService"; import { NOTIFY_NO_ACCOUNT_ERROR, NOTIFY_PROJECT_LOAD_ERROR, @@ -755,11 +756,10 @@ export default class ProjectsView extends Vue { * - Web-based QR interface for browser environments */ private handleQRCodeClick() { - if (Capacitor.isNativePlatform()) { - this.$router.push({ name: "contact-qr-scan-full" }); - } else { - this.$router.push({ name: "contact-qr" }); - } + const qrNavigationService = QRNavigationService.getInstance(); + const route = qrNavigationService.getQRScannerRoute(); + + this.$router.push(route); } /** From 49bf13021f85d21a52b57da7a09dd79d1c27fa97 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Thu, 31 Jul 2025 21:29:45 +0800 Subject: [PATCH 05/48] Removed icons.json --- src/assets/icons.json | 75 ------------------------------------------- 1 file changed, 75 deletions(-) delete mode 100644 src/assets/icons.json 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 From c969c536bf821ab554149919e967c130dd873765 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 11 Aug 2025 17:50:25 +0800 Subject: [PATCH 06/48] Fix: notify getting called before it's initialized - Initialize notify earlier inside created() --- src/views/ClaimView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 51b18573..f594dc9b 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -724,6 +724,8 @@ export default class ClaimView extends Vue { } async created() { + this.notify = createNotifyHelpers(this.$notify); + const settings = await this.$accountSettings(); this.activeDid = settings.activeDid || ""; @@ -754,8 +756,6 @@ export default class ClaimView extends Vue { this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; this.canShare = !!navigator.share; - - this.notify = createNotifyHelpers(this.$notify); } // insert a space before any capital letters except the initial letter From d30597a921bd0275072485f75ea4bcdd1209e1be Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 12 Aug 2025 03:52:46 +0000 Subject: [PATCH 07/48] feat(logging): implement configurable log levels via VITE_LOG_LEVEL Add comprehensive logging configuration system with environment variable support. Environment files now include appropriate log levels per build mode: - Development: debug (maximum visibility) - Production: warn (minimal noise) - Testing: info (balanced output) Includes smart default behavior based on platform and environment, enhanced logger methods for level checking, and comprehensive documentation. All existing logging calls remain backward compatible. Closes logging configuration request --- .env.development | 8 +- .env.production | 3 +- .env.test | 7 +- README.md | 29 +++++++ doc/logging-configuration.md | 117 +++++++++++++++++++++++++++ src/components/DataExportSection.vue | 7 +- src/components/OfferDialog.vue | 6 +- src/utils/logger.ts | 80 +++++++++++++----- 8 files changed, 223 insertions(+), 34 deletions(-) create mode 100644 doc/logging-configuration.md diff --git a/.env.development b/.env.development index 70091e74..44a2ffdf 100644 --- a/.env.development +++ b/.env.development @@ -1,10 +1,14 @@ - # Only the variables that start with VITE_ are seen in the application import.meta.env in Vue. +# Logging Configuration - Development environment gets maximum visibility +VITE_LOG_LEVEL=debug + # iOS doesn't like spaces in the app title. TIME_SAFARI_APP_TITLE="TimeSafari_Dev" VITE_APP_SERVER=http://localhost:8080 -# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production). +# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not + production). + VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 # Using shared server by default to ease setup, which works for shared test users. diff --git a/.env.production b/.env.production index bbf73a08..aef4d6a4 100644 --- a/.env.production +++ b/.env.production @@ -1,6 +1,7 @@ # Only the variables that start with VITE_ are seen in the application import.meta.env in Vue. - +# Logging Configuration - Production environment gets minimal logging for performance +VITE_LOG_LEVEL=warn VITE_APP_SERVER=https://timesafari.app # This is the claim ID for actions in the BVC project. diff --git a/.env.test b/.env.test index a01c323c..5776e66c 100644 --- a/.env.test +++ b/.env.test @@ -1,9 +1,14 @@ # Only the variables that start with VITE_ are seen in the application import.meta.env in Vue. +# Logging Configuration - Test environment gets balanced logging for debugging +VITE_LOG_LEVEL=info + # iOS doesn't like spaces in the app title. TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app -# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production). +# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not + production). + VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch diff --git a/README.md b/README.md index 6d987467..ba8bf89b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,35 @@ See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all plat TimeSafari provides a simple script-based approach to clear the database for development purposes. +## Logging Configuration + +TimeSafari supports configurable logging levels via the `VITE_LOG_LEVEL` environment variable. This allows developers to control console output verbosity without modifying code. + +### Quick Usage + +```bash +# Show only errors +VITE_LOG_LEVEL=error npm run dev + +# Show warnings and errors +VITE_LOG_LEVEL=warn npm run dev + +# Show info, warnings, and errors (default) +VITE_LOG_LEVEL=info npm run dev + +# Show all log levels including debug +VITE_LOG_LEVEL=debug npm run dev +``` + +### Available Levels + +- **`error`**: Critical errors only +- **`warn`**: Warnings and errors (default for production web) +- **`info`**: Info, warnings, and errors (default for development/capacitor) +- **`debug`**: All log levels including verbose debugging + +See [Logging Configuration Guide](doc/logging-configuration.md) for complete details. + ### Quick Usage ```bash # Run the database clearing script diff --git a/doc/logging-configuration.md b/doc/logging-configuration.md new file mode 100644 index 00000000..2ef2a6d2 --- /dev/null +++ b/doc/logging-configuration.md @@ -0,0 +1,117 @@ +# Logging Configuration Guide + +## Overview + +TimeSafari now supports configurable logging levels via the `VITE_LOG_LEVEL` environment variable. This allows developers to control the verbosity of console output without modifying code. + +## Available Log Levels + +| Level | Value | Description | Console Output | +|-------|-------|-------------|----------------| +| `error` | 0 | Errors only | Critical errors only | +| `warn` | 1 | Warnings and errors | Warnings and errors | +| `info` | 2 | Info, warnings, and errors | General information, warnings, and errors | +| `debug` | 3 | All log levels | Verbose debugging information | + +## Environment Variable + +Set the `VITE_LOG_LEVEL` environment variable to control logging: + +```bash +# Show only errors +VITE_LOG_LEVEL=error + +# Show warnings and errors (default for production web) +VITE_LOG_LEVEL=warn + +# Show info, warnings, and errors (default for development/capacitor) +VITE_LOG_LEVEL=info + +# Show all log levels including debug +VITE_LOG_LEVEL=debug +``` + +## Default Behavior by Platform + +The logger automatically selects appropriate default log levels based on your platform and environment: + +- **Production Web**: `warn` (warnings and errors only) +- **Electron**: `error` (errors only - very quiet) +- **Development/Capacitor**: `info` (info and above) + +## Usage Examples + +### Setting Log Level in Development + +```bash +# In your terminal before running the app +export VITE_LOG_LEVEL=debug +npm run dev + +# Or inline +VITE_LOG_LEVEL=debug npm run dev +``` + +### Setting Log Level in Production + +```bash +# For verbose production logging +VITE_LOG_LEVEL=info npm run build:web + +# For minimal production logging +VITE_LOG_LEVEL=warn npm run build:web +``` + +### Programmatic Access + +The logger provides methods to check current configuration: + +```typescript +import { logger } from '@/utils/logger'; + +// Get current log level +const currentLevel = logger.getCurrentLevel(); // 'info' + +// Check if a level is enabled +const debugEnabled = logger.isLevelEnabled('debug'); // false if level < debug + +// Get available levels +const levels = logger.getAvailableLevels(); // ['error', 'warn', 'info', 'debug'] +``` + +## Database Logging + +Database logging continues to work regardless of console log level settings. All log messages are still stored in the database for debugging and audit purposes. + +## Migration Notes + +- **Existing code**: No changes required - logging behavior remains the same +- **New feature**: Use `VITE_LOG_LEVEL` to override default behavior +- **Backward compatible**: All existing logging calls work as before + +## Best Practices + +1. **Development**: Use `VITE_LOG_LEVEL=debug` for maximum visibility +2. **Testing**: Use `VITE_LOG_LEVEL=info` for balanced output +3. **Production**: Use `VITE_LOG_LEVEL=warn` for minimal noise +4. **Debugging**: Temporarily set `VITE_LOG_LEVEL=debug` to troubleshoot issues + +## Troubleshooting + +### No Logs Appearing + +Check your `VITE_LOG_LEVEL` setting: +```bash +echo $VITE_LOG_LEVEL +``` + +### Too Many Logs + +Reduce verbosity by setting a lower log level: +```bash +VITE_LOG_LEVEL=warn +``` + +### Platform-Specific Issues + +Remember that Electron is very quiet by default. Use `VITE_LOG_LEVEL=info` to see more output in Electron builds. diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index ce8ee2fc..a21be7b2 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -187,9 +187,10 @@ export default class DataExportSection extends Vue { const exContact: Contact = R.omit(["contactMethods"], contact); // now add contactMethods as a true array of ContactMethod objects exContact.contactMethods = contact.contactMethods - ? (typeof contact.contactMethods === 'string' && contact.contactMethods.trim() !== '' - ? JSON.parse(contact.contactMethods) - : []) + ? typeof contact.contactMethods === "string" && + contact.contactMethods.trim() !== "" + ? JSON.parse(contact.contactMethods) + : [] : []; return exContact; }); diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index b8b8093f..81664088 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -18,7 +18,7 @@ Raymer */
@@ -152,8 +152,6 @@ export default class OfferDialog extends Vue { }; } - - // ================================================= // COMPONENT METHODS // ================================================= @@ -199,8 +197,6 @@ export default class OfferDialog extends Vue { this.visible = false; } - - /** * Handle amount updates from AmountInput component * @param value - New amount value diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 4b78ed3a..52ae5daa 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -2,9 +2,10 @@ * Enhanced logger with self-contained database logging * * Provides comprehensive logging with console and database output. + * Supports configurable log levels via VITE_LOG_LEVEL environment variable. * * @author Matthew Raymer - * @version 2.0.0 + * @version 2.1.0 * @since 2025-01-25 */ @@ -46,6 +47,42 @@ export function safeStringify(obj: unknown) { const isElectron = process.env.VITE_PLATFORM === "electron"; const isProduction = process.env.NODE_ENV === "production"; +// Log level configuration via environment variable +const LOG_LEVELS = { + error: 0, + warn: 1, + info: 2, + debug: 3, +} as const; + +type LogLevel = keyof typeof LOG_LEVELS; + +// Parse VITE_LOG_LEVEL environment variable +const getLogLevel = (): LogLevel => { + const envLogLevel = process.env.VITE_LOG_LEVEL?.toLowerCase(); + + if (envLogLevel && envLogLevel in LOG_LEVELS) { + return envLogLevel as LogLevel; + } + + // Default log levels based on environment + if (isProduction && !isElectron) { + return "warn"; // Production web: warnings and errors only + } else if (isElectron) { + return "error"; // Electron: errors only + } else { + return "info"; // Development/Capacitor: info and above + } +}; + +const currentLogLevel = getLogLevel(); +const currentLevelValue = LOG_LEVELS[currentLogLevel]; + +// Helper function to check if a log level should be displayed +const shouldLog = (level: LogLevel): boolean => { + return LOG_LEVELS[level] <= currentLevelValue; +}; + // Track initialization state to prevent circular dependencies let isInitializing = true; @@ -105,11 +142,11 @@ async function logToDatabase( } } -// Enhanced logger with self-contained database methods +// Enhanced logger with self-contained database methods and log level control export const logger = { debug: (message: string, ...args: unknown[]) => { - // Debug logs are very verbose - only show in development mode for web - if (!isProduction && !isElectron) { + // Debug logs only show if VITE_LOG_LEVEL allows it + if (shouldLog("debug")) { // eslint-disable-next-line no-console console.debug(message, ...args); } @@ -117,11 +154,8 @@ export const logger = { }, log: (message: string, ...args: unknown[]) => { - // Regular logs - show in development or for capacitor, but quiet for Electron - if ( - (!isProduction && !isElectron) || - process.env.VITE_PLATFORM === "capacitor" - ) { + // Regular logs - show if VITE_LOG_LEVEL allows info level + if (shouldLog("info")) { // eslint-disable-next-line no-console console.log(message, ...args); } @@ -132,11 +166,7 @@ export const logger = { }, info: (message: string, ...args: unknown[]) => { - if ( - process.env.NODE_ENV !== "production" || - process.env.VITE_PLATFORM === "capacitor" || - process.env.VITE_PLATFORM === "electron" - ) { + if (shouldLog("info")) { // eslint-disable-next-line no-console console.info(message, ...args); } @@ -147,8 +177,7 @@ export const logger = { }, warn: (message: string, ...args: unknown[]) => { - // Always show warnings, but for Electron, suppress routine database warnings - if (!isElectron || !message.includes("[CapacitorPlatformService]")) { + if (shouldLog("warn")) { // eslint-disable-next-line no-console console.warn(message, ...args); } @@ -159,9 +188,10 @@ export const logger = { }, error: (message: string, ...args: unknown[]) => { - // Errors will always be logged to console - // eslint-disable-next-line no-console - console.error(message, ...args); + if (shouldLog("error")) { + // eslint-disable-next-line no-console + console.error(message, ...args); + } // Database logging const messageString = safeStringify(message); @@ -175,11 +205,11 @@ export const logger = { }, toConsoleAndDb: async (message: string, isError = false): Promise => { - // Console output - if (isError) { + // Console output based on log level + if (isError && shouldLog("error")) { // eslint-disable-next-line no-console console.error(message); - } else { + } else if (!isError && shouldLog("info")) { // eslint-disable-next-line no-console console.log(message); } @@ -194,6 +224,12 @@ export const logger = { error: (message: string) => logToDatabase(`[${componentName}] ${message}`, "error"), }), + + // Log level information methods + getCurrentLevel: (): LogLevel => currentLogLevel, + getCurrentLevelValue: (): number => currentLevelValue, + isLevelEnabled: (level: LogLevel): boolean => shouldLog(level), + getAvailableLevels: (): LogLevel[] => Object.keys(LOG_LEVELS) as LogLevel[], }; // Function to manually mark initialization as complete From 7df52312bac3cc31f5ba591caef9fa8e57724069 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 12 Aug 2025 06:26:28 +0000 Subject: [PATCH 08/48] fix(build): resolve shell script export error in .env.development Fixed malformed comment in .env.development that was causing "export: production).=: not a valid identifier" error in build scripts. The comment was split across lines, causing the shell to interpret " production)." as a variable assignment. - Removed malformed comment line that was breaking build:web script - Build script now successfully validates environment and starts server - Resolves issue preventing Playwright tests from running properly --- .env.development | 2 +- package-lock.json | 2 +- package.json | 2 +- scripts/build-web.sh | 14 ++++++++------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.env.development b/.env.development index 44a2ffdf..726f3b7a 100644 --- a/.env.development +++ b/.env.development @@ -7,7 +7,7 @@ VITE_LOG_LEVEL=debug TIME_SAFARI_APP_TITLE="TimeSafari_Dev" VITE_APP_SERVER=http://localhost:8080 # This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not - production). + VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 diff --git a/package-lock.json b/package-lock.json index 6d19bd08..3b85f9c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,7 +96,7 @@ }, "devDependencies": { "@capacitor/assets": "^3.0.5", - "@playwright/test": "^1.45.2", + "@playwright/test": "^1.54.2", "@types/dom-webcodecs": "^0.1.7", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", diff --git a/package.json b/package.json index cd34bc17..f4fc0ea4 100644 --- a/package.json +++ b/package.json @@ -204,7 +204,7 @@ }, "devDependencies": { "@capacitor/assets": "^3.0.5", - "@playwright/test": "^1.45.2", + "@playwright/test": "^1.54.2", "@types/dom-webcodecs": "^0.1.7", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", diff --git a/scripts/build-web.sh b/scripts/build-web.sh index c01bd55d..de0710a2 100755 --- a/scripts/build-web.sh +++ b/scripts/build-web.sh @@ -300,18 +300,20 @@ serve_build() { exit 5 fi - # Use a simple HTTP server to serve the build - if command -v python3 &> /dev/null; then + # Use a server that supports SPA routing (serves index.html for all routes) + if command -v npx &> /dev/null; then + log_info "Starting npx serve with SPA support on port 8080..." + npx serve -s dist -l 8080 + elif command -v python3 &> /dev/null; then + log_warn "Python HTTP server doesn't support SPA routing. Routes like /discover, /account will return 404." log_info "Starting Python HTTP server on port 8080..." cd dist && python3 -m http.server 8080 elif command -v python &> /dev/null; then + log_warn "Python HTTP server doesn't support SPA routing. Routes like /discover, /account will return 404." log_info "Starting Python HTTP server on port 8080..." cd dist && python -m SimpleHTTPServer 8080 - elif command -v npx &> /dev/null; then - log_info "Starting npx serve on port 8080..." - npx serve -s dist -l 8080 else - log_error "No suitable HTTP server found. Install Python or npx serve." + log_error "No suitable HTTP server found. Install npx serve or Python." exit 5 fi } From 9196081f3444478c7e49ea4598524ad558512ce3 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 12 Aug 2025 06:56:18 +0000 Subject: [PATCH 09/48] fix(home): resolve nearby filter not refreshing feed view - Fix FeedFilters component missing activeDid context for settings updates - Update reloadFeedOnChange to retrieve actual settings without defaults - Add comprehensive logging throughout feed refresh process for debugging - Ensure filter state changes immediately trigger feed refresh without page reload The issue was caused by FeedFilters component calling $updateSettings() without the activeDid parameter, causing settings to be saved to wrong location. Now properly passes activeDid from HomeView and uses $accountSettings() for accurate user-specific settings retrieval. Closes filter refresh issue where turning ON nearby filter required page reload --- src/components/DataExportSection.vue | 7 +- src/components/FeedFilters.vue | 87 +++++++++++++++++---- src/components/OfferDialog.vue | 6 +- src/views/HomeView.vue | 108 +++++++++++++++++++++++++-- 4 files changed, 180 insertions(+), 28 deletions(-) diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index ce8ee2fc..a21be7b2 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -187,9 +187,10 @@ export default class DataExportSection extends Vue { const exContact: Contact = R.omit(["contactMethods"], contact); // now add contactMethods as a true array of ContactMethod objects exContact.contactMethods = contact.contactMethods - ? (typeof contact.contactMethods === 'string' && contact.contactMethods.trim() !== '' - ? JSON.parse(contact.contactMethods) - : []) + ? typeof contact.contactMethods === "string" && + contact.contactMethods.trim() !== "" + ? JSON.parse(contact.contactMethods) + : [] : []; return exContact; }); diff --git a/src/components/FeedFilters.vue b/src/components/FeedFilters.vue index e0ab2f9e..b188ebfe 100644 --- a/src/components/FeedFilters.vue +++ b/src/components/FeedFilters.vue @@ -119,11 +119,13 @@ export default class FeedFilters extends Vue { isNearby = false; settingChanged = false; visible = false; + activeDid = ""; - async open(onCloseIfChanged: () => void) { + async open(onCloseIfChanged: () => void, activeDid: string) { this.onCloseIfChanged = onCloseIfChanged; + this.activeDid = activeDid; - const settings = await this.$settings(); + const settings = await this.$accountSettings(activeDid); this.hasVisibleDid = !!settings.filterFeedByVisible; this.isNearby = !!settings.filterFeedByNearby; if (settings.searchBoxes && settings.searchBoxes.length > 0) { @@ -137,17 +139,45 @@ export default class FeedFilters extends Vue { async toggleHasVisibleDid() { this.settingChanged = true; this.hasVisibleDid = !this.hasVisibleDid; - await this.$updateSettings({ - filterFeedByVisible: this.hasVisibleDid, - }); + + if (this.activeDid) { + await this.$updateSettings( + { + filterFeedByVisible: this.hasVisibleDid, + }, + this.activeDid, + ); + } else { + await this.$updateSettings({ + filterFeedByVisible: this.hasVisibleDid, + }); + } } async toggleNearby() { this.settingChanged = true; this.isNearby = !this.isNearby; - await this.$updateSettings({ - filterFeedByNearby: this.isNearby, + + console.log("[FeedFilters] ๐Ÿ”„ Toggling nearby filter:", { + newValue: this.isNearby, + settingChanged: this.settingChanged, + activeDid: this.activeDid, }); + + if (this.activeDid) { + await this.$updateSettings( + { + filterFeedByNearby: this.isNearby, + }, + this.activeDid, + ); + } else { + await this.$updateSettings({ + filterFeedByNearby: this.isNearby, + }); + } + + console.log("[FeedFilters] โœ… Nearby filter updated in settings"); } async clearAll() { @@ -155,10 +185,20 @@ export default class FeedFilters extends Vue { this.settingChanged = true; } - await this.$updateSettings({ - filterFeedByNearby: false, - filterFeedByVisible: false, - }); + if (this.activeDid) { + await this.$updateSettings( + { + filterFeedByNearby: false, + filterFeedByVisible: false, + }, + this.activeDid, + ); + } else { + await this.$updateSettings({ + filterFeedByNearby: false, + filterFeedByVisible: false, + }); + } this.hasVisibleDid = false; this.isNearby = false; @@ -169,23 +209,40 @@ export default class FeedFilters extends Vue { this.settingChanged = true; } - await this.$updateSettings({ - filterFeedByNearby: true, - filterFeedByVisible: true, - }); + if (this.activeDid) { + await this.$updateSettings( + { + filterFeedByNearby: true, + filterFeedByVisible: true, + }, + this.activeDid, + ); + } else { + await this.$updateSettings({ + filterFeedByNearby: true, + filterFeedByVisible: true, + }); + } this.hasVisibleDid = true; this.isNearby = true; } close() { + console.log("[FeedFilters] ๐Ÿšช Closing dialog:", { + settingChanged: this.settingChanged, + hasCallback: !!this.onCloseIfChanged, + }); + if (this.settingChanged) { + console.log("[FeedFilters] ๐Ÿ”„ Settings changed, calling callback"); this.onCloseIfChanged(); } this.visible = false; } done() { + console.log("[FeedFilters] โœ… Done button clicked"); this.close(); } } diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index b8b8093f..81664088 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -18,7 +18,7 @@ Raymer */
@@ -152,8 +152,6 @@ export default class OfferDialog extends Vue { }; } - - // ================================================= // COMPONENT METHODS // ================================================= @@ -199,8 +197,6 @@ export default class OfferDialog extends Vue { this.visible = false; } - - /** * Handle amount updates from AmountInput component * @param value - New amount value diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 25bcac3e..98463ae3 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -763,17 +763,34 @@ export default class HomeView extends Vue { * Called by FeedFilters component when filters change */ async reloadFeedOnChange() { - const settings = await this.$accountSettings(this.activeDid, { - filterFeedByVisible: false, - filterFeedByNearby: false, + logger.info("[HomeView] ๐Ÿ”„ reloadFeedOnChange() called - refreshing feed"); + + // Get current settings without overriding with defaults + const settings = await this.$accountSettings(this.activeDid); + + logger.info("[HomeView] ๐Ÿ“Š Current filter settings:", { + filterFeedByVisible: settings.filterFeedByVisible, + filterFeedByNearby: settings.filterFeedByNearby, + searchBoxes: settings.searchBoxes?.length || 0, }); + this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); + logger.info("[HomeView] ๐ŸŽฏ Updated filter states:", { + isFeedFilteredByVisible: this.isFeedFilteredByVisible, + isFeedFilteredByNearby: this.isFeedFilteredByNearby, + isAnyFeedFilterOn: this.isAnyFeedFilterOn, + }); + this.feedData = []; this.feedPreviousOldestId = undefined; + + logger.info("[HomeView] ๐Ÿงน Cleared feed data, calling updateAllFeed()"); await this.updateAllFeed(); + + logger.info("[HomeView] โœ… Feed refresh completed"); } /** @@ -852,6 +869,14 @@ export default class HomeView extends Vue { * - this.feedLastViewedClaimId (via updateFeedLastViewedId) */ async updateAllFeed() { + logger.info("[HomeView] ๐Ÿš€ updateAllFeed() called", { + isFeedLoading: this.isFeedLoading, + currentFeedDataLength: this.feedData.length, + isAnyFeedFilterOn: this.isAnyFeedFilterOn, + isFeedFilteredByVisible: this.isFeedFilteredByVisible, + isFeedFilteredByNearby: this.isFeedFilteredByNearby, + }); + this.isFeedLoading = true; let endOfResults = true; @@ -860,21 +885,37 @@ export default class HomeView extends Vue { this.apiServer, this.feedPreviousOldestId, ); + + logger.info("[HomeView] ๐Ÿ“ก Retrieved gives from API", { + resultsCount: results.data.length, + endOfResults, + }); + if (results.data.length > 0) { endOfResults = false; // gather any contacts that user has blocked from view await this.processFeedResults(results.data); await this.updateFeedLastViewedId(results.data); + + logger.info("[HomeView] ๐Ÿ“ Processed feed results", { + processedCount: this.feedData.length, + }); } } catch (e) { + logger.error("[HomeView] โŒ Error in updateAllFeed:", e); this.handleFeedError(e); } if (this.feedData.length === 0 && !endOfResults) { + logger.info("[HomeView] ๐Ÿ”„ No results after filtering, retrying..."); await this.updateAllFeed(); } this.isFeedLoading = false; + logger.info("[HomeView] โœ… updateAllFeed() completed", { + finalFeedDataLength: this.feedData.length, + isFeedLoading: this.isFeedLoading, + }); } /** @@ -899,12 +940,35 @@ export default class HomeView extends Vue { * @param records Array of feed records to process */ private async processFeedResults(records: GiveSummaryRecord[]) { + logger.info("[HomeView] ๐Ÿ“ Processing feed results:", { + inputRecords: records.length, + currentFilters: { + isAnyFeedFilterOn: this.isAnyFeedFilterOn, + isFeedFilteredByVisible: this.isFeedFilteredByVisible, + isFeedFilteredByNearby: this.isFeedFilteredByNearby, + }, + }); + + let processedCount = 0; + let filteredCount = 0; + for (const record of records) { const processedRecord = await this.processRecord(record); if (processedRecord) { this.feedData.push(processedRecord); + processedCount++; + } else { + filteredCount++; } } + + logger.info("[HomeView] ๐Ÿ“Š Feed processing results:", { + processed: processedCount, + filtered: filteredCount, + total: records.length, + finalFeedLength: this.feedData.length, + }); + this.feedPreviousOldestId = records[records.length - 1].jwtId; } @@ -938,7 +1002,7 @@ export default class HomeView extends Vue { * - this.feedData (via createFeedRecord) * * @param record The record to process - * @returns Processed record with contact info if it passes filters, null otherwise + * @returns Processed record if it passes filters, null otherwise */ private async processRecord( record: GiveSummaryRecord, @@ -948,13 +1012,28 @@ export default class HomeView extends Vue { const recipientDid = this.extractRecipientDid(claim); const fulfillsPlan = await this.getFulfillsPlan(record); + + // Log record details for debugging + logger.debug("[HomeView] ๐Ÿ” Processing record:", { + recordId: record.jwtId, + hasFulfillsPlan: !!fulfillsPlan, + fulfillsPlanHandleId: record.fulfillsPlanHandleId, + filters: { + isAnyFeedFilterOn: this.isAnyFeedFilterOn, + isFeedFilteredByVisible: this.isFeedFilteredByNearby, + isFeedFilteredByNearby: this.isFeedFilteredByNearby, + }, + }); + if (!this.shouldIncludeRecord(record, fulfillsPlan)) { + logger.debug("[HomeView] โŒ Record filtered out:", record.jwtId); return null; } const provider = this.extractProvider(claim); const providedByPlan = await this.getProvidedByPlan(provider); + logger.debug("[HomeView] โœ… Record included:", record.jwtId); return this.createFeedRecord( record, claim, @@ -1103,6 +1182,22 @@ export default class HomeView extends Vue { } } + // Add debug logging for nearby filter + if (this.isFeedFilteredByNearby && record.fulfillsPlanHandleId) { + logger.debug("[HomeView] ๐Ÿ” Nearby filter check:", { + recordId: record.jwtId, + hasFulfillsPlan: !!fulfillsPlan, + hasLocation: !!(fulfillsPlan?.locLat && fulfillsPlan?.locLon), + location: fulfillsPlan + ? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon } + : null, + inSearchBox: fulfillsPlan + ? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon) + : null, + finalResult: anyMatch, + }); + } + return anyMatch; } @@ -1538,7 +1633,10 @@ export default class HomeView extends Vue { * Called by template click handler */ openFeedFilters() { - (this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange); + (this.$refs.feedFilters as FeedFilters).open( + this.reloadFeedOnChange, + this.activeDid, + ); } /** From 1dc534b61fc7384012811d19e979b1337870237a Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 12 Aug 2025 15:09:30 +0800 Subject: [PATCH 10/48] Fix: update element locators - Offer dialog was recently updated to use the component version of inputAmount --- test-playwright/50-record-offer.spec.ts | 2 +- test-playwright/60-new-activity.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test-playwright/50-record-offer.spec.ts b/test-playwright/50-record-offer.spec.ts index f8065498..25134370 100644 --- a/test-playwright/50-record-offer.spec.ts +++ b/test-playwright/50-record-offer.spec.ts @@ -23,7 +23,7 @@ test('Record an offer', async ({ page }) => { await page.locator('button', { hasText: 'Edit' }).isVisible(); // since the 'edit' takes longer to show, wait for that (lest the click miss) await page.getByTestId('offerButton').click(); await page.getByTestId('inputDescription').fill(description); - await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString()); + await page.getByTestId('inputOfferAmount').locator('input').fill(randomNonZeroNumber.toString()); expect(page.getByRole('button', { name: 'Sign & Send' })); await page.getByRole('button', { name: 'Sign & Send' }).click(); await expect(page.getByText('That offer was recorded.')).toBeVisible(); diff --git a/test-playwright/60-new-activity.spec.ts b/test-playwright/60-new-activity.spec.ts index daffbb30..889365f8 100644 --- a/test-playwright/60-new-activity.spec.ts +++ b/test-playwright/60-new-activity.spec.ts @@ -36,7 +36,7 @@ test('New offers for another user', async ({ page }) => { const randomString1 = Math.random().toString(36).substring(2, 5); await page.getByTestId('offerButton').click(); await page.getByTestId('inputDescription').fill(`help of ${randomString1} from #000`); - await page.getByTestId('inputOfferAmount').fill('1'); + await page.getByTestId('inputOfferAmount').locator('input').fill('1'); await page.getByRole('button', { name: 'Sign & Send' }).click(); await expect(page.getByText('That offer was recorded.')).toBeVisible(); await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert @@ -46,7 +46,7 @@ test('New offers for another user', async ({ page }) => { const randomString2 = Math.random().toString(36).substring(2, 5); await page.getByTestId('offerButton').click(); await page.getByTestId('inputDescription').fill(`help of ${randomString2} from #000`); - await page.getByTestId('inputOfferAmount').fill('3'); + await page.getByTestId('inputOfferAmount').locator('input').fill('3'); await page.getByRole('button', { name: 'Sign & Send' }).click(); await expect(page.getByText('That offer was recorded.')).toBeVisible(); await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert From bb357f294aeb3ba2a931fd99cbc64b4d87e205a3 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 12 Aug 2025 08:45:08 +0000 Subject: [PATCH 11/48] refactor(ui): remove unused image cache system and fix TypeScript compilation - Remove imageCache Map that only stored null values - Remove selectedImageData Blob property (never used) - Remove cacheImageData method and related function props - Remove handleImageLoad method from ActivityListItem - Clean up ImageViewer component props - Fix TypeScript compilation by replacing legacy logConsoleAndDb calls - Replace logConsoleAndDb with logger.error in deepLinks service - Images continue to work via direct URL references The image cache system was non-functional and only stored null values. ImageViewer component ignored blob data and only used URLs. Fixed production build failure caused by undefined logConsoleAndDb function. Removing dead code improves maintainability without affecting functionality. --- src/components/ActivityListItem.vue | 16 ---------------- src/components/ImageViewer.vue | 1 - src/services/deepLinks.ts | 10 ++++------ src/views/HomeView.vue | 27 +-------------------------- 4 files changed, 5 insertions(+), 49 deletions(-) diff --git a/src/components/ActivityListItem.vue b/src/components/ActivityListItem.vue index d1525e8d..257259b9 100644 --- a/src/components/ActivityListItem.vue +++ b/src/components/ActivityListItem.vue @@ -73,7 +73,6 @@ class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md" :src="record.image" alt="Activity image" - @load="handleImageLoad(record.image)" />
@@ -272,13 +271,6 @@ export default class ActivityListItem extends Vue { @Prop() isRegistered!: boolean; @Prop() activeDid!: string; - /** - * Function prop for handling image caching - * Called when an image loads successfully, allowing parent to control caching behavior - */ - @Prop({ type: Function, default: () => {} }) - onImageCache!: (imageUrl: string) => void | Promise; - isHiddenDid = isHiddenDid; notify!: ReturnType; $notify!: (notification: any, timeout?: number) => void; @@ -295,14 +287,6 @@ export default class ActivityListItem extends Vue { this.notify.warning(NOTIFY_UNKNOWN_PERSON.message, TIMEOUTS.STANDARD); } - /** - * Handle image load event - call function prop for caching - * Allows parent to control caching behavior and validation - */ - handleImageLoad(imageUrl: string): void { - this.onImageCache(imageUrl); - } - get fetchAmount(): string { const claim = (this.record.fullClaim as any)?.claim || this.record.fullClaim; diff --git a/src/components/ImageViewer.vue b/src/components/ImageViewer.vue index 07486705..3e8e577f 100644 --- a/src/components/ImageViewer.vue +++ b/src/components/ImageViewer.vue @@ -45,7 +45,6 @@ import { logger } from "../utils/logger"; @Component({ emits: ["update:isOpen"] }) export default class ImageViewer extends Vue { @Prop() imageUrl!: string; - @Prop() imageData!: Blob | null; @Prop() isOpen!: boolean; userAgent = new UAParser(); diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index ad97ef46..62ae7b15 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -53,6 +53,7 @@ import { DeepLinkRoute, } from "../interfaces/deepLinks"; import type { DeepLinkError } from "../interfaces/deepLinks"; +import { logger } from "../utils/logger"; // Helper function to extract the first key from a Zod object schema function getFirstKeyFromZodObject( @@ -204,9 +205,8 @@ export class DeepLinkHandler { validatedParams = await schema.parseAsync(params); } catch (error) { // For parameter validation errors, provide specific error feedback - logConsoleAndDb( + logger.error( `[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`, - true, ); await this.router.replace({ name: "deep-link-error", @@ -229,9 +229,8 @@ export class DeepLinkHandler { params: validatedParams, }); } catch (error) { - logConsoleAndDb( + logger.error( `[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`, - true, ); // For parameter validation errors, provide specific error feedback await this.router.replace({ @@ -263,9 +262,8 @@ export class DeepLinkHandler { await this.validateAndRoute(path, sanitizedParams, query); } catch (error) { const deepLinkError = error as DeepLinkError; - logConsoleAndDb( + logger.error( `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`, - true, ); throw { diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 25bcac3e..4af9b5bc 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -234,7 +234,6 @@ Raymer * @version 1.0.0 */ :last-viewed-claim-id="feedLastViewedClaimId" :is-registered="isRegistered" :active-did="activeDid" - :on-image-cache="cacheImageData" @load-claim="onClickLoadClaim" @view-image="openImageViewer" /> @@ -255,11 +254,7 @@ Raymer * @version 1.0.0 */ - + diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 62ae7b15..d8445607 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -179,7 +179,7 @@ export class DeepLinkHandler { const validRoute = routeSchema.parse(path) as DeepLinkRoute; routeName = ROUTE_MAP[validRoute].name; } catch (error) { - console.error(`[DeepLink] Invalid route path: ${path}`); + logger.error(`[DeepLink] Invalid route path: ${path}`); // Redirect to error page with information about the invalid link await this.router.replace({ diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 7ec13c01..a731ee22 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -693,7 +693,8 @@ export class WebPlatformService implements PlatformService { const setClause = keys.map((key) => `${key} = ?`).join(", "); const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`; const params = [...keys.map((key) => settings[key]), did]; - console.log( + // Log update operation for debugging + logger.debug( "[WebPlatformService] updateDidSpecificSettings", sql, JSON.stringify(params, null, 2), diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 892e1ebc..81c2978e 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -61,7 +61,8 @@ /> - +

- + Date: Wed, 13 Aug 2025 09:05:04 -0600 Subject: [PATCH 16/48] doc: Add helpful setup instructions for quick start for new devs. --- BUILDING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index ba5649da..e1e94fcd 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -8,8 +8,10 @@ This guide explains how to build TimeSafari for different platforms using the co ```bash # ๐Ÿ–ฅ๏ธ Web Development -npm run build:web:dev # Start development server with hot reload -npm run build:web:prod # Production build +npm install # setup -- and pkgx.dev `dev` command before this will set environment with npm, etc +npm run build:web:serve -- --test # Start with test endorser server +npm run build:web:dev # Start development server with hot reload with local endorser server +npm run build:web:prod # Production build # ๐Ÿ“ฑ Mobile Development npm run build:ios # iOS build (opens Xcode) @@ -2401,4 +2403,4 @@ All scripts use consistent error handling: --- -**Note**: This documentation is maintained alongside the build system. For the most up-to-date information, refer to the actual script files and Vite configuration files in the repository. \ No newline at end of file +**Note**: This documentation is maintained alongside the build system. For the most up-to-date information, refer to the actual script files and Vite configuration files in the repository. From 24a2dce43e32ef41d849d084acb62551117eaa24 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 13 Aug 2025 11:17:08 -0600 Subject: [PATCH 17/48] chore: Bump version and add "-beta". --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d19bd08..5b40d345 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "1.0.6", + "version": "1.0.7-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "1.0.6", + "version": "1.0.7-beta", "dependencies": { "@capacitor-community/electron": "^5.0.1", "@capacitor-community/sqlite": "6.0.2", diff --git a/package.json b/package.json index cd34bc17..53e3f5b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "1.0.6", + "version": "1.0.7-beta", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From a221a5c5ed4a385c13bd9db68f207f2472a29166 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 13 Aug 2025 12:18:06 -0600 Subject: [PATCH 18/48] feat: Add easier way for test users to register themselves. --- src/libs/endorserServer.ts | 10 +++--- src/router/index.ts | 18 +++++----- src/test/index.ts | 32 ++++++++++++++---- src/views/TestView.vue | 67 ++++++++++++++++++++++++++++++-------- 4 files changed, 92 insertions(+), 35 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index e28b3ac9..735252f7 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1348,12 +1348,12 @@ export async function createEndorserJwtVcFromClaim( } /** - * Create a JWT for a RegisterAction claim. + * Create a JWT for a RegisterAction claim, used for registrations & invites. * * @param activeDid - The DID of the user creating the invite - * @param contact - The contact to register, with a 'did' field (all optional for invites) - * @param identifier - The identifier for the invite, usually random - * @param expiresIn - The number of seconds until the invite expires + * @param contact - Optional - The contact to register, with a 'did' field (all optional for invites) + * @param identifier - Optional - The identifier for the invite, usually random + * @param expiresIn - Optional - The number of seconds until the invite expires * @returns The JWT for the RegisterAction claim */ export async function createInviteJwt( @@ -1367,7 +1367,7 @@ export async function createInviteJwt( "@type": "RegisterAction", agent: { identifier: activeDid }, object: SERVICE_ID, - identifier: identifier, + identifier: identifier, // not sent if undefined }; if (contact?.did) { vcClaim.participant = { identifier: contact.did }; diff --git a/src/router/index.ts b/src/router/index.ts index 7d1e738c..043d3d0c 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -82,6 +82,15 @@ const routes: Array = [ name: "database-migration", component: () => import("../views/DatabaseMigration.vue"), }, + { + path: "/deep-link-error", + name: "deep-link-error", + component: () => import("../views/DeepLinkErrorView.vue"), + meta: { + title: "Invalid Deep Link", + requiresAuth: false, + }, + }, { path: "/did/:did?", name: "did", @@ -276,15 +285,6 @@ const routes: Array = [ name: "user-profile", component: () => import("../views/UserProfileView.vue"), }, - { - path: "/deep-link-error", - name: "deep-link-error", - component: () => import("../views/DeepLinkErrorView.vue"), - meta: { - title: "Invalid Deep Link", - requiresAuth: false, - }, - }, ]; const isElectron = window.location.protocol === "file:"; diff --git a/src/test/index.ts b/src/test/index.ts index 28b0e349..8974713b 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,9 +1,29 @@ import axios from "axios"; import * as didJwt from "did-jwt"; import { SERVICE_ID } from "../libs/endorserServer"; -import { deriveAddress, newIdentifier } from "../libs/crypto"; +import { + DEFAULT_ROOT_DERIVATION_PATH, + deriveAddress, + newIdentifier, +} from "../libs/crypto"; import { logger } from "../utils/logger"; import { AppString } from "../constants/app"; +import { saveNewIdentity } from "@/libs/util"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; + +const TEST_USER_0_MNEMONIC = + "rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage"; + +export async function testBecomeUser0() { + const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC); + + const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); + await saveNewIdentity(identity0, TEST_USER_0_MNEMONIC, DEFAULT_ROOT_DERIVATION_PATH); + const platformService = await PlatformServiceFactory.getInstance(); + await platformService.updateDidSpecificSettings(identity0.did, { + isRegistered: true, + }); +} /** * Get User #0 to sign & submit a RegisterAction for the user's activeDid. @@ -15,10 +35,7 @@ import { AppString } from "../constants/app"; * @throws Error if registration fails or database access fails */ export async function testServerRegisterUser() { - const testUser0Mnem = - "seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control"; - - const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem); + const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC); const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); @@ -32,9 +49,9 @@ export async function testServerRegisterUser() { const vcClaim = { "@context": "https://schema.org", "@type": "RegisterAction", - agent: { did: identity0.did }, + agent: { identifier: identity0.did }, object: SERVICE_ID, - participant: { did: settings.activeDid }, + participant: { identifier: settings.activeDid }, }; // Make a payload for the claim @@ -71,4 +88,5 @@ export async function testServerRegisterUser() { const resp = await axios.post(url, payload, { headers }); logger.log("User registration result:", resp); + return resp; } diff --git a/src/views/TestView.vue b/src/views/TestView.vue index a7592afa..df879ad2 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -21,7 +21,17 @@

-
+
+

User Registration

+ + +
+ +

Notiwind Alerts

@@ -99,7 +109,7 @@
Register Passkey -
@@ -235,6 +245,7 @@ import { registerAndSavePasskey, SHARED_PHOTO_BASE64_KEY, } from "../libs/util"; +import { testBecomeUser0, testServerRegisterUser } from "@/test"; import { logger } from "../utils/logger"; import { Account } from "../db/tables/accounts"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; @@ -300,6 +311,7 @@ export default class Help extends Vue { // for passkeys credIdHex?: string; activeDid?: string; + apiServer?: string; jwt?: string; peerSetup?: PeerSetup; userName?: string; @@ -521,17 +533,6 @@ export default class Help extends Vue { ]; } - /** - * Method to trigger notification test - * Centralizes notification testing logic - */ - triggerTestNotification(config: { - notification: NotificationIface; - timeout?: number; - }) { - this.$notify(config.notification, config.timeout); - } - /** * Component initialization * @@ -541,6 +542,7 @@ export default class Help extends Vue { async mounted() { const settings = await this.$accountSettings(); this.activeDid = settings.activeDid || ""; + this.apiServer = settings.apiServer || ""; this.userName = settings.firstName; const account = await retrieveAccountMetadata(this.activeDid); @@ -553,6 +555,43 @@ export default class Help extends Vue { } } + /** + * Checks if running on production server + * + * @returns True if not on production server (enables test utilities) + */ + public isNotProdServer() { + return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER; + } + + async registerMe() { + const response = await testServerRegisterUser(); + if (response.status === 201) { + alert("Registration successful."); + this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change + } else { + logger.error("Registration failure response:", response); + alert("Registration failed: " + (response.data.error || response.data)); + } + } + + async becomeUser0() { + await testBecomeUser0(); + alert("You are now User 0."); + this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change + } + + /** + * Method to trigger notification test + * Centralizes notification testing logic + */ + triggerTestNotification(config: { + notification: NotificationIface; + timeout?: number; + }) { + this.$notify(config.notification, config.timeout); + } + /** * Handles file upload for image sharing tests * @@ -609,7 +648,7 @@ export default class Help extends Vue { * Includes validation and user confirmation workflow * Uses notification helpers for consistent messaging */ - public async register() { + public async registerPasskey() { const DEFAULT_USERNAME = AppString.APP_NAME + " Tester"; if (!this.userName) { const modalConfig = createPasskeyNameModal( From 81096a8bee0a2be936e8e90fae5e30efb401b73f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 13 Aug 2025 18:59:00 -0600 Subject: [PATCH 19/48] doc: Add instructions to become test user, and other README refactors. --- README.md | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 6d987467..59290d4d 100644 --- a/README.md +++ b/README.md @@ -3,36 +3,9 @@ [Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude and expand to crowd-fund with time & money, then record and see the impact of contributions. -## Database Migration Status - -**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place. - -### Migration Progress -- โœ… **SQLite Database Service**: Fully implemented with absurd-sql -- โœ… **Platform Service Layer**: Unified database interface across platforms -- โœ… **Settings Migration**: Core user settings transferred -- โœ… **Account Migration**: Identity and key management -- ๐Ÿ”„ **Contact Migration**: User contact data (via import interface) -- ๐Ÿ“‹ **Code Cleanup**: Remove unused Dexie imports - -### Migration Fence -The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details. - -**Key Points**: -- Legacy Dexie database is disabled by default -- All database operations go through `PlatformServiceMixin` -- Migration tools provide controlled access to both databases -- Clear separation between legacy and new code - -### Migration Documentation -- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process -- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules -- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools - ## Roadmap -See [project.task.yaml](project.task.yaml) for current priorities. -(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.) +See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e2ba0) for current priorities. ## Setup & Building @@ -42,14 +15,16 @@ Quick start: ```bash npm install -npm run dev +npm run build:web:serve -- --test ``` +To be able to make submissions: go to "profile" (bottom left), go to the bottom and expand "Show Advanced Settings", go to the bottom and to the "Test Page", and finally "Become User 0" to see all the functionality. + See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker). ## Development Database Clearing -TimeSafari provides a simple script-based approach to clear the database for development purposes. +TimeSafari provides a simple script-based approach to clear the local database (not the claim server) for development purposes. ### Quick Usage ```bash @@ -126,7 +101,6 @@ const apiUrl = `${APP_SERVER}/api/claim/123`; ### Documentation -- [Domain Configuration System](docs/domain-configuration.md) - Complete guide - [Constants and Configuration](src/constants/app.ts) - Core constants ## Tests From a2840675221c55344c3ccb459d8ea982910f1ada Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 14 Aug 2025 07:22:26 +0000 Subject: [PATCH 20/48] feat(assets): standardize asset configuration with capacitor-assets - Replace manual ImageMagick scripts with official capacitor-assets toolchain - Consolidate duplicate asset sources to single resources/ directory - Implement comprehensive asset configuration schema and validation - Add CI safeguards for asset validation and platform asset detection - Convert capacitor.config.json to TypeScript format - Pin Node.js version for deterministic builds - Remove legacy manual asset generation scripts: * generate-icons.sh, generate-ios-assets.sh, generate-android-icons.sh * check-android-resources.sh, check-ios-resources.sh * purge-generated-assets.sh - Add new asset management commands: * assets:config - generate/update configurations * assets:validate - validate configurations * assets:clean - clean generated assets (dev only) * build:native - build with asset generation - Create GitHub Actions workflow for asset validation - Update documentation with new asset management workflow This standardization eliminates asset duplication, improves build reliability, and provides a maintainable asset management system using Capacitor defaults. Breaking Changes: Manual asset generation scripts removed Migration: Assets now sourced from resources/ directory only CI: Automated validation prevents committed platform assets --- .cursor/rules/asset_configuration.mdc | 32 ++ .github/workflows/asset-validation.yml | 142 +++++++++ .gitignore | 12 + .node-version | 1 + .nvmrc | 1 + README.md | 45 ++- .../app/src/main/assets/capacitor.config.json | 2 +- assets/README.md | 2 - capacitor-assets.config.json | 38 +-- capacitor.config.ts | 116 +++++++ config/assets/capacitor-assets.config.json | 32 ++ config/assets/schema.json | 119 +++++++ doc/asset-migration-plan.md | 214 +++++++++++++ package.json | 4 + {assets => resources}/icon.png | Bin {assets => resources}/splash.png | Bin {assets => resources}/splash_dark.png | Bin scripts/assets-config.js | 174 +++++++++++ scripts/assets-validator.js | 218 +++++++++++++ scripts/check-android-resources.sh | 159 ---------- scripts/check-ios-resources.sh | 294 ------------------ scripts/generate-android-icons.sh | 107 ------- scripts/generate-icons.sh | 79 ----- scripts/generate-ios-assets.sh | 253 --------------- scripts/purge-generated-assets.sh | 67 ---- 25 files changed, 1125 insertions(+), 986 deletions(-) create mode 100644 .cursor/rules/asset_configuration.mdc create mode 100644 .github/workflows/asset-validation.yml create mode 100644 .node-version create mode 100644 .nvmrc delete mode 100644 assets/README.md create mode 100644 capacitor.config.ts create mode 100644 config/assets/capacitor-assets.config.json create mode 100644 config/assets/schema.json create mode 100644 doc/asset-migration-plan.md rename {assets => resources}/icon.png (100%) rename {assets => resources}/splash.png (100%) rename {assets => resources}/splash_dark.png (100%) create mode 100644 scripts/assets-config.js create mode 100644 scripts/assets-validator.js delete mode 100755 scripts/check-android-resources.sh delete mode 100755 scripts/check-ios-resources.sh delete mode 100755 scripts/generate-android-icons.sh delete mode 100755 scripts/generate-icons.sh delete mode 100755 scripts/generate-ios-assets.sh delete mode 100755 scripts/purge-generated-assets.sh 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/.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/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..eb12403e --- /dev/null +++ b/config/assets/capacitor-assets.config.json @@ -0,0 +1,32 @@ +{ + "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" + } + } +} 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/package.json b/package.json index 7b27258d..930cc8cf 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,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": "node scripts/assets-config.js", + "assets:validate": "node scripts/assets-validator.js", + "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", 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.js b/scripts/assets-config.js new file mode 100644 index 00000000..926ff1fa --- /dev/null +++ b/scripts/assets-config.js @@ -0,0 +1,174 @@ +#!/usr/bin/env node + +/** + * TimeSafari Asset Configuration Generator + * Generates capacitor-assets configuration files with deterministic outputs + * Author: Matthew Raymer + * + * Usage: node scripts/assets-config.js + */ + +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); + +/** + * Generate deterministic capacitor-assets configuration + * @returns {Object} Sorted, stable configuration object + */ +function generateAssetConfig() { + const config = { + 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 {Object} obj - Object to sort + * @returns {Object} Object with sorted keys + */ +function sortObjectKeys(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(sortObjectKeys); + } + + const sorted = {}; + Object.keys(obj) + .sort() + .forEach(key => { + sorted[key] = sortObjectKeys(obj[key]); + }); + + return sorted; +} + +/** + * Validate that required source files exist + */ +function validateSourceFiles() { + 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 {Object} config - Configuration object + * @param {string} outputPath - Output file path + */ +function writeConfig(config, outputPath) { + 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() { + 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.message); + process.exit(1); + } +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export { generateAssetConfig, sortObjectKeys, validateSourceFiles }; diff --git a/scripts/assets-validator.js b/scripts/assets-validator.js new file mode 100644 index 00000000..fcdbcdff --- /dev/null +++ b/scripts/assets-validator.js @@ -0,0 +1,218 @@ +#!/usr/bin/env node + +/** + * TimeSafari Asset Configuration Validator + * Validates capacitor-assets configuration against schema and source files + * Author: Matthew Raymer + * + * Usage: node scripts/assets-validator.js [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); + +/** + * Load and parse JSON file + * @param {string} filePath - Path to JSON file + * @returns {Object} Parsed JSON object + */ +function loadJsonFile(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } catch (error) { + throw new Error(`Failed to load ${filePath}: ${error.message}`); + } +} + +/** + * Validate configuration against schema + * @param {Object} config - Configuration object to validate + * @param {Object} schema - JSON schema for validation + * @returns {Array} Array of validation errors + */ +function validateAgainstSchema(config, schema) { + const errors = []; + + // Basic structure validation + if (!config.icon || !config.splash) { + errors.push('Configuration must contain both "icon" and "splash" sections'); + } + + // Icon validation + if (config.icon) { + if (!config.icon.source) { + errors.push('Icon section must contain "source" field'); + } else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { + errors.push('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('Android adaptive icon must have both foreground and background'); + } + if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { + errors.push('Android adaptive foreground must be a PNG or SVG file in resources/ directory'); + } + } + } + + // Splash validation + if (config.splash) { + if (!config.splash.source) { + errors.push('Splash section must contain "source" field'); + } else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { + errors.push('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('Dark splash source must be a PNG or SVG file in resources/ directory'); + } + } + + return errors; +} + +/** + * Validate that source files exist + * @param {Object} config - Configuration object + * @returns {Array} Array of missing file errors + */ +function validateSourceFiles(config) { + const errors = []; + 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(`Source file not found: ${file}`); + } + }); + + return errors; +} + +/** + * Validate target directories are writable + * @param {Object} config - Configuration object + * @returns {Array} Array of directory validation errors + */ +function validateTargetDirectories(config) { + const errors = []; + 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(`Parent directory does not exist: ${parentDir}`); + } else if (!fs.statSync(parentDir).isDirectory()) { + errors.push(`Parent path is not a directory: ${parentDir}`); + } + }); + + return errors; +} + +/** + * Main validation function + * @param {string} configPath - Path to configuration file + * @returns {boolean} True if validation passes + */ +function validateConfiguration(configPath) { + 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, schema); + 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}`); + }); + return false; + } + + } catch (error) { + console.error('โŒ Validation failed:', error.message); + return false; + } +} + +/** + * Main execution function + */ +function main() { + 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: node scripts/assets-validator.js 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 }; diff --git a/scripts/check-android-resources.sh b/scripts/check-android-resources.sh deleted file mode 100755 index cd994088..00000000 --- a/scripts/check-android-resources.sh +++ /dev/null @@ -1,159 +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..." -# Ensure drawable directory exists -if [ ! -d "$ANDROID_RES_DIR/drawable" ]; then - echo "[FIX] Creating drawable directory..." - mkdir -p "$ANDROID_RES_DIR/drawable" - fixes_applied=$((fixes_applied + 1)) -fi - -# 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..." -# Ensure mipmap directories exist -mipmap_dirs=("mdpi" "hdpi" "xhdpi" "xxhdpi" "xxxhdpi" "anydpi-v26") -for dir in "${mipmap_dirs[@]}"; do - if [ ! -d "$ANDROID_RES_DIR/mipmap-$dir" ]; then - echo "[FIX] Creating mipmap-$dir directory..." - mkdir -p "$ANDROID_RES_DIR/mipmap-$dir" - fixes_applied=$((fixes_applied + 1)) - fi -done - -# 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/check-ios-resources.sh b/scripts/check-ios-resources.sh deleted file mode 100755 index 70466fb2..00000000 --- a/scripts/check-ios-resources.sh +++ /dev/null @@ -1,294 +0,0 @@ -#!/bin/bash - -# TimeSafari iOS Resource Check Script -# Checks for missing iOS resources and automatically fixes common issues -# Author: Matthew Raymer - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -IOS_ASSETS_DIR="$PROJECT_ROOT/ios/App/App/Assets.xcassets" -RESOURCES_DIR="$PROJECT_ROOT/resources/ios" - -echo "=== TimeSafari iOS Resource Check ===" -echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Checking iOS 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 iOS asset catalog structure..." -# Check if Assets.xcassets directory exists -if ! check_directory "$IOS_ASSETS_DIR" "iOS Assets.xcassets directory"; then - echo "[FIX] Creating iOS Assets.xcassets directory..." - mkdir -p "$IOS_ASSETS_DIR" - fixes_applied=$((fixes_applied + 1)) -fi - -# Check main Contents.json -if ! check_file "$IOS_ASSETS_DIR/Contents.json" "Main Assets.xcassets Contents.json"; then - echo "[FIX] Creating main Assets.xcassets Contents.json..." - cat > "$IOS_ASSETS_DIR/Contents.json" << 'EOF' -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} -EOF - fixes_applied=$((fixes_applied + 1)) -fi - -echo "[INFO] Checking App Icon resources..." -# Check App Icon directory -if ! check_directory "$IOS_ASSETS_DIR/AppIcon.appiconset" "App Icon directory"; then - echo "[FIX] Creating App Icon directory..." - mkdir -p "$IOS_ASSETS_DIR/AppIcon.appiconset" - fixes_applied=$((fixes_applied + 1)) -fi - -# Check App Icon Contents.json -if ! check_file "$IOS_ASSETS_DIR/AppIcon.appiconset/Contents.json" "App Icon Contents.json"; then - echo "[FIX] Creating App Icon Contents.json..." - cat > "$IOS_ASSETS_DIR/AppIcon.appiconset/Contents.json" << 'EOF' -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} -EOF - fixes_applied=$((fixes_applied + 1)) -fi - -echo "[INFO] Checking Splash Screen resources..." -# Check Splash directory -if ! check_directory "$IOS_ASSETS_DIR/Splash.imageset" "Splash screen directory"; then - echo "[FIX] Creating Splash screen directory..." - mkdir -p "$IOS_ASSETS_DIR/Splash.imageset" - fixes_applied=$((fixes_applied + 1)) -fi - -# Check Splash Contents.json -if ! check_file "$IOS_ASSETS_DIR/Splash.imageset/Contents.json" "Splash screen Contents.json"; then - echo "[FIX] Creating Splash screen Contents.json..." - cat > "$IOS_ASSETS_DIR/Splash.imageset/Contents.json" << 'EOF' -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} -EOF - fixes_applied=$((fixes_applied + 1)) -fi - -# Check SplashDark directory -if ! check_directory "$IOS_ASSETS_DIR/SplashDark.imageset" "Dark splash screen directory"; then - echo "[FIX] Creating Dark splash screen directory..." - mkdir -p "$IOS_ASSETS_DIR/SplashDark.imageset" - fixes_applied=$((fixes_applied + 1)) -fi - -# Check SplashDark Contents.json -if ! check_file "$IOS_ASSETS_DIR/SplashDark.imageset/Contents.json" "Dark splash screen Contents.json"; then - echo "[FIX] Creating Dark splash screen Contents.json..." - cat > "$IOS_ASSETS_DIR/SplashDark.imageset/Contents.json" << 'EOF' -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} -EOF - fixes_applied=$((fixes_applied + 1)) -fi - -echo "[INFO] Checking source resource files..." -# Check if source resources exist -if ! check_file "$RESOURCES_DIR/icon/icon.png" "iOS icon source"; then - issues_found=$((issues_found + 1)) -fi - -if ! check_file "$RESOURCES_DIR/splash/splash.png" "iOS splash source"; then - issues_found=$((issues_found + 1)) -fi - -if ! check_file "$RESOURCES_DIR/splash/splash_dark.png" "iOS dark splash source"; then - issues_found=$((issues_found + 1)) -fi - -echo "[INFO] Checking iOS platform status..." -# Check if iOS platform is properly initialized -if [ ! -d "$PROJECT_ROOT/ios" ]; then - echo "[ERROR] iOS platform directory not found" - issues_found=$((issues_found + 1)) -elif [ ! -f "$PROJECT_ROOT/ios/App/App/Info.plist" ]; then - echo "[ERROR] Info.plist not found - platform may be corrupted" - issues_found=$((issues_found + 1)) -else - echo "[โœ“] iOS platform appears to be properly initialized" -fi - -# Summary -echo "" -echo "=== iOS Resource Check Summary ===" -if [ $issues_found -eq 0 ] && [ $fixes_applied -eq 0 ]; then - echo "[SUCCESS] All iOS 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" - echo "[NOTE] iOS builds require macOS with Xcode - cannot build on Linux" - exit 1 - else - exit 0 - fi -else - echo "[ERROR] Found $issues_found resource issues that require manual attention" - echo "[NOTE] iOS builds require macOS with Xcode - cannot build on Linux" - exit 1 -fi diff --git a/scripts/generate-android-icons.sh b/scripts/generate-android-icons.sh deleted file mode 100755 index aa2775ca..00000000 --- a/scripts/generate-android-icons.sh +++ /dev/null @@ -1,107 +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 and determine the correct command -IMAGEMAGICK_CMD="" -if command -v magick &> /dev/null; then - IMAGEMAGICK_CMD="magick" - echo "[INFO] Using ImageMagick v7+ (magick command)" -elif command -v convert &> /dev/null; then - IMAGEMAGICK_CMD="convert" - echo "[INFO] Using ImageMagick v6 (convert command)" -else - echo "[ERROR] ImageMagick 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" -mkdir -p "$ANDROID_RES_DIR/mipmap-anydpi-v26" - -echo "[INFO] Generating launcher icons..." - -# Function to resize image using the appropriate ImageMagick command -resize_image() { - local input="$1" - local output="$2" - local size="$3" - - if [ "$IMAGEMAGICK_CMD" = "magick" ]; then - # ImageMagick v7+ syntax - magick "$input" -resize "${size}x${size}" "$output" - else - # ImageMagick v6 syntax - convert "$input" -resize "${size}x${size}" "$output" - fi -} - -# Generate launcher icons for different densities -# Android launcher icon sizes: mdpi=48, hdpi=72, xhdpi=96, xxhdpi=144, xxxhdpi=192 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png" 48 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png" 72 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png" 96 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png" 144 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png" 192 - -# Generate round launcher icons -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png" 48 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png" 72 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png" 96 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png" 144 -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png" 192 - -# Create simple launcher XML files (no adaptive icons for now) -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 - -# Create foreground mipmap files for adaptive icons -resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_foreground.png" 108 - -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 " - mipmap-anydpi-v26/ic_launcher_foreground.png (108x108)" -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 3f0062ed..00000000 --- a/scripts/generate-icons.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -# TimeSafari Android Icon Generation Script (Placeholder Icons) -# Generates placeholder Android launcher icons with "TS" text -# 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" - -echo "=== TimeSafari Android Placeholder Icon Generation ===" -echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting Android placeholder icon generation" - -# Check if ImageMagick is available and determine the correct command -IMAGEMAGICK_CMD="" -if command -v magick &> /dev/null; then - IMAGEMAGICK_CMD="magick" - echo "[INFO] Using ImageMagick v7+ (magick command)" -elif command -v convert &> /dev/null; then - IMAGEMAGICK_CMD="convert" - echo "[INFO] Using ImageMagick v6 (convert command)" -else - echo "[ERROR] ImageMagick 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 directories if they don't exist -mkdir -p "$ANDROID_RES_DIR/mipmap-mdpi" -mkdir -p "$ANDROID_RES_DIR/mipmap-hdpi" -mkdir -p "$ANDROID_RES_DIR/mipmap-xhdpi" -mkdir -p "$ANDROID_RES_DIR/mipmap-xxhdpi" -mkdir -p "$ANDROID_RES_DIR/mipmap-xxxhdpi" - -# Function to generate placeholder icon using the appropriate ImageMagick command -generate_placeholder_icon() { - local size="$1" - local output="$2" - local pointsize="$3" - - if [ "$IMAGEMAGICK_CMD" = "magick" ]; then - # ImageMagick v7+ syntax - magick -size "${size}x${size}" xc:blue -gravity center -pointsize "$pointsize" -fill white -annotate 0 "TS" "$output" - else - # ImageMagick v6 syntax - convert -size "${size}x${size}" xc:blue -gravity center -pointsize "$pointsize" -fill white -annotate 0 "TS" "$output" - fi -} - -echo "[INFO] Generating placeholder launcher icons..." - -# Generate placeholder icons using ImageMagick -generate_placeholder_icon 48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png" 20 -generate_placeholder_icon 72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png" 30 -generate_placeholder_icon 96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png" 40 -generate_placeholder_icon 144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png" 60 -generate_placeholder_icon 192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png" 80 - -echo "[INFO] Copying to round versions..." - -# Copy to round versions -cp "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png" -cp "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png" -cp "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png" -cp "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png" -cp "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png" - -echo "[SUCCESS] Generated Android placeholder 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 " - All round versions created" -echo "[SUCCESS] Android placeholder icon generation completed successfully!" \ No newline at end of file diff --git a/scripts/generate-ios-assets.sh b/scripts/generate-ios-assets.sh deleted file mode 100755 index 17131d2d..00000000 --- a/scripts/generate-ios-assets.sh +++ /dev/null @@ -1,253 +0,0 @@ -#!/bin/bash - -# TimeSafari iOS Asset Generation Script -# Manually generates iOS assets using ImageMagick when capacitor-assets fails -# Author: Matthew Raymer - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -IOS_ASSETS_DIR="$PROJECT_ROOT/ios/App/App/Assets.xcassets" -RESOURCES_DIR="$PROJECT_ROOT/resources/ios" - -echo "=== TimeSafari iOS Asset Generation ===" -echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Generating iOS assets manually" - -# Check if ImageMagick is available -if ! command -v convert &> /dev/null; then - echo "[ERROR] ImageMagick 'convert' command not found. Please install ImageMagick." - exit 1 -fi - -# Check if source files exist -if [ ! -f "$RESOURCES_DIR/icon/icon.png" ]; then - echo "[ERROR] Source icon not found: $RESOURCES_DIR/icon/icon.png" - exit 1 -fi - -if [ ! -f "$RESOURCES_DIR/splash/splash.png" ]; then - echo "[ERROR] Source splash not found: $RESOURCES_DIR/splash/splash.png" - exit 1 -fi - -if [ ! -f "$RESOURCES_DIR/splash/splash_dark.png" ]; then - echo "[ERROR] Source dark splash not found: $RESOURCES_DIR/splash/splash_dark.png" - exit 1 -fi - -echo "[INFO] Generating iOS app icons..." - -# Generate app icons for different sizes -# iPhone icons -convert "$RESOURCES_DIR/icon/icon.png" -resize 40x40 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 60x60 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@3x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 58x58 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 87x87 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@3x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 80x80 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 120x120 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@3x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 120x120 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-60@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 180x180 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-60@3x.png" - -# iPad icons -convert "$RESOURCES_DIR/icon/icon.png" -resize 20x20 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@1x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 40x40 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 29x29 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@1x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 58x58 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 40x40 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@1x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 80x80 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 152x152 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-76@2x.png" -convert "$RESOURCES_DIR/icon/icon.png" -resize 167x167 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-83.5@2x.png" - -# App Store icon -convert "$RESOURCES_DIR/icon/icon.png" -resize 1024x1024 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-1024@1x.png" - -echo "[INFO] Generating iOS splash screens..." - -# Generate splash screens for different scales -convert "$RESOURCES_DIR/splash/splash.png" -resize 320x480 "$IOS_ASSETS_DIR/Splash.imageset/splash@1x.png" -convert "$RESOURCES_DIR/splash/splash.png" -resize 640x960 "$IOS_ASSETS_DIR/Splash.imageset/splash@2x.png" -convert "$RESOURCES_DIR/splash/splash.png" -resize 960x1440 "$IOS_ASSETS_DIR/Splash.imageset/splash@3x.png" - -# Generate dark splash screens -convert "$RESOURCES_DIR/splash/splash_dark.png" -resize 320x480 "$IOS_ASSETS_DIR/SplashDark.imageset/splash@1x.png" -convert "$RESOURCES_DIR/splash/splash_dark.png" -resize 640x960 "$IOS_ASSETS_DIR/SplashDark.imageset/splash@2x.png" -convert "$RESOURCES_DIR/splash/splash_dark.png" -resize 960x1440 "$IOS_ASSETS_DIR/SplashDark.imageset/splash@3x.png" - -echo "[INFO] Updating Contents.json files to reference generated images..." - -# Update AppIcon Contents.json to reference the generated files -cat > "$IOS_ASSETS_DIR/AppIcon.appiconset/Contents.json" << 'EOF' -{ - "images" : [ - { - "filename" : "AppIcon-20@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "AppIcon-20@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "AppIcon-29@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "AppIcon-29@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "AppIcon-40@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "AppIcon-40@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "AppIcon-60@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "AppIcon-60@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "AppIcon-20@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "AppIcon-20@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "AppIcon-29@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "AppIcon-29@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "AppIcon-40@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "AppIcon-40@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "AppIcon-76@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "AppIcon-83.5@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "AppIcon-1024@1x.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} -EOF - -# Update Splash Contents.json to reference the generated files -cat > "$IOS_ASSETS_DIR/Splash.imageset/Contents.json" << 'EOF' -{ - "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 - } -} -EOF - -# Update SplashDark Contents.json to reference the generated files -cat > "$IOS_ASSETS_DIR/SplashDark.imageset/Contents.json" << 'EOF' -{ - "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 - } -} -EOF - -echo "[SUCCESS] iOS assets generated successfully!" -echo "[INFO] Generated files:" -find "$IOS_ASSETS_DIR" -name "*.png" | sort - -echo "" -echo "[NOTE] iOS builds require macOS with Xcode - cannot build on Linux" -echo "[INFO] Assets are now ready for when you build on macOS" 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 From 76749a097d756c0abdad823f3e1fd26ccc3bc002 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 14 Aug 2025 08:43:40 +0000 Subject: [PATCH 21/48] fix(build): update Android build script to use new asset validation - Replace check-android-resources.sh call with npm run assets:validate - Fix 'No such file or directory' error in build-android.sh - Ensure builds work consistently across all development environments - Maintain build workflow while using new standardized asset system This fix resolves the build failure that occurred after removing legacy asset generation scripts, ensuring the new capacitor-assets workflow integrates seamlessly with existing build orchestration. Tested: Android build completes successfully with asset validation Assets: 87 platform assets generated automatically via capacitor-assets --- scripts/build-android.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build-android.sh b/scripts/build-android.sh index 0f139be1..50a76f90 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_warn "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 From 495a94827a84ee151a9499d84b688be69add4471 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 14 Aug 2025 09:08:06 +0000 Subject: [PATCH 22/48] refactor(assets): convert asset management scripts to TypeScript with tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace JavaScript asset scripts with TypeScript equivalents - Install tsx for direct TypeScript execution without compilation - Add proper TypeScript interfaces for AssetConfig and validation - Update package.json scripts to use tsx instead of node - Remove old JavaScript files (assets-config.js, assets-validator.js) - Maintain all existing functionality while improving type safety - Fix module syntax issues that caused build failures on macOS Scripts affected: - assets:config: node โ†’ tsx scripts/assets-config.ts - assets:validate: node โ†’ tsx scripts/assets-validator.ts Benefits: - Eliminates CommonJS/ES module syntax conflicts - Provides better type safety and IntelliSense - Modernizes development tooling - Ensures cross-platform compatibility --- config/assets/capacitor-assets.config.json | 14 +- package-lock.json | 528 ++++++++++++++++++ package.json | 5 +- .../{assets-config.js => assets-config.ts} | 82 ++- ...ssets-validator.js => assets-validator.ts} | 103 ++-- 5 files changed, 666 insertions(+), 66 deletions(-) rename scripts/{assets-config.js => assets-config.ts} (73%) rename scripts/{assets-validator.js => assets-validator.ts} (64%) diff --git a/config/assets/capacitor-assets.config.json b/config/assets/capacitor-assets.config.json index eb12403e..92bd0414 100644 --- a/config/assets/capacitor-assets.config.json +++ b/config/assets/capacitor-assets.config.json @@ -1,10 +1,9 @@ { "icon": { - "source": "resources/icon.png", "android": { "adaptive": { - "foreground": "resources/icon.png", "background": "#121212", + "foreground": "resources/icon.png", "monochrome": "resources/icon.png" }, "target": "android/app/src/main/res" @@ -13,20 +12,21 @@ "padding": 0, "target": "ios/App/App/Assets.xcassets/AppIcon.appiconset" }, + "source": "resources/icon.png", "web": { "target": "public/img/icons" } }, "splash": { - "source": "resources/splash.png", - "darkSource": "resources/splash_dark.png", "android": { "scale": "cover", "target": "android/app/src/main/res" }, + "darkSource": "resources/splash_dark.png", "ios": { - "useStoryBoard": true, - "target": "ios/App/App/Assets.xcassets" - } + "target": "ios/App/App/Assets.xcassets", + "useStoryBoard": true + }, + "source": "resources/splash.png" } } 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 930cc8cf..d961148d 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "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": "node scripts/assets-config.js", - "assets:validate": "node scripts/assets-validator.js", + "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", @@ -245,6 +245,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/scripts/assets-config.js b/scripts/assets-config.ts similarity index 73% rename from scripts/assets-config.js rename to scripts/assets-config.ts index 926ff1fa..baaadce5 100644 --- a/scripts/assets-config.js +++ b/scripts/assets-config.ts @@ -1,11 +1,11 @@ -#!/usr/bin/env node +#!/usr/bin/env tsx /** * TimeSafari Asset Configuration Generator * Generates capacitor-assets configuration files with deterministic outputs * Author: Matthew Raymer * - * Usage: node scripts/assets-config.js + * Usage: tsx scripts/assets-config.ts */ import fs from 'fs'; @@ -16,12 +16,62 @@ 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 {Object} Sorted, stable configuration object + * @returns Sorted, stable configuration object */ -function generateAssetConfig() { - const config = { +function generateAssetConfig(): AssetConfig { + const config: AssetConfig = { icon: { source: "resources/icon.png", android: { @@ -59,10 +109,10 @@ function generateAssetConfig() { /** * Sort object keys recursively for deterministic output - * @param {Object} obj - Object to sort - * @returns {Object} Object with sorted keys + * @param obj - Object to sort + * @returns Object with sorted keys */ -function sortObjectKeys(obj) { +function sortObjectKeys(obj: any): any { if (obj === null || typeof obj !== 'object') { return obj; } @@ -71,7 +121,7 @@ function sortObjectKeys(obj) { return obj.map(sortObjectKeys); } - const sorted = {}; + const sorted: any = {}; Object.keys(obj) .sort() .forEach(key => { @@ -84,7 +134,7 @@ function sortObjectKeys(obj) { /** * Validate that required source files exist */ -function validateSourceFiles() { +function validateSourceFiles(): void { const requiredFiles = [ 'resources/icon.png', 'resources/splash.png', @@ -107,10 +157,10 @@ function validateSourceFiles() { /** * Write configuration to file with consistent formatting - * @param {Object} config - Configuration object - * @param {string} outputPath - Output file path + * @param config - Configuration object + * @param outputPath - Output file path */ -function writeConfig(config, outputPath) { +function writeConfig(config: AssetConfig, outputPath: string): void { const jsonString = JSON.stringify(config, null, 2); // Ensure consistent line endings and no trailing whitespace @@ -126,7 +176,7 @@ function writeConfig(config, outputPath) { /** * Main execution function */ -function main() { +function main(): void { console.log('๐Ÿ”„ Generating TimeSafari asset configuration...'); console.log(`๐Ÿ“ Project root: ${PROJECT_ROOT}`); console.log(`๐Ÿ“… Generated: ${new Date().toISOString()}`); @@ -161,7 +211,7 @@ function main() { console.log(' 4. Use "npm run build:native" for builds'); } catch (error) { - console.error('โŒ Configuration generation failed:', error.message); + console.error('โŒ Configuration generation failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } } @@ -171,4 +221,4 @@ if (import.meta.url === `file://${process.argv[1]}`) { main(); } -export { generateAssetConfig, sortObjectKeys, validateSourceFiles }; +export { generateAssetConfig, sortObjectKeys, validateSourceFiles, AssetConfig }; diff --git a/scripts/assets-validator.js b/scripts/assets-validator.ts similarity index 64% rename from scripts/assets-validator.js rename to scripts/assets-validator.ts index fcdbcdff..587a25c7 100644 --- a/scripts/assets-validator.js +++ b/scripts/assets-validator.ts @@ -1,11 +1,11 @@ -#!/usr/bin/env node +#!/usr/bin/env tsx /** * TimeSafari Asset Configuration Validator * Validates capacitor-assets configuration against schema and source files * Author: Matthew Raymer * - * Usage: node scripts/assets-validator.js [config-path] + * Usage: tsx scripts/assets-validator.ts [config-path] */ import fs from 'fs'; @@ -16,50 +16,71 @@ 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 {string} filePath - Path to JSON file - * @returns {Object} Parsed JSON object + * @param filePath - Path to JSON file + * @returns Parsed JSON object */ -function loadJsonFile(filePath) { +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.message}`); + throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Validate configuration against schema - * @param {Object} config - Configuration object to validate - * @param {Object} schema - JSON schema for validation - * @returns {Array} Array of validation errors + * @param config - Configuration object to validate + * @returns Array of validation errors */ -function validateAgainstSchema(config, schema) { - const errors = []; +function validateAgainstSchema(config: AssetConfig): ValidationError[] { + const errors: ValidationError[] = []; // Basic structure validation if (!config.icon || !config.splash) { - errors.push('Configuration must contain both "icon" and "splash" sections'); + errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' }); } // Icon validation if (config.icon) { if (!config.icon.source) { - errors.push('Icon section must contain "source" field'); + errors.push({ message: 'Icon section must contain "source" field' }); } else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { - errors.push('Icon source must be a PNG or SVG file in resources/ directory'); + 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('Android adaptive icon must have both foreground and background'); + errors.push({ message: 'Android adaptive icon must have both foreground and background' }); } if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { - errors.push('Android adaptive foreground must be a PNG or SVG file in resources/ directory'); + errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' }); } } } @@ -67,13 +88,13 @@ function validateAgainstSchema(config, schema) { // Splash validation if (config.splash) { if (!config.splash.source) { - errors.push('Splash section must contain "source" field'); + errors.push({ message: 'Splash section must contain "source" field' }); } else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { - errors.push('Splash source must be a PNG or SVG file in resources/ directory'); + 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('Dark splash source must be a PNG or SVG file in resources/ directory'); + errors.push({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' }); } } @@ -82,12 +103,12 @@ function validateAgainstSchema(config, schema) { /** * Validate that source files exist - * @param {Object} config - Configuration object - * @returns {Array} Array of missing file errors + * @param config - Configuration object + * @returns Array of missing file errors */ -function validateSourceFiles(config) { - const errors = []; - const requiredFiles = new Set(); +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); @@ -100,7 +121,7 @@ function validateSourceFiles(config) { requiredFiles.forEach(file => { const filePath = path.join(PROJECT_ROOT, file); if (!fs.existsSync(filePath)) { - errors.push(`Source file not found: ${file}`); + errors.push({ message: `Source file not found: ${file}` }); } }); @@ -109,12 +130,12 @@ function validateSourceFiles(config) { /** * Validate target directories are writable - * @param {Object} config - Configuration object - * @returns {Array} Array of directory validation errors + * @param config - Configuration object + * @returns Array of directory validation errors */ -function validateTargetDirectories(config) { - const errors = []; - const targetDirs = new Set(); +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); @@ -129,9 +150,9 @@ function validateTargetDirectories(config) { const parentDir = path.dirname(dirPath); if (!fs.existsSync(parentDir)) { - errors.push(`Parent directory does not exist: ${parentDir}`); + errors.push({ message: `Parent directory does not exist: ${parentDir}` }); } else if (!fs.statSync(parentDir).isDirectory()) { - errors.push(`Parent path is not a directory: ${parentDir}`); + errors.push({ message: `Parent path is not a directory: ${parentDir}` }); } }); @@ -140,10 +161,10 @@ function validateTargetDirectories(config) { /** * Main validation function - * @param {string} configPath - Path to configuration file - * @returns {boolean} True if validation passes + * @param configPath - Path to configuration file + * @returns True if validation passes */ -function validateConfiguration(configPath) { +function validateConfiguration(configPath: string): boolean { console.log('๐Ÿ” Validating TimeSafari asset configuration...'); console.log(`๐Ÿ“ Config file: ${configPath}`); console.log(`๐Ÿ“ Project root: ${PROJECT_ROOT}`); @@ -159,7 +180,7 @@ function validateConfiguration(configPath) { console.log('โœ… Schema file loaded successfully'); // Perform validations - const schemaErrors = validateAgainstSchema(config, schema); + const schemaErrors = validateAgainstSchema(config); const fileErrors = validateSourceFiles(config); const dirErrors = validateTargetDirectories(config); @@ -179,13 +200,13 @@ function validateConfiguration(configPath) { } else { console.error('โŒ Validation failed with the following errors:'); allErrors.forEach((error, index) => { - console.error(` ${index + 1}. ${error}`); + console.error(` ${index + 1}. ${error.message}`); }); return false; } } catch (error) { - console.error('โŒ Validation failed:', error.message); + console.error('โŒ Validation failed:', error instanceof Error ? error.message : String(error)); return false; } } @@ -193,7 +214,7 @@ function validateConfiguration(configPath) { /** * Main execution function */ -function main() { +function main(): void { const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json'); if (!fs.existsSync(configPath)) { @@ -201,7 +222,7 @@ function main() { console.log(''); console.log('๐Ÿ’ก Available options:'); console.log(' - Use default: capacitor-assets.config.json'); - console.log(' - Specify path: node scripts/assets-validator.js path/to/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); } @@ -215,4 +236,4 @@ if (import.meta.url === `file://${process.argv[1]}`) { main(); } -export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories }; +export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories, AssetConfig }; From 79593f12b4be598e4bce60ad75c630cddebfe875 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 14 Aug 2025 11:05:34 +0000 Subject: [PATCH 23/48] fix(types): resolve notification system type safety issues - Replace $notify any types with proper NotifyFunction interface - Import NotifyFunction type from utils/notify - Eliminate 5 TypeScript any type warnings - Improve type safety for notification system across components Reduces lint warnings from 25 to 20 by addressing high-impact, low-effort notification type issues. Maintains full functionality while improving code quality and IntelliSense support. --- src/App.vue | 56 +++++++++++++++++++------- src/components/ActivityListItem.vue | 4 +- src/components/GiftedDialog.vue | 4 +- src/components/ImageMethodDialog.vue | 4 +- src/test/index.ts | 12 ++++-- src/views/ContactQRScanFullView.vue | 4 +- src/views/ImportDerivedAccountView.vue | 4 +- 7 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/App.vue b/src/App.vue index 0b190521..8bd39b52 100644 --- a/src/App.vue +++ b/src/App.vue @@ -28,8 +28,12 @@ class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md" >
-

{{ notification.title }}

-

{{ notification.text }}

+

+ {{ notification.title }} +

+

+ {{ notification.text }} +

@@ -46,9 +50,15 @@ >
-
-

{{ notification.title }}

-

{{ notification.text }}

+
+

+ {{ notification.title }} +

+

+ {{ notification.text }} +

-
-

{{ notification.title }}

-

{{ notification.text }}

+
+

+ {{ notification.title }} +

+

+ {{ notification.text }} +

-
-

{{ notification.title }}

-

{{ notification.text }}

+
+

+ {{ notification.title }} +

+

+ {{ notification.text }} +

-
-

{{ notification.title }}

-

{{ notification.text }}

+
+

+ {{ notification.title }} +

+

+ {{ notification.text }} +