diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/app.js | 127 | ||||
| -rw-r--r-- | web/index.html | 5 | ||||
| -rw-r--r-- | web/sw.js | 14 |
3 files changed, 146 insertions, 0 deletions
@@ -1165,6 +1165,9 @@ function renderActiveTab(allTasks) { .then(execs => renderStatsPanel(allTasks, execs)) .catch(() => {}); break; + case 'drops': + renderDropsPanel(); + break; case 'settings': renderSettingsPanel(); break; @@ -2591,6 +2594,124 @@ function renderStatsPanel(tasks, executions) { panel.appendChild(execSection); } +// ── Web Push Notifications ──────────────────────────────────────────────────── + +async function registerServiceWorker() { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) return null; + return navigator.serviceWorker.register('/sw.js'); +} + +function urlBase64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); + const rawData = window.atob(base64); + return Uint8Array.from([...rawData].map(c => c.charCodeAt(0))); +} + +async function enableNotifications(btn) { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) { + alert('Push notifications are not supported in this browser.'); + return; + } + try { + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + alert('Notification permission denied.'); + return; + } + + // Fetch VAPID public key. + const keyRes = await fetch(`${API_BASE}/api/push/vapid-key`); + if (!keyRes.ok) throw new Error(`Failed to get VAPID key: HTTP ${keyRes.status}`); + const { public_key: vapidKey } = await keyRes.json(); + + // Register service worker. + const registration = await registerServiceWorker(); + if (!registration) { + alert('Service worker registration failed.'); + return; + } + + // Subscribe via PushManager. + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(vapidKey), + }); + + const subJSON = subscription.toJSON(); + // POST subscription to server. + const res = await fetch(`${API_BASE}/api/push/subscribe`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + endpoint: subJSON.endpoint, + keys: { p256dh: subJSON.keys.p256dh, auth: subJSON.keys.auth }, + }), + }); + if (!res.ok) throw new Error(`Subscribe failed: HTTP ${res.status}`); + + if (btn) { + btn.textContent = '🔔 On'; + btn.disabled = true; + } + } catch (err) { + alert(`Notification setup failed: ${err.message}`); + } +} + +// ── File Drops ───────────────────────────────────────────────────────────────── + +async function fetchDrops() { + const res = await fetch(`${API_BASE}/api/drops`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +async function renderDropsPanel() { + const panel = document.querySelector('[data-panel="drops"] .drops-panel'); + if (!panel) return; + panel.innerHTML = '<p class="task-meta">Loading drops…</p>'; + + try { + const files = await fetchDrops(); + panel.innerHTML = ''; + + const heading = document.createElement('h3'); + heading.style.padding = '1rem 1rem 0.5rem'; + heading.textContent = 'Dropped Files'; + panel.appendChild(heading); + + if (files.length === 0) { + const empty = document.createElement('p'); + empty.className = 'task-meta'; + empty.style.padding = '0 1rem'; + empty.textContent = 'No files dropped yet. Agents can write files to the drops directory to share them here.'; + panel.appendChild(empty); + } else { + const list = document.createElement('ul'); + list.style.cssText = 'list-style:none;padding:0 1rem;margin:0'; + for (const f of files) { + const li = document.createElement('li'); + li.style.cssText = 'padding:0.5rem 0;border-bottom:1px solid var(--border,#e5e7eb)'; + const a = document.createElement('a'); + a.href = `${API_BASE}/api/drops/${encodeURIComponent(f.name)}`; + a.textContent = f.name; + a.download = f.name; + a.style.cssText = 'color:var(--accent,#2563eb);text-decoration:none'; + const meta = document.createElement('span'); + meta.className = 'task-meta'; + meta.style.cssText = 'margin-left:1rem'; + meta.textContent = `${(f.size / 1024).toFixed(1)} KB`; + li.append(a, meta); + list.appendChild(li); + } + panel.appendChild(list); + } + } catch (err) { + panel.innerHTML = `<p class="task-meta" style="padding:1rem">Failed to load drops: ${err.message}</p>`; + } +} + // ── Tab switching ───────────────────────────────────────────────────────────── function switchTab(name) { @@ -2642,6 +2763,12 @@ if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal); initProjectSelect(); + // Push notifications button + const btnNotify = document.getElementById('btn-notifications'); + if (btnNotify) { + btnNotify.addEventListener('click', () => enableNotifications(btnNotify)); + } + // Validate button document.getElementById('btn-validate').addEventListener('click', async () => { const btn = document.getElementById('btn-validate'); diff --git a/web/index.html b/web/index.html index 1746baf..64dd486 100644 --- a/web/index.html +++ b/web/index.html @@ -17,6 +17,7 @@ <option value="claude">Claude</option> <option value="gemini">Gemini</option> </select> + <button id="btn-notifications" class="btn-secondary" title="Enable push notifications">🔔 Notifications</button> <button id="btn-start-next" class="btn-secondary">Start Next</button> <button id="btn-new-task" class="btn-primary">New Task</button> </div> @@ -27,6 +28,7 @@ <button class="tab" data-tab="ready" title="Ready">✅<span class="tab-count-badge" hidden></span></button> <button class="tab" data-tab="running" title="Running">▶️<span class="tab-count-badge" hidden></span></button> <button class="tab" data-tab="all" title="All">☰<span class="tab-count-badge" hidden></span></button> + <button class="tab" data-tab="drops" title="Drops">📁</button> <button class="tab" data-tab="stats" title="Stats">📊</button> <button class="tab" data-tab="settings" title="Settings">⚙️</button> </nav> @@ -49,6 +51,9 @@ <div data-panel="all" hidden> <div class="all-history"></div> </div> + <div data-panel="drops" hidden> + <div class="drops-panel"></div> + </div> <div data-panel="stats" hidden></div> <div data-panel="settings" hidden> <p class="task-meta" style="padding:1rem">Settings coming soon.</p> diff --git a/web/sw.js b/web/sw.js new file mode 100644 index 0000000..09b53a6 --- /dev/null +++ b/web/sw.js @@ -0,0 +1,14 @@ +self.addEventListener('push', function(event) { + const data = event.data ? event.data.json() : {}; + const title = data.title || 'Claudomator'; + const options = { + body: data.body || '', + tag: data.tag || 'claudomator', + }; + event.waitUntil(self.registration.showNotification(title, options)); +}); + +self.addEventListener('notificationclick', function(event) { + event.notification.close(); + event.waitUntil(clients.openWindow('/')); +}); |
