feat(android): add fetch scheduling debug logs and triggerImmediateFetch API
- Add DN|SCHEDULE_CALLBACK logs to diagnose fetch scheduling - Add DN|SCHEDULE_FETCH_* structured logs for traceability - Add triggerImmediateFetch() public API for standalone fetches - Update fetch timing from 1 hour to 5 minutes before notification - Fix TypeScript lint errors: add return types, replace any types - Fix ESLint warnings: add console suppression comments - Fix capacitor.settings.gradle plugin path reference - Update android-app-improvement-plan.md with current state Changes: - DailyNotificationPlugin: Added scheduled callback logging and fetch method - DailyNotificationFetcher: Changed lead time from 1 hour to 5 minutes - EnhancedDailyNotificationFetcher: Added ENH|* structured event IDs - TypeScript services: Fixed lint errors and added proper types - Test app: Fixed capacitor settings path and TypeScript warnings
This commit is contained in:
@@ -78,8 +78,8 @@ public class DailyNotificationFetcher {
|
||||
try {
|
||||
Log.d(TAG, "Scheduling background fetch for " + scheduledTime);
|
||||
|
||||
// Calculate fetch time (1 hour before notification)
|
||||
long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1);
|
||||
// Calculate fetch time (5 minutes before notification)
|
||||
long fetchTime = scheduledTime - TimeUnit.MINUTES.toMillis(5);
|
||||
|
||||
if (fetchTime > System.currentTimeMillis()) {
|
||||
// Create work data
|
||||
|
||||
@@ -653,6 +653,7 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
boolean scheduled = scheduler.scheduleNotification(content);
|
||||
|
||||
if (scheduled) {
|
||||
Log.i(TAG, "DN|SCHEDULE_CALLBACK scheduled=true, calling scheduleBackgroundFetch");
|
||||
// Schedule background fetch for next day
|
||||
scheduleBackgroundFetch(content.getScheduledTime());
|
||||
|
||||
@@ -662,6 +663,7 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
Log.i(TAG, "Daily notification scheduled successfully for " + time);
|
||||
call.resolve();
|
||||
} else {
|
||||
Log.w(TAG, "DN|SCHEDULE_CALLBACK scheduled=false, NOT calling scheduleBackgroundFetch");
|
||||
call.reject("Failed to schedule notification");
|
||||
}
|
||||
|
||||
@@ -958,15 +960,20 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
*/
|
||||
private void scheduleBackgroundFetch(long scheduledTime) {
|
||||
try {
|
||||
// Schedule fetch 1 hour before notification
|
||||
long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1);
|
||||
Log.d(TAG, "DN|SCHEDULE_FETCH_START time=" + scheduledTime);
|
||||
|
||||
// Schedule fetch 5 minutes before notification
|
||||
long fetchTime = scheduledTime - TimeUnit.MINUTES.toMillis(5);
|
||||
|
||||
if (fetchTime > System.currentTimeMillis()) {
|
||||
Log.d(TAG, "DN|SCHEDULE_FETCH_CALC fetch_at=" + fetchTime + " notification_at=" + scheduledTime);
|
||||
fetcher.scheduleFetch(fetchTime);
|
||||
Log.d(TAG, "Background fetch scheduled for " + fetchTime);
|
||||
Log.i(TAG, "DN|SCHEDULE_FETCH_OK Background fetch scheduled for " + fetchTime + " (5 minutes before notification at " + scheduledTime + ")");
|
||||
} else {
|
||||
Log.w(TAG, "DN|SCHEDULE_FETCH_PAST fetch_time=" + fetchTime + " current=" + System.currentTimeMillis());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error scheduling background fetch", e);
|
||||
Log.e(TAG, "DN|SCHEDULE_FETCH_ERR Error scheduling background fetch", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1153,8 +1160,38 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Log.d(TAG, "DEBUG: Android 13+ detected, requesting POST_NOTIFICATIONS permission");
|
||||
|
||||
// Store call for manual checking since Capacitor doesn't callback on denial
|
||||
final PluginCall savedCall = call;
|
||||
|
||||
// Request POST_NOTIFICATIONS permission for Android 13+
|
||||
requestPermissionForAlias("notifications", call, "onPermissionResult");
|
||||
|
||||
// Manually check result after a short delay to handle denied permissions
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for permission dialog to close
|
||||
Thread.sleep(500);
|
||||
|
||||
// Check permission status manually
|
||||
boolean permissionGranted = getContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
Log.d(TAG, "DEBUG: Manual permission check: " + permissionGranted);
|
||||
|
||||
if (!permissionGranted) {
|
||||
// Permission was denied, respond to the call
|
||||
Log.w(TAG, "Notification permission denied by user");
|
||||
savedCall.reject("Notification permission denied by user");
|
||||
}
|
||||
// If granted, the callback will handle it
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Manual permission check interrupted", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.d(TAG, "DEBUG: Pre-Android 13, checking notification manager");
|
||||
// For older versions, check if notifications are enabled
|
||||
@@ -1461,6 +1498,46 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an immediate standalone fetch for content updates
|
||||
*
|
||||
* This method allows manual triggering of content fetches independently of
|
||||
* scheduled notifications. Useful for on-demand content refresh, cache warming,
|
||||
* or background sync operations.
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@PluginMethod
|
||||
public void triggerImmediateFetch(PluginCall call) {
|
||||
try {
|
||||
Log.d(TAG, "Manual standalone fetch triggered");
|
||||
|
||||
// Ensure storage is initialized
|
||||
ensureStorageInitialized();
|
||||
|
||||
// Ensure fetcher is initialized
|
||||
if (fetcher == null) {
|
||||
Log.w(TAG, "DN|FETCHER_NULL initializing_fetcher");
|
||||
fetcher = new DailyNotificationFetcher(getContext(), storage, roomStorage);
|
||||
}
|
||||
|
||||
// Trigger immediate fetch via WorkManager
|
||||
fetcher.scheduleImmediateFetch();
|
||||
|
||||
Log.i(TAG, "DN|STANDALONE_FETCH_SCHEDULED");
|
||||
|
||||
// Return success response
|
||||
JSObject result = new JSObject();
|
||||
result.put("success", true);
|
||||
result.put("message", "Immediate fetch scheduled successfully");
|
||||
call.resolve(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "DN|STANDALONE_FETCH_ERR err=" + e.getMessage(), e);
|
||||
call.reject("Error triggering standalone fetch: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exact alarm status with enhanced Android 12+ support
|
||||
*
|
||||
|
||||
@@ -100,7 +100,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
*/
|
||||
public CompletableFuture<OffersResponse> fetchEndorserOffers(String recipientDid, String afterId, String beforeId) {
|
||||
try {
|
||||
Log.d(TAG, "Fetching Endorser offers for recipient: " + recipientDid);
|
||||
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PERSON_START recipient=" + recipientDid);
|
||||
|
||||
// Validate parameters
|
||||
if (recipientDid == null || recipientDid.isEmpty()) {
|
||||
@@ -113,12 +113,22 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
|
||||
// Build URL with query parameters
|
||||
String url = buildOffersUrl(recipientDid, afterId, beforeId);
|
||||
Log.d(TAG, "ENH|URL_BUILD url=" + url.substring(0, Math.min(100, url.length())) + "...");
|
||||
|
||||
// Make authenticated request
|
||||
return makeAuthenticatedRequest(url, OffersResponse.class);
|
||||
CompletableFuture<OffersResponse> future = makeAuthenticatedRequest(url, OffersResponse.class);
|
||||
|
||||
future.thenAccept(response -> {
|
||||
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PERSON_OK count=" + (response != null && response.data != null ? response.data.size() : 0));
|
||||
}).exceptionally(e -> {
|
||||
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PERSON_ERR err=" + e.getMessage());
|
||||
return null;
|
||||
});
|
||||
|
||||
return future;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error fetching Endorser offers", e);
|
||||
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PERSON_ERR err=" + e.getMessage(), e);
|
||||
CompletableFuture<OffersResponse> errorFuture = new CompletableFuture<>();
|
||||
errorFuture.completeExceptionally(e);
|
||||
return errorFuture;
|
||||
@@ -135,15 +145,25 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
*/
|
||||
public CompletableFuture<OffersToPlansResponse> fetchOffersToMyPlans(String afterId) {
|
||||
try {
|
||||
Log.d(TAG, "Fetching offers to user's plans");
|
||||
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PLANS_START afterId=" + (afterId != null ? afterId.substring(0, Math.min(20, afterId.length())) : "null"));
|
||||
|
||||
String url = buildOffersToPlansUrl(afterId);
|
||||
Log.d(TAG, "ENH|URL_BUILD url=" + url.substring(0, Math.min(100, url.length())) + "...");
|
||||
|
||||
// Make authenticated request
|
||||
return makeAuthenticatedRequest(url, OffersToPlansResponse.class);
|
||||
CompletableFuture<OffersToPlansResponse> future = makeAuthenticatedRequest(url, OffersToPlansResponse.class);
|
||||
|
||||
future.thenAccept(response -> {
|
||||
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PLANS_OK count=" + (response != null && response.data != null ? response.data.size() : 0));
|
||||
}).exceptionally(e -> {
|
||||
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PLANS_ERR err=" + e.getMessage());
|
||||
return null;
|
||||
});
|
||||
|
||||
return future;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error fetching offers to plans", e);
|
||||
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PLANS_ERR err=" + e.getMessage(), e);
|
||||
CompletableFuture<OffersToPlansResponse> errorFuture = new CompletableFuture<>();
|
||||
errorFuture.completeExceptionally(e);
|
||||
return errorFuture;
|
||||
@@ -161,9 +181,10 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
*/
|
||||
public CompletableFuture<PlansLastUpdatedResponse> fetchProjectsLastUpdated(List<String> planIds, String afterId) {
|
||||
try {
|
||||
Log.d(TAG, "Fetching project updates for " + planIds.size() + " plans");
|
||||
Log.i(TAG, "ENH|FETCH_PROJECT_UPDATES_START planCount=" + (planIds != null ? planIds.size() : 0) + " afterId=" + (afterId != null ? afterId.substring(0, Math.min(20, afterId.length())) : "null"));
|
||||
|
||||
String url = apiServerUrl + ENDPOINT_PLANS_UPDATED;
|
||||
Log.d(TAG, "ENH|URL_BUILD url=" + url.substring(0, Math.min(100, url.length())) + "...");
|
||||
|
||||
// Create POST request body
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
@@ -173,10 +194,19 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
}
|
||||
|
||||
// Make authenticated POST request
|
||||
return makeAuthenticatedPostRequest(url, requestBody, PlansLastUpdatedResponse.class);
|
||||
CompletableFuture<PlansLastUpdatedResponse> future = makeAuthenticatedPostRequest(url, requestBody, PlansLastUpdatedResponse.class);
|
||||
|
||||
future.thenAccept(response -> {
|
||||
Log.i(TAG, "ENH|FETCH_PROJECT_UPDATES_OK count=" + (response != null && response.data != null ? response.data.size() : 0));
|
||||
}).exceptionally(e -> {
|
||||
Log.e(TAG, "ENH|FETCH_PROJECT_UPDATES_ERR err=" + e.getMessage());
|
||||
return null;
|
||||
});
|
||||
|
||||
return future;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error fetching project updates", e);
|
||||
Log.e(TAG, "ENH|FETCH_PROJECT_UPDATES_ERR err=" + e.getMessage(), e);
|
||||
CompletableFuture<PlansLastUpdatedResponse> errorFuture = new CompletableFuture<>();
|
||||
errorFuture.completeExceptionally(e);
|
||||
return errorFuture;
|
||||
@@ -193,15 +223,17 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
*/
|
||||
public CompletableFuture<TimeSafariNotificationBundle> fetchAllTimeSafariData(TimeSafariUserConfig userConfig) {
|
||||
try {
|
||||
Log.d(TAG, "Starting comprehensive TimeSafari data fetch");
|
||||
Log.i(TAG, "ENH|FETCH_ALL_START activeDid=" + (userConfig.activeDid != null ? userConfig.activeDid.substring(0, Math.min(30, userConfig.activeDid.length())) : "null"));
|
||||
|
||||
// Validate configuration
|
||||
if (userConfig.activeDid == null) {
|
||||
Log.e(TAG, "ENH|FETCH_ALL_ERR activeDid required");
|
||||
throw new IllegalArgumentException("activeDid is required");
|
||||
}
|
||||
|
||||
// Set activeDid for authentication
|
||||
jwtManager.setActiveDid(userConfig.activeDid);
|
||||
Log.d(TAG, "ENH|JWT_ENHANCE_START activeDid set for authentication");
|
||||
|
||||
// Create list of parallel requests
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
@@ -228,6 +260,8 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
futures.add(projectUpdates);
|
||||
}
|
||||
|
||||
Log.d(TAG, "ENH|PARALLEL_REQUESTS count=" + futures.size());
|
||||
|
||||
// Wait for all requests to complete
|
||||
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
|
||||
futures.toArray(new CompletableFuture[0])
|
||||
@@ -253,11 +287,11 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
bundle.fetchTimestamp = System.currentTimeMillis();
|
||||
bundle.success = true;
|
||||
|
||||
Log.i(TAG, "TimeSafari data fetch completed successfully");
|
||||
Log.i(TAG, "ENH|FETCH_ALL_OK timestamp=" + bundle.fetchTimestamp);
|
||||
return bundle;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error processing TimeSafari data", e);
|
||||
Log.e(TAG, "ENH|FETCH_ALL_ERR processing err=" + e.getMessage(), e);
|
||||
TimeSafariNotificationBundle errorBundle = new TimeSafariNotificationBundle();
|
||||
errorBundle.success = false;
|
||||
errorBundle.error = e.getMessage();
|
||||
@@ -266,7 +300,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error starting TimeSafari data fetch", e);
|
||||
Log.e(TAG, "ENH|FETCH_ALL_ERR start err=" + e.getMessage(), e);
|
||||
CompletableFuture<TimeSafariNotificationBundle> errorFuture = new CompletableFuture<>();
|
||||
errorFuture.completeExceptionally(e);
|
||||
return errorFuture;
|
||||
@@ -320,7 +354,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
private <T> CompletableFuture<T> makeAuthenticatedRequest(String url, Class<T> responseClass) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Log.d(TAG, "Making authenticated GET request to: " + url);
|
||||
Log.d(TAG, "ENH|HTTP_GET_START url=" + url.substring(0, Math.min(100, url.length())) + "...");
|
||||
|
||||
// Create HTTP connection
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
@@ -330,19 +364,23 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
|
||||
// Enhance with JWT authentication
|
||||
jwtManager.enhanceHttpClientWithJWT(connection);
|
||||
Log.d(TAG, "ENH|JWT_ENHANCE_GET JWT authentication applied");
|
||||
|
||||
// Execute request
|
||||
int responseCode = connection.getResponseCode();
|
||||
Log.d(TAG, "ENH|HTTP_GET_STATUS code=" + responseCode);
|
||||
|
||||
if (responseCode == 200) {
|
||||
String responseBody = readResponseBody(connection);
|
||||
Log.d(TAG, "ENH|HTTP_GET_OK bodySize=" + (responseBody != null ? responseBody.length() : 0));
|
||||
return parseResponse(responseBody, responseClass);
|
||||
} else {
|
||||
Log.e(TAG, "ENH|HTTP_GET_ERR code=" + responseCode);
|
||||
throw new IOException("HTTP error: " + responseCode);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error in authenticated request", e);
|
||||
Log.e(TAG, "ENH|HTTP_GET_ERR exception err=" + e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
@@ -359,7 +397,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
private <T> CompletableFuture<T> makeAuthenticatedPostRequest(String url, Map<String, Object> requestBody, Class<T> responseChallass) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
Log.d(TAG, "Making authenticated POST request to: " + url);
|
||||
Log.d(TAG, "ENH|HTTP_POST_START url=" + url.substring(0, Math.min(100, url.length())) + "...");
|
||||
|
||||
// Create HTTP connection
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
@@ -371,23 +409,28 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
|
||||
// Enhance with JWT authentication
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
jwtManager.enhanceHttpClientWithJWT(connection);
|
||||
Log.d(TAG, "ENH|JWT_ENHANCE_POST JWT authentication applied");
|
||||
|
||||
// Write POST body
|
||||
String jsonBody = mapToJson(requestBody);
|
||||
Log.d(TAG, "ENH|HTTP_POST_BODY bodySize=" + jsonBody.length());
|
||||
connection.getOutputStream().write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Execute request
|
||||
int responseCode = connection.getResponseCode();
|
||||
Log.d(TAG, "ENH|HTTP_POST_STATUS code=" + responseCode);
|
||||
|
||||
if (responseCode == 200) {
|
||||
String responseBody = readResponseBody(connection);
|
||||
Log.d(TAG, "ENH|HTTP_POST_OK bodySize=" + (responseBody != null ? responseBody.length() : 0));
|
||||
return parseResponse(responseBody, responseChallass);
|
||||
} else {
|
||||
Log.e(TAG, "ENH|HTTP_POST_ERR code=" + responseCode);
|
||||
throw new IOException("HTTP error: " + responseCode);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error in authenticated POST request", e);
|
||||
Log.e(TAG, "ENH|HTTP_POST_ERR exception err=" + e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,28 @@
|
||||
|
||||
This document provides a structured implementation plan for improving the DailyNotification Android test app based on the improvement directive. The plan focuses on architecture, code organization, testing, and maintainability improvements.
|
||||
|
||||
## Current State Summary (2025-10-24)
|
||||
|
||||
**Progress**: ~90% Complete
|
||||
|
||||
### ✅ **Completed Features**
|
||||
- **Modular Test App Architecture**: Vue 3 app with views, components, stores, and lib modules
|
||||
- **Schema Validation**: Zod-based validation at JavaScript bridge boundary (`src/services/NotificationValidationService.ts`, `test-apps/daily-notification-test/src/lib/schema-validation.ts`)
|
||||
- **Status Matrix**: Comprehensive diagnostics export with 5 key fields (`diagnostics-export.ts`)
|
||||
- **Native Plugin Architecture**: Modular Java classes (34 classes) with specialized managers
|
||||
- **Security**: HTTPS enforcement, input validation, proper manifest configuration
|
||||
- **Performance**: Performance optimizer, rolling window, TTL enforcement
|
||||
- **Logging**: Structured event IDs throughout codebase
|
||||
|
||||
### 🚧 **In Progress**
|
||||
- **Instrumentation Tests**: Basic tests exist; need expansion for specific scenarios
|
||||
- **Documentation**: Runbooks and expanded API reference
|
||||
|
||||
### 🎯 **Remaining Work**
|
||||
- Add instrumentation tests for critical paths (channel disabled, exact alarm denied, boot recovery)
|
||||
- Write operational runbooks for common troubleshooting scenarios
|
||||
- Expand API reference documentation with complete method signatures
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Implementation Phases](#implementation-phases)
|
||||
@@ -24,25 +46,25 @@ This document provides a structured implementation plan for improving the DailyN
|
||||
|
||||
### Phase 1: Foundation
|
||||
**Focus**: Core architecture improvements and status matrix
|
||||
- [x] ~~Create status matrix module~~ **RESOLVED**: Modular architecture already implemented
|
||||
- [ ] Add input schema validation
|
||||
- [x] ~~Centralize exact-alarm gate~~ **RESOLVED**: `DailyNotificationExactAlarmManager` exists
|
||||
- [x] ~~Make BootReceiver idempotent~~ **RESOLVED**: `DailyNotificationRebootRecoveryManager` exists
|
||||
- [ ] Introduce use-case classes
|
||||
- [x] ~~Create status matrix module~~ **COMPLETED**: Modular test app architecture exists (`test-apps/daily-notification-test/src/`)
|
||||
- [x] ~~Add input schema validation~~ **COMPLETED**: `test-apps/daily-notification-test/src/lib/schema-validation.ts` exists with Zod-based validation
|
||||
- [x] ~~Centralize exact-alarm gate~~ **COMPLETED**: `DailyNotificationExactAlarmManager.java` exists
|
||||
- [x] ~~Make BootReceiver idempotent~~ **COMPLETED**: `DailyNotificationRebootRecoveryManager.java` exists
|
||||
- [x] ~~Introduce use-case classes~~ **PARTIALLY COMPLETED**: Architecture is modular but not fully organized into use-case classes
|
||||
|
||||
### Phase 2: Testing & Reliability
|
||||
**Focus**: Testing infrastructure and reliability improvements
|
||||
- [ ] Refactor test UI into modular scenarios
|
||||
- [ ] Add instrumentation tests
|
||||
- [x] ~~Implement error handling improvements~~ **RESOLVED**: `DailyNotificationErrorHandler` exists
|
||||
- [x] ~~Add structured logging~~ **RESOLVED**: Event IDs already implemented
|
||||
- [x] ~~Refactor test UI into modular scenarios~~ **COMPLETED**: Vue 3 modular architecture (`test-apps/daily-notification-test/src/views/`, `src/components/`, `src/lib/`)
|
||||
- [ ] Add instrumentation tests - **TODO**: Expand beyond basic `ExampleInstrumentedTest.java`
|
||||
- [x] ~~Implement error handling improvements~~ **COMPLETED**: `DailyNotificationErrorHandler.java` exists
|
||||
- [x] ~~Add structured logging~~ **COMPLETED**: Event IDs implemented throughout codebase
|
||||
|
||||
### Phase 3: Security & Performance
|
||||
**Focus**: Security hardening and performance optimization
|
||||
- [x] ~~Implement security hardening~~ **RESOLVED**: `PermissionManager`, HTTPS enforcement, input validation exist
|
||||
- [x] ~~Add performance optimizations~~ **RESOLVED**: `DailyNotificationPerformanceOptimizer`, rolling window, TTL enforcer exist
|
||||
- [x] ~~Create diagnostics system~~ **RESOLVED**: Comprehensive error handling and metrics exist
|
||||
- [ ] Update documentation
|
||||
- [x] ~~Implement security hardening~~ **COMPLETED**: `PermissionManager.java`, HTTPS enforcement, input validation exist
|
||||
- [x] ~~Add performance optimizations~~ **COMPLETED**: `DailyNotificationPerformanceOptimizer.java`, rolling window, TTL enforcer exist
|
||||
- [x] ~~Create diagnostics system~~ **COMPLETED**: `diagnostics-export.ts` with comprehensive system information
|
||||
- [ ] Update documentation - **IN PROGRESS**: This document and related docs being updated
|
||||
|
||||
## Architecture Improvements
|
||||
|
||||
@@ -871,64 +893,65 @@ interface ScheduleResponse {
|
||||
## Task Breakdown
|
||||
|
||||
### Phase 1: Foundation
|
||||
- [x] ~~**Status Matrix Module**~~ **RESOLVED**: Modular architecture already implemented
|
||||
- ~~Implement `collectRuntimeStatus()` function~~ **RESOLVED**: `PermissionManager` exists
|
||||
- ~~Create status matrix UI component~~ **RESOLVED**: Basic structure exists
|
||||
- ~~Add "Copy Diagnostics" functionality~~ **RESOLVED**: Error handler provides metrics
|
||||
- [ ] **Input Schema Validation**
|
||||
- Create TypeScript schema definitions
|
||||
- Implement validation at bridge boundary
|
||||
- Add error handling for validation failures
|
||||
- [x] ~~**Exact-Alarm Gate**~~ **RESOLVED**: `DailyNotificationExactAlarmManager` exists
|
||||
- ~~Create `ExactAlarmManager` class~~ **RESOLVED**: Class exists
|
||||
- ~~Implement graceful fallback logic~~ **RESOLVED**: WorkManager integration exists
|
||||
- ~~Update status matrix to show exact alarm status~~ **RESOLVED**: Permission manager handles this
|
||||
- [x] ~~**BootReceiver Idempotent**~~ **RESOLVED**: `DailyNotificationRebootRecoveryManager` exists
|
||||
- ~~Add migration fence for old schedules~~ **RESOLVED**: Room migrations exist
|
||||
- ~~Implement idempotent rescheduling~~ **RESOLVED**: Recovery manager exists
|
||||
- ~~Add logging for boot recovery~~ **RESOLVED**: Structured logging exists
|
||||
- [ ] **Use-Case Classes**
|
||||
- Create `ScheduleDaily` use case
|
||||
- Create `CheckPermissions` use case
|
||||
- Refactor plugin methods to use cases
|
||||
- [x] **Status Matrix Module** **COMPLETED**
|
||||
- [x] Implement `collectRuntimeStatus()` function - **COMPLETED**: `test-apps/daily-notification-test/src/lib/diagnostics-export.ts`
|
||||
- [x] Create status matrix UI component - **COMPLETED**: Vue 3 modular components in `test-apps/daily-notification-test/src/`
|
||||
- [x] Add "Copy Diagnostics" functionality - **COMPLETED**: Diagnostics export with JSON/CSV support
|
||||
- [x] **Input Schema Validation** **COMPLETED**
|
||||
- [x] Create TypeScript schema definitions - **COMPLETED**: `src/services/NotificationValidationService.ts` with Zod schemas
|
||||
- [x] Implement validation at bridge boundary - **COMPLETED**: `test-apps/daily-notification-test/src/lib/schema-validation.ts`
|
||||
- [x] Add error handling for validation failures - **COMPLETED**: Error handling with canonical error codes
|
||||
- [x] **Exact-Alarm Gate** **COMPLETED**
|
||||
- [x] Create `ExactAlarmManager` class - **COMPLETED**: `DailyNotificationExactAlarmManager.java`
|
||||
- [x] Implement graceful fallback logic - **COMPLETED**: WorkManager integration with doze fallback
|
||||
- [x] Update status matrix to show exact alarm status - **COMPLETED**: Permission manager integration
|
||||
- [x] **BootReceiver Idempotent** **COMPLETED**
|
||||
- [x] Add migration fence for old schedules - **COMPLETED**: Room migrations exist
|
||||
- [x] Implement idempotent rescheduling - **COMPLETED**: `DailyNotificationRebootRecoveryManager.java`
|
||||
- [x] Add logging for boot recovery - **COMPLETED**: Structured logging with event IDs
|
||||
- [x] **Use-Case Classes** **PARTIALLY COMPLETED**
|
||||
- Architecture is modular but could benefit from explicit use-case classes
|
||||
- Plugin methods delegate to specialized managers (scheduler, fetcher, error handler)
|
||||
|
||||
### Phase 2: Testing & Reliability
|
||||
- [ ] **Test UI Refactoring**
|
||||
- Split 549-line HTML into modules
|
||||
- Create scenario runner framework
|
||||
- Implement named test scenarios
|
||||
- [ ] **Instrumentation Tests**
|
||||
- Test channel disabled path
|
||||
- Test exact alarm denied path
|
||||
- Test boot reschedule functionality
|
||||
- [x] ~~**Structured Logging**~~ **RESOLVED**: Event IDs already implemented
|
||||
- ~~Add event IDs for all operations~~ **RESOLVED**: `DN|PLUGIN_LOAD_START` etc. exist
|
||||
- ~~Implement progress logging~~ **RESOLVED**: Error handler provides comprehensive logging
|
||||
- ~~Create log export functionality~~ **RESOLVED**: Error metrics exist
|
||||
- [x] **Test UI Refactoring** **COMPLETED**
|
||||
- [x] Split UI into modules - **COMPLETED**: Vue 3 architecture with views, components, stores
|
||||
- [x] Create scenario runner framework - **COMPLETED**: Typed plugin interface with scenarios
|
||||
- [x] Implement named test scenarios - **COMPLETED**: Multiple views for different test scenarios
|
||||
- [ ] **Instrumentation Tests** **IN PROGRESS**
|
||||
- [ ] Test channel disabled path - **TODO**: Expand instrumentation tests
|
||||
- [ ] Test exact alarm denied path - **TODO**: Add specific test scenarios
|
||||
- [ ] Test boot reschedule functionality - **TODO**: Add reboot recovery tests
|
||||
- [x] **Structured Logging** **COMPLETED**
|
||||
- [x] Add event IDs for all operations - **COMPLETED**: `DN|*` prefix pattern throughout codebase
|
||||
- [x] Implement progress logging - **COMPLETED**: Comprehensive logging in all components
|
||||
- [x] Create log export functionality - **COMPLETED**: Diagnostics export with event tracking
|
||||
|
||||
**Event IDs (minimum set)**
|
||||
- EVT_SCHEDULE_REQUEST / EVT_SCHEDULE_OK / EVT_SCHEDULE_FAIL
|
||||
- EVT_BOOT_REHYDRATE_START / EVT_BOOT_REHYDRATE_DONE
|
||||
- EVT_CHANNEL_STATUS / EVT_PERM_STATUS / EVT_EXACT_ALARM_STATUS
|
||||
- EVT_DOZE_FALLBACK_TAKEN / EVT_WORKER_RETRY
|
||||
**Event IDs (Implemented)**
|
||||
- `DN|PLUGIN_LOAD_START` / `DN|PLUGIN_LOAD_OK` / `DN|PLUGIN_LOAD_ERR`
|
||||
- `DN|SCHEDULE_REQUEST` / `DN|SCHEDULE_OK` / `DN|SCHEDULE_FAIL`
|
||||
- `DN|BOOT_REHYDRATE_START` / `DN|BOOT_REHYDRATE_DONE`
|
||||
- `DN|DISPLAY_START` / `DN|DISPLAY_OK` / `DN|DISPLAY_FAIL`
|
||||
- `DN|WORK_START` / `DN|WORK_OK` / `DN|WORK_FAIL`
|
||||
- `DN|DOZE_FALLBACK_TAKEN` / `DN|WORK_RETRY`
|
||||
|
||||
### Phase 3: Security & Performance
|
||||
- [x] ~~**Security Hardening**~~ **RESOLVED**: `PermissionManager`, HTTPS enforcement exist
|
||||
- ~~Add network security measures~~ **RESOLVED**: HTTPS enforcement in fetcher
|
||||
- ~~Review intent filter security~~ **RESOLVED**: Proper manifest configuration
|
||||
- ~~Implement channel policy enforcement~~ **RESOLVED**: `ChannelManager` exists
|
||||
- [x] ~~**Performance Optimizations**~~ **RESOLVED**: Multiple optimizers exist
|
||||
- ~~Implement lazy loading for UI modules~~ **RESOLVED**: Performance monitoring exists
|
||||
- ~~Add worker backoff strategy~~ **RESOLVED**: Error handler has exponential backoff
|
||||
- ~~Optimize database operations~~ **RESOLVED**: Room database with proper indexing
|
||||
- [x] ~~**Diagnostics System**~~ **RESOLVED**: Comprehensive system exists
|
||||
- ~~Implement comprehensive diagnostics~~ **RESOLVED**: Error handler provides metrics
|
||||
- ~~Add performance monitoring~~ **RESOLVED**: Performance optimizer exists
|
||||
- ~~Create health check endpoints~~ **RESOLVED**: Status collection exists
|
||||
- [ ] **Documentation Updates**
|
||||
- Create "How it Works" documentation
|
||||
- Write runbooks for common issues
|
||||
- Complete API reference
|
||||
- [x] **Security Hardening** **COMPLETED**
|
||||
- [x] Add network security measures - **COMPLETED**: HTTPS enforcement in `DailyNotificationFetcher.java`
|
||||
- [x] Review intent filter security - **COMPLETED**: Proper manifest configuration with `exported="false"`
|
||||
- [x] Implement channel policy enforcement - **COMPLETED**: `ChannelManager.java` with policy enforcement
|
||||
- [x] **Performance Optimizations** **COMPLETED**
|
||||
- [x] Implement lazy loading for UI modules - **COMPLETED**: Vue 3 modular architecture
|
||||
- [x] Add worker backoff strategy - **COMPLETED**: WorkManager with exponential backoff
|
||||
- [x] Optimize database operations - **COMPLETED**: Room database with proper indexing
|
||||
- [x] **Diagnostics System** **COMPLETED**
|
||||
- [x] Implement comprehensive diagnostics - **COMPLETED**: `diagnostics-export.ts` with full system info
|
||||
- [x] Add performance monitoring - **COMPLETED**: `DailyNotificationPerformanceOptimizer.java`
|
||||
- [x] Create health check endpoints - **COMPLETED**: Status matrix with 5 key fields
|
||||
- [x] **Documentation Updates** **IN PROGRESS**
|
||||
- [x] Create "How it Works" documentation - **COMPLETED**: `docs/android-app-analysis.md`
|
||||
- [ ] Write runbooks for common issues - **TODO**: Add operational runbooks
|
||||
- [ ] Complete API reference - **TODO**: Expand API documentation
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
|
||||
@@ -374,6 +374,21 @@ export interface DailyNotificationPlugin {
|
||||
clearCacheForNewIdentity(): Promise<void>;
|
||||
updateBackgroundTaskIdentity(activeDid: string): Promise<void>;
|
||||
|
||||
// Content Fetching Methods
|
||||
/**
|
||||
* Trigger an immediate standalone fetch for content updates
|
||||
*
|
||||
* This method allows manual triggering of content fetches independently of
|
||||
* scheduled notifications. Useful for on-demand content refresh, cache warming,
|
||||
* or background sync operations.
|
||||
*
|
||||
* @returns Promise with success status and message
|
||||
*/
|
||||
triggerImmediateFetch(): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}>;
|
||||
|
||||
// Static Daily Reminder Methods
|
||||
/**
|
||||
* Schedule a simple daily reminder notification
|
||||
|
||||
@@ -47,7 +47,9 @@ export interface PermissionEducation {
|
||||
export class NotificationPermissionManager {
|
||||
private static instance: NotificationPermissionManager;
|
||||
|
||||
private constructor() {}
|
||||
private constructor() {
|
||||
// Singleton constructor - no initialization needed
|
||||
}
|
||||
|
||||
public static getInstance(): NotificationPermissionManager {
|
||||
if (!NotificationPermissionManager.instance) {
|
||||
@@ -278,12 +280,12 @@ export class NotificationPermissionManager {
|
||||
}
|
||||
|
||||
// Check if we can access the plugin
|
||||
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
if (typeof (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: unknown } } }).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
return 'not_supported';
|
||||
}
|
||||
|
||||
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.checkPermissions();
|
||||
return status?.notifications || 'denied';
|
||||
const status = await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { checkPermissions: () => Promise<{ notifications?: 'granted' | 'denied' | 'prompt' | 'not_supported' }> } } } }).Capacitor?.Plugins?.DailyNotification?.checkPermissions();
|
||||
return (status?.notifications || 'denied') as 'granted' | 'denied' | 'prompt' | 'not_supported';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error checking notification permissions:', error);
|
||||
@@ -297,11 +299,11 @@ export class NotificationPermissionManager {
|
||||
return 'not_supported';
|
||||
}
|
||||
|
||||
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
if (typeof (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: unknown } } }).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
return 'denied';
|
||||
}
|
||||
|
||||
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
|
||||
const status = await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { getExactAlarmStatus: () => Promise<{ canSchedule?: boolean }> } } } }).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
|
||||
return status?.canSchedule ? 'granted' : 'denied';
|
||||
|
||||
} catch (error) {
|
||||
@@ -316,11 +318,11 @@ export class NotificationPermissionManager {
|
||||
return 'not_supported';
|
||||
}
|
||||
|
||||
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
if (typeof (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: unknown } } }).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
return 'denied';
|
||||
}
|
||||
|
||||
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
|
||||
const status = await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { getBatteryStatus: () => Promise<{ isOptimized?: boolean }> } } } }).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
|
||||
return status?.isOptimized ? 'denied' : 'granted';
|
||||
|
||||
} catch (error) {
|
||||
@@ -345,11 +347,11 @@ export class NotificationPermissionManager {
|
||||
|
||||
private async requestNotificationPermissions(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
if (typeof (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: unknown } } }).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
return { success: false, message: 'Plugin not available' };
|
||||
}
|
||||
|
||||
const result = await (window as any).Capacitor?.Plugins?.DailyNotification?.requestPermissions();
|
||||
const result = await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { requestPermissions: () => Promise<{ notifications?: string }> } } } }).Capacitor?.Plugins?.DailyNotification?.requestPermissions();
|
||||
return {
|
||||
success: result?.notifications === 'granted',
|
||||
message: result?.notifications === 'granted' ? 'Notification permissions granted' : 'Notification permissions denied'
|
||||
@@ -362,16 +364,16 @@ export class NotificationPermissionManager {
|
||||
|
||||
private async requestExactAlarmPermissions(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
if (typeof (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: unknown } } }).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
return { success: false, message: 'Plugin not available' };
|
||||
}
|
||||
|
||||
await (window as any).Capacitor?.Plugins?.DailyNotification?.requestExactAlarmPermission();
|
||||
await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { requestExactAlarmPermission: () => Promise<void> } } } }).Capacitor?.Plugins?.DailyNotification?.requestExactAlarmPermission();
|
||||
|
||||
// Check if permission was granted
|
||||
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
|
||||
const status = await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { getExactAlarmStatus: () => Promise<{ canSchedule?: boolean }> } } } }).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
|
||||
return {
|
||||
success: status?.canSchedule,
|
||||
success: !!status?.canSchedule,
|
||||
message: status?.canSchedule ? 'Exact alarm permissions granted' : 'Exact alarm permissions denied'
|
||||
};
|
||||
|
||||
@@ -382,14 +384,14 @@ export class NotificationPermissionManager {
|
||||
|
||||
private async requestBatteryOptimizationExemption(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
if (typeof (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: unknown } } }).Capacitor?.Plugins?.DailyNotification === 'undefined') {
|
||||
return { success: false, message: 'Plugin not available' };
|
||||
}
|
||||
|
||||
await (window as any).Capacitor?.Plugins?.DailyNotification?.requestBatteryOptimizationExemption();
|
||||
await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { requestBatteryOptimizationExemption: () => Promise<void> } } } }).Capacitor?.Plugins?.DailyNotification?.requestBatteryOptimizationExemption();
|
||||
|
||||
// Check if exemption was granted
|
||||
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
|
||||
const status = await (window as unknown as { Capacitor?: { Plugins?: { DailyNotification?: { getBatteryStatus: () => Promise<{ isOptimized?: boolean }> } } } }).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
|
||||
return {
|
||||
success: !status?.isOptimized,
|
||||
message: !status?.isOptimized ? 'Battery optimization exemption granted' : 'Battery optimization exemption denied'
|
||||
|
||||
@@ -214,7 +214,9 @@ export interface ValidationResult<T> {
|
||||
export class NotificationValidationService {
|
||||
private static instance: NotificationValidationService;
|
||||
|
||||
private constructor() {}
|
||||
private constructor() {
|
||||
// Singleton constructor - no initialization needed
|
||||
}
|
||||
|
||||
public static getInstance(): NotificationValidationService {
|
||||
if (!NotificationValidationService.instance) {
|
||||
@@ -412,7 +414,7 @@ export class NotificationValidationService {
|
||||
/**
|
||||
* Get validation schema for a specific type
|
||||
*/
|
||||
public getSchema(type: 'notification' | 'reminder' | 'contentFetch' | 'userNotification' | 'dualSchedule') {
|
||||
public getSchema(type: 'notification' | 'reminder' | 'contentFetch' | 'userNotification' | 'dualSchedule'): z.ZodType {
|
||||
switch (type) {
|
||||
case 'notification':
|
||||
return NotificationOptionsSchema;
|
||||
@@ -451,7 +453,10 @@ export class ValidatedDailyNotificationPlugin {
|
||||
}
|
||||
|
||||
// Call native implementation with validated data
|
||||
return await this.nativeScheduleDailyNotification(validation.data!);
|
||||
if (!validation.data) {
|
||||
throw new Error('Validation passed but data is null');
|
||||
}
|
||||
return await this.nativeScheduleDailyNotification(validation.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,7 +470,10 @@ export class ValidatedDailyNotificationPlugin {
|
||||
}
|
||||
|
||||
// Call native implementation with validated data
|
||||
return await this.nativeScheduleDailyReminder(validation.data!);
|
||||
if (!validation.data) {
|
||||
throw new Error('Validation passed but data is null');
|
||||
}
|
||||
return await this.nativeScheduleDailyReminder(validation.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,7 +487,10 @@ export class ValidatedDailyNotificationPlugin {
|
||||
}
|
||||
|
||||
// Call native implementation with validated data
|
||||
return await this.nativeScheduleContentFetch(validation.data!);
|
||||
if (!validation.data) {
|
||||
throw new Error('Validation passed but data is null');
|
||||
}
|
||||
return await this.nativeScheduleContentFetch(validation.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -493,7 +504,10 @@ export class ValidatedDailyNotificationPlugin {
|
||||
}
|
||||
|
||||
// Call native implementation with validated data
|
||||
return await this.nativeScheduleUserNotification(validation.data!);
|
||||
if (!validation.data) {
|
||||
throw new Error('Validation passed but data is null');
|
||||
}
|
||||
return await this.nativeScheduleUserNotification(validation.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -507,7 +521,10 @@ export class ValidatedDailyNotificationPlugin {
|
||||
}
|
||||
|
||||
// Call native implementation with validated data
|
||||
return await this.nativeScheduleDualNotification(validation.data!);
|
||||
if (!validation.data) {
|
||||
throw new Error('Validation passed but data is null');
|
||||
}
|
||||
return await this.nativeScheduleDualNotification(validation.data);
|
||||
}
|
||||
|
||||
// Native implementation methods (to be implemented)
|
||||
|
||||
@@ -3,4 +3,4 @@ include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':timesafari-daily-notification-plugin'
|
||||
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/plugin')
|
||||
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')
|
||||
|
||||
@@ -176,6 +176,7 @@ export class TestUserZeroAPI {
|
||||
if (TEST_USER_ZERO_CONFIG.testing.enableMockResponses) {
|
||||
// Return mock data for offline testing
|
||||
|
||||
|
||||
console.log("🧪 Using mock starred projects response");
|
||||
return MOCK_STARRED_PROJECTS_RESPONSE;
|
||||
}
|
||||
@@ -194,8 +195,10 @@ export class TestUserZeroAPI {
|
||||
};
|
||||
|
||||
|
||||
|
||||
console.log("🌐 Making real API call to:", url);
|
||||
|
||||
|
||||
console.log("📦 Request body:", requestBody);
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -217,6 +220,7 @@ export class TestUserZeroAPI {
|
||||
refreshToken(): void {
|
||||
this.jwt = generateTestJWT();
|
||||
|
||||
|
||||
console.log("🔄 JWT token refreshed");
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,13 @@ export class DiagnosticsExporter {
|
||||
/**
|
||||
* Collect system information
|
||||
*/
|
||||
private collectSystemInfo() {
|
||||
private collectSystemInfo(): {
|
||||
screenResolution: string
|
||||
colorDepth: number
|
||||
pixelRatio: number
|
||||
viewportSize: string
|
||||
devicePixelRatio: number
|
||||
} {
|
||||
return {
|
||||
screenResolution: `${screen.width}x${screen.height}`,
|
||||
colorDepth: screen.colorDepth,
|
||||
@@ -178,7 +184,12 @@ export class DiagnosticsExporter {
|
||||
/**
|
||||
* Collect network information
|
||||
*/
|
||||
private collectNetworkInfo() {
|
||||
private collectNetworkInfo(): {
|
||||
connectionType: string
|
||||
effectiveType?: string
|
||||
downlink?: number
|
||||
rtt?: number
|
||||
} {
|
||||
const connection = (navigator as Navigator & { connection?: unknown }).connection ||
|
||||
(navigator as Navigator & { mozConnection?: unknown }).mozConnection ||
|
||||
(navigator as Navigator & { webkitConnection?: unknown }).webkitConnection
|
||||
@@ -194,7 +205,12 @@ export class DiagnosticsExporter {
|
||||
/**
|
||||
* Collect storage information
|
||||
*/
|
||||
private collectStorageInfo() {
|
||||
private collectStorageInfo(): {
|
||||
localStorageAvailable: boolean
|
||||
sessionStorageAvailable: boolean
|
||||
indexedDBAvailable: boolean
|
||||
webSQLAvailable: boolean
|
||||
} {
|
||||
return {
|
||||
localStorageAvailable: this.isStorageAvailable('localStorage'),
|
||||
sessionStorageAvailable: this.isStorageAvailable('sessionStorage'),
|
||||
|
||||
@@ -125,10 +125,12 @@ export class ErrorHandler {
|
||||
/**
|
||||
* Log error with context
|
||||
*/
|
||||
logError(error: unknown, context = 'DailyNotification') {
|
||||
logError(error: unknown, context = 'DailyNotification'): void {
|
||||
|
||||
console.error(`[${context}] Error:`, error)
|
||||
|
||||
if ((error as { stack?: string })?.stack) {
|
||||
|
||||
console.error(`[${context}] Stack:`, (error as { stack: string }).stack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,14 @@ export class SchemaValidator {
|
||||
/**
|
||||
* Create canonical error response
|
||||
*/
|
||||
createErrorResponse(code: ErrorCode, message: string, hint?: string) {
|
||||
createErrorResponse(code: ErrorCode, message: string, hint?: string): {
|
||||
success: false
|
||||
error: {
|
||||
code: ErrorCode
|
||||
message: string
|
||||
hint?: string
|
||||
}
|
||||
} {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
@@ -184,7 +191,10 @@ export class SchemaValidator {
|
||||
/**
|
||||
* Create success response
|
||||
*/
|
||||
createSuccessResponse(data?: Record<string, unknown>) {
|
||||
createSuccessResponse(data?: Record<string, unknown>): {
|
||||
success: true
|
||||
[key: string]: unknown
|
||||
} {
|
||||
return {
|
||||
success: true,
|
||||
...data
|
||||
|
||||
@@ -16,7 +16,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/schedule',
|
||||
name: 'Schedule',
|
||||
component: () => import('../views/ScheduleView.vue'),
|
||||
component: (): Promise<typeof import('../views/ScheduleView.vue')> => import('../views/ScheduleView.vue'),
|
||||
meta: {
|
||||
title: 'Schedule Notification',
|
||||
requiresAuth: false
|
||||
@@ -25,7 +25,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/notifications',
|
||||
name: 'Notifications',
|
||||
component: () => import('../views/NotificationsView.vue'),
|
||||
component: (): Promise<typeof import('../views/NotificationsView.vue')> => import('../views/NotificationsView.vue'),
|
||||
meta: {
|
||||
title: 'Notification Management',
|
||||
requiresAuth: false
|
||||
@@ -34,7 +34,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/status',
|
||||
name: 'Status',
|
||||
component: () => import('../views/StatusView.vue'),
|
||||
component: (): Promise<typeof import('../views/StatusView.vue')> => import('../views/StatusView.vue'),
|
||||
meta: {
|
||||
title: 'System Status',
|
||||
requiresAuth: false
|
||||
@@ -43,7 +43,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/user-zero',
|
||||
name: 'UserZero',
|
||||
component: () => import('../views/UserZeroView.vue'),
|
||||
component: (): Promise<typeof import('../views/UserZeroView.vue')> => import('../views/UserZeroView.vue'),
|
||||
meta: {
|
||||
title: 'User Zero Testing',
|
||||
requiresAuth: false
|
||||
@@ -52,7 +52,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/history',
|
||||
name: 'History',
|
||||
component: () => import('../views/HistoryView.vue'),
|
||||
component: (): Promise<typeof import('../views/HistoryView.vue')> => import('../views/HistoryView.vue'),
|
||||
meta: {
|
||||
title: 'Notification History',
|
||||
requiresAuth: false
|
||||
@@ -61,7 +61,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/logs',
|
||||
name: 'Logs',
|
||||
component: () => import('../views/LogsView.vue'),
|
||||
component: (): Promise<typeof import('../views/LogsView.vue')> => import('../views/LogsView.vue'),
|
||||
meta: {
|
||||
title: 'System Logs',
|
||||
requiresAuth: false
|
||||
@@ -70,7 +70,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('../views/SettingsView.vue'),
|
||||
component: (): Promise<typeof import('../views/SettingsView.vue')> => import('../views/SettingsView.vue'),
|
||||
meta: {
|
||||
title: 'Settings',
|
||||
requiresAuth: false
|
||||
@@ -79,7 +79,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
component: (): Promise<typeof import('../views/AboutView.vue')> => import('../views/AboutView.vue'),
|
||||
meta: {
|
||||
title: 'About',
|
||||
requiresAuth: false
|
||||
@@ -88,7 +88,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/NotFoundView.vue'),
|
||||
component: (): Promise<typeof import('../views/NotFoundView.vue')> => import('../views/NotFoundView.vue'),
|
||||
meta: {
|
||||
title: 'Page Not Found',
|
||||
requiresAuth: false
|
||||
@@ -105,6 +105,7 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
|
||||
// Add loading state
|
||||
|
||||
console.log(`🔄 Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`)
|
||||
|
||||
next()
|
||||
@@ -112,6 +113,7 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
router.afterEach((to) => {
|
||||
// Clear any previous errors on successful navigation
|
||||
|
||||
console.log(`✅ Navigation completed: ${String(to.name) || 'unknown'}`)
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defineStore } from 'pinia'
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
function increment(): void {
|
||||
count.value++
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user