jebin2 commited on
Commit
23af55d
·
1 Parent(s): 1bd7131
Files changed (2) hide show
  1. routers/blink.py +144 -1
  2. templates/index.html +319 -88
routers/blink.py CHANGED
@@ -6,7 +6,7 @@ import ipaddress
6
  import logging
7
 
8
  from core.database import get_db
9
- from core.models import BlinkData
10
  from services.encryption_service import decrypt_multiple_blocks
11
  from dependencies import get_geolocation
12
 
@@ -71,6 +71,149 @@ async def get_data(
71
  )
72
 
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  @router.get("/blink")
75
  async def blink(
76
  request: Request,
 
6
  import logging
7
 
8
  from core.database import get_db
9
+ from core.models import BlinkData, User, AuditLog, GeminiJob
10
  from services.encryption_service import decrypt_multiple_blocks
11
  from dependencies import get_geolocation
12
 
 
71
  )
72
 
73
 
74
+ @router.get("/api/users")
75
+ async def get_users(
76
+ page: int = Query(1, ge=1, description="Page number"),
77
+ limit: int = Query(50, ge=1, le=500, description="Items per page"),
78
+ db: AsyncSession = Depends(get_db)
79
+ ):
80
+ """
81
+ Get paginated users data.
82
+ """
83
+ try:
84
+ offset = (page - 1) * limit
85
+
86
+ # Get total count
87
+ total_result = await db.execute(select(func.count(User.id)))
88
+ total = total_result.scalar() or 0
89
+
90
+ # Get paginated items
91
+ query = select(User).order_by(User.id.desc()).offset(offset).limit(limit)
92
+ result = await db.execute(query)
93
+ items = result.scalars().all()
94
+
95
+ return {
96
+ "items": [
97
+ {
98
+ "id": item.id,
99
+ "user_id": item.user_id,
100
+ "email": item.email,
101
+ "name": item.name,
102
+ "google_id": item.google_id[:10] + "..." if item.google_id else None,
103
+ "credits": item.credits,
104
+ "is_active": item.is_active,
105
+ "created_at": item.created_at.isoformat() if item.created_at else None,
106
+ "last_used_at": item.last_used_at.isoformat() if item.last_used_at else None
107
+ }
108
+ for item in items
109
+ ],
110
+ "total": total,
111
+ "page": page,
112
+ "limit": limit
113
+ }
114
+ except Exception as e:
115
+ logger.error(f"Error fetching users: {e}")
116
+ raise HTTPException(
117
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
118
+ detail="Error fetching users"
119
+ )
120
+
121
+
122
+ @router.get("/api/audit-logs")
123
+ async def get_audit_logs(
124
+ page: int = Query(1, ge=1, description="Page number"),
125
+ limit: int = Query(50, ge=1, le=500, description="Items per page"),
126
+ db: AsyncSession = Depends(get_db)
127
+ ):
128
+ """
129
+ Get paginated audit logs.
130
+ """
131
+ try:
132
+ offset = (page - 1) * limit
133
+
134
+ # Get total count
135
+ total_result = await db.execute(select(func.count(AuditLog.id)))
136
+ total = total_result.scalar() or 0
137
+
138
+ # Get paginated items
139
+ query = select(AuditLog).order_by(AuditLog.id.desc()).offset(offset).limit(limit)
140
+ result = await db.execute(query)
141
+ items = result.scalars().all()
142
+
143
+ return {
144
+ "items": [
145
+ {
146
+ "id": item.id,
147
+ "user_id": item.user_id,
148
+ "action": item.action,
149
+ "ip_address": item.ip_address,
150
+ "status": item.status,
151
+ "error_message": item.error_message,
152
+ "timestamp": item.timestamp.isoformat() if item.timestamp else None
153
+ }
154
+ for item in items
155
+ ],
156
+ "total": total,
157
+ "page": page,
158
+ "limit": limit
159
+ }
160
+ except Exception as e:
161
+ logger.error(f"Error fetching audit logs: {e}")
162
+ raise HTTPException(
163
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
164
+ detail="Error fetching audit logs"
165
+ )
166
+
167
+
168
+ @router.get("/api/gemini-jobs")
169
+ async def get_gemini_jobs(
170
+ page: int = Query(1, ge=1, description="Page number"),
171
+ limit: int = Query(50, ge=1, le=500, description="Items per page"),
172
+ db: AsyncSession = Depends(get_db)
173
+ ):
174
+ """
175
+ Get paginated Gemini jobs.
176
+ """
177
+ try:
178
+ offset = (page - 1) * limit
179
+
180
+ # Get total count
181
+ total_result = await db.execute(select(func.count(GeminiJob.id)))
182
+ total = total_result.scalar() or 0
183
+
184
+ # Get paginated items
185
+ query = select(GeminiJob).order_by(GeminiJob.id.desc()).offset(offset).limit(limit)
186
+ result = await db.execute(query)
187
+ items = result.scalars().all()
188
+
189
+ return {
190
+ "items": [
191
+ {
192
+ "id": item.id,
193
+ "job_id": item.job_id,
194
+ "user_id": item.user_id,
195
+ "job_type": item.job_type,
196
+ "status": item.status,
197
+ "error_message": item.error_message,
198
+ "created_at": item.created_at.isoformat() if item.created_at else None,
199
+ "completed_at": item.completed_at.isoformat() if item.completed_at else None
200
+ }
201
+ for item in items
202
+ ],
203
+ "total": total,
204
+ "page": page,
205
+ "limit": limit
206
+ }
207
+ except Exception as e:
208
+ logger.error(f"Error fetching gemini jobs: {e}")
209
+ raise HTTPException(
210
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
211
+ detail="Error fetching gemini jobs"
212
+ )
213
+
214
+
215
+
216
+
217
  @router.get("/blink")
