RoyAalekh commited on
Commit
0dbb26a
·
1 Parent(s): c857b7b

feat(telemetry): add admin UI, server/client auth telemetry; grant admin to Ishita

Browse files
app.py CHANGED
@@ -99,6 +99,21 @@ def require_auth(request: Request) -> Dict[str, Any]:
99
  """Dependency that requires authentication"""
100
  user = get_current_user(request)
101
  if not user:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  raise HTTPException(
103
  status_code=status.HTTP_401_UNAUTHORIZED,
104
  detail="Authentication required",
@@ -308,6 +323,22 @@ async def login(login_data: LoginRequest, response: Response):
308
  """Authenticate user and create session"""
309
  result = auth_manager.authenticate(login_data.username, login_data.password)
310
  if not result:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  raise HTTPException(
312
  status_code=status.HTTP_401_UNAUTHORIZED,
313
  detail="Invalid username or password"
@@ -323,6 +354,18 @@ async def login(login_data: LoginRequest, response: Response):
323
  samesite="lax" # CSRF protection
324
  )
325
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  return result
327
 
328
  @app.get("/api/auth/validate", tags=["Authentication"])
@@ -345,6 +388,18 @@ async def logout(request: Request, response: Response):
345
  token = request.cookies.get('auth_token')
346
 
347
  if token:
 
 
 
 
 
 
 
 
 
 
 
 
348
  auth_manager.logout(token)
349
 
350
  # Clear the authentication cookie (must match creation parameters)
@@ -795,6 +850,28 @@ async def get_tree_codes_api():
795
 
796
 
797
  # Telemetry logging
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  class TelemetryEvent(BaseModel):
799
  event_type: str = Field(..., description="Type of event, e.g., 'upload', 'ui', 'error'")
800
  status: Optional[str] = Field(None, description="Status such as success/error")
@@ -845,6 +922,35 @@ async def telemetry(event: TelemetryEvent, request: Request, user: Dict[str, Any
845
  raise HTTPException(status_code=500, detail="Failed to record telemetry")
846
 
847
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  # Version info
849
  @app.get("/api/version", tags=["System"])
850
  async def get_version():
 
99
  """Dependency that requires authentication"""
100
  user = get_current_user(request)
101
  if not user:
102
+ # Server-side auth telemetry for unauthorized access
103
+ try:
104
+ _record_server_telemetry(
105
+ request=request,
106
+ event_type='auth',
107
+ status='unauthorized',
108
+ metadata={
109
+ 'path': str(request.url),
110
+ 'method': request.method,
111
+ 'has_auth_header': bool(request.headers.get('Authorization'))
112
+ },
113
+ user=None
114
+ )
115
+ except Exception:
116
+ pass
117
  raise HTTPException(
118
  status_code=status.HTTP_401_UNAUTHORIZED,
119
  detail="Authentication required",
 
323
  """Authenticate user and create session"""
324
  result = auth_manager.authenticate(login_data.username, login_data.password)
325
  if not result:
326
+ # Telemetry: login failure
327
+ try:
328
+ # Construct a minimal request-like object for _record_server_telemetry if needed
329
+ from fastapi import Request as _Req
330
+ except Exception:
331
+ pass
332
+ # We have the real request inside FastAPI dependency; emulate via middleware not needed here
333
+ # Instead, log via logger for this path
334
+ # Note: We cannot access Request here directly, so we skip client context
335
+ # Use file-based fallback
336
+ _write_telemetry({
337
+ 'event_type': 'auth',
338
+ 'status': 'login_failed',
339
+ 'metadata': {'username': login_data.username},
340
+ 'timestamp': datetime.now().isoformat()
341
+ })
342
  raise HTTPException(
343
  status_code=status.HTTP_401_UNAUTHORIZED,
344
  detail="Invalid username or password"
 
354
  samesite="lax" # CSRF protection
355
  )
356
 
357
+ # Telemetry: login success (server-side)
358
+ try:
359
+ # We cannot access Request here directly; emit without client metadata
360
+ _write_telemetry({
361
+ 'event_type': 'auth',
362
+ 'status': 'login_success',
363
+ 'metadata': {'username': login_data.username},
364
+ 'timestamp': datetime.now().isoformat(),
365
+ 'user': {'username': result['user']['username'], 'role': result['user']['role']}
366
+ })
367
+ except Exception:
368
+ pass
369
  return result
370
 
371
  @app.get("/api/auth/validate", tags=["Authentication"])
 
388
  token = request.cookies.get('auth_token')
389
 
390
  if token:
391
+ # Telemetry: logout server-side
392
+ try:
393
+ session = auth_manager.validate_session(token)
394
+ _record_server_telemetry(
395
+ request=request,
396
+ event_type='auth',
397
+ status='logout',
398
+ metadata={'path': str(request.url)},
399
+ user=session or None
400
+ )
401
+ except Exception:
402
+ pass
403
  auth_manager.logout(token)
404
 
405
  # Clear the authentication cookie (must match creation parameters)
 
850
 
851
 
852
  # Telemetry logging
853
+ # Internal helper to record server-side telemetry without requiring client call
854
+
855
+ def _record_server_telemetry(request: Request, event_type: str, status: str = None, metadata: Dict[str, Any] = None, user: Dict[str, Any] = None):
856
+ evt = {
857
+ 'event_type': event_type,
858
+ 'status': status,
859
+ 'metadata': metadata or {},
860
+ 'timestamp': datetime.now().isoformat(),
861
+ 'user': None,
862
+ 'client': {
863
+ 'ip': request.client.host if request.client else None,
864
+ 'user_agent': request.headers.get('user-agent')
865
+ }
866
+ }
867
+ if user:
868
+ evt['user'] = { 'username': user.get('username'), 'role': user.get('role') }
869
+ if getattr(db, 'connected', False):
870
+ if not db.log_telemetry(evt):
871
+ _write_telemetry(evt)
872
+ else:
873
+ _write_telemetry(evt)
874
+
875
  class TelemetryEvent(BaseModel):
876
  event_type: str = Field(..., description="Type of event, e.g., 'upload', 'ui', 'error'")
877
  status: Optional[str] = Field(None, description="Status such as success/error")
 
922
  raise HTTPException(status_code=500, detail="Failed to record telemetry")
923
 
924
 
925
+ # Telemetry query (admin-only)
926
+ @app.get("/api/telemetry", tags=["System"])
927
+ async def get_telemetry(limit: int = 100, user: Dict[str, Any] = Depends(require_permission("admin"))):
928
+ """Return recent telemetry events. Uses Supabase if connected, else reads telemetry.log."""
929
+ limit = max(1, min(1000, limit))
930
+ try:
931
+ if getattr(db, 'connected', False):
932
+ events = db.get_recent_telemetry(limit)
933
+ return {"events": events, "source": "supabase", "count": len(events)}
934
+ # Fallback to file
935
+ events: List[Dict[str, Any]] = []
936
+ try:
937
+ with open("telemetry.log", "r", encoding="utf-8") as f:
938
+ lines = f.readlines()
939
+ for line in lines[-limit:]:
940
+ line = line.strip()
941
+ if not line:
942
+ continue
943
+ try:
944
+ events.append(json.loads(line))
945
+ except Exception:
946
+ continue
947
+ except FileNotFoundError:
948
+ events = []
949
+ return {"events": events, "source": "file", "count": len(events)}
950
+ except Exception as e:
951
+ logger.error(f"Get telemetry failed: {e}")
952
+ raise HTTPException(status_code=500, detail="Failed to fetch telemetry")
953
+
954
  # Version info
955
  @app.get("/api/version", tags=["System"])
956
  async def get_version():
auth.py CHANGED
@@ -55,9 +55,9 @@ class AuthManager:
55
  # User accounts
56
  "ishita": {
57
  "password_hash": self._hash_password(ishita_password),
58
- "role": "researcher",
59
  "full_name": "Ishita",
60
- "permissions": ["read", "write", "edit_own"]
61
  },
62
 
63
  "jeeb": {
 
55
  # User accounts
56
  "ishita": {
57
  "password_hash": self._hash_password(ishita_password),
58
+ "role": "admin",
59
  "full_name": "Ishita",
60
+ "permissions": ["read", "write", "delete", "admin"]
61
  },
62
 
63
  "jeeb": {
static/index.html CHANGED
@@ -917,7 +917,7 @@
917
  // Force refresh if we detect cached version
918
  (function() {
919
  const currentVersion = '5.1.1';
920
- const timestamp = '1755112765'; // Cache-busting bump
921
  const lastVersion = sessionStorage.getItem('treetrack_version');
922
  const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
923
 
@@ -1162,7 +1162,7 @@
1162
  </div>
1163
  </div>
1164
 
1165
- <script type="module" src="/static/js/tree-track-app.js?v=5.1.1&t=1755112765"></script>
1166
 
1167
  <script>
1168
  // Initialize Granim background animation on page load
 
917
  // Force refresh if we detect cached version
918
  (function() {
919
  const currentVersion = '5.1.1';
920
+ const timestamp = '1755113716'; // Cache-busting bump
921
  const lastVersion = sessionStorage.getItem('treetrack_version');
922
  const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
923
 
 
1162
  </div>
1163
  </div>
1164
 
1165
+ <script type="module" src="/static/js/tree-track-app.js?v=5.1.1&t=1755113716"></script>
1166
 
1167
  <script>
1168
  // Initialize Granim background animation on page load
static/js/modules/auth-manager.js CHANGED
@@ -38,6 +38,22 @@ export class AuthManager {
38
 
39
  async logout() {
40
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  await fetch('/api/auth/logout', {
42
  method: 'POST',
43
  headers: {
 
38
 
39
  async logout() {
40
  try {
41
+ // Telemetry: logout initiated (best-effort)
42
+ try {
43
+ await fetch('/api/telemetry', {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ 'Authorization': `Bearer ${this.authToken}`
48
+ },
49
+ body: JSON.stringify({
50
+ event_type: 'auth',
51
+ status: 'logout_initiated',
52
+ metadata: { source: 'client' }
53
+ })
54
+ });
55
+ } catch (_) { /* ignore */ }
56
+
57
  await fetch('/api/auth/logout', {
58
  method: 'POST',
59
  headers: {
static/js/tree-track-app.js CHANGED
@@ -38,12 +38,25 @@ export class TreeTrackApp {
38
  // Setup event listeners
39
  this.setupEventListeners();
40
 
 
 
 
 
 
 
 
41
  // Load initial data
42
  await this.loadInitialData();
43
 
44
  } catch (error) {
45
  console.error('Error initializing TreeTrack app:', error);
46
  this.uiManager.showMessage('Error initializing application: ' + error.message, 'error');
 
 
 
 
 
 
47
  }
48
  }
49
 
@@ -155,16 +168,41 @@ export class TreeTrackApp {
155
  // Save or update tree
156
  let result;
157
  if (this.formManager.isInEditMode()) {
158
- result = await this.apiClient.updateTree(this.formManager.getCurrentEditId(), treeData);
 
 
 
 
 
 
 
159
  this.uiManager.showMessage(`Tree #${result.id} updated successfully!`, 'success');
 
 
 
 
 
 
160
  // Exit edit mode silently after successful update
161
  this.handleCancelEdit(false);
162
  } else {
 
 
 
 
 
 
163
  result = await this.apiClient.saveTree(treeData);
164
  this.uiManager.showMessage(
165
  `Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`,
166
  'success'
167
  );
 
 
 
 
 
 
168
  this.formManager.resetForm(true);
169
  }
170
 
@@ -176,6 +214,12 @@ export class TreeTrackApp {
176
  console.error('Error submitting form:', error);
177
  this.uiManager.showMessage('Error saving tree: ' + error.message, 'error');
178
  this.uiManager.focusFirstError();
 
 
 
 
 
 
179
  }
180
  }
181
 
@@ -209,12 +253,30 @@ export class TreeTrackApp {
209
  }
210
 
211
  try {
 
 
 
 
 
 
212
  await this.apiClient.deleteTree(treeId);
213
  this.uiManager.showMessage(`Tree #${treeId} deleted successfully.`, 'success');
 
 
 
 
 
 
214
  await this.loadTrees();
215
  } catch (error) {
216
  console.error('Error deleting tree:', error);
217
  this.uiManager.showMessage('Error deleting tree: ' + error.message, 'error');
 
 
 
 
 
 
218
  }
219
  }
220
 
@@ -232,9 +294,21 @@ export class TreeTrackApp {
232
  this.uiManager.showLoadingState('treeList', 'Loading trees...');
233
  const trees = await this.apiClient.loadTrees();
234
  this.uiManager.renderTreeList(trees);
 
 
 
 
 
 
235
  } catch (error) {
236
  console.error('Error loading trees:', error);
237
  this.uiManager.showErrorState('treeList', 'Error loading trees');
 
 
 
 
 
 
238
  }
239
  }
240
 
 
38
  // Setup event listeners
39
  this.setupEventListeners();
40
 
41
+ // Log UI load telemetry
42
+ this.apiClient.sendTelemetry({
43
+ event_type: 'ui',
44
+ status: 'view',
45
+ metadata: { page: 'index', action: 'load' }
46
+ }).catch(() => {});
47
+
48
  // Load initial data
49
  await this.loadInitialData();
50
 
51
  } catch (error) {
52
  console.error('Error initializing TreeTrack app:', error);
53
  this.uiManager.showMessage('Error initializing application: ' + error.message, 'error');
54
+ // Telemetry for init error
55
+ this.apiClient?.sendTelemetry({
56
+ event_type: 'ui',
57
+ status: 'error',
58
+ metadata: { page: 'index', action: 'init', error: error?.message }
59
+ }).catch(() => {});
60
  }
61
  }
62
 
 
168
  // Save or update tree
169
  let result;
170
  if (this.formManager.isInEditMode()) {
171
+ const id = this.formManager.getCurrentEditId();
172
+ // Telemetry: update start
173
+ this.apiClient.sendTelemetry({
174
+ event_type: 'tree_update',
175
+ status: 'start',
176
+ metadata: { tree_id: id }
177
+ }).catch(() => {});
178
+ result = await this.apiClient.updateTree(id, treeData);
179
  this.uiManager.showMessage(`Tree #${result.id} updated successfully!`, 'success');
180
+ // Telemetry: update success
181
+ this.apiClient.sendTelemetry({
182
+ event_type: 'tree_update',
183
+ status: 'success',
184
+ metadata: { tree_id: result.id }
185
+ }).catch(() => {});
186
  // Exit edit mode silently after successful update
187
  this.handleCancelEdit(false);
188
  } else {
189
+ // Telemetry: create start
190
+ this.apiClient.sendTelemetry({
191
+ event_type: 'tree_create',
192
+ status: 'start',
193
+ metadata: { has_location_name: !!treeData.location_name }
194
+ }).catch(() => {});
195
  result = await this.apiClient.saveTree(treeData);
196
  this.uiManager.showMessage(
197
  `Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`,
198
  'success'
199
  );
200
+ // Telemetry: create success
201
+ this.apiClient.sendTelemetry({
202
+ event_type: 'tree_create',
203
+ status: 'success',
204
+ metadata: { tree_id: result.id }
205
+ }).catch(() => {});
206
  this.formManager.resetForm(true);
207
  }
208
 
 
214
  console.error('Error submitting form:', error);
215
  this.uiManager.showMessage('Error saving tree: ' + error.message, 'error');
216
  this.uiManager.focusFirstError();
217
+ // Telemetry: save/update error
218
+ this.apiClient.sendTelemetry({
219
+ event_type: this.formManager.isInEditMode() ? 'tree_update' : 'tree_create',
220
+ status: 'error',
221
+ metadata: { error: error?.message }
222
+ }).catch(() => {});
223
  }
224
  }
225
 
 
253
  }
254
 
255
  try {
256
+ // Telemetry: delete start
257
+ this.apiClient.sendTelemetry({
258
+ event_type: 'tree_delete',
259
+ status: 'start',
260
+ metadata: { tree_id: treeId }
261
+ }).catch(() => {});
262
  await this.apiClient.deleteTree(treeId);
263
  this.uiManager.showMessage(`Tree #${treeId} deleted successfully.`, 'success');
264
+ // Telemetry: delete success
265
+ this.apiClient.sendTelemetry({
266
+ event_type: 'tree_delete',
267
+ status: 'success',
268
+ metadata: { tree_id: treeId }
269
+ }).catch(() => {});
270
  await this.loadTrees();
271
  } catch (error) {
272
  console.error('Error deleting tree:', error);
273
  this.uiManager.showMessage('Error deleting tree: ' + error.message, 'error');
274
+ // Telemetry: delete error
275
+ this.apiClient.sendTelemetry({
276
+ event_type: 'tree_delete',
277
+ status: 'error',
278
+ metadata: { tree_id: treeId, error: error?.message }
279
+ }).catch(() => {});
280
  }
281
  }
282
 
 
294
  this.uiManager.showLoadingState('treeList', 'Loading trees...');
295
  const trees = await this.apiClient.loadTrees();
296
  this.uiManager.renderTreeList(trees);
297
+ // Telemetry: list loaded
298
+ this.apiClient.sendTelemetry({
299
+ event_type: 'tree_list',
300
+ status: 'success',
301
+ metadata: { count: Array.isArray(trees) ? trees.length : null }
302
+ }).catch(() => {});
303
  } catch (error) {
304
  console.error('Error loading trees:', error);
305
  this.uiManager.showErrorState('treeList', 'Error loading trees');
306
+ // Telemetry: list error
307
+ this.apiClient.sendTelemetry({
308
+ event_type: 'tree_list',
309
+ status: 'error',
310
+ metadata: { error: error?.message }
311
+ }).catch(() => {});
312
  }
313
  }
314
 
static/map.html CHANGED
@@ -813,7 +813,7 @@
813
  // Force refresh if we detect cached version
814
  (function() {
815
  const currentVersion = '5.1.1';
816
- const timestamp = '1755112765'; // Current timestamp for cache busting
817
  const lastVersion = sessionStorage.getItem('treetrack_version');
818
  const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
819
 
@@ -939,7 +939,7 @@ const timestamp = '1755112765'; // Current timestamp for cache busting
939
 
940
  <!-- Leaflet JS -->
941
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
942
- <script src="/static/map.js?v=5.1.1&t=1755112765">
943
 
944
  "default-state": {
945
  gradients: [
 
813
  // Force refresh if we detect cached version
814
  (function() {
815
  const currentVersion = '5.1.1';
816
+ const timestamp = '1755113716'; // Current timestamp for cache busting
817
  const lastVersion = sessionStorage.getItem('treetrack_version');
818
  const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
819
 
 
939
 
940
  <!-- Leaflet JS -->
941
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
942
+ <script src="/static/map.js?v=5.1.1&t=1755113716">
943
 
944
  "default-state": {
945
  gradients: [
static/sw.js CHANGED
@@ -1,5 +1,5 @@
1
  // TreeTrack Service Worker - PWA and Offline Support
2
- const VERSION = 1755112765; // Cache busting bump - force clients to fetch new static assets and header image change
3
  const CACHE_NAME = `treetrack-v${VERSION}`;
4
  const STATIC_CACHE = `static-v${VERSION}`;
5
  const API_CACHE = `api-v${VERSION}`;
 
1
  // TreeTrack Service Worker - PWA and Offline Support
2
+ const VERSION = 1755113716; // Cache busting bump - force clients to fetch new static assets and header image change
3
  const CACHE_NAME = `treetrack-v${VERSION}`;
4
  const STATIC_CACHE = `static-v${VERSION}`;
5
  const API_CACHE = `api-v${VERSION}`;
static/telemetry.html ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Telemetry Viewer - Admin</title>
7
+ <link rel="stylesheet" href="/static/css/design-system.css" />
8
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
9
+ <meta http-equiv="Pragma" content="no-cache">
10
+ <meta http-equiv="Expires" content="0">
11
+ <style>
12
+ body { font-family: Inter, system-ui, -apple-system, Segoe UI, sans-serif; background: var(--gray-50); color: var(--gray-800); }
13
+ .container { max-width: 1200px; margin: 0 auto; padding: 1rem; }
14
+ .header { display: flex; justify-content: space-between; align-items: center; margin: 1rem 0; }
15
+ .title { font-size: 1.25rem; font-weight: 700; }
16
+ .controls { display: flex; gap: .5rem; align-items: center; }
17
+ .filter-input { padding: .5rem .75rem; border: 1px solid var(--gray-300); border-radius: .5rem; font-size: .9rem; }
18
+ .btn { padding: .5rem .75rem; border: 1px solid var(--gray-300); background: white; border-radius: .5rem; cursor: pointer; }
19
+ .btn-primary { background: var(--primary-600); color: white; border-color: var(--primary-600); }
20
+ .btn:disabled { opacity: .5; cursor: not-allowed; }
21
+ .meta { color: var(--gray-500); font-size: .85rem; }
22
+ .table { width: 100%; border-collapse: collapse; background: white; border: 1px solid var(--gray-200); border-radius: .75rem; overflow: hidden; }
23
+ .table th, .table td { padding: .5rem .75rem; border-bottom: 1px solid var(--gray-100); text-align: left; vertical-align: top; font-size: .9rem; }
24
+ .table th { background: var(--gray-50); font-weight: 600; }
25
+ .pill { display: inline-block; padding: .15rem .4rem; border-radius: .5rem; font-size: .75rem; border: 1px solid var(--gray-300); }
26
+ .pill-success { background: var(--green-50); color: var(--green-700); border-color: var(--green-300); }
27
+ .pill-error { background: var(--red-50); color: var(--red-700); border-color: var(--red-300); }
28
+ .pill-start { background: var(--orange-50); color: var(--orange-700); border-color: var(--orange-300); }
29
+ .code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .8rem; background: var(--gray-50); padding: .35rem .5rem; border-radius: .35rem; border: 1px solid var(--gray-200); display: inline-block; max-width: 520px; overflow: auto; }
30
+ .row { transition: background .2s ease; }
31
+ .row:hover { background: var(--gray-50); }
32
+ .footer { display: flex; justify-content: space-between; align-items: center; margin-top: .75rem; }
33
+ </style>
34
+ </head>
35
+ <body>
36
+ <div class="container">
37
+ <div class="header">
38
+ <div>
39
+ <div class="title">Telemetry Viewer (Admin)</div>
40
+ <div class="meta">Inspect recent telemetry events to monitor app health and user actions.</div>
41
+ </div>
42
+ <div class="controls">
43
+ <input id="limit" class="filter-input" type="number" min="1" max="1000" value="200" title="Max events" />
44
+ <input id="search" class="filter-input" placeholder="Search text (type, status, user, etc.)" />
45
+ <button id="refresh" class="btn btn-primary">Refresh</button>
46
+ <a class="btn" href="/">Back</a>
47
+ </div>
48
+ </div>
49
+
50
+ <div id="meta" class="meta" style="margin-bottom: .5rem;">&nbsp;</div>
51
+
52
+ <table class="table">
53
+ <thead>
54
+ <tr>
55
+ <th style="width: 10rem;">Timestamp</th>
56
+ <th style="width: 8rem;">Type</th>
57
+ <th style="width: 8rem;">Status</th>
58
+ <th style="width: 10rem;">User</th>
59
+ <th>Metadata</th>
60
+ </tr>
61
+ </thead>
62
+ <tbody id="tbody"></tbody>
63
+ </table>
64
+
65
+ <div class="footer">
66
+ <div id="source" class="meta"></div>
67
+ <div class="meta">Only accessible to admin users</div>
68
+ </div>
69
+ </div>
70
+
71
+ <script type="module">
72
+ async function fetchTelemetry(limit) {
73
+ const params = new URLSearchParams({ limit: String(limit) });
74
+ const res = await fetch(`/api/telemetry?${params.toString()}`, { headers: authHeaders() });
75
+ if (res.status === 401 || res.status === 403) {
76
+ alert('Unauthorized. You must be an admin to view telemetry.');
77
+ window.location.href = '/login';
78
+ return null;
79
+ }
80
+ if (!res.ok) {
81
+ throw new Error('Failed to fetch telemetry');
82
+ }
83
+ return res.json();
84
+ }
85
+
86
+ function authHeaders() {
87
+ const token = localStorage.getItem('auth_token');
88
+ return token ? { 'Authorization': `Bearer ${token}` } : {};
89
+ }
90
+
91
+ function pill(status) {
92
+ if (!status) return '';
93
+ const cls = status === 'success' ? 'pill-success' : status === 'error' ? 'pill-error' : status === 'start' ? 'pill-start' : '';
94
+ return `<span class="pill ${cls}">${status}</span>`;
95
+ }
96
+
97
+ function escapeHtml(s) { return s.replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
98
+
99
+ function renderRows(events) {
100
+ const tbody = document.getElementById('tbody');
101
+ const q = (document.getElementById('search').value || '').toLowerCase().trim();
102
+ tbody.innerHTML = '';
103
+ for (const evt of events) {
104
+ const ts = evt.timestamp || '';
105
+ const type = evt.event_type || '';
106
+ const status = evt.status || '';
107
+ const user = (evt.user && evt.user.username) ? `${evt.user.username} (${evt.user.role || ''})` : '';
108
+ const meta = evt.metadata ? escapeHtml(JSON.stringify(evt.metadata)) : '';
109
+ const line = `${ts} ${type} ${status} ${user} ${meta}`.toLowerCase();
110
+ if (q && !line.includes(q)) continue;
111
+ const tr = document.createElement('tr');
112
+ tr.className = 'row';
113
+ tr.innerHTML = `
114
+ <td>${escapeHtml(ts)}</td>
115
+ <td><span class="pill">${escapeHtml(type)}</span></td>
116
+ <td>${pill(status)}</td>
117
+ <td>${escapeHtml(user)}</td>
118
+ <td><span class="code">${meta}</span></td>
119
+ `;
120
+ tbody.appendChild(tr);
121
+ }
122
+ }
123
+
124
+ async function refresh() {
125
+ const limit = Math.max(1, Math.min(1000, parseInt(document.getElementById('limit').value || '200', 10)));
126
+ document.getElementById('refresh').disabled = true;
127
+ try {
128
+ const data = await fetchTelemetry(limit);
129
+ if (!data) return;
130
+ document.getElementById('meta').textContent = `Loaded ${data.count} events`;
131
+ document.getElementById('source').textContent = `Source: ${data.source}`;
132
+ renderRows(data.events || []);
133
+ } catch (e) {
134
+ alert(e.message || 'Failed to load telemetry');
135
+ } finally {
136
+ document.getElementById('refresh').disabled = false;
137
+ }
138
+ }
139
+
140
+ document.getElementById('refresh').addEventListener('click', refresh);
141
+ document.getElementById('search').addEventListener('input', () => {
142
+ // quick re-filter without fetching again
143
+ const meta = document.getElementById('meta').textContent || '';
144
+ // no-op; filtering reads from table content
145
+ const rows = Array.from(document.querySelectorAll('#tbody tr'));
146
+ const q = (document.getElementById('search').value || '').toLowerCase().trim();
147
+ rows.forEach(row => {
148
+ const text = row.textContent.toLowerCase();
149
+ row.style.display = q && !text.includes(q) ? 'none' : '';
150
+ });
151
+ });
152
+
153
+ // On load: validate auth and fetch
154
+ (async () => {
155
+ try {
156
+ const token = localStorage.getItem('auth_token');
157
+ if (!token) {
158
+ window.location.href = '/login';
159
+ return;
160
+ }
161
+ // Validate user and ensure admin
162
+ const res = await fetch('/api/auth/validate', { headers: authHeaders() });
163
+ if (!res.ok) { window.location.href = '/login'; return; }
164
+ const info = await res.json();
165
+ const perms = (info.user && info.user.permissions) || [];
166
+ if (!perms.includes('admin')) {
167
+ alert('Admin access required');
168
+ window.location.href = '/';
169
+ return;
170
+ }
171
+ await refresh();
172
+ } catch {
173
+ window.location.href = '/login';
174
+ }
175
+ })();
176
+ </script>
177
+ </body>
178
+ </html>
179
+
supabase_database.py CHANGED
@@ -295,3 +295,16 @@ class SupabaseDatabase:
295
  except Exception as e:
296
  logger.error(f"Failed to log telemetry: {e}")
297
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  except Exception as e:
296
  logger.error(f"Failed to log telemetry: {e}")
297
  return False
298
+
299
+ def get_recent_telemetry(self, limit: int = 100) -> List[Dict[str, Any]]:
300
+ """Fetch recent telemetry events from Supabase."""
301
+ try:
302
+ result = self.client.table('telemetry_events') \
303
+ .select('*') \
304
+ .order('timestamp', desc=True) \
305
+ .limit(limit) \
306
+ .execute()
307
+ return result.data or []
308
+ except Exception as e:
309
+ logger.error(f"Failed to fetch telemetry: {e}")
310
+ return []
version.json CHANGED
@@ -1,4 +1,4 @@
1
  {
2
  "version": "5.1.1",
3
- "timestamp": 1755112765
4
  }
 
1
  {
2
  "version": "5.1.1",
3
+ "timestamp": 1755113716
4
  }