New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View File

@@ -1,219 +0,0 @@
/**
* Next-Generation Alert Types
*
* TypeScript definitions for enriched, context-aware alerts
* Matches shared/schemas/alert_types.py
*/
export enum AlertTypeClass {
ACTION_NEEDED = 'action_needed',
PREVENTED_ISSUE = 'prevented_issue',
TREND_WARNING = 'trend_warning',
ESCALATION = 'escalation',
INFORMATION = 'information'
}
export enum PriorityLevel {
CRITICAL = 'critical', // 90-100: Needs decision in next 2 hours
IMPORTANT = 'important', // 70-89: Needs decision today
STANDARD = 'standard', // 50-69: Review when convenient
INFO = 'info' // 0-49: For awareness
}
export enum PlacementHint {
TOAST = 'toast',
ACTION_QUEUE = 'action_queue',
DASHBOARD_INLINE = 'dashboard_inline',
NOTIFICATION_PANEL = 'notification_panel',
EMAIL_DIGEST = 'email_digest'
}
export enum SmartActionType {
APPROVE_PO = 'approve_po',
REJECT_PO = 'reject_po',
CALL_SUPPLIER = 'call_supplier',
NAVIGATE = 'navigate',
ADJUST_PRODUCTION = 'adjust_production',
NOTIFY_CUSTOMER = 'notify_customer',
CANCEL_AUTO_ACTION = 'cancel_auto_action',
OPEN_REASONING = 'open_reasoning',
SNOOZE = 'snooze',
DISMISS = 'dismiss',
MARK_READ = 'mark_read'
}
export interface SmartAction {
label: string;
type: SmartActionType;
variant: 'primary' | 'secondary' | 'tertiary' | 'danger';
metadata: Record<string, any>;
disabled?: boolean;
disabled_reason?: string;
estimated_time_minutes?: number;
consequence?: string;
}
export interface OrchestratorContext {
already_addressed: boolean;
action_type?: string;
action_id?: string;
action_status?: string;
delivery_date?: string;
reasoning?: Record<string, any>;
estimated_resolution_time?: string;
}
export interface BusinessImpact {
financial_impact_eur?: number;
affected_orders?: number;
affected_customers?: string[];
production_batches_at_risk?: string[];
stockout_risk_hours?: number;
waste_risk_kg?: number;
customer_satisfaction_impact?: 'high' | 'medium' | 'low';
}
export interface UrgencyContext {
deadline?: string;
time_until_consequence_hours?: number;
can_wait_until_tomorrow: boolean;
peak_hour_relevant: boolean;
auto_action_countdown_seconds?: number;
}
export interface UserAgency {
can_user_fix: boolean;
requires_external_party: boolean;
external_party_name?: string;
external_party_contact?: string;
blockers?: string[];
suggested_workaround?: string;
}
export interface TrendContext {
metric_name: string;
current_value: number;
baseline_value: number;
change_percentage: number;
direction: 'increasing' | 'decreasing';
significance: 'high' | 'medium' | 'low';
period_days: number;
possible_causes?: string[];
}
export interface EnrichedAlert {
// Original Alert Data
id: string;
tenant_id: string;
service: string;
alert_type: string;
title: string;
message: string;
// Classification
type_class: AlertTypeClass;
priority_level: PriorityLevel;
priority_score: number;
// Context Enrichment
orchestrator_context?: OrchestratorContext;
business_impact?: BusinessImpact;
urgency_context?: UrgencyContext;
user_agency?: UserAgency;
trend_context?: TrendContext;
// AI Reasoning
ai_reasoning_summary?: string;
reasoning_data?: Record<string, any>;
confidence_score?: number;
// Actions
actions: SmartAction[];
primary_action?: SmartAction;
// UI Placement
placement: PlacementHint[];
// Grouping
group_id?: string;
is_group_summary: boolean;
grouped_alert_count?: number;
grouped_alert_ids?: string[];
// Metadata
created_at: string;
enriched_at: string;
alert_metadata: Record<string, any>;
status: 'active' | 'resolved' | 'acknowledged' | 'snoozed';
}
export interface PriorityScoreComponents {
business_impact_score: number;
urgency_score: number;
user_agency_score: number;
confidence_score: number;
final_score: number;
weights: Record<string, number>;
}
// Helper functions
export function getPriorityColor(level: PriorityLevel): string {
switch (level) {
case PriorityLevel.CRITICAL:
return 'var(--color-error)';
case PriorityLevel.IMPORTANT:
return 'var(--color-warning)';
case PriorityLevel.STANDARD:
return 'var(--color-info)';
case PriorityLevel.INFO:
return 'var(--color-success)';
}
}
export function getPriorityIcon(level: PriorityLevel): string {
switch (level) {
case PriorityLevel.CRITICAL:
return 'alert-triangle';
case PriorityLevel.IMPORTANT:
return 'alert-circle';
case PriorityLevel.STANDARD:
return 'info';
case PriorityLevel.INFO:
return 'check-circle';
}
}
export function getTypeClassBadgeVariant(typeClass: AlertTypeClass): 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'outline' {
switch (typeClass) {
case AlertTypeClass.ACTION_NEEDED:
return 'error';
case AlertTypeClass.PREVENTED_ISSUE:
return 'success';
case AlertTypeClass.TREND_WARNING:
return 'warning';
case AlertTypeClass.ESCALATION:
return 'error';
case AlertTypeClass.INFORMATION:
return 'info';
}
}
export function formatTimeUntilConsequence(hours?: number): string {
if (!hours) return '';
if (hours < 1) {
return `${Math.round(hours * 60)} minutes`;
} else if (hours < 24) {
return `${Math.round(hours)} hours`;
} else {
return `${Math.round(hours / 24)} days`;
}
}
export function shouldShowToast(alert: EnrichedAlert): boolean {
return alert.placement.includes(PlacementHint.TOAST);
}
export function shouldShowInActionQueue(alert: EnrichedAlert): boolean {
return alert.placement.includes(PlacementHint.ACTION_QUEUE);
}

