Dipika Bhadane commited on
Commit
df568a8
·
1 Parent(s): 4904300

changes file upload and login register

Browse files
app.py CHANGED
@@ -1,20 +1,37 @@
1
- from flask import Flask, render_template, request, jsonify, Response
2
  import os
3
 
 
 
4
  from verify_v2 import verify_claim
5
  import db
6
  import health_passport as hp
7
  import ocr_utils
8
  import translate_utils
9
- from gnn.gnn_predict import predict_spread
10
- print("✅ VERIMED GNN MODULE LOADED SUCCESSFULLY — v2")
 
 
 
11
 
12
  app = Flask(__name__)
13
  app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-only-change-me")
14
 
 
 
 
 
 
 
 
 
 
 
 
15
  # Make sure tables exist before anything else runs
16
  db.init_db()
17
  hp.init_passport_table()
 
18
 
19
  # --- PAGE ROUTING ---
20
 
@@ -39,8 +56,91 @@ def predictor():
39
  return render_template('predictor.html')
40
 
41
  @app.route('/passport')
 
42
  def passport():
43
- return render_template('passport.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  # --- API ENDPOINTS ---
46
 
@@ -153,6 +253,7 @@ def api_predict_spread():
153
  # risk features (verdict, confidence, entities) are grounded in real
154
  # evidence, not guessed independently.
155
  verification = verify_claim(claim)
 
156
  graph_data = predict_spread(
157
  claim_text=claim,
158
  verdict=verification.get("verdict", "Unverified"),
@@ -163,6 +264,7 @@ def api_predict_spread():
163
 
164
 
165
  @app.route('/api/passport', methods=['GET', 'POST'])
 
166
  def api_passport():
167
  if request.method == 'POST':
168
  data = request.get_json(force=True) or {}
@@ -173,23 +275,110 @@ def api_passport():
173
  }
174
  for key, default in required_defaults.items():
175
  data.setdefault(key, default)
176
- hp.save_passport(data)
177
  return jsonify({"status": "saved"})
178
 
179
- passport_data = hp.get_passport()
180
  if not passport_data:
181
  return jsonify(None)
182
  return jsonify(passport_data)
183
 
184
 
185
  @app.route('/api/passport/qr', methods=['GET'])
 
186
  def api_passport_qr():
187
- passport_data = hp.get_passport()
188
  if not passport_data:
189
  return jsonify({"error": "No passport saved yet."}), 404
190
- png_bytes = hp.generate_qr_code(passport_data)
191
  return Response(png_bytes, mimetype='image/png')
192
 
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  if __name__ == '__main__':
195
  app.run(debug=True, port=5000, use_reloader=False)
 
1
+ from flask import Flask, render_template, request, jsonify, Response, redirect, url_for, flash, send_file
2
  import os
3
 
4
+ from flask_login import LoginManager, login_user, logout_user, login_required, current_user
5
+
6
  from verify_v2 import verify_claim
7
  import db
8
  import health_passport as hp
9
  import ocr_utils
10
  import translate_utils
11
+ import auth
12
+ import pdf_export
13
+ import sys, os
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "gnn"))
15
+ from gnn_predict import predict_spread
16
 
17
  app = Flask(__name__)
18
  app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-only-change-me")
19
 
20
+ login_manager = LoginManager()
21
+ login_manager.init_app(app)
22
+ login_manager.login_view = "login"
23
+ login_manager.login_message = "Please log in to access your Health Passport."
24
+
25
+
26
+ @login_manager.user_loader
27
+ def load_user(user_id):
28
+ return auth.get_user_by_id(user_id)
29
+
30
+
31
  # Make sure tables exist before anything else runs
32
  db.init_db()
33
  hp.init_passport_table()
34
+ auth.init_users_table()
35
 
36
  # --- PAGE ROUTING ---
37
 
 
56
  return render_template('predictor.html')
57
 
58
  @app.route('/passport')
59
+ @login_required
60
  def passport():
61
+ return render_template('passport.html', user=current_user)
62
+
63
+
64
+ @app.route('/register', methods=['GET', 'POST'])
65
+ def register():
66
+ if current_user.is_authenticated:
67
+ return redirect(url_for('passport'))
68
+
69
+ if request.method == 'POST':
70
+ name = request.form.get('name', '').strip()
71
+ email = request.form.get('email', '').strip()
72
+ password = request.form.get('password', '')
73
+ confirm_password = request.form.get('confirm_password', '')
74
+
75
+ if not name or not email or not password:
76
+ flash("Please fill in all fields.", "error")
77
+ return render_template('register.html')
78
+
79
+ if password != confirm_password:
80
+ flash("Passwords do not match.", "error")
81
+ return render_template('register.html')
82
+
83
+ if len(password) < 8:
84
+ flash("Password must be at least 8 characters.", "error")
85
+ return render_template('register.html')
86
+
87
+ user = auth.create_user(name, email, password)
88
+ if user is None:
89
+ flash("An account with that email already exists. Try logging in instead.", "error")
90
+ return render_template('register.html')
91
+
92
+ login_user(user)
93
+ return redirect(url_for('passport'))
94
+
95
+ return render_template('register.html')
96
+
97
+
98
+ @app.route('/login', methods=['GET', 'POST'])
99
+ def login():
100
+ if current_user.is_authenticated:
101
+ return redirect(url_for('passport'))
102
+
103
+ if request.method == 'POST':
104
+ email = request.form.get('email', '').strip()
105
+ password = request.form.get('password', '')
106
+
107
+ user = auth.verify_login(email, password)
108
+ if user is None:
109
+ flash("Incorrect email or password.", "error")
110
+ return render_template('login.html')
111
+
112
+ login_user(user)
113
+ next_page = request.args.get('next')
114
+ return redirect(next_page or url_for('passport'))
115
+
116
+ return render_template('login.html')
117
+
118
+
119
+ @app.route('/logout')
120
+ @login_required
121
+ def logout():
122
+ logout_user()
123
+ return redirect(url_for('home'))
124
+
125
+
126
+ @app.route('/emergency-help')
127
+ def emergency_help():
128
+ return render_template('emergency_help.html')
129
+
130
+
131
+ @app.route('/emergency/<share_token>')
132
+ def emergency_view(share_token):
133
+ """
134
+ Public, read-only emergency view -- this is what the QR code opens.
135
+ No login required, intentionally shows only emergency-relevant fields.
136
+ """
137
+ passport_data = hp.get_passport_by_token(share_token)
138
+ if not passport_data:
139
+ return render_template('emergency_view.html', passport=None), 404
140
+
141
+ surgeries = hp.get_surgeries(passport_data['user_id'])
142
+ vaccinations = hp.get_vaccinations(passport_data['user_id'])
143
+ return render_template('emergency_view.html', passport=passport_data, surgeries=surgeries, vaccinations=vaccinations)
144
 
145
  # --- API ENDPOINTS ---
146
 
 
253
  # risk features (verdict, confidence, entities) are grounded in real
254
  # evidence, not guessed independently.
255
  verification = verify_claim(claim)
256
+
257
  graph_data = predict_spread(
258
  claim_text=claim,
259
  verdict=verification.get("verdict", "Unverified"),
 
264
 
265
 
266
  @app.route('/api/passport', methods=['GET', 'POST'])
267
+ @login_required
268
  def api_passport():
269
  if request.method == 'POST':
270
  data = request.get_json(force=True) or {}
 
275
  }
276
  for key, default in required_defaults.items():
277
  data.setdefault(key, default)
278
+ hp.save_passport(int(current_user.id), data)
279
  return jsonify({"status": "saved"})
280
 
281
+ passport_data = hp.get_passport(int(current_user.id))
282
  if not passport_data:
283
  return jsonify(None)
284
  return jsonify(passport_data)
285
 
286
 
287
  @app.route('/api/passport/qr', methods=['GET'])
288
+ @login_required
289
  def api_passport_qr():
290
+ passport_data = hp.get_passport(int(current_user.id))
291
  if not passport_data:
292
  return jsonify({"error": "No passport saved yet."}), 404
293
+ png_bytes = hp.generate_qr_code(passport_data['share_token'], request.host_url)
294
  return Response(png_bytes, mimetype='image/png')
295
 
296
 
297
+ @app.route('/api/passport/surgeries', methods=['GET', 'POST'])
298
+ @login_required
299
+ def api_surgeries():
300
+ if request.method == 'POST':
301
+ data = request.get_json(force=True) or {}
302
+ hp.add_surgery(int(current_user.id), data.get('year', ''), data.get('description', ''))
303
+ return jsonify({"status": "added"})
304
+ return jsonify(hp.get_surgeries(int(current_user.id)))
305
+
306
+
307
+ @app.route('/api/passport/surgeries/<int:surgery_id>', methods=['DELETE'])
308
+ @login_required
309
+ def api_delete_surgery(surgery_id):
310
+ hp.delete_surgery(int(current_user.id), surgery_id)
311
+ return jsonify({"status": "deleted"})
312
+
313
+
314
+ @app.route('/api/passport/vaccinations', methods=['GET', 'POST'])
315
+ @login_required
316
+ def api_vaccinations():
317
+ if request.method == 'POST':
318
+ data = request.get_json(force=True) or {}
319
+ hp.add_vaccination(int(current_user.id), data.get('vaccine_name', ''), data.get('month', ''), data.get('year', ''))
320
+ return jsonify({"status": "added"})
321
+ return jsonify(hp.get_vaccinations(int(current_user.id)))
322
+
323
+
324
+ @app.route('/api/passport/vaccinations/<int:vaccination_id>', methods=['DELETE'])
325
+ @login_required
326
+ def api_delete_vaccination(vaccination_id):
327
+ hp.delete_vaccination(int(current_user.id), vaccination_id)
328
+ return jsonify({"status": "deleted"})
329
+
330
+
331
+ @app.route('/api/passport/reports', methods=['GET', 'POST'])
332
+ @login_required
333
+ def api_reports():
334
+ if request.method == 'POST':
335
+ if 'file' not in request.files or request.files['file'].filename == '':
336
+ return jsonify({"error": "No file provided."}), 400
337
+ file_storage = request.files['file']
338
+ category = request.form.get('category', 'Other')
339
+ month = request.form.get('month', '')
340
+ year = request.form.get('year', '')
341
+ report = hp.save_report_file(int(current_user.id), file_storage, category, month, year)
342
+ return jsonify(report)
343
+
344
+ return jsonify(hp.get_reports(int(current_user.id)))
345
+
346
+
347
+ @app.route('/api/passport/reports/<int:report_id>', methods=['DELETE'])
348
+ @login_required
349
+ def api_delete_report(report_id):
350
+ hp.delete_report(int(current_user.id), report_id)
351
+ return jsonify({"status": "deleted"})
352
+
353
+
354
+ @app.route('/api/passport/reports/<int:report_id>/download', methods=['GET'])
355
+ @login_required
356
+ def api_download_report(report_id):
357
+ report = hp.get_report_by_id(int(current_user.id), report_id)
358
+ if not report or not os.path.exists(report['stored_path']):
359
+ return jsonify({"error": "Report not found."}), 404
360
+ return send_file(report['stored_path'], as_attachment=True, download_name=report['filename'])
361
+
362
+
363
+ @app.route('/api/passport/pdf', methods=['GET'])
364
+ @login_required
365
+ def api_passport_pdf():
366
+ passport_data = hp.get_passport(int(current_user.id))
367
+ if not passport_data:
368
+ return jsonify({"error": "No passport saved yet."}), 404
369
+
370
+ surgeries = hp.get_surgeries(int(current_user.id))
371
+ vaccinations = hp.get_vaccinations(int(current_user.id))
372
+ qr_bytes = hp.generate_qr_code(passport_data['share_token'], request.host_url)
373
+
374
+ pdf_bytes = pdf_export.generate_passport_pdf(passport_data, surgeries, vaccinations, qr_bytes)
375
+
376
+ return Response(
377
+ pdf_bytes,
378
+ mimetype='application/pdf',
379
+ headers={"Content-Disposition": "attachment; filename=VeriMed_Health_Passport.pdf"},
380
+ )
381
+
382
+
383
  if __name__ == '__main__':