218
  async def blink(
219
  request: Request,
templates/index.html CHANGED
@@ -4,7 +4,7 @@
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>API Gateway - Blink Data</title>
8
  <style>
9
  * {
10
  margin: 0;
@@ -21,7 +21,7 @@
21
  }
22
 
23
  .container {
24
- max-width: 1400px;
25
  margin: 0 auto;
26
  }
27
 
@@ -44,11 +44,43 @@
44
  font-size: 1rem;
45
  }
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  .stats {
48
  display: flex;
49
  gap: 20px;
50
  justify-content: center;
51
  margin-bottom: 30px;
 
52
  }
53
 
54
  .stat-card {
@@ -80,9 +112,14 @@
80
  overflow: hidden;
81
  }
82
 
 
 
 
 
83
  table {
84
  width: 100%;
85
  border-collapse: collapse;
 
86
  }
87
 
88
  th {
@@ -92,6 +129,7 @@
92
  font-weight: 600;
93
  color: #00d4ff;
94
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
 
95
  }
96
 
97
  td {
@@ -116,15 +154,13 @@
116
  background: rgba(0, 0, 0, 0.3);
117
  padding: 8px 12px;
118
  border-radius: 6px;
119
- max-width: 500px;
120
  overflow-x: auto;
121
  white-space: pre-wrap;
122
  word-break: break-all;
123
  }
124
 
