Browse Source

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
research/notification-plugin-enhancement
Matthew Raymer 1 week ago
parent
commit
fe82fd2147
  1. 1612
      INTEGRATION_GUIDE.md
  2. 73
      test-apps/README.md
  3. 11
      test-apps/android-test/src/index.html
  4. 342
      test-apps/android-test/src/index.ts
  5. 152
      test-apps/config/timesafari-config.json
  6. 8
      test-apps/electron-test/src/index.html
  7. 325
      test-apps/electron-test/src/index.ts
  8. 11
      test-apps/ios-test/src/index.html
  9. 337
      test-apps/ios-test/src/index.ts
  10. 522
      test-apps/shared/config-loader.ts
  11. 424
      test-apps/test-api/server.js

1612
INTEGRATION_GUIDE.md

File diff suppressed because it is too large

73
test-apps/README.md

@ -1,8 +1,8 @@
# Test Apps Setup Guide
# TimeSafari Test Apps Setup Guide
## 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
@ -11,7 +11,11 @@ test-apps/
├── android-test/ # Android test app
├── ios-test/ # iOS 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-ios.sh # iOS setup script
├── setup-electron.sh # Electron setup script
@ -27,6 +31,8 @@ test-apps/
- Android Studio (for Android)
- Xcode (for iOS)
- Platform-specific SDKs
- Understanding of TimeSafari's community-building purpose
- Familiarity with Endorser.ch API patterns
## Quick Start
@ -61,8 +67,9 @@ See [Enhanced Setup Guide](SETUP_GUIDE.md) for detailed manual setup instruction
## Test App Features
Each test app includes:
- **Plugin Configuration**: Test shared SQLite, TTL, prefetch settings
- **Notification Scheduling**: Basic daily notification setup
- **TimeSafari Configuration**: Test community-focused notification settings
- **Endorser.ch API Integration**: Test real API patterns with pagination
- **Community Notification Scheduling**: Test offers, projects, people, and items notifications
- **Platform-Specific Features**:
- Android: Exact alarm permissions, reboot recovery
- iOS: Rolling window management, BGTaskScheduler
@ -71,13 +78,13 @@ Each test app includes:
- **Error Handling**: Comprehensive error testing
- **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
```bash
# Start the test API server
# Start the TimeSafari Test API server
cd test-apps/test-api
npm install
npm start
@ -87,19 +94,30 @@ npm run demo
```
### Key Features
- **Content Endpoints**: Generate mock notification content
- **ETag Support**: Full HTTP caching with conditional requests
- **Endorser.ch API Simulation**: Mock endpoints for offers, projects, and pagination
- **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
- **Metrics**: Monitor API usage and performance
- **CORS Enabled**: Cross-origin requests supported
### 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 /api/content/:slotId` - Get notification content
- `GET /api/error/:type` - Simulate errors
- `GET /api/metrics` - API metrics
- `PUT /api/content/:slotId` - Update content
- `DELETE /api/content` - Clear all content
### Platform-Specific URLs
- **Web/Electron**: `http://localhost:3001`
@ -110,19 +128,25 @@ npm run demo
## Platform-Specific Testing
### Android Test App
- **TimeSafari Configuration**: Test community notification settings
- **Endorser.ch API Integration**: Test parallel API requests
- **Exact Alarm Status**: Check permission and capability
- **Permission Requests**: Test exact alarm permission flow
- **Performance Metrics**: Monitor Android-specific optimizations
- **Reboot Recovery**: Validate system restart handling
### iOS Test App
- **TimeSafari Configuration**: Test iOS community features
- **Rolling Window**: Test notification limit management
- **Endorser.ch API Integration**: Test pagination patterns
- **Background Tasks**: Validate BGTaskScheduler integration
- **Performance Metrics**: Monitor iOS-specific optimizations
- **Memory Management**: Test object pooling and cleanup
### Electron Test App
- **TimeSafari Configuration**: Test Electron community features
- **Mock Implementations**: Test web platform compatibility
- **Endorser.ch API Integration**: Test API patterns
- **IPC Communication**: Validate Electron-specific APIs
- **Development Workflow**: Test plugin integration
- **Debug Information**: Platform-specific status display
@ -155,8 +179,9 @@ npm run dev # Run in development mode
## Testing Checklist
### Core Functionality
- [ ] Plugin configuration works
- [ ] Notification scheduling succeeds
- [ ] TimeSafari configuration works
- [ ] Community notification scheduling succeeds
- [ ] Endorser.ch API integration functions properly
- [ ] Error handling functions properly
- [ ] Performance metrics are accurate
@ -166,9 +191,11 @@ npm run dev # Run in development mode
- [ ] Electron mock implementations
- [ ] Cross-platform API consistency
### Integration
### TimeSafari Integration
- [ ] Plugin loads without errors
- [ ] Configuration persists across sessions
- [ ] Endorser.ch API pagination works
- [ ] Community notification types process correctly
- [ ] Performance optimizations active
- [ ] 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
3. **Build failures** → Check Node.js version (18+) and clear cache: `npm cache clean --force`
4. **Platform errors** → Verify platform-specific SDKs are installed
5. **API connection errors** → Ensure test API server is running on port 3001
### Quick Fixes
```bash
@ -193,6 +221,9 @@ npx cap clean
# Re-sync platforms
npx cap sync
# Restart test API server
cd test-api && npm start
```
### Detailed Help
@ -201,7 +232,9 @@ See [Enhanced Setup Guide](SETUP_GUIDE.md) for comprehensive troubleshooting and
## Next Steps
1. **Run Setup Scripts**: Execute platform-specific setup
2. **Test Core Features**: Validate basic functionality
3. **Test Platform Features**: Verify platform-specific capabilities
4. **Integration Testing**: Test with actual plugin implementation
5. **Performance Validation**: Monitor metrics and optimizations
2. **Start Test API Server**: Run the TimeSafari Test API server
3. **Test Core Features**: Validate basic TimeSafari functionality
4. **Test Platform Features**: Verify platform-specific capabilities
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

