Fadhili Sumaye commited on
Commit
436892e
·
1 Parent(s): cb1034b

Implement admin password page, database indexing, and optimize app lifecycle polling

Browse files
app/src/main/java/com/example/pestdetection/ApiConfig.java CHANGED
@@ -27,17 +27,6 @@ public final class ApiConfig {
27
  return toHealthUrl(getPredictUrl(context));
28
  }
29
 
30
- public static String getLoginUrl(Context context) {
31
- return toAuthUrl(getPredictUrl(context), "/auth/login");
32
- }
33
-
34
- public static String getRegisterUrl(Context context) {
35
- return toAuthUrl(getPredictUrl(context), "/auth/register");
36
- }
37
-
38
- public static String getHistoryUrl(Context context) {
39
- return toAuthUrl(getPredictUrl(context), "/history");
40
- }
41
 
42
  public static void savePredictUrl(Context context, String url) {
43
  if (url == null || url.trim().isEmpty()) {
@@ -72,10 +61,4 @@ public final class ApiConfig {
72
  return predictUrl + "/health";
73
  }
74
 
75
- private static String toAuthUrl(String predictUrl, String endpoint) {
76
- if (predictUrl.endsWith("/predict")) {
77
- return predictUrl.substring(0, predictUrl.length() - "/predict".length()) + endpoint;
78
- }
79
- return predictUrl + endpoint;
80
- }
81
  }
 
27
  return toHealthUrl(getPredictUrl(context));
28
  }
29
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  public static void savePredictUrl(Context context, String url) {
32
  if (url == null || url.trim().isEmpty()) {
 
61
  return predictUrl + "/health";
62
  }
63
 
 
 
 
 
 
 
64
  }
