File size: 45,631 Bytes
8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 0fc0648 fd8f0aa 0fc0648 8e73aa2 fd8f0aa 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 fd8f0aa 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 fd8f0aa 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 0fc0648 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 0fc0648 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa 8e73aa2 fd8f0aa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 | """
utils/db.py β All Supabase + SQLite database operations for FitPlan Pro.
"""
import os, json, time, uuid, sqlite3
import requests
SUPABASE_URL = os.getenv("SUPABASE_URL", "").strip()
SUPABASE_KEY = os.getenv("SUPABASE_KEY", "").strip()
DB_PATH = "/tmp/fitplan_plans.db"
USE_SUPABASE = bool(SUPABASE_URL and SUPABASE_KEY)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SQLite setup
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_sqlite():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS plans (
plan_id TEXT PRIMARY KEY,
username TEXT NOT NULL,
created_at REAL,
dietary_type TEXT,
cuisine_type TEXT DEFAULT '',
total_days INTEGER,
is_active INTEGER DEFAULT 1
)
""")
for _col in ["ALTER TABLE plans ADD COLUMN cuisine_type TEXT DEFAULT ''"]:
try: c.execute(_col)
except Exception: pass
c.execute("""
CREATE TABLE IF NOT EXISTS plan_days (
id TEXT PRIMARY KEY,
plan_id TEXT,
day_number INTEGER,
muscle_group TEXT DEFAULT 'Full Body',
is_rest_day INTEGER DEFAULT 0,
workout_json TEXT,
dietary_json TEXT,
pre_stretch_json TEXT,
post_stretch_json TEXT
)
""")
for _col in [
"ALTER TABLE plan_days ADD COLUMN muscle_group TEXT DEFAULT 'Full Body'",
"ALTER TABLE plan_days ADD COLUMN is_rest_day INTEGER DEFAULT 0",
]:
try: c.execute(_col)
except Exception: pass
c.execute("""
CREATE TABLE IF NOT EXISTS daily_progress (
id TEXT PRIMARY KEY,
username TEXT,
plan_id TEXT,
day_number INTEGER,
date TEXT,
workout_checks TEXT,
dietary_checks TEXT,
day_completed INTEGER DEFAULT 0,
completed_at REAL
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS streaks (
username TEXT PRIMARY KEY,
current_streak INTEGER DEFAULT 0,
longest_streak INTEGER DEFAULT 0,
last_completed_date TEXT,
streak_history TEXT DEFAULT '[]',
opted_in INTEGER DEFAULT 0,
display_name TEXT DEFAULT '',
goal TEXT DEFAULT ''
)
""")
for _col in [
"ALTER TABLE streaks ADD COLUMN opted_in INTEGER DEFAULT 0",
"ALTER TABLE streaks ADD COLUMN display_name TEXT DEFAULT ''",
"ALTER TABLE streaks ADD COLUMN goal TEXT DEFAULT ''",
]:
try: c.execute(_col)
except Exception: pass
for _idx in [
"CREATE INDEX IF NOT EXISTS idx_plans_username ON plans(username)",
"CREATE INDEX IF NOT EXISTS idx_plans_active ON plans(username,is_active)",
"CREATE INDEX IF NOT EXISTS idx_plan_days_plan_id ON plan_days(plan_id)",
"CREATE INDEX IF NOT EXISTS idx_daily_progress_user_plan ON daily_progress(username,plan_id)",
"CREATE INDEX IF NOT EXISTS idx_streaks_username ON streaks(username)",
]:
try: c.execute(_idx)
except Exception: pass
conn.commit()
conn.close()
_init_sqlite()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Helpers
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _headers():
return {
"apikey": SUPABASE_KEY,
"Authorization": f"Bearer {SUPABASE_KEY}",
"Content-Type": "application/json",
"Prefer": "return=representation"
}
def _sb_get(table, filters=""):
r = requests.get(
f"{SUPABASE_URL}/rest/v1/{table}?{filters}",
headers=_headers(), timeout=10
)
return r.json() if r.ok else []
def _sb_post(table, data, upsert=False):
h = _headers()
if upsert:
h["Prefer"] = "resolution=merge-duplicates,return=representation"
data = {k: (1 if v is True else (0 if v is False else v)) for k, v in data.items()}
r = requests.post(
f"{SUPABASE_URL}/rest/v1/{table}",
headers=h, json=data, timeout=10
)
return r.ok
def _sb_patch(table, filters, data):
data = {k: (1 if v is True else (0 if v is False else v)) for k, v in data.items()}
r = requests.patch(
f"{SUPABASE_URL}/rest/v1/{table}?{filters}",
headers=_headers(), json=data, timeout=10
)
return r.ok
def _sb_delete(table, filters):
r = requests.delete(
f"{SUPABASE_URL}/rest/v1/{table}?{filters}",
headers=_headers(), timeout=10
)
return r.ok
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PLAN OPERATIONS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def save_plan(username, dietary_type, total_days, days_data, cuisine_type=""):
plan_id = str(uuid.uuid4())
if USE_SUPABASE:
_sb_patch("plans", f"username=eq.{username}&is_active=eq.1", {"is_active": 0})
ok = _sb_post("plans", {
"plan_id": plan_id, "username": username, "created_at": time.time(),
"dietary_type": dietary_type, "cuisine_type": cuisine_type,
"total_days": total_days, "is_active": 1
})
if not ok:
raise RuntimeError("Failed to save plan header to Supabase")
for day_num, day in enumerate(days_data, 1):
_sb_post("plan_days", {
"id": str(uuid.uuid4()), "plan_id": plan_id, "day_number": day_num,
"muscle_group": day.get("muscle_group", "Full Body"),
"is_rest_day": int(day.get("is_rest_day", False)),
"workout_json": json.dumps(day.get("workout", [])),
"dietary_json": json.dumps(day.get("dietary", {})),
"pre_stretch_json": json.dumps(day.get("pre_stretch", [])),
"post_stretch_json": json.dumps(day.get("post_stretch", []))
})
else:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE plans SET is_active=0 WHERE username=?", (username,))
c.execute("INSERT INTO plans VALUES (?,?,?,?,?,?,?)",
(plan_id, username, time.time(), dietary_type, cuisine_type, total_days, 1))
for day_num, day in enumerate(days_data, 1):
c.execute("INSERT INTO plan_days VALUES (?,?,?,?,?,?,?,?,?)", (
str(uuid.uuid4()), plan_id, day_num,
day.get("muscle_group", "Full Body"),
int(day.get("is_rest_day", False)),
json.dumps(day.get("workout", [])),
json.dumps(day.get("dietary", {})),
json.dumps(day.get("pre_stretch", [])),
json.dumps(day.get("post_stretch", []))
))
conn.commit()
conn.close()
return plan_id
def get_active_plan(username):
if USE_SUPABASE:
plans = _sb_get("plans",
f"username=eq.{username}&is_active=eq.1&order=created_at.desc&limit=1")
if not plans:
return None
plan = plans[0]
plan["days"] = _sb_get("plan_days",
f"plan_id=eq.{plan['plan_id']}&order=day_number.asc")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM plans WHERE username=? AND is_active=1 ORDER BY created_at DESC LIMIT 1",
(username,))
row = c.fetchone()
if not row:
conn.close()
return None
plan = dict(row)
c.execute("SELECT * FROM plan_days WHERE plan_id=? ORDER BY day_number ASC", (plan["plan_id"],))
plan["days"] = [dict(r) for r in c.fetchall()]
conn.close()
import datetime
try:
ts = plan.get("created_at", 0)
plan["created_at_date"] = (
datetime.datetime.fromtimestamp(ts).date().isoformat()
if ts else datetime.date.today().isoformat()
)
except Exception:
plan["created_at_date"] = datetime.date.today().isoformat()
for day in plan.get("days", []):
for field in ["workout_json", "dietary_json", "pre_stretch_json", "post_stretch_json"]:
if isinstance(day.get(field), str):
try:
day[field] = json.loads(day[field])
except Exception:
day[field] = [] if field != "dietary_json" else {}
return plan
def delete_active_plan(username):
if USE_SUPABASE:
_sb_patch("plans", f"username=eq.{username}&is_active=eq.1", {"is_active": 0})
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("UPDATE plans SET is_active=0 WHERE username=?", (username,))
conn.commit()
conn.close()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PROGRESS OPERATIONS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def save_progress(username, plan_id, day_number, workout_checks, dietary_checks):
from datetime import date as _date
today = str(_date.today())
all_workout = all(workout_checks.values()) if workout_checks else False
all_dietary = all(dietary_checks.values()) if dietary_checks else False
day_completed = all_workout and all_dietary
row_id = f"{username}_{plan_id}_{day_number}"
payload = {
"id": row_id, "username": username, "plan_id": plan_id,
"day_number": day_number, "date": today,
"workout_checks": json.dumps(workout_checks),
"dietary_checks": json.dumps(dietary_checks),
"day_completed": int(day_completed),
"completed_at": time.time() if day_completed else None
}
if USE_SUPABASE:
_sb_post("daily_progress", payload, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT OR REPLACE INTO daily_progress VALUES (?,?,?,?,?,?,?,?,?)", (
row_id, username, plan_id, day_number, today,
json.dumps(workout_checks), json.dumps(dietary_checks),
int(day_completed), time.time() if day_completed else None
))
conn.commit()
conn.close()
return day_completed
def get_progress(username, plan_id, day_number):
if USE_SUPABASE:
rows = _sb_get("daily_progress",
f"username=eq.{username}&plan_id=eq.{plan_id}&day_number=eq.{day_number}&limit=1")
row = rows[0] if rows else None
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM daily_progress WHERE username=? AND plan_id=? AND day_number=?",
(username, plan_id, day_number))
r = c.fetchone()
row = dict(r) if r else None
conn.close()
if not row:
return {"workout_checks": {}, "dietary_checks": {}, "day_completed": False}
return {
"workout_checks": json.loads(row.get("workout_checks") or "{}"),
"dietary_checks": json.loads(row.get("dietary_checks") or "{}"),
"day_completed": bool(row.get("day_completed", False))
}
def get_all_progress(username, plan_id):
if USE_SUPABASE:
rows = _sb_get("daily_progress",
f"username=eq.{username}&plan_id=eq.{plan_id}&order=day_number.asc")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM daily_progress WHERE username=? AND plan_id=? ORDER BY day_number ASC",
(username, plan_id))
rows = [dict(r) for r in c.fetchall()]
conn.close()
return [{
"day_number": r["day_number"],
"workout_checks": json.loads(r.get("workout_checks") or "{}"),
"dietary_checks": json.loads(r.get("dietary_checks") or "{}"),
"day_completed": bool(r.get("day_completed", False)),
"date": r.get("date", "")
} for r in rows]
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# STREAK OPERATIONS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_streak(username):
if USE_SUPABASE:
rows = _sb_get("streaks", f"username=eq.{username}&limit=1")
row = rows[0] if rows else None
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM streaks WHERE username=?", (username,))
r = c.fetchone()
row = dict(r) if r else None
conn.close()
if not row:
return {"username": username, "current_streak": 0, "longest_streak": 0,
"last_completed_date": None, "streak_history": []}
return {
"username": row["username"],
"current_streak": row.get("current_streak", 0),
"longest_streak": row.get("longest_streak", 0),
"last_completed_date": row.get("last_completed_date"),
"streak_history": json.loads(row.get("streak_history") or "[]")
}
def save_streak(streak_data):
payload = {
"username": streak_data["username"],
"current_streak": streak_data.get("current_streak", 0),
"longest_streak": streak_data.get("longest_streak", 0),
"last_completed_date": streak_data.get("last_completed_date"),
"streak_history": json.dumps(streak_data.get("streak_history", [])),
"opted_in": int(streak_data.get("opted_in", False)),
"display_name": streak_data.get("display_name", ""),
"goal": streak_data.get("goal", ""),
}
if USE_SUPABASE:
_sb_post("streaks", payload, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("""
INSERT OR REPLACE INTO streaks
(username,current_streak,longest_streak,last_completed_date,
streak_history,opted_in,display_name,goal)
VALUES (?,?,?,?,?,?,?,?)
""", (
payload["username"], payload["current_streak"], payload["longest_streak"],
payload["last_completed_date"], payload["streak_history"],
payload["opted_in"], payload["display_name"], payload["goal"]
))
conn.commit()
conn.close()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# USER PROFILE OPERATIONS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_profile_table():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS user_profiles (
username TEXT PRIMARY KEY,
name TEXT,
age INTEGER,
gender TEXT,
height REAL,
weight REAL,
fitness_level TEXT,
goal TEXT,
days_per_week INTEGER,
months INTEGER,
total_days INTEGER,
equipment TEXT DEFAULT '[]',
home_eq TEXT DEFAULT '[]',
gym_eq TEXT DEFAULT '[]',
no_eq INTEGER DEFAULT 0,
injuries TEXT DEFAULT '[]',
diet_type TEXT DEFAULT 'nonveg',
cuisine_preference TEXT DEFAULT '',
cuisine_label TEXT DEFAULT '',
cuisine_icon TEXT DEFAULT '',
cuisine_notes TEXT DEFAULT '',
updated_at REAL
)
""")
for _col in [
"ALTER TABLE user_profiles ADD COLUMN injuries TEXT DEFAULT '[]'",
"ALTER TABLE user_profiles ADD COLUMN diet_type TEXT DEFAULT 'nonveg'",
"ALTER TABLE user_profiles ADD COLUMN cuisine_preference TEXT DEFAULT ''",
"ALTER TABLE user_profiles ADD COLUMN cuisine_label TEXT DEFAULT ''",
"ALTER TABLE user_profiles ADD COLUMN cuisine_icon TEXT DEFAULT ''",
"ALTER TABLE user_profiles ADD COLUMN cuisine_notes TEXT DEFAULT ''",
]:
try: conn.execute(_col)
except Exception: pass
conn.commit()
conn.close()
_init_profile_table()
def save_user_profile(username, profile_data):
payload = {
"username": username, "name": profile_data.get("name", ""),
"age": profile_data.get("age", 25), "gender": profile_data.get("gender", "Male"),
"height": profile_data.get("height", 170), "weight": profile_data.get("weight", 70),
"fitness_level": profile_data.get("level", "Beginner"),
"goal": profile_data.get("goal", "General Fitness"),
"days_per_week": profile_data.get("days_per_week", 5),
"months": profile_data.get("months", 1),
"total_days": profile_data.get("total_days", 20),
"equipment": json.dumps(profile_data.get("equipment", [])),
"home_eq": json.dumps(profile_data.get("home_eq", [])),
"gym_eq": json.dumps(profile_data.get("gym_eq", [])),
"no_eq": int(profile_data.get("no_eq", False)),
"injuries": json.dumps(profile_data.get("injuries", [])),
"diet_type": profile_data.get("diet_type", "nonveg"),
"cuisine_preference": profile_data.get("cuisine_preference", ""),
"cuisine_label": profile_data.get("cuisine_label", ""),
"cuisine_icon": profile_data.get("cuisine_icon", ""),
"cuisine_notes": profile_data.get("cuisine_notes", ""),
"updated_at": time.time(),
}
if USE_SUPABASE:
_sb_post("user_profiles", payload, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("""
INSERT OR REPLACE INTO user_profiles
(username,name,age,gender,height,weight,fitness_level,goal,
days_per_week,months,total_days,equipment,home_eq,gym_eq,no_eq,
injuries,diet_type,cuisine_preference,cuisine_label,cuisine_icon,
cuisine_notes,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (
payload["username"], payload["name"], payload["age"],
payload["gender"], payload["height"], payload["weight"],
payload["fitness_level"], payload["goal"],
payload["days_per_week"], payload["months"], payload["total_days"],
payload["equipment"], payload["home_eq"], payload["gym_eq"],
payload["no_eq"], payload["injuries"], payload["diet_type"],
payload["cuisine_preference"], payload["cuisine_label"],
payload["cuisine_icon"], payload["cuisine_notes"], payload["updated_at"]
))
conn.commit()
conn.close()
def get_user_profile(username):
if USE_SUPABASE:
import urllib.request, urllib.parse
headers = {
"apikey": SUPABASE_KEY,
"Authorization": f"Bearer {SUPABASE_KEY}",
"Content-Type": "application/json",
}
url = (f"{SUPABASE_URL}/rest/v1/user_profiles"
f"?username=eq.{urllib.parse.quote(username)}&select=*&limit=1")
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=8) as r:
rows = json.loads(r.read().decode())
row = rows[0] if rows else None
except Exception:
row = None
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM user_profiles WHERE username=?", (username,))
r = c.fetchone()
row = dict(r) if r else None
conn.close()
if not row:
return None
return {
"name": row.get("name", ""), "age": row.get("age", 25),
"gender": row.get("gender", "Male"), "height": row.get("height", 170),
"weight": row.get("weight", 70), "level": row.get("fitness_level", "Beginner"),
"goal": row.get("goal", "General Fitness"),
"days_per_week": row.get("days_per_week", 5), "months": row.get("months", 1),
"total_days": row.get("total_days", 20),
"equipment": json.loads(row.get("equipment") or "[]"),
"home_eq": json.loads(row.get("home_eq") or "[]"),
"gym_eq": json.loads(row.get("gym_eq") or "[]"),
"no_eq": bool(row.get("no_eq", False)),
"injuries": json.loads(row.get("injuries") or "[]"),
"diet_type": row.get("diet_type", "nonveg"),
"cuisine_preference": row.get("cuisine_preference", ""),
"cuisine_label": row.get("cuisine_label", ""),
"cuisine_icon": row.get("cuisine_icon", ""),
"cuisine_notes": row.get("cuisine_notes", ""),
}
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# WEIGHT LOG
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_weight_log():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS weight_log (
id TEXT PRIMARY KEY, username TEXT NOT NULL,
date TEXT NOT NULL, weight_kg REAL NOT NULL
)
""")
conn.commit(); conn.close()
_init_weight_log()
def save_weight_log(username, date_str, weight_kg):
row_id = f"{username}_{date_str}"
if USE_SUPABASE:
_sb_post("weight_log", {"id": row_id, "username": username,
"date": date_str, "weight_kg": weight_kg}, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT OR REPLACE INTO weight_log VALUES (?,?,?,?)",
(row_id, username, date_str, weight_kg))
conn.commit(); conn.close()
def get_weight_log(username, limit=60):
if USE_SUPABASE:
rows = _sb_get("weight_log", f"username=eq.{username}&order=date.asc&limit={limit}")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = [dict(r) for r in conn.execute(
"SELECT * FROM weight_log WHERE username=? ORDER BY date ASC LIMIT ?",
(username, limit)).fetchall()]
conn.close()
return [{"date": r["date"], "weight_kg": r["weight_kg"]} for r in rows]
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# WORKOUT NOTES
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_workout_notes():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS workout_notes (
id TEXT PRIMARY KEY, username TEXT NOT NULL,
plan_id TEXT NOT NULL, day_number INTEGER NOT NULL,
exercise_idx INTEGER NOT NULL, note TEXT DEFAULT ''
)
""")
conn.commit(); conn.close()
_init_workout_notes()
def save_workout_note(username, plan_id, day_number, exercise_idx, note):
row_id = f"{username}_{plan_id}_{day_number}_{exercise_idx}"
if USE_SUPABASE:
_sb_post("workout_notes", {"id": row_id, "username": username, "plan_id": plan_id,
"day_number": day_number, "exercise_idx": exercise_idx,
"note": note}, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT OR REPLACE INTO workout_notes VALUES (?,?,?,?,?,?)",
(row_id, username, plan_id, day_number, exercise_idx, note))
conn.commit(); conn.close()
def get_workout_notes(username, plan_id, day_number):
if USE_SUPABASE:
rows = _sb_get("workout_notes",
f"username=eq.{username}&plan_id=eq.{plan_id}&day_number=eq.{day_number}")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = [dict(r) for r in conn.execute(
"SELECT * FROM workout_notes WHERE username=? AND plan_id=? AND day_number=?",
(username, plan_id, day_number)).fetchall()]
conn.close()
return {r["exercise_idx"]: r["note"] for r in rows if r.get("note")}
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# WATER TRACKER
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_water_tracker():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS water_tracker (
id TEXT PRIMARY KEY, username TEXT NOT NULL,
date TEXT NOT NULL, glasses INTEGER DEFAULT 0
)
""")
conn.commit(); conn.close()
_init_water_tracker()
def save_water(username, date_str, glasses):
row_id = f"{username}_{date_str}"
if USE_SUPABASE:
_sb_post("water_tracker", {"id": row_id, "username": username,
"date": date_str, "glasses": glasses}, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT OR REPLACE INTO water_tracker VALUES (?,?,?,?)",
(row_id, username, date_str, glasses))
conn.commit(); conn.close()
def get_water(username, date_str):
if USE_SUPABASE:
rows = _sb_get("water_tracker", f"username=eq.{username}&date=eq.{date_str}&limit=1")
return rows[0]["glasses"] if rows else 0
else:
conn = sqlite3.connect(DB_PATH)
row = conn.execute("SELECT glasses FROM water_tracker WHERE username=? AND date=?",
(username, date_str)).fetchone()
conn.close()
return row[0] if row else 0
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PERSONAL RECORDS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_personal_records():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS personal_records (
id TEXT PRIMARY KEY, username TEXT NOT NULL,
exercise TEXT NOT NULL, value REAL NOT NULL,
unit TEXT DEFAULT 'reps', date TEXT NOT NULL, note TEXT DEFAULT ''
)
""")
conn.commit(); conn.close()
_init_personal_records()
def save_personal_record(username, exercise, value, unit, date_str, note=""):
row_id = str(uuid.uuid4())
payload = {"id": row_id, "username": username, "exercise": exercise,
"value": value, "unit": unit, "date": date_str, "note": note}
if USE_SUPABASE:
_sb_post("personal_records", payload)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT INTO personal_records VALUES (?,?,?,?,?,?,?)",
(row_id, username, exercise, value, unit, date_str, note))
conn.commit(); conn.close()
def get_personal_records(username):
if USE_SUPABASE:
rows = _sb_get("personal_records", f"username=eq.{username}&order=date.asc")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = [dict(r) for r in conn.execute(
"SELECT * FROM personal_records WHERE username=? ORDER BY date ASC",
(username,)).fetchall()]
conn.close()
return rows
def delete_personal_record(record_id):
if USE_SUPABASE:
_sb_delete("personal_records", f"id=eq.{record_id}")
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("DELETE FROM personal_records WHERE id=?", (record_id,))
conn.commit(); conn.close()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# BODY MEASUREMENTS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_measurements():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS body_measurements (
id TEXT PRIMARY KEY, username TEXT NOT NULL,
date TEXT NOT NULL, chest REAL, waist REAL,
hips REAL, arms REAL, thighs REAL
)
""")
conn.commit(); conn.close()
_init_measurements()
def save_measurements(username, date_str, chest, waist, hips, arms, thighs):
row_id = f"{username}_{date_str}"
payload = {"id": row_id, "username": username, "date": date_str,
"chest": chest, "waist": waist, "hips": hips, "arms": arms, "thighs": thighs}
if USE_SUPABASE:
_sb_post("body_measurements", payload, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT OR REPLACE INTO body_measurements VALUES (?,?,?,?,?,?,?,?)",
(row_id, username, date_str, chest, waist, hips, arms, thighs))
conn.commit(); conn.close()
def get_measurements(username, limit=20):
if USE_SUPABASE:
rows = _sb_get("body_measurements",
f"username=eq.{username}&order=date.asc&limit={limit}")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = [dict(r) for r in conn.execute(
"SELECT * FROM body_measurements WHERE username=? ORDER BY date ASC LIMIT ?",
(username, limit)).fetchall()]
conn.close()
return rows
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# CHAT HISTORY
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_chat():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS chat_history (
id TEXT PRIMARY KEY, username TEXT NOT NULL,
role TEXT NOT NULL, content TEXT NOT NULL, created_at REAL
)
""")
conn.commit(); conn.close()
_init_chat()
def save_chat_message(username, role, content):
row_id = str(uuid.uuid4())
if USE_SUPABASE:
_sb_post("chat_history", {"id": row_id, "username": username,
"role": role, "content": content, "created_at": time.time()})
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT INTO chat_history VALUES (?,?,?,?,?)",
(row_id, username, role, content, time.time()))
conn.commit(); conn.close()
def get_chat_messages_raw(username, limit=50):
if USE_SUPABASE:
rows = _sb_get("chat_history",
f"username=eq.{username}&order=created_at.asc&limit={limit}")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = [dict(r) for r in conn.execute(
"SELECT * FROM chat_history WHERE username=? ORDER BY created_at ASC LIMIT ?",
(username, limit)).fetchall()]
conn.close()
return [{"role": r["role"], "content": r["content"]} for r in rows]
def clear_chat_history(username):
if USE_SUPABASE:
_sb_delete("chat_history", f"username=eq.{username}")
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("DELETE FROM chat_history WHERE username=?", (username,))
conn.commit(); conn.close()
def save_chat_history(username, messages):
clear_chat_history(username)
for msg in messages:
save_chat_message(username, msg.get("role", "user"), msg.get("content", ""))
def get_chat_history(username, limit=20):
return get_chat_messages_raw(username, limit=limit)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# USER SETTINGS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_user_settings():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS user_settings (
username TEXT NOT NULL, setting_key TEXT NOT NULL,
setting_value TEXT, updated_at REAL,
PRIMARY KEY (username, setting_key)
)
""")
conn.commit(); conn.close()
_init_user_settings()
def save_user_setting(username, key, value):
if USE_SUPABASE:
_sb_post("user_settings", {"username": username, "setting_key": key,
"setting_value": str(value), "updated_at": time.time()},
upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT OR REPLACE INTO user_settings VALUES (?,?,?,?)",
(username, key, str(value), time.time()))
conn.commit(); conn.close()
def get_user_setting(username, key, default=None):
if USE_SUPABASE:
rows = _sb_get("user_settings",
f"username=eq.{username}&setting_key=eq.{key}&limit=1")
if rows: return rows[0].get("setting_value", default)
else:
conn = sqlite3.connect(DB_PATH)
row = conn.execute(
"SELECT setting_value FROM user_settings WHERE username=? AND setting_key=?",
(username, key)).fetchone()
conn.close()
if row: return row[0]
return default
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PROGRESS PHOTOS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_progress_photos():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS progress_photos (
id TEXT PRIMARY KEY, username TEXT NOT NULL,
photo_date TEXT, label TEXT, b64 TEXT, mime TEXT, created_at REAL
)
""")
conn.commit(); conn.close()
_init_progress_photos()
def save_progress_photo(username, photo_dict):
row_id = str(uuid.uuid4())
if USE_SUPABASE:
_sb_post("progress_photos", {"id": row_id, "username": username,
"photo_date": photo_dict.get("date", ""),
"label": photo_dict.get("label", ""),
"b64": photo_dict.get("b64", ""),
"mime": photo_dict.get("mime", "image/jpeg"),
"created_at": time.time()})
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("INSERT INTO progress_photos VALUES (?,?,?,?,?,?,?)",
(row_id, username, photo_dict.get("date", ""), photo_dict.get("label", ""),
photo_dict.get("b64", ""), photo_dict.get("mime", "image/jpeg"), time.time()))
conn.commit(); conn.close()
def get_progress_photos(username, limit=50):
if USE_SUPABASE:
rows = _sb_get("progress_photos",
f"username=eq.{username}&order=created_at.asc&limit={limit}")
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = [dict(r) for r in conn.execute(
"SELECT * FROM progress_photos WHERE username=? ORDER BY created_at ASC LIMIT ?",
(username, limit)).fetchall()]
conn.close()
return [{"date": r.get("photo_date", ""), "label": r.get("label", ""),
"b64": r.get("b64", ""), "mime": r.get("mime", "image/jpeg")} for r in rows]
def delete_progress_photo(username, photo_date, label):
if USE_SUPABASE:
_sb_delete("progress_photos",
f"username=eq.{username}&photo_date=eq.{photo_date}&label=eq.{label}")
else:
conn = sqlite3.connect(DB_PATH)
conn.execute("DELETE FROM progress_photos WHERE username=? AND photo_date=? AND label=?",
(username, photo_date, label))
conn.commit(); conn.close()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# WORKOUT HISTORY (persists across plan deletions / regeneration)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _init_workout_history():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS workout_history (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
date TEXT NOT NULL,
day_number INTEGER DEFAULT 0,
muscle_group TEXT DEFAULT 'Workout',
status TEXT DEFAULT 'done',
exercises INTEGER DEFAULT 0,
is_rest INTEGER DEFAULT 0,
UNIQUE (username, date)
)
""")
conn.commit()
conn.close()
_init_workout_history()
def save_workout_history(username, history_entries):
"""Save completed/skipped sessions permanently. Survives plan deletion."""
if USE_SUPABASE:
for entry in history_entries:
_sb_post("workout_history", {
"id": f"{username}_{entry.get('date_str', str(entry.get('date', '')))}",
"username": username,
"date": entry.get("date_str", str(entry.get("date", ""))),
"day_number": entry.get("day", 0),
"muscle_group": entry.get("muscle", "Workout"),
"status": entry.get("status", "done"),
"exercises": entry.get("exercises", 0),
"is_rest": 1 if entry.get("is_rest") else 0,
}, upsert=True)
else:
conn = sqlite3.connect(DB_PATH)
for entry in history_entries:
conn.execute("""
INSERT OR REPLACE INTO workout_history
(id, username, date, day_number, muscle_group, status, exercises, is_rest)
VALUES (?,?,?,?,?,?,?,?)
""", (
f"{username}_{entry.get('date_str', str(entry.get('date', '')))}",
username,
entry.get("date_str", str(entry.get("date", ""))),
entry.get("day", 0),
entry.get("muscle", "Workout"),
entry.get("status", "done"),
entry.get("exercises", 0),
1 if entry.get("is_rest") else 0,
))
conn.commit()
conn.close()
def get_workout_history(username):
"""Load all saved workout history, independent of current active plan."""
if USE_SUPABASE:
rows = _sb_get("workout_history",
f"username=eq.{username}&order=date.desc&limit=500")
return [{
"date": r.get("date", ""),
"day": r.get("day_number", 0),
"muscle_group": r.get("muscle_group", "Workout"),
"status": r.get("status", "done"),
"exercises": r.get("exercises", 0),
"is_rest": bool(r.get("is_rest", 0)),
} for r in rows] if rows else []
else:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = [dict(r) for r in conn.execute(
"SELECT * FROM workout_history WHERE username=? ORDER BY date DESC LIMIT 500",
(username,)).fetchall()]
conn.close()
return [{
"date": r["date"],
"day": r["day_number"],
"muscle_group": r["muscle_group"],
"status": r["status"],
"exercises": r["exercises"],
"is_rest": bool(r["is_rest"]),
} for r in rows]
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# COMPATIBILITY ALIASES
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_water_intake(username, date_str):
return get_water(username, date_str)
def save_water_intake(username, date_str, glasses):
return save_water(username, date_str, glasses)
def save_personal_records(username, records_dict):
from datetime import date as _d
today = _d.today().isoformat()
for ex_name, entries in records_dict.items():
if entries:
e = entries[-1]
save_personal_record(username, ex_name, e.get("value", 0),
e.get("unit", "reps"), e.get("date", today), e.get("note", ""))
def get_body_measurements(username, limit=20):
return get_measurements(username, limit)
def save_body_measurements(username, meas_list):
from datetime import date as _d
today = _d.today().isoformat()
if meas_list:
entry = meas_list[-1]
m = entry.get("measurements", {})
save_measurements(username, entry.get("date", today),
m.get("Chest (cm)", 0), m.get("Waist (cm)", 0),
m.get("Hips (cm)", 0), m.get("Left Arm (cm)", 0),
m.get("Left Thigh (cm)", 0)) |