384
  app.run(debug=True, port=5000, use_reloader=False)
auth.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication module.
3
+
4
+ Real password hashing (werkzeug's generate_password_hash / check_password_hash
5
+ -- salted, uses PBKDF2/scrypt depending on werkzeug version) and Flask-Login
6
+ session management.
7
+
8
+ NOTE ON SCOPE: this is hackathon-appropriate auth -- correct password hashing,
9
+ proper session cookies via Flask-Login, but no email verification, no
10
+ password reset flow, no rate limiting on login attempts. Be upfront about
11
+ that if asked -- it's a normal, expected scope cut, not a hidden flaw.
12
+ """
13
+
14
+ import sqlite3
15
+ from datetime import datetime
16
+ from werkzeug.security import generate_password_hash, check_password_hash
17
+ from flask_login import UserMixin
18
+
19
+ DB_FILE = "verimed.db"
20
+
21
+
22
+ class User(UserMixin):
23
+ """Flask-Login needs an object with .id, .is_authenticated, etc. -- UserMixin provides those."""
24
+ def __init__(self, id, name, email):
25
+ self.id = str(id)
26
+ self.name = name
27
+ self.email = email
28
+
29
+
30
+ def init_users_table():
31
+ conn = sqlite3.connect(DB_FILE)
32
+ cur = conn.cursor()
33
+ cur.execute("""
34
+ CREATE TABLE IF NOT EXISTS users (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ name TEXT NOT NULL,
37
+ email TEXT UNIQUE NOT NULL,
38
+ password_hash TEXT NOT NULL,
39
+ created_at TEXT NOT NULL
40
+ )
41
+ """)
42
+ conn.commit()
43
+ conn.close()
44
+
45
+
46
+ def create_user(name: str, email: str, password: str) -> "User | None":
47
+ """Returns the created User, or None if the email is already registered."""
48
+ conn = sqlite3.connect(DB_FILE)
49
+ cur = conn.cursor()
50
+ cur.execute("SELECT id FROM users WHERE email = ?", (email.lower().strip(),))
51
+ if cur.fetchone():
52
+ conn.close()
53
+ return None # email already taken
54
+
55
+ password_hash = generate_password_hash(password)
56
+ now = datetime.now().isoformat(timespec="seconds")
57
+ cur.execute(
58
+ "INSERT INTO users (name, email, password_hash, created_at) VALUES (?, ?, ?, ?)",
59
+ (name.strip(), email.lower().strip(), password_hash, now),
60
+ )
61
+ conn.commit()
62
+ user_id = cur.lastrowid
63
+ conn.close()
64
+ return User(user_id, name.strip(), email.lower().strip())
65
+
66
+
67
+ def verify_login(email: str, password: str) -> "User | None":
68
+ """Returns the User if email+password match, else None."""
69
+ conn = sqlite3.connect(DB_FILE)
70
+ conn.row_factory = sqlite3.Row
71
+ cur = conn.cursor()
72
+ cur.execute("SELECT * FROM users WHERE email = ?", (email.lower().strip(),))
73
+ row = cur.fetchone()
74
+ conn.close()
75
+
76
+ if not row:
77
+ return None
78
+ if not check_password_hash(row["password_hash"], password):
79
+ return None
80
+
81
+ return User(row["id"], row["name"], row["email"])
82
+
83
+
84
+ def get_user_by_id(user_id: str) -> "User | None":
85
+ """Required by Flask-Login's user_loader callback."""
86
+ conn = sqlite3.connect(DB_FILE)
87
+ conn.row_factory = sqlite3.Row
88
+ cur = conn.cursor()
89
+ cur.execute("SELECT * FROM users WHERE id = ?", (user_id,))
90
+ row = cur.fetchone()
91
+ conn.close()
92
+
93
+ if not row:
94
+ return None
95
+ return User(row["id"], row["name"], row["email"])
96
+
97
+
98
+ if __name__ == "__main__":
99
+ # Quick manual test -- run: python auth.py
100
+ import os
101
+ if os.path.exists(DB_FILE):
102
+ os.remove(DB_FILE) # clean slate for this test only
103
+
104
+ init_users_table()
105
+
106
+ user = create_user("Test User", "test@example.com", "correct-password-123")
107
+ print(f"Created user: {user.name} ({user.email}), id={user.id}")
108
+
109
+ dupe = create_user("Someone Else", "test@example.com", "whatever")
110
+ print(f"Duplicate email rejected: {dupe is None}")
111
+
112
+ good_login = verify_login("test@example.com", "correct-password-123")
113
+ print(f"Correct password login: {'SUCCESS' if good_login else 'FAILED'}")
114
+
115
+ bad_login = verify_login("test@example.com", "wrong-password")
116
+ print(f"Wrong password login: {'correctly rejected' if bad_login is None else 'SECURITY BUG'}")
117
+
118
+ fetched = get_user_by_id(user.id)
119
+ print(f"Fetched by id: {fetched.name} ({fetched.email})")
health_passport.py CHANGED
@@ -1,29 +1,40 @@
1
  """
2
- Health Passport module.
3
 
4
- Stores a user's personal health profile locally (SQLite) and generates a
5
- QR code encoding a compact emergency-readable summary of it.
 
6
 
7
- NOTE: This is a hackathon-scope, single-user local demo -- no multi-user auth,
8
- no encryption at rest. If you present this, be upfront that a production
9
- version would need proper auth + encryption (HIPAA-style) -- judges respect
10
- that honesty far more than pretending it's production-secure.
 
 
 
 
11
  """
12
 
13
  import sqlite3
14
- import json
15
- import qrcode
16
  import io
 
 
 
 
 
17
 
18
  DB_FILE = "verimed.db"
 
19
 
20
 
21
  def init_passport_table():
22
  conn = sqlite3.connect(DB_FILE)
23
  cur = conn.cursor()
 
24
  cur.execute("""
25
  CREATE TABLE IF NOT EXISTS health_passport (
26
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
27
  full_name TEXT,
28
  blood_group TEXT,
29
  date_of_birth TEXT,
@@ -32,21 +43,59 @@ def init_passport_table():
32
  current_medicines TEXT,
33
  emergency_contact_name TEXT,
34
  emergency_contact_phone TEXT,
 
35
  updated_at TEXT
36
  )
37
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  conn.commit()
39
  conn.close()
 
 
40
 
 
 
 
41
 
42
- def save_passport(data: dict):
43
- """Upserts the single local passport record (id=1, since this is single-user demo)."""
44
  conn = sqlite3.connect(DB_FILE)
45
  cur = conn.cursor()
46
- cur.execute("SELECT id FROM health_passport LIMIT 1")
47
  existing = cur.fetchone()
48
-
49
- from datetime import datetime
50
  now = datetime.now().isoformat(timespec="seconds")
51
 
52
  if existing:
@@ -55,60 +104,232 @@ def save_passport(data: dict):
55
  full_name=?, blood_group=?, date_of_birth=?, allergies=?,
56
  chronic_conditions=?, current_medicines=?,
57
  emergency_contact_name=?, emergency_contact_phone=?, updated_at=?
58
- WHERE id=?
59
  """, (
60
  data["full_name"], data["blood_group"], data["date_of_birth"],
61
  data["allergies"], data["chronic_conditions"], data["current_medicines"],
62
  data["emergency_contact_name"], data["emergency_contact_phone"], now,
63
- existing[0],
64
  ))
65
  else:
 
66
  cur.execute("""
67
  INSERT INTO health_passport
68
- (full_name, blood_group, date_of_birth, allergies, chronic_conditions,
69
- current_medicines, emergency_contact_name, emergency_contact_phone, updated_at)
70
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
71
  """, (
72
- data["full_name"], data["blood_group"], data["date_of_birth"],
73
  data["allergies"], data["chronic_conditions"], data["current_medicines"],
74
- data["emergency_contact_name"], data["emergency_contact_phone"], now,
75
  ))
76
  conn.commit()
77
  conn.close()
78
 
79
 
80
- def get_passport() -> dict | None:
81
  conn = sqlite3.connect(DB_FILE)
82
  conn.row_factory = sqlite3.Row
83
  cur = conn.cursor()
84
- cur.execute("SELECT * FROM health_passport LIMIT 1")
85
  row = cur.fetchone()
86
  conn.close()
87
  return dict(row) if row else None
88
 
89
 
90
- def generate_qr_code(passport: dict) -> bytes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  """
92
- Generates a QR code encoding a compact emergency summary.
93
- Returns PNG image bytes ready to display in Streamlit.
94
  """
