refactor(test-app): consolidate native fetcher config and fix ES module issues
- Move native fetcher configuration from HomeView.vue to App.vue mounted() hook - Single source of truth for configuration on app startup - Removed duplicate configuration logic from HomeView - Added diagnostic logging to trace configuration flow - Fix ES module compatibility issue with Capacitor CLI - Replace direct logger import with lazy async loading in test-user-zero.ts - Prevents 'exports is not defined' error when Capacitor CLI loads config - Update refreshToken() and setBaseUrl() methods to async for logger access - Add centralized logger utility (src/lib/logger.ts) - Single ESLint whitelist location for console usage - Structured logging with levels and emoji support - Updated router/index.ts and stores/app.ts to use logger - Enhance Android notification deduplication - Add within-batch duplicate detection in fetch workers - Improve storage deduplication with alarm cancellation - Cancel alarms for removed duplicate notifications - Update UserZeroView.vue to await async refreshToken() call Fixes: - npx cap sync android ES module error - Duplicate notification accumulation - Console statement lint warnings All changes maintain backward compatibility and improve debugging visibility.
This commit is contained in:
@@ -15,5 +15,13 @@
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["test-apps/daily-notification-test/src/lib/logger.ts"],
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -262,32 +262,55 @@ public class DailyNotificationFetchWorker extends Worker {
|
||||
java.util.List<NotificationContent> existingNotifications = storage.getAllNotifications();
|
||||
long toleranceMs = 60 * 1000; // 1 minute tolerance for DST shifts
|
||||
|
||||
// Track scheduled times in current batch to prevent within-batch duplicates
|
||||
java.util.Set<Long> batchScheduledTimes = new java.util.HashSet<>();
|
||||
|
||||
// Save all contents and schedule notifications (with duplicate checking)
|
||||
int scheduledCount = 0;
|
||||
int skippedCount = 0;
|
||||
for (NotificationContent content : contents) {
|
||||
try {
|
||||
// Check for duplicate notification at the same scheduled time
|
||||
// This ensures prefetch doesn't create a duplicate if a manual notification already exists
|
||||
boolean duplicateFound = false;
|
||||
for (NotificationContent existing : existingNotifications) {
|
||||
if (Math.abs(existing.getScheduledTime() - content.getScheduledTime()) <= toleranceMs) {
|
||||
Log.w(TAG, "PR2: DUPLICATE_SKIP id=" + content.getId() +
|
||||
" existing_id=" + existing.getId() +
|
||||
" scheduled_time=" + content.getScheduledTime() +
|
||||
" time_diff_ms=" + Math.abs(existing.getScheduledTime() - content.getScheduledTime()));
|
||||
duplicateFound = true;
|
||||
// First check within current batch (prevents duplicates in same fetch)
|
||||
long scheduledTime = content.getScheduledTime();
|
||||
boolean duplicateInBatch = false;
|
||||
for (Long batchTime : batchScheduledTimes) {
|
||||
if (Math.abs(batchTime - scheduledTime) <= toleranceMs) {
|
||||
Log.w(TAG, "PR2: DUPLICATE_SKIP_BATCH id=" + content.getId() +
|
||||
" scheduled_time=" + scheduledTime +
|
||||
" time_diff_ms=" + Math.abs(batchTime - scheduledTime));
|
||||
duplicateInBatch = true;
|
||||
skippedCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateFound) {
|
||||
// Skip this notification - one already exists for this time
|
||||
// Ensures one prefetch → one notification pairing
|
||||
if (duplicateInBatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Then check against existing notifications in storage
|
||||
boolean duplicateInStorage = false;
|
||||
for (NotificationContent existing : existingNotifications) {
|
||||
if (Math.abs(existing.getScheduledTime() - scheduledTime) <= toleranceMs) {
|
||||
Log.w(TAG, "PR2: DUPLICATE_SKIP_STORAGE id=" + content.getId() +
|
||||
" existing_id=" + existing.getId() +
|
||||
" scheduled_time=" + scheduledTime +
|
||||
" time_diff_ms=" + Math.abs(existing.getScheduledTime() - scheduledTime));
|
||||
duplicateInStorage = true;
|
||||
skippedCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateInStorage) {
|
||||
// Skip this notification - one already exists for this time
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark this scheduledTime as processed in current batch
|
||||
batchScheduledTimes.add(scheduledTime);
|
||||
|
||||
// Save content to storage
|
||||
storage.saveNotificationContent(content);
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ public class DailyNotificationStorage {
|
||||
|
||||
loadNotificationsFromStorage();
|
||||
cleanupOldNotifications();
|
||||
// Remove duplicates on startup and cancel their alarms/workers
|
||||
java.util.List<String> removedIds = deduplicateNotifications();
|
||||
cancelRemovedNotifications(removedIds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -484,6 +487,99 @@ public class DailyNotificationStorage {
|
||||
getLastFetchTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate notifications (same scheduledTime within tolerance)
|
||||
*
|
||||
* Keeps the most recently created notification for each scheduledTime,
|
||||
* removes older duplicates to prevent accumulation.
|
||||
*
|
||||
* @return List of notification IDs that were removed (for cancellation of alarms/workers)
|
||||
*/
|
||||
public java.util.List<String> deduplicateNotifications() {
|
||||
try {
|
||||
long toleranceMs = 60 * 1000; // 1 minute tolerance
|
||||
java.util.Map<Long, NotificationContent> scheduledTimeMap = new java.util.HashMap<>();
|
||||
java.util.List<String> idsToRemove = new java.util.ArrayList<>();
|
||||
|
||||
synchronized (notificationList) {
|
||||
// First pass: find all duplicates, keep the one with latest fetchedAt
|
||||
for (NotificationContent notification : notificationList) {
|
||||
long scheduledTime = notification.getScheduledTime();
|
||||
boolean foundMatch = false;
|
||||
|
||||
for (java.util.Map.Entry<Long, NotificationContent> entry : scheduledTimeMap.entrySet()) {
|
||||
if (Math.abs(entry.getKey() - scheduledTime) <= toleranceMs) {
|
||||
// Found a duplicate - keep the one with latest fetchedAt
|
||||
if (notification.getFetchedAt() > entry.getValue().getFetchedAt()) {
|
||||
idsToRemove.add(entry.getValue().getId());
|
||||
entry.setValue(notification);
|
||||
} else {
|
||||
idsToRemove.add(notification.getId());
|
||||
}
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundMatch) {
|
||||
scheduledTimeMap.put(scheduledTime, notification);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
if (!idsToRemove.isEmpty()) {
|
||||
notificationList.removeIf(n -> idsToRemove.contains(n.getId()));
|
||||
for (String id : idsToRemove) {
|
||||
notificationCache.remove(id);
|
||||
}
|
||||
saveNotificationsToStorage();
|
||||
Log.i(TAG, "DN|DEDUPE_CLEANUP removed=" + idsToRemove.size() + " duplicates");
|
||||
}
|
||||
}
|
||||
|
||||
return idsToRemove;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error during deduplication", e);
|
||||
return new java.util.ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel alarms and workers for removed notification IDs
|
||||
*
|
||||
* This ensures that when notifications are removed (e.g., during deduplication),
|
||||
* their associated alarms and WorkManager workers are also cancelled to prevent
|
||||
* zombie workers trying to display non-existent notifications.
|
||||
*
|
||||
* @param removedIds List of notification IDs that were removed
|
||||
*/
|
||||
private void cancelRemovedNotifications(java.util.List<String> removedIds) {
|
||||
if (removedIds == null || removedIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Cancel alarms for removed notifications
|
||||
DailyNotificationScheduler scheduler = new DailyNotificationScheduler(
|
||||
context,
|
||||
(android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE)
|
||||
);
|
||||
|
||||
for (String id : removedIds) {
|
||||
scheduler.cancelNotification(id);
|
||||
}
|
||||
|
||||
// Note: WorkManager workers can't be cancelled by notification ID directly
|
||||
// Workers will handle missing content gracefully by returning Result.success()
|
||||
// (see DailyNotificationWorker.handleDisplayNotification - it returns success for missing content)
|
||||
// This prevents retry loops for notifications removed during deduplication
|
||||
|
||||
Log.i(TAG, "DN|DEDUPE_CLEANUP cancelled alarms for " + removedIds.size() + " removed notifications");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "DN|DEDUPE_CLEANUP_ERR failed to cancel alarms/workers", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce storage limits and retention policy
|
||||
*
|
||||
|
||||
@@ -131,8 +131,10 @@ public class DailyNotificationWorker extends Worker {
|
||||
NotificationContent content = getContentFromRoomOrLegacy(notificationId);
|
||||
|
||||
if (content == null) {
|
||||
Log.w(TAG, "DN|DISPLAY_ERR content_not_found id=" + notificationId);
|
||||
return Result.failure();
|
||||
// Content not found - likely removed during deduplication or cleanup
|
||||
// Return success instead of failure to prevent retries for intentionally removed notifications
|
||||
Log.w(TAG, "DN|DISPLAY_SKIP content_not_found id=" + notificationId + " (likely removed during deduplication)");
|
||||
return Result.success(); // Success prevents retry loops for removed notifications
|
||||
}
|
||||
|
||||
// Check if notification is ready to display
|
||||
|
||||
@@ -298,9 +298,52 @@ public final class TimeSafariIntegrationManager {
|
||||
// 4) Convert bundle to NotificationContent[] and save/schedule
|
||||
List<NotificationContent> contents = convertBundleToNotificationContent(bundle);
|
||||
|
||||
// Get existing notifications for duplicate checking
|
||||
java.util.List<NotificationContent> existingNotifications = storage.getAllNotifications();
|
||||
long toleranceMs = 60 * 1000; // 1 minute tolerance for DST shifts
|
||||
java.util.Set<Long> batchScheduledTimes = new java.util.HashSet<>();
|
||||
|
||||
int scheduledCount = 0;
|
||||
int skippedCount = 0;
|
||||
for (NotificationContent content : contents) {
|
||||
try {
|
||||
// Check for duplicates within current batch
|
||||
long scheduledTime = content.getScheduledTime();
|
||||
boolean duplicateInBatch = false;
|
||||
for (Long batchTime : batchScheduledTimes) {
|
||||
if (Math.abs(batchTime - scheduledTime) <= toleranceMs) {
|
||||
logger.w("TS: DUPLICATE_SKIP_BATCH id=" + content.getId() +
|
||||
" scheduled_time=" + scheduledTime);
|
||||
duplicateInBatch = true;
|
||||
skippedCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateInBatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for duplicates in existing storage
|
||||
boolean duplicateInStorage = false;
|
||||
for (NotificationContent existing : existingNotifications) {
|
||||
if (Math.abs(existing.getScheduledTime() - scheduledTime) <= toleranceMs) {
|
||||
logger.w("TS: DUPLICATE_SKIP_STORAGE id=" + content.getId() +
|
||||
" existing_id=" + existing.getId() +
|
||||
" scheduled_time=" + scheduledTime);
|
||||
duplicateInStorage = true;
|
||||
skippedCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateInStorage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark this scheduledTime as processed
|
||||
batchScheduledTimes.add(scheduledTime);
|
||||
|
||||
// Save content first
|
||||
storage.saveNotificationContent(content);
|
||||
// TTL validation happens inside scheduler.scheduleNotification()
|
||||
@@ -312,8 +355,9 @@ public final class TimeSafariIntegrationManager {
|
||||
logger.w("TS: schedule/save failed for id=" + content.getId() + " " + perItem.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
logger.i("TS: fetchAndScheduleFromServer done; scheduled=" + scheduledCount + "/" + contents.size());
|
||||
|
||||
logger.i("TS: fetchAndScheduleFromServer done; scheduled=" + scheduledCount + "/" + contents.size() +
|
||||
(skippedCount > 0 ? ", " + skippedCount + " duplicates skipped" : ""));
|
||||
|
||||
} catch (Exception ex) {
|
||||
logger.e("TS: fetchAndScheduleFromServer error", ex);
|
||||
|
||||
@@ -26,6 +26,14 @@ export default defineConfigWithVueTs(
|
||||
'**/.gradle/**'
|
||||
]),
|
||||
|
||||
// Whitelist console usage in logger utility (single source of truth)
|
||||
{
|
||||
files: ['src/lib/logger.ts'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
)
|
||||
|
||||
15
test-apps/daily-notification-test/package-lock.json
generated
15
test-apps/daily-notification-test/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "daily-notification-test",
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.2.1",
|
||||
"@capacitor/cli": "^6.2.1",
|
||||
@@ -116,6 +117,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -415,6 +417,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.4"
|
||||
},
|
||||
@@ -633,6 +636,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
|
||||
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -2064,6 +2068,7 @@
|
||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
@@ -2662,6 +2667,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2910,6 +2916,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
@@ -3372,6 +3379,7 @@
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3433,6 +3441,7 @@
|
||||
"integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -4292,6 +4301,7 @@
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -5720,6 +5730,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5797,6 +5808,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -6030,6 +6042,7 @@
|
||||
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -6300,6 +6313,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -6319,6 +6333,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
|
||||
@@ -36,12 +36,76 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, toNative } from 'vue-facing-decorator'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { DailyNotification } from '@timesafari/daily-notification-plugin'
|
||||
import { TEST_USER_ZERO_CONFIG, generateEndorserJWT } from './config/test-user-zero'
|
||||
import { logger } from './lib/logger'
|
||||
|
||||
@Component
|
||||
class App extends Vue {
|
||||
isLoading = false
|
||||
errorMessage = ''
|
||||
|
||||
async mounted() {
|
||||
// CRITICAL: Log immediately to verify this hook executes
|
||||
console.log('🚀 App.vue: mounted() hook EXECUTING')
|
||||
console.log('🚀 App.vue: Capacitor.isNativePlatform() =', Capacitor.isNativePlatform())
|
||||
|
||||
// Configure native fetcher on native platforms (Android/iOS)
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
console.log('🚀 App.vue: Entering native platform configuration block')
|
||||
try {
|
||||
// Use console.log for visibility in adb logcat
|
||||
console.log('🔧 App.vue: Starting native fetcher configuration...')
|
||||
logger.info('Configuring native fetcher...')
|
||||
|
||||
// Get API server URL first (before JWT generation)
|
||||
const apiBaseUrl = TEST_USER_ZERO_CONFIG.getApiServerUrl()
|
||||
console.log('🔧 App.vue: API Base URL:', apiBaseUrl)
|
||||
console.log('🔧 App.vue: Server Mode:', TEST_USER_ZERO_CONFIG.api.serverMode)
|
||||
|
||||
// Skip configuration if in mock mode (no real API calls)
|
||||
if (TEST_USER_ZERO_CONFIG.api.serverMode === 'mock') {
|
||||
console.log('⏭️ App.vue: Mock mode - skipping configuration')
|
||||
logger.warn('Mock mode enabled - native fetcher will not be configured')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔧 App.vue: Generating JWT token...')
|
||||
// Generate JWT token for authentication
|
||||
const jwtToken = await generateEndorserJWT()
|
||||
console.log('✅ App.vue: JWT token generated, length:', jwtToken.length)
|
||||
|
||||
console.log('🔧 App.vue: Calling configureNativeFetcher with:', {
|
||||
apiBaseUrl,
|
||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did.substring(0, 30) + '...',
|
||||
jwtTokenLength: jwtToken.length
|
||||
})
|
||||
|
||||
// Configure native fetcher with credentials
|
||||
await DailyNotification.configureNativeFetcher({
|
||||
apiBaseUrl: apiBaseUrl,
|
||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did,
|
||||
jwtToken: jwtToken
|
||||
})
|
||||
|
||||
console.log('✅ App.vue: Native fetcher configured successfully!')
|
||||
logger.info('Native fetcher configured successfully', {
|
||||
apiBaseUrl: apiBaseUrl.substring(0, 50) + '...',
|
||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did.substring(0, 30) + '...'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ App.vue: Failed to configure native fetcher:', error)
|
||||
console.error('❌ App.vue: Error details:', error instanceof Error ? error.stack : String(error))
|
||||
logger.error('Failed to configure native fetcher:', error)
|
||||
this.errorMessage = `Failed to configure native fetcher: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
} else {
|
||||
console.log('⏭️ App.vue: Web platform - skipping configuration')
|
||||
logger.info('Web platform detected - native fetcher configuration skipped')
|
||||
}
|
||||
}
|
||||
|
||||
clearError() {
|
||||
this.errorMessage = ''
|
||||
}
|
||||
|
||||
@@ -9,6 +9,17 @@
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
// Lazy import logger to avoid ES module issues when loaded by Capacitor CLI (CommonJS)
|
||||
// Logger is only used inside functions, not at module scope
|
||||
let logger: { error: (...args: unknown[]) => void; custom: (emoji: string, ...args: unknown[]) => void } | null = null;
|
||||
const getLogger = async () => {
|
||||
if (!logger) {
|
||||
const loggerModule = await import('../lib/logger');
|
||||
logger = loggerModule.logger;
|
||||
}
|
||||
return logger;
|
||||
};
|
||||
|
||||
export const TEST_USER_ZERO_CONFIG = {
|
||||
// User Zero Identity (from crowd-master testUtils.ts)
|
||||
identity: {
|
||||
@@ -25,7 +36,7 @@ export const TEST_USER_ZERO_CONFIG = {
|
||||
// - "production": Use production API server
|
||||
// - "mock": Use mock responses (no network calls)
|
||||
// - "custom": Use servers.custom URL
|
||||
serverMode: "mock" as "localhost" | "staging" | "production" | "mock" | "custom",
|
||||
serverMode: "localhost" as "localhost" | "staging" | "production" | "mock" | "custom",
|
||||
|
||||
// Server URLs for different modes
|
||||
servers: {
|
||||
@@ -258,7 +269,8 @@ export async function generateEndorserJWT(): Promise<string> {
|
||||
|
||||
return jwt;
|
||||
} catch (error) {
|
||||
console.error('Failed to generate ES256K JWT:', error);
|
||||
const log = await getLogger();
|
||||
log.error('Failed to generate ES256K JWT:', error);
|
||||
throw new Error(`JWT generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
@@ -311,10 +323,10 @@ export class TestUserZeroAPI {
|
||||
/**
|
||||
* Set API server URL (useful for switching between localhost, staging, etc.)
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
async setBaseUrl(url: string): Promise<void> {
|
||||
this.baseUrl = url;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("🔧 API base URL updated to:", url);
|
||||
const log = await getLogger();
|
||||
log.custom("🔧", "API base URL updated to:", url);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -331,8 +343,8 @@ export class TestUserZeroAPI {
|
||||
|
||||
if (useMock) {
|
||||
// Return mock data for offline testing
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("🧪 Using mock starred projects response");
|
||||
const log = await getLogger();
|
||||
log.custom("🧪", "Using mock starred projects response");
|
||||
return MOCK_STARRED_PROJECTS_RESPONSE;
|
||||
}
|
||||
|
||||
@@ -350,10 +362,9 @@ export class TestUserZeroAPI {
|
||||
afterId: afterId || TEST_USER_ZERO_CONFIG.starredProjects.lastAckedJwtId
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("🌐 Making real API call to:", url);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("📦 Request body:", requestBody);
|
||||
const log = await getLogger();
|
||||
log.custom("🌐", "Making real API call to:", url);
|
||||
log.custom("📦", "Request body:", requestBody);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -371,10 +382,10 @@ export class TestUserZeroAPI {
|
||||
/**
|
||||
* Refresh JWT token
|
||||
*/
|
||||
refreshToken(): void {
|
||||
async refreshToken(): Promise<void> {
|
||||
this.jwt = generateTestJWT();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("🔄 JWT token refreshed");
|
||||
const log = await getLogger();
|
||||
log.custom("🔄", "JWT token refreshed");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
152
test-apps/daily-notification-test/src/lib/logger.ts
Normal file
152
test-apps/daily-notification-test/src/lib/logger.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
|
||||
/**
|
||||
* Logger Utility
|
||||
*
|
||||
* Centralized logging utility that wraps console methods with ESLint suppression
|
||||
* in a single location. Provides structured logging with log levels and optional
|
||||
* emoji prefixes for visual distinction.
|
||||
*
|
||||
* This file is the single whitelisted location for console usage in the application.
|
||||
* All console methods are allowed here as this is the designated logging utility.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
NONE = 4
|
||||
}
|
||||
|
||||
interface LoggerConfig {
|
||||
level: LogLevel
|
||||
enableEmojis: boolean
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private config: LoggerConfig
|
||||
|
||||
constructor(config: Partial<LoggerConfig> = {}) {
|
||||
this.config = {
|
||||
level: config.level ?? LogLevel.DEBUG,
|
||||
enableEmojis: config.enableEmojis ?? true,
|
||||
prefix: config.prefix
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update logger configuration
|
||||
*/
|
||||
configure(config: Partial<LoggerConfig>): void {
|
||||
this.config = { ...this.config, ...config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug message
|
||||
*/
|
||||
debug(...args: unknown[]): void {
|
||||
if (this.config.level <= LogLevel.DEBUG) {
|
||||
this.logInternal('🐛', 'DEBUG', args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
*/
|
||||
info(...args: unknown[]): void {
|
||||
if (this.config.level <= LogLevel.INFO) {
|
||||
this.logInternal('ℹ️', 'INFO', args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
*/
|
||||
warn(...args: unknown[]): void {
|
||||
if (this.config.level <= LogLevel.WARN) {
|
||||
this.logInternal('⚠️', 'WARN', args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error message
|
||||
*/
|
||||
error(...args: unknown[]): void {
|
||||
if (this.config.level <= LogLevel.ERROR) {
|
||||
this.logInternal('❌', 'ERROR', args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message without emoji (for custom formatting)
|
||||
*/
|
||||
log(...args: unknown[]): void {
|
||||
if (this.config.level <= LogLevel.INFO) {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal logging method with emoji and level support
|
||||
*/
|
||||
private logInternal(emoji: string, level: string, args: unknown[]): void {
|
||||
const prefix = this.config.prefix ? `[${this.config.prefix}] ` : ''
|
||||
const emojiPrefix = this.config.enableEmojis ? `${emoji} ` : ''
|
||||
const levelPrefix = `[${level}] `
|
||||
|
||||
console.log(`${emojiPrefix}${prefix}${levelPrefix}`, ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log with custom emoji (convenience method)
|
||||
*/
|
||||
custom(emoji: string, ...args: unknown[]): void {
|
||||
if (this.config.level <= LogLevel.INFO) {
|
||||
const prefix = this.config.prefix ? `[${this.config.prefix}] ` : ''
|
||||
const emojiPrefix = this.config.enableEmojis ? `${emoji} ` : ''
|
||||
|
||||
console.log(`${emojiPrefix}${prefix}`, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log grouped message (uses console.group)
|
||||
*/
|
||||
group(label: string): void {
|
||||
if (this.config.level <= LogLevel.INFO) {
|
||||
console.group(label)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End log group
|
||||
*/
|
||||
groupEnd(): void {
|
||||
if (this.config.level <= LogLevel.INFO) {
|
||||
console.groupEnd()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log table (uses console.table)
|
||||
*/
|
||||
table(data: unknown): void {
|
||||
if (this.config.level <= LogLevel.DEBUG) {
|
||||
console.table(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const logger = new Logger({
|
||||
level: import.meta.env.DEV ? LogLevel.DEBUG : LogLevel.INFO,
|
||||
enableEmojis: true
|
||||
})
|
||||
|
||||
// Export class for creating custom logger instances
|
||||
export { Logger }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { logger } from '../lib/logger'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
@@ -105,16 +106,14 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
|
||||
// Add loading state
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🔄 Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`)
|
||||
logger.custom("🔄", `Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`)
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
// Clear any previous errors on successful navigation
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`✅ Navigation completed: ${String(to.name) || 'unknown'}`)
|
||||
logger.custom("✅", `Navigation completed: ${String(to.name) || 'unknown'}`)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
* Pinia store for managing global app state, loading, and errors
|
||||
* Platform-neutral design for Android/iOS/Electron
|
||||
*
|
||||
* Note: This file uses the centralized logger utility (logger) instead of
|
||||
* direct console usage. Console usage is whitelisted only in src/lib/logger.ts
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { logger } from '../lib/logger'
|
||||
|
||||
export interface AppState {
|
||||
isLoading: boolean
|
||||
@@ -52,7 +56,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
function setError(message: string): void {
|
||||
errorMessage.value = message
|
||||
console.error('App Error:', message)
|
||||
logger.error('App Error:', message)
|
||||
}
|
||||
|
||||
function clearError(): void {
|
||||
|
||||
@@ -115,7 +115,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import ActionCard from '@/components/cards/ActionCard.vue'
|
||||
import StatusCard from '@/components/cards/StatusCard.vue'
|
||||
import { TEST_USER_ZERO_CONFIG, generateEndorserJWT } from '@/config/test-user-zero'
|
||||
// Note: Native fetcher configuration moved to App.vue mounted() hook
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
@@ -430,54 +430,10 @@ const openConsole = (): void => {
|
||||
alert('📖 Console Logs\n\nOpen your browser\'s Developer Tools (F12) and check the Console tab for detailed diagnostic information.')
|
||||
}
|
||||
|
||||
// Configure native fetcher with test user zero credentials
|
||||
const configureNativeFetcher = async (): Promise<void> => {
|
||||
try {
|
||||
const { Capacitor } = await import('@capacitor/core')
|
||||
const isNative = Capacitor.isNativePlatform()
|
||||
|
||||
if (isNative) {
|
||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
||||
|
||||
// Get API server URL based on mode and platform
|
||||
const apiBaseUrl = TEST_USER_ZERO_CONFIG.getApiServerUrl()
|
||||
|
||||
// Only configure if not in mock mode
|
||||
if (TEST_USER_ZERO_CONFIG.api.serverMode !== 'mock' && apiBaseUrl !== 'mock://localhost') {
|
||||
console.log('🔧 Configuring native fetcher with:', {
|
||||
apiBaseUrl,
|
||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did.substring(0, 30) + '...',
|
||||
serverMode: TEST_USER_ZERO_CONFIG.api.serverMode
|
||||
})
|
||||
|
||||
// Generate ES256K JWT token using did-jwt library
|
||||
// This mimics TimeSafari's createEndorserJwtForKey() function
|
||||
// In production TimeSafari app, this would use:
|
||||
// const account = await retrieveFullyDecryptedAccount(activeDid);
|
||||
// const jwtToken = await createEndorserJwtForKey(account, {...});
|
||||
const jwtToken = await generateEndorserJWT()
|
||||
|
||||
await DailyNotification.configureNativeFetcher({
|
||||
apiBaseUrl: apiBaseUrl,
|
||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did,
|
||||
jwtToken: jwtToken // Pre-generated token (ES256K signed in production)
|
||||
})
|
||||
|
||||
console.log('✅ Native fetcher configured successfully')
|
||||
} else {
|
||||
console.log('⏭️ Skipping native fetcher configuration (mock mode enabled)')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to configure native fetcher:', error)
|
||||
// Don't block app initialization if configuration fails
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize system status when component mounts
|
||||
// Note: Native fetcher configuration is handled in App.vue mounted() hook
|
||||
onMounted(async () => {
|
||||
console.log('🏠 HomeView mounted - checking initial system status...')
|
||||
await configureNativeFetcher()
|
||||
await checkSystemStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -172,7 +172,7 @@ async function testJWTGeneration() {
|
||||
|
||||
// Generate test JWT
|
||||
// Generate a fresh JWT token
|
||||
apiClient.refreshToken()
|
||||
await apiClient.refreshToken()
|
||||
const jwt = apiClient.getJWT() // Get the JWT from the client
|
||||
|
||||
console.log('✅ JWT generation successful')
|
||||
|
||||
Reference in New Issue
Block a user