push notifications
All checks were successful
/ upload (release) Successful in 1m4s

This commit is contained in:
pavel 2026-02-12 01:28:24 +01:00
commit 91db5861c8
21 changed files with 1263 additions and 79 deletions

View file

@ -1,5 +1,6 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import './style.css';
const API_URL = '/api';
// These should ideally be environment-specific
@ -244,6 +245,19 @@ function selectTask(id, runId = null) {
renderRunHistory(task);
showTaskView(task);
// Fetch and update subscription status
fetch(`${API_URL}/tasks/${id}/subscription`)
.then(res => res.json())
.then(data => {
const btn = document.getElementById('notify-task-btn');
if (data.isSubscribed) {
btn.classList.add('notified');
} else {
btn.classList.remove('notified');
}
})
.catch(err => console.error('Failed to fetch subscription status', err));
// Close sidebar on mobile after selection
if (window.innerWidth <= 768) {
closeMobileMenu();
@ -802,7 +816,68 @@ if (window.location.pathname === '/callback' || window.location.search.includes(
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered', reg))
.then(reg => {
console.log('SW registered', reg);
setupPush(reg);
})
.catch(err => console.error('SW registration failed', err));
});
}
async function setupPush(registration) {
try {
const vapidResponse = await fetch(`${API_URL}/notifications/vapid-key`);
const { publicKey } = await vapidResponse.json();
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
await fetch(`${API_URL}/notifications/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
endpoint: subscription.endpoint,
p256dh: b64(subscription.getKey('p256dh')),
auth: b64(subscription.getKey('auth'))
})
});
console.log('Push registered');
} catch (err) {
console.warn('Push registration failed:', err);
}
}
function b64(buffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
async function toggleTaskSubscription(taskId) {
const btn = document.getElementById('notify-task-btn');
const isNotified = btn.classList.contains('notified');
const method = isNotified ? 'DELETE' : 'POST';
try {
await fetch(`${API_URL}/tasks/${taskId}/subscribe`, { method });
btn.classList.toggle('notified');
showToast(isNotified ? 'Notifications disabled' : 'Notifications enabled');
} catch (err) {
showToast('Failed to update notifications');
}
}
document.getElementById('notify-task-btn').addEventListener('click', () => {
if (state.selectedTaskId) toggleTaskSubscription(state.selectedTaskId);
});