app/src/main/java/com/example/pestdetection/MainActivity.java CHANGED
@@ -188,8 +188,19 @@ public class MainActivity extends AppCompatActivity {
188
  protected void onResume() {
189
  super.onResume();
190
  checkBackendConnection();
 
 
 
191
  }
192
 
 
 
 
 
 
 
 
 
193
 
194
  @Override
195
  protected void onDestroy() {
@@ -396,7 +407,7 @@ public class MainActivity extends AppCompatActivity {
396
  } else if ("None".equalsIgnoreCase(pest)) {
397
  resultText.setText("No Pests Detected");
398
  treatmentText.setText(treatment);
399
- if (btnReportUnrecognized != null) btnReportUnrecognized.setVisibility(View.GONE);
400
  } else {
401
  resultText.setText(pest + " (" + (int) (confidence * 100) + "%)");
402
  treatmentText.setText(treatment);
 
188
  protected void onResume() {
189
  super.onResume();
190
  checkBackendConnection();
191
+ if (activeReportId != -1) {
192
+ startPolling();
193
+ }
194
  }
195
 
196
+ @Override
197
+ protected void onPause() {
198
+ super.onPause();
199
+ if (pollingRunnable != null) {
200
+ pollingHandler.removeCallbacks(pollingRunnable);
201
+ pollingRunnable = null;
202
+ }
203
+ }
204
 
205
  @Override
206
  protected void onDestroy() {
 
407
  } else if ("None".equalsIgnoreCase(pest)) {
408
  resultText.setText("No Pests Detected");
409
  treatmentText.setText(treatment);
410
+ if (btnReportUnrecognized != null) btnReportUnrecognized.setVisibility(View.VISIBLE);
411
  } else {
412
  resultText.setText(pest + " (" + (int) (confidence * 100) + "%)");
413
  treatmentText.setText(treatment);
backend/admin.html CHANGED
@@ -308,55 +308,73 @@
308
  </style>
309
  </head>
310
  <body>
311
- <header>
312
- <div class="logo-container">
313
- <span class="logo-icon">🌾</span>
314
- <div>
315
- <h1>Pest Detection Admin</h1>
316
- <p style="font-size: 0.75rem; color: var(--text-muted);">Real-time Crop Diagnostics Console</p>
317
- </div>
 
 
 
 
 
 
318
  </div>
319
- <div class="nav-stats">
320
- <div class="stat-badge">
321
- <span>Pending Review:</span>
322
- <span class="stat-count pending" id="pending-count">0</span>
 
 
 
 
 
 
323
  </div>
324
- <div class="stat-badge">
325
- <span>Resolved:</span>
326
- <span class="stat-count" id="resolved-count">0</span>
 
 
 
 
 
 
327
  </div>
328
- </div>
329
- </header>
330
 
331
- <main>
332
- <div class="notification-banner" id="notif-banner" style="display: none;">
333
- <p>🔔 Enable desktop notifications to receive instant audio and visual alerts when users report unrecognized pests.</p>
334
- <button onclick="requestNotificationPermission()">Enable Notifications</button>
335
- </div>
336
 
337
- <section>
338
- <h2 class="section-title">🕒 Pending User Submissions</h2>
339
- <div class="reports-grid" id="pending-reports-grid">
340
- <div class="empty-state">
341
- <div class="empty-icon">🎉</div>
342
- <p>All clear! There are no pending unrecognized pest reports.</p>
 
343
  </div>
344
- </div>
345
- </section>
346
 
347
- <section style="margin-top: 4rem;">
348
- <h2 class="section-title">✅ Resolved Submissions</h2>
349
- <div class="reports-grid" id="resolved-reports-grid">
350
- <div class="empty-state">
351
- <p>No reports resolved yet.</p>
 
352
  </div>
353
- </div>
354
- </section>
355
- </main>
356
 
357
- <footer>
358
- <p>Administered by Fadhili and Didas</p>
359
- </footer>
 
360
 
361
  <audio id="alert-sound" src="https://assets.mixkit.co/active_storage/sfx/911/911-500.wav" preload="auto"></audio>
362
 
@@ -364,9 +382,55 @@
364
  let previousPendingIds = new Set();
365
  let isInitialLoad = true;
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  async function fetchReports() {
368
  try {
369
- const response = await fetch('/admin/api/reports');
 
 
 
 
 
 
 
 
370
  const data = await response.json();
371
  if (data.status === 'success') {
372
  renderReports(data.reports);
@@ -383,6 +447,9 @@
383
  const pendingReports = reports.filter(r => r.status === 'pending');
384
  const resolvedReports = reports.filter(r => r.status === 'resolved');
385
 
 
 
 
386
  // Update stats in header
387
  document.getElementById('pending-count').textContent = pendingReports.length;
388
  document.getElementById('resolved-count').textContent = resolvedReports.length;
@@ -424,7 +491,7 @@
424
  pendingGrid.innerHTML = pendingReports.map(r => `
425
  <div class="report-card" id="card-${r.id}">
426
  <div class="report-image-container">
427
- <img class="report-image" src="/admin/image/${r.image_path}" alt="Unrecognized pest">
428
  </div>
429
  <div class="report-details">
430
  <div>
@@ -461,7 +528,7 @@
461
  resolvedGrid.innerHTML = resolvedReports.map(r => `
462
  <div class="report-card resolved-card">
463
  <div class="report-image-container">
464
- <img class="report-image" src="/admin/image/${r.image_path}" alt="Resolved pest">
465
  </div>
466
  <div class="report-details">
467
  <div>
@@ -494,8 +561,14 @@
494
  try {
495
  const response = await fetch(`/admin/api/resolve/${id}`, {
496
  method: 'POST',
 
497
  body: formData
498
  });
 
 
 
 
 
499
  const data = await response.json();
500
  if (data.status === 'success') {
501
  // Remove card with visual fade effect
@@ -546,10 +619,16 @@
546
  }
547
  }
548
 
549
- // Initialize and poll
550
- checkNotificationPermission();
551
- fetchReports();
552
- setInterval(fetchReports, 10000);
 
 
 
 
 
 
553
  </script>
554
  </body>
555
  </html>
 
308
  </style>
309
  </head>
310
  <body>
311
+ <div id="login-view" style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 2rem; flex: 1;">
312
+ <div style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 1.5rem; padding: 3rem; width: 100%; max-width: 450px; text-align: center; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
313
+ <span style="font-size: 3.5rem; display: block; margin-bottom: 1rem;">🌾</span>
314
+ <h2 style="font-size: 1.75rem; margin-bottom: 0.5rem; font-weight: 700; background: linear-gradient(to right, #10b981, #34d399); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Pest Detection Admin</h2>
315
+ <p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 2rem;">Enter admin password to access the console</p>
316
+ <form onsubmit="handleLogin(event)" style="display: flex; flex-direction: column; gap: 1rem; width: 100%;">
317
+ <div style="display: flex; flex-direction: column; text-align: left; gap: 0.5rem;">
318
+ <label for="admin-password">Admin Password</label>
319
+ <input type="password" id="admin-password" required placeholder="••••••••" style="background-color: #0b0f19; border: 1px solid var(--card-border); border-radius: 0.5rem; padding: 0.75rem 1rem; color: var(--text-main); font-size: 1rem; outline: none; transition: border-color 0.2s ease;">
320
+ </div>
321
+ <div id="login-error" style="color: var(--danger); font-size: 0.875rem; display: none; text-align: left; margin-top: 0.25rem;">❌ Incorrect password, please try again.</div>
322
+ <button type="submit" style="background-color: var(--accent-primary); color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 0.5rem; cursor: pointer; font-weight: 600; font-size: 1rem; transition: background-color 0.2s ease; margin-top: 0.5rem;">Access Dashboard</button>
323
+ </form>
324
  </div>
325
+ </div>
326
+
327
+ <div id="dashboard-view" style="display: none; flex-direction: column; flex: 1; width: 100%;">
328
+ <header>
329
+ <div class="logo-container">
330
+ <span class="logo-icon">🌾</span>
331
+ <div>
332
+ <h1>Pest Detection Admin</h1>
333
+ <p style="font-size: 0.75rem; color: var(--text-muted);">Real-time Crop Diagnostics Console</p>
334
+ </div>
335
  </div>
336
+ <div class="nav-stats">
337
+ <div class="stat-badge">
338
+ <span>Pending Review:</span>
339
+ <span class="stat-count pending" id="pending-count">0</span>
340
+ </div>
341
+ <div class="stat-badge">
342
+ <span>Resolved:</span>
343
+ <span class="stat-count" id="resolved-count">0</span>
344
+ </div>
345
  </div>
346
+ </header>
 
347
 
348
+ <main>
349
+ <div class="notification-banner" id="notif-banner" style="display: none;">
350
+ <p>🔔 Enable desktop notifications to receive instant audio and visual alerts when users report unrecognized pests.</p>
351
+ <button onclick="requestNotificationPermission()">Enable Notifications</button>
352
+ </div>
353
 
354
+ <section>
355
+ <h2 class="section-title">🕒 Pending User Submissions</h2>
356
+ <div class="reports-grid" id="pending-reports-grid">
357
+ <div class="empty-state">
358
+ <div class="empty-icon">🎉</div>
359
+ <p>All clear! There are no pending unrecognized pest reports.</p>
360
+ </div>
361
  </div>
362
+ </section>
 
363
 
364
+ <section style="margin-top: 4rem;">
365
+ <h2 class="section-title">✅ Resolved Submissions</h2>
366
+ <div class="reports-grid" id="resolved-reports-grid">
367
+ <div class="empty-state">
368
+ <p>No reports resolved yet.</p>
369
+ </div>
370
  </div>
371
+ </section>
372
+ </main>
 
373
 
374
+ <footer>
375
+ <p>Administered by Fadhili and Didas</p>
376
+ </footer>
377
+ </div>
378
 
379
  <audio id="alert-sound" src="https://assets.mixkit.co/active_storage/sfx/911/911-500.wav" preload="auto"></audio>
380
 
 
382
  let previousPendingIds = new Set();
383
  let isInitialLoad = true;
384
 
385
+ function getAuthHeaders() {
386
+ const password = sessionStorage.getItem('adminPassword') || '';
387
+ return {
388
+ 'X-Admin-Password': password
389
+ };
390
+ }
391
+
392
+ async function handleLogin(event) {
393
+ event.preventDefault();
394
+ const passwordInput = document.getElementById('admin-password').value;
395
+ const errorDiv = document.getElementById('login-error');
396
+
397
+ try {
398
+ const response = await fetch('/admin/api/reports', {
399
+ headers: { 'X-Admin-Password': passwordInput }
400
+ });
401
+
402
+ if (response.status === 200) {
403
+ sessionStorage.setItem('adminPassword', passwordInput);
404
+ errorDiv.style.display = 'none';
405
+ showDashboard();
406
+ } else {
407
+ errorDiv.style.display = 'block';
408
+ }
409
+ } catch (err) {
410
+ console.error("Login failed:", err);
411
+ errorDiv.style.display = 'block';
412
+ }
413
+ }
414
+
415
+ function showDashboard() {
416
+ document.getElementById('login-view').style.display = 'none';
417
+ document.getElementById('dashboard-view').style.display = 'flex';
418
+ checkNotificationPermission();
419
+ fetchReports();
420
+ setInterval(fetchReports, 10000);
421
+ }
422
+
423
  async function fetchReports() {
424
  try {
425
+ const response = await fetch('/admin/api/reports', {
426
+ headers: getAuthHeaders()
427
+ });
428
+ if (response.status === 401) {
429
+ // Password expired or changed, kick back to login
430
+ sessionStorage.removeItem('adminPassword');
431
+ location.reload();
432
+ return;
433
+ }
434
  const data = await response.json();
435
  if (data.status === 'success') {
436
  renderReports(data.reports);
 
447
  const pendingReports = reports.filter(r => r.status === 'pending');
448
  const resolvedReports = reports.filter(r => r.status === 'resolved');
449
 
450
+ const password = sessionStorage.getItem('adminPassword') || '';
451
+ const passwordParam = `?password=${encodeURIComponent(password)}`;
452
+
453
  // Update stats in header
454
  document.getElementById('pending-count').textContent = pendingReports.length;
455
  document.getElementById('resolved-count').textContent = resolvedReports.length;
 
491
  pendingGrid.innerHTML = pendingReports.map(r => `
492
  <div class="report-card" id="card-${r.id}">
493
  <div class="report-image-container">
494
+ <img class="report-image" src="/admin/image/${r.image_path}${passwordParam}" alt="Unrecognized pest">
495
  </div>
496
  <div class="report-details">
497
  <div>
 
528
  resolvedGrid.innerHTML = resolvedReports.map(r => `
529
  <div class="report-card resolved-card">
530
  <div class="report-image-container">
531
+ <img class="report-image" src="/admin/image/${r.image_path}${passwordParam}" alt="Resolved pest">
532
  </div>
533
  <div class="report-details">
534
  <div>
 
561
  try {
562
  const response = await fetch(`/admin/api/resolve/${id}`, {
563
  method: 'POST',
564
+ headers: getAuthHeaders(),
565
  body: formData
566
  });
567
+ if (response.status === 401) {
568
+ sessionStorage.removeItem('adminPassword');
569
+ location.reload();
570
+ return;
571
+ }
572
  const data = await response.json();
573
  if (data.status === 'success') {
574
  // Remove card with visual fade effect
 
619
  }
620
  }
621
 
622
+ // Initialize session check
623
+ window.addEventListener('DOMContentLoaded', () => {
624
+ const savedPassword = sessionStorage.getItem('adminPassword');
625
+ if (savedPassword) {
626
+ showDashboard();
627
+ } else {
628
+ document.getElementById('login-view').style.display = 'flex';
629
+ document.getElementById('dashboard-view').style.display = 'none';
630
+ }
631
+ });
632
  </script>
633
  </body>
634
  </html>
backend/app.py CHANGED
@@ -49,6 +49,7 @@ def init_db():
49
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
50
  )
51
  """)
 
52
  conn.commit()
53
 
54
  # Prune audit logs older than 30 days
@@ -78,6 +79,21 @@ def log_audit(username: Optional[str], endpoint: str, status: str, details: str,
78
 
79
  init_db()
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  app = FastAPI(title="Pest Detection API")
82
 
83
  # Configure CORS Middleware
@@ -605,7 +621,7 @@ def admin_dashboard():
605
  return HTMLResponse(content="<h1>Admin Dashboard Template Missing</h1>")
606
 
607
  @app.get("/admin/api/reports")
608
- def admin_get_reports():
609
  try:
610
  conn = sqlite3.connect(str(DB_FILE), timeout=30.0)
611
  conn.row_factory = sqlite3.Row
@@ -633,7 +649,8 @@ def admin_get_reports():
633
  def admin_resolve_report(
634
  report_id: int,
635
  pest_name: str = Form(...),
636
- treatment: str = Form(...)
 
637
  ):
638
  try:
639
  conn = sqlite3.connect(str(DB_FILE), timeout=30.0)
@@ -649,7 +666,7 @@ def admin_resolve_report(
649
  raise HTTPException(status_code=500, detail=f"Database error: {e}")
650
 
651
  @app.get("/admin/image/{filename}")
652
- def admin_serve_image(filename: str):
653
  file_path = BASE_DIR / "uploads" / "unrecognized" / filename
654
  if file_path.exists():
655
  return FileResponse(str(file_path))
 
49
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
50
  )
51
  """)
52
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_unrecognized_reports_device_id ON unrecognized_reports (device_id);")
53
  conn.commit()
54
 
55
  # Prune audit logs older than 30 days
 
79
 
80
  init_db()
81
 
82
+ from fastapi import Depends, Header, Query
83
+
84
+ def authenticate_admin(
85
+ x_admin_password: Optional[str] = Header(None),
86
+ password: Optional[str] = Query(None)
87
+ ):
88
+ admin_pass = os.environ.get("ADMIN_PASSWORD", "admin123")
89
+ passed_pass = x_admin_password or password
90
+ if not passed_pass or passed_pass != admin_pass:
91
+ raise HTTPException(
92
+ status_code=401,
93
+ detail="Unauthorized: Invalid admin password"
94
+ )
95
+ return True
96
+
97
  app = FastAPI(title="Pest Detection API")
98
 
99
  # Configure CORS Middleware
 
621
  return HTMLResponse(content="<h1>Admin Dashboard Template Missing</h1>")
622
 
623
  @app.get("/admin/api/reports")
624
+ def admin_get_reports(authenticated: bool = Depends(authenticate_admin)):
625
  try:
626
  conn = sqlite3.connect(str(DB_FILE), timeout=30.0)
627
  conn.row_factory = sqlite3.Row
 
649
  def admin_resolve_report(
650
  report_id: int,
651
  pest_name: str = Form(...),
652
+ treatment: str = Form(...),
653
+ authenticated: bool = Depends(authenticate_admin)
654
  ):
655
  try:
656
  conn = sqlite3.connect(str(DB_FILE), timeout=30.0)
 
666
  raise HTTPException(status_code=500, detail=f"Database error: {e}")
667
 
668
  @app.get("/admin/image/{filename}")
669
+ def admin_serve_image(filename: str, authenticated: bool = Depends(authenticate_admin)):
670
  file_path = BASE_DIR / "uploads" / "unrecognized" / filename
671
  if file_path.exists():
672
  return FileResponse(str(file_path))