add administrator notification bell
This commit is contained in:
+28
-1
@@ -16,6 +16,8 @@
|
|||||||
let realtimeState = 'DISCONNECTED';
|
let realtimeState = 'DISCONNECTED';
|
||||||
let realtimeSocket: WebSocket | undefined;
|
let realtimeSocket: WebSocket | undefined;
|
||||||
let activityEvents: Array<{ event_id: string; event_type: string; occurred_at: string }> = [];
|
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 showStreamEditor = false;
|
||||||
let streamBusy = false;
|
let streamBusy = false;
|
||||||
let streamError = '';
|
let streamError = '';
|
||||||
@@ -89,6 +91,7 @@
|
|||||||
mustReplaceCredentials = result.must_replace_credentials;
|
mustReplaceCredentials = result.must_replace_credentials;
|
||||||
connectionState = 'CONNECTED';
|
connectionState = 'CONNECTED';
|
||||||
await loadActivity();
|
await loadActivity();
|
||||||
|
await loadNotifications();
|
||||||
await loadStreamInfo();
|
await loadStreamInfo();
|
||||||
connectRealtime();
|
connectRealtime();
|
||||||
if (!mustReplaceCredentials) password = '';
|
if (!mustReplaceCredentials) password = '';
|
||||||
@@ -136,6 +139,8 @@
|
|||||||
authenticated = false;
|
authenticated = false;
|
||||||
mustReplaceCredentials = false;
|
mustReplaceCredentials = false;
|
||||||
oauthLink = '';
|
oauthLink = '';
|
||||||
|
notifications = [];
|
||||||
|
showNotifications = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectRealtime() {
|
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() {
|
async function loadStreamInfo() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${serverAddress}/api/v1/twitch/stream-info`, {
|
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>
|
<button class:active={activeView === item} class="nav-item" onclick={() => (activeView = item)}>{item}</button>
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</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>
|
</header>
|
||||||
|
|
||||||
<main class="workspace">
|
<main class="workspace">
|
||||||
|
|||||||
@@ -174,6 +174,70 @@ button:hover:not(:disabled) {
|
|||||||
color: var(--ed-dim);
|
color: var(--ed-dim);
|
||||||
font-size: 18px;
|
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 {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 214px 1fr;
|
grid-template-columns: 214px 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user