125
- .refer-url {
126
- color: #888;
127
- font-size: 0.9rem;
128
  max-width: 200px;
129
  overflow: hidden;
130
  text-overflow: ellipsis;
@@ -134,6 +170,27 @@
134
  .timestamp {
135
  color: #666;
136
  font-size: 0.85rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  }
138
 
139
  .pagination {
@@ -204,6 +261,10 @@
204
  margin-bottom: 20px;
205
  opacity: 0.3;
206
  }
 
 
 
 
207
  </style>
208
  </head>
209
 
@@ -211,139 +272,309 @@
211
  <div class="container">
212
  <header>
213
  <h1>🔗 API Gateway</h1>
214
- <p class="subtitle">Blink Data Viewer</p>
215
  </header>
216
 
217
- <div class="stats">
 
 
 
 
 
 
 
218
  <div class="stat-card">
219
  <div class="stat-value" id="totalRecords">-</div>
220
  <div class="stat-label">Total Records</div>
221
  </div>
222
- <div class="stat-card">
223
  <div class="stat-value" id="uniqueUsers">-</div>
224
  <div class="stat-label">Unique Users</div>
225
  </div>
226
  </div>
227
 
228
- <div class="table-container">
229
- <table>
230
- <thead>
231
- <tr>
232
- <th>ID</th>
233
- <th>User ID</th>
234
- <th>Refer URL</th>
235
- <th>IPv4</th>
236
- <th>IPv6</th>
237
- <th>Country</th>
238
- <th>Region</th>
239
- <th>JSON Data</th>
240
- <th>Created At</th>
241
- </tr>
242
- </thead>
243
- <tbody id="tableBody">
244
- <tr>
245
- <td colspan="8">
246
- <div class="loading">
247
- <div class="spinner"></div>
248
- Loading data...
249
- </div>
250
- </td>
251
- </tr>
252
- </tbody>
253
- </table>
254
  <div class="pagination">
255
- <button id="prevBtn" onclick="prevPage()" disabled>← Previous</button>
256
- <span class="page-info">Page <span id="currentPage">1</span> of <span id="totalPages">1</span></span>
257
- <button id="nextBtn" onclick="nextPage()">Next →</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  </div>
259
  </div>
260
  </div>
261
 
262
  <script>
263
- const PAGE_SIZE = 100;
264
- let currentPage = 1;
265
- let totalRecords = 0;
266
-
267
- async function fetchData(page = 1) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  try {
269
- const response = await fetch(`/api/data?page=${page}&limit=${PAGE_SIZE}`);
270
- const data = await response.json();
271
- return data;
272
  } catch (error) {
273
  console.error('Error fetching data:', error);
274
- return { items: [], total: 0, unique_users: 0 };
275
  }
276
  }
277
 
278
- function renderTable(items) {
279
- const tbody = document.getElementById('tableBody');
280
-
281
  if (items.length === 0) {
282
- tbody.innerHTML = `
283
- <tr>
284
- <td colspan="8">
285
- <div class="empty-state">
286
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
287
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
288
- </svg>
289
- <p>No data available</p>
290
- </div>
291
- </td>
292
- </tr>
293
- `;
294
  return;
295
  }
296
-
297
  tbody.innerHTML = items.map(item => `
298
  <tr>
299
  <td>${item.id}</td>
300
  <td class="user-id">${item.user_id}</td>
301
- <td class="refer-url" title="${item.refer_url || ''}">${item.refer_url || '-'}</td>
302
- <td class="ip-address">${item.ipv4_address || '-'}</td>
303
- <td class="ip-address">${item.ipv6_address || '-'}</td>
304
  <td>${item.country || '-'}</td>
305
  <td>${item.region || '-'}</td>
306
  <td><pre class="json-data">${JSON.stringify(item.json_data, null, 2)}</pre></td>
307
- <td class="timestamp">${new Date(item.created_at).toLocaleString()}</td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  </tr>
309
  `).join('');
310
  }
311
 
312
- function updatePagination() {
313
- const totalPages = Math.ceil(totalRecords / PAGE_SIZE) || 1;
314
- document.getElementById('currentPage').textContent = currentPage;
315
- document.getElementById('totalPages').textContent = totalPages;
316
- document.getElementById('prevBtn').disabled = currentPage <= 1;
317
- document.getElementById('nextBtn').disabled = currentPage >= totalPages;
 
 
 
 
 
 
 
 
 
 
 
318
  }
319
 
320
- async function loadPage(page) {
321
- currentPage = page;
322
- const data = await fetchData(page);
323
- totalRecords = data.total;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
  document.getElementById('totalRecords').textContent = data.total.toLocaleString();
326
- document.getElementById('uniqueUsers').textContent = data.unique_users.toLocaleString();
 
 
327
 
328
- renderTable(data.items);
329
- updatePagination();
330
  }
331
 
332
- function prevPage() {
333
- if (currentPage > 1) {
334
- loadPage(currentPage - 1);
335
  }
336
  }
337
 
338
- function nextPage() {
339
- const totalPages = Math.ceil(totalRecords / PAGE_SIZE);
340
- if (currentPage < totalPages) {
341
- loadPage(currentPage + 1);
342
  }
343
  }
344
 
345
  // Initial load
346
- loadPage(1);
347
  </script>
348
  </body>
349
 
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>API Gateway - Data Viewer</title>
8
  <style>
9
  * {
10
  margin: 0;
 
21
  }
22
 
