import Alpine from 'alpinejs'; import 'htmx.org'; import { Chart, BarController, BarElement, LineController, LineElement, PointElement, PieController, ArcElement, CategoryScale, LinearScale, Tooltip, Title, Legend, Filler, } from 'chart.js'; // Dark mode toggle. Layout's inline script handles the FOUC-free // initial paint; this just wires the toggle button. function applyTheme(theme) { if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } try { localStorage.setItem('irdb-theme', theme); } catch (e) { /* ignore */ } } document.addEventListener('click', (e) => { const target = e.target.closest('[data-theme-toggle]'); if (!target) return; const next = document.documentElement.classList.contains('dark') ? 'light' : 'dark'; applyTheme(next); }); // htmx: send the per-session CSRF token on every state-changing request. document.body.addEventListener('htmx:configRequest', (e) => { const meta = document.querySelector('meta[name="csrf-token"]'); if (meta && meta.content) { e.detail.headers['X-CSRF-Token'] = meta.content; } }); // Dashboard charts. Each canvas carries its data in a data-attribute // (server-pre-bucketed; no AJAX). Chart.js is tree-shaken to just the // pieces we use so the bundle stays small. Chart.register( BarController, BarElement, LineController, LineElement, PointElement, PieController, ArcElement, CategoryScale, LinearScale, Tooltip, Title, Legend, Filler, ); // Expose Chart globally so per-page inline scripts (e.g. the policy // score-distribution component) can render charts without re-bundling. window.Chart = Chart; // Faded "glass" palette: Tailwind -400 tones for a powdery, translucent // feel. Emerald-400 (#34d399) matches the mid-stop of the logo's gradient. const PIE_COLORS = [ '#34d399', // emerald-400 (matches logo's mid-stop) '#22d3ee', // cyan-400 '#818cf8', // indigo-400 '#a78bfa', // violet-400 '#f472b6', // pink-400 '#fb7185', // rose-400 '#fbbf24', // amber-400 '#a3e635', // lime-400 '#2dd4bf', // teal-400 '#60a5fa', // blue-400 ]; function chartTheme() { const isDark = document.documentElement.classList.contains('dark'); return { tickColor: isDark ? '#94a3b8' : '#475569', gridColor: isDark ? 'rgba(148,163,184,0.15)' : 'rgba(148,163,184,0.3)', legendColor: isDark ? '#cbd5e1' : '#334155', }; } function hexToRgba(hex, alpha) { const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex); if (!m) return hex; return `rgba(${parseInt(m[1], 16)},${parseInt(m[2], 16)},${parseInt(m[3], 16)},${alpha})`; } function parseSeries(canvas, attr) { try { return JSON.parse(canvas.dataset[attr] || '[]'); } catch (e) { return []; } } function renderReportsChart() { const canvas = document.getElementById('reports-chart'); if (!canvas) return; const buckets = parseSeries(canvas, 'buckets'); const labels = buckets.map((b) => (b.hour || '').replace(/.*T(\d{2}).*/, '$1h')); const data = buckets.map((b) => b.count || 0); const t = chartTheme(); new Chart(canvas, { type: 'bar', data: { labels, datasets: [{ label: 'reports', data, backgroundColor: 'rgba(52, 211, 153, 0.7)', // emerald-400 @ 70% — frosted bar borderColor: 'rgba(52, 211, 153, 0.9)', borderWidth: 1, borderRadius: 4, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: t.tickColor }, grid: { color: t.gridColor } }, y: { ticks: { color: t.tickColor, precision: 0 }, grid: { color: t.gridColor }, beginAtZero: true }, }, }, }); } function renderPieChart(canvasId) { const canvas = document.getElementById(canvasId); if (!canvas) return; const series = parseSeries(canvas, 'series'); if (series.length === 0) return; const labelKey = canvas.dataset.labelKey || 'name'; const labels = series.map((row) => String(row[labelKey] ?? '')); const data = series.map((row) => row.count || 0); const colors = labels.map((_, i) => PIE_COLORS[i % PIE_COLORS.length]); const t = chartTheme(); new Chart(canvas, { type: 'pie', data: { labels, datasets: [{ data, backgroundColor: colors, borderColor: 'rgba(255,255,255,0.25)', // crisper glass-segment seam borderWidth: 1.5, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: t.legendColor, boxWidth: 12, font: { size: 11 } }, }, tooltip: { callbacks: { label: (ctx) => { const total = ctx.dataset.data.reduce((acc, v) => acc + v, 0) || 1; const pct = ((ctx.parsed / total) * 100).toFixed(1); return `${ctx.label}: ${ctx.parsed} (${pct}%)`; }, }, }, }, }, }); } function renderBlockedIpsChart() { const canvas = document.getElementById('blocked-ips-chart'); if (!canvas) return; let payload; try { payload = JSON.parse(canvas.dataset.blocked || '{}'); } catch (e) { return; } const days = Array.isArray(payload.days) ? payload.days : []; const series = Array.isArray(payload.series) ? payload.series : []; if (days.length === 0 || series.length === 0) return; // Day-of-month for axis ticks; the ISO date is preserved in the // tooltip title via the closure on `days`. const labels = days.map((iso) => { const d = new Date(iso + 'T00:00:00Z'); return isNaN(d.getTime()) ? iso : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); }); const datasets = series.map((row, i) => { const colour = PIE_COLORS[i % PIE_COLORS.length]; return { label: row.category || '—', data: Array.isArray(row.counts) ? row.counts.map((c) => c || 0) : [], backgroundColor: hexToRgba(colour, 0.7), // frosted fill borderColor: colour, // crisp glass edge borderWidth: 1, }; }); const t = chartTheme(); new Chart(canvas, { type: 'bar', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: t.legendColor, boxWidth: 12, font: { size: 11 } }, }, tooltip: { callbacks: { title: (items) => (items[0] && days[items[0].dataIndex]) || '', }, }, }, scales: { x: { stacked: true, ticks: { color: t.tickColor }, grid: { color: t.gridColor } }, y: { stacked: true, ticks: { color: t.tickColor, precision: 0 }, grid: { color: t.gridColor }, beginAtZero: true, }, }, }, }); } document.addEventListener('DOMContentLoaded', () => { renderReportsChart(); renderPieChart('top-reporters-chart'); renderPieChart('top-categories-chart'); renderBlockedIpsChart(); }); // Locale-aware