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:
beabigegg
2025-12-30 20:52:08 +08:00
parent 10db2c9d1f
commit 64874d5425
25 changed files with 1034 additions and 140 deletions

View File

@@ -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