95
- summary = (
96
- f"VERIMED HEALTH PASSPORT\n"
97
- f"Name: {passport.get('full_name', '')}\n"
98
- f"Blood Group: {passport.get('blood_group', '')}\n"
99
- f"DOB: {passport.get('date_of_birth', '')}\n"
100
- f"Allergies: {passport.get('allergies', 'None listed')}\n"
101
- f"Chronic Conditions: {passport.get('chronic_conditions', 'None listed')}\n"
102
- f"Current Medicines: {passport.get('current_medicines', 'None listed')}\n"
103
- f"Emergency Contact: {passport.get('emergency_contact_name', '')} "
104
- f"({passport.get('emergency_contact_phone', '')})"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  qr = qrcode.QRCode(version=None, box_size=8, border=3)
108
- qr.add_data(summary)
109
  qr.make(fit=True)
110
  img = qr.make_image(fill_color="#0F6FFF", back_color="white")
111
 
112
  buf = io.BytesIO()
113
  img.save(buf, format="PNG")
114
  return buf.getvalue()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Health Passport module -- v2, per-user.
3
 
4
+ Every table now has a user_id foreign key, so each logged-in user has their
5
+ own private passport, surgery history, vaccination record, and uploaded
6
+ reports.
7
 
8
+ QR CODE DESIGN CHANGE FROM v1:
9
+ v1 encoded the passport data as raw text directly in the QR code. That only
10
+ works if the scanning device has software that can parse that specific text
11
+ format. v2 instead gives each user a random, unguessable share_token and
12
+ encodes a URL (e.g. http://yourhost/emergency/<token>) in the QR code. Any
13
+ phone camera can scan it and it opens directly in a browser -- no app
14
+ needed on the doctor's side. That page is intentionally read-only and shows
15
+ only emergency-relevant fields, not the full account or uploaded documents.
16
  """
17
 
18
  import sqlite3
 
 
19
  import io
20
+ import os
21
+ import secrets
22
+ from datetime import datetime
23
+
24
+ import qrcode
25
 
26
  DB_FILE = "verimed.db"
27
+ UPLOAD_DIR = "uploads/medical_reports"
28
 
29
 
30
  def init_passport_table():
31
  conn = sqlite3.connect(DB_FILE)
32
  cur = conn.cursor()
33
+
34
  cur.execute("""
35
  CREATE TABLE IF NOT EXISTS health_passport (
36
  id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ user_id INTEGER NOT NULL UNIQUE,
38
  full_name TEXT,
39
  blood_group TEXT,
40
  date_of_birth TEXT,
 
43
  current_medicines TEXT,
44
  emergency_contact_name TEXT,
45
  emergency_contact_phone TEXT,
46
+ share_token TEXT UNIQUE,
47
  updated_at TEXT
48
  )
49
  """)
50
+
51
+ cur.execute("""
52
+ CREATE TABLE IF NOT EXISTS surgeries (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
54
+ user_id INTEGER NOT NULL,
55
+ year TEXT,
56
+ description TEXT,
57
+ created_at TEXT
58
+ )
59
+ """)
60
+
61
+ cur.execute("""
62
+ CREATE TABLE IF NOT EXISTS vaccinations (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ user_id INTEGER NOT NULL,
65
+ vaccine_name TEXT,
66
+ month TEXT,
67
+ year TEXT,
68
+ created_at TEXT
69
+ )
70
+ """)
71
+
72
+ cur.execute("""
73
+ CREATE TABLE IF NOT EXISTS medical_reports (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ user_id INTEGER NOT NULL,
76
+ filename TEXT,
77
+ stored_path TEXT,
78
+ category TEXT,
79
+ month TEXT,
80
+ year TEXT,
81
+ uploaded_at TEXT
82
+ )
83
+ """)
84
+
85
  conn.commit()
86
  conn.close()
87
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
88
+
89
 
90
+ # ---------------------------------------------------------------------------
91
+ # Core passport (personal details)
92
+ # ---------------------------------------------------------------------------
93
 
94
+ def save_passport(user_id: int, data: dict):
 
95
  conn = sqlite3.connect(DB_FILE)
96
  cur = conn.cursor()
97
+ cur.execute("SELECT id, share_token FROM health_passport WHERE user_id = ?", (user_id,))
98
  existing = cur.fetchone()
 
 
99
  now = datetime.now().isoformat(timespec="seconds")
100
 
101
  if existing:
 
104
  full_name=?, blood_group=?, date_of_birth=?, allergies=?,
105
  chronic_conditions=?, current_medicines=?,
106
  emergency_contact_name=?, emergency_contact_phone=?, updated_at=?
107
+ WHERE user_id=?
108
  """, (
109
  data["full_name"], data["blood_group"], data["date_of_birth"],
110
  data["allergies"], data["chronic_conditions"], data["current_medicines"],
111
  data["emergency_contact_name"], data["emergency_contact_phone"], now,
112
+ user_id,
113
  ))
114
  else:
115
+ share_token = secrets.token_urlsafe(16)
116
  cur.execute("""
117
  INSERT INTO health_passport
118
+ (user_id, full_name, blood_group, date_of_birth, allergies, chronic_conditions,
119
+ current_medicines, emergency_contact_name, emergency_contact_phone, share_token, updated_at)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
121
  """, (
122
+ user_id, data["full_name"], data["blood_group"], data["date_of_birth"],
123
  data["allergies"], data["chronic_conditions"], data["current_medicines"],
124
+ data["emergency_contact_name"], data["emergency_contact_phone"], share_token, now,
125
  ))
126
  conn.commit()
127
  conn.close()
128
 
129
 
130
+ def get_passport(user_id: int) -> dict | None:
131
  conn = sqlite3.connect(DB_FILE)
132
  conn.row_factory = sqlite3.Row
133
  cur = conn.cursor()
134
+ cur.execute("SELECT * FROM health_passport WHERE user_id = ?", (user_id,))
135
  row = cur.fetchone()
136
  conn.close()
137
  return dict(row) if row else None
138
 
139
 
140
+ def get_passport_by_token(share_token: str) -> dict | None:
141
+ """Used by the public /emergency/<token> view -- no auth required."""
142
+ conn = sqlite3.connect(DB_FILE)
143
+ conn.row_factory = sqlite3.Row
144
+ cur = conn.cursor()
145
+ cur.execute("SELECT * FROM health_passport WHERE share_token = ?", (share_token,))
146
+ row = cur.fetchone()
147
+ conn.close()
148
+ return dict(row) if row else None
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Surgeries
153
+ # ---------------------------------------------------------------------------
154
+
155
+ def add_surgery(user_id: int, year: str, description: str):
156
+ conn = sqlite3.connect(DB_FILE)
157
+ cur = conn.cursor()
158
+ cur.execute(
159
+ "INSERT INTO surgeries (user_id, year, description, created_at) VALUES (?, ?, ?, ?)",
160
+ (user_id, year, description, datetime.now().isoformat(timespec="seconds")),
161
+ )
162
+ conn.commit()
163
+ conn.close()
164
+
165
+
166
+ def get_surgeries(user_id: int) -> list[dict]:
167
+ conn = sqlite3.connect(DB_FILE)
168
+ conn.row_factory = sqlite3.Row
169
+ cur = conn.cursor()
170
+ cur.execute("SELECT * FROM surgeries WHERE user_id = ? ORDER BY year DESC", (user_id,))
171
+ rows = [dict(r) for r in cur.fetchall()]
172
+ conn.close()
173
+ return rows
174
+
175
+
176
+ def delete_surgery(user_id: int, surgery_id: int):
177
+ conn = sqlite3.connect(DB_FILE)
178
+ cur = conn.cursor()
179
+ cur.execute("DELETE FROM surgeries WHERE id = ? AND user_id = ?", (surgery_id, user_id))
180
+ conn.commit()
181
+ conn.close()
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # Vaccinations
186
+ # ---------------------------------------------------------------------------
187
+
188
+ def add_vaccination(user_id: int, vaccine_name: str, month: str, year: str):
189
+ conn = sqlite3.connect(DB_FILE)
190
+ cur = conn.cursor()
191
+ cur.execute(
192
+ "INSERT INTO vaccinations (user_id, vaccine_name, month, year, created_at) VALUES (?, ?, ?, ?, ?)",
193
+ (user_id, vaccine_name, month, year, datetime.now().isoformat(timespec="seconds")),
194
+ )
195
+ conn.commit()
196
+ conn.close()
197
+
198
+
199
+ def get_vaccinations(user_id: int) -> list[dict]:
200
+ conn = sqlite3.connect(DB_FILE)
201
+ conn.row_factory = sqlite3.Row
202
+ cur = conn.cursor()
203
+ cur.execute("SELECT * FROM vaccinations WHERE user_id = ? ORDER BY year DESC", (user_id,))
204
+ rows = [dict(r) for r in cur.fetchall()]
205
+ conn.close()
206
+ return rows
207
+
208
+
209
+ def delete_vaccination(user_id: int, vaccination_id: int):
210
+ conn = sqlite3.connect(DB_FILE)
211
+ cur = conn.cursor()
212
+ cur.execute("DELETE FROM vaccinations WHERE id = ? AND user_id = ?", (vaccination_id, user_id))
213
+ conn.commit()
214
+ conn.close()
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Medical report uploads
219
+ # ---------------------------------------------------------------------------
220
+
221
+ def save_report_file(user_id: int, file_storage, category: str, month: str, year: str) -> dict:
222
  """
223
+ file_storage: a Flask FileStorage object (from request.files[...]).
224
+ Saves the file to disk under a per-user subfolder and records metadata.
225
  """
226
+ user_folder = os.path.join(UPLOAD_DIR, str(user_id))
227
+ os.makedirs(user_folder, exist_ok=True)
228
+
229
+ # Prefix with a timestamp so same-named uploads don't collide
230
+ safe_name = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{file_storage.filename}"
231
+ stored_path = os.path.join(user_folder, safe_name)
232
+ file_storage.save(stored_path)
233
+
234
+ conn = sqlite3.connect(DB_FILE)
235
+ cur = conn.cursor()
236
+ cur.execute("""
237
+ INSERT INTO medical_reports (user_id, filename, stored_path, category, month, year, uploaded_at)
238
+ VALUES (?, ?, ?, ?, ?, ?, ?)
239
+ """, (
240
+ user_id, file_storage.filename, stored_path, category, month, year,
241
+ datetime.now().isoformat(timespec="seconds"),
242
+ ))
243
+ conn.commit()
244
+ report_id = cur.lastrowid
245
+ conn.close()
246
+
247
+ return {
248
+ "id": report_id, "filename": file_storage.filename, "category": category,
249
+ "month": month, "year": year,
250
+ }
251
+
252
+
253
+ def get_reports(user_id: int) -> list[dict]:
254
+ conn = sqlite3.connect(DB_FILE)
255
+ conn.row_factory = sqlite3.Row
256
+ cur = conn.cursor()
257
+ cur.execute(
258
+ "SELECT * FROM medical_reports WHERE user_id = ? ORDER BY year DESC, month DESC",
259
+ (user_id,),
260
  )
261
+ rows = [dict(r) for r in cur.fetchall()]
262
+ conn.close()
263
+ return rows
264
+
265
+
266
+ def get_report_by_id(user_id: int, report_id: int) -> dict | None:
267
+ conn = sqlite3.connect(DB_FILE)
268
+ conn.row_factory = sqlite3.Row
269
+ cur = conn.cursor()
270
+ cur.execute("SELECT * FROM medical_reports WHERE id = ? AND user_id = ?", (report_id, user_id))
271
+ row = cur.fetchone()
272
+ conn.close()
273
+ return dict(row) if row else None
274
+
275
+
276
+ def delete_report(user_id: int, report_id: int):
277
+ report = get_report_by_id(user_id, report_id)
278
+ if report and os.path.exists(report["stored_path"]):
279
+ os.remove(report["stored_path"])
280
+
281
+ conn = sqlite3.connect(DB_FILE)
282
+ cur = conn.cursor()
283
+ cur.execute("DELETE FROM medical_reports WHERE id = ? AND user_id = ?", (report_id, user_id))
284
+ conn.commit()
285
+ conn.close()
286
+
287
+
288
+ # ---------------------------------------------------------------------------
289
+ # QR code -- now encodes a URL to the public emergency view, not raw text
290
+ # ---------------------------------------------------------------------------
291
+
292
+ def generate_qr_code(share_token: str, base_url: str) -> bytes:
293
+ """
294
+ base_url: e.g. "http://localhost:5000" -- passed in from app.py using
295
+ request.host_url so it works correctly whether running locally or deployed.
296
+ """
297
+ emergency_url = f"{base_url.rstrip('/')}/emergency/{share_token}"
298
 
299
  qr = qrcode.QRCode(version=None, box_size=8, border=3)
300
+ qr.add_data(emergency_url)
301
  qr.make(fit=True)
302
  img = qr.make_image(fill_color="#0F6FFF", back_color="white")
303
 
304
  buf = io.BytesIO()
305
  img.save(buf, format="PNG")
306
  return buf.getvalue()
307
+
308
+
309
+ if __name__ == "__main__":
310
+ # Quick manual test -- run: python health_passport.py
311
+ import os as _os
312
+ if _os.path.exists(DB_FILE):
313
+ _os.remove(DB_FILE)
314
+
315
+ init_passport_table()
316
+
317
+ save_passport(1, {
318
+ "full_name": "Jane Doe", "blood_group": "O+", "date_of_birth": "01-01-1995",
319
+ "allergies": "Penicillin", "chronic_conditions": "Asthma",
320
+ "current_medicines": "Ventolin inhaler", "emergency_contact_name": "John Doe",
321
+ "emergency_contact_phone": "+91-9999999999",
322
+ })
323
+ passport = get_passport(1)
324
+ print(f"Saved passport for: {passport['full_name']}, share_token={passport['share_token']}")
325
+
326
+ add_surgery(1, "2019", "Appendectomy")
327
+ add_vaccination(1, "COVID-19 Booster", "March", "2024")
328
+ print(f"Surgeries: {get_surgeries(1)}")
329
+ print(f"Vaccinations: {get_vaccinations(1)}")
330
+
331
+ by_token = get_passport_by_token(passport["share_token"])
332
+ print(f"Fetched via public token: {by_token['full_name']}")
333
+
334
+ qr_bytes = generate_qr_code(passport["share_token"], "http://localhost:5000")
335
+ print(f"QR code generated: {len(qr_bytes)} bytes")
pdf_export.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PDF export for the Health Passport.
3
+
4
+ Generates a clean, printable one-page PDF with all passport info, surgeries,
5
+ vaccinations, and the emergency QR code embedded -- so a physical printed
6
+ copy works exactly like the digital one for a doctor scanning it.
7
+
8
+ Uses fpdf2 (pure Python, no system dependencies like wkhtmltopdf/Chrome
9
+ needed -- important for easy deployment).
10
+ """
11
+
12
+ import io
13
+ from fpdf import FPDF
14
+
15
+ PRIMARY_RGB = (15, 111, 255) # #0F6FFF
16
+ SECONDARY_RGB = (16, 185, 129) # #10B981
17
+ TEXT_RGB = (30, 41, 59)
18
+ MUTED_RGB = (100, 116, 139)
19
+
20
+
21
+ class PassportPDF(FPDF):
22
+ def header(self):
23
+ self.set_fill_color(*PRIMARY_RGB)
24
+ self.rect(0, 0, 210, 22, style="F")
25
+ self.set_text_color(255, 255, 255)
26
+ self.set_font("Helvetica", "B", 16)
27
+ self.set_xy(10, 6)
28
+ self.cell(0, 10, "VeriMed AI -- Health Passport", new_x="LMARGIN", new_y="NEXT")
29
+ self.set_font("Helvetica", "", 9)
30
+ self.set_xy(10, 15)
31
+ self.cell(0, 6, "Emergency Medical Information Card", new_x="LMARGIN", new_y="NEXT")
32
+ self.ln(8)
33
+
34
+ def footer(self):
35
+ self.set_y(-15)
36
+ self.set_font("Helvetica", "I", 7)
37
+ self.set_text_color(*MUTED_RGB)
38
+ self.cell(0, 10, "Generated by VeriMed AI. Educational project -- not a substitute for official medical records.", align="C")
39
+
40
+ def section_title(self, title: str):
41
+ self.ln(3)
42
+ self.set_font("Helvetica", "B", 12)
43
+ self.set_text_color(*PRIMARY_RGB)
44
+ self.cell(0, 8, title, new_x="LMARGIN", new_y="NEXT")
45
+ self.set_draw_color(*PRIMARY_RGB)
46
+ self.line(10, self.get_y(), 200, self.get_y())
47
+ self.ln(2)
48
+
49
+ def field_row(self, label: str, value: str):
50
+ self.set_font("Helvetica", "B", 10)
51
+ self.set_text_color(*MUTED_RGB)
52
+ self.cell(55, 7, label, new_x="RIGHT", new_y="TOP")
53
+ self.set_font("Helvetica", "", 10)
54
+ self.set_text_color(*TEXT_RGB)
55
+ self.multi_cell(135, 7, value or "Not provided", new_x="LMARGIN", new_y="NEXT")
56
+
57
+
58
+ def generate_passport_pdf(passport: dict, surgeries: list[dict], vaccinations: list[dict], qr_png_bytes: bytes) -> bytes:
59
+ pdf = PassportPDF(format="A4")
60
+ pdf.add_page()
61
+ pdf.set_auto_page_break(auto=True, margin=20)
62
+
63
+ # ---- Personal details ----
64
+ pdf.section_title("Personal Details")
65
+ pdf.field_row("Full Name:", passport.get("full_name", ""))
66
+ pdf.field_row("Date of Birth:", passport.get("date_of_birth", ""))
67
+ pdf.field_row("Blood Group:", passport.get("blood_group", ""))
68
+
69
+ # ---- Medical info ----
70
+ pdf.section_title("Medical Information")
71
+ pdf.field_row("Allergies:", passport.get("allergies") or "None listed")
72
+ pdf.field_row("Chronic Conditions:", passport.get("chronic_conditions") or "None listed")
73
+ pdf.field_row("Current Medicines:", passport.get("current_medicines") or "None listed")
74
+
75
+ # ---- Surgeries ----
76
+ pdf.section_title("Surgical History")
77
+ if surgeries:
78
+ for s in surgeries:
79
+ pdf.field_row(f"{s['year']}:", s["description"])
80
+ else:
81
+ pdf.set_font("Helvetica", "I", 10)
82
+ pdf.set_text_color(*MUTED_RGB)
83
+ pdf.cell(0, 7, "None recorded.", new_x="LMARGIN", new_y="NEXT")
84
+
85
+ # ---- Vaccinations ----
86
+ pdf.section_title("Vaccination Record")
87
+ if vaccinations:
88
+ for v in vaccinations:
89
+ pdf.field_row(f"{v['month']} {v['year']}:", v["vaccine_name"])
90
+ else:
91
+ pdf.set_font("Helvetica", "I", 10)
92
+ pdf.set_text_color(*MUTED_RGB)
93
+ pdf.cell(0, 7, "None recorded.", new_x="LMARGIN", new_y="NEXT")
94
+
95
+ # ---- Emergency contact ----
96
+ pdf.section_title("Emergency Contact")
97
+ pdf.field_row("Name:", passport.get("emergency_contact_name", ""))
98
+ pdf.field_row("Phone:", passport.get("emergency_contact_phone", ""))
99
+
100
+ # ---- QR code ----
101
+ pdf.section_title("Emergency QR Code")
102
+ pdf.set_font("Helvetica", "", 9)
103
+ pdf.set_text_color(*MUTED_RGB)
104
+ pdf.multi_cell(0, 6, "Scan this code to view this person's emergency medical summary instantly -- no app or login required.")
105
+ pdf.ln(2)
106
+
107
+ qr_stream = io.BytesIO(qr_png_bytes)
108
+ pdf.image(qr_stream, x=10, y=pdf.get_y(), w=35, h=35)
109
+ pdf.ln(40)
110
+
111
+ output = pdf.output()
112
+ return bytes(output)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ # Quick manual test -- run: python pdf_export.py
117
+ import health_passport as hp
118
+ import os
119
+
120
+ if os.path.exists(hp.DB_FILE):
121
+ os.remove(hp.DB_FILE)
122
+ hp.init_passport_table()
123
+ hp.save_passport(1, {
124
+ "full_name": "Jane Doe", "blood_group": "O+", "date_of_birth": "01-01-1995",
125
+ "allergies": "Penicillin", "chronic_conditions": "Asthma",
126
+ "current_medicines": "Ventolin inhaler", "emergency_contact_name": "John Doe",
127
+ "emergency_contact_phone": "+91-9999999999",
128
+ })
129
+ hp.add_surgery(1, "2019", "Appendectomy")
130
+ hp.add_vaccination(1, "COVID-19 Booster", "March", "2024")
131
+
132
+ passport = hp.get_passport(1)
133
+ surgeries = hp.get_surgeries(1)
134
+ vaccinations = hp.get_vaccinations(1)
135
+ qr_bytes = hp.generate_qr_code(passport["share_token"], "http://localhost:5000")
136
+
137
+ pdf_bytes = generate_passport_pdf(passport, surgeries, vaccinations, qr_bytes)
138
+
139
+ with open("test_passport_output.pdf", "wb") as f:
140
+ f.write(pdf_bytes)
141
+
142
+ print(f"✅ PDF generated: {len(pdf_bytes)} bytes, saved to test_passport_output.pdf")
requirements.txt CHANGED
@@ -29,3 +29,7 @@ qrcode[pil]
29
  # ==== Translation (Quick Win) ====
30
  deep-translator
31
 
 
 
 
 
 
29
  # ==== Translation (Quick Win) ====
30
  deep-translator
31
 
32
+ # ==== Auth + PDF Export (Login/Register feature) ====
33
+ flask-login
34
+ fpdf2
35
+
static/js/main.js CHANGED
@@ -16,10 +16,166 @@ function verdictColorClasses(verdict) {
16
  }
17
  }
18
 
19
- function escapeHtml(str) {
20
- const div = document.createElement("div");
21
- div.textContent = str == null ? "" : String(str);
22
- return div.innerHTML;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
  // ---------- Checker page ----------
@@ -259,7 +415,9 @@ if (passportForm) {
259
  if (!data) return;
260
  document.getElementById("passName").value = data.full_name || "";
261
  document.getElementById("passBlood").value = data.blood_group || "";
 
262
  document.getElementById("passAllergies").value = data.allergies || "";
 
263
  document.getElementById("passMeds").value = data.current_medicines || "";
264
  document.getElementById("passContact").value = data.emergency_contact_name || "";
265
  document.getElementById("passPhone").value = data.emergency_contact_phone || "";
@@ -273,9 +431,9 @@ if (passportForm) {
273
  const payload = {
274
  full_name: document.getElementById("passName").value,
275
  blood_group: document.getElementById("passBlood").value,
276
- date_of_birth: "",
277
  allergies: document.getElementById("passAllergies").value,
278
- chronic_conditions: "",
279
  current_medicines: document.getElementById("passMeds").value,
280
  emergency_contact_name: document.getElementById("passContact").value,
281
  emergency_contact_phone: document.getElementById("passPhone").value,
@@ -310,6 +468,152 @@ function showQrCode() {
310
  qrContainer.innerHTML = `<img src="/api/passport/qr?t=${Date.now()}" alt="Health Passport QR Code" class="w-40 h-40 object-contain" />`;
311
  }
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  // ---------- Home page: Personalized Snapshot ----------
314
 
315
  const passportSnapshotCard = document.getElementById("passportSnapshotCard");
@@ -399,4 +703,13 @@ if (latestAlertsContainer) {
399
  .catch(() => {
400
  latestAlertsContainer.innerHTML = `<div class="md:col-span-3 text-center text-red-400 text-sm py-6">Couldn't load recent alerts.</div>`;
401
  });
 
 
 
 
 
 
 
 
 
402
  }
 
16
  }
17
  }
18
 
19
+ // ---------- Emergency Help page ----------
20
+
21
+ const useGpsBtn = document.getElementById("useGpsBtn");
22
+ if (useGpsBtn) {
23
+ const manualSearchBtn = document.getElementById("manualSearchBtn");
24
+ const manualLocationInput = document.getElementById("manualLocationInput");
25
+ const hospitalStatus = document.getElementById("hospitalStatus");
26
+ const hospitalList = document.getElementById("hospitalList");
27
+
28
+ function setStatus(message, isError = false) {
29
+ hospitalStatus.textContent = message;
30
+ hospitalStatus.className = isError
31
+ ? "text-sm text-red-500 text-center py-6"
32
+ : "text-sm text-slate-400 text-center py-6";
33
+ }
34
+
35
+ function haversineDistanceKm(lat1, lon1, lat2, lon2) {
36
+ const R = 6371;
37
+ const dLat = ((lat2 - lat1) * Math.PI) / 180;
38
+ const dLon = ((lon2 - lon1) * Math.PI) / 180;
39
+ const a =
40
+ Math.sin(dLat / 2) ** 2 +
41
+ Math.cos((lat1 * Math.PI) / 180) *
42
+ Math.cos((lat2 * Math.PI) / 180) *
43
+ Math.sin(dLon / 2) ** 2;
44
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
45
+ }
46
+
47
+ function buildAddress(tags) {
48
+ const parts = [
49
+ tags["addr:housenumber"],
50
+ tags["addr:street"],
51
+ tags["addr:suburb"],
52
+ tags["addr:city"],
53
+ tags["addr:postcode"],
54
+ ].filter(Boolean);
55
+ return parts.length ? parts.join(", ") : "Address not available";
56
+ }
57
+
58
+ async function fetchNearbyHospitals(lat, lon) {
59
+ hospitalList.innerHTML = "";
60
+ setStatus("Searching for hospitals and clinics nearby...");
61
+
62
+ // Overpass API (OpenStreetMap) -- free, no API key required
63
+ const query = `
64
+ [out:json][timeout:25];
65
+ (
66
+ node["amenity"="hospital"](around:6000,${lat},${lon});
67
+ way["amenity"="hospital"](around:6000,${lat},${lon});
68
+ node["amenity"="clinic"](around:6000,${lat},${lon});
69
+ way["amenity"="clinic"](around:6000,${lat},${lon});
70
+ );
71
+ out center 40;
72
+ `;
73
+
74
+ try {
75
+ const res = await fetch("https://overpass-api.de/api/interpreter", {
76
+ method: "POST",
77
+ body: "data=" + encodeURIComponent(query),
78
+ });
79
+ if (!res.ok) throw new Error("Overpass API request failed.");
80
+ const data = await res.json();
81
+
82
+ const results = (data.elements || [])
83
+ .filter((el) => el.tags && el.tags.name)
84
+ .map((el) => {
85
+ const elLat = el.lat || (el.center && el.center.lat);
86
+ const elLon = el.lon || (el.center && el.center.lon);
87
+ return {
88
+ name: el.tags.name,
89
+ type: el.tags.amenity === "hospital" ? "Hospital" : "Clinic",
90
+ address: buildAddress(el.tags),
91
+ phone: el.tags.phone || el.tags["contact:phone"] || null,
92
+ lat: elLat,
93
+ lon: elLon,
94
+ distanceKm: elLat && elLon ? haversineDistanceKm(lat, lon, elLat, elLon) : null,
95
+ };
96
+ })
97
+ .filter((h) => h.lat && h.lon)
98
+ .sort((a, b) => (a.distanceKm ?? 999) - (b.distanceKm ?? 999))
99
+ .slice(0, 20);
100
+
101
+ if (results.length === 0) {
102
+ setStatus("No hospitals found nearby. Try a different location.", true);
103
+ return;
104
+ }
105
+
106
+ setStatus(`Found ${results.length} hospitals/clinics nearby, sorted by distance.`);
107
+ renderHospitalList(results);
108
+ } catch (err) {
109
+ setStatus("Couldn't fetch nearby hospitals. Please try again.", true);
110
+ }
111
+ }
112
+
113
+ function renderHospitalList(hospitals) {
114
+ hospitalList.innerHTML = hospitals
115
+ .map((h) => {
116
+ const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${h.lat},${h.lon}`;
117
+ const distanceLabel = h.distanceKm !== null ? `${h.distanceKm.toFixed(1)} km away` : "";
118
+ return `
119
+ <div class="bg-slate-50 border border-slate-100 rounded-2xl p-4">
120
+ <div class="flex items-start justify-between gap-2">
121
+ <div>
122
+ <h4 class="font-bold text-slate-800">${escapeHtml(h.name)}</h4>
123
+ <span class="inline-block text-[10px] font-bold uppercase tracking-wide text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full mt-1">${escapeHtml(h.type)}</span>
124
+ ${distanceLabel ? `<span class="text-xs text-slate-400 ml-2">${distanceLabel}</span>` : ""}
125
+ </div>
126
+ </div>
127
+ <p class="text-xs text-slate-500 mt-2">${escapeHtml(h.address)}</p>
128
+ <div class="flex gap-2 mt-3">
129
+ ${h.phone ? `<a href="tel:${escapeHtml(h.phone)}" class="flex-1 text-center bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold py-2 rounded-lg transition-colors"><i class="fa-solid fa-phone mr-1"></i>Call</a>` : ""}
130
+ <a href="${directionsUrl}" target="_blank" class="flex-1 text-center bg-blue-600 hover:bg-blue-700 text-white text-xs font-bold py-2 rounded-lg transition-colors"><i class="fa-solid fa-location-arrow mr-1"></i>Directions</a>
131
+ </div>
132
+ </div>`;
133
+ })
134
+ .join("");
135
+ }
136
+
137
+ useGpsBtn.addEventListener("click", () => {
138
+ if (!navigator.geolocation) {
139
+ setStatus("Your browser doesn't support GPS location.", true);
140
+ return;
141
+ }
142
+ setStatus("Requesting your location...");
143
+ navigator.geolocation.getCurrentPosition(
144
+ (position) => {
145
+ fetchNearbyHospitals(position.coords.latitude, position.coords.longitude);
146
+ },
147
+ () => {
148
+ setStatus("Location access denied. Try typing a location manually instead.", true);
149
+ }
150
+ );
151
+ });
152
+
153
+ async function searchManualLocation() {
154
+ const query = manualLocationInput.value.trim();
155
+ if (!query) return;
156
+
157
+ setStatus(`Looking up "${query}"...`);
158
+ try {
159
+ // Nominatim (OpenStreetMap) geocoding -- free, no API key required
160
+ const res = await fetch(
161
+ `https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(query)}`
162
+ );
163
+ const results = await res.json();
164
+ if (!results || results.length === 0) {
165
+ setStatus(`Couldn't find "${query}". Try a more specific location.`, true);
166
+ return;
167
+ }
168
+ const { lat, lon } = results[0];
169
+ fetchNearbyHospitals(parseFloat(lat), parseFloat(lon));
170
+ } catch (err) {
171
+ setStatus("Location search failed. Please try again.", true);
172
+ }
173
+ }
174
+
175
+ manualSearchBtn.addEventListener("click", searchManualLocation);
176
+ manualLocationInput.addEventListener("keypress", (e) => {
177
+ if (e.key === "Enter") searchManualLocation();
178
+ });
179
  }
180
 
181
  // ---------- Checker page ----------
 
415
  if (!data) return;
416
  document.getElementById("passName").value = data.full_name || "";
417
  document.getElementById("passBlood").value = data.blood_group || "";
418
+ document.getElementById("passDob").value = data.date_of_birth || "";
419
  document.getElementById("passAllergies").value = data.allergies || "";
420
+ document.getElementById("passChronic").value = data.chronic_conditions || "";
421
  document.getElementById("passMeds").value = data.current_medicines || "";
422
  document.getElementById("passContact").value = data.emergency_contact_name || "";
423
  document.getElementById("passPhone").value = data.emergency_contact_phone || "";
 
431
  const payload = {
432
  full_name: document.getElementById("passName").value,
433
  blood_group: document.getElementById("passBlood").value,
434
+ date_of_birth: document.getElementById("passDob").value,
435
  allergies: document.getElementById("passAllergies").value,
436
+ chronic_conditions: document.getElementById("passChronic").value,
437
  current_medicines: document.getElementById("passMeds").value,
438
  emergency_contact_name: document.getElementById("passContact").value,
439
  emergency_contact_phone: document.getElementById("passPhone").value,
 
468
  qrContainer.innerHTML = `<img src="/api/passport/qr?t=${Date.now()}" alt="Health Passport QR Code" class="w-40 h-40 object-contain" />`;
469
  }
470
 
471
+ // ---------- Surgeries ----------
472
+
473
+ const surgeryForm = document.getElementById("surgeryForm");
474
+ if (surgeryForm) {
475
+ function loadSurgeries() {
476
+ fetch("/api/passport/surgeries")
477
+ .then((res) => res.json())
478
+ .then((surgeries) => {
479
+ const list = document.getElementById("surgeryList");
480
+ if (!surgeries || surgeries.length === 0) {
481
+ list.innerHTML = `<p class="text-xs text-slate-400 text-center py-3">No surgeries recorded yet.</p>`;
482
+ return;
483
+ }
484
+ list.innerHTML = surgeries.map((s) => `
485
+ <div class="flex items-center justify-between bg-slate-50 border border-slate-100 rounded-xl p-3">
486
+ <div><span class="font-bold text-sm text-slate-700">${escapeHtml(s.year)}</span> <span class="text-sm text-slate-500">-- ${escapeHtml(s.description)}</span></div>
487
+ <button onclick="deleteSurgery(${s.id})" class="text-red-400 hover:text-red-600 text-xs"><i class="fa-solid fa-trash"></i></button>
488
+ </div>`).join("");
489
+ });
490
+ }
491
+
492
+ surgeryForm.addEventListener("submit", async (e) => {
493
+ e.preventDefault();
494
+ const year = document.getElementById("surgeryYear").value;
495
+ const description = document.getElementById("surgeryDescription").value;
496
+ await fetch("/api/passport/surgeries", {
497
+ method: "POST",
498
+ headers: { "Content-Type": "application/json" },
499
+ body: JSON.stringify({ year, description }),
500
+ });
501
+ surgeryForm.reset();
502
+ loadSurgeries();
503
+ });
504
+
505
+ window.deleteSurgery = async (id) => {
506
+ await fetch(`/api/passport/surgeries/${id}`, { method: "DELETE" });
507
+ loadSurgeries();
508
+ };
509
+
510
+ loadSurgeries();
511
+ }
512
+
513
+ // ---------- Vaccinations ----------
514
+
515
+ const vaccinationForm = document.getElementById("vaccinationForm");
516
+ if (vaccinationForm) {
517
+ function loadVaccinations() {
518
+ fetch("/api/passport/vaccinations")
519
+ .then((res) => res.json())
520
+ .then((vaccinations) => {
521
+ const list = document.getElementById("vaccinationList");
522
+ if (!vaccinations || vaccinations.length === 0) {
523
+ list.innerHTML = `<p class="text-xs text-slate-400 text-center py-3">No vaccinations recorded yet.</p>`;
524
+ return;
525
+ }
526
+ list.innerHTML = vaccinations.map((v) => `
527
+ <div class="flex items-center justify-between bg-slate-50 border border-slate-100 rounded-xl p-3">
528
+ <div><span class="font-bold text-sm text-slate-700">${escapeHtml(v.month)} ${escapeHtml(v.year)}</span> <span class="text-sm text-slate-500">-- ${escapeHtml(v.vaccine_name)}</span></div>
529
+ <button onclick="deleteVaccination(${v.id})" class="text-red-400 hover:text-red-600 text-xs"><i class="fa-solid fa-trash"></i></button>
530
+ </div>`).join("");
531
+ });
532
+ }
533
+
534
+ vaccinationForm.addEventListener("submit", async (e) => {
535
+ e.preventDefault();
536
+ const vaccine_name = document.getElementById("vaccineName").value;
537
+ const month = document.getElementById("vaccineMonth").value;
538
+ const year = document.getElementById("vaccineYear").value;
539
+ await fetch("/api/passport/vaccinations", {
540
+ method: "POST",
541
+ headers: { "Content-Type": "application/json" },
542
+ body: JSON.stringify({ vaccine_name, month, year }),
543
+ });
544
+ vaccinationForm.reset();
545
+ loadVaccinations();
546
+ });
547
+
548
+ window.deleteVaccination = async (id) => {
549
+ await fetch(`/api/passport/vaccinations/${id}`, { method: "DELETE" });
550
+ loadVaccinations();
551
+ };
552
+
553
+ loadVaccinations();
554
+ }
555
+
556
+ // ---------- Medical Report Uploads ----------
557
+
558
+ const reportForm = document.getElementById("reportForm");
559
+ if (reportForm) {
560
+ function loadReports() {
561
+ fetch("/api/passport/reports")
562
+ .then((res) => res.json())
563
+ .then((reports) => {
564
+ const list = document.getElementById("reportList");
565
+ if (!reports || reports.length === 0) {
566
+ list.innerHTML = `<p class="text-xs text-slate-400 text-center py-3">No reports uploaded yet.</p>`;
567
+ return;
568
+ }
569
+ list.innerHTML = reports.map((r) => `
570
+ <div class="flex items-center justify-between bg-slate-50 border border-slate-100 rounded-xl p-3">
571
+ <div>
572
+ <span class="font-bold text-sm text-slate-700">${escapeHtml(r.category)}</span>
573
+ <span class="text-xs text-slate-400 ml-2">${escapeHtml(r.month)} ${escapeHtml(r.year)}</span>
574
+ <div class="text-xs text-slate-500">${escapeHtml(r.filename)}</div>
575
+ </div>
576
+ <div class="flex items-center gap-3">
577
+ <a href="/api/passport/reports/${r.id}/download" class="text-blue-500 hover:text-blue-700 text-xs"><i class="fa-solid fa-download"></i></a>
578
+ <button onclick="deleteReport(${r.id})" class="text-red-400 hover:text-red-600 text-xs"><i class="fa-solid fa-trash"></i></button>
579
+ </div>
580
+ </div>`).join("");
581
+ });
582
+ }
583
+
584
+ reportForm.addEventListener("submit", async (e) => {
585
+ e.preventDefault();
586
+ const fileInput = document.getElementById("reportFile");
587
+ if (!fileInput.files.length) return;
588
+
589
+ const formData = new FormData();
590
+ formData.append("file", fileInput.files[0]);
591
+ formData.append("category", document.getElementById("reportCategory").value);
592
+ formData.append("month", document.getElementById("reportMonth").value);
593
+ formData.append("year", document.getElementById("reportYear").value);
594
+
595
+ const submitBtn = reportForm.querySelector("button[type='submit']");
596
+ submitBtn.disabled = true;
597
+ submitBtn.textContent = "Uploading...";
598
+
599
+ try {
600
+ await fetch("/api/passport/reports", { method: "POST", body: formData });
601
+ reportForm.reset();
602
+ loadReports();
603
+ } finally {
604
+ submitBtn.disabled = false;
605
+ submitBtn.textContent = "Upload Report";
606
+ }
607
+ });
608
+
609
+ window.deleteReport = async (id) => {
610
+ await fetch(`/api/passport/reports/${id}`, { method: "DELETE" });
611
+ loadReports();
612
+ };
613
+
614
+ loadReports();
615
+ }
616
+
617
  // ---------- Home page: Personalized Snapshot ----------
618
 
619
  const passportSnapshotCard = document.getElementById("passportSnapshotCard");
 
703
  .catch(() => {
704
  latestAlertsContainer.innerHTML = `<div class="md:col-span-3 text-center text-red-400 text-sm py-6">Couldn't load recent alerts.</div>`;
705
  });
706
+ }
707
+
708
+ function escapeHtml(str) {
709
+ if (str === null || str === undefined) return "";
710
+ return String(str)
711
+ .replace(/&/g, "&amp;")
712
+ .replace(/</g, "&lt;")
713
+ .replace(/>/g, "&gt;")
714
+ .replace(/"/g, "&quot;");
715
  }
steps.txt CHANGED
@@ -58,4 +58,8 @@ pip install deep-translator
58
 
59
  ____________________--
60
  gnn
61
- pip install torch-geometric networkx
 
 
 
 
 
58
 
59
  ____________________--
60
  gnn
61
+ pip install torch-geometric networkx
62
+
63
+ _____________________________________________________________________________________________________________________________________________________
64
+ passport page
65
+ pip install flask-login fpdf2
templates/base.html CHANGED
@@ -28,9 +28,31 @@
28
  <a href="/checker" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-magnifying-glass-shield mr-1"></i> AI Fact Checker</a>
29
  <a href="/predictor" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-diagram-project mr-1"></i> GNN Predictor</a>
30
  <a href="/passport" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-id-card-clip mr-1"></i> Health Passport</a>
 
 
 
 
 
 
 
 
 
 
31
  </div>
32
  </nav>
33
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  <!-- Main Dynamic Content Wrapper -->
35
  <main class="flex-1 max-w-7xl w-full mx-auto p-6">
36
  {% block content %}{% endblock %}
 
28
  <a href="/checker" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-magnifying-glass-shield mr-1"></i> AI Fact Checker</a>
29
  <a href="/predictor" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-diagram-project mr-1"></i> GNN Predictor</a>
30
  <a href="/passport" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-id-card-clip mr-1"></i> Health Passport</a>
31
+ <a href="/emergency-help" class="text-red-500 hover:text-red-700 transition-colors font-bold"><i class="fa-solid fa-truck-medical mr-1"></i> Emergency</a>
32
+ {% if current_user.is_authenticated %}
33
+ <span class="text-slate-400">|</span>
34
+ <span class="text-slate-500"><i class="fa-solid fa-circle-user mr-1"></i>{{ current_user.name }}</span>
35
+ <a href="/logout" class="text-red-500 hover:text-red-700 transition-colors"><i class="fa-solid fa-right-from-bracket mr-1"></i>Logout</a>
36
+ {% else %}
37
+ <span class="text-slate-400">|</span>
38
+ <a href="/login" class="hover:text-blue-600 transition-colors">Login</a>
39
+ <a href="/register" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">Register</a>
40
+ {% endif %}
41
  </div>
42
  </nav>
43
 
44
+ {% with messages = get_flashed_messages(with_categories=true) %}
45
+ {% if messages %}
46
+ <div class="max-w-7xl w-full mx-auto px-6 pt-4">
47
+ {% for category, message in messages %}
48
+ <div class="p-3 rounded-xl text-sm font-semibold mb-2 {{ 'bg-red-50 text-red-700 border border-red-200' if category == 'error' else 'bg-emerald-50 text-emerald-700 border border-emerald-200' }}">
49
+ {{ message }}
50
+ </div>
51
+ {% endfor %}
52
+ </div>
53
+ {% endif %}
54
+ {% endwith %}
55
+
56
  <!-- Main Dynamic Content Wrapper -->
57
  <main class="flex-1 max-w-7xl w-full mx-auto p-6">
58
  {% block content %}{% endblock %}
templates/emergency_help.html ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'base.html' %}
2
+ {% block content %}
3
+
4
+ <div class="mb-8 p-8 rounded-3xl bg-gradient-to-r from-red-600 via-rose-600 to-orange-600 text-white shadow-xl shadow-red-100">
5
+ <h1 class="text-3xl font-black mb-2"><i class="fa-solid fa-truck-medical mr-2"></i>Emergency Help</h1>
6
+ <p class="text-red-100 max-w-2xl text-sm leading-relaxed">
7
+ Immediate emergency contact numbers and hospitals near your current location.
8
+ </p>
9
+ </div>
10
+
11
+ <!-- Emergency Numbers -->
12
+ <div class="mb-8">
13
+ <h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-phone-volume text-red-600 mr-2"></i>Emergency Numbers (India)</h2>
14
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
15
+ <a href="tel:112" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
16
+ <div class="text-2xl font-black text-red-600">112</div>
17
+ <div class="text-xs font-bold text-slate-500 mt-1">National Emergency</div>
18
+ </a>
19
+ <a href="tel:108" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
20
+ <div class="text-2xl font-black text-red-600">108</div>
21
+ <div class="text-xs font-bold text-slate-500 mt-1">Ambulance</div>
22
+ </a>
23
+ <a href="tel:101" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
24
+ <div class="text-2xl font-black text-red-600">101</div>
25
+ <div class="text-xs font-bold text-slate-500 mt-1">Fire Brigade</div>
26
+ </a>
27
+ <a href="tel:100" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
28
+ <div class="text-2xl font-black text-red-600">100</div>
29
+ <div class="text-xs font-bold text-slate-500 mt-1">Police</div>
30
+ </a>
31
+ <a href="tel:1091" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
32
+ <div class="text-2xl font-black text-red-600">1091</div>
33
+ <div class="text-xs font-bold text-slate-500 mt-1">Women's Helpline</div>
34
+ </a>
35
+ <a href="tel:1098" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
36
+ <div class="text-2xl font-black text-red-600">1098</div>
37
+ <div class="text-xs font-bold text-slate-500 mt-1">Child Helpline</div>
38
+ </a>
39
+ <a href="tel:1075" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
40
+ <div class="text-2xl font-black text-red-600">1075</div>
41
+ <div class="text-xs font-bold text-slate-500 mt-1">Disaster Mgmt</div>
42
+ </a>
43
+ <a href="tel:1930" class="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md hover:border-red-200 transition-all text-center">
44
+ <div class="text-2xl font-black text-red-600">1930</div>
45
+ <div class="text-xs font-bold text-slate-500 mt-1">Cyber Crime</div>
46
+ </a>
47
+ </div>
48
+ <p class="text-xs text-slate-400 mt-3">Tap any number to call directly on mobile. Numbers shown are for India -- update for your region if deploying elsewhere.</p>
49
+ </div>
50
+
51
+ <!-- Nearby Hospitals -->
52
+ <div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
53
+ <h2 class="text-lg font-bold text-slate-800 mb-1"><i class="fa-solid fa-hospital text-blue-600 mr-2"></i>Find Nearby Hospitals</h2>
54
+ <p class="text-xs text-slate-400 mb-4">Data from OpenStreetMap. Use GPS for instant results, or search a location manually.</p>
55
+
56
+ <div class="flex flex-col md:flex-row gap-3 mb-6">
57
+ <button id="useGpsBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-5 rounded-xl text-sm flex items-center justify-center gap-2 transition-colors">
58
+ <i class="fa-solid fa-location-crosshairs"></i> Use My Current Location (GPS)
59
+ </button>
60
+ <div class="flex-1 flex gap-2">
61
+ <input type="text" id="manualLocationInput" placeholder="Or type a city / area name..." class="flex-1 bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500">
62
+ <button id="manualSearchBtn" class="bg-slate-700 hover:bg-slate-800 text-white font-bold py-3 px-5 rounded-xl text-sm transition-colors">
63
+ <i class="fa-solid fa-magnifying-glass"></i>
64
+ </button>
65
+ </div>
66
+ </div>
67
+
68
+ <div id="hospitalStatus" class="text-sm text-slate-400 text-center py-6">
69
+ Choose GPS or type a location to find nearby hospitals.
70
+ </div>
71
+
72
+ <div id="hospitalList" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
73
+ </div>
74
+
75
+ {% endblock %}
templates/emergency_view.html ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Emergency Medical Info -- VeriMed AI</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ </head>
10
+ <body class="bg-slate-50 min-h-screen">
11
+
12
+ <div class="bg-red-600 text-white text-center py-3 font-bold text-sm tracking-wide">
13
+ <i class="fa-solid fa-triangle-exclamation mr-2"></i>EMERGENCY MEDICAL INFORMATION -- READ ONLY
14
+ </div>
15
+
16
+ <div class="max-w-xl mx-auto p-6">
17
+
18
+ {% if not passport %}
19
+ <div class="bg-white p-8 rounded-3xl border border-slate-100 shadow-xl text-center mt-10">
20
+ <i class="fa-solid fa-circle-question text-4xl text-slate-300 mb-4"></i>
21
+ <h1 class="text-xl font-black text-slate-700">No Record Found</h1>
22
+ <p class="text-sm text-slate-500 mt-2">This QR code link is invalid or has expired.</p>
23
+ </div>
24
+ {% else %}
25
+ <div class="bg-white p-6 rounded-3xl border border-slate-100 shadow-xl mt-6">
26
+ <div class="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100">
27
+ <div class="bg-blue-600 text-white w-12 h-12 rounded-xl flex items-center justify-center">
28
+ <i class="fa-solid fa-id-card-clip text-xl"></i>
29
+ </div>
30
+ <div>
31
+ <h1 class="text-xl font-black text-slate-800">{{ passport.full_name }}</h1>
32
+ <p class="text-xs text-slate-400">VeriMed AI Health Passport</p>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="grid grid-cols-2 gap-4 mb-5">
37
+ <div class="bg-red-50 border border-red-100 p-4 rounded-2xl text-center">
38
+ <p class="text-[10px] uppercase font-bold text-red-400">Blood Group</p>
39
+ <p class="text-2xl font-black text-red-600">{{ passport.blood_group or '--' }}</p>
40
+ </div>
41
+ <div class="bg-slate-50 border border-slate-100 p-4 rounded-2xl text-center">
42
+ <p class="text-[10px] uppercase font-bold text-slate-400">Date of Birth</p>
43
+ <p class="text-lg font-bold text-slate-700 mt-1">{{ passport.date_of_birth or '--' }}</p>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="space-y-4">
48
+ <div class="bg-amber-50 border border-amber-100 p-4 rounded-2xl">
49
+ <p class="text-xs uppercase font-bold text-amber-600 mb-1"><i class="fa-solid fa-triangle-exclamation mr-1"></i>Allergies</p>
50
+ <p class="text-sm font-semibold text-slate-700">{{ passport.allergies or 'None listed' }}</p>
51
+ </div>
52
+
53
+ <div class="bg-purple-50 border border-purple-100 p-4 rounded-2xl">
54
+ <p class="text-xs uppercase font-bold text-purple-600 mb-1"><i class="fa-solid fa-heart-pulse mr-1"></i>Chronic Conditions</p>
55
+ <p class="text-sm font-semibold text-slate-700">{{ passport.chronic_conditions or 'None listed' }}</p>
56
+ </div>
57
+
58
+ <div class="bg-blue-50 border border-blue-100 p-4 rounded-2xl">
59
+ <p class="text-xs uppercase font-bold text-blue-600 mb-1"><i class="fa-solid fa-pills mr-1"></i>Current Medicines</p>
60
+ <p class="text-sm font-semibold text-slate-700">{{ passport.current_medicines or 'None listed' }}</p>
61
+ </div>
62
+
63
+ {% if surgeries %}
64
+ <div class="bg-slate-50 border border-slate-100 p-4 rounded-2xl">
65
+ <p class="text-xs uppercase font-bold text-slate-500 mb-2"><i class="fa-solid fa-user-doctor mr-1"></i>Surgical History</p>
66
+ {% for s in surgeries %}
67
+ <p class="text-sm text-slate-700"><span class="font-bold">{{ s.year }}:</span> {{ s.description }}</p>
68
+ {% endfor %}
69
+ </div>
70
+ {% endif %}
71
+
72
+ {% if vaccinations %}
73
+ <div class="bg-emerald-50 border border-emerald-100 p-4 rounded-2xl">
74
+ <p class="text-xs uppercase font-bold text-emerald-600 mb-2"><i class="fa-solid fa-syringe mr-1"></i>Vaccination Record</p>
75
+ {% for v in vaccinations %}
76
+ <p class="text-sm text-slate-700"><span class="font-bold">{{ v.month }} {{ v.year }}:</span> {{ v.vaccine_name }}</p>
77
+ {% endfor %}
78
+ </div>
79
+ {% endif %}
80
+
81
+ <div class="bg-red-600 text-white p-4 rounded-2xl">
82
+ <p class="text-xs uppercase font-bold text-red-200 mb-1"><i class="fa-solid fa-phone mr-1"></i>Emergency Contact</p>
83
+ <p class="text-lg font-bold">{{ passport.emergency_contact_name or '--' }}</p>
84
+ <p class="text-sm text-red-100">{{ passport.emergency_contact_phone or '--' }}</p>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <p class="text-center text-xs text-slate-400 mt-4">
90
+ Generated by VeriMed AI. Educational project -- verify critical decisions with attending medical staff.
91
+ </p>
92
+ {% endif %}
93
+
94
+ </div>
95
+ </body>
96
+ </html>
templates/index.html CHANGED
@@ -12,6 +12,9 @@
12
  <a href="/passport" class="bg-white/15 border border-white/40 text-white font-bold text-sm px-6 py-3 rounded-xl hover:bg-white/25 transition-colors inline-flex items-center gap-2">
13
  <i class="fa-solid fa-id-card-clip"></i> Check Your Health Passport
14
  </a>
 
 
 
15
  </div>
16
  </div>
17
 
 
12
  <a href="/passport" class="bg-white/15 border border-white/40 text-white font-bold text-sm px-6 py-3 rounded-xl hover:bg-white/25 transition-colors inline-flex items-center gap-2">
13
  <i class="fa-solid fa-id-card-clip"></i> Check Your Health Passport
14
  </a>
15
+ <a href="/emergency-help" class="bg-red-600 text-white font-bold text-sm px-6 py-3 rounded-xl shadow-lg hover:bg-red-700 transition-colors inline-flex items-center gap-2">
16
+ <i class="fa-solid fa-truck-medical"></i> Emergency Help
17
+ </a>
18
  </div>
19
  </div>
20
 
templates/login.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <div class="min-h-[70vh] flex items-center justify-center">
4
+ <div class="w-full max-w-md bg-white p-8 rounded-3xl border border-slate-100 shadow-xl shadow-blue-50">
5
+ <div class="text-center mb-6">
6
+ <div class="bg-blue-600 text-white w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-blue-200">
7
+ <i class="fa-solid fa-id-card-clip text-2xl"></i>
8
+ </div>
9
+ <h1 class="text-2xl font-black text-slate-800">Welcome Back</h1>
10
+ <p class="text-sm text-slate-500 mt-1">Log in to access your Health Passport.</p>
11
+ </div>
12
+
13
+ <form method="POST" action="/login" class="space-y-4">
14
+ <div>
15
+ <label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Email</label>
16
+ <input type="email" name="email" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="you@example.com">
17
+ </div>
18
+ <div>
19
+ <label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Password</label>
20
+ <input type="password" name="password" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="••••••••">
21
+ </div>
22
+ <button type="submit" class="w-full bg-blue-600 text-white font-bold py-3 rounded-xl hover:bg-blue-700 transition-colors shadow-lg shadow-blue-200">
23
+ <i class="fa-solid fa-right-to-bracket mr-2"></i>Log In
24
+ </button>
25
+ </form>
26
+
27
+ <p class="text-center text-sm text-slate-500 mt-6">
28
+ Don't have an account? <a href="/register" class="text-blue-600 font-bold hover:underline">Register here</a>
29
+ </p>
30
+ </div>
31
+ </div>
32
+ {% endblock %}
templates/passport.html CHANGED
@@ -1,55 +1,114 @@
1
  {% extends 'base.html' %}
2
  {% block content %}
 
 
 
 
 
3
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
4
- <div class="lg:col-span-7 bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
5
- <h2 class="text-xl font-bold text-slate-800 mb-4"><i class="fa-solid fa-id-card-clip text-emerald-600 mr-2"></i>Emergency Profile Assembly</h2>
6
-
7
- <form id="passportForm" class="grid grid-cols-1 md:grid-cols-2 gap-4">
8
- <div>
9
- <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Full Name</label>
10
- <input type="text" id="passName" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
11
- </div>
12
- <div>
13
- <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Blood Classification Group</label>
14
- <select id="passBlood" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
15
- <option>A+</option><option>O+</option><option>B+</option><option>AB+</option>
16
- <option>A-</option><option>O-</option><option>B-</option><option>AB-</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  </select>
18
- </div>
19
- <div class="md:col-span-2">
20
- <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Critical Medical Allergies</label>
21
- <input type="text" id="passAllergies" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" placeholder="e.g. Penicillin, Sulfa drugs, Nuts">
22
- </div>
23
- <div class="md:col-span-2">
24
- <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Active Prescribed Medications</label>
25
- <textarea id="passMeds" rows="2" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500"></textarea>
26
- </div>
27
- <div>
28
- <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Emergency Point of Contact</label>
29
- <input type="text" id="passContact" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
30
- </div>
31
- <div>
32
- <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Contact Emergency Phone Line</label>
33
- <input type="tel" id="passPhone" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
34
- </div>
35
-
36
- <button type="submit" class="md:col-span-2 mt-2 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 rounded-xl transition-colors text-xs tracking-wider uppercase">
37
- Compile & Encrypt Locally
38
- </button>
39
- </form>
40
  </div>
41
 
42
- <!-- Generated Target Output Summary Display Section -->
43
  <div class="lg:col-span-5 flex flex-col gap-6">
44
- <div class="bg-gradient-to-br from-emerald-800 to-teal-950 text-white p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center text-center gap-4 min-h-[350px]">
45
- <h4 class="font-bold text-sm tracking-wider uppercase text-emerald-300">Generated Verification Engine Target Token</h4>
46
- <!-- QR Verification Placeholder Area -->
47
  <div class="bg-white p-4 rounded-xl shadow-inner inline-block" id="qrContainer">
48
  <i class="fa-solid fa-qrcode text-8xl text-slate-300 p-4"></i>
49
  </div>
50
  <p class="text-[11px] text-emerald-200/80 max-w-xs leading-relaxed">
51
- Scan targets execute cryptographic vector translations parsing medical criteria during intake cycles instantly.
52
  </p>
 
 
 
53
  </div>
54
  </div>
55
  </div>
 
1
  {% extends 'base.html' %}
2
  {% block content %}
3
+ <div class="mb-6">
4
+ <h1 class="text-2xl font-black text-slate-800">Your Health Passport</h1>
5
+ <p class="text-sm text-slate-500 mt-1">Logged in as <span class="font-bold">{{ user.name }}</span> ({{ user.email }})</p>
6
+ </div>
7
+
8
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
9
+
10
+ <!-- LEFT COLUMN: forms -->
11
+ <div class="lg:col-span-7 flex flex-col gap-6">
12
+
13
+ <!-- Personal + Medical Details -->
14
+ <div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
15
+ <h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-id-card-clip text-emerald-600 mr-2"></i>Personal & Medical Details</h2>
16
+ <form id="passportForm" class="grid grid-cols-1 md:grid-cols-2 gap-4">
17
+ <div>
18
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Full Name</label>
19
+ <input type="text" id="passName" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
20
+ </div>
21
+ <div>
22
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Blood Group</label>
23
+ <select id="passBlood" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
24
+ <option>A+</option><option>O+</option><option>B+</option><option>AB+</option>
25
+ <option>A-</option><option>O-</option><option>B-</option><option>AB-</option>
26
+ </select>
27
+ </div>
28
+ <div class="md:col-span-2">
29
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Date of Birth</label>
30
+ <input type="text" id="passDob" placeholder="DD-MM-YYYY" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
31
+ </div>
32
+ <div class="md:col-span-2">
33
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Allergies</label>
34
+ <input type="text" id="passAllergies" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" placeholder="e.g. Penicillin, Sulfa drugs, Nuts">
35
+ </div>
36
+ <div class="md:col-span-2">
37
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Chronic Diseases</label>
38
+ <input type="text" id="passChronic" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" placeholder="e.g. Diabetes, Asthma, Hypertension">
39
+ </div>
40
+ <div class="md:col-span-2">
41
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Current Medicines</label>
42
+ <textarea id="passMeds" rows="2" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500"></textarea>
43
+ </div>
44
+ <div>
45
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Emergency Contact Name</label>
46
+ <input type="text" id="passContact" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
47
+ </div>
48
+ <div>
49
+ <label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1">Emergency Contact Phone</label>
50
+ <input type="tel" id="passPhone" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500">
51
+ </div>
52
+ <button type="submit" class="md:col-span-2 mt-2 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 rounded-xl transition-colors text-xs tracking-wider uppercase">
53
+ Save Passport Details
54
+ </button>
55
+ </form>
56
+ </div>
57
+
58
+ <!-- Surgeries -->
59
+ <div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
60
+ <h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-user-doctor text-blue-600 mr-2"></i>Surgical History</h2>
61
+ <form id="surgeryForm" class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
62
+ <input type="text" id="surgeryYear" placeholder="Year" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-blue-500" required>
63
+ <input type="text" id="surgeryDescription" placeholder="e.g. Appendectomy -- brief description" class="md:col-span-2 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-blue-500" required>
64
+ <button type="submit" class="md:col-span-1 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 rounded-xl text-xs uppercase tracking-wider">Add</button>
65
+ </form>
66
+ <div id="surgeryList" class="flex flex-col gap-2"></div>
67
+ </div>
68
+
69
+ <!-- Vaccinations -->
70
+ <div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
71
+ <h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-syringe text-emerald-600 mr-2"></i>Vaccination Record</h2>
72
+ <form id="vaccinationForm" class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
73
+ <input type="text" id="vaccineName" placeholder="Vaccine name" class="md:col-span-2 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
74
+ <input type="text" id="vaccineMonth" placeholder="Month" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
75
+ <input type="text" id="vaccineYear" placeholder="Year" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-emerald-500" required>
76
+ <button type="submit" class="md:col-span-4 bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-2.5 rounded-xl text-xs uppercase tracking-wider">Add Vaccination</button>
77
+ </form>
78
+ <div id="vaccinationList" class="flex flex-col gap-2"></div>
79
+ </div>
80
+
81
+ <!-- Medical Report Uploads -->
82
+ <div class="bg-white p-6 rounded-2xl border border-slate-200/80 shadow-sm">
83
+ <h2 class="text-lg font-bold text-slate-800 mb-4"><i class="fa-solid fa-file-medical text-purple-600 mr-2"></i>Medical Report Documents</h2>
84
+ <form id="reportForm" class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4" enctype="multipart/form-data">
85
+ <input type="file" id="reportFile" class="md:col-span-4 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500" required>
86
+ <select id="reportCategory" class="md:col-span-2 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500">
87
+ <option>Blood Report</option><option>MRI</option><option>X-Ray</option>
88
+ <option>ECG</option><option>Prescription</option><option>CT Scan</option>
89
+ <option>Discharge Summary</option><option>Other</option>
90
  </select>
91
+ <input type="text" id="reportMonth" placeholder="Month" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500">
92
+ <input type="text" id="reportYear" placeholder="Year" class="md:col-span-1 bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs focus:outline-none focus:border-purple-500">
93
+ <button type="submit" class="md:col-span-4 bg-purple-600 hover:bg-purple-700 text-white font-bold py-2.5 rounded-xl text-xs uppercase tracking-wider">Upload Report</button>
94
+ </form>
95
+ <div id="reportList" class="flex flex-col gap-2"></div>
96
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
98
 
99
+ <!-- RIGHT COLUMN: QR + PDF -->
100
  <div class="lg:col-span-5 flex flex-col gap-6">
101
+ <div class="bg-gradient-to-br from-emerald-800 to-teal-950 text-white p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center text-center gap-4 min-h-[300px] sticky top-24">
102
+ <h4 class="font-bold text-sm tracking-wider uppercase text-emerald-300">Emergency QR Code</h4>
 
103
  <div class="bg-white p-4 rounded-xl shadow-inner inline-block" id="qrContainer">
104
  <i class="fa-solid fa-qrcode text-8xl text-slate-300 p-4"></i>
105
  </div>
106
  <p class="text-[11px] text-emerald-200/80 max-w-xs leading-relaxed">
107
+ Any phone camera can scan this -- opens a read-only emergency summary instantly. No app or login needed for whoever scans it.
108
  </p>
109
+ <a href="/api/passport/pdf" id="downloadPdfBtn" class="w-full bg-white text-emerald-800 font-bold py-3 rounded-xl hover:bg-emerald-50 transition-colors text-xs uppercase tracking-wider">
110
+ <i class="fa-solid fa-file-pdf mr-2"></i>Download Printable PDF
111
+ </a>
112
  </div>
113
  </div>
114
  </div>
templates/register.html ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <div class="min-h-[70vh] flex items-center justify-center">
4
+ <div class="w-full max-w-md bg-white p-8 rounded-3xl border border-slate-100 shadow-xl shadow-blue-50">
5
+ <div class="text-center mb-6">
6
+ <div class="bg-emerald-600 text-white w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-emerald-200">
7
+ <i class="fa-solid fa-user-plus text-2xl"></i>
8
+ </div>
9
+ <h1 class="text-2xl font-black text-slate-800">Create Your Account</h1>
10
+ <p class="text-sm text-slate-500 mt-1">Set up your personal Health Passport in a minute.</p>
11
+ </div>
12
+
13
+ <form method="POST" action="/register" class="space-y-4">
14
+ <div>
15
+ <label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Full Name</label>
16
+ <input type="text" name="name" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="Jane Doe">
17
+ </div>
18
+ <div>
19
+ <label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Email</label>
20
+ <input type="email" name="email" required class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="you@example.com">
21
+ </div>
22
+ <div>
23
+ <label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Password</label>
24
+ <input type="password" name="password" required minlength="8" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="At least 8 characters">
25
+ </div>
26
+ <div>
27
+ <label class="block text-xs uppercase tracking-wider font-bold text-slate-400 mb-2">Confirm Password</label>
28
+ <input type="password" name="confirm_password" required minlength="8" class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-sm focus:outline-none focus:border-blue-500" placeholder="Re-enter password">
29
+ </div>
30
+ <button type="submit" class="w-full bg-emerald-600 text-white font-bold py-3 rounded-xl hover:bg-emerald-700 transition-colors shadow-lg shadow-emerald-200">
31
+ <i class="fa-solid fa-user-plus mr-2"></i>Create Account
32
+ </button>
33
+ </form>
34
+
35
+ <p class="text-center text-sm text-slate-500 mt-6">
36
+ Already have an account? <a href="/login" class="text-blue-600 font-bold hover:underline">Log in here</a>
37
+ </p>
38
+ </div>
39
+ </div>
40
+ {% endblock %}