add administrator notification bell

This commit is contained in:
2026-10-25 04:53:31 +01:00
parent 0c684d4831
commit aa16fc29d3
2 changed files with 92 additions and 1 deletions
+28 -1
View File
@@ -16,6 +16,8 @@
let realtimeState = 'DISCONNECTED';
let realtimeSocket: WebSocket | undefined;
let activityEvents: Array<{ event_id: string; event_type: string; occurred_at: string }> = [];
let notifications: Array<{ id: string; severity: string; title: string; message: string; seen_at: string | null; created_at: string }> = [];
let showNotifications = false;
let showStreamEditor = false;
let streamBusy = false;
let streamError = '';
@@ -89,6 +91,7 @@
mustReplaceCredentials = result.must_replace_credentials;
connectionState = 'CONNECTED';
await loadActivity();
await loadNotifications();
await loadStreamInfo();
connectRealtime();
if (!mustReplaceCredentials) password = '';
@@ -136,6 +139,8 @@
authenticated = false;
mustReplaceCredentials = false;
oauthLink = '';
notifications = [];
showNotifications = false;
}
function connectRealtime() {
@@ -182,6 +187,28 @@
}
}
async function loadNotifications() {
try {
const response = await fetch(`${serverAddress}/api/v1/admin/notifications`, {
headers: { authorization: `Bearer ${sessionToken}` },
});
if (!response.ok) return;
notifications = await response.json();
} catch {
notifications = [];
}
}
async function markNotificationSeen(notificationId: string) {
const response = await fetch(`${serverAddress}/api/v1/admin/notifications/${notificationId}/seen`, {
method: 'POST',
headers: { authorization: `Bearer ${sessionToken}` },
});
if (response.ok) {
notifications = notifications.map((notification) => notification.id === notificationId ? { ...notification, seen_at: new Date().toISOString() } : notification);
}
}
async function loadStreamInfo() {
try {
const response = await fetch(`${serverAddress}/api/v1/twitch/stream-info`, {
@@ -319,7 +346,7 @@
<button class:active={activeView === item} class="nav-item" onclick={() => (activeView = item)}>{item}</button>
{/each}
</nav>
<div class="app-actions"><span class:online={connectionState === 'CONNECTED'} class="connection-state">{connectionState}</span>{#if connectionNotice}<span class="connection-notice">{connectionNotice}</span>{/if}<button class="icon-button" aria-label="Settings">⚙</button></div>
<div class="app-actions"><span class:online={connectionState === 'CONNECTED'} class="connection-state">{connectionState}</span>{#if connectionNotice}<span class="connection-notice">{connectionNotice}</span>{/if}{#if authenticated}<div class="notification-wrap"><button class="icon-button notification-button" aria-label="Notifications" onclick={() => (showNotifications = !showNotifications)}>♢{#if notifications.some((notification) => !notification.seen_at)}<span class="notification-dot"></span>{/if}</button>{#if showNotifications}<div class="notification-popover"><div class="notification-heading"><span>NOTIFICATIONS</span><button class="secondary" onclick={loadNotifications}>↻</button></div>{#if notifications.length}{#each notifications as notification}<div class:unseen={!notification.seen_at} class="notification-item"><div class="notification-title"><span>{notification.title}</span><span class="micro">{notification.severity}</span></div><div class="notification-message">{notification.message}</div>{#if !notification.seen_at}<button class="secondary-action" onclick={() => markNotificationSeen(notification.id)}>MARK SEEN</button>{/if}</div>{/each}{:else}<div class="notification-empty">No notifications.</div>{/if}</div>{/if}</div>{/if}<button class="icon-button" aria-label="Settings">⚙</button></div>
</header>
<main class="workspace">
+64
View File
@@ -174,6 +174,70 @@ button:hover:not(:disabled) {
color: var(--ed-dim);
font-size: 18px;
}
.notification-wrap {
position: relative;
}
.notification-button {
position: relative;
}
.notification-dot {
position: absolute;
top: 2px;
right: 2px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ed-warn);
}
.notification-popover {
position: absolute;
z-index: 5;
top: 34px;
right: 0;
width: 320px;
max-height: 420px;
overflow: auto;
border: 1px solid var(--ui-line);
background: var(--ed-panel);
box-shadow: 0 8px 24px rgb(0 0 0 / 35%);
}
.notification-heading,
.notification-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.notification-heading {
padding: 9px 10px;
border-bottom: 1px solid var(--ui-divider);
color: var(--ed-fg);
font: 600 11px var(--font-data);
letter-spacing: 0.12em;
}
.notification-item {
padding: 10px;
border-bottom: 1px solid var(--ui-divider);
opacity: 0.65;
}
.notification-item.unseen {
opacity: 1;
background: var(--ui-surface);
}
.notification-title {
color: var(--ed-fg);
font-weight: 600;
}
.notification-message {
margin: 6px 0 9px;
color: var(--ed-dim);
font-size: 11px;
line-height: 1.4;
}
.notification-empty {
padding: 18px 10px;
color: var(--ed-dim);
font: 11px var(--font-data);
}
.workspace {
display: grid;
grid-template-columns: 214px 1fr;