View File

@@ -1,369 +0,0 @@
/**
* Event System Type Definitions
*
* Matches backend event architecture with three-tier model:
* - ALERT: Actionable events requiring user decision
* - NOTIFICATION: Informational state changes
* - RECOMMENDATION: AI-generated suggestions
*/
// ============================================================
// Event Classifications
// ============================================================
export type EventClass = 'alert' | 'notification' | 'recommendation';
export type EventDomain =
| 'inventory'
| 'production'
| 'supply_chain'
| 'demand'
| 'operations';
export type PriorityLevel = 'critical' | 'important' | 'standard' | 'info';
export type AlertTypeClass =
| 'action_needed'
| 'prevented_issue'
| 'trend_warning'
| 'escalation'
| 'information';
export type NotificationType =
| 'state_change'
| 'completion'
| 'arrival'
| 'departure'
| 'update'
| 'system_event';
export type RecommendationType =
| 'optimization'
| 'cost_reduction'
| 'risk_mitigation'
| 'trend_insight'
| 'best_practice';
// ============================================================
// Base Event Interface
// ============================================================
export interface BaseEvent {
id: string;
tenant_id: string;
event_class: EventClass;
event_domain: EventDomain;
event_type: string;
service: string;
title: string;
message: string;
timestamp: string;
created_at: string;
metadata?: Record<string, any>;
_channel?: string; // Added by gateway for frontend routing
}
// ============================================================
// Alert (Full Enrichment)
// ============================================================
export interface OrchestratorContext {
already_addressed?: boolean;
action_type?: string;
action_id?: string;
action_status?: string;
delivery_date?: string;
reasoning?: string;
}
export interface BusinessImpact {
financial_impact_eur?: number;
affected_orders?: number;
affected_customers?: string[];
production_batches_at_risk?: string[];
stockout_risk_hours?: number;
waste_risk_kg?: number;
customer_satisfaction_impact?: 'high' | 'medium' | 'low';
}
export interface UrgencyContext {
deadline?: string;
time_until_consequence_hours?: number;
can_wait_until_tomorrow?: boolean;
peak_hour_relevant?: boolean;
auto_action_countdown_seconds?: number;
}
export interface UserAgency {
can_user_fix?: boolean;
requires_external_party?: boolean;
external_party_name?: string;
external_party_contact?: string;
blockers?: string[];
suggested_workaround?: string;
}
export interface SmartAction {
type: string;
label: string;
variant?: 'primary' | 'secondary' | 'danger' | 'success';
metadata?: Record<string, any>;
estimated_time_minutes?: number;
consequence?: string;
disabled?: boolean;
disabled_reason?: string;
}
export interface Alert extends BaseEvent {
event_class: 'alert';
type_class: AlertTypeClass;
status: 'active' | 'acknowledged' | 'resolved' | 'dismissed' | 'in_progress';
// Priority
priority_score: number; // 0-100
priority_level: PriorityLevel;
// Enrichment context
orchestrator_context?: OrchestratorContext;
business_impact?: BusinessImpact;
urgency_context?: UrgencyContext;
user_agency?: UserAgency;
trend_context?: Record<string, any>;
// Smart actions
actions?: SmartAction[];
// AI reasoning
ai_reasoning_summary?: string;
confidence_score?: number;
// Timing
timing_decision?: 'send_now' | 'schedule_later' | 'batch_for_digest';
scheduled_send_time?: string;
placement?: string[];
// Escalation & chaining
action_created_at?: string;
superseded_by_action_id?: string;
hidden_from_ui?: boolean;
// Timestamps
updated_at?: string;
resolved_at?: string;
// Legacy fields (for backward compatibility)
alert_type?: string;
item_type?: 'alert';
}
// ============================================================
// Notification (Lightweight)
// ============================================================
export interface Notification extends BaseEvent {
event_class: 'notification';
notification_type: NotificationType;
// Entity context
entity_type?: string;
entity_id?: string;
old_state?: string;
new_state?: string;
// Display
placement?: string[];
// TTL
expires_at?: string;
// Legacy fields
item_type?: 'notification';
}
// ============================================================
// Recommendation (AI Suggestions)
// ============================================================
export interface Recommendation extends BaseEvent {
event_class: 'recommendation';
recommendation_type: RecommendationType;
// Light priority
priority_level: PriorityLevel;
// Context
estimated_impact?: {
financial_savings_eur?: number;
time_saved_hours?: number;
efficiency_gain_percent?: number;
[key: string]: any;
};
suggested_actions?: SmartAction[];
// AI reasoning
ai_reasoning_summary?: string;
confidence_score?: number;
// Dismissal
dismissed_at?: string;
dismissed_by?: string;
// Timestamps
updated_at?: string;
// Legacy fields
item_type?: 'recommendation';
}
// ============================================================
// Union Types
// ============================================================
export type Event = Alert | Notification | Recommendation;
// Type guards
export function isAlert(event: Event): event is Alert {
return event.event_class === 'alert' || event.item_type === 'alert';
}
export function isNotification(event: Event): event is Notification {
return event.event_class === 'notification';
}
export function isRecommendation(event: Event): event is Recommendation {
return event.event_class === 'recommendation' || event.item_type === 'recommendation';
}
// ============================================================
// Channel Patterns
// ============================================================
export type ChannelPattern =
| `${EventDomain}.${Exclude<EventClass, 'recommendation'>}` // e.g., "inventory.alerts"
| `${EventDomain}.*` // e.g., "inventory.*"
| `*.${Exclude<EventClass, 'recommendation'>}` // e.g., "*.alerts"
| 'recommendations'
| '*.*';
// ============================================================
// Hook Configuration Types
// ============================================================
export interface UseAlertsConfig {
domains?: EventDomain[];
minPriority?: PriorityLevel;
typeClass?: AlertTypeClass[];
includeResolved?: boolean;
maxAge?: number; // seconds
}
export interface UseNotificationsConfig {
domains?: EventDomain[];
eventTypes?: string[];
maxAge?: number; // seconds, default 3600 (1 hour)
}
export interface UseRecommendationsConfig {
domains?: EventDomain[];
includeDismissed?: boolean;
minConfidence?: number; // 0.0 - 1.0
}
// ============================================================
// SSE Event Types
// ============================================================
export interface SSEConnectionEvent {
type: 'connected';
message: string;
channels: string[];
timestamp: number;
}
export interface SSEHeartbeatEvent {
type: 'heartbeat';
timestamp: number;
}
export interface SSEInitialStateEvent {
events: Event[];
}
// ============================================================
// Backward Compatibility (Legacy Alert Format)
// ============================================================
/**
* @deprecated Use Alert type instead
*/
export interface LegacyAlert {
id: string;
tenant_id: string;
item_type: 'alert' | 'recommendation';
alert_type: string;
service: string;
title: string;
message: string;
priority_level?: string;
priority_score?: number;
type_class?: string;
status?: string;
actions?: any[];
metadata?: Record<string, any>;
timestamp: string;
created_at: string;
}
/**
* Convert legacy alert format to new Event format
*/
export function convertLegacyAlert(legacy: LegacyAlert): Event {
const eventClass: EventClass = legacy.item_type === 'recommendation' ? 'recommendation' : 'alert';
// Infer domain from service (best effort)
const domainMap: Record<string, EventDomain> = {
'inventory': 'inventory',
'production': 'production',
'procurement': 'supply_chain',
'forecasting': 'demand',
'orchestrator': 'operations',
};
const event_domain = domainMap[legacy.service] || 'operations';
const base = {
id: legacy.id,
tenant_id: legacy.tenant_id,
event_class: eventClass,
event_domain,
event_type: legacy.alert_type,
service: legacy.service,
title: legacy.title,
message: legacy.message,
timestamp: legacy.timestamp,
created_at: legacy.created_at,
metadata: legacy.metadata,
};
if (eventClass === 'alert') {
return {
...base,
event_class: 'alert',
type_class: (legacy.type_class as AlertTypeClass) || 'action_needed',
status: (legacy.status as any) || 'active',
priority_score: legacy.priority_score || 50,
priority_level: (legacy.priority_level as PriorityLevel) || 'standard',
actions: legacy.actions as SmartAction[],
alert_type: legacy.alert_type,
item_type: 'alert',
} as Alert;
} else {
return {
...base,
event_class: 'recommendation',
recommendation_type: 'trend_insight',
priority_level: (legacy.priority_level as PriorityLevel) || 'info',
suggested_actions: legacy.actions as SmartAction[],
item_type: 'recommendation',
} as Recommendation;
}
}