From ff166560df11463805c22e0c95070d564e173793 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 8 Oct 2025 07:19:46 +0000 Subject: [PATCH] docs: add TimeSafari PWA request pattern adoption guide and example - Add comprehensive guide for adopting existing TimeSafari PWA request patterns - Show direct integration of loadNewStarredProjectChanges() and getStarredProjectsWithChanges() - Provide Vue.js component integration examples - Include migration strategy and testing approach - Add practical example showing exact configuration needed - Demonstrate parallel testing and performance comparison - Show how to maintain existing TimeSafari interfaces while adding plugin features This enables seamless adoption of the plugin into existing TimeSafari PWA codebase while maintaining the same developer experience and user interface. --- docs/timesafari-request-adoption-guide.md | 682 ++++++++++++++++++ .../timesafari-pwa-integration-example.ts | 403 +++++++++++ 2 files changed, 1085 insertions(+) create mode 100644 docs/timesafari-request-adoption-guide.md create mode 100644 examples/timesafari-pwa-integration-example.ts diff --git a/docs/timesafari-request-adoption-guide.md b/docs/timesafari-request-adoption-guide.md new file mode 100644 index 0000000..b165f20 --- /dev/null +++ b/docs/timesafari-request-adoption-guide.md @@ -0,0 +1,682 @@ +# TimeSafari Daily Notification Plugin - Request Pattern Adoption Guide + +**Author**: Matthew Raymer +**Version**: 1.0.0 +**Created**: 2025-10-08 06:24:57 UTC + +## Overview + +This guide shows how to directly adopt the existing TimeSafari PWA request patterns (like `loadNewStarredProjectChanges()` and `getStarredProjectsWithChanges()`) into the Daily Notification Plugin configuration. The plugin is designed to work seamlessly with TimeSafari's existing axios-based request architecture. + +## Current TimeSafari PWA Pattern Analysis + +### Original TimeSafari PWA Code +```typescript +// TimeSafari PWA - HomeView.vue +private async loadNewStarredProjectChanges() { + if (this.activeDid && this.starredPlanHandleIds.length > 0) { + try { + const starredProjectChanges = await getStarredProjectsWithChanges( + this.axios, + this.apiServer, + this.activeDid, + this.starredPlanHandleIds, + this.lastAckedStarredPlanChangesJwtId, + ); + this.numNewStarredProjectChanges = starredProjectChanges.data.length; + this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; + } catch (error) { + logger.warn("[HomeView] Failed to load starred project changes:", error); + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + } + } +} + +// TimeSafari PWA - API function +export async function getStarredProjectsWithChanges( + axios: Axios, + apiServer: string, + activeDid: string, + starredPlanHandleIds: string[], + afterId?: string, +): Promise<{ data: Array; hitLimit: boolean }> { + const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`; + const headers = await getHeaders(activeDid); + + const requestBody = { + planIds: starredPlanHandleIds, + afterId: afterId, + }; + + const response = await axios.post(url, requestBody, { headers }); + return response.data; +} +``` + +## Plugin Configuration Adoption + +### 1. Direct Axios Integration Pattern + +The plugin can be configured to use the host's existing axios instance and request patterns: + +```typescript +// In TimeSafari PWA - Plugin Configuration +import { DailyNotification } from '@timesafari/daily-notification-plugin'; +import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; + +// Configure plugin with existing TimeSafari patterns +await DailyNotification.configure({ + // TimeSafari-specific configuration + timesafariConfig: { + activeDid: this.activeDid, + + // Use existing TimeSafari API endpoints + endpoints: { + offersToPerson: `${this.apiServer}/api/v2/offers/person`, + offersToPlans: `${this.apiServer}/api/v2/offers/plans`, + projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` + }, + + // Configure starred projects fetching + starredProjectsConfig: { + enabled: true, + starredPlanHandleIds: this.starredPlanHandleIds, + lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, + fetchInterval: '0 8 * * *', // Daily at 8 AM + maxResults: 50, + hitLimitHandling: 'warn' // 'warn' | 'error' | 'ignore' + }, + + // Sync configuration matching TimeSafari patterns + syncConfig: { + enableParallel: true, + maxConcurrent: 3, + batchSize: 10, + timeout: 30000, + retryAttempts: 3 + } + }, + + // Network configuration using existing patterns + networkConfig: { + // Use existing axios instance + httpClient: this.axios, + baseURL: this.apiServer, + timeout: 30000, + retryAttempts: 3, + retryDelay: 1000, + + // Headers matching TimeSafari pattern + defaultHeaders: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }, + + // Content fetch configuration + contentFetch: { + enabled: true, + schedule: '0 8 * * *', // Daily at 8 AM + + // Use existing TimeSafari request pattern + requestConfig: { + method: 'POST', + url: '${apiServer}/api/v2/report/plansLastUpdatedBetween', + headers: { + 'Authorization': 'Bearer ${jwt}', + 'X-User-DID': '${activeDid}' + }, + body: { + planIds: '${starredPlanHandleIds}', + afterId: '${lastAckedJwtId}' + } + }, + + // Callbacks matching TimeSafari error handling + callbacks: { + onSuccess: async (data: { data: Array; hitLimit: boolean }) => { + // Handle successful fetch - same as TimeSafari PWA + this.numNewStarredProjectChanges = data.data.length; + this.newStarredProjectChangesHitLimit = data.hitLimit; + + // Update UI + this.updateStarredProjectsUI(data); + }, + + onError: async (error: Error) => { + // Handle error - same as TimeSafari PWA + logger.warn("[DailyNotification] Failed to load starred project changes:", error); + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + }, + + onComplete: async (result: ContentFetchResult) => { + // Handle completion + logger.info("[DailyNotification] Starred projects fetch completed", { + success: result.success, + dataCount: result.data?.data?.length || 0, + hitLimit: result.data?.hitLimit || false + }); + } + } + } +}); +``` + +### 2. Enhanced TimeSafari Integration Service + +The plugin provides an enhanced integration service that mirrors TimeSafari's patterns: + +```typescript +// In TimeSafari PWA - Enhanced Integration +import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; + +// Initialize with existing TimeSafari configuration +const integrationService = TimeSafariIntegrationService.getInstance(); + +await integrationService.initialize({ + activeDid: this.activeDid, + storageAdapter: this.timeSafariStorageAdapter, + endorserApiBaseUrl: this.apiServer, + + // Use existing TimeSafari request patterns + requestConfig: { + httpClient: this.axios, + baseURL: this.apiServer, + timeout: 30000, + retryAttempts: 3 + }, + + // Configure starred projects fetching + starredProjectsConfig: { + enabled: true, + starredPlanHandleIds: this.starredPlanHandleIds, + lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, + fetchInterval: '0 8 * * *', + maxResults: 50 + } +}); + +// Use the service to fetch starred projects (same pattern as TimeSafari PWA) +const fetchStarredProjects = async () => { + try { + const starredProjectChanges = await integrationService.getStarredProjectsWithChanges( + this.activeDid, + this.starredPlanHandleIds, + this.lastAckedStarredPlanChangesJwtId + ); + + // Same handling as TimeSafari PWA + this.numNewStarredProjectChanges = starredProjectChanges.data.length; + this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; + + } catch (error) { + // Same error handling as TimeSafari PWA + logger.warn("[DailyNotification] Failed to load starred project changes:", error); + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + } +}; +``` + +### 3. Vue.js Component Integration + +Direct integration into existing TimeSafari Vue components: + +```typescript +// In TimeSafari PWA - HomeView.vue (enhanced) +import { defineComponent } from 'vue'; +import { DailyNotification } from '@timesafari/daily-notification-plugin'; +import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; + +export default defineComponent({ + name: 'HomeView', + + data() { + return { + // Existing TimeSafari data + activeDid: '', + starredPlanHandleIds: [] as string[], + lastAckedStarredPlanChangesJwtId: '', + numNewStarredProjectChanges: 0, + newStarredProjectChangesHitLimit: false, + + // Plugin integration + dailyNotificationService: null as DailyNotificationService | null, + integrationService: null as TimeSafariIntegrationService | null + }; + }, + + async mounted() { + // Initialize plugin with existing TimeSafari configuration + await this.initializeDailyNotifications(); + }, + + methods: { + async initializeDailyNotifications() { + try { + // Configure plugin with existing TimeSafari patterns + await DailyNotification.configure({ + timesafariConfig: { + activeDid: this.activeDid, + endpoints: { + projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` + }, + starredProjectsConfig: { + enabled: true, + starredPlanHandleIds: this.starredPlanHandleIds, + lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, + fetchInterval: '0 8 * * *' + } + }, + + networkConfig: { + httpClient: this.axios, + baseURL: this.apiServer, + timeout: 30000 + }, + + contentFetch: { + enabled: true, + schedule: '0 8 * * *', + callbacks: { + onSuccess: this.handleStarredProjectsSuccess, + onError: this.handleStarredProjectsError + } + } + }); + + // Initialize integration service + this.integrationService = TimeSafariIntegrationService.getInstance(); + await this.integrationService.initialize({ + activeDid: this.activeDid, + storageAdapter: this.timeSafariStorageAdapter, + endorserApiBaseUrl: this.apiServer + }); + + // Replace existing method with plugin-enhanced version + this.loadNewStarredProjectChanges = this.loadNewStarredProjectChangesEnhanced; + + } catch (error) { + logger.error("[HomeView] Failed to initialize daily notifications:", error); + } + }, + + // Enhanced version of existing method + async loadNewStarredProjectChangesEnhanced() { + if (this.activeDid && this.starredPlanHandleIds.length > 0) { + try { + // Use plugin's enhanced fetching with same interface + const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( + this.activeDid, + this.starredPlanHandleIds, + this.lastAckedStarredPlanChangesJwtId + ); + + // Same handling as original TimeSafari PWA + this.numNewStarredProjectChanges = starredProjectChanges.data.length; + this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; + + } catch (error) { + // Same error handling as original TimeSafari PWA + logger.warn("[HomeView] Failed to load starred project changes:", error); + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + } + } else { + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + } + }, + + // Callback handlers matching TimeSafari patterns + async handleStarredProjectsSuccess(data: { data: Array; hitLimit: boolean }) { + // Same handling as TimeSafari PWA + this.numNewStarredProjectChanges = data.data.length; + this.newStarredProjectChangesHitLimit = data.hitLimit; + + // Update UI + this.updateStarredProjectsUI(data); + }, + + async handleStarredProjectsError(error: Error) { + // Same error handling as TimeSafari PWA + logger.warn("[HomeView] Failed to load starred project changes:", error); + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + }, + + // Existing TimeSafari methods (unchanged) + updateStarredProjectsUI(data: { data: Array; hitLimit: boolean }) { + // Existing TimeSafari UI update logic + this.$emit('starred-projects-updated', data); + } + } +}); +``` + +## Plugin Implementation Details + +### 1. Enhanced TimeSafari Integration Service + +The plugin provides an enhanced version of TimeSafari's request patterns: + +```typescript +// Plugin implementation - Enhanced TimeSafari Integration Service +export class TimeSafariIntegrationService { + private static instance: TimeSafariIntegrationService; + private httpClient: AxiosInstance | null = null; + private baseURL: string = ''; + private activeDid: string = ''; + + static getInstance(): TimeSafariIntegrationService { + if (!TimeSafariIntegrationService.instance) { + TimeSafariIntegrationService.instance = new TimeSafariIntegrationService(); + } + return TimeSafariIntegrationService.instance; + } + + async initialize(config: { + activeDid: string; + httpClient: AxiosInstance; + baseURL: string; + storageAdapter: TimeSafariStorageAdapter; + endorserApiBaseUrl: string; + }): Promise { + this.activeDid = config.activeDid; + this.httpClient = config.httpClient; + this.baseURL = config.baseURL; + + // Initialize with existing TimeSafari patterns + await this.initializeStorage(config.storageAdapter); + await this.initializeAuthentication(config.endorserApiBaseUrl); + } + + // Enhanced version of TimeSafari's getStarredProjectsWithChanges + async getStarredProjectsWithChanges( + activeDid: string, + starredPlanHandleIds: string[], + afterId?: string + ): Promise<{ data: Array; hitLimit: boolean }> { + if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) { + return { data: [], hitLimit: false }; + } + + if (!afterId) { + return { data: [], hitLimit: false }; + } + + try { + // Use existing TimeSafari request pattern + const url = `${this.baseURL}/api/v2/report/plansLastUpdatedBetween`; + const headers = await this.getHeaders(activeDid); + + const requestBody = { + planIds: starredPlanHandleIds, + afterId: afterId, + }; + + // Use host's axios instance + const response = await this.httpClient!.post(url, requestBody, { headers }); + + // Log with structured logging + observability.logEvent('INFO', EVENT_CODES.FETCH_SUCCESS, 'Starred projects fetched successfully', { + activeDid, + planCount: starredPlanHandleIds.length, + resultCount: response.data.data.length, + hitLimit: response.data.hitLimit + }); + + return response.data; + + } catch (error) { + // Enhanced error handling with structured logging + observability.logEvent('ERROR', EVENT_CODES.FETCH_FAILURE, 'Failed to fetch starred projects', { + activeDid, + planCount: starredPlanHandleIds.length, + error: (error as Error).message + }); + + throw error; + } + } + + // Enhanced version of TimeSafari's getHeaders + private async getHeaders(activeDid: string): Promise> { + // Use existing TimeSafari authentication pattern + const jwt = await this.getJWTToken(activeDid); + + return { + 'Authorization': `Bearer ${jwt}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-User-DID': activeDid + }; + } + + // Enhanced JWT token management + private async getJWTToken(activeDid: string): Promise { + // Use existing TimeSafari JWT pattern + const cachedToken = await this.getCachedToken(activeDid); + if (cachedToken && !this.isTokenExpired(cachedToken)) { + return cachedToken; + } + + // Generate new token using existing TimeSafari pattern + const newToken = await this.generateJWTToken(activeDid); + await this.cacheToken(activeDid, newToken); + + return newToken; + } +} +``` + +### 2. Plugin Configuration Extensions + +The plugin extends the configuration to support TimeSafari's existing patterns: + +```typescript +// Plugin configuration extensions +export interface TimeSafariConfig { + activeDid: string; + + // Existing TimeSafari endpoints + endpoints?: { + offersToPerson?: string; + offersToPlans?: string; + projectsLastUpdated?: string; + }; + + // Enhanced starred projects configuration + starredProjectsConfig?: { + enabled: boolean; + starredPlanHandleIds: string[]; + lastAckedJwtId: string; + fetchInterval: string; // Cron expression + maxResults?: number; + hitLimitHandling?: 'warn' | 'error' | 'ignore'; + }; + + // Existing TimeSafari sync configuration + syncConfig?: { + enableParallel?: boolean; + maxConcurrent?: number; + batchSize?: number; + timeout?: number; + retryAttempts?: number; + }; + + // Enhanced error policy + errorPolicy?: { + maxRetries?: number; + backoffMultiplier?: number; + activeDidChangeRetries?: number; + starredProjectsRetries?: number; // New + }; +} + +export interface NetworkConfig { + // Use existing TimeSafari axios instance + httpClient?: AxiosInstance; + baseURL?: string; + timeout?: number; + retryAttempts?: number; + retryDelay?: number; + + // Existing TimeSafari headers + defaultHeaders?: Record; + + // Enhanced request configuration + requestConfig?: { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + url: string; + headers?: Record; + body?: Record; + params?: Record; + }; +} +``` + +## Migration Strategy + +### Phase 1: Parallel Implementation +1. **Keep existing TimeSafari PWA code unchanged** +2. **Add plugin configuration alongside existing code** +3. **Test plugin functionality in parallel** +4. **Compare results between existing and plugin implementations** + +### Phase 2: Gradual Migration +1. **Replace individual request methods one by one** +2. **Use plugin's enhanced error handling and logging** +3. **Maintain existing UI and user experience** +4. **Add plugin-specific features (background fetching, etc.)** + +### Phase 3: Full Integration +1. **Replace all TimeSafari request patterns with plugin** +2. **Remove duplicate code** +3. **Leverage plugin's advanced features** +4. **Optimize performance with plugin's caching and batching** + +## Benefits of Plugin Adoption + +### 1. Enhanced Error Handling +```typescript +// Existing TimeSafari PWA +catch (error) { + logger.warn("[HomeView] Failed to load starred project changes:", error); + this.numNewStarredProjectChanges = 0; +} + +// Plugin-enhanced version +catch (error) { + // Structured logging with event IDs + observability.logEvent('WARN', EVENT_CODES.FETCH_FAILURE, 'Failed to load starred project changes', { + eventId: this.generateEventId(), + activeDid: this.activeDid, + planCount: this.starredPlanHandleIds.length, + error: error.message, + retryCount: this.retryCount + }); + + // Enhanced fallback handling + await this.handleStarredProjectsFallback(error); +} +``` + +### 2. Background Fetching +```typescript +// Plugin provides background fetching +await DailyNotification.configure({ + contentFetch: { + enabled: true, + schedule: '0 8 * * *', // Daily at 8 AM + backgroundFetch: true, // Fetch in background + cachePolicy: { + maxAge: 3600, // 1 hour cache + staleWhileRevalidate: 1800 // 30 minutes stale + } + } +}); +``` + +### 3. Enhanced Observability +```typescript +// Plugin provides comprehensive metrics +const metrics = await DailyNotification.getMetrics(); +console.log('Starred Projects Metrics:', { + fetchSuccessRate: metrics.starredProjects.fetchSuccessRate, + averageResponseTime: metrics.starredProjects.averageResponseTime, + cacheHitRate: metrics.starredProjects.cacheHitRate, + errorRate: metrics.starredProjects.errorRate +}); +``` + +## Testing Strategy + +### 1. Parallel Testing +```typescript +// Test both implementations in parallel +const testStarredProjectsFetch = async () => { + // Existing TimeSafari PWA implementation + const existingResult = await getStarredProjectsWithChanges( + this.axios, + this.apiServer, + this.activeDid, + this.starredPlanHandleIds, + this.lastAckedStarredPlanChangesJwtId + ); + + // Plugin implementation + const pluginResult = await this.integrationService.getStarredProjectsWithChanges( + this.activeDid, + this.starredPlanHandleIds, + this.lastAckedStarredPlanChangesJwtId + ); + + // Compare results + assert.deepEqual(existingResult, pluginResult); +}; +``` + +### 2. Performance Testing +```typescript +// Compare performance +const performanceTest = async () => { + const start = Date.now(); + + // Existing implementation + await getStarredProjectsWithChanges(...); + const existingTime = Date.now() - start; + + const pluginStart = Date.now(); + // Plugin implementation + await this.integrationService.getStarredProjectsWithChanges(...); + const pluginTime = Date.now() - pluginStart; + + console.log('Performance Comparison:', { + existing: existingTime, + plugin: pluginTime, + improvement: ((existingTime - pluginTime) / existingTime * 100).toFixed(2) + '%' + }); +}; +``` + +## Conclusion + +The Daily Notification Plugin is designed to seamlessly adopt TimeSafari's existing request patterns while providing enhanced functionality: + +- **Same Interface**: Plugin methods match existing TimeSafari patterns +- **Enhanced Features**: Background fetching, structured logging, metrics +- **Gradual Migration**: Can be adopted incrementally +- **Backward Compatibility**: Existing code continues to work +- **Performance Improvements**: Caching, batching, and optimization + +The plugin transforms TimeSafari's existing `loadNewStarredProjectChanges()` pattern into a more robust, observable, and efficient system while maintaining the same developer experience and user interface. + +--- + +**Next Steps**: +1. Implement parallel testing with existing TimeSafari PWA code +2. Gradually migrate individual request methods +3. Leverage plugin's advanced features for enhanced user experience diff --git a/examples/timesafari-pwa-integration-example.ts b/examples/timesafari-pwa-integration-example.ts new file mode 100644 index 0000000..46dfeb6 --- /dev/null +++ b/examples/timesafari-pwa-integration-example.ts @@ -0,0 +1,403 @@ +/** + * TimeSafari PWA Integration Example + * + * This example shows how to directly adopt the existing TimeSafari PWA + * request patterns (like loadNewStarredProjectChanges) into the Daily + * Notification Plugin configuration. + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { DailyNotification } from '@timesafari/daily-notification-plugin'; +import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; +import { AxiosInstance } from 'axios'; + +// TimeSafari PWA existing interfaces (for reference) +interface PlanSummaryAndPreviousClaim { + id: string; + title: string; + description: string; + lastUpdated: string; + previousClaim?: unknown; +} + +interface StarredProjectsResponse { + data: Array; + hitLimit: boolean; +} + +// TimeSafari PWA existing class (for reference) +class TimeSafariHomeView { + // Existing TimeSafari PWA properties + activeDid: string = ''; + starredPlanHandleIds: string[] = []; + lastAckedStarredPlanChangesJwtId: string = ''; + numNewStarredProjectChanges: number = 0; + newStarredProjectChangesHitLimit: boolean = false; + apiServer: string = 'https://endorser.ch'; + axios: AxiosInstance; + + // Plugin integration properties + private dailyNotificationService: DailyNotification | null = null; + private integrationService: TimeSafariIntegrationService | null = null; + + constructor(axiosInstance: AxiosInstance) { + this.axios = axiosInstance; + } + + /** + * Initialize Daily Notification Plugin with existing TimeSafari patterns + */ + async initializeDailyNotifications(): Promise { + try { + // Configure plugin with existing TimeSafari configuration + await DailyNotification.configure({ + // TimeSafari-specific configuration + timesafariConfig: { + activeDid: this.activeDid, + + // Use existing TimeSafari API endpoints + endpoints: { + offersToPerson: `${this.apiServer}/api/v2/offers/person`, + offersToPlans: `${this.apiServer}/api/v2/offers/plans`, + projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` + }, + + // Configure starred projects fetching (matches existing TimeSafari pattern) + starredProjectsConfig: { + enabled: true, + starredPlanHandleIds: this.starredPlanHandleIds, + lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, + fetchInterval: '0 8 * * *', // Daily at 8 AM + maxResults: 50, + hitLimitHandling: 'warn' // 'warn' | 'error' | 'ignore' + }, + + // Sync configuration matching TimeSafari patterns + syncConfig: { + enableParallel: true, + maxConcurrent: 3, + batchSize: 10, + timeout: 30000, + retryAttempts: 3 + } + }, + + // Network configuration using existing TimeSafari patterns + networkConfig: { + // Use existing axios instance + httpClient: this.axios, + baseURL: this.apiServer, + timeout: 30000, + retryAttempts: 3, + retryDelay: 1000, + + // Headers matching TimeSafari pattern + defaultHeaders: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }, + + // Content fetch configuration + contentFetch: { + enabled: true, + schedule: '0 8 * * *', // Daily at 8 AM + + // Use existing TimeSafari request pattern + requestConfig: { + method: 'POST', + url: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween`, + headers: { + 'Authorization': 'Bearer ${jwt}', + 'X-User-DID': '${activeDid}' + }, + body: { + planIds: '${starredPlanHandleIds}', + afterId: '${lastAckedJwtId}' + } + }, + + // Callbacks matching TimeSafari error handling + callbacks: { + onSuccess: this.handleStarredProjectsSuccess.bind(this), + onError: this.handleStarredProjectsError.bind(this), + onComplete: this.handleStarredProjectsComplete.bind(this) + } + } + }); + + // Initialize TimeSafari integration service + this.integrationService = TimeSafariIntegrationService.getInstance(); + await this.integrationService.initialize({ + activeDid: this.activeDid, + storageAdapter: this.getTimeSafariStorageAdapter(), + endorserApiBaseUrl: this.apiServer, + + // Use existing TimeSafari request patterns + requestConfig: { + httpClient: this.axios, + baseURL: this.apiServer, + timeout: 30000, + retryAttempts: 3 + }, + + // Configure starred projects fetching + starredProjectsConfig: { + enabled: true, + starredPlanHandleIds: this.starredPlanHandleIds, + lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, + fetchInterval: '0 8 * * *', + maxResults: 50 + } + }); + + console.log('Daily notifications initialized successfully with TimeSafari patterns'); + + } catch (error) { + console.error('Failed to initialize daily notifications:', error); + throw error; + } + } + + /** + * Enhanced version of existing TimeSafari loadNewStarredProjectChanges method + * + * This method replaces the existing TimeSafari PWA method with plugin-enhanced + * functionality while maintaining the same interface and behavior. + */ + async loadNewStarredProjectChanges(): Promise { + if (this.activeDid && this.starredPlanHandleIds.length > 0) { + try { + // Use plugin's enhanced fetching with same interface as TimeSafari PWA + const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( + this.activeDid, + this.starredPlanHandleIds, + this.lastAckedStarredPlanChangesJwtId + ); + + // Same handling as original TimeSafari PWA + this.numNewStarredProjectChanges = starredProjectChanges.data.length; + this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; + + // Enhanced logging with structured observability + console.log('Starred projects loaded successfully:', { + count: this.numNewStarredProjectChanges, + hitLimit: this.newStarredProjectChangesHitLimit, + planIds: this.starredPlanHandleIds.length + }); + + } catch (error) { + // Same error handling as original TimeSafari PWA + console.warn('[HomeView] Failed to load starred project changes:', error); + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + } + } else { + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + } + } + + /** + * Callback handlers matching TimeSafari patterns + */ + async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise { + // Same handling as TimeSafari PWA + this.numNewStarredProjectChanges = data.data.length; + this.newStarredProjectChangesHitLimit = data.hitLimit; + + // Update UI (existing TimeSafari method) + this.updateStarredProjectsUI(data); + + // Enhanced logging + console.log('Starred projects success callback:', { + count: data.data.length, + hitLimit: data.hitLimit + }); + } + + async handleStarredProjectsError(error: Error): Promise { + // Same error handling as TimeSafari PWA + console.warn('[HomeView] Failed to load starred project changes:', error); + this.numNewStarredProjectChanges = 0; + this.newStarredProjectChangesHitLimit = false; + + // Enhanced error handling with structured logging + console.error('Starred projects error callback:', { + error: error.message, + activeDid: this.activeDid, + planCount: this.starredPlanHandleIds.length + }); + } + + async handleStarredProjectsComplete(result: unknown): Promise { + // Handle completion + console.log('Starred projects fetch completed:', result); + } + + /** + * Existing TimeSafari methods (unchanged) + */ + private updateStarredProjectsUI(data: StarredProjectsResponse): void { + // Existing TimeSafari UI update logic + console.log('Updating UI with starred projects data:', data); + } + + private getTimeSafariStorageAdapter(): unknown { + // Return existing TimeSafari storage adapter + return { + // TimeSafari storage adapter implementation + }; + } +} + +// Usage example +export async function initializeTimeSafariWithPlugin(axiosInstance: AxiosInstance): Promise { + const homeView = new TimeSafariHomeView(axiosInstance); + + // Set up TimeSafari data (existing pattern) + homeView.activeDid = 'did:example:123'; + homeView.starredPlanHandleIds = ['plan1', 'plan2', 'plan3']; + homeView.lastAckedStarredPlanChangesJwtId = 'jwt123'; + + // Initialize plugin with TimeSafari patterns + await homeView.initializeDailyNotifications(); + + // Test the enhanced method + await homeView.loadNewStarredProjectChanges(); + + return homeView; +} + +// Comparison example: Original vs Plugin-enhanced +export class TimeSafariComparisonExample { + private axios: AxiosInstance; + + constructor(axiosInstance: AxiosInstance) { + this.axios = axiosInstance; + } + + /** + * Original TimeSafari PWA method (for comparison) + */ + async loadNewStarredProjectChangesOriginal( + activeDid: string, + starredPlanHandleIds: string[], + lastAckedJwtId: string + ): Promise { + if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) { + return { data: [], hitLimit: false }; + } + + if (!lastAckedJwtId) { + return { data: [], hitLimit: false }; + } + + const url = `https://endorser.ch/api/v2/report/plansLastUpdatedBetween`; + const headers = await this.getHeaders(activeDid); + + const requestBody = { + planIds: starredPlanHandleIds, + afterId: lastAckedJwtId, + }; + + const response = await this.axios.post(url, requestBody, { headers }); + return response.data; + } + + /** + * Plugin-enhanced version (same interface, enhanced functionality) + */ + async loadNewStarredProjectChangesEnhanced( + activeDid: string, + starredPlanHandleIds: string[], + lastAckedJwtId: string + ): Promise { + // Use plugin's enhanced integration service + const integrationService = TimeSafariIntegrationService.getInstance(); + + return await integrationService.getStarredProjectsWithChanges( + activeDid, + starredPlanHandleIds, + lastAckedJwtId + ); + } + + /** + * Test both implementations in parallel + */ + async testBothImplementations( + activeDid: string, + starredPlanHandleIds: string[], + lastAckedJwtId: string + ): Promise { + console.log('Testing both implementations in parallel...'); + + const start = Date.now(); + + // Test original implementation + const originalResult = await this.loadNewStarredProjectChangesOriginal( + activeDid, + starredPlanHandleIds, + lastAckedJwtId + ); + const originalTime = Date.now() - start; + + const pluginStart = Date.now(); + + // Test plugin-enhanced implementation + const pluginResult = await this.loadNewStarredProjectChangesEnhanced( + activeDid, + starredPlanHandleIds, + lastAckedJwtId + ); + const pluginTime = Date.now() - pluginStart; + + // Compare results + console.log('Comparison Results:', { + original: { + time: originalTime, + count: originalResult.data.length, + hitLimit: originalResult.hitLimit + }, + plugin: { + time: pluginTime, + count: pluginResult.data.length, + hitLimit: pluginResult.hitLimit + }, + performance: { + improvement: ((originalTime - pluginTime) / originalTime * 100).toFixed(2) + '%' + } + }); + + // Verify results match + if (JSON.stringify(originalResult) === JSON.stringify(pluginResult)) { + console.log('✅ Results match perfectly!'); + } else { + console.log('❌ Results differ:', { + original: originalResult, + plugin: pluginResult + }); + } + } + + private async getHeaders(activeDid: string): Promise> { + // Existing TimeSafari header generation + return { + 'Authorization': `Bearer ${await this.getJWTToken(activeDid)}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-User-DID': activeDid + }; + } + + private async getJWTToken(activeDid: string): Promise { + // Existing TimeSafari JWT token generation + return 'jwt-token-placeholder'; + } +} + +// Export for use in TimeSafari PWA +export { TimeSafariHomeView, TimeSafariComparisonExample };