855 lines
22 KiB
HTML
855 lines
22 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="da">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>SYNC QUEUE VISUALIZATION | Calendar System</title>
|
||
|
|
<style>
|
||
|
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||
|
|
|
||
|
|
* {
|
||
|
|
box-sizing: border-box;
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
:root {
|
||
|
|
--bg-primary: #f8f9fa;
|
||
|
|
--bg-secondary: #ffffff;
|
||
|
|
--bg-tertiary: #f1f3f5;
|
||
|
|
--border-color: #dee2e6;
|
||
|
|
--text-primary: #212529;
|
||
|
|
--text-secondary: #495057;
|
||
|
|
--text-muted: #6c757d;
|
||
|
|
--accent-primary: #0066cc;
|
||
|
|
--accent-secondary: #6610f2;
|
||
|
|
--success: #28a745;
|
||
|
|
--warning: #ffc107;
|
||
|
|
--error: #dc3545;
|
||
|
|
--info: #17a2b8;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||
|
|
background: var(--bg-primary);
|
||
|
|
color: var(--text-primary);
|
||
|
|
padding: 20px;
|
||
|
|
line-height: 1.6;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header {
|
||
|
|
background: var(--bg-secondary);
|
||
|
|
padding: 24px;
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
margin-bottom: 20px;
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
height: 3px;
|
||
|
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
color: var(--text-primary);
|
||
|
|
margin-bottom: 6px;
|
||
|
|
font-size: 18px;
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1::before {
|
||
|
|
content: '▶ ';
|
||
|
|
color: var(--accent-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.subtitle {
|
||
|
|
color: var(--text-secondary);
|
||
|
|
font-size: 12px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
font-weight: 400;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-bar {
|
||
|
|
display: flex;
|
||
|
|
gap: 16px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
padding: 12px 0;
|
||
|
|
border-top: 2px solid var(--border-color);
|
||
|
|
border-bottom: 2px solid var(--border-color);
|
||
|
|
margin: 16px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
font-size: 11px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.5px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-label {
|
||
|
|
color: var(--text-muted);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge {
|
||
|
|
padding: 4px 12px;
|
||
|
|
border-radius: 2px;
|
||
|
|
font-size: 10px;
|
||
|
|
font-weight: 600;
|
||
|
|
letter-spacing: 0.8px;
|
||
|
|
border: 1px solid;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-online {
|
||
|
|
background: var(--success);
|
||
|
|
color: white;
|
||
|
|
border-color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-offline {
|
||
|
|
background: var(--error);
|
||
|
|
color: white;
|
||
|
|
border-color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-syncing {
|
||
|
|
background: var(--warning);
|
||
|
|
color: #212529;
|
||
|
|
border-color: var(--warning);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-idle {
|
||
|
|
background: var(--text-muted);
|
||
|
|
color: white;
|
||
|
|
border-color: var(--text-muted);
|
||
|
|
}
|
||
|
|
|
||
|
|
.controls {
|
||
|
|
display: flex;
|
||
|
|
gap: 8px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
button {
|
||
|
|
padding: 8px 14px;
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
background: var(--bg-secondary);
|
||
|
|
color: var(--text-primary);
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 11px;
|
||
|
|
font-weight: 600;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
font-family: 'JetBrains Mono', monospace;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.5px;
|
||
|
|
}
|
||
|
|
|
||
|
|
button:hover {
|
||
|
|
border-color: var(--accent-primary);
|
||
|
|
background: var(--bg-tertiary);
|
||
|
|
color: var(--accent-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
button:active {
|
||
|
|
transform: scale(0.98);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary {
|
||
|
|
border-color: var(--info);
|
||
|
|
color: var(--info);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-success {
|
||
|
|
border-color: var(--success);
|
||
|
|
color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-danger {
|
||
|
|
border-color: var(--error);
|
||
|
|
color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||
|
|
gap: 20px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel {
|
||
|
|
background: var(--bg-secondary);
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
height: 2px;
|
||
|
|
background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-title {
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--accent-primary);
|
||
|
|
padding: 16px 20px;
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
border-bottom: 2px solid var(--border-color);
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
background: var(--bg-tertiary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.count-badge {
|
||
|
|
background: var(--bg-secondary);
|
||
|
|
color: var(--text-primary);
|
||
|
|
padding: 4px 10px;
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
font-size: 10px;
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 0.5px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-list {
|
||
|
|
max-height: 400px;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-list::-webkit-scrollbar {
|
||
|
|
width: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-list::-webkit-scrollbar-track {
|
||
|
|
background: var(--bg-tertiary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-list::-webkit-scrollbar-thumb {
|
||
|
|
background: var(--border-color);
|
||
|
|
border: 2px solid var(--bg-tertiary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-list::-webkit-scrollbar-thumb:hover {
|
||
|
|
background: var(--accent-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-item {
|
||
|
|
padding: 12px;
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
margin-bottom: 8px;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
background: var(--bg-tertiary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-item:hover {
|
||
|
|
border-color: var(--accent-primary);
|
||
|
|
transform: translateX(2px);
|
||
|
|
box-shadow: 2px 0 0 var(--accent-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-title {
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--text-primary);
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sync-status {
|
||
|
|
padding: 3px 10px;
|
||
|
|
font-size: 9px;
|
||
|
|
font-weight: 700;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.8px;
|
||
|
|
border: 2px solid;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sync-synced {
|
||
|
|
background: #d4edda;
|
||
|
|
color: var(--success);
|
||
|
|
border-color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.sync-pending {
|
||
|
|
background: #fff3cd;
|
||
|
|
color: #856404;
|
||
|
|
border-color: var(--warning);
|
||
|
|
}
|
||
|
|
|
||
|
|
.sync-error {
|
||
|
|
background: #f8d7da;
|
||
|
|
color: var(--error);
|
||
|
|
border-color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-details {
|
||
|
|
font-size: 11px;
|
||
|
|
color: var(--text-muted);
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.queue-item {
|
||
|
|
padding: 12px;
|
||
|
|
border-left: 3px solid var(--accent-primary);
|
||
|
|
background: var(--bg-tertiary);
|
||
|
|
margin-bottom: 8px;
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
border-left: 3px solid var(--accent-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.queue-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.operation-type {
|
||
|
|
padding: 3px 10px;
|
||
|
|
font-size: 9px;
|
||
|
|
font-weight: 700;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.8px;
|
||
|
|
border: 2px solid;
|
||
|
|
}
|
||
|
|
|
||
|
|
.op-create {
|
||
|
|
background: #d4edda;
|
||
|
|
color: var(--success);
|
||
|
|
border-color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.op-update {
|
||
|
|
background: #d1ecf1;
|
||
|
|
color: var(--info);
|
||
|
|
border-color: var(--info);
|
||
|
|
}
|
||
|
|
|
||
|
|
.op-delete {
|
||
|
|
background: #f8d7da;
|
||
|
|
color: var(--error);
|
||
|
|
border-color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.retry-count {
|
||
|
|
font-size: 10px;
|
||
|
|
color: var(--text-muted);
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-panel {
|
||
|
|
grid-column: 1 / -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-list {
|
||
|
|
max-height: 300px;
|
||
|
|
overflow-y: auto;
|
||
|
|
background: var(--bg-tertiary);
|
||
|
|
padding: 16px;
|
||
|
|
font-size: 11px;
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry {
|
||
|
|
margin-bottom: 4px;
|
||
|
|
padding: 8px;
|
||
|
|
border-left: 3px solid transparent;
|
||
|
|
padding-left: 12px;
|
||
|
|
background: var(--bg-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry:hover {
|
||
|
|
background: var(--bg-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-timestamp {
|
||
|
|
color: var(--text-muted);
|
||
|
|
margin-right: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-info { border-left-color: var(--info); }
|
||
|
|
.log-success { border-left-color: var(--success); }
|
||
|
|
.log-warning { border-left-color: var(--warning); }
|
||
|
|
.log-error { border-left-color: var(--error); }
|
||
|
|
|
||
|
|
.log-info .log-message { color: var(--info); }
|
||
|
|
.log-success .log-message { color: var(--success); }
|
||
|
|
.log-warning .log-message { color: #856404; }
|
||
|
|
.log-error .log-message { color: var(--error); }
|
||
|
|
|
||
|
|
.empty-state {
|
||
|
|
text-align: center;
|
||
|
|
padding: 40px;
|
||
|
|
color: var(--text-muted);
|
||
|
|
font-size: 11px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
|
|
gap: 12px;
|
||
|
|
padding: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-item {
|
||
|
|
text-align: center;
|
||
|
|
padding: 16px;
|
||
|
|
background: var(--bg-tertiary);
|
||
|
|
border: 2px solid var(--border-color);
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-item::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
height: 3px;
|
||
|
|
background: var(--accent-primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value {
|
||
|
|
font-size: 32px;
|
||
|
|
font-weight: 700;
|
||
|
|
color: var(--accent-primary);
|
||
|
|
line-height: 1;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-label {
|
||
|
|
font-size: 9px;
|
||
|
|
color: var(--text-muted);
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.refresh-indicator {
|
||
|
|
display: inline-block;
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
background: var(--success);
|
||
|
|
border-radius: 50%;
|
||
|
|
margin-left: 8px;
|
||
|
|
animation: pulse 2s infinite;
|
||
|
|
box-shadow: 0 0 8px var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes pulse {
|
||
|
|
0%, 100% { opacity: 1; }
|
||
|
|
50% { opacity: 0.3; }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="header">
|
||
|
|
<h1>SYNC QUEUE VISUALIZATION</h1>
|
||
|
|
<p class="subtitle">// Live monitoring of offline-first calendar sync operations</p>
|
||
|
|
|
||
|
|
<div id="initStatus" style="background: var(--warning); color: #000; padding: 12px; margin-bottom: 16px; border: 2px solid var(--border-color); text-align: center; font-weight: 600; letter-spacing: 0.5px;">
|
||
|
|
[⏳] INITIALIZING CALENDAR SYSTEM...
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="status-bar">
|
||
|
|
<div class="status-item">
|
||
|
|
<span class="status-label">NETWORK:</span>
|
||
|
|
<span id="networkStatus" class="status-badge status-online">ONLINE</span>
|
||
|
|
</div>
|
||
|
|
<div class="status-item">
|
||
|
|
<span class="status-label">SYNC:</span>
|
||
|
|
<span id="syncStatus" class="status-badge status-idle">IDLE</span>
|
||
|
|
</div>
|
||
|
|
<div class="status-item">
|
||
|
|
<span class="status-label">AUTO-REFRESH:</span>
|
||
|
|
<span class="refresh-indicator"></span>
|
||
|
|
</div>
|
||
|
|
<div class="status-item">
|
||
|
|
<span class="status-label">LAST SYNC:</span>
|
||
|
|
<span id="lastSyncTime" class="status-badge status-idle">NEVER</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="controls">
|
||
|
|
<button class="btn-primary" onclick="manualSync()">TRIGGER SYNC</button>
|
||
|
|
<button class="btn-success" onclick="refreshData()">REFRESH DATA</button>
|
||
|
|
<button onclick="toggleNetworkSimulator()">TOGGLE NETWORK</button>
|
||
|
|
<button class="btn-danger" onclick="clearQueue()">CLEAR QUEUE</button>
|
||
|
|
<button class="btn-danger" onclick="clearDatabase()">CLEAR DATABASE</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid">
|
||
|
|
<!-- IndexedDB Events -->
|
||
|
|
<div class="panel">
|
||
|
|
<div class="panel-title">
|
||
|
|
<span>INDEXEDDB EVENTS</span>
|
||
|
|
<span id="eventCount" class="count-badge">0</span>
|
||
|
|
</div>
|
||
|
|
<div id="eventList" class="event-list"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Operation Queue -->
|
||
|
|
<div class="panel">
|
||
|
|
<div class="panel-title">
|
||
|
|
<span>OPERATION QUEUE</span>
|
||
|
|
<span id="queueCount" class="count-badge">0</span>
|
||
|
|
</div>
|
||
|
|
<div id="queueList" class="event-list"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Statistics -->
|
||
|
|
<div class="panel">
|
||
|
|
<div class="panel-title">
|
||
|
|
<span>STATISTICS</span>
|
||
|
|
</div>
|
||
|
|
<div class="stats-grid">
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value" id="statSynced">0</div>
|
||
|
|
<div class="stat-label">Synced</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value" id="statPending">0</div>
|
||
|
|
<div class="stat-label">Pending</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value" id="statError">0</div>
|
||
|
|
<div class="stat-label">Errors</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-item">
|
||
|
|
<div class="stat-value" id="statQueue">0</div>
|
||
|
|
<div class="stat-label">In Queue</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Sync Log -->
|
||
|
|
<div class="panel log-panel">
|
||
|
|
<div class="panel-title">
|
||
|
|
<span>SYNC LOG</span>
|
||
|
|
<button onclick="clearLog()" style="font-size: 10px; padding: 4px 8px;">CLEAR</button>
|
||
|
|
</div>
|
||
|
|
<div id="logList" class="log-list"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Load Test Initialization Script -->
|
||
|
|
<script src="test-init.js"></script>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
let logEntries = [];
|
||
|
|
const MAX_LOG_ENTRIES = 100;
|
||
|
|
let calendarReady = false;
|
||
|
|
|
||
|
|
// Wait for calendar to initialize
|
||
|
|
function waitForCalendar() {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
if (window.calendarDebug?.indexedDB) {
|
||
|
|
calendarReady = true;
|
||
|
|
const initStatus = document.getElementById('initStatus');
|
||
|
|
if (initStatus) {
|
||
|
|
initStatus.style.display = 'none';
|
||
|
|
}
|
||
|
|
resolve();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const checkInterval = setInterval(() => {
|
||
|
|
if (window.calendarDebug?.indexedDB) {
|
||
|
|
calendarReady = true;
|
||
|
|
clearInterval(checkInterval);
|
||
|
|
const initStatus = document.getElementById('initStatus');
|
||
|
|
if (initStatus) {
|
||
|
|
initStatus.style.background = 'var(--success)';
|
||
|
|
initStatus.style.color = '#fff';
|
||
|
|
initStatus.textContent = '[✓] CALENDAR SYSTEM READY';
|
||
|
|
setTimeout(() => {
|
||
|
|
initStatus.style.display = 'none';
|
||
|
|
}, 1000);
|
||
|
|
}
|
||
|
|
resolve();
|
||
|
|
}
|
||
|
|
}, 100);
|
||
|
|
|
||
|
|
// Timeout after 10 seconds
|
||
|
|
setTimeout(() => {
|
||
|
|
if (!calendarReady) {
|
||
|
|
clearInterval(checkInterval);
|
||
|
|
const initStatus = document.getElementById('initStatus');
|
||
|
|
if (initStatus) {
|
||
|
|
initStatus.style.background = 'var(--error)';
|
||
|
|
initStatus.style.color = '#fff';
|
||
|
|
initStatus.textContent = '[✗] CALENDAR SYSTEM FAILED TO INITIALIZE - Check console for details';
|
||
|
|
}
|
||
|
|
reject(new Error('Calendar failed to initialize within 10 seconds'));
|
||
|
|
}
|
||
|
|
}, 10000);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize
|
||
|
|
async function init() {
|
||
|
|
log('info', 'Waiting for calendar system to initialize...');
|
||
|
|
|
||
|
|
try {
|
||
|
|
await waitForCalendar();
|
||
|
|
log('success', 'Connected to calendar IndexedDB');
|
||
|
|
} catch (error) {
|
||
|
|
log('error', 'Calendar system failed to initialize: ' + error.message);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Listen to network events
|
||
|
|
window.addEventListener('online', () => {
|
||
|
|
updateNetworkStatus(true);
|
||
|
|
log('success', 'Network online');
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener('offline', () => {
|
||
|
|
updateNetworkStatus(false);
|
||
|
|
log('warning', 'Network offline');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Initial load
|
||
|
|
await refreshData();
|
||
|
|
|
||
|
|
// Auto-refresh every 2 seconds
|
||
|
|
setInterval(refreshData, 2000);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function refreshData() {
|
||
|
|
try {
|
||
|
|
const db = window.calendarDebug.indexedDB;
|
||
|
|
const queue = window.calendarDebug.queue;
|
||
|
|
|
||
|
|
if (!db || !queue) {
|
||
|
|
log('error', 'IndexedDB or Queue not available');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get events
|
||
|
|
const events = await db.getAllEvents();
|
||
|
|
renderEvents(events);
|
||
|
|
|
||
|
|
// Get queue
|
||
|
|
const queueItems = await queue.getAll();
|
||
|
|
renderQueue(queueItems);
|
||
|
|
|
||
|
|
// Update statistics
|
||
|
|
updateStatistics(events, queueItems);
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
log('error', `Refresh failed: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderEvents(events) {
|
||
|
|
const container = document.getElementById('eventList');
|
||
|
|
document.getElementById('eventCount').textContent = events.length;
|
||
|
|
|
||
|
|
if (events.length === 0) {
|
||
|
|
container.innerHTML = '<div class="empty-state">No events in IndexedDB</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = events.map(event => `
|
||
|
|
<div class="event-item">
|
||
|
|
<div class="event-header">
|
||
|
|
<span class="event-title">${event.title}</span>
|
||
|
|
<span class="sync-status sync-${event.syncStatus}">${event.syncStatus}</span>
|
||
|
|
</div>
|
||
|
|
<div class="event-details">
|
||
|
|
ID: ${event.id}<br>
|
||
|
|
${event.allDay ? 'ALL-DAY' : formatTime(event.start) + ' - ' + formatTime(event.end)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderQueue(queueItems) {
|
||
|
|
const container = document.getElementById('queueList');
|
||
|
|
document.getElementById('queueCount').textContent = queueItems.length;
|
||
|
|
|
||
|
|
if (queueItems.length === 0) {
|
||
|
|
container.innerHTML = '<div class="empty-state">Queue is empty</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = queueItems.map(item => `
|
||
|
|
<div class="queue-item">
|
||
|
|
<div class="queue-header">
|
||
|
|
<span class="operation-type op-${item.type}">${item.type}</span>
|
||
|
|
<span class="retry-count">RETRY: ${item.retryCount}/5</span>
|
||
|
|
</div>
|
||
|
|
<div class="event-details">
|
||
|
|
EVENT ID: ${item.eventId}<br>
|
||
|
|
TIMESTAMP: ${new Date(item.timestamp).toLocaleTimeString('da-DK')}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateStatistics(events, queueItems) {
|
||
|
|
const synced = events.filter(e => e.syncStatus === 'synced').length;
|
||
|
|
const pending = events.filter(e => e.syncStatus === 'pending').length;
|
||
|
|
const error = events.filter(e => e.syncStatus === 'error').length;
|
||
|
|
|
||
|
|
document.getElementById('statSynced').textContent = synced;
|
||
|
|
document.getElementById('statPending').textContent = pending;
|
||
|
|
document.getElementById('statError').textContent = error;
|
||
|
|
document.getElementById('statQueue').textContent = queueItems.length;
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateNetworkStatus(isOnline) {
|
||
|
|
const badge = document.getElementById('networkStatus');
|
||
|
|
badge.textContent = isOnline ? 'ONLINE' : 'OFFLINE';
|
||
|
|
badge.className = `status-badge ${isOnline ? 'status-online' : 'status-offline'}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateSyncStatus(isSyncing) {
|
||
|
|
const badge = document.getElementById('syncStatus');
|
||
|
|
badge.textContent = isSyncing ? 'SYNCING' : 'IDLE';
|
||
|
|
badge.className = `status-badge ${isSyncing ? 'status-syncing' : 'status-idle'}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function manualSync() {
|
||
|
|
const timestamp = new Date().toLocaleTimeString('da-DK');
|
||
|
|
log('info', `Manual sync triggered at ${timestamp}`);
|
||
|
|
updateSyncStatus(true);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const syncManager = window.calendarDebug.syncManager;
|
||
|
|
if (syncManager) {
|
||
|
|
await syncManager.triggerManualSync();
|
||
|
|
log('success', `Manual sync completed at ${timestamp}`);
|
||
|
|
updateLastSyncTime(timestamp, 'success');
|
||
|
|
} else {
|
||
|
|
log('error', 'SyncManager not available');
|
||
|
|
updateLastSyncTime(timestamp, 'error');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
log('error', `Manual sync failed: ${error.message}`);
|
||
|
|
updateLastSyncTime(timestamp, 'error');
|
||
|
|
} finally {
|
||
|
|
updateSyncStatus(false);
|
||
|
|
await refreshData();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateLastSyncTime(timestamp, status = 'success') {
|
||
|
|
const badge = document.getElementById('lastSyncTime');
|
||
|
|
badge.textContent = timestamp;
|
||
|
|
badge.className = `status-badge status-${status}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function clearQueue() {
|
||
|
|
if (!confirm('Clear all operations from the queue?')) return;
|
||
|
|
|
||
|
|
log('warning', 'Clearing queue...');
|
||
|
|
try {
|
||
|
|
const queue = window.calendarDebug.queue;
|
||
|
|
await queue.clear();
|
||
|
|
log('success', 'Queue cleared');
|
||
|
|
await refreshData();
|
||
|
|
} catch (error) {
|
||
|
|
log('error', `Failed to clear queue: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function clearDatabase() {
|
||
|
|
if (!confirm('⚠️ WARNING: This will delete ALL events from IndexedDB! Continue?')) return;
|
||
|
|
|
||
|
|
log('warning', 'Clearing database...');
|
||
|
|
try {
|
||
|
|
const db = window.calendarDebug.indexedDB;
|
||
|
|
db.close();
|
||
|
|
|
||
|
|
await new Promise((resolve, reject) => {
|
||
|
|
const request = indexedDB.deleteDatabase('CalendarDB');
|
||
|
|
request.onsuccess = resolve;
|
||
|
|
request.onerror = reject;
|
||
|
|
});
|
||
|
|
|
||
|
|
log('success', 'Database cleared - please reload the page');
|
||
|
|
alert('Database cleared! Please reload the page.');
|
||
|
|
} catch (error) {
|
||
|
|
log('error', `Failed to clear database: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleNetworkSimulator() {
|
||
|
|
const isCurrentlyOnline = navigator.onLine;
|
||
|
|
log('info', `Network simulator toggle (currently ${isCurrentlyOnline ? 'online' : 'offline'})`);
|
||
|
|
log('warning', 'Use DevTools > Network > Offline for real offline testing');
|
||
|
|
}
|
||
|
|
|
||
|
|
function log(level, message) {
|
||
|
|
const timestamp = new Date().toLocaleTimeString('da-DK');
|
||
|
|
const entry = { timestamp, level, message };
|
||
|
|
|
||
|
|
logEntries.unshift(entry);
|
||
|
|
if (logEntries.length > MAX_LOG_ENTRIES) {
|
||
|
|
logEntries.pop();
|
||
|
|
}
|
||
|
|
|
||
|
|
renderLog();
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderLog() {
|
||
|
|
const container = document.getElementById('logList');
|
||
|
|
container.innerHTML = logEntries.map(entry => `
|
||
|
|
<div class="log-entry log-${entry.level}">
|
||
|
|
<span class="log-timestamp">[${entry.timestamp}]</span>
|
||
|
|
<span class="log-message">${entry.message}</span>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearLog() {
|
||
|
|
logEntries = [];
|
||
|
|
renderLog();
|
||
|
|
log('info', 'Log cleared');
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatTime(date) {
|
||
|
|
return new Date(date).toLocaleTimeString('da-DK', {
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Start on load
|
||
|
|
if (document.readyState === 'loading') {
|
||
|
|
document.addEventListener('DOMContentLoaded', init);
|
||
|
|
} else {
|
||
|
|
init();
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|