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

@ -10,7 +10,6 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/manifest.json">
<title>Antigravity Agent Dashboard</title>
<link rel="stylesheet" href="/src/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
@ -145,6 +144,9 @@
<button id="rerun-btn" class="btn btn-ghost">
<span></span> Re-run Task
</button>
<button id="notify-task-btn" class="btn btn-ghost">
<span class="notify-icon">🔔</span> Notify Me
</button>
</div>
</header>

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);
});

View file

@ -1101,6 +1101,37 @@ textarea:focus {
}
/* Responsive Styles */
#notify-task-btn.notified {
color: var(--primary);
background: var(--primary-glow);
}
#notify-task-btn.notified .notify-icon {
animation: ring 0.5s ease;
}
@keyframes ring {
0% {
transform: rotate(0);
}
25% {
transform: rotate(15deg);
}
50% {
transform: rotate(-15deg);
}
75% {
transform: rotate(10deg);
}
100% {
transform: rotate(0);
}
}
@media (max-width: 768px) {
.menu-toggle {
display: flex;

View file

@ -24,3 +24,28 @@ self.addEventListener('fetch', (event) => {
})
);
});
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : { title: 'Notification', body: 'New update from Agency' };
const options = {
body: data.body,
icon: '/icon-192.png',
badge: '/icon-192.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: '1'
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});