feat: enhance weekly report and realtime notifications
Weekly Report (fix-weekly-report): - Remove 5-task limit, show all tasks per category - Add blocked tasks with blocker_reason and blocked_since - Add next week tasks (due in coming week) - Add assignee_name, completed_at, days_overdue to task details - Frontend collapsible sections for each task category - 8 new tests for enhanced report content Realtime Notifications (fix-realtime-notifications): - SQLAlchemy event-based notification publishing - Redis Pub/Sub for multi-process broadcast - Fix soft rollback handler stacking issue - Fix ping scheduling drift (send immediately when interval expires) - Frontend NotificationContext with WebSocket reconnection Spec Fixes: - Add missing ## Purpose sections to 5 specs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,8 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const refreshUnreadCount = useCallback(async () => {
|
||||
try {
|
||||
@@ -79,8 +79,15 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) return
|
||||
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws/notifications?token=${token}`
|
||||
// Use env var if available, otherwise derive from current location
|
||||
let wsUrl: string
|
||||
const envWsUrl = import.meta.env.VITE_WS_URL
|
||||
if (envWsUrl) {
|
||||
wsUrl = `${envWsUrl}/ws/notifications?token=${token}`
|
||||
} else {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
wsUrl = `${wsProtocol}//${window.location.host}/ws/notifications?token=${token}`
|
||||
}
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(wsUrl)
|
||||
@@ -101,6 +108,32 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
const message = JSON.parse(event.data)
|
||||
|
||||
switch (message.type) {
|
||||
case 'connected':
|
||||
console.log('WebSocket authenticated:', message.data.message)
|
||||
break
|
||||
|
||||
case 'unread_sync':
|
||||
// Merge unread notifications without removing already-loaded notifications
|
||||
setNotifications(prev => {
|
||||
const unreadNotifications = message.data.notifications || []
|
||||
const existingIds = new Set(prev.map(n => n.id))
|
||||
|
||||
// Add new unread notifications that don't exist in current list
|
||||
const newNotifications = unreadNotifications.filter(
|
||||
(n: Notification) => !existingIds.has(n.id)
|
||||
)
|
||||
|
||||
// Update existing unread notifications and prepend new ones
|
||||
const updated = prev.map(existing => {
|
||||
const fromSync = unreadNotifications.find((n: Notification) => n.id === existing.id)
|
||||
return fromSync || existing
|
||||
})
|
||||
|
||||
return [...newNotifications, ...updated]
|
||||
})
|
||||
setUnreadCount(message.data.unread_count || 0)
|
||||
break
|
||||
|
||||
case 'notification':
|
||||
// Add new notification to the top
|
||||
setNotifications(prev => [message.data, ...prev])
|
||||
@@ -111,6 +144,13 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
setUnreadCount(message.data.unread_count)
|
||||
break
|
||||
|
||||
case 'ping':
|
||||
// Server ping - respond with pong
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'pong' }))
|
||||
}
|
||||
break
|
||||
|
||||
case 'pong':
|
||||
// Pong received, connection is alive
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user