Files
flipstone-radar/src/services/notifications.ts
2025-07-10 21:59:56 -04:00

156 lines
4.0 KiB
TypeScript

import { WorkflowRun } from '../types/github';
export class NotificationService {
private static instance: NotificationService;
private hasPermission = false;
private constructor() {
this.checkPermission();
}
static getInstance(): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService();
}
return NotificationService.instance;
}
private checkPermission(): void {
if (!('Notification' in window)) {
console.log('This browser does not support desktop notification');
return;
}
this.hasPermission = Notification.permission === 'granted';
}
async requestPermission(): Promise<boolean> {
if (!('Notification' in window)) {
console.log('This browser does not support desktop notification');
return false;
}
if (Notification.permission === 'granted') {
this.hasPermission = true;
return true;
}
if (Notification.permission !== 'denied') {
const permission = await Notification.requestPermission();
this.hasPermission = permission === 'granted';
return this.hasPermission;
}
return false;
}
canShowNotifications(): boolean {
return this.hasPermission && 'Notification' in window;
}
showFailureNotification(failedRuns: WorkflowRun[]): void {
if (!this.canShowNotifications()) {
return;
}
const count = failedRuns.length;
const title = count === 1
? 'Build Failed'
: `${count} Builds Failed`;
const body = count === 1
? `${failedRuns[0].repository.full_name} - ${failedRuns[0].display_title}`
: `Multiple builds have failed across ${new Set(failedRuns.map(r => r.repository.full_name)).size} repositories`;
const notification = new Notification(title, {
body,
icon: '/favicon.ico',
badge: '/favicon.ico',
tag: 'build-failure',
renotify: true,
requireInteraction: false,
silent: false
});
// Auto-close after 5 seconds
setTimeout(() => {
notification.close();
}, 5000);
// Optional: Click to focus window
notification.onclick = () => {
window.focus();
notification.close();
};
}
showSuccessNotification(successRuns: WorkflowRun[]): void {
if (!this.canShowNotifications()) {
return;
}
const count = successRuns.length;
const title = count === 1
? 'Build Recovered'
: `${count} Builds Recovered`;
const body = count === 1
? `${successRuns[0].repository.full_name} - ${successRuns[0].display_title}`
: `Multiple builds have recovered across ${new Set(successRuns.map(r => r.repository.full_name)).size} repositories`;
const notification = new Notification(title, {
body,
icon: '/favicon.ico',
badge: '/favicon.ico',
tag: 'build-success',
renotify: true,
requireInteraction: false,
silent: false
});
// Auto-close after 3 seconds
setTimeout(() => {
notification.close();
}, 3000);
notification.onclick = () => {
window.focus();
notification.close();
};
}
showWaitingNotification(waitingRuns: WorkflowRun[]): void {
if (!this.canShowNotifications()) {
return;
}
const count = waitingRuns.length;
const title = count === 1
? 'Build Waiting'
: `${count} Builds Waiting`;
const body = count === 1
? `${waitingRuns[0].repository.full_name} - ${waitingRuns[0].display_title}`
: `Multiple builds are now waiting across ${new Set(waitingRuns.map(r => r.repository.full_name)).size} repositories`;
const notification = new Notification(title, {
body,
icon: '/favicon.ico',
badge: '/favicon.ico',
tag: 'build-waiting',
renotify: true,
requireInteraction: false,
silent: false
});
// Auto-close after 4 seconds
setTimeout(() => {
notification.close();
}, 4000);
notification.onclick = () => {
window.focus();
notification.close();
};
}
}