23
  .container {
24
+ max-width: 1600px;
25
  margin: 0 auto;
26
  }
27
 
 
44
  font-size: 1rem;
45
  }
46
 
47
+ /* Tabs */
48
+ .tabs {
49
+ display: flex;
50
+ gap: 10px;
51
+ margin-bottom: 20px;
52
+ justify-content: center;
53
+ flex-wrap: wrap;
54
+ }
55
+
56
+ .tab-btn {
57
+ background: rgba(255, 255, 255, 0.05);
58
+ border: 1px solid rgba(255, 255, 255, 0.1);
59
+ color: #888;
60
+ padding: 12px 24px;
61
+ border-radius: 8px;
62
+ cursor: pointer;
63
+ font-weight: 600;
64
+ transition: all 0.3s ease;
65
+ }
66
+
67
+ .tab-btn:hover {
68
+ background: rgba(255, 255, 255, 0.1);
69
+ color: #fff;
70
+ }
71
+
72
+ .tab-btn.active {
73
+ background: linear-gradient(135deg, #00d4ff, #7b2cbf);
74
+ color: white;
75
+ border-color: transparent;
76
+ }
77
+
78
  .stats {
79
  display: flex;
80
  gap: 20px;
81
  justify-content: center;
82
  margin-bottom: 30px;
83
+ flex-wrap: wrap;
84
  }
85
 
86
  .stat-card {
 
112
  overflow: hidden;
113
  }
114
 
115
+ .table-wrapper {
116
+ overflow-x: auto;
117
+ }
118
+
119
  table {
120
  width: 100%;
121
  border-collapse: collapse;
122
+ min-width: 800px;
123
  }
124
 
125
  th {
 
129
  font-weight: 600;
130
  color: #00d4ff;
131
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
132
+ white-space: nowrap;
133
  }
134
 
135
  td {
 
154
  background: rgba(0, 0, 0, 0.3);
155
  padding: 8px 12px;
156
  border-radius: 6px;
157
+ max-width: 400px;
158
  overflow-x: auto;
159
  white-space: pre-wrap;
160
  word-break: break-all;
161
  }
162
 
163
+ .truncate {
 
 
164
  max-width: 200px;
165
  overflow: hidden;
166
  text-overflow: ellipsis;
 
170
  .timestamp {
171
  color: #666;
172
  font-size: 0.85rem;
173
+ white-space: nowrap;
174
+ }
175
+
176
+ .status-badge {
177
+ padding: 4px 12px;
178
+ border-radius: 20px;
179
+ font-size: 0.8rem;
180
+ font-weight: 600;
181
+ }
182
+
183
+ .status-success { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
184
+ .status-failed { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
185
+ .status-queued { background: rgba(234, 179, 8, 0.2); color: #eab308; }
186
+ .status-processing { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
187
+ .status-completed { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
188
+
189
+ .credits-badge {
190
+ background: linear-gradient(135deg, #00d4ff, #7b2cbf);
191
+ padding: 4px 12px;
192
+ border-radius: 20px;
193
+ font-weight: 600;
194
  }
195
 
196
  .pagination {
 
261
  margin-bottom: 20px;
262
  opacity: 0.3;
263
  }
264
+
265
+ .hidden {
266
+ display: none;
267
+ }
268
  </style>
269
  </head>
270
 
 
272
  <div class="container">
273
  <header>
274
  <h1>🔗 API Gateway</h1>
275
+ <p class="subtitle">Data Viewer</p>
276
  </header>
277
 
278
+ <div class="tabs">
279
+ <button class="tab-btn active" onclick="switchTab('blink')">📊 Blink Data</button>
280
+ <button class="tab-btn" onclick="switchTab('users')">👥 Users</button>
281
+ <button class="tab-btn" onclick="switchTab('audit')">📝 Audit Logs</button>
282
+ <button class="tab-btn" onclick="switchTab('jobs')">⚡ Gemini Jobs</button>
283
+ </div>
284
+
285
+ <div class="stats" id="statsContainer">
286
  <div class="stat-card">
287
  <div class="stat-value" id="totalRecords">-</div>
288
  <div class="stat-label">Total Records</div>
289
  </div>
290
+ <div class="stat-card" id="uniqueUsersCard">
291
  <div class="stat-value" id="uniqueUsers">-</div>
292
  <div class="stat-label">Unique Users</div>
293
  </div>
294
  </div>
295
 
296
+ <!-- Blink Data Table -->
297
+ <div class="table-container" id="blinkTable">
298
+ <div class="table-wrapper">
299
+ <table>
300
+ <thead>
301
+ <tr>
302
+ <th>ID</th>
303
+ <th>User ID</th>
304
+ <th>Refer URL</th>
305
+ <th>IPv4</th>
306
+ <th>Country</th>
307
+ <th>Region</th>
308
+ <th>JSON Data</th>
309
+ <th>Created At</th>
310
+ </tr>
311
+ </thead>
312
+ <tbody id="blinkBody">
313
+ <tr><td colspan="8"><div class="loading"><div class="spinner"></div>Loading...</div></td></tr>
314
+ </tbody>
315
+ </table>
316
+ </div>
 
 
 
 
 
317
  <div class="pagination">
318
+ <button onclick="prevPage('blink')" id="blinkPrev" disabled>← Previous</button>
319
+ <span class="page-info">Page <span id="blinkCurrentPage">1</span> of <span id="blinkTotalPages">1</span></span>
320
+ <button onclick="nextPage('blink')" id="blinkNext">Next →</button>
321
+ </div>
322
+ </div>
323
+
324
+ <!-- Users Table -->
325
+ <div class="table-container hidden" id="usersTable">
326
+ <div class="table-wrapper">
327
+ <table>
328
+ <thead>
329
+ <tr>
330
+ <th>ID</th>
331
+ <th>User ID</th>
332
+ <th>Email</th>
333
+ <th>Name</th>
334
+ <th>Credits</th>
335
+ <th>Active</th>
336
+ <th>Created At</th>
337
+ <th>Last Used</th>
338
+ </tr>
339
+ </thead>
340
+ <tbody id="usersBody">
341
+ <tr><td colspan="8"><div class="loading"><div class="spinner"></div>Loading...</div></td></tr>
342
+ </tbody>
343
+ </table>
344
+ </div>
345
+ <div class="pagination">
346
+ <button onclick="prevPage('users')" id="usersPrev" disabled>← Previous</button>
347
+ <span class="page-info">Page <span id="usersCurrentPage">1</span> of <span id="usersTotalPages">1</span></span>
348
+ <button onclick="nextPage('users')" id="usersNext">Next →</button>
349
+ </div>
350
+ </div>
351
+
352
+ <!-- Audit Logs Table -->
353
+ <div class="table-container hidden" id="auditTable">
354
+ <div class="table-wrapper">
355
+ <table>
356
+ <thead>
357
+ <tr>
358
+ <th>ID</th>
359
+ <th>User ID</th>
360
+ <th>Action</th>
361
+ <th>IP Address</th>
362
+ <th>Status</th>
363
+ <th>Error</th>
364
+ <th>Timestamp</th>
365
+ </tr>
366
+ </thead>
367
+ <tbody id="auditBody">
368
+ <tr><td colspan="7"><div class="loading"><div class="spinner"></div>Loading...</div></td></tr>
369
+ </tbody>
370
+ </table>
371
+ </div>
372
+ <div class="pagination">
373
+ <button onclick="prevPage('audit')" id="auditPrev" disabled>← Previous</button>
374
+ <span class="page-info">Page <span id="auditCurrentPage">1</span> of <span id="auditTotalPages">1</span></span>
375
+ <button onclick="nextPage('audit')" id="auditNext">Next →</button>
376
+ </div>
377
+ </div>
378
+
379
+ <!-- Gemini Jobs Table -->
380
+ <div class="table-container hidden" id="jobsTable">
381
+ <div class="table-wrapper">
382
+ <table>
383
+ <thead>
384
+ <tr>
385
+ <th>ID</th>
386
+ <th>Job ID</th>
387
+ <th>User ID</th>
388
+ <th>Type</th>
389
+ <th>Status</th>
390
+ <th>Error</th>
391
+ <th>Created At</th>
392
+ <th>Completed At</th>
393
+ </tr>
394
+ </thead>
395
+ <tbody id="jobsBody">
396
+ <tr><td colspan="8"><div class="loading"><div class="spinner"></div>Loading...</div></td></tr>
397
+ </tbody>
398
+ </table>
399
+ </div>
400
+ <div class="pagination">
401
+ <button onclick="prevPage('jobs')" id="jobsPrev" disabled>← Previous</button>
402
+ <span class="page-info">Page <span id="jobsCurrentPage">1</span> of <span id="jobsTotalPages">1</span></span>
403
+ <button onclick="nextPage('jobs')" id="jobsNext">Next →</button>
404
  </div>
405
  </div>
406
  </div>
407
 
408
  <script>
409
+ const PAGE_SIZE = 50;
410
+ const state = {
411
+ blink: { page: 1, total: 0 },
412
+ users: { page: 1, total: 0 },
413
+ audit: { page: 1, total: 0 },
414
+ jobs: { page: 1, total: 0 }
415
+ };
416
+ let currentTab = 'blink';
417
+
418
+ const endpoints = {
419
+ blink: '/api/data',
420
+ users: '/api/users',
421
+ audit: '/api/audit-logs',
422
+ jobs: '/api/gemini-jobs'
423
+ };
424
+
425
+ function switchTab(tab) {
426
+ currentTab = tab;
427
+
428
+ // Update tab buttons
429
+ document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
430
+ event.target.classList.add('active');
431
+
432
+ // Show/hide tables
433
+ document.getElementById('blinkTable').classList.toggle('hidden', tab !== 'blink');
434
+ document.getElementById('usersTable').classList.toggle('hidden', tab !== 'users');
435
+ document.getElementById('auditTable').classList.toggle('hidden', tab !== 'audit');
436
+ document.getElementById('jobsTable').classList.toggle('hidden', tab !== 'jobs');
437
+
438
+ // Show/hide unique users stat (only for blink)
439
+ document.getElementById('uniqueUsersCard').classList.toggle('hidden', tab !== 'blink');
440
+
441
+ // Load data if not loaded
442
+ loadPage(tab, state[tab].page);
443
+ }
444
+
445
+ async function fetchData(tab, page) {
446
  try {
447
+ const response = await fetch(`${endpoints[tab]}?page=${page}&limit=${PAGE_SIZE}`);
448
+ return await response.json();
 
449
  } catch (error) {
450
  console.error('Error fetching data:', error);
451
+ return { items: [], total: 0 };
452
  }
453
  }
454
 
455
+ function renderBlinkTable(items) {
456
+ const tbody = document.getElementById('blinkBody');
 
457
  if (items.length === 0) {
458
+ tbody.innerHTML = '<tr><td colspan="8"><div class="empty-state">No data available</div></td></tr>';
 
 
 
 
 
 
 
 
 
 
 
459
  return;
460
  }
 
461
  tbody.innerHTML = items.map(item => `
462
  <tr>
463
  <td>${item.id}</td>
464
  <td class="user-id">${item.user_id}</td>
465
+ <td class="truncate" title="${item.refer_url || ''}">${item.refer_url || '-'}</td>
466
+ <td>${item.ipv4_address || item.ip_address || '-'}</td>
 
467
  <td>${item.country || '-'}</td>
468
  <td>${item.region || '-'}</td>
469
  <td><pre class="json-data">${JSON.stringify(item.json_data, null, 2)}</pre></td>
470
+ <td class="timestamp">${item.created_at ? new Date(item.created_at).toLocaleString() : '-'}</td>
471
+ </tr>
472
+ `).join('');
473
+ }
474
+
475
+ function renderUsersTable(items) {
476
+ const tbody = document.getElementById('usersBody');
477
+ if (items.length === 0) {
478
+ tbody.innerHTML = '<tr><td colspan="8"><div class="empty-state">No users found</div></td></tr>';
479
+ return;
480
+ }
481
+ tbody.innerHTML = items.map(item => `
482
+ <tr>
483
+ <td>${item.id}</td>
484
+ <td class="user-id">${item.user_id}</td>
485
+ <td>${item.email}</td>
486
+ <td>${item.name || '-'}</td>
487
+ <td><span class="credits-badge">${item.credits}</span></td>
488
+ <td><span class="status-badge ${item.is_active ? 'status-success' : 'status-failed'}">${item.is_active ? 'Active' : 'Inactive'}</span></td>
489
+ <td class="timestamp">${item.created_at ? new Date(item.created_at).toLocaleString() : '-'}</td>
490
+ <td class="timestamp">${item.last_used_at ? new Date(item.last_used_at).toLocaleString() : '-'}</td>
491
  </tr>
492
  `).join('');
493
  }
494
 
495
+ function renderAuditTable(items) {
496
+ const tbody = document.getElementById('auditBody');
497
+ if (items.length === 0) {
498
+ tbody.innerHTML = '<tr><td colspan="7"><div class="empty-state">No audit logs found</div></td></tr>';
499
+ return;
500
+ }
501
+ tbody.innerHTML = items.map(item => `
502
+ <tr>
503
+ <td>${item.id}</td>
504
+ <td class="user-id">${item.user_id || '-'}</td>
505
+ <td>${item.action}</td>
506
+ <td>${item.ip_address}</td>
507
+ <td><span class="status-badge status-${item.status}">${item.status}</span></td>
508
+ <td class="truncate" title="${item.error_message || ''}">${item.error_message || '-'}</td>
509
+ <td class="timestamp">${item.timestamp ? new Date(item.timestamp).toLocaleString() : '-'}</td>
510
+ </tr>
511
+ `).join('');
512
  }
513
 
514
+ function renderJobsTable(items) {
515
+ const tbody = document.getElementById('jobsBody');
516
+ if (items.length === 0) {
517
+ tbody.innerHTML = '<tr><td colspan="8"><div class="empty-state">No jobs found</div></td></tr>';
518
+ return;
519
+ }
520
+ tbody.innerHTML = items.map(item => `
521
+ <tr>
522
+ <td>${item.id}</td>
523
+ <td class="user-id">${item.job_id}</td>
524
+ <td class="user-id">${item.user_id}</td>
525
+ <td>${item.job_type}</td>
526
+ <td><span class="status-badge status-${item.status}">${item.status}</span></td>
527
+ <td class="truncate" title="${item.error_message || ''}">${item.error_message || '-'}</td>
528
+ <td class="timestamp">${item.created_at ? new Date(item.created_at).toLocaleString() : '-'}</td>
529
+ <td class="timestamp">${item.completed_at ? new Date(item.completed_at).toLocaleString() : '-'}</td>
530
+ </tr>
531
+ `).join('');
532
+ }
533
+
534
+ const renderers = {
535
+ blink: renderBlinkTable,
536
+ users: renderUsersTable,
537
+ audit: renderAuditTable,
538
+ jobs: renderJobsTable
539
+ };
540
+
541
+ function updatePagination(tab) {
542
+ const totalPages = Math.ceil(state[tab].total / PAGE_SIZE) || 1;
543
+ document.getElementById(`${tab}CurrentPage`).textContent = state[tab].page;
544
+ document.getElementById(`${tab}TotalPages`).textContent = totalPages;
545
+ document.getElementById(`${tab}Prev`).disabled = state[tab].page <= 1;
546
+ document.getElementById(`${tab}Next`).disabled = state[tab].page >= totalPages;
547
+ }
548
+
549
+ async function loadPage(tab, page) {
550
+ state[tab].page = page;
551
+ const data = await fetchData(tab, page);
552
+ state[tab].total = data.total;
553
 
554
  document.getElementById('totalRecords').textContent = data.total.toLocaleString();
555
+ if (data.unique_users !== undefined) {
556
+ document.getElementById('uniqueUsers').textContent = data.unique_users.toLocaleString();
557
+ }
558
 
559
+ renderers[tab](data.items);
560
+ updatePagination(tab);
561
  }
562
 
563
+ function prevPage(tab) {
564
+ if (state[tab].page > 1) {
565
+ loadPage(tab, state[tab].page - 1);
566
  }
567
  }
568
 
569
+ function nextPage(tab) {
570
+ const totalPages = Math.ceil(state[tab].total / PAGE_SIZE);
571
+ if (state[tab].page < totalPages) {
572
+ loadPage(tab, state[tab].page + 1);
573
  }
574
  }
575
 
576
  // Initial load
577
+ loadPage('blink', 1);
578
  </script>
579
  </body>
580