feat: notifikasi real-time bug baru via WebSocket
- presence.ts: tambah notifSubs (ADMIN+DEVELOPER) dan broadcastNotification - app.ts: broadcast new_bug event setelah bug dibuat, update WS handler - usePresence: terima callback onNewBug, expose NewBugPayload type - DashboardLayout: pasang usePresence, tampilkan Mantine notification saat bug baru masuk
This commit is contained in:
20
src/app.ts
20
src/app.ts
@@ -8,7 +8,7 @@ import { prisma } from './lib/db'
|
||||
import { env } from './lib/env'
|
||||
import { createSystemLog } from './lib/logger'
|
||||
import { getMinioDownloadUrl, uploadBugImage } from './lib/minio'
|
||||
import { addConnection, broadcastToAdmins, getOnlineUserIds, removeConnection } from './lib/presence'
|
||||
import { addConnection, broadcastNotification, broadcastToAdmins, getOnlineUserIds, removeConnection } from './lib/presence'
|
||||
import { parseSchema } from './lib/schema-parser'
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
@@ -921,6 +921,18 @@ export function createApp() {
|
||||
},
|
||||
})
|
||||
|
||||
broadcastNotification({
|
||||
type: 'new_bug',
|
||||
bug: {
|
||||
id: bug.id,
|
||||
description: bug.description,
|
||||
appId: bug.appId,
|
||||
source: bug.source,
|
||||
affectedVersion: bug.affectedVersion,
|
||||
createdAt: bug.createdAt,
|
||||
},
|
||||
})
|
||||
|
||||
return bug
|
||||
}, {
|
||||
body: t.Object({
|
||||
@@ -1241,9 +1253,11 @@ export function createApp() {
|
||||
include: { user: { select: { id: true, role: true } } },
|
||||
})
|
||||
if (!session || session.expiresAt < new Date()) { ws.close(4001, 'Unauthorized'); return }
|
||||
const isAdmin = session.user.role === 'DEVELOPER'
|
||||
const role = session.user.role
|
||||
const isAdmin = role === 'DEVELOPER'
|
||||
const canReceiveNotifs = role === 'DEVELOPER' || role === 'ADMIN'
|
||||
;(ws.data as unknown as { userId: string }).userId = session.user.id
|
||||
addConnection(ws as any, session.user.id, isAdmin)
|
||||
addConnection(ws as any, session.user.id, isAdmin, canReceiveNotifs)
|
||||
},
|
||||
close(ws) { removeConnection(ws as any) },
|
||||
message() {},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { APP_CONFIGS } from '@/frontend/config/appMenus'
|
||||
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
|
||||
import { usePresence } from '@/frontend/hooks/usePresence'
|
||||
import React from 'react'
|
||||
import {
|
||||
ActionIcon,
|
||||
@@ -24,12 +25,14 @@ import {
|
||||
useMantineColorScheme
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router'
|
||||
import {
|
||||
TbAlertTriangle,
|
||||
TbApps,
|
||||
TbArrowLeft,
|
||||
TbBug,
|
||||
TbChevronRight,
|
||||
TbClock,
|
||||
TbDashboard,
|
||||
@@ -64,6 +67,20 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const user = sessionData?.user
|
||||
const logout = useLogout()
|
||||
|
||||
// ─── Real-time bug notifications ─────────────────────
|
||||
usePresence((bug) => {
|
||||
const appLabel = bug.appId ? bug.appId.toUpperCase() : 'Unknown App'
|
||||
notifications.show({
|
||||
id: `new-bug-${bug.id}`,
|
||||
title: `New bug report — ${appLabel}`,
|
||||
message: bug.description.length > 80 ? `${bug.description.slice(0, 80)}…` : bug.description,
|
||||
color: 'red',
|
||||
icon: React.createElement(TbBug, { size: 18 }),
|
||||
autoClose: 8000,
|
||||
withBorder: true,
|
||||
})
|
||||
})
|
||||
|
||||
// Redirect USER role to profile (pending approval)
|
||||
React.useEffect(() => {
|
||||
if (!sessionLoading && user?.role === 'USER') {
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useSession } from './useAuth'
|
||||
|
||||
export function usePresence() {
|
||||
export interface NewBugPayload {
|
||||
id: string
|
||||
description: string
|
||||
appId: string | null
|
||||
source: string
|
||||
affectedVersion: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export function usePresence(onNewBug?: (bug: NewBugPayload) => void) {
|
||||
const { data } = useSession()
|
||||
const [onlineUserIds, setOnlineUserIds] = useState<string[]>([])
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
const onNewBugRef = useRef(onNewBug)
|
||||
onNewBugRef.current = onNewBug
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.user) return
|
||||
@@ -18,6 +29,7 @@ export function usePresence() {
|
||||
ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data)
|
||||
if (msg.type === 'presence') setOnlineUserIds(msg.online)
|
||||
if (msg.type === 'new_bug') onNewBugRef.current?.(msg.bug)
|
||||
}
|
||||
ws.onclose = () => {
|
||||
wsRef.current = null
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ServerWebSocket } from 'bun'
|
||||
|
||||
const connections = new Map<string, Set<ServerWebSocket<{ userId: string }>>>()
|
||||
const adminSubs = new Set<ServerWebSocket<{ userId: string }>>()
|
||||
const notifSubs = new Set<ServerWebSocket<{ userId: string }>>()
|
||||
|
||||
export function getOnlineUserIds(): string[] {
|
||||
return Array.from(connections.keys())
|
||||
@@ -13,7 +14,12 @@ function broadcast() {
|
||||
for (const ws of adminSubs) ws.send(msg)
|
||||
}
|
||||
|
||||
export function addConnection(ws: ServerWebSocket<{ userId: string }>, userId: string, isAdmin: boolean) {
|
||||
export function addConnection(
|
||||
ws: ServerWebSocket<{ userId: string }>,
|
||||
userId: string,
|
||||
isAdmin: boolean,
|
||||
canReceiveNotifs: boolean,
|
||||
) {
|
||||
let set = connections.get(userId)
|
||||
if (!set) {
|
||||
set = new Set()
|
||||
@@ -24,6 +30,7 @@ export function addConnection(ws: ServerWebSocket<{ userId: string }>, userId: s
|
||||
adminSubs.add(ws)
|
||||
ws.send(JSON.stringify({ type: 'presence', online: getOnlineUserIds() }))
|
||||
}
|
||||
if (canReceiveNotifs) notifSubs.add(ws)
|
||||
broadcast()
|
||||
}
|
||||
|
||||
@@ -32,6 +39,11 @@ export function broadcastToAdmins(message: object) {
|
||||
for (const ws of adminSubs) ws.send(msg)
|
||||
}
|
||||
|
||||
export function broadcastNotification(message: object) {
|
||||
const msg = JSON.stringify(message)
|
||||
for (const ws of notifSubs) ws.send(msg)
|
||||
}
|
||||
|
||||
export function removeConnection(ws: ServerWebSocket<{ userId: string }>) {
|
||||
const userId = ws.data.userId
|
||||
const set = connections.get(userId)
|
||||
@@ -40,5 +52,6 @@ export function removeConnection(ws: ServerWebSocket<{ userId: string }>) {
|
||||
if (set.size === 0) connections.delete(userId)
|
||||
}
|
||||
adminSubs.delete(ws)
|
||||
notifSubs.delete(ws)
|
||||
broadcast()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user