import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; import { Chart, registerables } from 'chart.js'; import Sortable from 'sortablejs'; Chart.register(...registerables); // Drag-to-reorder tile grid → persists order via $wire.reorder([uuid, …]). document.addEventListener('alpine:init', () => { window.Alpine.data('sortableGrid', () => ({ init() { Sortable.create(this.$refs.grid, { handle: '.drag-handle', animation: 150, ghostClass: 'opacity-40', onEnd: () => { const order = [...this.$refs.grid.children].map((c) => c.dataset.uuid).filter(Boolean); this.$wire.reorder(order); }, }); }, })); }); // Themed line-chart island (Livewire bundles Alpine). Usage: //
// where $config = { labels: [...], series: [{ key, label, color, data, axis }], units: {...} } document.addEventListener('alpine:init', () => { window.Alpine.data('lineChart', (config) => ({ chart: null, init() { const css = getComputedStyle(document.documentElement); const token = (name, fallback) => (css.getPropertyValue(name).trim() || fallback); const grid = token('--color-line', '#1C2846'); const ink = token('--color-ink-3', '#5E6C8A'); this.chart = new Chart(this.$refs.canvas, { type: 'line', data: { labels: config.labels, datasets: config.series.map((s) => ({ label: s.label, data: s.data, borderColor: token(s.color, '#4FC1FF'), backgroundColor: 'transparent', borderWidth: 2, tension: 0.35, pointRadius: 0, yAxisID: s.axis || 'y', })), }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: ink, boxWidth: 10, boxHeight: 10, usePointStyle: true } } }, scales: { x: { ticks: { color: ink, maxTicksLimit: 6 }, grid: { color: grid } }, y: { position: 'left', ticks: { color: ink }, grid: { color: grid }, beginAtZero: true }, y1: { position: 'right', ticks: { color: ink }, grid: { drawOnChartArea: false }, beginAtZero: true, max: 100 }, }, }, }); }, destroy() { this.chart?.destroy(); }, })); }); // Reverb (Pusher protocol), proxied same-origin through nginx (/app, /apps). // The browser connects to the page's own host/port — works on localhost and over the LAN // with no separate WS port and no host mismatch. Every channel is PRIVATE (/broadcasting/auth). const loc = window.location; const isHttps = loc.protocol === 'https:'; const port = loc.port ? Number(loc.port) : (isHttps ? 443 : 80); window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: loc.hostname, wsPort: port, wssPort: port, forceTLS: isHttps, enabledTransports: ['ws', 'wss'], }); // Register the PWA service worker so HomeOS installs on tablets/phones and survives // brief server blips (offline shell). sw.js lives at the web root for a "/" scope. if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => {}); }); }