This commit is contained in:
parent
cc8fcd682b
commit
91db5861c8
21 changed files with 1263 additions and 79 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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('/')
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue