coderuday21 commited on
Commit
e55b60b
·
1 Parent(s): 1691e34

Remove login; open detection page directly

Browse files
Files changed (6) hide show
  1. Dockerfile +1 -1
  2. app/auth.py +17 -0
  3. app/main.py +9 -30
  4. static/css/style.css +14 -2
  5. static/js/app.js +12 -130
  6. templates/index.html +7 -138
Dockerfile CHANGED
@@ -19,7 +19,7 @@ WORKDIR /app
19
 
20
  # Build-time info + cache-bust:
21
  # Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
22
- ARG APP_BUILD=21
23
  ENV APP_BUILD=${APP_BUILD}
24
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
25
 
 
19
 
20
  # Build-time info + cache-bust:
21
  # Changing APP_BUILD forces Docker to re-run subsequent layers (including pip install).
22
+ ARG APP_BUILD=22
23
  ENV APP_BUILD=${APP_BUILD}
24
  RUN echo "Docker build start: APP_BUILD=${APP_BUILD}" && python -V
25
 
app/auth.py CHANGED
@@ -71,6 +71,23 @@ def get_user_from_token(token: str, db: Session) -> Optional[User]:
71
  return get_user_by_id(db, user_id)
72
 
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  def get_current_user(
75
  request: Request,
76
  credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
 
71
  return get_user_by_id(db, user_id)
72
 
73
 
74
+ def get_or_create_guest_user(db: Session) -> User:
75
+ """Shared anonymous account when login is disabled."""
76
+ guest_email = "__guest__@system.local"
77
+ user = get_user_by_email(db, guest_email)
78
+ if user:
79
+ return user
80
+ user = User(
81
+ email=guest_email,
82
+ hashed_password=get_password_hash("guest-not-used"),
83
+ full_name="Guest",
84
+ )
85
+ db.add(user)
86
+ db.commit()
87
+ db.refresh(user)
88
+ return user
89
+
90
+
91
  def get_current_user(
92
  request: Request,
93
  credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
app/main.py CHANGED
@@ -8,7 +8,7 @@ from pathlib import Path
8
  from typing import Optional
9
 
10
  from sqlalchemy import text as sa_text
11
- from fastapi import FastAPI, Depends, File, Form, HTTPException, Request, UploadFile
12
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
13
  from fastapi.staticfiles import StaticFiles
14
  from pydantic import BaseModel
@@ -21,6 +21,7 @@ from .auth import (
21
  get_password_hash,
22
  get_user_by_email,
23
  get_current_user,
 
24
  get_user_from_token,
25
  verify_password,
26
  )
@@ -226,7 +227,6 @@ def me(user: Optional[User] = Depends(get_current_user)):
226
  # --- Detection route ---
227
  @app.post("/api/detect")
228
  async def detect(
229
- request: Request,
230
  before: UploadFile = File(...),
231
  after: UploadFile = File(...),
232
  method: str = Form("AI-Based Deep Learning"),
@@ -238,21 +238,9 @@ async def detect(
238
  detection_sensitivity: float = Form(0.5),
239
  min_region_area: Optional[int] = Form(None),
240
  notify_email: Optional[str] = Form(None),
241
- access_token: Optional[str] = Form(None),
242
  db: Session = Depends(get_db),
243
  ):
244
- # Resolve user from token (header, cookie, or form - in case browser strips headers for multipart)
245
- token = None
246
- auth_header = request.headers.get("authorization") or request.headers.get("Authorization")
247
- if auth_header and auth_header.lower().startswith("bearer "):
248
- token = auth_header[7:].strip()
249
- if not token:
250
- token = request.cookies.get(COOKIE_NAME)
251
- if not token:
252
- token = access_token
253
- user = get_user_from_token(token, db) if token else None
254
- if not user:
255
- raise HTTPException(status_code=401, detail="Login required")
256
  MAX_UPLOAD_BYTES = 20 * 1024 * 1024 # 20 MB
257
 
258
  def _read_upload(upload: UploadFile, field_name: str):
@@ -418,10 +406,9 @@ async def detect(
418
  @app.post("/api/notify/test")
419
  def notify_test(
420
  data: EmailRequest,
421
- user: Optional[User] = Depends(get_current_user),
422
  ):
423
- if not user:
424
- raise HTTPException(status_code=401, detail="Login required")
425
  sent, error = send_test_email(data.email.strip())
426
  if not sent:
427
  raise HTTPException(status_code=400, detail=error or "Failed to send test email")
@@ -445,11 +432,9 @@ def serve_overlay(path: str):
445
  # --- History ---
446
  @app.get("/api/history")
447
  def history(
448
- user: Optional[User] = Depends(get_current_user),
449
  db: Session = Depends(get_db),
450
  ):
451
- if not user:
452
- raise HTTPException(status_code=401, detail="Login required")
453
  runs = db.query(DetectionRun).filter(DetectionRun.user_id == user.id).order_by(DetectionRun.created_at.desc()).limit(100).all()
454
  return [
455
  {
@@ -474,12 +459,10 @@ def history(
474
  @app.get("/api/history/{run_id}")
475
  def get_run(
476
  run_id: int,
477
- user: Optional[User] = Depends(get_current_user),
478
  db: Session = Depends(get_db),
479
  ):
480
  """Fetch a single run by id for opening from history (result view with slider, table, zoom)."""
481
- if not user:
482
- raise HTTPException(status_code=401, detail="Login required")
483
  run = db.query(DetectionRun).filter(DetectionRun.id == run_id, DetectionRun.user_id == user.id).first()
484
  if not run:
485
  raise HTTPException(status_code=404, detail="Run not found")
@@ -509,11 +492,9 @@ def get_run(
509
  def notify_run(
510
  run_id: int,
511
  data: EmailRequest,
512
- user: Optional[User] = Depends(get_current_user),
513
  db: Session = Depends(get_db),
514
  ):
515
- if not user:
516
- raise HTTPException(status_code=401, detail="Login required")
517
  run = db.query(DetectionRun).filter(DetectionRun.id == run_id, DetectionRun.user_id == user.id).first()
518
  if not run:
519
  raise HTTPException(status_code=404, detail="Run not found")
@@ -539,11 +520,9 @@ def notify_run(
539
  @app.delete("/api/history/{run_id}")
540
  def delete_run(
541
  run_id: int,
542
- user: Optional[User] = Depends(get_current_user),
543
  db: Session = Depends(get_db),
544
  ):
545
- if not user:
546
- raise HTTPException(status_code=401, detail="Login required")
547
  run = db.query(DetectionRun).filter(DetectionRun.id == run_id, DetectionRun.user_id == user.id).first()
548
  if not run:
549
  raise HTTPException(status_code=404, detail="Run not found")
 
8
  from typing import Optional
9
 
10
  from sqlalchemy import text as sa_text
11
+ from fastapi import FastAPI, Depends, File, Form, HTTPException, UploadFile
12
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
13
  from fastapi.staticfiles import StaticFiles
14
  from pydantic import BaseModel
 
21
  get_password_hash,
22
  get_user_by_email,
23
  get_current_user,
24
+ get_or_create_guest_user,
25
  get_user_from_token,
26
  verify_password,
27
  )
 
227
  # --- Detection route ---
228
  @app.post("/api/detect")
229
  async def detect(
 
230
  before: UploadFile = File(...),
231
  after: UploadFile = File(...),
232
  method: str = Form("AI-Based Deep Learning"),
 
238
  detection_sensitivity: float = Form(0.5),
239
  min_region_area: Optional[int] = Form(None),
240
  notify_email: Optional[str] = Form(None),
 
241
  db: Session = Depends(get_db),
242
  ):
243
+ user = get_or_create_guest_user(db)
 
 
 
 
 
 
 
 
 
 
 
244
  MAX_UPLOAD_BYTES = 20 * 1024 * 1024 # 20 MB
245
 
246
  def _read_upload(upload: UploadFile, field_name: str):
 
406
  @app.post("/api/notify/test")
407
  def notify_test(
408
  data: EmailRequest,
409
+ db: Session = Depends(get_db),
410
  ):
411
+ get_or_create_guest_user(db)
 
412
  sent, error = send_test_email(data.email.strip())
413
  if not sent:
414
  raise HTTPException(status_code=400, detail=error or "Failed to send test email")
 
432
  # --- History ---
433
  @app.get("/api/history")
434
  def history(
 
435
  db: Session = Depends(get_db),
436
  ):
437
+ user = get_or_create_guest_user(db)
 
438
  runs = db.query(DetectionRun).filter(DetectionRun.user_id == user.id).order_by(DetectionRun.created_at.desc()).limit(100).all()
439
  return [
440
  {
 
459
  @app.get("/api/history/{run_id}")
460
  def get_run(
461
  run_id: int,
 
462
  db: Session = Depends(get_db),
463
  ):
464
  """Fetch a single run by id for opening from history (result view with slider, table, zoom)."""
465
+ user = get_or_create_guest_user(db)
 
466
  run = db.query(DetectionRun).filter(DetectionRun.id == run_id, DetectionRun.user_id == user.id).first()
467
  if not run:
468
  raise HTTPException(status_code=404, detail="Run not found")
 
492
  def notify_run(
493
  run_id: int,
494
  data: EmailRequest,
 
495
  db: Session = Depends(get_db),
496
  ):
497
+ user = get_or_create_guest_user(db)
 
498
  run = db.query(DetectionRun).filter(DetectionRun.id == run_id, DetectionRun.user_id == user.id).first()
499
  if not run:
500
  raise HTTPException(status_code=404, detail="Run not found")
 
520
  @app.delete("/api/history/{run_id}")
521
  def delete_run(
522
  run_id: int,
 
523
  db: Session = Depends(get_db),
524
  ):
525
+ user = get_or_create_guest_user(db)
 
526
  run = db.query(DetectionRun).filter(DetectionRun.id == run_id, DetectionRun.user_id == user.id).first()
527
  if not run:
528
  raise HTTPException(status_code=404, detail="Run not found")
static/css/style.css CHANGED
@@ -337,12 +337,24 @@ input:focus, select:focus, textarea:focus {
337
  .btn-sm { font-size: 0.8rem; padding: 0.45rem 0.8rem; }
338
  .btn-lg { padding: 0.8rem 1.6rem; font-size: 0.95rem; }
339
 
340
- /* ---- Topbar (user avatar) ---- */
341
  .topbar {
342
  display: flex;
343
- justify-content: flex-end;
344
  margin-bottom: 1rem;
345
  }
 
 
 
 
 
 
 
 
 
 
 
 
346
  .nav-user {
347
  position: relative;
348
  }
 
337
  .btn-sm { font-size: 0.8rem; padding: 0.45rem 0.8rem; }
338
  .btn-lg { padding: 0.8rem 1.6rem; font-size: 0.95rem; }
339
 
340
+ /* ---- Topbar ---- */
341
  .topbar {
342
  display: flex;
343
+ justify-content: flex-start;
344
  margin-bottom: 1rem;
345
  }
346
+ .app-brand {
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 0.6rem;
350
+ font-weight: 600;
351
+ font-size: 1.05rem;
352
+ color: var(--text);
353
+ }
354
+ .app-brand svg {
355
+ color: var(--grad-start);
356
+ flex-shrink: 0;
357
+ }
358
  .nav-user {
359
  position: relative;
360
  }
static/js/app.js CHANGED
@@ -1,15 +1,16 @@
1
  const API_BASE = '';
2
 
3
- function getToken() { return localStorage.getItem('token'); }
4
- function setToken(token) {
5
- if (token) localStorage.setItem('token', token);
6
- else localStorage.removeItem('token');
7
- }
8
-
9
- function showView(id) {
10
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
11
- const el = document.getElementById('view-' + id);
12
- if (el) el.classList.add('active');
 
13
  }
14
 
15
  function showError(id, msg) {
@@ -34,120 +35,10 @@ function isValidEmail(email) {
34
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((email || '').trim());
35
  }
36
 
37
- async function api(method, path, options = {}) {
38
- const headers = { ...options.headers };
39
- const token = getToken();
40
- if (token) headers['Authorization'] = 'Bearer ' + token;
41
- if (options.body && !(options.body instanceof FormData)) {
42
- headers['Content-Type'] = 'application/json';
43
- }
44
- const res = await fetch(API_BASE + path, { method, headers, credentials: 'include', ...options });
45
- const text = await res.text();
46
- let data = null;
47
- try { data = text ? JSON.parse(text) : null; } catch (_) {}
48
- if (!res.ok) throw new Error(data?.detail || res.statusText || 'Request failed');
49
- return data;
50
- }
51
-
52
- // ---- Auth ----
53
- document.getElementById('form-login')?.addEventListener('submit', async (e) => {
54
- e.preventDefault();
55
- hideError('login-error');
56
- const email = document.getElementById('login-email').value.trim();
57
- const password = document.getElementById('login-password').value;
58
- try {
59
- const data = await api('POST', '/api/auth/login', { body: JSON.stringify({ email, password }) });
60
- setToken(data.access_token);
61
- document.getElementById('user-email').textContent = data.user.email;
62
- handlePostAuthNavigation();
63
- } catch (err) { showError('login-error', err.message); }
64
- });
65
-
66
- document.getElementById('form-register')?.addEventListener('submit', async (e) => {
67
- e.preventDefault();
68
- hideError('register-error');
69
- const full_name = document.getElementById('register-name').value.trim();
70
- const email = document.getElementById('register-email').value.trim();
71
- const password = document.getElementById('register-password').value;
72
- try {
73
- const data = await api('POST', '/api/auth/register', { body: JSON.stringify({ email, password, full_name }) });
74
- setToken(data.access_token);
75
- document.getElementById('user-email').textContent = data.user.email;
76
- handlePostAuthNavigation();
77
- } catch (err) { showError('register-error', err.message); }
78
- });
79
-
80
- function handlePostAuthNavigation() {
81
- showView('dashboard');
82
  loadHistory();
83
  }
84
 
85
- // ---- Forgot password ----
86
- document.getElementById('form-forgot')?.addEventListener('submit', async (e) => {
87
- e.preventDefault();
88
- hideError('forgot-error');
89
- hideError('forgot-success');
90
- const email = document.getElementById('forgot-email').value.trim();
91
- const new_password = document.getElementById('forgot-password').value;
92
- try {
93
- const data = await api('POST', '/api/auth/reset-password', { body: JSON.stringify({ email, new_password }) });
94
- showSuccess('forgot-success', data.message || 'Password reset! You can now sign in.');
95
- document.getElementById('form-forgot').reset();
96
- } catch (err) { showError('forgot-error', err.message); }
97
- });
98
-
99
- // ---- Password visibility toggle ----
100
- document.querySelectorAll('.password-toggle').forEach((btn) => {
101
- btn.addEventListener('click', () => {
102
- const input = document.getElementById(btn.dataset.target);
103
- if (!input) return;
104
- const showing = input.type !== 'password';
105
- input.type = showing ? 'password' : 'text';
106
- btn.style.opacity = showing ? '' : '0.9';
107
- });
108
- });
109
-
110
- document.querySelectorAll('[data-view]').forEach((a) => {
111
- a.addEventListener('click', (e) => {
112
- e.preventDefault();
113
- showView(a.getAttribute('data-view'));
114
- hideError('login-error');
115
- hideError('register-error');
116
- hideError('forgot-error');
117
- hideError('forgot-success');
118
- });
119
- });
120
-
121
- document.getElementById('btn-logout')?.addEventListener('click', async () => {
122
- try { await fetch(API_BASE + '/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch (_) {}
123
- setToken(null);
124
- document.getElementById('nav-dropdown')?.classList.add('hidden');
125
- showView('login');
126
- });
127
-
128
- // ---- Avatar dropdown toggle ----
129
- document.getElementById('btn-avatar')?.addEventListener('click', (e) => {
130
- e.stopPropagation();
131
- document.getElementById('nav-dropdown')?.classList.toggle('hidden');
132
- });
133
- document.addEventListener('click', (e) => {
134
- const dd = document.getElementById('nav-dropdown');
135
- if (dd && !dd.classList.contains('hidden') && !e.target.closest('.nav-user')) {
136
- dd.classList.add('hidden');
137
- }
138
- });
139
-
140
- async function init() {
141
- const token = getToken();
142
- if (!token) { showView('login'); return; }
143
- try {
144
- const user = await api('GET', '/api/me');
145
- document.getElementById('user-email').textContent = user.email;
146
- showView('dashboard');
147
- loadHistory();
148
- } catch (_) { setToken(null); showView('login'); }
149
- }
150
-
151
  // ---- Upload zones with preview ----
152
  function setupUploadZone(inputId, nameId, zoneId, previewId) {
153
  const input = document.getElementById(inputId);
@@ -409,7 +300,6 @@ document.getElementById('form-detect')?.addEventListener('submit', async (e) =>
409
  loading.classList.remove('hidden');
410
  startDetectionProgress();
411
 
412
- const token = getToken();
413
  const form = new FormData();
414
  form.append('before', before);
415
  form.append('after', after);
@@ -457,15 +347,7 @@ document.getElementById('form-detect')?.addEventListener('submit', async (e) =>
457
  form.append('notify_email', email);
458
  }
459
 
460
- if (token) form.append('access_token', token);
461
-
462
  try {
463
- if (!token) {
464
- showError('dashboard-error', 'Session expired. Please sign in again.');
465
- setToken(null);
466
- showView('login');
467
- return;
468
- }
469
  const data = await api('POST', '/api/detect', { body: form });
470
  showResult(data);
471
  const notifyCbDone = document.getElementById('detect-notify');
 
1
  const API_BASE = '';
2
 
3
+ async function api(method, path, options = {}) {
4
+ const headers = { ...options.headers };
5
+ if (options.body && !(options.body instanceof FormData)) {
6
+ headers['Content-Type'] = 'application/json';
7
+ }
8
+ const res = await fetch(API_BASE + path, { method, headers, credentials: 'include', ...options });
9
+ const text = await res.text();
10
+ let data = null;
11
+ try { data = text ? JSON.parse(text) : null; } catch (_) {}
12
+ if (!res.ok) throw new Error(data?.detail || res.statusText || 'Request failed');
13
+ return data;
14
  }
15
 
16
  function showError(id, msg) {
 
35
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((email || '').trim());
36
  }
37
 
38
+ function init() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  loadHistory();
40
  }
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  // ---- Upload zones with preview ----
43
  function setupUploadZone(inputId, nameId, zoneId, previewId) {
44
  const input = document.getElementById(inputId);
 
300
  loading.classList.remove('hidden');
301
  startDetectionProgress();
302
 
 
303
  const form = new FormData();
304
  form.append('before', before);
305
  form.append('after', after);
 
347
  form.append('notify_email', email);
348
  }
349
 
 
 
350
  try {
 
 
 
 
 
 
351
  const data = await api('POST', '/api/detect', { body: form });
352
  showResult(data);
353
  const notifyCbDone = document.getElementById('detect-notify');
templates/index.html CHANGED
@@ -4,147 +4,16 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>AI Change Detection</title>
7
- <link rel="stylesheet" href="/static/css/style.css?v=26" />
8
  </head>
9
  <body>
10
  <div class="app">
11
- <!-- Login view -->
12
- <section id="view-login" class="view">
13
- <div class="auth-container">
14
- <div class="auth-logo">
15
- <div class="auth-logo-icon">
16
- <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
17
- </div>
18
- <span>AI Change Detection</span>
19
- </div>
20
- <div class="card">
21
- <h2>Welcome back</h2>
22
- <p class="sub">Sign in to your account to continue.</p>
23
- <div id="login-error" class="alert alert-error hidden"></div>
24
- <form id="form-login">
25
- <div class="form-group">
26
- <label for="login-email">Email</label>
27
- <input type="email" id="login-email" required placeholder="you@example.com" />
28
- </div>
29
- <div class="form-group">
30
- <label for="login-password">Password</label>
31
- <div class="input-password-wrap">
32
- <input type="password" id="login-password" required placeholder="Enter your password" />
33
- <button type="button" class="password-toggle" data-target="login-password" aria-label="Toggle password visibility">
34
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
35
- </button>
36
- </div>
37
- </div>
38
- <div class="auth-actions">
39
- <button type="submit" class="btn btn-primary btn-block">Sign in</button>
40
- </div>
41
- </form>
42
- <p class="forgot-link"><a href="#" data-view="forgot">Forgot your password?</a></p>
43
- <p class="toggle-auth">Don't have an account? <a href="#" data-view="register">Create one</a></p>
44
- </div>
45
- </div>
46
- </section>
47
-
48
- <!-- Register view -->
49
- <section id="view-register" class="view">
50
- <div class="auth-container">
51
- <div class="auth-logo">
52
- <div class="auth-logo-icon">
53
- <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
54
- </div>
55
- <span>AI Change Detection</span>
56
- </div>
57
- <div class="card">
58
- <h2>Create account</h2>
59
- <p class="sub">Register to save and manage your detection runs.</p>
60
- <div id="register-error" class="alert alert-error hidden"></div>
61
- <form id="form-register">
62
- <div class="form-group">
63
- <label for="register-name">Full name</label>
64
- <input type="text" id="register-name" placeholder="Your name" />
65
- </div>
66
- <div class="form-group">
67
- <label for="register-email">Email</label>
68
- <input type="email" id="register-email" required placeholder="you@example.com" />
69
- </div>
70
- <div class="form-group">
71
- <label for="register-password">Password</label>
72
- <div class="input-password-wrap">
73
- <input type="password" id="register-password" required placeholder="Min. 6 characters" minlength="6" />
74
- <button type="button" class="password-toggle" data-target="register-password" aria-label="Toggle password visibility">
75
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
76
- </button>
77
- </div>
78
- </div>
79
- <div class="auth-actions">
80
- <button type="submit" class="btn btn-primary btn-block">Create account</button>
81
- </div>
82
- </form>
83
- <p class="toggle-auth">Already have an account? <a href="#" data-view="login">Sign in</a></p>
84
- </div>
85
- </div>
86
- </section>
87
-
88
- <!-- Forgot password view -->
89
- <section id="view-forgot" class="view">
90
- <div class="auth-container">
91
- <div class="auth-logo">
92
- <div class="auth-logo-icon">
93
- <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
94
- </div>
95
- <span>AI Change Detection</span>
96
- </div>
97
- <div class="card">
98
- <div class="forgot-header-icon">
99
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
100
- </div>
101
- <h2 style="text-align:center;">Reset your password</h2>
102
- <p class="sub" style="text-align:center;">Enter the email you registered with and choose a new password.</p>
103
- <div id="forgot-error" class="alert alert-error hidden"></div>
104
- <div id="forgot-success" class="alert alert-success hidden"></div>
105
- <form id="form-forgot">
106
- <div class="form-group">
107
- <label for="forgot-email">Email address</label>
108
- <input type="email" id="forgot-email" required placeholder="you@example.com" />
109
- </div>
110
- <div class="form-group">
111
- <label for="forgot-password">New password</label>
112
- <div class="input-password-wrap">
113
- <input type="password" id="forgot-password" required placeholder="Min. 6 characters" minlength="6" />
114
- <button type="button" class="password-toggle" data-target="forgot-password" aria-label="Toggle password visibility">
115
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
116
- </button>
117
- </div>
118
- </div>
119
- <div class="auth-actions">
120
- <button type="submit" class="btn btn-primary btn-block">Reset password</button>
121
- </div>
122
- </form>
123
- <div style="text-align:center;">
124
- <a href="#" class="back-to-login" data-view="login">
125
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
126
- Back to sign in
127
- </a>
128
- </div>
129
- </div>
130
- </div>
131
- </section>
132
-
133
- <!-- Dashboard view -->
134
- <section id="view-dashboard" class="view">
135
  <div class="topbar">
136
- <div class="nav-user">
137
- <button type="button" class="nav-avatar" id="btn-avatar" aria-label="Account menu">
138
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
139
- </button>
140
- <div class="nav-dropdown hidden" id="nav-dropdown">
141
- <div class="nav-dropdown-email" id="user-email"></div>
142
- <div class="nav-dropdown-divider"></div>
143
- <button type="button" class="nav-dropdown-item" id="btn-logout">
144
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
145
- Log out
146
- </button>
147
- </div>
148
  </div>
149
  </div>
150
 
@@ -364,6 +233,6 @@
364
  </div>
365
  </div>
366
 
367
- <script src="/static/js/app.js?v=41"></script>
368
  </body>
369
  </html>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>AI Change Detection</title>
7
+ <link rel="stylesheet" href="/static/css/style.css?v=28" />
8
  </head>
9
  <body>
10
  <div class="app">
11
+ <!-- Detection dashboard (shown immediately — no login) -->
12
+ <section id="view-dashboard" class="view active">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  <div class="topbar">
14
+ <div class="app-brand">
15
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
16
+ <span>AI Change Detection</span>
 
 
 
 
 
 
 
 
 
17
  </div>
18
  </div>
19
 
 
233
  </div>
234
  </div>
235
 
236
+ <script src="/static/js/app.js?v=42"></script>
237
  </body>
238
  </html>