feat(phase5.1+6): fallback material cleanup + notification batch refactor
Phase 5.1 — MATERIAL_PALETTE removal:
- Remove MATERIAL_PALETTE + _material_to_color() from step_processor.py
- build_part_colors() now returns {part→material_name} for Blender resolver
Phase 6 — Notification Center Refactor:
- Migration 051: add channel (activity|notification|alert) to audit_log,
add frequency (immediate|daily|never) to notification_configs
- Three notification channels: activity (per-render), notification (batch
order summaries), alert (admin infrastructure)
- Per-render emit_notification_sync calls demoted to channel=activity
- New emit_batch_render_notification_sync(): single summary notification
when all order lines reach terminal state ("47/50 succeeded, 3 failed")
- Beat task batch_render_notifications every 60s: safety-net for missed
batch notifications after order completion
- GET /notifications: defaults to channel IN (notification, alert);
accepts ?channel=activity for activity feed
- Unread count badge counts only notification+alert channels
- Notifications.tsx: three tabs (Notifications | Activity | Alerts)
- NotificationSettings.tsx: frequency dropdown per event type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,13 +6,14 @@ import {
|
||||
updateNotificationConfig,
|
||||
resetNotificationConfigs,
|
||||
type NotificationConfig,
|
||||
type NotificationFrequency,
|
||||
} from '../api/notifications'
|
||||
|
||||
const EVENT_LABELS: Record<string, string> = {
|
||||
'order.submitted': 'Order submitted',
|
||||
'order.completed': 'Order completed',
|
||||
'render.completed': 'Render completed',
|
||||
'render.failed': 'Render failed',
|
||||
'render.completed': 'Render completed (activity)',
|
||||
'render.failed': 'Render failed (activity)',
|
||||
'excel.imported': 'Excel imported',
|
||||
}
|
||||
|
||||
@@ -22,6 +23,12 @@ const CHANNELS: Array<{ key: 'in_app' | 'email'; label: string; comingSoon?: boo
|
||||
{ key: 'email', label: 'E-Mail', comingSoon: true },
|
||||
]
|
||||
|
||||
const FREQUENCY_OPTIONS: Array<{ value: string; label: string }> = [
|
||||
{ value: 'immediate', label: 'Immediate' },
|
||||
{ value: 'daily', label: 'Daily summary' },
|
||||
{ value: 'never', label: 'Disabled' },
|
||||
]
|
||||
|
||||
function Toggle({
|
||||
enabled,
|
||||
disabled,
|
||||
@@ -61,8 +68,8 @@ export default function NotificationSettingsPage() {
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ eventType, channel, enabled }: { eventType: string; channel: string; enabled: boolean }) =>
|
||||
updateNotificationConfig(eventType, channel, enabled),
|
||||
mutationFn: ({ eventType, channel, enabled, frequency }: { eventType: string; channel: string; enabled: boolean; frequency?: NotificationFrequency }) =>
|
||||
updateNotificationConfig(eventType, channel, enabled, frequency),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['notification-configs'] }),
|
||||
onError: () => toast.error('Failed to update setting'),
|
||||
})
|
||||
@@ -76,15 +83,20 @@ export default function NotificationSettingsPage() {
|
||||
onError: () => toast.error('Failed to reset settings'),
|
||||
})
|
||||
|
||||
// Build lookup map: eventType+channel → enabled
|
||||
// Build lookup maps
|
||||
const configMap = new Map<string, boolean>()
|
||||
const frequencyMap = new Map<string, NotificationFrequency>()
|
||||
for (const c of configs) {
|
||||
configMap.set(`${c.event_type}:${c.channel}`, c.enabled)
|
||||
frequencyMap.set(`${c.event_type}:${c.channel}`, c.frequency ?? 'immediate')
|
||||
}
|
||||
|
||||
const isEnabled = (event: string, channel: string) =>
|
||||
configMap.get(`${event}:${channel}`) ?? (channel === 'in_app')
|
||||
|
||||
const getFrequency = (event: string, channel: string): NotificationFrequency =>
|
||||
frequencyMap.get(`${event}:${channel}`) ?? 'immediate'
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-5 max-w-2xl">
|
||||
{/* Header */}
|
||||
@@ -127,12 +139,15 @@ export default function NotificationSettingsPage() {
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
<th className="px-4 py-3 text-center text-xs font-semibold text-content-muted uppercase tracking-wide w-40">
|
||||
Frequency
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-default">
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={CHANNELS.length + 1} className="px-4 py-8 text-center text-sm text-content-muted">
|
||||
<td colSpan={CHANNELS.length + 2} className="px-4 py-8 text-center text-sm text-content-muted">
|
||||
Loading...
|
||||
</td>
|
||||
</tr>
|
||||
@@ -154,6 +169,25 @@ export default function NotificationSettingsPage() {
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td className="px-4 py-3 text-center">
|
||||
<select
|
||||
value={getFrequency(event, 'in_app')}
|
||||
disabled={updateMutation.isPending || !isEnabled(event, 'in_app')}
|
||||
onChange={e =>
|
||||
updateMutation.mutate({
|
||||
eventType: event,
|
||||
channel: 'in_app',
|
||||
enabled: isEnabled(event, 'in_app'),
|
||||
frequency: e.target.value as NotificationFrequency,
|
||||
})
|
||||
}
|
||||
className="text-xs border border-border-default rounded px-2 py-1 bg-surface text-content disabled:opacity-40"
|
||||
>
|
||||
{FREQUENCY_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user