11
test-apps/android-test/src/index.html

@ -89,15 +89,16 @@
</head>
<body>
<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="button-grid">
<button id="configure">Configure Plugin</button>
<button id="schedule">Schedule Notification</button>
<button id="alarm-status">Check Alarm Status</button>
<button id="request-permission">Request Permission</button>
<button id="configure">Configure TimeSafari</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="status">Check Status</button>
<button id="performance">Performance Metrics</button>
</div>

342
test-apps/android-test/src/index.ts

@ -1,77 +1,47 @@
import { Capacitor } from '@capacitor/core';
import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader';
// Mock plugin for development
const DailyNotification = {
async configure(options: any) {
console.log('Configure called:', options);
return Promise.resolve();
},
async scheduleDailyNotification(options: any) {
console.log('Schedule called:', options);
return Promise.resolve();
},
async getExactAlarmStatus() {
return Promise.resolve({
supported: true,
enabled: false,
canSchedule: false,
fallbackWindow: '±10 minutes'
});
},
async requestExactAlarmPermission() {
console.log('Request exact alarm permission');
return Promise.resolve();
},
async getPerformanceMetrics() {
return Promise.resolve({
overallScore: 85,
databasePerformance: 90,
memoryEfficiency: 80,
batteryEfficiency: 85,
objectPoolEfficiency: 90,
totalDatabaseQueries: 150,
averageMemoryUsage: 25.5,
objectPoolHits: 45,
backgroundCpuUsage: 2.3,
totalNetworkRequests: 12,
recommendations: ['Enable ETag support', 'Optimize memory usage']
});
}
};
// Test interface
class TestApp {
// Test interface for TimeSafari Android integration
class TimeSafariAndroidTestApp {
private statusElement: HTMLElement;
private logElement: HTMLElement;
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('Test app initialized');
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('alarm-status')?.addEventListener('click', () => this.testAlarmStatus());
document.getElementById('request-permission')?.addEventListener('click', () => this.testRequestPermission());
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 configuration...');
await DailyNotification.configure({
storage: 'shared',
ttlSeconds: 1800,
prefetchLeadMinutes: 15,
enableETagSupport: true,
enableErrorHandling: true,
enablePerformanceOptimization: true
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.log('✅ Configuration successful');
this.updateStatus('Configured');
} catch (error) {
this.log(`❌ Configuration failed: ${error}`);
@ -80,53 +50,271 @@ class TestApp {
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.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);
},
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('✅ Community notification scheduled successfully');
this.updateStatus('Scheduled');
} catch (error) {
this.log(`❌ Scheduling failed: ${error}`);
}
}
private async testAlarmStatus() {
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 exact alarm status...');
const status = await DailyNotification.getExactAlarmStatus();
this.log(`📱 Alarm Status:`, status);
this.updateStatus(`Alarm: ${status.canSchedule ? 'Enabled' : 'Disabled'}`);
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(`❌ Alarm status check failed: ${error}`);
this.log(`Callback registration failed: ${error}`);
}
}
private async testRequestPermission() {
private async testStatus() {
try {
this.log('Testing permission request...');
await DailyNotification.requestExactAlarmPermission();
this.log('✅ Permission request sent');
this.updateStatus('Permission Requested');
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(`❌ Permission request failed: ${error}`);
this.log(`Status check failed: ${error}`);
}
}
private async testPerformance() {
try {
this.log('Testing performance metrics...');
const metrics = await DailyNotification.getPerformanceMetrics();
this.log(`📊 Performance Metrics:`, metrics);
this.log('Testing Android performance metrics...');
const metrics = {
overallScore: 85,
databasePerformance: 90,
memoryEfficiency: 80,
batteryEfficiency: 85,
objectPoolEfficiency: 90,
totalDatabaseQueries: 150,
averageMemoryUsage: 25.5,
objectPoolHits: 45,
backgroundCpuUsage: 2.3,
totalNetworkRequests: 12,
recommendations: ['Enable ETag support', 'Optimize memory usage']
};
this.log('📊 Android Performance Metrics:', metrics);
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
} catch (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) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
@ -150,5 +338,5 @@ class TestApp {
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new TestApp();
new TimeSafariAndroidTestApp();
});

152
test-apps/config/timesafari-config.json

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

8
test-apps/electron-test/src/index.html

@ -89,13 +89,15 @@
</head>
<body>
<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="button-grid">
<button id="configure">Configure Plugin</button>
<button id="schedule">Schedule Notification</button>
<button id="configure">Configure TimeSafari</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="performance">Performance Metrics</button>
</div>

325
test-apps/electron-test/src/index.ts

@ -1,18 +1,28 @@
// Electron test interface
class TestApp {
import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader';
// Test interface for TimeSafari Electron integration
class TimeSafariElectronTestApp {
private statusElement: HTMLElement;
private logElement: HTMLElement;
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('Electron Test app initialized');
this.log('TimeSafari Electron 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('debug-info')?.addEventListener('click', () => this.testDebugInfo());
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
@ -20,80 +30,299 @@ class TestApp {
private async testConfigure() {
try {
this.log('Testing Electron configuration...');
const result = await (window as any).electronAPI.configurePlugin({
storage: 'mock',
ttlSeconds: 1800,
prefetchLeadMinutes: 15,
enableETagSupport: true,
enableErrorHandling: true,
enablePerformanceOptimization: true
});
this.log('Testing TimeSafari Electron configuration...');
await this.configLoader.loadConfig();
const config = this.configLoader.getConfig();
if (result.success) {
this.log('✅ Electron Configuration successful');
this.updateStatus('Configured');
} else {
this.log(`❌ Configuration failed: ${result.error}`);
}
await this.notificationService.initialize();
this.log('✅ TimeSafari Electron configuration successful', {
appId: config.timesafari.appId,
appName: config.timesafari.appName,
version: config.timesafari.version
});
this.updateStatus('Configured');
} catch (error) {
this.log(`❌ Configuration error: ${error}`);
this.log(`❌ Configuration failed: ${error}`);
}
}
private async testSchedule() {
try {
this.log('Testing Electron notification scheduling...');
const result = await (window as any).electronAPI.scheduleNotification({
url: 'https://api.example.com/daily-content',
time: '09:00',
title: 'Daily Electron Test Notification',
body: 'This is a test notification from the Electron test app'
this.log('Testing TimeSafari Electron 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);
},
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.log('✅ Electron Notification scheduled successfully');
this.updateStatus('Scheduled');
} else {
this.log(`❌ Scheduling failed: ${result.error}`);
}
this.updateStatus('API Connected');
} 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() {
try {
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:', result.data);
this.updateStatus(`Debug: ${result.data.status}`);
} else {
this.log(`❌ Debug info failed: ${result.error}`);
}
this.log('🔍 Electron Debug Info:', debugInfo);
this.updateStatus(`Debug: ${debugInfo.status}`);
} catch (error) {
this.log(`❌ Debug info error: ${error}`);
this.log(`❌ Debug info failed: ${error}`);
}
}
private async testPerformance() {
try {
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']
};
this.log('📊 Electron Performance Metrics:', metrics);
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
} catch (error) {
this.log(`❌ Performance check failed: ${error}`);
}
}
if (result.success) {
this.log('📊 Electron Performance Metrics:', result.data);
this.updateStatus(`Performance: ${result.data.overallScore}/100`);
} else {
this.log(`❌ Performance check failed: ${result.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(`❌ Performance error: ${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) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
@ -117,5 +346,5 @@ class TestApp {
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new TestApp();
new TimeSafariElectronTestApp();
});

11
test-apps/ios-test/src/index.html

@ -89,15 +89,16 @@
</head>
<body>
<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="button-grid">
<button id="configure">Configure Plugin</button>
<button id="schedule">Schedule Notification</button>
<button id="rolling-window">Maintain Window</button>
<button id="window-stats">Window Stats</button>
<button id="configure">Configure TimeSafari</button>
<button id="schedule">Schedule Community Notifications</button>
<button id="rolling-window">Maintain Rolling Window</button>
<button id="endorser-api">Test Endorser.ch API</button>
<button id="callbacks">Register Callbacks</button>
<button id="performance">Performance Metrics</button>
</div>

337
test-apps/ios-test/src/index.ts

@ -1,76 +1,47 @@
import { Capacitor } from '@capacitor/core';
import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader';
// Mock plugin for development
const DailyNotification = {
async configure(options: any) {
console.log('Configure called:', options);
return Promise.resolve();
},
async scheduleDailyNotification(options: any) {
console.log('Schedule called:', options);
return Promise.resolve();
},
async maintainRollingWindow() {
console.log('Maintain rolling window called');
return Promise.resolve();
},
async getRollingWindowStats() {
return Promise.resolve({
stats: '64 pending notifications, 20 daily limit',
maintenanceNeeded: false,
timeUntilNextMaintenance: 900000
});
},
async getPerformanceMetrics() {
return Promise.resolve({
overallScore: 88,
databasePerformance: 92,
memoryEfficiency: 85,
batteryEfficiency: 90,
objectPoolEfficiency: 88,
totalDatabaseQueries: 120,
averageMemoryUsage: 22.3,
objectPoolHits: 38,
backgroundCpuUsage: 1.8,
totalNetworkRequests: 8,
recommendations: ['Enable background tasks', 'Optimize memory usage']
});
}
};
// Test interface
class TestApp {
// Test interface for TimeSafari iOS integration
class TimeSafariIOSTestApp {
private statusElement: HTMLElement;
private logElement: HTMLElement;
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('iOS Test app initialized');
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('window-stats')?.addEventListener('click', () => this.testWindowStats());
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 iOS configuration...');
await DailyNotification.configure({
storage: 'shared',
ttlSeconds: 1800,
prefetchLeadMinutes: 15,
enableETagSupport: true,
enableErrorHandling: true,
enablePerformanceOptimization: true
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.log('✅ iOS Configuration successful');
this.updateStatus('Configured');
} catch (error) {
this.log(`❌ Configuration failed: ${error}`);
@ -79,53 +50,277 @@ class TestApp {
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.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);
},
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('✅ iOS community notification scheduled successfully');
this.updateStatus('Scheduled');
} catch (error) {
this.log(`❌ iOS Scheduling failed: ${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');
// Simulate rolling window maintenance
const stats = {
stats: '64 pending notifications, 20 daily limit',
maintenanceNeeded: false,
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 testWindowStats() {
private async testEndorserAPI() {
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'}`);
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
});
this.updateStatus('API Connected');
} catch (error) {
this.log(`❌ Window stats check failed: ${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 = await DailyNotification.getPerformanceMetrics();
this.log(`📊 iOS Performance Metrics:`, metrics);
const metrics = {
overallScore: 88,
databasePerformance: 92,
memoryEfficiency: 85,
batteryEfficiency: 90,
objectPoolEfficiency: 88,
totalDatabaseQueries: 120,
averageMemoryUsage: 22.3,
objectPoolHits: 38,
backgroundCpuUsage: 1.8,
totalNetworkRequests: 8,
recommendations: ['Enable background tasks', 'Optimize memory usage']
};
this.log('📊 iOS Performance Metrics:', metrics);
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
} catch (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) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
@ -149,5 +344,5 @@ class TestApp {
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new TestApp();
new TimeSafariIOSTestApp();
});

522
test-apps/shared/config-loader.ts

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

424
test-apps/test-api/server.js

@ -1,13 +1,13 @@
#!/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
* network fetching, ETag support, and error handling capabilities.
* Simulates Endorser.ch API endpoints for testing the plugin's
* network fetching, pagination, and TimeSafari-specific functionality.
*
* @author Matthew Raymer
* @version 1.0.0
* @version 2.0.0
*/
const express = require('express');
@ -24,67 +24,110 @@ app.use(express.json());
// In-memory storage for testing
let contentStore = new Map();
let etagStore = new Map();
let offersStore = new Map();
let projectsStore = new Map();
/**
* Generate mock notification content for a given slot
* @param {string} slotId - The notification slot identifier
* @param {number} timestamp - Current timestamp
* @returns {Object} Mock notification content
* Generate mock offer data for TimeSafari testing
* @param {string} recipientDid - DID of the recipient
* @param {string} afterId - JWT ID for pagination
* @returns {Object} Mock offer data
*/
function generateMockContent(slotId, timestamp) {
const slotTime = slotId.split('-')[1] || '08:00';
const contentId = crypto.randomUUID().substring(0, 8);
function generateMockOffers(recipientDid, afterId) {
const offers = [];
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 {
id: contentId,
slotId: slotId,
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()
}
data: offers,
hitLimit: offers.length >= 3 // Simulate hit limit
};
}
/**
* Generate ETag for content
* @param {Object} content - Content object
* @returns {string} ETag value
* Generate mock project data for TimeSafari testing
* @param {Array} planIds - Array of plan IDs
* @param {string} afterId - JWT ID for pagination
* @returns {Object} Mock project data
*/
function generateETag(content) {
const contentString = JSON.stringify(content);
return `"${crypto.createHash('md5').update(contentString).digest('hex')}"`;
}
function generateMockProjects(planIds, afterId) {
const projects = [];
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
});
});
/**
* Store content with ETag
* @param {string} slotId - Slot identifier
* @param {Object} content - Content object
* @param {string} etag - ETag value
*/
function storeContent(slotId, content, etag) {
contentStore.set(slotId, content);
etagStore.set(slotId, etag);
return {
data: projects,
hitLimit: projects.length >= 2 // Simulate hit limit
};
}
/**
* Get stored content and ETag
* @param {string} slotId - Slot identifier
* @returns {Object} { content, etag } or null
* Generate mock notification bundle for TimeSafari
* @param {Object} params - Request parameters
* @returns {Object} Mock notification bundle
*/
function getStoredContent(slotId) {
const content = contentStore.get(slotId);
const etag = etagStore.get(slotId);
return content && etag ? { content, etag } : null;
function generateNotificationBundle(params) {
const { userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId } = params;
return {
offersToPerson: generateMockOffers(userDid, lastKnownOfferId),
offersToProjects: {
data: [],
hitLimit: false
},
starredChanges: generateMockProjects(starredPlanIds, lastKnownPlanId),
timestamp: new Date().toISOString(),
bundleId: crypto.randomUUID()
};
}
// Routes
@ -96,19 +139,125 @@ app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: Date.now(),
version: '1.0.0',
version: '2.0.0',
service: 'TimeSafari Test API',
endpoints: {
content: '/api/content/:slotId',
health: '/health',
metrics: '/api/metrics',
error: '/api/error/:type'
offers: '/api/v2/report/offers',
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
* Supports ETag conditional requests
* Endorser.ch API: Get offers to person
*/
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) => {
const { slotId } = req.params;
@ -127,91 +276,61 @@ app.get('/api/content/:slotId', (req, res) => {
}
// 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
console.log(` → 304 Not Modified (ETag match)`);
return res.status(304).end();
}
// Generate new content
const content = generateMockContent(slotId, timestamp);
const etag = generateETag(content);
const 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
storeContent(slotId, content, etag);
contentStore.set(slotId, content);
etagStore.set(slotId, newEtag);
// Set ETag header
res.set('ETag', etag);
res.set('ETag', newEtag);
res.set('Cache-Control', 'no-cache');
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);
});
/**
* 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
*/
app.get('/api/metrics', (req, res) => {
const metrics = {
timestamp: Date.now(),
service: 'TimeSafari Test API',
version: '2.0.0',
contentStore: {
size: contentStore.size,
slots: Array.from(contentStore.keys())
@ -221,7 +340,21 @@ app.get('/api/metrics', (req, res) => {
etags: Array.from(etagStore.entries())
},
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);
@ -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
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] Error:`, err);
@ -289,14 +395,16 @@ app.use((req, res) => {
// Start server
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(` GET /health - Health check`);
console.log(` GET /api/content/:slotId - Get notification content`);
console.log(` PUT /api/content/:slotId - Update content`);
console.log(` DELETE /api/content - Clear all content`);
console.log(` GET /api/error/:type - Simulate errors`);
console.log(` GET /api/metrics - API metrics`);
console.log(` GET /health - Health check`);
console.log(` GET /api/v2/report/offers - Get offers to person`);
console.log(` GET /api/v2/report/offersToPlansOwnedByMe - Get offers to user's projects`);
console.log(` POST /api/v2/report/plansLastUpdatedBetween - Get changes to starred projects`);
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(``);
console.log(`🔧 Environment:`);
console.log(` NODE_ENV: ${process.env.NODE_ENV || 'development'}`);
@ -304,18 +412,18 @@ app.listen(PORT, () => {
console.log(``);
console.log(`📝 Usage examples:`);
console.log(` curl http://localhost:${PORT}/health`);
console.log(` curl http://localhost:${PORT}/api/content/slot-08:00`);
console.log(` curl -H "If-None-Match: \\"abc123\\"" http://localhost:${PORT}/api/content/slot-08:00`);
console.log(` curl http://localhost:${PORT}/api/error/timeout`);
console.log(` curl "http://localhost:${PORT}/api/v2/report/offers?recipientId=did:example:testuser123&afterId=01HSE3R9MAC0FT3P3KZ382TWV7"`);
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/v2/report/notifications/bundle?userDid=did:example:testuser123&starredPlanIds=[\"plan-123\",\"plan-456\"]"`);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down Test API Server...');
console.log('\n🛑 Shutting down TimeSafari Test API Server...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 Shutting down Test API Server...');
console.log('\n🛑 Shutting down TimeSafari Test API Server...');
process.exit(0);
});

Loading…
Cancel
Save