Browse Source

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.
master
Matthew Raymer 1 day ago
parent
commit
d4bb902cbe
  1. 10
      .eslintrc.json
  2. 41
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java
  3. 96
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java
  4. 6
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java
  5. 48
      android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java
  6. 8
      test-apps/daily-notification-test/eslint.config.ts
  7. 15
      test-apps/daily-notification-test/package-lock.json
  8. 64
      test-apps/daily-notification-test/src/App.vue
  9. 39
      test-apps/daily-notification-test/src/config/test-user-zero.ts
  10. 152
      test-apps/daily-notification-test/src/lib/logger.ts
  11. 7
      test-apps/daily-notification-test/src/router/index.ts
  12. 6
      test-apps/daily-notification-test/src/stores/app.ts
  13. 48
      test-apps/daily-notification-test/src/views/HomeView.vue
  14. 2
      test-apps/daily-notification-test/src/views/UserZeroView.vue

10
.eslintrc.json

@ -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"
}
}
]
}

41
android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java

@ -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;
// 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 (duplicateInBatch) {
continue;
}
// Then check against existing notifications in storage
boolean duplicateInStorage = false;
for (NotificationContent existing : existingNotifications) {
if (Math.abs(existing.getScheduledTime() - content.getScheduledTime()) <= toleranceMs) {
Log.w(TAG, "PR2: DUPLICATE_SKIP id=" + content.getId() +
if (Math.abs(existing.getScheduledTime() - scheduledTime) <= toleranceMs) {
Log.w(TAG, "PR2: DUPLICATE_SKIP_STORAGE id=" + content.getId() +
" existing_id=" + existing.getId() +
" scheduled_time=" + content.getScheduledTime() +
" time_diff_ms=" + Math.abs(existing.getScheduledTime() - content.getScheduledTime()));
duplicateFound = true;
" scheduled_time=" + scheduledTime +
" time_diff_ms=" + Math.abs(existing.getScheduledTime() - scheduledTime));
duplicateInStorage = true;
skippedCount++;
break;
}
}
if (duplicateFound) {
if (duplicateInStorage) {
// Skip this notification - one already exists for this time
// Ensures one prefetch → one notification pairing
continue;
}
// Mark this scheduledTime as processed in current batch
batchScheduledTimes.add(scheduledTime);
// Save content to storage
storage.saveNotificationContent(content);

96
android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java

@ -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
*

6
android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java

@ -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

48
android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java

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

8
test-apps/daily-notification-test/eslint.config.ts

@ -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

@ -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",

64
test-apps/daily-notification-test/src/App.vue

@ -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 = ''
}

39
test-apps/daily-notification-test/src/config/test-user-zero.ts

@ -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

@ -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 }

7
test-apps/daily-notification-test/src/router/index.ts

@ -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

6
test-apps/daily-notification-test/src/stores/app.ts

@ -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 {

48
test-apps/daily-notification-test/src/views/HomeView.vue

@ -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>

2
test-apps/daily-notification-test/src/views/UserZeroView.vue

@ -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')

Loading…
Cancel
Save