feat: Update test-apps for TimeSafari integration with Endorser.ch API patterns
- Add comprehensive configuration system with timesafari-config.json - Create shared config-loader.ts with TypeScript interfaces and mock services - Update Android test app to use TimeSafari community notification patterns - Update iOS test app with rolling window and community features - Update Electron test app with desktop-specific TimeSafari integration - Enhance test API server to simulate Endorser.ch API endpoints - Add pagination support with afterId/beforeId parameters - Implement parallel API requests pattern for offers, projects, people, items - Add community analytics and notification bundle endpoints - Update all test app UIs for TimeSafari-specific functionality - Update README with comprehensive TimeSafari testing guide All test apps now demonstrate: - Real Endorser.ch API integration patterns - TimeSafari community-building features - Platform-specific optimizations (Android/iOS/Electron) - Comprehensive error handling and performance monitoring - Configuration-driven testing with type safety
This commit is contained in:
1612
INTEGRATION_GUIDE.md
Normal file
1612
INTEGRATION_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
# Test Apps Setup Guide
|
# TimeSafari Test Apps Setup Guide
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This guide creates minimal Capacitor test apps for validating the Daily Notification Plugin across all target platforms.
|
This guide creates minimal Capacitor test apps for validating the TimeSafari Daily Notification Plugin integration across all target platforms. The test apps demonstrate TimeSafari's community-building features, Endorser.ch API integration, and notification patterns.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
@@ -11,7 +11,11 @@ test-apps/
|
|||||||
├── android-test/ # Android test app
|
├── android-test/ # Android test app
|
||||||
├── ios-test/ # iOS test app
|
├── ios-test/ # iOS test app
|
||||||
├── electron-test/ # Electron test app
|
├── electron-test/ # Electron test app
|
||||||
├── test-api/ # Test API server
|
├── test-api/ # TimeSafari Test API server
|
||||||
|
├── shared/ # Shared configuration and utilities
|
||||||
|
│ └── config-loader.ts # Configuration loader and mock services
|
||||||
|
├── config/ # Configuration files
|
||||||
|
│ └── timesafari-config.json
|
||||||
├── setup-android.sh # Android setup script
|
├── setup-android.sh # Android setup script
|
||||||
├── setup-ios.sh # iOS setup script
|
├── setup-ios.sh # iOS setup script
|
||||||
├── setup-electron.sh # Electron setup script
|
├── setup-electron.sh # Electron setup script
|
||||||
@@ -27,6 +31,8 @@ test-apps/
|
|||||||
- Android Studio (for Android)
|
- Android Studio (for Android)
|
||||||
- Xcode (for iOS)
|
- Xcode (for iOS)
|
||||||
- Platform-specific SDKs
|
- Platform-specific SDKs
|
||||||
|
- Understanding of TimeSafari's community-building purpose
|
||||||
|
- Familiarity with Endorser.ch API patterns
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -61,8 +67,9 @@ See [Enhanced Setup Guide](SETUP_GUIDE.md) for detailed manual setup instruction
|
|||||||
## Test App Features
|
## Test App Features
|
||||||
|
|
||||||
Each test app includes:
|
Each test app includes:
|
||||||
- **Plugin Configuration**: Test shared SQLite, TTL, prefetch settings
|
- **TimeSafari Configuration**: Test community-focused notification settings
|
||||||
- **Notification Scheduling**: Basic daily notification setup
|
- **Endorser.ch API Integration**: Test real API patterns with pagination
|
||||||
|
- **Community Notification Scheduling**: Test offers, projects, people, and items notifications
|
||||||
- **Platform-Specific Features**:
|
- **Platform-Specific Features**:
|
||||||
- Android: Exact alarm permissions, reboot recovery
|
- Android: Exact alarm permissions, reboot recovery
|
||||||
- iOS: Rolling window management, BGTaskScheduler
|
- iOS: Rolling window management, BGTaskScheduler
|
||||||
@@ -71,13 +78,13 @@ Each test app includes:
|
|||||||
- **Error Handling**: Comprehensive error testing
|
- **Error Handling**: Comprehensive error testing
|
||||||
- **Debug Information**: Platform-specific debug data
|
- **Debug Information**: Platform-specific debug data
|
||||||
|
|
||||||
## Test API Server
|
## TimeSafari Test API Server
|
||||||
|
|
||||||
A mock REST API server (`test-api/`) provides endpoints for testing the plugin's network functionality:
|
A comprehensive REST API server (`test-api/`) simulates Endorser.ch API endpoints for testing the plugin's TimeSafari-specific functionality:
|
||||||
|
|
||||||
### Quick Start
|
### Quick Start
|
||||||
```bash
|
```bash
|
||||||
# Start the test API server
|
# Start the TimeSafari Test API server
|
||||||
cd test-apps/test-api
|
cd test-apps/test-api
|
||||||
npm install
|
npm install
|
||||||
npm start
|
npm start
|
||||||
@@ -87,19 +94,30 @@ npm run demo
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
- **Content Endpoints**: Generate mock notification content
|
- **Endorser.ch API Simulation**: Mock endpoints for offers, projects, and pagination
|
||||||
- **ETag Support**: Full HTTP caching with conditional requests
|
- **TimeSafari Notification Bundle**: Single route for bundled notifications
|
||||||
|
- **Community Analytics**: Analytics endpoint for community events
|
||||||
|
- **Pagination Support**: Full afterId/beforeId pagination testing
|
||||||
|
- **ETag Support**: HTTP caching with conditional requests
|
||||||
- **Error Simulation**: Test various error scenarios
|
- **Error Simulation**: Test various error scenarios
|
||||||
- **Metrics**: Monitor API usage and performance
|
- **Metrics**: Monitor API usage and performance
|
||||||
- **CORS Enabled**: Cross-origin requests supported
|
- **CORS Enabled**: Cross-origin requests supported
|
||||||
|
|
||||||
### API Endpoints
|
### API Endpoints
|
||||||
|
|
||||||
|
#### Endorser.ch API Endpoints
|
||||||
|
- `GET /api/v2/report/offers` - Get offers to person
|
||||||
|
- `GET /api/v2/report/offersToPlansOwnedByMe` - Get offers to user's projects
|
||||||
|
- `POST /api/v2/report/plansLastUpdatedBetween` - Get changes to starred projects
|
||||||
|
|
||||||
|
#### TimeSafari API Endpoints
|
||||||
|
- `GET /api/v2/report/notifications/bundle` - Get bundled notifications
|
||||||
|
- `POST /api/analytics/community-events` - Send community analytics
|
||||||
|
|
||||||
|
#### Legacy Endpoints
|
||||||
- `GET /health` - Health check
|
- `GET /health` - Health check
|
||||||
- `GET /api/content/:slotId` - Get notification content
|
- `GET /api/content/:slotId` - Get notification content
|
||||||
- `GET /api/error/:type` - Simulate errors
|
|
||||||
- `GET /api/metrics` - API metrics
|
- `GET /api/metrics` - API metrics
|
||||||
- `PUT /api/content/:slotId` - Update content
|
|
||||||
- `DELETE /api/content` - Clear all content
|
|
||||||
|
|
||||||
### Platform-Specific URLs
|
### Platform-Specific URLs
|
||||||
- **Web/Electron**: `http://localhost:3001`
|
- **Web/Electron**: `http://localhost:3001`
|
||||||
@@ -110,19 +128,25 @@ npm run demo
|
|||||||
## Platform-Specific Testing
|
## Platform-Specific Testing
|
||||||
|
|
||||||
### Android Test App
|
### Android Test App
|
||||||
|
- **TimeSafari Configuration**: Test community notification settings
|
||||||
|
- **Endorser.ch API Integration**: Test parallel API requests
|
||||||
- **Exact Alarm Status**: Check permission and capability
|
- **Exact Alarm Status**: Check permission and capability
|
||||||
- **Permission Requests**: Test exact alarm permission flow
|
- **Permission Requests**: Test exact alarm permission flow
|
||||||
- **Performance Metrics**: Monitor Android-specific optimizations
|
- **Performance Metrics**: Monitor Android-specific optimizations
|
||||||
- **Reboot Recovery**: Validate system restart handling
|
- **Reboot Recovery**: Validate system restart handling
|
||||||
|
|
||||||
### iOS Test App
|
### iOS Test App
|
||||||
|
- **TimeSafari Configuration**: Test iOS community features
|
||||||
- **Rolling Window**: Test notification limit management
|
- **Rolling Window**: Test notification limit management
|
||||||
|
- **Endorser.ch API Integration**: Test pagination patterns
|
||||||
- **Background Tasks**: Validate BGTaskScheduler integration
|
- **Background Tasks**: Validate BGTaskScheduler integration
|
||||||
- **Performance Metrics**: Monitor iOS-specific optimizations
|
- **Performance Metrics**: Monitor iOS-specific optimizations
|
||||||
- **Memory Management**: Test object pooling and cleanup
|
- **Memory Management**: Test object pooling and cleanup
|
||||||
|
|
||||||
### Electron Test App
|
### Electron Test App
|
||||||
|
- **TimeSafari Configuration**: Test Electron community features
|
||||||
- **Mock Implementations**: Test web platform compatibility
|
- **Mock Implementations**: Test web platform compatibility
|
||||||
|
- **Endorser.ch API Integration**: Test API patterns
|
||||||
- **IPC Communication**: Validate Electron-specific APIs
|
- **IPC Communication**: Validate Electron-specific APIs
|
||||||
- **Development Workflow**: Test plugin integration
|
- **Development Workflow**: Test plugin integration
|
||||||
- **Debug Information**: Platform-specific status display
|
- **Debug Information**: Platform-specific status display
|
||||||
@@ -155,8 +179,9 @@ npm run dev # Run in development mode
|
|||||||
## Testing Checklist
|
## Testing Checklist
|
||||||
|
|
||||||
### Core Functionality
|
### Core Functionality
|
||||||
- [ ] Plugin configuration works
|
- [ ] TimeSafari configuration works
|
||||||
- [ ] Notification scheduling succeeds
|
- [ ] Community notification scheduling succeeds
|
||||||
|
- [ ] Endorser.ch API integration functions properly
|
||||||
- [ ] Error handling functions properly
|
- [ ] Error handling functions properly
|
||||||
- [ ] Performance metrics are accurate
|
- [ ] Performance metrics are accurate
|
||||||
|
|
||||||
@@ -166,9 +191,11 @@ npm run dev # Run in development mode
|
|||||||
- [ ] Electron mock implementations
|
- [ ] Electron mock implementations
|
||||||
- [ ] Cross-platform API consistency
|
- [ ] Cross-platform API consistency
|
||||||
|
|
||||||
### Integration
|
### TimeSafari Integration
|
||||||
- [ ] Plugin loads without errors
|
- [ ] Plugin loads without errors
|
||||||
- [ ] Configuration persists across sessions
|
- [ ] Configuration persists across sessions
|
||||||
|
- [ ] Endorser.ch API pagination works
|
||||||
|
- [ ] Community notification types process correctly
|
||||||
- [ ] Performance optimizations active
|
- [ ] Performance optimizations active
|
||||||
- [ ] Debug information accessible
|
- [ ] Debug information accessible
|
||||||
|
|
||||||
@@ -179,6 +206,7 @@ npm run dev # Run in development mode
|
|||||||
2. **"android platform has not been added yet"** → Run `npx cap add android` first
|
2. **"android platform has not been added yet"** → Run `npx cap add android` first
|
||||||
3. **Build failures** → Check Node.js version (18+) and clear cache: `npm cache clean --force`
|
3. **Build failures** → Check Node.js version (18+) and clear cache: `npm cache clean --force`
|
||||||
4. **Platform errors** → Verify platform-specific SDKs are installed
|
4. **Platform errors** → Verify platform-specific SDKs are installed
|
||||||
|
5. **API connection errors** → Ensure test API server is running on port 3001
|
||||||
|
|
||||||
### Quick Fixes
|
### Quick Fixes
|
||||||
```bash
|
```bash
|
||||||
@@ -193,6 +221,9 @@ npx cap clean
|
|||||||
|
|
||||||
# Re-sync platforms
|
# Re-sync platforms
|
||||||
npx cap sync
|
npx cap sync
|
||||||
|
|
||||||
|
# Restart test API server
|
||||||
|
cd test-api && npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Detailed Help
|
### Detailed Help
|
||||||
@@ -201,7 +232,9 @@ See [Enhanced Setup Guide](SETUP_GUIDE.md) for comprehensive troubleshooting and
|
|||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. **Run Setup Scripts**: Execute platform-specific setup
|
1. **Run Setup Scripts**: Execute platform-specific setup
|
||||||
2. **Test Core Features**: Validate basic functionality
|
2. **Start Test API Server**: Run the TimeSafari Test API server
|
||||||
3. **Test Platform Features**: Verify platform-specific capabilities
|
3. **Test Core Features**: Validate basic TimeSafari functionality
|
||||||
4. **Integration Testing**: Test with actual plugin implementation
|
4. **Test Platform Features**: Verify platform-specific capabilities
|
||||||
5. **Performance Validation**: Monitor metrics and optimizations
|
5. **Test Endorser.ch Integration**: Validate API patterns and pagination
|
||||||
|
6. **Integration Testing**: Test with actual plugin implementation
|
||||||
|
7. **Performance Validation**: Monitor metrics and optimizations
|
||||||
|
|||||||
@@ -89,15 +89,16 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>📱 Daily Notification Plugin - Android Test</h1>
|
<h1>📱 TimeSafari Daily Notification - Android Test</h1>
|
||||||
|
|
||||||
<div class="status" id="status">Ready</div>
|
<div class="status" id="status">Ready</div>
|
||||||
|
|
||||||
<div class="button-grid">
|
<div class="button-grid">
|
||||||
<button id="configure">Configure Plugin</button>
|
<button id="configure">Configure TimeSafari</button>
|
||||||
<button id="schedule">Schedule Notification</button>
|
<button id="schedule">Schedule Community Notifications</button>
|
||||||
<button id="alarm-status">Check Alarm Status</button>
|
<button id="endorser-api">Test Endorser.ch API</button>
|
||||||
<button id="request-permission">Request Permission</button>
|
<button id="callbacks">Register Callbacks</button>
|
||||||
|
<button id="status">Check Status</button>
|
||||||
<button id="performance">Performance Metrics</button>
|
<button id="performance">Performance Metrics</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,209 @@
|
|||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader';
|
||||||
|
|
||||||
// Mock plugin for development
|
// Test interface for TimeSafari Android integration
|
||||||
const DailyNotification = {
|
class TimeSafariAndroidTestApp {
|
||||||
async configure(options: any) {
|
private statusElement: HTMLElement;
|
||||||
console.log('Configure called:', options);
|
private logElement: HTMLElement;
|
||||||
return Promise.resolve();
|
private configLoader: ConfigLoader;
|
||||||
},
|
private notificationService: MockDailyNotificationService;
|
||||||
async scheduleDailyNotification(options: any) {
|
private logger: TestLogger;
|
||||||
console.log('Schedule called:', options);
|
|
||||||
return Promise.resolve();
|
constructor() {
|
||||||
},
|
this.statusElement = document.getElementById('status')!;
|
||||||
async getExactAlarmStatus() {
|
this.logElement = document.getElementById('log')!;
|
||||||
return Promise.resolve({
|
this.configLoader = ConfigLoader.getInstance();
|
||||||
supported: true,
|
this.logger = new TestLogger('debug');
|
||||||
enabled: false,
|
this.notificationService = new MockDailyNotificationService(this.configLoader.getConfig());
|
||||||
canSchedule: false,
|
this.setupEventListeners();
|
||||||
fallbackWindow: '±10 minutes'
|
this.log('TimeSafari Android Test app initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEventListeners() {
|
||||||
|
document.getElementById('configure')?.addEventListener('click', () => this.testConfigure());
|
||||||
|
document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule());
|
||||||
|
document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI());
|
||||||
|
document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks());
|
||||||
|
document.getElementById('status')?.addEventListener('click', () => this.testStatus());
|
||||||
|
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
|
||||||
|
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testConfigure() {
|
||||||
|
try {
|
||||||
|
this.log('Testing TimeSafari configuration...');
|
||||||
|
await this.configLoader.loadConfig();
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
|
||||||
|
await this.notificationService.initialize();
|
||||||
|
|
||||||
|
this.log('✅ TimeSafari configuration successful', {
|
||||||
|
appId: config.timesafari.appId,
|
||||||
|
appName: config.timesafari.appName,
|
||||||
|
version: config.timesafari.version
|
||||||
});
|
});
|
||||||
|
this.updateStatus('Configured');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Configuration failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testSchedule() {
|
||||||
|
try {
|
||||||
|
this.log('Testing TimeSafari community notification scheduling...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
|
||||||
|
const dualConfig = {
|
||||||
|
contentFetch: {
|
||||||
|
enabled: true,
|
||||||
|
schedule: config.scheduling.contentFetch.schedule,
|
||||||
|
url: this.configLoader.getEndorserUrl('notificationsBundle'),
|
||||||
|
headers: this.configLoader.getAuthHeaders(),
|
||||||
|
ttlSeconds: 3600,
|
||||||
|
timeout: 30000,
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 5000,
|
||||||
|
callbacks: {
|
||||||
|
onSuccess: async (data: any) => {
|
||||||
|
this.log('✅ Content fetch successful', data);
|
||||||
|
await this.processEndorserNotificationBundle(data);
|
||||||
},
|
},
|
||||||
async requestExactAlarmPermission() {
|
onError: async (error: any) => {
|
||||||
console.log('Request exact alarm permission');
|
this.log('❌ Content fetch failed', error);
|
||||||
return Promise.resolve();
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async getPerformanceMetrics() {
|
userNotification: {
|
||||||
return Promise.resolve({
|
enabled: true,
|
||||||
|
schedule: config.scheduling.userNotification.schedule,
|
||||||
|
title: 'TimeSafari Community Update',
|
||||||
|
body: 'New offers, projects, people, and items await your attention!',
|
||||||
|
sound: true,
|
||||||
|
vibration: true,
|
||||||
|
priority: 'high',
|
||||||
|
actions: [
|
||||||
|
{ id: 'view_offers', title: 'View Offers' },
|
||||||
|
{ id: 'view_projects', title: 'See Projects' },
|
||||||
|
{ id: 'view_people', title: 'Check People' },
|
||||||
|
{ id: 'view_items', title: 'Browse Items' },
|
||||||
|
{ id: 'dismiss', title: 'Dismiss' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
relationship: {
|
||||||
|
autoLink: true,
|
||||||
|
contentTimeout: 300000,
|
||||||
|
fallbackBehavior: 'show_default'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.notificationService.scheduleDualNotification(dualConfig);
|
||||||
|
this.log('✅ Community notification scheduled successfully');
|
||||||
|
this.updateStatus('Scheduled');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Scheduling failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testEndorserAPI() {
|
||||||
|
try {
|
||||||
|
this.log('Testing Endorser.ch API integration...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
const testData = config.testData;
|
||||||
|
|
||||||
|
// Test parallel API requests pattern
|
||||||
|
const requests = [
|
||||||
|
// Offers to person
|
||||||
|
fetch(`${this.configLoader.getEndorserUrl('offers')}?recipientId=${testData.userDid}&afterId=${testData.lastKnownOfferId}`, {
|
||||||
|
headers: this.configLoader.getAuthHeaders()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Offers to user's projects
|
||||||
|
fetch(`${this.configLoader.getEndorserUrl('offersToPlans')}?afterId=${testData.lastKnownOfferId}`, {
|
||||||
|
headers: this.configLoader.getAuthHeaders()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Changes to starred projects
|
||||||
|
fetch(this.configLoader.getEndorserUrl('plansLastUpdated'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.configLoader.getAuthHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
planIds: testData.starredPlanIds,
|
||||||
|
afterId: testData.lastKnownPlanId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests);
|
||||||
|
|
||||||
|
const notificationData = {
|
||||||
|
offersToPerson: await offersToPerson.json(),
|
||||||
|
offersToProjects: await offersToProjects.json(),
|
||||||
|
starredChanges: await starredChanges.json()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.log('✅ Endorser.ch API integration successful', {
|
||||||
|
offersToPerson: notificationData.offersToPerson.data?.length || 0,
|
||||||
|
offersToProjects: notificationData.offersToProjects.data?.length || 0,
|
||||||
|
starredChanges: notificationData.starredChanges.data?.length || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateStatus('API Connected');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Endorser.ch API test failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testCallbacks() {
|
||||||
|
try {
|
||||||
|
this.log('Testing TimeSafari notification callbacks...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
|
||||||
|
// Register offers callback
|
||||||
|
await this.notificationService.registerCallback('offers', async (event: any) => {
|
||||||
|
this.log('📨 Offers callback triggered', event);
|
||||||
|
await this.handleOffersNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register projects callback
|
||||||
|
await this.notificationService.registerCallback('projects', async (event: any) => {
|
||||||
|
this.log('📨 Projects callback triggered', event);
|
||||||
|
await this.handleProjectsNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register people callback
|
||||||
|
await this.notificationService.registerCallback('people', async (event: any) => {
|
||||||
|
this.log('📨 People callback triggered', event);
|
||||||
|
await this.handlePeopleNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register items callback
|
||||||
|
await this.notificationService.registerCallback('items', async (event: any) => {
|
||||||
|
this.log('📨 Items callback triggered', event);
|
||||||
|
await this.handleItemsNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('✅ All callbacks registered successfully');
|
||||||
|
this.updateStatus('Callbacks Registered');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Callback registration failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testStatus() {
|
||||||
|
try {
|
||||||
|
this.log('Testing notification status...');
|
||||||
|
const status = await this.notificationService.getDualScheduleStatus();
|
||||||
|
this.log('📊 Notification Status:', status);
|
||||||
|
this.updateStatus(`Status: ${status.contentFetch.enabled ? 'Active' : 'Inactive'}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Status check failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testPerformance() {
|
||||||
|
try {
|
||||||
|
this.log('Testing Android performance metrics...');
|
||||||
|
const metrics = {
|
||||||
overallScore: 85,
|
overallScore: 85,
|
||||||
databasePerformance: 90,
|
databasePerformance: 90,
|
||||||
memoryEfficiency: 80,
|
memoryEfficiency: 80,
|
||||||
@@ -35,98 +215,106 @@ const DailyNotification = {
|
|||||||
backgroundCpuUsage: 2.3,
|
backgroundCpuUsage: 2.3,
|
||||||
totalNetworkRequests: 12,
|
totalNetworkRequests: 12,
|
||||||
recommendations: ['Enable ETag support', 'Optimize memory usage']
|
recommendations: ['Enable ETag support', 'Optimize memory usage']
|
||||||
});
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test interface
|
this.log('📊 Android Performance Metrics:', metrics);
|
||||||
class TestApp {
|
|
||||||
private statusElement: HTMLElement;
|
|
||||||
private logElement: HTMLElement;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.statusElement = document.getElementById('status')!;
|
|
||||||
this.logElement = document.getElementById('log')!;
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.log('Test app initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupEventListeners() {
|
|
||||||
document.getElementById('configure')?.addEventListener('click', () => this.testConfigure());
|
|
||||||
document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule());
|
|
||||||
document.getElementById('alarm-status')?.addEventListener('click', () => this.testAlarmStatus());
|
|
||||||
document.getElementById('request-permission')?.addEventListener('click', () => this.testRequestPermission());
|
|
||||||
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
|
|
||||||
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testConfigure() {
|
|
||||||
try {
|
|
||||||
this.log('Testing configuration...');
|
|
||||||
await DailyNotification.configure({
|
|
||||||
storage: 'shared',
|
|
||||||
ttlSeconds: 1800,
|
|
||||||
prefetchLeadMinutes: 15,
|
|
||||||
enableETagSupport: true,
|
|
||||||
enableErrorHandling: true,
|
|
||||||
enablePerformanceOptimization: true
|
|
||||||
});
|
|
||||||
this.log('✅ Configuration successful');
|
|
||||||
this.updateStatus('Configured');
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ Configuration failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testSchedule() {
|
|
||||||
try {
|
|
||||||
this.log('Testing notification scheduling...');
|
|
||||||
await DailyNotification.scheduleDailyNotification({
|
|
||||||
url: 'https://api.example.com/daily-content',
|
|
||||||
time: '09:00',
|
|
||||||
title: 'Daily Test Notification',
|
|
||||||
body: 'This is a test notification from the Android test app'
|
|
||||||
});
|
|
||||||
this.log('✅ Notification scheduled successfully');
|
|
||||||
this.updateStatus('Scheduled');
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ Scheduling failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testAlarmStatus() {
|
|
||||||
try {
|
|
||||||
this.log('Testing exact alarm status...');
|
|
||||||
const status = await DailyNotification.getExactAlarmStatus();
|
|
||||||
this.log(`📱 Alarm Status:`, status);
|
|
||||||
this.updateStatus(`Alarm: ${status.canSchedule ? 'Enabled' : 'Disabled'}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ Alarm status check failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testRequestPermission() {
|
|
||||||
try {
|
|
||||||
this.log('Testing permission request...');
|
|
||||||
await DailyNotification.requestExactAlarmPermission();
|
|
||||||
this.log('✅ Permission request sent');
|
|
||||||
this.updateStatus('Permission Requested');
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ Permission request failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testPerformance() {
|
|
||||||
try {
|
|
||||||
this.log('Testing performance metrics...');
|
|
||||||
const metrics = await DailyNotification.getPerformanceMetrics();
|
|
||||||
this.log(`📊 Performance Metrics:`, metrics);
|
|
||||||
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
|
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`❌ Performance check failed: ${error}`);
|
this.log(`❌ Performance check failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Endorser.ch notification bundle using parallel API requests
|
||||||
|
*/
|
||||||
|
private async processEndorserNotificationBundle(data: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.log('Processing Endorser.ch notification bundle...');
|
||||||
|
|
||||||
|
// Process each notification type
|
||||||
|
if (data.offersToPerson?.data?.length > 0) {
|
||||||
|
await this.handleOffersNotification(data.offersToPerson);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.starredChanges?.data?.length > 0) {
|
||||||
|
await this.handleProjectsNotification(data.starredChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('✅ Notification bundle processed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Bundle processing failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle offers notification events from Endorser.ch API
|
||||||
|
*/
|
||||||
|
private async handleOffersNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling offers notification:', event);
|
||||||
|
|
||||||
|
if (event.data && event.data.length > 0) {
|
||||||
|
// Process OfferSummaryArrayMaybeMoreBody format
|
||||||
|
event.data.forEach((offer: any) => {
|
||||||
|
this.log('Processing offer:', {
|
||||||
|
jwtId: offer.jwtId,
|
||||||
|
handleId: offer.handleId,
|
||||||
|
offeredByDid: offer.offeredByDid,
|
||||||
|
recipientDid: offer.recipientDid,
|
||||||
|
objectDescription: offer.objectDescription
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there are more offers to fetch
|
||||||
|
if (event.hitLimit) {
|
||||||
|
const lastOffer = event.data[event.data.length - 1];
|
||||||
|
this.log('More offers available, last JWT ID:', lastOffer.jwtId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle projects notification events from Endorser.ch API
|
||||||
|
*/
|
||||||
|
private async handleProjectsNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling projects notification:', event);
|
||||||
|
|
||||||
|
if (event.data && event.data.length > 0) {
|
||||||
|
// Process PlanSummaryAndPreviousClaimArrayMaybeMore format
|
||||||
|
event.data.forEach((planData: any) => {
|
||||||
|
const { plan, wrappedClaimBefore } = planData;
|
||||||
|
this.log('Processing project change:', {
|
||||||
|
jwtId: plan.jwtId,
|
||||||
|
handleId: plan.handleId,
|
||||||
|
name: plan.name,
|
||||||
|
issuerDid: plan.issuerDid,
|
||||||
|
hasPreviousClaim: !!wrappedClaimBefore
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there are more project changes to fetch
|
||||||
|
if (event.hitLimit) {
|
||||||
|
const lastPlan = event.data[event.data.length - 1];
|
||||||
|
this.log('More project changes available, last JWT ID:', lastPlan.plan.jwtId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle people notification events
|
||||||
|
*/
|
||||||
|
private async handlePeopleNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling people notification:', event);
|
||||||
|
// Implementation would process people data and update local state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle items notification events
|
||||||
|
*/
|
||||||
|
private async handleItemsNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling items notification:', event);
|
||||||
|
// Implementation would process items data and update local state
|
||||||
|
}
|
||||||
|
|
||||||
private log(message: string, data?: any) {
|
private log(message: string, data?: any) {
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
const logEntry = document.createElement('div');
|
const logEntry = document.createElement('div');
|
||||||
@@ -150,5 +338,5 @@ class TestApp {
|
|||||||
|
|
||||||
// Initialize app when DOM is ready
|
// Initialize app when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
new TestApp();
|
new TimeSafariAndroidTestApp();
|
||||||
});
|
});
|
||||||
|
|||||||
152
test-apps/config/timesafari-config.json
Normal file
152
test-apps/config/timesafari-config.json
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"timesafari": {
|
||||||
|
"appId": "app.timesafari.test",
|
||||||
|
"appName": "TimeSafari Test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Test app for TimeSafari Daily Notification Plugin integration"
|
||||||
|
},
|
||||||
|
"endorser": {
|
||||||
|
"baseUrl": "http://localhost:3001",
|
||||||
|
"apiVersion": "v2",
|
||||||
|
"endpoints": {
|
||||||
|
"offers": "/api/v2/report/offers",
|
||||||
|
"offersToPlans": "/api/v2/report/offersToPlansOwnedByMe",
|
||||||
|
"plansLastUpdated": "/api/v2/report/plansLastUpdatedBetween",
|
||||||
|
"notificationsBundle": "/api/v2/report/notifications/bundle"
|
||||||
|
},
|
||||||
|
"authentication": {
|
||||||
|
"type": "Bearer",
|
||||||
|
"token": "test-jwt-token-12345",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer test-jwt-token-12345",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Privacy-Level": "user-controlled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"defaultLimit": 50,
|
||||||
|
"maxLimit": 100,
|
||||||
|
"hitLimitThreshold": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notificationTypes": {
|
||||||
|
"offers": {
|
||||||
|
"enabled": true,
|
||||||
|
"types": [
|
||||||
|
"new_to_me",
|
||||||
|
"changed_to_me",
|
||||||
|
"new_to_projects",
|
||||||
|
"changed_to_projects",
|
||||||
|
"new_to_favorites",
|
||||||
|
"changed_to_favorites"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"enabled": true,
|
||||||
|
"types": [
|
||||||
|
"local_new",
|
||||||
|
"local_changed",
|
||||||
|
"content_interest_new",
|
||||||
|
"favorited_changed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"people": {
|
||||||
|
"enabled": true,
|
||||||
|
"types": [
|
||||||
|
"local_new",
|
||||||
|
"local_changed",
|
||||||
|
"content_interest_new",
|
||||||
|
"favorited_changed",
|
||||||
|
"contacts_changed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"enabled": true,
|
||||||
|
"types": [
|
||||||
|
"local_new",
|
||||||
|
"local_changed",
|
||||||
|
"favorited_changed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scheduling": {
|
||||||
|
"contentFetch": {
|
||||||
|
"schedule": "0 8 * * *",
|
||||||
|
"time": "08:00",
|
||||||
|
"description": "8 AM daily - fetch community updates"
|
||||||
|
},
|
||||||
|
"userNotification": {
|
||||||
|
"schedule": "0 9 * * *",
|
||||||
|
"time": "09:00",
|
||||||
|
"description": "9 AM daily - notify users of community updates"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"testData": {
|
||||||
|
"userDid": "did:example:testuser123",
|
||||||
|
"starredPlanIds": [
|
||||||
|
"plan-community-garden",
|
||||||
|
"plan-local-food",
|
||||||
|
"plan-sustainability"
|
||||||
|
],
|
||||||
|
"lastKnownOfferId": "01HSE3R9MAC0FT3P3KZ382TWV7",
|
||||||
|
"lastKnownPlanId": "01HSE3R9MAC0FT3P3KZ382TWV8",
|
||||||
|
"mockOffers": [
|
||||||
|
{
|
||||||
|
"jwtId": "01HSE3R9MAC0FT3P3KZ382TWV7",
|
||||||
|
"handleId": "offer-web-dev-001",
|
||||||
|
"offeredByDid": "did:example:offerer123",
|
||||||
|
"recipientDid": "did:example:testuser123",
|
||||||
|
"objectDescription": "Web development services for community project",
|
||||||
|
"unit": "USD",
|
||||||
|
"amount": 1000,
|
||||||
|
"amountGiven": 500,
|
||||||
|
"amountGivenConfirmed": 250
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mockProjects": [
|
||||||
|
{
|
||||||
|
"plan": {
|
||||||
|
"jwtId": "01HSE3R9MAC0FT3P3KZ382TWV8",
|
||||||
|
"handleId": "plan-community-garden",
|
||||||
|
"name": "Community Garden Project",
|
||||||
|
"description": "Building a community garden for local food production",
|
||||||
|
"issuerDid": "did:example:issuer123",
|
||||||
|
"agentDid": "did:example:agent123"
|
||||||
|
},
|
||||||
|
"wrappedClaimBefore": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"callbacks": {
|
||||||
|
"offers": {
|
||||||
|
"enabled": true,
|
||||||
|
"localHandler": "handleOffersNotification"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"enabled": true,
|
||||||
|
"localHandler": "handleProjectsNotification"
|
||||||
|
},
|
||||||
|
"people": {
|
||||||
|
"enabled": true,
|
||||||
|
"localHandler": "handlePeopleNotification"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"enabled": true,
|
||||||
|
"localHandler": "handleItemsNotification"
|
||||||
|
},
|
||||||
|
"communityAnalytics": {
|
||||||
|
"enabled": true,
|
||||||
|
"endpoint": "http://localhost:3001/api/analytics/community-events",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Privacy-Level": "aggregated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"observability": {
|
||||||
|
"enableLogging": true,
|
||||||
|
"logLevel": "debug",
|
||||||
|
"enableMetrics": true,
|
||||||
|
"enableHealthChecks": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,13 +89,15 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>⚡ Daily Notification Plugin - Electron Test</h1>
|
<h1>⚡ TimeSafari Daily Notification - Electron Test</h1>
|
||||||
|
|
||||||
<div class="status" id="status">Ready</div>
|
<div class="status" id="status">Ready</div>
|
||||||
|
|
||||||
<div class="button-grid">
|
<div class="button-grid">
|
||||||
<button id="configure">Configure Plugin</button>
|
<button id="configure">Configure TimeSafari</button>
|
||||||
<button id="schedule">Schedule Notification</button>
|
<button id="schedule">Schedule Community Notifications</button>
|
||||||
|
<button id="endorser-api">Test Endorser.ch API</button>
|
||||||
|
<button id="callbacks">Register Callbacks</button>
|
||||||
<button id="debug-info">Debug Info</button>
|
<button id="debug-info">Debug Info</button>
|
||||||
<button id="performance">Performance Metrics</button>
|
<button id="performance">Performance Metrics</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
// Electron test interface
|
import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader';
|
||||||
class TestApp {
|
|
||||||
|
// Test interface for TimeSafari Electron integration
|
||||||
|
class TimeSafariElectronTestApp {
|
||||||
private statusElement: HTMLElement;
|
private statusElement: HTMLElement;
|
||||||
private logElement: HTMLElement;
|
private logElement: HTMLElement;
|
||||||
|
private configLoader: ConfigLoader;
|
||||||
|
private notificationService: MockDailyNotificationService;
|
||||||
|
private logger: TestLogger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.statusElement = document.getElementById('status')!;
|
this.statusElement = document.getElementById('status')!;
|
||||||
this.logElement = document.getElementById('log')!;
|
this.logElement = document.getElementById('log')!;
|
||||||
|
this.configLoader = ConfigLoader.getInstance();
|
||||||
|
this.logger = new TestLogger('debug');
|
||||||
|
this.notificationService = new MockDailyNotificationService(this.configLoader.getConfig());
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.log('Electron Test app initialized');
|
this.log('TimeSafari Electron Test app initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupEventListeners() {
|
private setupEventListeners() {
|
||||||
document.getElementById('configure')?.addEventListener('click', () => this.testConfigure());
|
document.getElementById('configure')?.addEventListener('click', () => this.testConfigure());
|
||||||
document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule());
|
document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule());
|
||||||
|
document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI());
|
||||||
|
document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks());
|
||||||
document.getElementById('debug-info')?.addEventListener('click', () => this.testDebugInfo());
|
document.getElementById('debug-info')?.addEventListener('click', () => this.testDebugInfo());
|
||||||
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
|
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
|
||||||
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
|
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
|
||||||
@@ -20,80 +30,299 @@ class TestApp {
|
|||||||
|
|
||||||
private async testConfigure() {
|
private async testConfigure() {
|
||||||
try {
|
try {
|
||||||
this.log('Testing Electron configuration...');
|
this.log('Testing TimeSafari Electron configuration...');
|
||||||
const result = await (window as any).electronAPI.configurePlugin({
|
await this.configLoader.loadConfig();
|
||||||
storage: 'mock',
|
const config = this.configLoader.getConfig();
|
||||||
ttlSeconds: 1800,
|
|
||||||
prefetchLeadMinutes: 15,
|
|
||||||
enableETagSupport: true,
|
|
||||||
enableErrorHandling: true,
|
|
||||||
enablePerformanceOptimization: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
await this.notificationService.initialize();
|
||||||
this.log('✅ Electron Configuration successful');
|
|
||||||
|
this.log('✅ TimeSafari Electron configuration successful', {
|
||||||
|
appId: config.timesafari.appId,
|
||||||
|
appName: config.timesafari.appName,
|
||||||
|
version: config.timesafari.version
|
||||||
|
});
|
||||||
this.updateStatus('Configured');
|
this.updateStatus('Configured');
|
||||||
} else {
|
|
||||||
this.log(`❌ Configuration failed: ${result.error}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`❌ Configuration error: ${error}`);
|
this.log(`❌ Configuration failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async testSchedule() {
|
private async testSchedule() {
|
||||||
try {
|
try {
|
||||||
this.log('Testing Electron notification scheduling...');
|
this.log('Testing TimeSafari Electron community notification scheduling...');
|
||||||
const result = await (window as any).electronAPI.scheduleNotification({
|
const config = this.configLoader.getConfig();
|
||||||
url: 'https://api.example.com/daily-content',
|
|
||||||
time: '09:00',
|
const dualConfig = {
|
||||||
title: 'Daily Electron Test Notification',
|
contentFetch: {
|
||||||
body: 'This is a test notification from the Electron test app'
|
enabled: true,
|
||||||
|
schedule: config.scheduling.contentFetch.schedule,
|
||||||
|
url: this.configLoader.getEndorserUrl('notificationsBundle'),
|
||||||
|
headers: this.configLoader.getAuthHeaders(),
|
||||||
|
ttlSeconds: 3600,
|
||||||
|
timeout: 30000,
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 5000,
|
||||||
|
callbacks: {
|
||||||
|
onSuccess: async (data: any) => {
|
||||||
|
this.log('✅ Content fetch successful', data);
|
||||||
|
await this.processEndorserNotificationBundle(data);
|
||||||
|
},
|
||||||
|
onError: async (error: any) => {
|
||||||
|
this.log('❌ Content fetch failed', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
userNotification: {
|
||||||
|
enabled: true,
|
||||||
|
schedule: config.scheduling.userNotification.schedule,
|
||||||
|
title: 'TimeSafari Community Update',
|
||||||
|
body: 'New offers, projects, people, and items await your attention!',
|
||||||
|
sound: true,
|
||||||
|
vibration: true,
|
||||||
|
priority: 'high',
|
||||||
|
actions: [
|
||||||
|
{ id: 'view_offers', title: 'View Offers' },
|
||||||
|
{ id: 'view_projects', title: 'See Projects' },
|
||||||
|
{ id: 'view_people', title: 'Check People' },
|
||||||
|
{ id: 'view_items', title: 'Browse Items' },
|
||||||
|
{ id: 'dismiss', title: 'Dismiss' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
relationship: {
|
||||||
|
autoLink: true,
|
||||||
|
contentTimeout: 300000,
|
||||||
|
fallbackBehavior: 'show_default'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.notificationService.scheduleDualNotification(dualConfig);
|
||||||
|
this.log('✅ Electron community notification scheduled successfully');
|
||||||
|
this.updateStatus('Scheduled');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Electron scheduling failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testEndorserAPI() {
|
||||||
|
try {
|
||||||
|
this.log('Testing Endorser.ch API integration on Electron...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
const testData = config.testData;
|
||||||
|
|
||||||
|
// Test parallel API requests pattern
|
||||||
|
const requests = [
|
||||||
|
// Offers to person
|
||||||
|
fetch(`${this.configLoader.getEndorserUrl('offers')}?recipientId=${testData.userDid}&afterId=${testData.lastKnownOfferId}`, {
|
||||||
|
headers: this.configLoader.getAuthHeaders()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Offers to user's projects
|
||||||
|
fetch(`${this.configLoader.getEndorserUrl('offersToPlans')}?afterId=${testData.lastKnownOfferId}`, {
|
||||||
|
headers: this.configLoader.getAuthHeaders()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Changes to starred projects
|
||||||
|
fetch(this.configLoader.getEndorserUrl('plansLastUpdated'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.configLoader.getAuthHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
planIds: testData.starredPlanIds,
|
||||||
|
afterId: testData.lastKnownPlanId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests);
|
||||||
|
|
||||||
|
const notificationData = {
|
||||||
|
offersToPerson: await offersToPerson.json(),
|
||||||
|
offersToProjects: await offersToProjects.json(),
|
||||||
|
starredChanges: await starredChanges.json()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.log('✅ Endorser.ch API integration successful on Electron', {
|
||||||
|
offersToPerson: notificationData.offersToPerson.data?.length || 0,
|
||||||
|
offersToProjects: notificationData.offersToProjects.data?.length || 0,
|
||||||
|
starredChanges: notificationData.starredChanges.data?.length || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
this.updateStatus('API Connected');
|
||||||
this.log('✅ Electron Notification scheduled successfully');
|
|
||||||
this.updateStatus('Scheduled');
|
|
||||||
} else {
|
|
||||||
this.log(`❌ Scheduling failed: ${result.error}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`❌ Scheduling error: ${error}`);
|
this.log(`❌ Endorser.ch API test failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testCallbacks() {
|
||||||
|
try {
|
||||||
|
this.log('Testing TimeSafari Electron notification callbacks...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
|
||||||
|
// Register offers callback
|
||||||
|
await this.notificationService.registerCallback('offers', async (event: any) => {
|
||||||
|
this.log('📨 Electron Offers callback triggered', event);
|
||||||
|
await this.handleOffersNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register projects callback
|
||||||
|
await this.notificationService.registerCallback('projects', async (event: any) => {
|
||||||
|
this.log('📨 Electron Projects callback triggered', event);
|
||||||
|
await this.handleProjectsNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register people callback
|
||||||
|
await this.notificationService.registerCallback('people', async (event: any) => {
|
||||||
|
this.log('📨 Electron People callback triggered', event);
|
||||||
|
await this.handlePeopleNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register items callback
|
||||||
|
await this.notificationService.registerCallback('items', async (event: any) => {
|
||||||
|
this.log('📨 Electron Items callback triggered', event);
|
||||||
|
await this.handleItemsNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('✅ All Electron callbacks registered successfully');
|
||||||
|
this.updateStatus('Callbacks Registered');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Electron callback registration failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async testDebugInfo() {
|
private async testDebugInfo() {
|
||||||
try {
|
try {
|
||||||
this.log('Testing Electron debug info...');
|
this.log('Testing Electron debug info...');
|
||||||
const result = await (window as any).electronAPI.getDebugInfo();
|
const debugInfo = {
|
||||||
|
platform: 'electron',
|
||||||
|
nodeVersion: process.versions.node,
|
||||||
|
electronVersion: process.versions.electron,
|
||||||
|
chromeVersion: process.versions.chrome,
|
||||||
|
status: 'running',
|
||||||
|
config: this.configLoader.getConfig().timesafari,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
if (result.success) {
|
this.log('🔍 Electron Debug Info:', debugInfo);
|
||||||
this.log('🔍 Electron Debug Info:', result.data);
|
this.updateStatus(`Debug: ${debugInfo.status}`);
|
||||||
this.updateStatus(`Debug: ${result.data.status}`);
|
|
||||||
} else {
|
|
||||||
this.log(`❌ Debug info failed: ${result.error}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`❌ Debug info error: ${error}`);
|
this.log(`❌ Debug info failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async testPerformance() {
|
private async testPerformance() {
|
||||||
try {
|
try {
|
||||||
this.log('Testing Electron performance metrics...');
|
this.log('Testing Electron performance metrics...');
|
||||||
const result = await (window as any).electronAPI.getPerformanceMetrics();
|
const metrics = {
|
||||||
|
overallScore: 82,
|
||||||
|
databasePerformance: 85,
|
||||||
|
memoryEfficiency: 78,
|
||||||
|
batteryEfficiency: 80,
|
||||||
|
objectPoolEfficiency: 85,
|
||||||
|
totalDatabaseQueries: 100,
|
||||||
|
averageMemoryUsage: 30.2,
|
||||||
|
objectPoolHits: 25,
|
||||||
|
backgroundCpuUsage: 3.1,
|
||||||
|
totalNetworkRequests: 15,
|
||||||
|
recommendations: ['Optimize IPC communication', 'Reduce memory usage']
|
||||||
|
};
|
||||||
|
|
||||||
if (result.success) {
|
this.log('📊 Electron Performance Metrics:', metrics);
|
||||||
this.log('📊 Electron Performance Metrics:', result.data);
|
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
|
||||||
this.updateStatus(`Performance: ${result.data.overallScore}/100`);
|
|
||||||
} else {
|
|
||||||
this.log(`❌ Performance check failed: ${result.error}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`❌ Performance error: ${error}`);
|
this.log(`❌ Performance check failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Endorser.ch notification bundle using parallel API requests
|
||||||
|
*/
|
||||||
|
private async processEndorserNotificationBundle(data: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.log('Processing Endorser.ch notification bundle on Electron...');
|
||||||
|
|
||||||
|
// Process each notification type
|
||||||
|
if (data.offersToPerson?.data?.length > 0) {
|
||||||
|
await this.handleOffersNotification(data.offersToPerson);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.starredChanges?.data?.length > 0) {
|
||||||
|
await this.handleProjectsNotification(data.starredChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('✅ Electron notification bundle processed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Electron bundle processing failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle offers notification events from Endorser.ch API
|
||||||
|
*/
|
||||||
|
private async handleOffersNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling Electron offers notification:', event);
|
||||||
|
|
||||||
|
if (event.data && event.data.length > 0) {
|
||||||
|
// Process OfferSummaryArrayMaybeMoreBody format
|
||||||
|
event.data.forEach((offer: any) => {
|
||||||
|
this.log('Processing Electron offer:', {
|
||||||
|
jwtId: offer.jwtId,
|
||||||
|
handleId: offer.handleId,
|
||||||
|
offeredByDid: offer.offeredByDid,
|
||||||
|
recipientDid: offer.recipientDid,
|
||||||
|
objectDescription: offer.objectDescription
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there are more offers to fetch
|
||||||
|
if (event.hitLimit) {
|
||||||
|
const lastOffer = event.data[event.data.length - 1];
|
||||||
|
this.log('More offers available, last JWT ID:', lastOffer.jwtId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle projects notification events from Endorser.ch API
|
||||||
|
*/
|
||||||
|
private async handleProjectsNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling Electron projects notification:', event);
|
||||||
|
|
||||||
|
if (event.data && event.data.length > 0) {
|
||||||
|
// Process PlanSummaryAndPreviousClaimArrayMaybeMore format
|
||||||
|
event.data.forEach((planData: any) => {
|
||||||
|
const { plan, wrappedClaimBefore } = planData;
|
||||||
|
this.log('Processing Electron project change:', {
|
||||||
|
jwtId: plan.jwtId,
|
||||||
|
handleId: plan.handleId,
|
||||||
|
name: plan.name,
|
||||||
|
issuerDid: plan.issuerDid,
|
||||||
|
hasPreviousClaim: !!wrappedClaimBefore
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there are more project changes to fetch
|
||||||
|
if (event.hitLimit) {
|
||||||
|
const lastPlan = event.data[event.data.length - 1];
|
||||||
|
this.log('More project changes available, last JWT ID:', lastPlan.plan.jwtId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle people notification events
|
||||||
|
*/
|
||||||
|
private async handlePeopleNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling Electron people notification:', event);
|
||||||
|
// Implementation would process people data and update local state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle items notification events
|
||||||
|
*/
|
||||||
|
private async handleItemsNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling Electron items notification:', event);
|
||||||
|
// Implementation would process items data and update local state
|
||||||
|
}
|
||||||
|
|
||||||
private log(message: string, data?: any) {
|
private log(message: string, data?: any) {
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
const logEntry = document.createElement('div');
|
const logEntry = document.createElement('div');
|
||||||
@@ -117,5 +346,5 @@ class TestApp {
|
|||||||
|
|
||||||
// Initialize app when DOM is ready
|
// Initialize app when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
new TestApp();
|
new TimeSafariElectronTestApp();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,15 +89,16 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>🍎 Daily Notification Plugin - iOS Test</h1>
|
<h1>🍎 TimeSafari Daily Notification - iOS Test</h1>
|
||||||
|
|
||||||
<div class="status" id="status">Ready</div>
|
<div class="status" id="status">Ready</div>
|
||||||
|
|
||||||
<div class="button-grid">
|
<div class="button-grid">
|
||||||
<button id="configure">Configure Plugin</button>
|
<button id="configure">Configure TimeSafari</button>
|
||||||
<button id="schedule">Schedule Notification</button>
|
<button id="schedule">Schedule Community Notifications</button>
|
||||||
<button id="rolling-window">Maintain Window</button>
|
<button id="rolling-window">Maintain Rolling Window</button>
|
||||||
<button id="window-stats">Window Stats</button>
|
<button id="endorser-api">Test Endorser.ch API</button>
|
||||||
|
<button id="callbacks">Register Callbacks</button>
|
||||||
<button id="performance">Performance Metrics</button>
|
<button id="performance">Performance Metrics</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,215 @@
|
|||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader';
|
||||||
|
|
||||||
// Mock plugin for development
|
// Test interface for TimeSafari iOS integration
|
||||||
const DailyNotification = {
|
class TimeSafariIOSTestApp {
|
||||||
async configure(options: any) {
|
private statusElement: HTMLElement;
|
||||||
console.log('Configure called:', options);
|
private logElement: HTMLElement;
|
||||||
return Promise.resolve();
|
private configLoader: ConfigLoader;
|
||||||
|
private notificationService: MockDailyNotificationService;
|
||||||
|
private logger: TestLogger;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.statusElement = document.getElementById('status')!;
|
||||||
|
this.logElement = document.getElementById('log')!;
|
||||||
|
this.configLoader = ConfigLoader.getInstance();
|
||||||
|
this.logger = new TestLogger('debug');
|
||||||
|
this.notificationService = new MockDailyNotificationService(this.configLoader.getConfig());
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.log('TimeSafari iOS Test app initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEventListeners() {
|
||||||
|
document.getElementById('configure')?.addEventListener('click', () => this.testConfigure());
|
||||||
|
document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule());
|
||||||
|
document.getElementById('rolling-window')?.addEventListener('click', () => this.testRollingWindow());
|
||||||
|
document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI());
|
||||||
|
document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks());
|
||||||
|
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
|
||||||
|
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testConfigure() {
|
||||||
|
try {
|
||||||
|
this.log('Testing TimeSafari iOS configuration...');
|
||||||
|
await this.configLoader.loadConfig();
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
|
||||||
|
await this.notificationService.initialize();
|
||||||
|
|
||||||
|
this.log('✅ TimeSafari iOS configuration successful', {
|
||||||
|
appId: config.timesafari.appId,
|
||||||
|
appName: config.timesafari.appName,
|
||||||
|
version: config.timesafari.version
|
||||||
|
});
|
||||||
|
this.updateStatus('Configured');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Configuration failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testSchedule() {
|
||||||
|
try {
|
||||||
|
this.log('Testing TimeSafari iOS community notification scheduling...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
|
||||||
|
const dualConfig = {
|
||||||
|
contentFetch: {
|
||||||
|
enabled: true,
|
||||||
|
schedule: config.scheduling.contentFetch.schedule,
|
||||||
|
url: this.configLoader.getEndorserUrl('notificationsBundle'),
|
||||||
|
headers: this.configLoader.getAuthHeaders(),
|
||||||
|
ttlSeconds: 3600,
|
||||||
|
timeout: 30000,
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 5000,
|
||||||
|
callbacks: {
|
||||||
|
onSuccess: async (data: any) => {
|
||||||
|
this.log('✅ Content fetch successful', data);
|
||||||
|
await this.processEndorserNotificationBundle(data);
|
||||||
},
|
},
|
||||||
async scheduleDailyNotification(options: any) {
|
onError: async (error: any) => {
|
||||||
console.log('Schedule called:', options);
|
this.log('❌ Content fetch failed', error);
|
||||||
return Promise.resolve();
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async maintainRollingWindow() {
|
userNotification: {
|
||||||
console.log('Maintain rolling window called');
|
enabled: true,
|
||||||
return Promise.resolve();
|
schedule: config.scheduling.userNotification.schedule,
|
||||||
|
title: 'TimeSafari Community Update',
|
||||||
|
body: 'New offers, projects, people, and items await your attention!',
|
||||||
|
sound: true,
|
||||||
|
vibration: true,
|
||||||
|
priority: 'high',
|
||||||
|
actions: [
|
||||||
|
{ id: 'view_offers', title: 'View Offers' },
|
||||||
|
{ id: 'view_projects', title: 'See Projects' },
|
||||||
|
{ id: 'view_people', title: 'Check People' },
|
||||||
|
{ id: 'view_items', title: 'Browse Items' },
|
||||||
|
{ id: 'dismiss', title: 'Dismiss' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
async getRollingWindowStats() {
|
relationship: {
|
||||||
return Promise.resolve({
|
autoLink: true,
|
||||||
|
contentTimeout: 300000,
|
||||||
|
fallbackBehavior: 'show_default'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.notificationService.scheduleDualNotification(dualConfig);
|
||||||
|
this.log('✅ iOS community notification scheduled successfully');
|
||||||
|
this.updateStatus('Scheduled');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ iOS scheduling failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testRollingWindow() {
|
||||||
|
try {
|
||||||
|
this.log('Testing iOS rolling window maintenance...');
|
||||||
|
// Simulate rolling window maintenance
|
||||||
|
const stats = {
|
||||||
stats: '64 pending notifications, 20 daily limit',
|
stats: '64 pending notifications, 20 daily limit',
|
||||||
maintenanceNeeded: false,
|
maintenanceNeeded: false,
|
||||||
timeUntilNextMaintenance: 900000
|
timeUntilNextMaintenance: 900000
|
||||||
|
};
|
||||||
|
|
||||||
|
this.log('✅ Rolling window maintenance completed', stats);
|
||||||
|
this.updateStatus('Rolling Window Maintained');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ Rolling window maintenance failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testEndorserAPI() {
|
||||||
|
try {
|
||||||
|
this.log('Testing Endorser.ch API integration on iOS...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
const testData = config.testData;
|
||||||
|
|
||||||
|
// Test parallel API requests pattern
|
||||||
|
const requests = [
|
||||||
|
// Offers to person
|
||||||
|
fetch(`${this.configLoader.getEndorserUrl('offers')}?recipientId=${testData.userDid}&afterId=${testData.lastKnownOfferId}`, {
|
||||||
|
headers: this.configLoader.getAuthHeaders()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Offers to user's projects
|
||||||
|
fetch(`${this.configLoader.getEndorserUrl('offersToPlans')}?afterId=${testData.lastKnownOfferId}`, {
|
||||||
|
headers: this.configLoader.getAuthHeaders()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Changes to starred projects
|
||||||
|
fetch(this.configLoader.getEndorserUrl('plansLastUpdated'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.configLoader.getAuthHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
planIds: testData.starredPlanIds,
|
||||||
|
afterId: testData.lastKnownPlanId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests);
|
||||||
|
|
||||||
|
const notificationData = {
|
||||||
|
offersToPerson: await offersToPerson.json(),
|
||||||
|
offersToProjects: await offersToProjects.json(),
|
||||||
|
starredChanges: await starredChanges.json()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.log('✅ Endorser.ch API integration successful on iOS', {
|
||||||
|
offersToPerson: notificationData.offersToPerson.data?.length || 0,
|
||||||
|
offersToProjects: notificationData.offersToProjects.data?.length || 0,
|
||||||
|
starredChanges: notificationData.starredChanges.data?.length || 0
|
||||||
});
|
});
|
||||||
},
|
|
||||||
async getPerformanceMetrics() {
|
this.updateStatus('API Connected');
|
||||||
return Promise.resolve({
|
} catch (error) {
|
||||||
|
this.log(`❌ Endorser.ch API test failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testCallbacks() {
|
||||||
|
try {
|
||||||
|
this.log('Testing TimeSafari iOS notification callbacks...');
|
||||||
|
const config = this.configLoader.getConfig();
|
||||||
|
|
||||||
|
// Register offers callback
|
||||||
|
await this.notificationService.registerCallback('offers', async (event: any) => {
|
||||||
|
this.log('📨 iOS Offers callback triggered', event);
|
||||||
|
await this.handleOffersNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register projects callback
|
||||||
|
await this.notificationService.registerCallback('projects', async (event: any) => {
|
||||||
|
this.log('📨 iOS Projects callback triggered', event);
|
||||||
|
await this.handleProjectsNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register people callback
|
||||||
|
await this.notificationService.registerCallback('people', async (event: any) => {
|
||||||
|
this.log('📨 iOS People callback triggered', event);
|
||||||
|
await this.handlePeopleNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register items callback
|
||||||
|
await this.notificationService.registerCallback('items', async (event: any) => {
|
||||||
|
this.log('📨 iOS Items callback triggered', event);
|
||||||
|
await this.handleItemsNotification(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('✅ All iOS callbacks registered successfully');
|
||||||
|
this.updateStatus('Callbacks Registered');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ iOS callback registration failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testPerformance() {
|
||||||
|
try {
|
||||||
|
this.log('Testing iOS performance metrics...');
|
||||||
|
const metrics = {
|
||||||
overallScore: 88,
|
overallScore: 88,
|
||||||
databasePerformance: 92,
|
databasePerformance: 92,
|
||||||
memoryEfficiency: 85,
|
memoryEfficiency: 85,
|
||||||
@@ -34,98 +221,106 @@ const DailyNotification = {
|
|||||||
backgroundCpuUsage: 1.8,
|
backgroundCpuUsage: 1.8,
|
||||||
totalNetworkRequests: 8,
|
totalNetworkRequests: 8,
|
||||||
recommendations: ['Enable background tasks', 'Optimize memory usage']
|
recommendations: ['Enable background tasks', 'Optimize memory usage']
|
||||||
});
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test interface
|
this.log('📊 iOS Performance Metrics:', metrics);
|
||||||
class TestApp {
|
|
||||||
private statusElement: HTMLElement;
|
|
||||||
private logElement: HTMLElement;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.statusElement = document.getElementById('status')!;
|
|
||||||
this.logElement = document.getElementById('log')!;
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.log('iOS Test app initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupEventListeners() {
|
|
||||||
document.getElementById('configure')?.addEventListener('click', () => this.testConfigure());
|
|
||||||
document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule());
|
|
||||||
document.getElementById('rolling-window')?.addEventListener('click', () => this.testRollingWindow());
|
|
||||||
document.getElementById('window-stats')?.addEventListener('click', () => this.testWindowStats());
|
|
||||||
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
|
|
||||||
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testConfigure() {
|
|
||||||
try {
|
|
||||||
this.log('Testing iOS configuration...');
|
|
||||||
await DailyNotification.configure({
|
|
||||||
storage: 'shared',
|
|
||||||
ttlSeconds: 1800,
|
|
||||||
prefetchLeadMinutes: 15,
|
|
||||||
enableETagSupport: true,
|
|
||||||
enableErrorHandling: true,
|
|
||||||
enablePerformanceOptimization: true
|
|
||||||
});
|
|
||||||
this.log('✅ iOS Configuration successful');
|
|
||||||
this.updateStatus('Configured');
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ Configuration failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testSchedule() {
|
|
||||||
try {
|
|
||||||
this.log('Testing iOS notification scheduling...');
|
|
||||||
await DailyNotification.scheduleDailyNotification({
|
|
||||||
url: 'https://api.example.com/daily-content',
|
|
||||||
time: '09:00',
|
|
||||||
title: 'Daily iOS Test Notification',
|
|
||||||
body: 'This is a test notification from the iOS test app'
|
|
||||||
});
|
|
||||||
this.log('✅ iOS Notification scheduled successfully');
|
|
||||||
this.updateStatus('Scheduled');
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ iOS Scheduling failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testRollingWindow() {
|
|
||||||
try {
|
|
||||||
this.log('Testing iOS rolling window maintenance...');
|
|
||||||
await DailyNotification.maintainRollingWindow();
|
|
||||||
this.log('✅ Rolling window maintenance completed');
|
|
||||||
this.updateStatus('Rolling Window Maintained');
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ Rolling window maintenance failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testWindowStats() {
|
|
||||||
try {
|
|
||||||
this.log('Testing iOS rolling window stats...');
|
|
||||||
const stats = await DailyNotification.getRollingWindowStats();
|
|
||||||
this.log(`📊 Rolling Window Stats:`, stats);
|
|
||||||
this.updateStatus(`Window: ${stats.maintenanceNeeded ? 'Needs Maintenance' : 'OK'}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.log(`❌ Window stats check failed: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testPerformance() {
|
|
||||||
try {
|
|
||||||
this.log('Testing iOS performance metrics...');
|
|
||||||
const metrics = await DailyNotification.getPerformanceMetrics();
|
|
||||||
this.log(`📊 iOS Performance Metrics:`, metrics);
|
|
||||||
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
|
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log(`❌ Performance check failed: ${error}`);
|
this.log(`❌ Performance check failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Endorser.ch notification bundle using parallel API requests
|
||||||
|
*/
|
||||||
|
private async processEndorserNotificationBundle(data: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.log('Processing Endorser.ch notification bundle on iOS...');
|
||||||
|
|
||||||
|
// Process each notification type
|
||||||
|
if (data.offersToPerson?.data?.length > 0) {
|
||||||
|
await this.handleOffersNotification(data.offersToPerson);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.starredChanges?.data?.length > 0) {
|
||||||
|
await this.handleProjectsNotification(data.starredChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('✅ iOS notification bundle processed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
this.log(`❌ iOS bundle processing failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle offers notification events from Endorser.ch API
|
||||||
|
*/
|
||||||
|
private async handleOffersNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling iOS offers notification:', event);
|
||||||
|
|
||||||
|
if (event.data && event.data.length > 0) {
|
||||||
|
// Process OfferSummaryArrayMaybeMoreBody format
|
||||||
|
event.data.forEach((offer: any) => {
|
||||||
|
this.log('Processing iOS offer:', {
|
||||||
|
jwtId: offer.jwtId,
|
||||||
|
handleId: offer.handleId,
|
||||||
|
offeredByDid: offer.offeredByDid,
|
||||||
|
recipientDid: offer.recipientDid,
|
||||||
|
objectDescription: offer.objectDescription
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there are more offers to fetch
|
||||||
|
if (event.hitLimit) {
|
||||||
|
const lastOffer = event.data[event.data.length - 1];
|
||||||
|
this.log('More offers available, last JWT ID:', lastOffer.jwtId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle projects notification events from Endorser.ch API
|
||||||
|
*/
|
||||||
|
private async handleProjectsNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling iOS projects notification:', event);
|
||||||
|
|
||||||
|
if (event.data && event.data.length > 0) {
|
||||||
|
// Process PlanSummaryAndPreviousClaimArrayMaybeMore format
|
||||||
|
event.data.forEach((planData: any) => {
|
||||||
|
const { plan, wrappedClaimBefore } = planData;
|
||||||
|
this.log('Processing iOS project change:', {
|
||||||
|
jwtId: plan.jwtId,
|
||||||
|
handleId: plan.handleId,
|
||||||
|
name: plan.name,
|
||||||
|
issuerDid: plan.issuerDid,
|
||||||
|
hasPreviousClaim: !!wrappedClaimBefore
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there are more project changes to fetch
|
||||||
|
if (event.hitLimit) {
|
||||||
|
const lastPlan = event.data[event.data.length - 1];
|
||||||
|
this.log('More project changes available, last JWT ID:', lastPlan.plan.jwtId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle people notification events
|
||||||
|
*/
|
||||||
|
private async handlePeopleNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling iOS people notification:', event);
|
||||||
|
// Implementation would process people data and update local state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle items notification events
|
||||||
|
*/
|
||||||
|
private async handleItemsNotification(event: any): Promise<void> {
|
||||||
|
this.log('Handling iOS items notification:', event);
|
||||||
|
// Implementation would process items data and update local state
|
||||||
|
}
|
||||||
|
|
||||||
private log(message: string, data?: any) {
|
private log(message: string, data?: any) {
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
const logEntry = document.createElement('div');
|
const logEntry = document.createElement('div');
|
||||||
@@ -149,5 +344,5 @@ class TestApp {
|
|||||||
|
|
||||||
// Initialize app when DOM is ready
|
// Initialize app when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
new TestApp();
|
new TimeSafariIOSTestApp();
|
||||||
});
|
});
|
||||||
|
|||||||
522
test-apps/shared/config-loader.ts
Normal file
522
test-apps/shared/config-loader.ts
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
/**
|
||||||
|
* Configuration loader for TimeSafari test apps
|
||||||
|
*
|
||||||
|
* Loads configuration from JSON files and provides typed access
|
||||||
|
* to TimeSafari-specific settings, Endorser.ch API endpoints,
|
||||||
|
* and test data.
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TimeSafariConfig {
|
||||||
|
timesafari: {
|
||||||
|
appId: string;
|
||||||
|
appName: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
endorser: {
|
||||||
|
baseUrl: string;
|
||||||
|
apiVersion: string;
|
||||||
|
endpoints: {
|
||||||
|
offers: string;
|
||||||
|
offersToPlans: string;
|
||||||
|
plansLastUpdated: string;
|
||||||
|
notificationsBundle: string;
|
||||||
|
};
|
||||||
|
authentication: {
|
||||||
|
type: string;
|
||||||
|
token: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
};
|
||||||
|
pagination: {
|
||||||
|
defaultLimit: number;
|
||||||
|
maxLimit: number;
|
||||||
|
hitLimitThreshold: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
notificationTypes: {
|
||||||
|
offers: {
|
||||||
|
enabled: boolean;
|
||||||
|
types: string[];
|
||||||
|
};
|
||||||
|
projects: {
|
||||||
|
enabled: boolean;
|
||||||
|
types: string[];
|
||||||
|
};
|
||||||
|
people: {
|
||||||
|
enabled: boolean;
|
||||||
|
types: string[];
|
||||||
|
};
|
||||||
|
items: {
|
||||||
|
enabled: boolean;
|
||||||
|
types: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
scheduling: {
|
||||||
|
contentFetch: {
|
||||||
|
schedule: string;
|
||||||
|
time: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
userNotification: {
|
||||||
|
schedule: string;
|
||||||
|
time: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
testData: {
|
||||||
|
userDid: string;
|
||||||
|
starredPlanIds: string[];
|
||||||
|
lastKnownOfferId: string;
|
||||||
|
lastKnownPlanId: string;
|
||||||
|
mockOffers: any[];
|
||||||
|
mockProjects: any[];
|
||||||
|
};
|
||||||
|
callbacks: {
|
||||||
|
offers: {
|
||||||
|
enabled: boolean;
|
||||||
|
localHandler: string;
|
||||||
|
};
|
||||||
|
projects: {
|
||||||
|
enabled: boolean;
|
||||||
|
localHandler: string;
|
||||||
|
};
|
||||||
|
people: {
|
||||||
|
enabled: boolean;
|
||||||
|
localHandler: string;
|
||||||
|
};
|
||||||
|
items: {
|
||||||
|
enabled: boolean;
|
||||||
|
localHandler: string;
|
||||||
|
};
|
||||||
|
communityAnalytics: {
|
||||||
|
enabled: boolean;
|
||||||
|
endpoint: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
observability: {
|
||||||
|
enableLogging: boolean;
|
||||||
|
logLevel: string;
|
||||||
|
enableMetrics: boolean;
|
||||||
|
enableHealthChecks: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration loader class
|
||||||
|
*/
|
||||||
|
export class ConfigLoader {
|
||||||
|
private static instance: ConfigLoader;
|
||||||
|
private config: TimeSafariConfig | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance
|
||||||
|
*/
|
||||||
|
public static getInstance(): ConfigLoader {
|
||||||
|
if (!ConfigLoader.instance) {
|
||||||
|
ConfigLoader.instance = new ConfigLoader();
|
||||||
|
}
|
||||||
|
return ConfigLoader.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from JSON file
|
||||||
|
*/
|
||||||
|
public async loadConfig(): Promise<TimeSafariConfig> {
|
||||||
|
if (this.config) {
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// In a real app, this would fetch from a config file
|
||||||
|
// For test apps, we'll use a hardcoded config
|
||||||
|
this.config = {
|
||||||
|
timesafari: {
|
||||||
|
appId: "app.timesafari.test",
|
||||||
|
appName: "TimeSafari Test",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "Test app for TimeSafari Daily Notification Plugin integration"
|
||||||
|
},
|
||||||
|
endorser: {
|
||||||
|
baseUrl: "http://localhost:3001",
|
||||||
|
apiVersion: "v2",
|
||||||
|
endpoints: {
|
||||||
|
offers: "/api/v2/report/offers",
|
||||||
|
offersToPlans: "/api/v2/report/offersToPlansOwnedByMe",
|
||||||
|
plansLastUpdated: "/api/v2/report/plansLastUpdatedBetween",
|
||||||
|
notificationsBundle: "/api/v2/report/notifications/bundle"
|
||||||
|
},
|
||||||
|
authentication: {
|
||||||
|
type: "Bearer",
|
||||||
|
token: "test-jwt-token-12345",
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer test-jwt-token-12345",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Privacy-Level": "user-controlled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
defaultLimit: 50,
|
||||||
|
maxLimit: 100,
|
||||||
|
hitLimitThreshold: 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notificationTypes: {
|
||||||
|
offers: {
|
||||||
|
enabled: true,
|
||||||
|
types: [
|
||||||
|
"new_to_me",
|
||||||
|
"changed_to_me",
|
||||||
|
"new_to_projects",
|
||||||
|
"changed_to_projects",
|
||||||
|
"new_to_favorites",
|
||||||
|
"changed_to_favorites"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
enabled: true,
|
||||||
|
types: [
|
||||||
|
"local_new",
|
||||||
|
"local_changed",
|
||||||
|
"content_interest_new",
|
||||||
|
"favorited_changed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
people: {
|
||||||
|
enabled: true,
|
||||||
|
types: [
|
||||||
|
"local_new",
|
||||||
|
"local_changed",
|
||||||
|
"content_interest_new",
|
||||||
|
"favorited_changed",
|
||||||
|
"contacts_changed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
enabled: true,
|
||||||
|
types: [
|
||||||
|
"local_new",
|
||||||
|
"local_changed",
|
||||||
|
"favorited_changed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scheduling: {
|
||||||
|
contentFetch: {
|
||||||
|
schedule: "0 8 * * *",
|
||||||
|
time: "08:00",
|
||||||
|
description: "8 AM daily - fetch community updates"
|
||||||
|
},
|
||||||
|
userNotification: {
|
||||||
|
schedule: "0 9 * * *",
|
||||||
|
time: "09:00",
|
||||||
|
description: "9 AM daily - notify users of community updates"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
testData: {
|
||||||
|
userDid: "did:example:testuser123",
|
||||||
|
starredPlanIds: [
|
||||||
|
"plan-community-garden",
|
||||||
|
"plan-local-food",
|
||||||
|
"plan-sustainability"
|
||||||
|
],
|
||||||
|
lastKnownOfferId: "01HSE3R9MAC0FT3P3KZ382TWV7",
|
||||||
|
lastKnownPlanId: "01HSE3R9MAC0FT3P3KZ382TWV8",
|
||||||
|
mockOffers: [
|
||||||
|
{
|
||||||
|
jwtId: "01HSE3R9MAC0FT3P3KZ382TWV7",
|
||||||
|
handleId: "offer-web-dev-001",
|
||||||
|
offeredByDid: "did:example:offerer123",
|
||||||
|
recipientDid: "did:example:testuser123",
|
||||||
|
objectDescription: "Web development services for community project",
|
||||||
|
unit: "USD",
|
||||||
|
amount: 1000,
|
||||||
|
amountGiven: 500,
|
||||||
|
amountGivenConfirmed: 250
|
||||||
|
}
|
||||||
|
],
|
||||||
|
mockProjects: [
|
||||||
|
{
|
||||||
|
plan: {
|
||||||
|
jwtId: "01HSE3R9MAC0FT3P3KZ382TWV8",
|
||||||
|
handleId: "plan-community-garden",
|
||||||
|
name: "Community Garden Project",
|
||||||
|
description: "Building a community garden for local food production",
|
||||||
|
issuerDid: "did:example:issuer123",
|
||||||
|
agentDid: "did:example:agent123"
|
||||||
|
},
|
||||||
|
wrappedClaimBefore: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
offers: {
|
||||||
|
enabled: true,
|
||||||
|
localHandler: "handleOffersNotification"
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
enabled: true,
|
||||||
|
localHandler: "handleProjectsNotification"
|
||||||
|
},
|
||||||
|
people: {
|
||||||
|
enabled: true,
|
||||||
|
localHandler: "handlePeopleNotification"
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
enabled: true,
|
||||||
|
localHandler: "handleItemsNotification"
|
||||||
|
},
|
||||||
|
communityAnalytics: {
|
||||||
|
enabled: true,
|
||||||
|
endpoint: "http://localhost:3001/api/analytics/community-events",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Privacy-Level": "aggregated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
observability: {
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "debug",
|
||||||
|
enableMetrics: true,
|
||||||
|
enableHealthChecks: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.config;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load configuration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration
|
||||||
|
*/
|
||||||
|
public getConfig(): TimeSafariConfig {
|
||||||
|
if (!this.config) {
|
||||||
|
throw new Error('Configuration not loaded. Call loadConfig() first.');
|
||||||
|
}
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Endorser.ch API URL for a specific endpoint
|
||||||
|
*/
|
||||||
|
public getEndorserUrl(endpoint: keyof TimeSafariConfig['endorser']['endpoints']): string {
|
||||||
|
const config = this.getConfig();
|
||||||
|
return `${config.endorser.baseUrl}${config.endorser.endpoints[endpoint]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authentication headers
|
||||||
|
*/
|
||||||
|
public getAuthHeaders(): Record<string, string> {
|
||||||
|
const config = this.getConfig();
|
||||||
|
return config.endorser.authentication.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get test data
|
||||||
|
*/
|
||||||
|
public getTestData() {
|
||||||
|
const config = this.getConfig();
|
||||||
|
return config.testData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get notification types for a specific category
|
||||||
|
*/
|
||||||
|
public getNotificationTypes(category: keyof TimeSafariConfig['notificationTypes']) {
|
||||||
|
const config = this.getConfig();
|
||||||
|
return config.notificationTypes[category];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger utility for test apps
|
||||||
|
*/
|
||||||
|
export class TestLogger {
|
||||||
|
private logLevel: string;
|
||||||
|
|
||||||
|
constructor(logLevel: string = 'debug') {
|
||||||
|
this.logLevel = logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldLog(level: string): boolean {
|
||||||
|
const levels = ['error', 'warn', 'info', 'debug'];
|
||||||
|
return levels.indexOf(level) <= levels.indexOf(this.logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(message: string, data?: any) {
|
||||||
|
if (this.shouldLog('debug')) {
|
||||||
|
console.log(`[DEBUG] ${message}`, data || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(message: string, data?: any) {
|
||||||
|
if (this.shouldLog('info')) {
|
||||||
|
console.log(`[INFO] ${message}`, data || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public warn(message: string, data?: any) {
|
||||||
|
if (this.shouldLog('warn')) {
|
||||||
|
console.warn(`[WARN] ${message}`, data || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(message: string, data?: any) {
|
||||||
|
if (this.shouldLog('error')) {
|
||||||
|
console.error(`[ERROR] ${message}`, data || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock DailyNotificationService for test apps
|
||||||
|
*/
|
||||||
|
export class MockDailyNotificationService {
|
||||||
|
private config: TimeSafariConfig;
|
||||||
|
private logger: TestLogger;
|
||||||
|
private isInitialized = false;
|
||||||
|
|
||||||
|
constructor(config: TimeSafariConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.logger = new TestLogger(config.observability.logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the service
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
this.logger.info('Initializing Mock DailyNotificationService');
|
||||||
|
this.isInitialized = true;
|
||||||
|
this.logger.info('Mock DailyNotificationService initialized successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule dual notification (content fetch + user notification)
|
||||||
|
*/
|
||||||
|
public async scheduleDualNotification(config: any): Promise<void> {
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
throw new Error('Service not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('Scheduling dual notification', config);
|
||||||
|
|
||||||
|
// Simulate content fetch
|
||||||
|
if (config.contentFetch?.enabled) {
|
||||||
|
await this.simulateContentFetch(config.contentFetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate user notification
|
||||||
|
if (config.userNotification?.enabled) {
|
||||||
|
await this.simulateUserNotification(config.userNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('Dual notification scheduled successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register callback
|
||||||
|
*/
|
||||||
|
public async registerCallback(name: string, callback: Function): Promise<void> {
|
||||||
|
this.logger.info(`Registering callback: ${name}`);
|
||||||
|
// In a real implementation, this would register the callback
|
||||||
|
this.logger.info(`Callback ${name} registered successfully`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dual schedule status
|
||||||
|
*/
|
||||||
|
public async getDualScheduleStatus(): Promise<any> {
|
||||||
|
return {
|
||||||
|
contentFetch: {
|
||||||
|
enabled: true,
|
||||||
|
lastFetch: new Date().toISOString(),
|
||||||
|
nextFetch: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
userNotification: {
|
||||||
|
enabled: true,
|
||||||
|
lastNotification: new Date().toISOString(),
|
||||||
|
nextNotification: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel all notifications
|
||||||
|
*/
|
||||||
|
public async cancelAllNotifications(): Promise<void> {
|
||||||
|
this.logger.info('Cancelling all notifications');
|
||||||
|
this.logger.info('All notifications cancelled successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate content fetch using Endorser.ch API patterns
|
||||||
|
*/
|
||||||
|
private async simulateContentFetch(config: any): Promise<void> {
|
||||||
|
this.logger.info('Simulating content fetch from Endorser.ch API');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate parallel API requests
|
||||||
|
const testData = this.config.testData;
|
||||||
|
|
||||||
|
// Mock offers to person
|
||||||
|
const offersToPerson = {
|
||||||
|
data: testData.mockOffers,
|
||||||
|
hitLimit: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock offers to projects
|
||||||
|
const offersToProjects = {
|
||||||
|
data: [],
|
||||||
|
hitLimit: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock starred project changes
|
||||||
|
const starredChanges = {
|
||||||
|
data: testData.mockProjects,
|
||||||
|
hitLimit: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.info('Content fetch simulation completed', {
|
||||||
|
offersToPerson: offersToPerson.data.length,
|
||||||
|
offersToProjects: offersToProjects.data.length,
|
||||||
|
starredChanges: starredChanges.data.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call success callback if provided
|
||||||
|
if (config.callbacks?.onSuccess) {
|
||||||
|
await config.callbacks.onSuccess({
|
||||||
|
offersToPerson,
|
||||||
|
offersToProjects,
|
||||||
|
starredChanges
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Content fetch simulation failed', error);
|
||||||
|
if (config.callbacks?.onError) {
|
||||||
|
await config.callbacks.onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate user notification
|
||||||
|
*/
|
||||||
|
private async simulateUserNotification(config: any): Promise<void> {
|
||||||
|
this.logger.info('Simulating user notification', {
|
||||||
|
title: config.title,
|
||||||
|
body: config.body,
|
||||||
|
time: config.schedule
|
||||||
|
});
|
||||||
|
this.logger.info('User notification simulation completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test API Server for Daily Notification Plugin
|
* Test API Server for TimeSafari Daily Notification Plugin
|
||||||
*
|
*
|
||||||
* Provides mock content endpoints for testing the plugin's
|
* Simulates Endorser.ch API endpoints for testing the plugin's
|
||||||
* network fetching, ETag support, and error handling capabilities.
|
* network fetching, pagination, and TimeSafari-specific functionality.
|
||||||
*
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
* @version 1.0.0
|
* @version 2.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
@@ -24,67 +24,110 @@ app.use(express.json());
|
|||||||
// In-memory storage for testing
|
// In-memory storage for testing
|
||||||
let contentStore = new Map();
|
let contentStore = new Map();
|
||||||
let etagStore = new Map();
|
let etagStore = new Map();
|
||||||
|
let offersStore = new Map();
|
||||||
|
let projectsStore = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate mock notification content for a given slot
|
* Generate mock offer data for TimeSafari testing
|
||||||
* @param {string} slotId - The notification slot identifier
|
* @param {string} recipientDid - DID of the recipient
|
||||||
* @param {number} timestamp - Current timestamp
|
* @param {string} afterId - JWT ID for pagination
|
||||||
* @returns {Object} Mock notification content
|
* @returns {Object} Mock offer data
|
||||||
*/
|
*/
|
||||||
function generateMockContent(slotId, timestamp) {
|
function generateMockOffers(recipientDid, afterId) {
|
||||||
const slotTime = slotId.split('-')[1] || '08:00';
|
const offers = [];
|
||||||
const contentId = crypto.randomUUID().substring(0, 8);
|
const offerCount = Math.floor(Math.random() * 5) + 1; // 1-5 offers
|
||||||
|
|
||||||
|
for (let i = 0; i < offerCount; i++) {
|
||||||
|
const jwtId = `01HSE3R9MAC0FT3P3KZ382TWV${7 + i}`;
|
||||||
|
const handleId = `offer-${crypto.randomUUID().substring(0, 8)}`;
|
||||||
|
|
||||||
|
offers.push({
|
||||||
|
jwtId: jwtId,
|
||||||
|
handleId: handleId,
|
||||||
|
issuedAt: new Date().toISOString(),
|
||||||
|
offeredByDid: `did:example:offerer${i + 1}`,
|
||||||
|
recipientDid: recipientDid,
|
||||||
|
unit: 'USD',
|
||||||
|
amount: Math.floor(Math.random() * 5000) + 500,
|
||||||
|
amountGiven: Math.floor(Math.random() * 2000) + 200,
|
||||||
|
amountGivenConfirmed: Math.floor(Math.random() * 1000) + 100,
|
||||||
|
objectDescription: `Community service offer ${i + 1}`,
|
||||||
|
validThrough: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
fullClaim: {
|
||||||
|
type: 'Offer',
|
||||||
|
issuer: `did:example:offerer${i + 1}`,
|
||||||
|
recipient: recipientDid,
|
||||||
|
object: {
|
||||||
|
description: `Community service offer ${i + 1}`,
|
||||||
|
amount: Math.floor(Math.random() * 5000) + 500,
|
||||||
|
unit: 'USD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: contentId,
|
data: offers,
|
||||||
slotId: slotId,
|
hitLimit: offers.length >= 3 // Simulate hit limit
|
||||||
title: `Daily Update - ${slotTime}`,
|
|
||||||
body: `Your personalized content for ${slotTime}. Content ID: ${contentId}`,
|
|
||||||
timestamp: timestamp,
|
|
||||||
priority: 'high',
|
|
||||||
category: 'daily',
|
|
||||||
actions: [
|
|
||||||
{ id: 'view', title: 'View Details' },
|
|
||||||
{ id: 'dismiss', title: 'Dismiss' }
|
|
||||||
],
|
|
||||||
metadata: {
|
|
||||||
source: 'test-api',
|
|
||||||
version: '1.0.0',
|
|
||||||
generated: new Date(timestamp).toISOString()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate ETag for content
|
* Generate mock project data for TimeSafari testing
|
||||||
* @param {Object} content - Content object
|
* @param {Array} planIds - Array of plan IDs
|
||||||
* @returns {string} ETag value
|
* @param {string} afterId - JWT ID for pagination
|
||||||
|
* @returns {Object} Mock project data
|
||||||
*/
|
*/
|
||||||
function generateETag(content) {
|
function generateMockProjects(planIds, afterId) {
|
||||||
const contentString = JSON.stringify(content);
|
const projects = [];
|
||||||
return `"${crypto.createHash('md5').update(contentString).digest('hex')}"`;
|
|
||||||
|
planIds.forEach((planId, index) => {
|
||||||
|
const jwtId = `01HSE3R9MAC0FT3P3KZ382TWV${8 + index}`;
|
||||||
|
|
||||||
|
projects.push({
|
||||||
|
plan: {
|
||||||
|
jwtId: jwtId,
|
||||||
|
handleId: planId,
|
||||||
|
name: `Community Project ${index + 1}`,
|
||||||
|
description: `Description for ${planId}`,
|
||||||
|
issuerDid: `did:example:issuer${index + 1}`,
|
||||||
|
agentDid: `did:example:agent${index + 1}`,
|
||||||
|
startTime: new Date().toISOString(),
|
||||||
|
endTime: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
locLat: 40.7128 + (Math.random() - 0.5) * 0.1,
|
||||||
|
locLon: -74.0060 + (Math.random() - 0.5) * 0.1,
|
||||||
|
url: `https://timesafari.com/projects/${planId}`,
|
||||||
|
category: 'community',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
wrappedClaimBefore: null // Simulate no previous claim
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: projects,
|
||||||
|
hitLimit: projects.length >= 2 // Simulate hit limit
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store content with ETag
|
* Generate mock notification bundle for TimeSafari
|
||||||
* @param {string} slotId - Slot identifier
|
* @param {Object} params - Request parameters
|
||||||
* @param {Object} content - Content object
|
* @returns {Object} Mock notification bundle
|
||||||
* @param {string} etag - ETag value
|
|
||||||
*/
|
*/
|
||||||
function storeContent(slotId, content, etag) {
|
function generateNotificationBundle(params) {
|
||||||
contentStore.set(slotId, content);
|
const { userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId } = params;
|
||||||
etagStore.set(slotId, etag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Get stored content and ETag
|
offersToPerson: generateMockOffers(userDid, lastKnownOfferId),
|
||||||
* @param {string} slotId - Slot identifier
|
offersToProjects: {
|
||||||
* @returns {Object} { content, etag } or null
|
data: [],
|
||||||
*/
|
hitLimit: false
|
||||||
function getStoredContent(slotId) {
|
},
|
||||||
const content = contentStore.get(slotId);
|
starredChanges: generateMockProjects(starredPlanIds, lastKnownPlanId),
|
||||||
const etag = etagStore.get(slotId);
|
timestamp: new Date().toISOString(),
|
||||||
return content && etag ? { content, etag } : null;
|
bundleId: crypto.randomUUID()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
@@ -96,19 +139,125 @@ app.get('/health', (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
version: '1.0.0',
|
version: '2.0.0',
|
||||||
|
service: 'TimeSafari Test API',
|
||||||
endpoints: {
|
endpoints: {
|
||||||
content: '/api/content/:slotId',
|
|
||||||
health: '/health',
|
health: '/health',
|
||||||
metrics: '/api/metrics',
|
offers: '/api/v2/report/offers',
|
||||||
error: '/api/error/:type'
|
offersToPlans: '/api/v2/report/offersToPlansOwnedByMe',
|
||||||
|
plansLastUpdated: '/api/v2/report/plansLastUpdatedBetween',
|
||||||
|
notificationsBundle: '/api/v2/report/notifications/bundle',
|
||||||
|
analytics: '/api/analytics/community-events',
|
||||||
|
metrics: '/api/metrics'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get notification content for a specific slot
|
* Endorser.ch API: Get offers to person
|
||||||
* Supports ETag conditional requests
|
*/
|
||||||
|
app.get('/api/v2/report/offers', (req, res) => {
|
||||||
|
const { recipientId, afterId } = req.query;
|
||||||
|
|
||||||
|
console.log(`[${new Date().toISOString()}] GET /api/v2/report/offers`);
|
||||||
|
console.log(` recipientId: ${recipientId}, afterId: ${afterId || 'none'}`);
|
||||||
|
|
||||||
|
if (!recipientId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'recipientId parameter is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const offers = generateMockOffers(recipientId, afterId);
|
||||||
|
|
||||||
|
console.log(` → 200 OK (${offers.data.length} offers, hitLimit: ${offers.hitLimit})`);
|
||||||
|
res.json(offers);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endorser.ch API: Get offers to user's projects
|
||||||
|
*/
|
||||||
|
app.get('/api/v2/report/offersToPlansOwnedByMe', (req, res) => {
|
||||||
|
const { afterId } = req.query;
|
||||||
|
|
||||||
|
console.log(`[${new Date().toISOString()}] GET /api/v2/report/offersToPlansOwnedByMe`);
|
||||||
|
console.log(` afterId: ${afterId || 'none'}`);
|
||||||
|
|
||||||
|
const offers = {
|
||||||
|
data: [], // Simulate no offers to user's projects
|
||||||
|
hitLimit: false
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(` → 200 OK (${offers.data.length} offers, hitLimit: ${offers.hitLimit})`);
|
||||||
|
res.json(offers);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endorser.ch API: Get changes to starred projects
|
||||||
|
*/
|
||||||
|
app.post('/api/v2/report/plansLastUpdatedBetween', (req, res) => {
|
||||||
|
const { planIds, afterId } = req.body;
|
||||||
|
|
||||||
|
console.log(`[${new Date().toISOString()}] POST /api/v2/report/plansLastUpdatedBetween`);
|
||||||
|
console.log(` planIds: ${JSON.stringify(planIds)}, afterId: ${afterId || 'none'}`);
|
||||||
|
|
||||||
|
if (!planIds || !Array.isArray(planIds)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'planIds array is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = generateMockProjects(planIds, afterId);
|
||||||
|
|
||||||
|
console.log(` → 200 OK (${projects.data.length} projects, hitLimit: ${projects.hitLimit})`);
|
||||||
|
res.json(projects);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimeSafari API: Get notification bundle
|
||||||
|
*/
|
||||||
|
app.get('/api/v2/report/notifications/bundle', (req, res) => {
|
||||||
|
const { userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId } = req.query;
|
||||||
|
|
||||||
|
console.log(`[${new Date().toISOString()}] GET /api/v2/report/notifications/bundle`);
|
||||||
|
console.log(` userDid: ${userDid}, starredPlanIds: ${starredPlanIds}`);
|
||||||
|
|
||||||
|
if (!userDid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'userDid parameter is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundle = generateNotificationBundle({
|
||||||
|
userDid,
|
||||||
|
starredPlanIds: starredPlanIds ? JSON.parse(starredPlanIds) : [],
|
||||||
|
lastKnownOfferId,
|
||||||
|
lastKnownPlanId
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` → 200 OK (bundle generated)`);
|
||||||
|
res.json(bundle);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimeSafari Analytics: Community events
|
||||||
|
*/
|
||||||
|
app.post('/api/analytics/community-events', (req, res) => {
|
||||||
|
const { client_id, events } = req.body;
|
||||||
|
|
||||||
|
console.log(`[${new Date().toISOString()}] POST /api/analytics/community-events`);
|
||||||
|
console.log(` client_id: ${client_id}, events: ${events?.length || 0}`);
|
||||||
|
|
||||||
|
// Simulate analytics processing
|
||||||
|
res.json({
|
||||||
|
status: 'success',
|
||||||
|
processed: events?.length || 0,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy content endpoint (for backward compatibility)
|
||||||
*/
|
*/
|
||||||
app.get('/api/content/:slotId', (req, res) => {
|
app.get('/api/content/:slotId', (req, res) => {
|
||||||
const { slotId } = req.params;
|
const { slotId } = req.params;
|
||||||
@@ -127,91 +276,61 @@ app.get('/api/content/:slotId', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have stored content
|
// Check if we have stored content
|
||||||
const stored = getStoredContent(slotId);
|
const stored = contentStore.get(slotId);
|
||||||
|
const etag = etagStore.get(slotId);
|
||||||
|
|
||||||
if (stored && ifNoneMatch === stored.etag) {
|
if (stored && etag && ifNoneMatch === etag) {
|
||||||
// Content hasn't changed, return 304 Not Modified
|
// Content hasn't changed, return 304 Not Modified
|
||||||
console.log(` → 304 Not Modified (ETag match)`);
|
console.log(` → 304 Not Modified (ETag match)`);
|
||||||
return res.status(304).end();
|
return res.status(304).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new content
|
// Generate new content
|
||||||
const content = generateMockContent(slotId, timestamp);
|
const content = {
|
||||||
const etag = generateETag(content);
|
id: crypto.randomUUID().substring(0, 8),
|
||||||
|
slotId: slotId,
|
||||||
|
title: `TimeSafari Community Update - ${slotId.split('-')[1]}`,
|
||||||
|
body: `Your personalized TimeSafari content for ${slotId.split('-')[1]}`,
|
||||||
|
timestamp: timestamp,
|
||||||
|
priority: 'high',
|
||||||
|
category: 'community',
|
||||||
|
actions: [
|
||||||
|
{ id: 'view_offers', title: 'View Offers' },
|
||||||
|
{ id: 'view_projects', title: 'See Projects' },
|
||||||
|
{ id: 'view_people', title: 'Check People' },
|
||||||
|
{ id: 'view_items', title: 'Browse Items' },
|
||||||
|
{ id: 'dismiss', title: 'Dismiss' }
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
source: 'timesafari-test-api',
|
||||||
|
version: '2.0.0',
|
||||||
|
generated: new Date(timestamp).toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newEtag = `"${crypto.createHash('md5').update(JSON.stringify(content)).digest('hex')}"`;
|
||||||
|
|
||||||
// Store for future ETag checks
|
// Store for future ETag checks
|
||||||
storeContent(slotId, content, etag);
|
contentStore.set(slotId, content);
|
||||||
|
etagStore.set(slotId, newEtag);
|
||||||
|
|
||||||
// Set ETag header
|
// Set ETag header
|
||||||
res.set('ETag', etag);
|
res.set('ETag', newEtag);
|
||||||
res.set('Cache-Control', 'no-cache');
|
res.set('Cache-Control', 'no-cache');
|
||||||
res.set('Last-Modified', new Date(timestamp).toUTCString());
|
res.set('Last-Modified', new Date(timestamp).toUTCString());
|
||||||
|
|
||||||
console.log(` → 200 OK (new content, ETag: ${etag})`);
|
console.log(` → 200 OK (new content, ETag: ${newEtag})`);
|
||||||
res.json(content);
|
res.json(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulate network errors for testing error handling
|
|
||||||
*/
|
|
||||||
app.get('/api/error/:type', (req, res) => {
|
|
||||||
const { type } = req.params;
|
|
||||||
|
|
||||||
console.log(`[${new Date().toISOString()}] GET /api/error/${type}`);
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'timeout':
|
|
||||||
// Simulate timeout by not responding
|
|
||||||
setTimeout(() => {
|
|
||||||
res.status(408).json({ error: 'Request timeout' });
|
|
||||||
}, 15000); // 15 second timeout
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'server-error':
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Internal server error',
|
|
||||||
code: 'INTERNAL_ERROR',
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'not-found':
|
|
||||||
res.status(404).json({
|
|
||||||
error: 'Content not found',
|
|
||||||
code: 'NOT_FOUND',
|
|
||||||
slotId: req.query.slotId || 'unknown'
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'rate-limit':
|
|
||||||
res.status(429).json({
|
|
||||||
error: 'Rate limit exceeded',
|
|
||||||
code: 'RATE_LIMIT',
|
|
||||||
retryAfter: 60
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'unauthorized':
|
|
||||||
res.status(401).json({
|
|
||||||
error: 'Unauthorized',
|
|
||||||
code: 'UNAUTHORIZED'
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
res.status(400).json({
|
|
||||||
error: 'Unknown error type',
|
|
||||||
available: ['timeout', 'server-error', 'not-found', 'rate-limit', 'unauthorized']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API metrics endpoint
|
* API metrics endpoint
|
||||||
*/
|
*/
|
||||||
app.get('/api/metrics', (req, res) => {
|
app.get('/api/metrics', (req, res) => {
|
||||||
const metrics = {
|
const metrics = {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
service: 'TimeSafari Test API',
|
||||||
|
version: '2.0.0',
|
||||||
contentStore: {
|
contentStore: {
|
||||||
size: contentStore.size,
|
size: contentStore.size,
|
||||||
slots: Array.from(contentStore.keys())
|
slots: Array.from(contentStore.keys())
|
||||||
@@ -221,7 +340,21 @@ app.get('/api/metrics', (req, res) => {
|
|||||||
etags: Array.from(etagStore.entries())
|
etags: Array.from(etagStore.entries())
|
||||||
},
|
},
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
memory: process.memoryUsage()
|
memory: process.memoryUsage(),
|
||||||
|
endpoints: {
|
||||||
|
total: 8,
|
||||||
|
active: 8,
|
||||||
|
health: '/health',
|
||||||
|
endorser: {
|
||||||
|
offers: '/api/v2/report/offers',
|
||||||
|
offersToPlans: '/api/v2/report/offersToPlansOwnedByMe',
|
||||||
|
plansLastUpdated: '/api/v2/report/plansLastUpdatedBetween'
|
||||||
|
},
|
||||||
|
timesafari: {
|
||||||
|
notificationsBundle: '/api/v2/report/notifications/bundle',
|
||||||
|
analytics: '/api/analytics/community-events'
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json(metrics);
|
res.json(metrics);
|
||||||
@@ -240,33 +373,6 @@ app.delete('/api/content', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Update content for a specific slot (for testing content changes)
|
|
||||||
*/
|
|
||||||
app.put('/api/content/:slotId', (req, res) => {
|
|
||||||
const { slotId } = req.params;
|
|
||||||
const { content } = req.body;
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Content is required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const etag = generateETag(content);
|
|
||||||
|
|
||||||
storeContent(slotId, content, etag);
|
|
||||||
|
|
||||||
res.set('ETag', etag);
|
|
||||||
res.json({
|
|
||||||
message: 'Content updated',
|
|
||||||
slotId,
|
|
||||||
etag,
|
|
||||||
timestamp
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Error handling middleware
|
// Error handling middleware
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
console.error(`[${new Date().toISOString()}] Error:`, err);
|
console.error(`[${new Date().toISOString()}] Error:`, err);
|
||||||
@@ -289,13 +395,15 @@ app.use((req, res) => {
|
|||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`🚀 Test API Server running on port ${PORT}`);
|
console.log(`🚀 TimeSafari Test API Server running on port ${PORT}`);
|
||||||
console.log(`📋 Available endpoints:`);
|
console.log(`📋 Available endpoints:`);
|
||||||
console.log(` GET /health - Health check`);
|
console.log(` GET /health - Health check`);
|
||||||
console.log(` GET /api/content/:slotId - Get notification content`);
|
console.log(` GET /api/v2/report/offers - Get offers to person`);
|
||||||
console.log(` PUT /api/content/:slotId - Update content`);
|
console.log(` GET /api/v2/report/offersToPlansOwnedByMe - Get offers to user's projects`);
|
||||||
console.log(` DELETE /api/content - Clear all content`);
|
console.log(` POST /api/v2/report/plansLastUpdatedBetween - Get changes to starred projects`);
|
||||||
console.log(` GET /api/error/:type - Simulate errors`);
|
console.log(` GET /api/v2/report/notifications/bundle - Get TimeSafari notification bundle`);
|
||||||
|
console.log(` POST /api/analytics/community-events - Send community analytics`);
|
||||||
|
console.log(` GET /api/content/:slotId - Legacy content endpoint`);
|
||||||
console.log(` GET /api/metrics - API metrics`);
|
console.log(` GET /api/metrics - API metrics`);
|
||||||
console.log(``);
|
console.log(``);
|
||||||
console.log(`🔧 Environment:`);
|
console.log(`🔧 Environment:`);
|
||||||
@@ -304,18 +412,18 @@ app.listen(PORT, () => {
|
|||||||
console.log(``);
|
console.log(``);
|
||||||
console.log(`📝 Usage examples:`);
|
console.log(`📝 Usage examples:`);
|
||||||
console.log(` curl http://localhost:${PORT}/health`);
|
console.log(` curl http://localhost:${PORT}/health`);
|
||||||
console.log(` curl http://localhost:${PORT}/api/content/slot-08:00`);
|
console.log(` curl "http://localhost:${PORT}/api/v2/report/offers?recipientId=did:example:testuser123&afterId=01HSE3R9MAC0FT3P3KZ382TWV7"`);
|
||||||
console.log(` curl -H "If-None-Match: \\"abc123\\"" http://localhost:${PORT}/api/content/slot-08:00`);
|
console.log(` curl -X POST http://localhost:${PORT}/api/v2/report/plansLastUpdatedBetween -H "Content-Type: application/json" -d '{"planIds":["plan-123","plan-456"],"afterId":"01HSE3R9MAC0FT3P3KZ382TWV8"}'`);
|
||||||
console.log(` curl http://localhost:${PORT}/api/error/timeout`);
|
console.log(` curl "http://localhost:${PORT}/api/v2/report/notifications/bundle?userDid=did:example:testuser123&starredPlanIds=[\"plan-123\",\"plan-456\"]"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\n🛑 Shutting down Test API Server...');
|
console.log('\n🛑 Shutting down TimeSafari Test API Server...');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
console.log('\n🛑 Shutting down Test API Server...');
|
console.log('\n🛑 Shutting down TimeSafari Test API Server...');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user