mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
169 lines
6.5 KiB
JavaScript
169 lines
6.5 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const { db } = require('../db/database');
|
|
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
|
|
|
|
// Helper: scope reports to user's devices
|
|
function getUserDeviceFilter(user) {
|
|
if (PLATFORM_ROLES.includes(user.role)) return { sql: '', params: [] };
|
|
return { sql: ' AND d.user_id = ?', params: [user.id] };
|
|
}
|
|
|
|
// Query play logs
|
|
router.get('/plays', (req, res) => {
|
|
const { device_id, content_id, start, end, limit: lim } = req.query;
|
|
const scope = getUserDeviceFilter(req.user);
|
|
let sql = `SELECT pl.*, d.name as device_name
|
|
FROM play_logs pl
|
|
JOIN devices d ON pl.device_id = d.id
|
|
WHERE 1=1${scope.sql}`;
|
|
const params = [...scope.params];
|
|
|
|
if (device_id) { sql += ' AND pl.device_id = ?'; params.push(device_id); }
|
|
if (content_id) { sql += ' AND pl.content_id = ?'; params.push(content_id); }
|
|
if (start) { sql += ' AND pl.started_at >= ?'; params.push(Math.floor(new Date(start).getTime() / 1000)); }
|
|
if (end) { sql += ' AND pl.started_at <= ?'; params.push(Math.floor(new Date(end).getTime() / 1000)); }
|
|
|
|
sql += ' ORDER BY pl.started_at DESC LIMIT ?';
|
|
params.push(parseInt(lim) || 500);
|
|
|
|
res.json(db.prepare(sql).all(...params));
|
|
});
|
|
|
|
// Summary report
|
|
router.get('/summary', (req, res) => {
|
|
const { device_id, start, end, group_by } = req.query;
|
|
const startEpoch = start ? Math.floor(new Date(start).getTime() / 1000) : Math.floor(Date.now() / 1000) - 30 * 86400;
|
|
const endEpoch = end ? Math.floor(new Date(end + 'T23:59:59').getTime() / 1000) : Math.floor(Date.now() / 1000);
|
|
|
|
let deviceFilter = '';
|
|
const params = [startEpoch, endEpoch];
|
|
// Scope to user's devices (non-admin)
|
|
if (!ELEVATED_ROLES.includes(req.user.role)) {
|
|
deviceFilter += ' AND device_id IN (SELECT id FROM devices WHERE user_id = ?)';
|
|
params.push(req.user.id);
|
|
}
|
|
if (device_id) { deviceFilter += ' AND device_id = ?'; params.push(device_id); }
|
|
|
|
// Overall stats
|
|
const overall = db.prepare(`
|
|
SELECT COUNT(*) as total_plays,
|
|
COALESCE(SUM(duration_sec), 0) as total_duration_sec,
|
|
COUNT(DISTINCT content_id) as unique_content,
|
|
COUNT(DISTINCT device_id) as unique_devices,
|
|
AVG(duration_sec) as avg_duration_sec
|
|
FROM play_logs
|
|
WHERE started_at >= ? AND started_at <= ? ${deviceFilter}
|
|
`).get(...params);
|
|
|
|
// By content
|
|
const byContent = db.prepare(`
|
|
SELECT content_id, content_name, COUNT(*) as plays,
|
|
COALESCE(SUM(duration_sec), 0) as total_seconds,
|
|
SUM(completed) as completed_plays
|
|
FROM play_logs
|
|
WHERE started_at >= ? AND started_at <= ? ${deviceFilter}
|
|
GROUP BY content_id, content_name
|
|
ORDER BY plays DESC LIMIT 50
|
|
`).all(...params);
|
|
|
|
// By device
|
|
const byDevice = db.prepare(`
|
|
SELECT pl.device_id, d.name as device_name, COUNT(*) as plays,
|
|
COALESCE(SUM(pl.duration_sec), 0) as total_seconds
|
|
FROM play_logs pl
|
|
JOIN devices d ON pl.device_id = d.id
|
|
WHERE pl.started_at >= ? AND pl.started_at <= ? ${deviceFilter}
|
|
GROUP BY pl.device_id
|
|
ORDER BY plays DESC
|
|
`).all(...params);
|
|
|
|
// By hour of day
|
|
const byHour = db.prepare(`
|
|
SELECT CAST(strftime('%H', started_at, 'unixepoch', 'localtime') AS INTEGER) as hour,
|
|
COUNT(*) as plays
|
|
FROM play_logs
|
|
WHERE started_at >= ? AND started_at <= ? ${deviceFilter}
|
|
GROUP BY hour ORDER BY hour
|
|
`).all(...params);
|
|
|
|
// By day
|
|
const byDay = db.prepare(`
|
|
SELECT date(started_at, 'unixepoch', 'localtime') as day, COUNT(*) as plays,
|
|
COALESCE(SUM(duration_sec), 0) as total_seconds
|
|
FROM play_logs
|
|
WHERE started_at >= ? AND started_at <= ? ${deviceFilter}
|
|
GROUP BY day ORDER BY day
|
|
`).all(...params);
|
|
|
|
res.json({
|
|
period: { start: new Date(startEpoch * 1000).toISOString(), end: new Date(endEpoch * 1000).toISOString() },
|
|
overall: {
|
|
total_plays: overall.total_plays,
|
|
total_hours: Math.round(overall.total_duration_sec / 3600 * 10) / 10,
|
|
unique_content: overall.unique_content,
|
|
unique_devices: overall.unique_devices,
|
|
avg_duration_sec: Math.round(overall.avg_duration_sec || 0),
|
|
},
|
|
by_content: byContent,
|
|
by_device: byDevice,
|
|
by_hour: byHour,
|
|
by_day: byDay,
|
|
});
|
|
});
|
|
|
|
// Export CSV
|
|
router.get('/export', (req, res) => {
|
|
const { device_id, start, end } = req.query;
|
|
const startEpoch = start ? Math.floor(new Date(start).getTime() / 1000) : 0;
|
|
const endEpoch = end ? Math.floor(new Date(end + 'T23:59:59').getTime() / 1000) : Math.floor(Date.now() / 1000);
|
|
|
|
let sql = `SELECT pl.*, d.name as device_name FROM play_logs pl JOIN devices d ON pl.device_id = d.id WHERE pl.started_at >= ? AND pl.started_at <= ?`;
|
|
const params = [startEpoch, endEpoch];
|
|
if (device_id) { sql += ' AND pl.device_id = ?'; params.push(device_id); }
|
|
sql += ' ORDER BY pl.started_at ASC';
|
|
|
|
const rows = db.prepare(sql).all(...params);
|
|
|
|
const header = 'Device,Content,Started,Ended,Duration (sec),Completed\n';
|
|
const csv = header + rows.map(r => {
|
|
const started = new Date(r.started_at * 1000).toISOString();
|
|
const ended = r.ended_at ? new Date(r.ended_at * 1000).toISOString() : '';
|
|
return `"${r.device_name}","${r.content_name}","${started}","${ended}",${r.duration_sec || ''},${r.completed ? 'Yes' : 'No'}`;
|
|
}).join('\n');
|
|
|
|
res.setHeader('Content-Type', 'text/csv');
|
|
res.setHeader('Content-Disposition', 'attachment; filename=proof-of-play.csv');
|
|
res.send(csv);
|
|
});
|
|
|
|
// Device uptime report
|
|
router.get('/uptime', (req, res) => {
|
|
const { device_id, start, end } = req.query;
|
|
const startEpoch = start ? Math.floor(new Date(start).getTime() / 1000) : Math.floor(Date.now() / 1000) - 30 * 86400;
|
|
const endEpoch = end ? Math.floor(new Date(end + 'T23:59:59').getTime() / 1000) : Math.floor(Date.now() / 1000);
|
|
|
|
let sql = `SELECT dt.device_id, d.name as device_name,
|
|
COUNT(*) as heartbeat_count,
|
|
MIN(dt.reported_at) as first_seen,
|
|
MAX(dt.reported_at) as last_seen
|
|
FROM device_telemetry dt
|
|
JOIN devices d ON dt.device_id = d.id
|
|
WHERE dt.reported_at >= ? AND dt.reported_at <= ?`;
|
|
const params = [startEpoch, endEpoch];
|
|
if (device_id) { sql += ' AND dt.device_id = ?'; params.push(device_id); }
|
|
sql += ' GROUP BY dt.device_id ORDER BY d.name';
|
|
|
|
const uptimeData = db.prepare(sql).all(...params);
|
|
|
|
// Estimate uptime: heartbeats are every 15s, so heartbeat_count * 15 / total_period
|
|
const totalPeriod = endEpoch - startEpoch;
|
|
uptimeData.forEach(d => {
|
|
d.estimated_uptime_pct = Math.min(100, Math.round((d.heartbeat_count * 15 / totalPeriod) * 100 * 10) / 10);
|
|
});
|
|
|
|
res.json(uptimeData);
|
|
});
|
|
|
|
module.exports = router;
|