Files
video-player-log-analyzer/video_player_log_analyzer.html
jessikitty f58b395b75 Initial commit - Video Player Log Analyzer with print report feature
Complete HTML-based log analyzer for Moonlight Drive-In Theater NFC video player system.

Features:
- Parse NFC scan events from runtime logs (not config loading)
- Extract and resolve NFC mappings from startup configuration
- Statistics dashboard with scan counts, unique videos, and timing metrics
- Interactive charts: Timeline, Hourly Activity, Video Counts, Time-Between-Scans
- Detailed video playback lists with drill-down functionality
- Print Report feature for comprehensive activity reports
- Purple gradient theme matching drive-in UI
- Complete scan and trailer tables for reporting
- No dependencies except Chart.js CDN

Technical Implementation:
- Single-file HTML with embedded CSS and JavaScript
- Dual-pass parsing: Config mappings first, then runtime events
- Smart event detection distinguishing NFC scans from config loading
- Folder video support with multi-file sequential playback
- Print-optimized CSS with page breaks and alternating row colors
- Error handling and console debugging for troubleshooting
2026-01-18 21:39:38 +11:00

1256 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Player Log Analyzer</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
text-align: center;
}
h1 {
color: #667eea;
margin-bottom: 10px;
font-size: 2.5em;
}
.upload-section {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
text-align: center;
}
.file-input-wrapper {
position: relative;
display: inline-block;
cursor: pointer;
}
.file-input-wrapper input[type=file] {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-input-label {
display: inline-block;
padding: 15px 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
}
.file-input-label:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary {
display: inline-block;
padding: 15px 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
margin-left: 15px;
vertical-align: middle;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
text-align: center;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-value {
font-size: 3em;
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
}
.stat-label {
color: #666;
font-size: 1.1em;
font-weight: 500;
}
.chart-container {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
position: relative;
height: 500px;
}
.video-list {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
}
.video-list h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.8em;
}
.video-item {
padding: 15px;
margin-bottom: 10px;
background: #f8f9fa;
border-radius: 10px;
border-left: 5px solid #667eea;
cursor: pointer;
transition: all 0.2s;
}
.video-item:hover {
background: #e9ecef;
transform: translateX(5px);
}
.video-name {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.video-stats {
color: #666;
font-size: 0.9em;
}
.detail-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 1000;
padding: 20px;
overflow-y: auto;
}
.detail-content {
background: white;
max-width: 900px;
margin: 50px auto;
padding: 40px;
border-radius: 15px;
position: relative;
}
.close-btn {
position: absolute;
top: 20px;
right: 20px;
font-size: 2em;
cursor: pointer;
color: #666;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.detail-header {
color: #667eea;
margin-bottom: 20px;
font-size: 2em;
}
.playback-timeline {
margin-top: 20px;
}
.timeline-item {
padding: 15px;
margin-bottom: 10px;
background: #f8f9fa;
border-radius: 10px;
border-left: 3px solid #667eea;
}
.timeline-time {
font-weight: 600;
color: #667eea;
margin-bottom: 5px;
}
.timeline-info {
color: #666;
font-size: 0.95em;
}
.filters {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
}
.filters h3 {
color: #667eea;
margin-bottom: 15px;
}
.filter-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.filter-group label {
font-weight: 600;
color: #333;
}
.filter-group select, .filter-group input {
padding: 8px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
outline: none;
transition: border-color 0.2s;
}
.filter-group select:focus, .filter-group input:focus {
border-color: #667eea;
}
.loading {
display: none;
text-align: center;
padding: 20px;
color: white;
font-size: 1.2em;
}
.hidden {
display: none;
}
.badge {
display: inline-block;
padding: 5px 10px;
background: #667eea;
color: white;
border-radius: 15px;
font-size: 0.85em;
margin-left: 10px;
}
.nfc-badge {
background: #764ba2;
}
.trailer-badge {
background: #48bb78;
}
#results {
display: none;
}
/* Print Styles */
@media print {
body {
background: white;
padding: 0;
}
.container, .header, .upload-section, .loading,
#results, .detail-modal, #printBtn {
display: none !important;
}
#printReport {
display: block !important;
max-width: 100%;
padding: 20px;
}
.print-header {
text-align: center;
margin-bottom: 30px;
border-bottom: 3px solid #7c3aed;
padding-bottom: 20px;
}
.print-header h1 {
color: #7c3aed;
margin: 0;
font-size: 28px;
}
.print-summary, .print-charts, .print-scans, .print-trailers {
margin-bottom: 40px;
page-break-inside: avoid;
}
.print-summary h2, .print-charts h2,
.print-scans h2, .print-trailers h2 {
color: #7c3aed;
border-bottom: 2px solid #e9d5ff;
padding-bottom: 10px;
margin-bottom: 20px;
}
.print-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.print-stat-card {
border: 2px solid #e9d5ff;
padding: 15px;
text-align: center;
border-radius: 8px;
}
.print-stat-value {
font-size: 24px;
font-weight: bold;
color: #7c3aed;
}
.print-stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.print-chart {
page-break-inside: avoid;
margin-bottom: 30px;
}
.print-chart h3 {
color: #7c3aed;
margin-bottom: 10px;
}
.print-chart canvas {
max-width: 100%;
height: auto !important;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
}
table th {
background: #7c3aed;
color: white;
padding: 8px;
text-align: left;
font-weight: bold;
}
table td {
padding: 6px 8px;
border-bottom: 1px solid #e9d5ff;
}
table tr:nth-child(even) {
background: #faf5ff;
}
.page-break {
page-break-before: always;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎬 Video Player Log Analyzer</h1>
<p>Analyze NFC scans, video playback, and usage patterns</p>
</div>
<div class="upload-section">
<div class="file-input-wrapper">
<input type="file" id="fileInput" accept=".log,.txt">
<label class="file-input-label">📁 Load Log File</label>
</div>
<button onclick="printReport()" class="btn-primary" id="printBtn" style="display: none;">
🖨️ Print Report
</button>
<p style="margin-top: 15px; color: #666;">Upload your video_player_*.log file to begin analysis</p>
</div>
<div class="loading" id="loading">
<p>⏳ Analyzing log file...</p>
</div>
<div id="results">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalNfcScans">0</div>
<div class="stat-label">NFC Scans</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalVideos">0</div>
<div class="stat-label">Unique Videos</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalTrailers">0</div>
<div class="stat-label">Trailers Played</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgTimeBetween">0s</div>
<div class="stat-label">Avg. Time Between Scans</div>
</div>
</div>
<div class="filters">
<h3>📊 Chart Options</h3>
<div class="filter-group">
<label>View:</label>
<select id="chartType">
<option value="timeline">Timeline (All Events)</option>
<option value="hourly">Hourly Activity</option>
<option value="videoCount">Video Play Count</option>
<option value="timeBetween">Time Between Scans</option>
</select>
<label style="margin-left: 20px;">Filter:</label>
<select id="eventFilter">
<option value="all">All Events</option>
<option value="nfc">NFC Scans Only</option>
<option value="trailers">Trailers Only</option>
</select>
</div>
</div>
<div class="chart-container">
<canvas id="mainChart"></canvas>
</div>
<div class="video-list">
<h2>🎥 Video Playback Details</h2>
<div id="videoListContainer"></div>
</div>
</div>
<div class="detail-modal" id="detailModal">
<div class="detail-content">
<span class="close-btn" onclick="closeDetail()">×</span>
<h2 class="detail-header" id="detailTitle"></h2>
<div id="detailStats"></div>
<div class="playback-timeline" id="detailTimeline"></div>
</div>
</div>
</div>
<!-- Print Report (hidden, only visible when printing) -->
<div id="printReport" style="display: none;">
<div class="print-header">
<h1>🎬 Video Player Activity Report</h1>
<p id="printReportDate"></p>
</div>
<div class="print-summary">
<h2>Summary Statistics</h2>
<div id="printStats"></div>
</div>
<div class="print-charts">
<h2>Activity Charts</h2>
<div id="printChartsContainer"></div>
</div>
<div class="print-scans">
<h2>NFC Scan Details</h2>
<table id="printScansTable"></table>
</div>
<div class="print-trailers">
<h2>Trailer Playback Details</h2>
<table id="printTrailersTable"></table>
</div>
</div>
<script>
let logData = {
nfcScans: [],
videoPlays: [],
trailers: [],
allEvents: []
};
let currentChart = null;
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
document.getElementById('chartType').addEventListener('change', updateChart);
document.getElementById('eventFilter').addEventListener('change', updateChart);
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
document.getElementById('loading').style.display = 'block';
document.getElementById('results').style.display = 'none';
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
setTimeout(() => {
parseLogFile(content);
displayResults();
document.getElementById('loading').style.display = 'none';
document.getElementById('results').style.display = 'block';
}, 100);
};
reader.readAsText(file);
}
function parseLogFile(content) {
const lines = content.split('\n');
logData = {
nfcScans: [],
videoPlays: [],
trailers: [],
allEvents: []
};
// First pass: Parse NFC mappings from startup configuration
const nfcMappings = {};
const singleFilePattern = /Mapped NFC (\d+) -> (.+)/;
const folderPattern = /Mapped NFC (\d+) -> Folder '(.+?)' \((\d+) videos?\)/;
for (const line of lines) {
const folderMatch = line.match(folderPattern);
const singleMatch = line.match(singleFilePattern);
if (folderMatch) {
const [, nfcId, folderName, videoCount] = folderMatch;
nfcMappings[nfcId] = `Folder '${folderName}' (${videoCount} videos)`;
} else if (singleMatch && !line.includes('Folder')) {
const [, nfcId, videoName] = singleMatch;
nfcMappings[nfcId] = videoName.trim();
}
}
// Updated patterns to match actual runtime scan events
const nfcScanPattern = /(\d{2}:\d{2}:\d{2}\.\d{3}).*Processing NFC input: (\d+)/;
const folderVideoPattern = /(\d{2}:\d{2}:\d{2}\.\d{3}).*Folder '(.+?)': Playing video (\d+)\/(\d+)/;
const queuedSpecificPattern = /(\d{2}:\d{2}:\d{2}\.\d{3}).*Queued specific video: (.+)/;
const queuedFolderPattern = /(\d{2}:\d{2}:\d{2}\.\d{3}).*Queued folder video: (.+)/;
const trailerPattern = /(\d{2}:\d{2}:\d{2}\.\d{3}).*Selected trailer: (.+)/;
let lastNfcScan = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// NFC Scan detection (actual runtime scans)
const nfcMatch = line.match(nfcScanPattern);
if (nfcMatch) {
const [, time, nfcId] = nfcMatch;
// Look up the video/folder name from the mappings
const videoName = nfcMappings[nfcId] || `Unknown (NFC ${nfcId})`;
const event = {
time: time,
timestamp: parseTime(time),
nfcId: nfcId,
video: videoName,
type: 'nfc'
};
logData.nfcScans.push(event);
logData.allEvents.push(event);
lastNfcScan = event;
}
// Trailer detection
const trailerMatch = line.match(trailerPattern);
if (trailerMatch) {
const [, time, trailer] = trailerMatch;
const event = {
time: time,
timestamp: parseTime(time),
video: trailer.trim(),
type: 'trailer'
};
logData.trailers.push(event);
logData.allEvents.push(event);
}
// Video play events for counting (both NFC-triggered and trailers)
const specificMatch = line.match(queuedSpecificPattern);
const folderMatch = line.match(queuedFolderPattern);
if (specificMatch || folderMatch) {
const videoName = (specificMatch ? specificMatch[2] : folderMatch[2]).trim();
const time = (specificMatch ? specificMatch[1] : folderMatch[1]);
// Determine if this is tied to an NFC scan or is a trailer
const isNfcTriggered = lastNfcScan &&
(parseTime(time) - lastNfcScan.timestamp < 5000); // Within 5 seconds
if (isNfcTriggered) {
const event = {
time: time,
timestamp: parseTime(time),
video: videoName,
type: 'video'
};
logData.videoPlays.push(event);
logData.allEvents.push(event);
}
}
}
// Sort all events by timestamp
logData.allEvents.sort((a, b) => a.timestamp - b.timestamp);
}
function parseTime(timeStr) {
const [hours, minutes, seconds] = timeStr.split(':');
const [secs, ms] = seconds.split('.');
return (parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(secs)) * 1000 + parseInt(ms);
}
function formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
function formatDuration(ms) {
const totalSeconds = Math.floor(ms / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes < 60) return `${minutes}m ${seconds}s`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}h ${mins}m`;
}
function displayResults() {
// Show print button
document.getElementById('printBtn').style.display = 'inline-block';
// Update stats
document.getElementById('totalNfcScans').textContent = logData.nfcScans.length;
const uniqueVideos = new Set([
...logData.videoPlays.map(v => v.video),
...logData.nfcScans.map(v => v.video)
]);
document.getElementById('totalVideos').textContent = uniqueVideos.size;
document.getElementById('totalTrailers').textContent = logData.trailers.length;
// Calculate average time between NFC scans
if (logData.nfcScans.length > 1) {
let totalTime = 0;
for (let i = 1; i < logData.nfcScans.length; i++) {
totalTime += logData.nfcScans[i].timestamp - logData.nfcScans[i-1].timestamp;
}
const avgTime = totalTime / (logData.nfcScans.length - 1);
document.getElementById('avgTimeBetween').textContent = formatDuration(avgTime);
}
// Create video list
createVideoList();
// Create chart
updateChart();
}
function createVideoList() {
const container = document.getElementById('videoListContainer');
container.innerHTML = '';
// Count plays per video
const videoCounts = {};
[...logData.nfcScans, ...logData.videoPlays].forEach(event => {
const video = event.video;
if (!videoCounts[video]) {
videoCounts[video] = {
name: video,
count: 0,
nfcCount: 0,
events: []
};
}
videoCounts[video].count++;
if (event.type === 'nfc') videoCounts[video].nfcCount++;
videoCounts[video].events.push(event);
});
// Sort by play count
const sortedVideos = Object.values(videoCounts).sort((a, b) => b.count - a.count);
sortedVideos.forEach(video => {
const item = document.createElement('div');
item.className = 'video-item';
item.onclick = () => showVideoDetail(video);
const badges = video.nfcCount > 0 ?
`<span class="badge nfc-badge">NFC: ${video.nfcCount}</span>` : '';
item.innerHTML = `
<div class="video-name">${video.name} ${badges}</div>
<div class="video-stats">
Played ${video.count} time${video.count !== 1 ? 's' : ''}
</div>
`;
container.appendChild(item);
});
}
function showVideoDetail(video) {
document.getElementById('detailTitle').textContent = video.name;
const stats = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${video.count}</div>
<div class="stat-label">Total Plays</div>
</div>
<div class="stat-card">
<div class="stat-value">${video.nfcCount}</div>
<div class="stat-label">NFC Scans</div>
</div>
</div>
`;
document.getElementById('detailStats').innerHTML = stats;
const timeline = document.getElementById('detailTimeline');
timeline.innerHTML = '<h3 style="color: #667eea; margin-bottom: 15px;">Playback Timeline</h3>';
video.events.forEach((event, index) => {
const item = document.createElement('div');
item.className = 'timeline-item';
let timeBetween = '';
if (index > 0) {
const timeDiff = event.timestamp - video.events[index-1].timestamp;
timeBetween = `<br><small style="color: #999;">+${formatDuration(timeDiff)} since last play</small>`;
}
const typeLabel = event.type === 'nfc' ?
'<span class="badge nfc-badge">NFC Scan</span>' :
'<span class="badge">Auto-play</span>';
item.innerHTML = `
<div class="timeline-time">${event.time} ${typeLabel}</div>
<div class="timeline-info">
${event.nfcId ? `NFC ID: ${event.nfcId}` : 'Automatic playback'}
${timeBetween}
</div>
`;
timeline.appendChild(item);
});
document.getElementById('detailModal').style.display = 'block';
}
function closeDetail() {
document.getElementById('detailModal').style.display = 'none';
}
function updateChart() {
const chartType = document.getElementById('chartType').value;
const eventFilter = document.getElementById('eventFilter').value;
if (currentChart) {
currentChart.destroy();
}
let filteredEvents = logData.allEvents;
if (eventFilter === 'nfc') {
filteredEvents = logData.nfcScans;
} else if (eventFilter === 'trailers') {
filteredEvents = logData.trailers;
}
const ctx = document.getElementById('mainChart').getContext('2d');
switch (chartType) {
case 'timeline':
createTimelineChart(ctx, filteredEvents);
break;
case 'hourly':
createHourlyChart(ctx, filteredEvents);
break;
case 'videoCount':
createVideoCountChart(ctx, filteredEvents);
break;
case 'timeBetween':
createTimeBetweenChart(ctx);
break;
}
}
function createTimelineChart(ctx, events) {
const data = events.map(event => ({
x: event.timestamp,
y: 1,
label: event.video,
type: event.type
}));
currentChart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: 'NFC Scans',
data: data.filter(d => d.type === 'nfc'),
backgroundColor: 'rgba(118, 75, 162, 0.8)',
pointRadius: 8,
pointHoverRadius: 12
}, {
label: 'Videos',
data: data.filter(d => d.type === 'video'),
backgroundColor: 'rgba(102, 126, 234, 0.8)',
pointRadius: 6,
pointHoverRadius: 10
}, {
label: 'Trailers',
data: data.filter(d => d.type === 'trailer'),
backgroundColor: 'rgba(72, 187, 120, 0.8)',
pointRadius: 4,
pointHoverRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Event Timeline',
font: { size: 18, weight: 'bold' }
},
tooltip: {
callbacks: {
label: function(context) {
const point = context.raw;
return `${point.label} (${formatTime(point.x)})`;
}
}
}
},
scales: {
x: {
type: 'linear',
title: {
display: true,
text: 'Time'
},
ticks: {
callback: function(value) {
return formatTime(value);
}
}
},
y: {
display: false
}
}
}
});
}
function createHourlyChart(ctx, events) {
const hourCounts = new Array(24).fill(0);
events.forEach(event => {
const hour = Math.floor(event.timestamp / (3600 * 1000));
if (hour < 24) hourCounts[hour]++;
});
currentChart = new Chart(ctx, {
type: 'bar',
data: {
labels: hourCounts.map((_, i) => `${String(i).padStart(2, '0')}:00`),
datasets: [{
label: 'Events per Hour',
data: hourCounts,
backgroundColor: 'rgba(102, 126, 234, 0.8)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Hourly Activity',
font: { size: 18, weight: 'bold' }
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Number of Events'
}
},
x: {
title: {
display: true,
text: 'Hour of Day'
}
}
}
}
});
}
function createVideoCountChart(ctx, events) {
const videoCounts = {};
events.forEach(event => {
videoCounts[event.video] = (videoCounts[event.video] || 0) + 1;
});
const sorted = Object.entries(videoCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 15);
currentChart = new Chart(ctx, {
type: 'bar',
data: {
labels: sorted.map(([name]) => name.length > 30 ? name.substring(0, 27) + '...' : name),
datasets: [{
label: 'Play Count',
data: sorted.map(([, count]) => count),
backgroundColor: 'rgba(118, 75, 162, 0.8)',
borderColor: 'rgba(118, 75, 162, 1)',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
plugins: {
title: {
display: true,
text: 'Top 15 Most Played Videos',
font: { size: 18, weight: 'bold' }
}
},
scales: {
x: {
beginAtZero: true,
title: {
display: true,
text: 'Number of Plays'
}
}
}
}
});
}
function createTimeBetweenChart(ctx) {
if (logData.nfcScans.length < 2) {
ctx.font = '20px Arial';
ctx.fillStyle = '#666';
ctx.textAlign = 'center';
ctx.fillText('Not enough NFC scan data', ctx.canvas.width / 2, ctx.canvas.height / 2);
return;
}
const timeBetween = [];
for (let i = 1; i < logData.nfcScans.length; i++) {
timeBetween.push({
x: i,
y: (logData.nfcScans[i].timestamp - logData.nfcScans[i-1].timestamp) / 1000
});
}
currentChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Time Between Scans (seconds)',
data: timeBetween,
borderColor: 'rgba(102, 126, 234, 1)',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Time Between Consecutive NFC Scans',
font: { size: 18, weight: 'bold' }
}
},
scales: {
x: {
type: 'linear',
title: {
display: true,
text: 'Scan Number'
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Seconds'
}
}
}
}
});
}
function printReport() {
try {
console.log('Print Report clicked');
console.log('logData:', logData);
// Generate report date
const now = new Date();
document.getElementById('printReportDate').textContent =
`Generated: ${now.toLocaleDateString()} ${now.toLocaleTimeString()}`;
// Generate summary statistics
const statsHtml = `
<div class="print-stats-grid">
<div class="print-stat-card">
<div class="print-stat-value">${logData.nfcScans.length}</div>
<div class="print-stat-label">Total NFC Scans</div>
</div>
<div class="print-stat-card">
<div class="print-stat-value">${logData.videoPlays.length}</div>
<div class="print-stat-label">Videos Played</div>
</div>
<div class="print-stat-card">
<div class="print-stat-value">${logData.trailers.length}</div>
<div class="print-stat-label">Trailers Played</div>
</div>
<div class="print-stat-card">
<div class="print-stat-value">${document.getElementById('avgTimeBetween').textContent}</div>
<div class="print-stat-label">Avg Time Between Scans</div>
</div>
</div>
`;
document.getElementById('printStats').innerHTML = statsHtml;
console.log('Stats HTML generated');
// Generate charts
const chartsContainer = document.getElementById('printChartsContainer');
chartsContainer.innerHTML = '';
console.log('Charts container cleared');
// Create print versions of all charts
const chartTypes = [
{ type: 'timeline', title: 'Event Timeline', height: 200 },
{ type: 'hourly', title: 'Hourly Activity', height: 300 },
{ type: 'video-count', title: 'Top 15 Videos', height: 400 },
{ type: 'time-between', title: 'Time Between Scans', height: 300 }
];
chartTypes.forEach(({ type, title, height }) => {
const chartDiv = document.createElement('div');
chartDiv.className = 'print-chart';
chartDiv.innerHTML = `<h3>${title}</h3>`;
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = height;
chartDiv.appendChild(canvas);
chartsContainer.appendChild(chartDiv);
const ctx = canvas.getContext('2d');
// Get filtered events based on current filter
const filterValue = document.getElementById('eventFilter').value;
let events = logData.allEvents;
if (filterValue === 'nfc') {
events = logData.nfcScans;
} else if (filterValue === 'trailers') {
events = logData.trailers;
}
// Generate chart based on type
switch(type) {
case 'timeline':
createTimelineChart(ctx, events);
break;
case 'hourly':
createHourlyChart(ctx, events);
break;
case 'video-count':
createVideoCountChart(ctx, events);
break;
case 'time-between':
createTimeBetweenChart(ctx, logData.nfcScans);
break;
}
});
// Generate NFC scans table
let scansTableHtml = `
<thead>
<tr>
<th>#</th>
<th>Time</th>
<th>NFC ID</th>
<th>Video/Folder</th>
</tr>
</thead>
<tbody>
`;
logData.nfcScans.forEach((scan, index) => {
scansTableHtml += `
<tr>
<td>${index + 1}</td>
<td>${scan.time}</td>
<td>${scan.nfcId}</td>
<td>${scan.video}</td>
</tr>
`;
});
scansTableHtml += '</tbody>';
document.getElementById('printScansTable').innerHTML = scansTableHtml;
// Generate trailers table
let trailersTableHtml = `
<thead>
<tr>
<th>#</th>
<th>Time</th>
<th>Trailer</th>
</tr>
</thead>
<tbody>
`;
logData.trailers.forEach((trailer, index) => {
trailersTableHtml += `
<tr>
<td>${index + 1}</td>
<td>${trailer.time}</td>
<td>${trailer.video}</td>
</tr>
`;
});
trailersTableHtml += '</tbody>';
document.getElementById('printTrailersTable').innerHTML = trailersTableHtml;
console.log('Tables generated successfully');
// Trigger print dialog
console.log('Triggering print dialog in 500ms...');
setTimeout(() => {
console.log('Opening print dialog now');
window.print();
}, 500); // Give charts time to render
} catch (error) {
console.error('Error in printReport:', error);
alert('Error generating print report: ' + error.message);
}
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('detailModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>