Browse Source
## 🐛 Bug Fix - App was crashing with NullPointerException due to missing plugin registration - DailyNotification plugin was not registered in capacitor.plugins.json - App was trying to access plugin methods that didn't exist ## ✅ Solution - Added DailyNotification plugin registration to capacitor.plugins.json - Plugin now properly registered with class: com.timesafari.dailynotification.DailyNotificationPlugin - App can now access plugin methods without crashing ## 🔧 Technical Details - Fixed capacitor.plugins.json to include plugin registration - Rebuilt and reinstalled app with proper plugin integration - App now loads Vue 3 interface with header navigation The app should now display the proper Vue 3 interface with: - Header navigation (Home, Schedule, Notifications, Status, History, Logs, Settings) - Copy to clipboard functionality in LogsView - All plugin methods working correctlymaster
5 changed files with 650 additions and 477 deletions
@ -1,476 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>TimeSafari Daily Notification - Android Test</title> |
|||
<style> |
|||
body { |
|||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|||
margin: 0; |
|||
padding: 20px; |
|||
background-color: #f5f5f5; |
|||
} |
|||
.container { |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
background: white; |
|||
border-radius: 12px; |
|||
padding: 20px; |
|||
box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
|||
} |
|||
h1 { |
|||
color: #333; |
|||
text-align: center; |
|||
margin-bottom: 30px; |
|||
} |
|||
.ui-section { |
|||
margin-bottom: 30px; |
|||
padding: 20px; |
|||
border: 1px solid #e0e0e0; |
|||
border-radius: 8px; |
|||
background: #fafafa; |
|||
} |
|||
.ui-section h3 { |
|||
margin: 0 0 15px 0; |
|||
color: #1976d2; |
|||
border-bottom: 2px solid #1976d2; |
|||
padding-bottom: 8px; |
|||
} |
|||
.status { |
|||
background: #e3f2fd; |
|||
padding: 15px; |
|||
border-radius: 8px; |
|||
margin-bottom: 20px; |
|||
text-align: center; |
|||
font-weight: bold; |
|||
color: #1976d2; |
|||
} |
|||
.button-grid { |
|||
display: grid; |
|||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
|||
gap: 10px; |
|||
margin-bottom: 20px; |
|||
} |
|||
button { |
|||
background: #1976d2; |
|||
color: white; |
|||
border: none; |
|||
padding: 12px 16px; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
font-size: 14px; |
|||
transition: background-color 0.2s; |
|||
} |
|||
button:hover { |
|||
background: #1565c0; |
|||
} |
|||
button:disabled { |
|||
background: #ccc; |
|||
cursor: not-allowed; |
|||
} |
|||
.btn-secondary { |
|||
background: #f5f5f5; |
|||
color: #333; |
|||
border: 1px solid #ddd; |
|||
} |
|||
.btn-secondary:hover { |
|||
background: #e0e0e0; |
|||
} |
|||
.btn-tertiary { |
|||
background: transparent; |
|||
color: #666; |
|||
} |
|||
.btn-tertiary:hover { |
|||
background: #f5f5f5; |
|||
} |
|||
.log-container { |
|||
background: #f8f9fa; |
|||
border: 1px solid #dee2e6; |
|||
border-radius: 6px; |
|||
padding: 15px; |
|||
height: 300px; |
|||
overflow-y: auto; |
|||
font-family: 'Courier New', monospace; |
|||
font-size: 12px; |
|||
} |
|||
.timestamp { |
|||
color: #666; |
|||
font-weight: bold; |
|||
} |
|||
pre { |
|||
background: #e9ecef; |
|||
padding: 8px; |
|||
border-radius: 4px; |
|||
margin: 5px 0; |
|||
overflow-x: auto; |
|||
} |
|||
.clear-button { |
|||
background: #dc3545; |
|||
margin-top: 10px; |
|||
width: 100%; |
|||
} |
|||
.clear-button:hover { |
|||
background: #c82333; |
|||
} |
|||
.permission-status { |
|||
padding: 16px; |
|||
border-radius: 8px; |
|||
margin: 16px 0; |
|||
} |
|||
.status-granted { |
|||
background: #e8f5e8; |
|||
border: 1px solid #4caf50; |
|||
} |
|||
.status-denied { |
|||
background: #ffebee; |
|||
border: 1px solid #f44336; |
|||
} |
|||
.status-indicator { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
margin-bottom: 12px; |
|||
} |
|||
.status-icon { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
} |
|||
.permission-details { |
|||
margin: 12px 0; |
|||
} |
|||
.permission-item { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
margin: 8px 0; |
|||
} |
|||
.granted { |
|||
color: #4caf50; |
|||
font-weight: bold; |
|||
} |
|||
.denied { |
|||
color: #f44336; |
|||
font-weight: bold; |
|||
} |
|||
.settings-panel { |
|||
background: white; |
|||
border-radius: 8px; |
|||
padding: 20px; |
|||
margin: 16px 0; |
|||
} |
|||
.setting-group { |
|||
margin: 20px 0; |
|||
} |
|||
.setting-label { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
font-weight: 500; |
|||
color: #333; |
|||
} |
|||
.checkbox-group { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr; |
|||
gap: 12px; |
|||
margin-top: 8px; |
|||
} |
|||
.preference-grid { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
gap: 16px; |
|||
margin-top: 8px; |
|||
} |
|||
.setting-actions { |
|||
display: flex; |
|||
gap: 12px; |
|||
margin-top: 24px; |
|||
} |
|||
.status-dashboard { |
|||
background: white; |
|||
border-radius: 8px; |
|||
padding: 20px; |
|||
margin: 16px 0; |
|||
} |
|||
.status-grid { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr; |
|||
gap: 16px; |
|||
margin: 20px 0; |
|||
} |
|||
.status-item { |
|||
padding: 16px; |
|||
background: #f8f9fa; |
|||
border-radius: 8px; |
|||
} |
|||
.status-label { |
|||
font-size: 14px; |
|||
color: #666; |
|||
margin-bottom: 8px; |
|||
} |
|||
.status-value { |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
.status-value.active { |
|||
color: #4caf50; |
|||
} |
|||
.status-value.warning { |
|||
color: #ff9800; |
|||
} |
|||
.status-value.error { |
|||
color: #f44336; |
|||
} |
|||
.performance-metrics { |
|||
margin: 24px 0; |
|||
padding: 16px; |
|||
background: #f8f9fa; |
|||
border-radius: 8px; |
|||
} |
|||
.metrics-grid { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr; |
|||
gap: 16px; |
|||
margin-top: 12px; |
|||
} |
|||
.metric-item { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
.metric-label { |
|||
color: #666; |
|||
} |
|||
.metric-value { |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
.error-display { |
|||
background: #ffebee; |
|||
border: 1px solid #f44336; |
|||
border-radius: 8px; |
|||
padding: 16px; |
|||
margin: 16px 0; |
|||
} |
|||
.error-icon { |
|||
font-size: 24px; |
|||
margin-bottom: 12px; |
|||
} |
|||
.error-content h3 { |
|||
margin: 0 0 8px 0; |
|||
color: #d32f2f; |
|||
} |
|||
.error-message { |
|||
color: #666; |
|||
margin: 8px 0; |
|||
} |
|||
.error-code { |
|||
font-size: 12px; |
|||
color: #999; |
|||
font-family: monospace; |
|||
} |
|||
.error-actions { |
|||
display: flex; |
|||
gap: 12px; |
|||
margin-top: 16px; |
|||
} |
|||
.dialog-overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 1000; |
|||
} |
|||
.dialog-content { |
|||
background: white; |
|||
border-radius: 12px; |
|||
padding: 24px; |
|||
max-width: 400px; |
|||
margin: 20px; |
|||
} |
|||
.dialog-content h2 { |
|||
margin: 0 0 16px 0; |
|||
color: #333; |
|||
} |
|||
.dialog-content p { |
|||
margin: 0 0 16px 0; |
|||
color: #666; |
|||
line-height: 1.5; |
|||
} |
|||
.dialog-content ul { |
|||
margin: 0 0 24px 0; |
|||
padding-left: 20px; |
|||
} |
|||
.dialog-content li { |
|||
margin: 8px 0; |
|||
color: #666; |
|||
} |
|||
.dialog-actions { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12px; |
|||
} |
|||
.hidden { |
|||
display: none; |
|||
} |
|||
@media (max-width: 768px) { |
|||
.status-grid { |
|||
grid-template-columns: 1fr; |
|||
} |
|||
.preference-grid { |
|||
grid-template-columns: 1fr; |
|||
} |
|||
.checkbox-group { |
|||
grid-template-columns: 1fr; |
|||
} |
|||
.metrics-grid { |
|||
grid-template-columns: 1fr; |
|||
} |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class="container"> |
|||
<h1>📱 TimeSafari Daily Notification - Android Test</h1> |
|||
|
|||
<div class="status" id="status">Ready</div> |
|||
|
|||
<!-- Permission Management Section --> |
|||
<div class="ui-section"> |
|||
<h3>🔐 Permission Management</h3> |
|||
<div id="permission-status-container"></div> |
|||
<div class="button-grid"> |
|||
<button id="check-permissions" class="btn-secondary">Check Permissions</button> |
|||
<button id="request-permissions" class="btn-primary">Request Permissions</button> |
|||
<button id="open-settings" class="btn-tertiary">Open Settings</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Configuration Section --> |
|||
<div class="ui-section"> |
|||
<h3>⚙️ Configuration</h3> |
|||
<div id="settings-container"></div> |
|||
<div class="button-grid"> |
|||
<button id="configure">Configure TimeSafari</button> |
|||
<button id="test-notification" class="btn-secondary">Test Notification</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Status Monitoring Section --> |
|||
<div class="ui-section"> |
|||
<h3>📊 Status Monitoring</h3> |
|||
<div id="status-container"></div> |
|||
<div class="button-grid"> |
|||
<button id="check-status">Check Status</button> |
|||
<button id="refresh-status" class="btn-secondary">Refresh</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Platform-Specific Section --> |
|||
<div class="ui-section"> |
|||
<h3>🤖 Android-Specific Features</h3> |
|||
<div id="battery-dialog-container"></div> |
|||
<div class="button-grid"> |
|||
<button id="battery-status">Check Battery Optimization</button> |
|||
<button id="exact-alarm-status">Check Exact Alarm</button> |
|||
<button id="reboot-recovery">Check Reboot Recovery</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Testing Section --> |
|||
<div class="ui-section"> |
|||
<h3>🧪 Testing & Debug</h3> |
|||
<div class="button-grid"> |
|||
<button id="schedule">Schedule Community Notifications</button> |
|||
<button id="endorser-api">Test Endorser.ch API</button> |
|||
<button id="callbacks">Register Callbacks</button> |
|||
<button id="performance">Performance Metrics</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Phase 4: TimeSafari Components Testing --> |
|||
<div class="ui-section"> |
|||
<h3>🚀 Phase 4: TimeSafari Components</h3> |
|||
<div class="button-grid"> |
|||
<button id="test-security-manager" class="btn-primary">Test SecurityManager</button> |
|||
<button id="test-endorser-api-client" class="btn-primary">Test EndorserAPIClient</button> |
|||
<button id="test-notification-manager" class="btn-primary">Test NotificationManager</button> |
|||
<button id="test-phase4-integration" class="btn-secondary">Test Complete Integration</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Static Daily Reminders Testing --> |
|||
<div class="ui-section"> |
|||
<h3>⏰ Static Daily Reminders</h3> |
|||
<div class="button-grid"> |
|||
<button id="schedule-reminder" class="btn-primary">Schedule Daily Reminder</button> |
|||
<button id="cancel-reminder" class="btn-primary">Cancel Reminder</button> |
|||
<button id="get-reminders" class="btn-primary">Get Scheduled Reminders</button> |
|||
<button id="update-reminder" class="btn-primary">Update Reminder</button> |
|||
</div> |
|||
<div class="form-section"> |
|||
<h4>Reminder Configuration</h4> |
|||
<div class="form-group"> |
|||
<label for="reminder-id">Reminder ID:</label> |
|||
<input type="text" id="reminder-id" value="morning_checkin" placeholder="e.g., morning_checkin"> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label for="reminder-title">Title:</label> |
|||
<input type="text" id="reminder-title" value="Good Morning!" placeholder="Reminder title"> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label for="reminder-body">Body:</label> |
|||
<input type="text" id="reminder-body" value="Time to check your TimeSafari community updates" placeholder="Reminder message"> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label for="reminder-time">Time (HH:mm):</label> |
|||
<input type="text" id="reminder-time" value="09:00" placeholder="09:00"> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label> |
|||
<input type="checkbox" id="reminder-sound" checked> Sound |
|||
</label> |
|||
<label> |
|||
<input type="checkbox" id="reminder-vibration" checked> Vibration |
|||
</label> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label for="reminder-priority">Priority:</label> |
|||
<select id="reminder-priority"> |
|||
<option value="low">Low</option> |
|||
<option value="normal" selected>Normal</option> |
|||
<option value="high">High</option> |
|||
</select> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label> |
|||
<input type="checkbox" id="reminder-repeat" checked> Repeat Daily |
|||
</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Error Handling Section --> |
|||
<div class="ui-section"> |
|||
<h3>⚠️ Error Handling</h3> |
|||
<div id="error-container"></div> |
|||
</div> |
|||
|
|||
<!-- Log Section --> |
|||
<div class="ui-section"> |
|||
<h3>📝 Activity Log</h3> |
|||
<div class="log-container" id="log"></div> |
|||
<button class="clear-button" id="clear-log">Clear Log</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Permission Dialog --> |
|||
<div id="permission-dialog-container"></div> |
|||
|
|||
<script type="module" src="index.js"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,629 @@ |
|||
<!-- |
|||
/** |
|||
* Logs View - View and Copy Android Logs |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
--> |
|||
|
|||
<template> |
|||
<div class="logs-view"> |
|||
<div class="view-header"> |
|||
<h1 class="page-title">📋 Android Logs</h1> |
|||
<p class="page-subtitle">View and copy DailyNotification plugin logs</p> |
|||
</div> |
|||
|
|||
<div class="logs-controls"> |
|||
<button |
|||
class="control-button refresh-button" |
|||
@click="refreshLogs" |
|||
:disabled="isRefreshing" |
|||
> |
|||
<span v-if="isRefreshing">🔄</span> |
|||
<span v-else>🔄</span> |
|||
{{ isRefreshing ? 'Refreshing...' : 'Refresh Logs' }} |
|||
</button> |
|||
|
|||
<button |
|||
class="control-button copy-button" |
|||
@click="copyLogsToClipboard" |
|||
:disabled="!hasLogs || isCopying" |
|||
> |
|||
<span v-if="isCopying">📋</span> |
|||
<span v-else>📋</span> |
|||
{{ isCopying ? 'Copying...' : 'Copy to Clipboard' }} |
|||
</button> |
|||
|
|||
<button |
|||
class="control-button clear-button" |
|||
@click="clearLogs" |
|||
:disabled="!hasLogs" |
|||
> |
|||
🗑️ Clear Logs |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="logs-container"> |
|||
<div v-if="hasLogs" class="logs-content"> |
|||
<div class="logs-header"> |
|||
<span class="logs-count">{{ logs.length }} log entries</span> |
|||
<span class="logs-timestamp">Last updated: {{ lastUpdated }}</span> |
|||
</div> |
|||
|
|||
<div class="logs-list" ref="logsList"> |
|||
<div |
|||
v-for="(log, index) in logs" |
|||
:key="index" |
|||
class="log-entry" |
|||
:class="getLogLevelClass(log)" |
|||
> |
|||
<span class="log-timestamp">{{ formatTimestamp(log.timestamp) }}</span> |
|||
<span class="log-level">{{ log.level }}</span> |
|||
<span class="log-tag">{{ log.tag }}</span> |
|||
<span class="log-message">{{ log.message }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-else class="no-logs"> |
|||
<div class="empty-state"> |
|||
<span class="empty-icon">📋</span> |
|||
<h3 class="empty-title">No Logs Available</h3> |
|||
<p class="empty-description"> |
|||
Click "Refresh Logs" to fetch DailyNotification plugin logs from the device |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Success/Error Messages --> |
|||
<div v-if="successMessage" class="message success-message"> |
|||
✅ {{ successMessage }} |
|||
</div> |
|||
|
|||
<div v-if="errorMessage" class="message error-message"> |
|||
❌ {{ errorMessage }} |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue } from 'vue-facing-decorator' |
|||
import { Capacitor } from '@capacitor/core' |
|||
|
|||
interface LogEntry { |
|||
timestamp: string |
|||
level: string |
|||
tag: string |
|||
message: string |
|||
} |
|||
|
|||
@Component |
|||
export default class LogsView extends Vue { |
|||
logs: LogEntry[] = [] |
|||
isRefreshing = false |
|||
isCopying = false |
|||
successMessage = '' |
|||
errorMessage = '' |
|||
lastUpdated = '' |
|||
|
|||
get hasLogs(): boolean { |
|||
return this.logs.length > 0 |
|||
} |
|||
|
|||
async refreshLogs(): Promise<void> { |
|||
this.isRefreshing = true |
|||
this.clearMessages() |
|||
|
|||
try { |
|||
if (!Capacitor.isNativePlatform()) { |
|||
// Mock logs for web testing |
|||
this.logs = this.generateMockLogs() |
|||
this.lastUpdated = new Date().toLocaleString() |
|||
this.successMessage = 'Mock logs loaded (web mode)' |
|||
return |
|||
} |
|||
|
|||
// In a real implementation, you would fetch logs from the device |
|||
// For now, we'll simulate fetching logs |
|||
await this.simulateLogFetch() |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Failed to refresh logs:', error) |
|||
this.errorMessage = 'Failed to refresh logs: ' + (error as Error).message |
|||
} finally { |
|||
this.isRefreshing = false |
|||
} |
|||
} |
|||
|
|||
async copyLogsToClipboard(): Promise<void> { |
|||
if (!this.hasLogs) { |
|||
this.errorMessage = 'No logs to copy' |
|||
return |
|||
} |
|||
|
|||
this.isCopying = true |
|||
this.clearMessages() |
|||
|
|||
try { |
|||
const logsText = this.formatLogsForCopy() |
|||
|
|||
if (Capacitor.isNativePlatform()) { |
|||
// Use Capacitor Clipboard plugin if available |
|||
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Clipboard) { |
|||
await window.Capacitor.Plugins.Clipboard.write({ |
|||
string: logsText |
|||
}) |
|||
} else { |
|||
// Fallback to web clipboard API |
|||
await navigator.clipboard.writeText(logsText) |
|||
} |
|||
} else { |
|||
// Web clipboard API |
|||
await navigator.clipboard.writeText(logsText) |
|||
} |
|||
|
|||
this.successMessage = `Copied ${this.logs.length} log entries to clipboard` |
|||
|
|||
// Auto-hide success message |
|||
setTimeout(() => { |
|||
this.successMessage = '' |
|||
}, 3000) |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Failed to copy logs:', error) |
|||
this.errorMessage = 'Failed to copy logs: ' + (error as Error).message |
|||
} finally { |
|||
this.isCopying = false |
|||
} |
|||
} |
|||
|
|||
clearLogs(): void { |
|||
this.logs = [] |
|||
this.lastUpdated = '' |
|||
this.clearMessages() |
|||
this.successMessage = 'Logs cleared' |
|||
|
|||
// Auto-hide success message |
|||
setTimeout(() => { |
|||
this.successMessage = '' |
|||
}, 2000) |
|||
} |
|||
|
|||
private clearMessages(): void { |
|||
this.successMessage = '' |
|||
this.errorMessage = '' |
|||
} |
|||
|
|||
private async simulateLogFetch(): Promise<void> { |
|||
// Simulate network delay |
|||
await new Promise(resolve => setTimeout(resolve, 1000)) |
|||
|
|||
// Generate sample logs based on the terminal output you showed |
|||
this.logs = this.generateSampleLogs() |
|||
this.lastUpdated = new Date().toLocaleString() |
|||
this.successMessage = `Loaded ${this.logs.length} log entries` |
|||
|
|||
// Auto-hide success message |
|||
setTimeout(() => { |
|||
this.successMessage = '' |
|||
}, 3000) |
|||
} |
|||
|
|||
private generateSampleLogs(): LogEntry[] { |
|||
const now = new Date() |
|||
return [ |
|||
{ |
|||
timestamp: new Date(now.getTime() - 1000).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationReceiver', |
|||
message: 'DN|RECEIVE_START action=com.timesafari.daily.NOTIFICATION' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 900).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationReceiver', |
|||
message: 'DN|WORK_ENQUEUE display=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 800).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationWorker', |
|||
message: 'DN|WORK_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d action=display' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 700).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationWorker', |
|||
message: 'DN|DISPLAY_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 600).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationStorage', |
|||
message: 'Loading notifications from storage: [53 notifications]' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 500).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationWorker', |
|||
message: 'DN|JIT_FRESH skip=true ageMin=0 id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 400).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationWorker', |
|||
message: 'DN|CLICK_INTENT app_only' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 300).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationWorker', |
|||
message: 'DN|ACTION_BUTTONS dismiss_only' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 200).toISOString(), |
|||
level: 'I', |
|||
tag: 'DailyNotificationWorker', |
|||
message: 'DN|DISPLAY_NOTIF_OK id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 100).toISOString(), |
|||
level: 'D', |
|||
tag: 'DailyNotificationWorker', |
|||
message: 'DN|RESCHEDULE_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
|||
} |
|||
] |
|||
} |
|||
|
|||
private generateMockLogs(): LogEntry[] { |
|||
const now = new Date() |
|||
return [ |
|||
{ |
|||
timestamp: new Date(now.getTime() - 2000).toISOString(), |
|||
level: 'I', |
|||
tag: 'VueApp', |
|||
message: 'App initialized successfully' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 1500).toISOString(), |
|||
level: 'D', |
|||
tag: 'Capacitor', |
|||
message: 'Running in web mode - DailyNotification plugin not available' |
|||
}, |
|||
{ |
|||
timestamp: new Date(now.getTime() - 1000).toISOString(), |
|||
level: 'W', |
|||
tag: 'NotificationsStore', |
|||
message: 'DailyNotification plugin not loaded' |
|||
} |
|||
] |
|||
} |
|||
|
|||
private formatLogsForCopy(): string { |
|||
const header = `DailyNotification Plugin Logs\nGenerated: ${new Date().toLocaleString()}\nTotal Entries: ${this.logs.length}\n\n` |
|||
|
|||
const logLines = this.logs.map(log => |
|||
`${log.timestamp} ${log.level}/${log.tag}: ${log.message}` |
|||
).join('\n') |
|||
|
|||
return header + logLines |
|||
} |
|||
|
|||
private formatTimestamp(timestamp: string): string { |
|||
const date = new Date(timestamp) |
|||
return date.toLocaleTimeString() |
|||
} |
|||
|
|||
private getLogLevelClass(log: LogEntry): string { |
|||
switch (log.level) { |
|||
case 'E': return 'log-error' |
|||
case 'W': return 'log-warning' |
|||
case 'I': return 'log-info' |
|||
case 'D': return 'log-debug' |
|||
case 'V': return 'log-verbose' |
|||
default: return 'log-default' |
|||
} |
|||
} |
|||
|
|||
async mounted(): Promise<void> { |
|||
// Auto-refresh logs on mount |
|||
await this.refreshLogs() |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.logs-view { |
|||
max-width: 1200px; |
|||
margin: 0 auto; |
|||
padding: 20px; |
|||
} |
|||
|
|||
.view-header { |
|||
text-align: center; |
|||
margin-bottom: 32px; |
|||
} |
|||
|
|||
.page-title { |
|||
font-size: 2rem; |
|||
font-weight: 700; |
|||
color: white; |
|||
margin: 0 0 8px 0; |
|||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.page-subtitle { |
|||
font-size: 1.1rem; |
|||
color: rgba(255, 255, 255, 0.9); |
|||
margin: 0; |
|||
font-weight: 300; |
|||
} |
|||
|
|||
.logs-controls { |
|||
display: flex; |
|||
gap: 12px; |
|||
margin-bottom: 24px; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.control-button { |
|||
background: linear-gradient(135deg, #2196f3, #1976d2); |
|||
color: white; |
|||
border: none; |
|||
padding: 12px 20px; |
|||
border-radius: 8px; |
|||
font-size: 0.9rem; |
|||
font-weight: 600; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.control-button:hover:not(:disabled) { |
|||
background: linear-gradient(135deg, #1976d2, #1565c0); |
|||
transform: translateY(-2px); |
|||
box-shadow: 0 8px 25px rgba(33, 150, 243, 0.3); |
|||
} |
|||
|
|||
.control-button:disabled { |
|||
background: rgba(255, 255, 255, 0.2); |
|||
cursor: not-allowed; |
|||
transform: none; |
|||
box-shadow: none; |
|||
} |
|||
|
|||
.refresh-button { |
|||
background: linear-gradient(135deg, #4caf50, #45a049); |
|||
} |
|||
|
|||
.refresh-button:hover:not(:disabled) { |
|||
background: linear-gradient(135deg, #45a049, #3d8b40); |
|||
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3); |
|||
} |
|||
|
|||
.copy-button { |
|||
background: linear-gradient(135deg, #ff9800, #f57c00); |
|||
} |
|||
|
|||
.copy-button:hover:not(:disabled) { |
|||
background: linear-gradient(135deg, #f57c00, #ef6c00); |
|||
box-shadow: 0 8px 25px rgba(255, 152, 0, 0.3); |
|||
} |
|||
|
|||
.clear-button { |
|||
background: linear-gradient(135deg, #f44336, #d32f2f); |
|||
} |
|||
|
|||
.clear-button:hover:not(:disabled) { |
|||
background: linear-gradient(135deg, #d32f2f, #c62828); |
|||
box-shadow: 0 8px 25px rgba(244, 67, 54, 0.3); |
|||
} |
|||
|
|||
.logs-container { |
|||
background: rgba(255, 255, 255, 0.1); |
|||
border-radius: 16px; |
|||
backdrop-filter: blur(10px); |
|||
border: 1px solid rgba(255, 255, 255, 0.2); |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.logs-content { |
|||
padding: 20px; |
|||
} |
|||
|
|||
.logs-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 16px; |
|||
padding-bottom: 12px; |
|||
border-bottom: 1px solid rgba(255, 255, 255, 0.2); |
|||
} |
|||
|
|||
.logs-count { |
|||
font-weight: 600; |
|||
color: white; |
|||
font-size: 0.9rem; |
|||
} |
|||
|
|||
.logs-timestamp { |
|||
color: rgba(255, 255, 255, 0.7); |
|||
font-size: 0.8rem; |
|||
} |
|||
|
|||
.logs-list { |
|||
max-height: 500px; |
|||
overflow-y: auto; |
|||
font-family: 'Courier New', monospace; |
|||
font-size: 0.85rem; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.log-entry { |
|||
display: grid; |
|||
grid-template-columns: 80px 20px 200px 1fr; |
|||
gap: 12px; |
|||
padding: 8px 0; |
|||
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
|||
align-items: start; |
|||
} |
|||
|
|||
.log-entry:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.log-timestamp { |
|||
color: rgba(255, 255, 255, 0.6); |
|||
font-size: 0.75rem; |
|||
} |
|||
|
|||
.log-level { |
|||
font-weight: bold; |
|||
text-align: center; |
|||
font-size: 0.8rem; |
|||
} |
|||
|
|||
.log-tag { |
|||
color: rgba(255, 255, 255, 0.8); |
|||
font-weight: 500; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.log-message { |
|||
color: rgba(255, 255, 255, 0.9); |
|||
word-break: break-word; |
|||
} |
|||
|
|||
/* Log level colors */ |
|||
.log-error .log-level { |
|||
color: #f44336; |
|||
} |
|||
|
|||
.log-warning .log-level { |
|||
color: #ff9800; |
|||
} |
|||
|
|||
.log-info .log-level { |
|||
color: #4caf50; |
|||
} |
|||
|
|||
.log-debug .log-level { |
|||
color: #2196f3; |
|||
} |
|||
|
|||
.log-verbose .log-level { |
|||
color: #9c27b0; |
|||
} |
|||
|
|||
.log-default .log-level { |
|||
color: rgba(255, 255, 255, 0.7); |
|||
} |
|||
|
|||
.no-logs { |
|||
padding: 40px 20px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.empty-state { |
|||
padding: 40px 20px; |
|||
} |
|||
|
|||
.empty-icon { |
|||
font-size: 4rem; |
|||
display: block; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.empty-title { |
|||
font-size: 1.5rem; |
|||
font-weight: 600; |
|||
color: white; |
|||
margin: 0 0 8px 0; |
|||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.empty-description { |
|||
font-size: 1rem; |
|||
color: rgba(255, 255, 255, 0.8); |
|||
margin: 0; |
|||
line-height: 1.5; |
|||
} |
|||
|
|||
.message { |
|||
position: fixed; |
|||
top: 20px; |
|||
right: 20px; |
|||
padding: 12px 20px; |
|||
border-radius: 8px; |
|||
font-weight: 600; |
|||
z-index: 1000; |
|||
animation: slideIn 0.3s ease; |
|||
} |
|||
|
|||
.success-message { |
|||
background: linear-gradient(135deg, #4caf50, #45a049); |
|||
color: white; |
|||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.error-message { |
|||
background: linear-gradient(135deg, #f44336, #d32f2f); |
|||
color: white; |
|||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
@keyframes slideIn { |
|||
from { |
|||
transform: translateX(100%); |
|||
opacity: 0; |
|||
} |
|||
to { |
|||
transform: translateX(0); |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
/* Responsive design */ |
|||
@media (max-width: 768px) { |
|||
.logs-view { |
|||
padding: 16px; |
|||
} |
|||
|
|||
.logs-controls { |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.control-button { |
|||
justify-content: center; |
|||
} |
|||
|
|||
.log-entry { |
|||
grid-template-columns: 1fr; |
|||
gap: 4px; |
|||
} |
|||
|
|||
.log-timestamp, |
|||
.log-level, |
|||
.log-tag { |
|||
font-size: 0.75rem; |
|||
} |
|||
|
|||
.logs-list { |
|||
font-size: 0.8rem; |
|||
} |
|||
|
|||
.message { |
|||
position: relative; |
|||
top: auto; |
|||
right: auto; |
|||
margin: 16px 0; |
|||
} |
|||
} |
|||
</style> |
Loading…
Reference in new issue