Calendar/test/integrationtesting/sync-visualization.html

855 lines
22 KiB
HTML
Raw Normal View History

<!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>