jebin2 commited on
Commit
0c30f00
Β·
1 Parent(s): a2c1726

Add HTML UI with paginated table view

Browse files
Files changed (2) hide show
  1. app.py +71 -1
  2. templates/index.html +337 -0
app.py CHANGED
@@ -10,8 +10,10 @@ from typing import Optional
10
 
11
  from fastapi import FastAPI, Query, Request, Depends, HTTPException, status
12
  from fastapi.middleware.cors import CORSMiddleware
13
- from fastapi.responses import JSONResponse
 
14
  from sqlalchemy.ext.asyncio import AsyncSession
 
15
 
16
  from database import get_db, init_db
17
  from models import BlinkData
@@ -70,6 +72,74 @@ async def health_check():
70
  return {"status": "healthy", "service": "url-blink-api"}
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  @app.get("/blink")
74
  async def blink(
75
  request: Request,
 
10
 
11
  from fastapi import FastAPI, Query, Request, Depends, HTTPException, status
12
  from fastapi.middleware.cors import CORSMiddleware
13
+ from fastapi.responses import JSONResponse, HTMLResponse
14
+ from fastapi.staticfiles import StaticFiles
15
  from sqlalchemy.ext.asyncio import AsyncSession
16
+ from sqlalchemy import select, func
17
 
18
  from database import get_db, init_db
19
  from models import BlinkData
 
72
  return {"status": "healthy", "service": "url-blink-api"}
73
 
74
 
75
+ @app.get("/", response_class=HTMLResponse)
76
+ async def root():
77
+ """
78
+ Serve the main HTML page.
79
+ """
80
+ import os
81
+ template_path = os.path.join(os.path.dirname(__file__), "templates", "index.html")
82
+ with open(template_path, "r") as f:
83
+ return HTMLResponse(content=f.read())
84
+
85
+
86
+ @app.get("/api/data")
87
+ async def get_data(
88
+ page: int = Query(1, ge=1, description="Page number"),
89
+ limit: int = Query(100, ge=1, le=500, description="Items per page"),
90
+ db: AsyncSession = Depends(get_db)
91
+ ):
92
+ """
93
+ Get paginated blink data.
94
+
95
+ Args:
96
+ page: Page number (1-indexed)
97
+ limit: Number of items per page
98
+ db: Database session
99
+
100
+ Returns:
101
+ Paginated list of blink data records
102
+ """
103
+ try:
104
+ offset = (page - 1) * limit
105
+
106
+ # Get total count
107
+ total_result = await db.execute(select(func.count(BlinkData.id)))
108
+ total = total_result.scalar() or 0
109
+
110
+ # Get unique users count
111
+ unique_result = await db.execute(select(func.count(func.distinct(BlinkData.user_id))))
112
+ unique_users = unique_result.scalar() or 0
113
+
114
+ # Get paginated items
115
+ query = select(BlinkData).order_by(BlinkData.id.desc()).offset(offset).limit(limit)
116
+ result = await db.execute(query)
117
+ items = result.scalars().all()
118
+
119
+ return {
120
+ "items": [
121
+ {
122
+ "id": item.id,
123
+ "user_id": item.user_id,
124
+ "refer_url": item.refer_url,
125
+ "json_data": item.json_data,
126
+ "created_at": item.created_at.isoformat() if item.created_at else None
127
+ }
128
+ for item in items
129
+ ],
130
+ "total": total,
131
+ "unique_users": unique_users,
132
+ "page": page,
133
+ "limit": limit
134
+ }
135
+ except Exception as e:
136
+ logger.error(f"Error fetching data: {e}")
137
+ raise HTTPException(
138
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
139
+ detail="Error fetching data"
140
+ )
141
+
142
+
143
  @app.get("/blink")
144
  async def blink(
145
  request: Request,
templates/index.html ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>API Gateway - Blink Data</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
17
+ min-height: 100vh;
18
+ color: #e4e4e4;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ header {
28
+ text-align: center;
29
+ margin-bottom: 30px;
30
+ }
31
+
32
+ h1 {
33
+ font-size: 2.5rem;
34
+ background: linear-gradient(90deg, #00d4ff, #7b2cbf);
35
+ -webkit-background-clip: text;
36
+ -webkit-text-fill-color: transparent;
37
+ background-clip: text;
38
+ margin-bottom: 10px;
39
+ }
40
+
41
+ .subtitle {
42
+ color: #888;
43
+ font-size: 1rem;
44
+ }
45
+
46
+ .stats {
47
+ display: flex;
48
+ gap: 20px;
49
+ justify-content: center;
50
+ margin-bottom: 30px;
51
+ }
52
+
53
+ .stat-card {
54
+ background: rgba(255, 255, 255, 0.05);
55
+ backdrop-filter: blur(10px);
56
+ border: 1px solid rgba(255, 255, 255, 0.1);
57
+ border-radius: 12px;
58
+ padding: 20px 40px;
59
+ text-align: center;
60
+ }
61
+
62
+ .stat-value {
63
+ font-size: 2rem;
64
+ font-weight: bold;
65
+ color: #00d4ff;
66
+ }
67
+
68
+ .stat-label {
69
+ color: #888;
70
+ font-size: 0.9rem;
71
+ margin-top: 5px;
72
+ }
73
+
74
+ .table-container {
75
+ background: rgba(255, 255, 255, 0.03);
76
+ backdrop-filter: blur(10px);
77
+ border: 1px solid rgba(255, 255, 255, 0.1);
78
+ border-radius: 16px;
79
+ overflow: hidden;
80
+ }
81
+
82
+ table {
83
+ width: 100%;
84
+ border-collapse: collapse;
85
+ }
86
+
87
+ th {
88
+ background: rgba(0, 212, 255, 0.1);
89
+ padding: 16px;
90
+ text-align: left;
91
+ font-weight: 600;
92
+ color: #00d4ff;
93
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
94
+ }
95
+
96
+ td {
97
+ padding: 14px 16px;
98
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
99
+ vertical-align: top;
100
+ }
101
+
102
+ tr:hover {
103
+ background: rgba(255, 255, 255, 0.03);
104
+ }
105
+
106
+ .user-id {
107
+ font-family: 'Courier New', monospace;
108
+ color: #7b2cbf;
109
+ font-weight: 500;
110
+ }
111
+
112
+ .json-data {
113
+ font-family: 'Courier New', monospace;
114
+ font-size: 0.85rem;
115
+ background: rgba(0, 0, 0, 0.3);
116
+ padding: 8px 12px;
117
+ border-radius: 6px;
118
+ max-width: 500px;
119
+ overflow-x: auto;
120
+ white-space: pre-wrap;
121
+ word-break: break-all;
122
+ }
123
+
124
+ .refer-url {
125
+ color: #888;
126
+ font-size: 0.9rem;
127
+ max-width: 200px;
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ white-space: nowrap;
131
+ }
132
+
133
+ .timestamp {
134
+ color: #666;
135
+ font-size: 0.85rem;
136
+ }
137
+
138
+ .pagination {
139
+ display: flex;
140
+ justify-content: center;
141
+ align-items: center;
142
+ gap: 15px;
143
+ padding: 20px;
144
+ background: rgba(0, 0, 0, 0.2);
145
+ }
146
+
147
+ .pagination button {
148
+ background: linear-gradient(135deg, #00d4ff, #7b2cbf);
149
+ border: none;
150
+ color: white;
151
+ padding: 10px 24px;
152
+ border-radius: 8px;
153
+ cursor: pointer;
154
+ font-weight: 600;
155
+ transition: all 0.3s ease;
156
+ }
157
+
158
+ .pagination button:hover:not(:disabled) {
159
+ transform: translateY(-2px);
160
+ box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
161
+ }
162
+
163
+ .pagination button:disabled {
164
+ opacity: 0.5;
165
+ cursor: not-allowed;
166
+ }
167
+
168
+ .page-info {
169
+ color: #888;
170
+ }
171
+
172
+ .loading {
173
+ text-align: center;
174
+ padding: 60px;
175
+ color: #888;
176
+ }
177
+
178
+ .spinner {
179
+ width: 40px;
180
+ height: 40px;
181
+ border: 3px solid rgba(255, 255, 255, 0.1);
182
+ border-top-color: #00d4ff;
183
+ border-radius: 50%;
184
+ animation: spin 1s linear infinite;
185
+ margin: 0 auto 20px;
186
+ }
187
+
188
+ @keyframes spin {
189
+ to { transform: rotate(360deg); }
190
+ }
191
+
192
+ .empty-state {
193
+ text-align: center;
194
+ padding: 60px;
195
+ color: #666;
196
+ }
197
+
198
+ .empty-state svg {
199
+ width: 80px;
200
+ height: 80px;
201
+ margin-bottom: 20px;
202
+ opacity: 0.3;
203
+ }
204
+ </style>
205
+ </head>
206
+ <body>
207
+ <div class="container">
208
+ <header>
209
+ <h1>πŸ”— API Gateway</h1>
210
+ <p class="subtitle">Blink Data Viewer</p>
211
+ </header>
212
+
213
+ <div class="stats">
214
+ <div class="stat-card">
215
+ <div class="stat-value" id="totalRecords">-</div>
216
+ <div class="stat-label">Total Records</div>
217
+ </div>
218
+ <div class="stat-card">
219
+ <div class="stat-value" id="uniqueUsers">-</div>
220
+ <div class="stat-label">Unique Users</div>
221
+ </div>
222
+ </div>
223
+
224
+ <div class="table-container">
225
+ <table>
226
+ <thead>
227
+ <tr>
228
+ <th>ID</th>
229
+ <th>User ID</th>
230
+ <th>Refer URL</th>
231
+ <th>JSON Data</th>
232
+ <th>Created At</th>
233
+ </tr>
234
+ </thead>
235
+ <tbody id="tableBody">
236
+ <tr>
237
+ <td colspan="5">
238
+ <div class="loading">
239
+ <div class="spinner"></div>
240
+ Loading data...
241
+ </div>
242
+ </td>
243
+ </tr>
244
+ </tbody>
245
+ </table>
246
+ <div class="pagination">
247
+ <button id="prevBtn" onclick="prevPage()" disabled>← Previous</button>
248
+ <span class="page-info">Page <span id="currentPage">1</span> of <span id="totalPages">1</span></span>
249
+ <button id="nextBtn" onclick="nextPage()">Next β†’</button>
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ <script>
255
+ const PAGE_SIZE = 100;
256
+ let currentPage = 1;
257
+ let totalRecords = 0;
258
+
259
+ async function fetchData(page = 1) {
260
+ try {
261
+ const response = await fetch(`/api/data?page=${page}&limit=${PAGE_SIZE}`);
262
+ const data = await response.json();
263
+ return data;
264
+ } catch (error) {
265
+ console.error('Error fetching data:', error);
266
+ return { items: [], total: 0, unique_users: 0 };
267
+ }
268
+ }
269
+
270
+ function renderTable(items) {
271
+ const tbody = document.getElementById('tableBody');
272
+
273
+ if (items.length === 0) {
274
+ tbody.innerHTML = `
275
+ <tr>
276
+ <td colspan="5">
277
+ <div class="empty-state">
278
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
279
+ <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" />
280
+ </svg>
281
+ <p>No data available</p>
282
+ </div>
283
+ </td>
284
+ </tr>
285
+ `;
286
+ return;
287
+ }
288
+
289
+ tbody.innerHTML = items.map(item => `
290
+ <tr>
291
+ <td>${item.id}</td>
292
+ <td class="user-id">${item.user_id}</td>
293
+ <td class="refer-url" title="${item.refer_url || ''}">${item.refer_url || '-'}</td>
294
+ <td><pre class="json-data">${JSON.stringify(item.json_data, null, 2)}</pre></td>
295
+ <td class="timestamp">${new Date(item.created_at).toLocaleString()}</td>
296
+ </tr>
297
+ `).join('');
298
+ }
299
+
300
+ function updatePagination() {
301
+ const totalPages = Math.ceil(totalRecords / PAGE_SIZE) || 1;
302
+ document.getElementById('currentPage').textContent = currentPage;
303
+ document.getElementById('totalPages').textContent = totalPages;
304
+ document.getElementById('prevBtn').disabled = currentPage <= 1;
305
+ document.getElementById('nextBtn').disabled = currentPage >= totalPages;
306
+ }
307
+
308
+ async function loadPage(page) {
309
+ currentPage = page;
310
+ const data = await fetchData(page);
311
+ totalRecords = data.total;
312
+
313
+ document.getElementById('totalRecords').textContent = data.total.toLocaleString();
314
+ document.getElementById('uniqueUsers').textContent = data.unique_users.toLocaleString();
315
+
316
+ renderTable(data.items);
317
+ updatePagination();
318
+ }
319
+
320
+ function prevPage() {
321
+ if (currentPage > 1) {
322
+ loadPage(currentPage - 1);
323
+ }
324
+ }
325
+
326
+ function nextPage() {
327
+ const totalPages = Math.ceil(totalRecords / PAGE_SIZE);
328
+ if (currentPage < totalPages) {
329
+ loadPage(currentPage + 1);
330
+ }
331
+ }
332
+
333
+ // Initial load
334
+ loadPage(1);
335
+ </script>
336
+ </body>
337
+ </html>