DinoPLayZ commited on
Commit
93dcfb6
·
verified ·
1 Parent(s): c76bdf2

Upload event_state_tracker.py

Browse files
Files changed (1) hide show
  1. event_state_tracker.py +223 -0
event_state_tracker.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ from datetime import datetime
5
+ from main import send_email_message
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ STATE_FILE = "event_state.json"
10
+
11
+ def load_state():
12
+ """Loads the previous state of events from the JSON file."""
13
+ if not os.path.exists(STATE_FILE):
14
+ return {}
15
+
16
+ try:
17
+ with open(STATE_FILE, "r", encoding="utf-8") as f:
18
+ return json.load(f)
19
+ except Exception as e:
20
+ logger.warning(f"Failed to load {STATE_FILE}, starting fresh. Error: {e}")
21
+ return {}
22
+
23
+ def save_state(state):
24
+ """Saves the current state of events to the JSON file."""
25
+ try:
26
+ with open(STATE_FILE, "w", encoding="utf-8") as f:
27
+ json.dump(state, f, indent=4)
28
+ except Exception as e:
29
+ logger.error(f"Failed to save {STATE_FILE}. Error: {e}")
30
+
31
+ def extract_tracked_fields(event):
32
+ """Extracts only the fields we care about tracking."""
33
+ return {
34
+ "status": str(event.get("status", "Unknown")).strip(),
35
+ "maximum_count": event.get("maximum_count", 0),
36
+ "event_code": event.get("event_code", "-"),
37
+ "event_name": event.get("event_name", "-")
38
+ }
39
+
40
+ def send_change_alert(event, changed_field, old_value, new_value):
41
+ """Sends an HTML email alert when a tracked field changes."""
42
+ recipient = os.getenv("STATUS_EVENT_EMAIL_RECIPIENT", "")
43
+ if not recipient:
44
+ logger.warning("No STATUS_EVENT_EMAIL_RECIPIENT configured. Skipping change alert.")
45
+ return
46
+
47
+ subject = f"🚨 Event {changed_field} Changed: {event.get('event_code', '-')} → {new_value}"
48
+
49
+ def format_date(date_str):
50
+ try:
51
+ dt = datetime.strptime(date_str, "%Y-%m-%d")
52
+ return dt.strftime('%d-%m-%Y')
53
+ except:
54
+ return date_str
55
+
56
+ start = format_date(event.get("start_date", "")) if event.get("start_date") else ""
57
+ end = format_date(event.get("end_date", "")) if event.get("end_date") else ""
58
+ date_str = f"{start} to {end}" if start and end else (start or end or "-")
59
+
60
+ try:
61
+ max_count = int(event.get("maximum_count", 0))
62
+ except (ValueError, TypeError):
63
+ max_count = 0
64
+
65
+ try:
66
+ applied = int(event.get("applied_count", 0))
67
+ except (ValueError, TypeError):
68
+ applied = 0
69
+
70
+ try:
71
+ balance = int(event.get("ComputedField", 0))
72
+ except (ValueError, TypeError):
73
+ balance = 0
74
+
75
+ body = f"""
76
+ <div style="font-family: Arial, sans-serif; background-color: #f4f6f9; padding: 20px;">
77
+ <div style="max-width: 600px; margin: 0 auto; background: #ffffff; border-radius: 8px;
78
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1); overflow: hidden;">
79
+
80
+ <div style="background-color: #0056b3; color: #ffffff; padding: 15px 20px;
81
+ font-size: 18px; font-weight: bold; text-align: center;">
82
+ 🚨 Event {changed_field} Changed
83
+ </div>
84
+
85
+ <div style="padding: 20px;">
86
+ <div style="background-color: #f0fdf4; padding: 15px; border-radius: 6px; margin-bottom: 20px; text-align: center; border: 1px solid #bbf7d0;">
87
+ <span style="font-weight: bold; color: #166534;">{changed_field} Update:</span><br>
88
+ <span style="font-size: 16px;">{old_value} &rarr; <b style="color: #15803d;">{new_value}</b></span>
89
+ </div>
90
+
91
+ <table style="width: 100%; border-collapse: collapse; margin-bottom: 25px; font-size: 15px; color: #333333; text-align: left;">
92
+ <tr>
93
+ <td style="padding: 8px 0; font-weight: bold; width: 35%;">Code</td>
94
+ <td style="padding: 8px 0; width: 5%;">:</td>
95
+ <td style="padding: 8px 0; width: 60%; font-weight: bold;">{event.get('event_code', '-')}</td>
96
+ </tr>
97
+ <tr>
98
+ <td style="padding: 8px 0; font-weight: bold;">Name</td>
99
+ <td style="padding: 8px 0;">:</td>
100
+ <td style="padding: 8px 0;">{event.get('event_name', '-')}</td>
101
+ </tr>
102
+ <tr>
103
+ <td style="padding: 8px 0; font-weight: bold;">Organizer</td>
104
+ <td style="padding: 8px 0;">:</td>
105
+ <td style="padding: 8px 0;">{event.get('organizer', '-')}</td>
106
+ </tr>
107
+ <tr>
108
+ <td style="padding: 8px 0; font-weight: bold;">Date</td>
109
+ <td style="padding: 8px 0;">:</td>
110
+ <td style="padding: 8px 0;">{date_str}</td>
111
+ </tr>
112
+ <tr>
113
+ <td style="padding: 8px 0; font-weight: bold;">Category</td>
114
+ <td style="padding: 8px 0;">:</td>
115
+ <td style="padding: 8px 0;">{event.get('event_category', '-')}</td>
116
+ </tr>
117
+ <tr>
118
+ <td style="padding: 8px 0; font-weight: bold;">Location</td>
119
+ <td style="padding: 8px 0;">:</td>
120
+ <td style="padding: 8px 0;">{event.get('location', '-')}</td>
121
+ </tr>
122
+ <tr>
123
+ <td style="padding: 8px 0; font-weight: bold;">Status</td>
124
+ <td style="padding: 8px 0;">:</td>
125
+ <td style="padding: 8px 0;">{event.get('status', '-')}</td>
126
+ </tr>
127
+ <tr>
128
+ <td style="padding: 8px 0; font-weight: bold;">Logger URL</td>
129
+ <td style="padding: 8px 0;">:</td>
130
+ <td style="padding: 8px 0;">
131
+ <a href="https://bip.bitsathy.ac.in/nova/resources/student-achievement-loggers/new"
132
+ style="color: #0056b3; text-decoration: none;">Link</a>
133
+ </td>
134
+ </tr>
135
+ <tr>
136
+ <td style="padding: 8px 0; font-weight: bold;">View Link</td>
137
+ <td style="padding: 8px 0;">:</td>
138
+ <td style="padding: 8px 0;">
139
+ <a href="{event.get('web_url', '#')}"
140
+ style="color: #0056b3; text-decoration: none;">View Event Here</a>
141
+ </td>
142
+ </tr>
143
+ <tr>
144
+ <td style="padding: 12px 0; font-weight: bold;">Count</td>
145
+ <td style="padding: 12px 0;">:</td>
146
+ <td style="padding: 12px 0;">
147
+ Max: <b>{max_count}</b> | Balance: <b>{balance}</b> | Applied: <b>{applied}</b>
148
+ </td>
149
+ </tr>
150
+ </table>
151
+ </div>
152
+ <div style="background-color: #f8f9fa; padding: 15px 20px; text-align: center; font-size: 12px; color: #777777; border-top: 1px solid #e0e0e0;">
153
+ Automated Notification from BIP Tracker
154
+ </div>
155
+ </div>
156
+ </div>
157
+ """
158
+
159
+ logger.info(f"🚨 STATUS TRACKER: Sending {changed_field} alert for {event.get('event_code', '-')}")
160
+ send_email_message(subject, body, is_html=True, recipient=recipient)
161
+
162
+ def track_event_changes(events):
163
+ """
164
+ Analyzes a list of events fetched from the API, compares them to the
165
+ previous state, and triggers alerts for any that just became 'Active'.
166
+ """
167
+ if not events:
168
+ return
169
+
170
+ state = load_state()
171
+ state_changed = False
172
+
173
+ for event in events:
174
+ event_id = str(event.get("id"))
175
+ if not event_id:
176
+ continue
177
+
178
+ current_data = extract_tracked_fields(event)
179
+
180
+ # Hugging Face Restart Protection:
181
+ # If the event ID doesn't exist in our state file at all, this might
182
+ # be the first run, or the container just restarted and lost its file.
183
+ # We record it, but we DO NOT trigger an alert to prevent false positives.
184
+ if event_id not in state:
185
+ state[event_id] = current_data
186
+ state_changed = True
187
+ logger.debug(f"Tracking new state for event {event_id} (No alert)")
188
+ continue
189
+
190
+ previous_data = state[event_id]
191
+
192
+ # Detect if status changed
193
+ old_status = str(previous_data.get("status", "")).strip()
194
+ new_status = str(current_data.get("status", "")).strip()
195
+
196
+ if old_status != new_status:
197
+ # Only trigger alert if old_status has SOME value (not completely missing) to avoid edge noise
198
+ if old_status:
199
+ logger.info(f"Event {event_id} ({current_data.get('event_code', '-')}) changed status to {new_status}!")
200
+ send_change_alert(event, "Status", old_status, new_status)
201
+
202
+ # Detect if maximum_count increased
203
+ try:
204
+ old_max = int(previous_data.get("maximum_count", 0))
205
+ except (ValueError, TypeError):
206
+ old_max = 0
207
+
208
+ try:
209
+ new_max = int(current_data.get("maximum_count", 0))
210
+ except (ValueError, TypeError):
211
+ new_max = 0
212
+
213
+ if new_max > old_max:
214
+ logger.info(f"Event {event_id} ({current_data.get('event_code', '-')}) increased max count from {old_max} to {new_max}!")
215
+ send_change_alert(event, "Capacity", str(old_max), str(new_max))
216
+
217
+ # Update state if anything changed
218
+ if current_data != previous_data:
219
+ state[event_id] = current_data
220
+ state_changed = True
221
+
222
+ if state_changed:
223
+ save_state(state)