אפליקציית כושר חכמה: מבנה נתונים, סכימת מסד נתונים ומפרט עיצוב שרת
Artifactזהו פריט המידע שמשמש בפרק 2: תכנון מפרט המערכת של האפליקציה (בלי צורך בקוד). אתם יכולים להשתמש במפרט הזה כחומר עזר לבניית אוספים ב-Cloud Firestore, נקודות קצה (endpoints) של שרתים וזיכרון של נציג בשלוש רמות.
כדי לגשת לפריט המידע הזה, צריך להיכנס לחשבון
כדי לראות ולהעתיק את קובץ ה-artifact המלא שמשמש בסדרת הסרטונים Build It!, צריך להיכנס לפרופיל המפתח ב-Google.
נדרש פרופיל מפַתח
אין לך אפשרות ליצור פרופיל מפתח. כדי להפעיל את התכונה הזו, צריך לפנות לאדמין ב-Workspace.
יצירת פרופיל מפתח
כדי לקבל גישה לכל ההנחיות והתוצרים של Build It!, צריך ליצור פרופיל מפתח ב-Google בחינם.
מצטרפים ל-Builder Central ומקבלים גישה
בלחיצה אחת אתם יכולים לקבל את תג מועדון החברים בחינם ב-Builder, ולקבל גישה לכל ההנחיות ופריטי המידע של Build It!.
סטטוס: בתהליך (ניסוח באמצעות Planning Companion)
מסד נתונים לטירגוט: Google Cloud Firestore
יעד הארכיטקטורה: בנייה מחדש של אב הטיפוס של הכושר החכם ב-AI Studio, כך שיהיה נקי, קל לתחזוקה ומוכן לייצור.
0. החלטות לגבי ארכיטקטורת המערכת והמחסנית הטכנית הבסיסית
כדי להבטיח יציבות, ביצועים וגבולות ברורים בסביבת הייצור, קו הבסיס הארכיטקטוני הבא נעול לכל ההטמעות:
- Frontend Framework: Next.js (נתב אפליקציות, React 19) עם אחסון נכסים לא מקוונים במטמון של Progressive Web App (PWA) (באמצעות Serwist /
@ducanh2912/next-pwa). - עיצוב ורכיבי ממשק משתמש: Tailwind CSS גרסה 4 + Tailwind Variants (לניהול וריאנטים) + Lucide React (סמלים).
- ארכיטקטורת מצב הלקוח:
- Zustand (עם תוכנת ביניים
persist): מטפל במצב הלקוח המקומי, המוחשי והזמני (טיימרים של שעון עצר, מאגרי נתונים זמניים של סטים פעילים, תרשים שלבים וקלט לא שמור של רצפת חדר הכושר). - TanStack Query (React Query): מטפל בכל מצבי השרת האסינכרוניים, בשמירת מסמכי Firestore במטמון, באימות מחדש ברקע, בביטול כפילויות של בקשות ובמוטציות אופטימיות.
- Zustand (עם תוכנת ביניים
- סביבת זמן ריצה של קצה עורפי ואירוח API: פונקציות טיפול בניתוב של Next.js (
app/api/...) שמתארחות ב-Google Cloud Run באותו אזור כמו Firestore ושמירת הקשר במטמון של Gemini, כדי להשיג זמן אחזור פנימי של פחות מ-15 אלפיות השנייה. - אימות: אימות ב-Firebase (כניסה באמצעות חשבון Google, כתובת אימייל/סיסמה וקישור אנונימי של משתמשים חדשים). הלקוח מצרף את
Authorization: Bearer <Firebase_ID_Token>לכל בקשות ה-API, והשרת מאמת את הזהות באמצעותfirebase-admin.auth().verifyIdToken(token). - מסד נתונים ראשי: Google Cloud Firestore.
1. סקירה כללית של המערכת והתהליך העיקרי שעובר המשתמש
1.1 היקף ועקרונות ארכיטקטורה
המפרט הזה מגדיר את מודלי הנתונים, הסכימות של Cloud Firestore, החוזים של נקודות הקצה בשרת ומבני הזיכרון של סוכן ה-AI באפליקציית Smart Fitness. הוא יוצר מקור אמת יחיד (SSOT) לכל תכנון האימונים, רישום היסטוריית הסשנים וזיכרון האימון המדורג האוטונומי, ומנותק לחלוטין ממסגרות התצוגה של ממשק המשתמש (שמטופלות במפרט ייעודי של עיצוב ממשק משתמש/חוויית משתמש).
1.2 עקרונות הליבה של הנתונים
- הפרדת תחומי האחריות: גבולות ברורים בין חוזי הנתונים (קצה עורפי/מסד נתונים) לבין עיצוב ממשק המשתמש של הלקוח.
- ארכיטקטורת ספרייה היברידית: אנציקלופדיית תרגילים סטטית ובלתי ניתנת לשינוי (
exercises) בשילוב עם קומפוזיציה דינמית של AI של לוחות זמנים שבועיים (weekly_plans). - שינויים בשלבים עם האדם שבתהליך: שינויים באימונים שנוצרו על ידי AI מוחזרים כהצעות בשלבים. כתיבה במסד הנתונים מתרחשת רק לאחר שהמשתמש מאשר זאת באופן מפורש.
- זיכרון חי רציף ב-3 רמות: ה-AI מתעד, מעדכן ומנקה באופן אוטונומי עובדות על המשתמשים, כולל עובדות קבועות, מצבים פיזיולוגיים שפג תוקפם (עם TTL מפורש) והקשרים מצביים זמניים.
- קריאות מפורטות של מסמך יחיד: הנתונים מאורגנים במסמכים שעברו אופטימיזציה (כמו מחברות ספירלה של תוכניות שבועיות וקבוצות שבועיות של צ'אטים) כדי לצמצם את זמן האחזור של הקריאה ולמנוע שאילתות מיותרות.
2. נקודות קצה של שרתים וחוזי API
כל נקודת קצה (endpoint) חייבת לציין את מקור התגובה (
AGENT,DATABASEאוHYBRID) כדי שיהיה אפשר להגדיר כראוי נקודות קצה שמגובות על ידי נציג במהלך ההטמעה.
POST /api/coach/chat
- מטרה: אינטראקציה בממשק שיחה עם המאמן האישי מבוסס-AI, טיפול בשאלות אימון, תמיכה במוטיבציה ויצירת הצעות לשינויים באימון (התאמות יומיות ושינויים שבועיים).
- טריגר: המשתמש שולח הודעה בכרטיסייה "מאמן" או לוחץ על צ'יפ הצעות תלויות הקשר לעריכה (למשל, "החלפת לחיצת ארנולד בלחיצת כתפיים עם משקולות יד").
- מקור התשובה:
AGENT— משתמש ב-Gemini עם יכולות סטרימינג וקריאה להפעלת פונקציות או כלים מובנות. לא משנה ישירות את מסד הנתונים, אלא מחזירה הצעה בשלבים לבדיקה של האדם שבתהליך. - כותרות:
Authorization: Bearer <Firebase_ID_Token>,Content-Type: application/json
מטען ייעודי (payload) של בקשה:
{
"message": "string (user input or selected prompt chip text)",
"context": {
"active_tab": "string (enum: today | week | coach)",
"week_start_date": "string (ISO date YYYY-MM-DD)",
"selected_day": "string (e.g. monday, optional)"
}
}מטען ייעודי (payload) של התגובה:
{
"message": "string (conversational coaching response displayed in chat bubble)",
"proposal": {
"id": "string (unique proposal ID, nullable)",
"type": "string (enum: NONE | UPDATE_DAY | UPDATE_WEEK)",
"target_day": "string (e.g. monday, nullable)",
"summary_diff": "string (concise summary of changes, e.g. 'Swapped Arnold Press ➔ DB Shoulder Press', nullable)",
"data": "map (full proposed workout day object or weekly plan object, nullable)"
},
"memory_mutations": [
{
"operation": "string (enum: CREATE | UPDATE | DELETE)",
"target_tier": "string (enum: tier_1_staples | tier_2_physiological | tier_3_situational)",
"memory_id": "string (required for UPDATE and DELETE)",
"entry": "map (memory payload with fact, category, expires_at, etc., required for CREATE and UPDATE)",
"reason": "string (concise rationale for why memory was mutated)"
}
]
}- כתיבה כפולה אל
weekly_chats: בכל תור, השרת שומר את הודעת המשתמש הנכנסת (text) ואת התגובה של המאמן (text) ב-/users/{userId}/weekly_chats/{week_start_date}באמצעותFieldValue.arrayUnion(). - תבנית מוטציה מדורגת: החזרת המטען הייעודי (payload) של ההצעה בלי לשנות את
/weekly_plansעד שהמשתמש לוחץ על [אישור התוכנית]. - ניהול זיכרון אוטונומי רציף: פולט
memory_mutationsכדי ליצור, לעדכן או למחוק פריטים מהפרופיל התלת-שכבתי של המשתמש.
POST /api/workouts/log
- מטרה: לשמור אימון שהושלם בארכיון האימונים הקבוע של המשתמש, לחשב את הנפח הסופי או את מספר החזרות המצטבר, לבדוק אם יש שיאים אישיים ולסמן את היום המתוכנן כיום שהושלם.
- טריגר: המשתמש לוחץ על "סיום האימון" במסך מעקב האימון.
- מקור התגובה:
DATABASE– טרנזקציה/כתיבה ישירה של מסד נתונים ב-Cloud Firestore.
מטען ייעודי (payload) של בקשה:
{
"plan_reference": {
"week_start_date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. monday)"
},
"title": "string (e.g. Upper Body Hypertrophy)",
"started_at": "string (ISO timestamp)",
"completed_at": "string (ISO timestamp)",
"duration_seconds": "number (e.g. 3120)",
"exercises_logged": [
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"exercise_name": "string (e.g. Incline DB Bench Press)",
"sets": [
{
"set_number": "number (e.g. 1)",
"weight_lbs": "number (e.g. 60)",
"reps_completed": "number (e.g. 12)",
"completed": "boolean"
}
]
}
],
"user_notes": "string (optional subjective feedback)",
"perceived_exertion": "number (optional 1-10 RPE rating)"
}מטען ייעודי (payload) של התגובה:
{
"success": "boolean",
"log_id": "string (newly created workout log document ID)",
"summary": {
"total_volume_lbs": "number",
"total_sets_completed": "number",
"total_reps_completed": "number"
},
"new_prs": [
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"metric": "string (e.g. '10 Rep Max' or 'Estimated 1RM')",
"value": "string (e.g. '65 lbs' or '105 lbs')"
}
]
}3. ארכיטקטורת מסד הנתונים של Cloud Firestore
אוסף: exercises
היררכיה: אוסף בסיס (/exercises/{exercise_id}) • מטרה: אנציקלופדיה סטטית של תרגילים שכוללת מולטימדיה, רמזים לגבי הצורה, דרישות ציוד והוראות.
{
"id": "string (canonical slug matching document ID, e.g. incline-db-bench-press)",
"name": "string (e.g. Incline DB Bench Press)",
"category": "string (e.g. Strength, Hypertrophy, Cardio, Mobility)",
"target_muscles": {
"primary": ["string (e.g. Upper Chest)"],
"secondary": ["string (e.g. Front Deltoids, Triceps)"]
},
"media": {
"video_url": "string (URL to demonstration video)",
"poster_url": "string (URL to video poster / thumbnail image)"
},
"instructions": ["string (step-by-step execution guidance)"],
"form_tips": ["string (critical cues and common pitfalls)"],
"equipment_required": ["string (e.g. Dumbbells, Incline Bench)"]
}אוסף: weekly_plans
היררכיה: /users/{userId}/weekly_plans/{week_start_date} • מטרה: "מחברת הספירלה" שמכילה את לוח הזמנים המלא של האימונים השבועיים, מדדי סיכום ואימונים יומיים נפרדים.
{
"week_start_date": "string (ISO date YYYY-MM-DD, e.g. 2026-09-07)",
"theme": "string (e.g. Hypertrophy Focus & Upper Body Overload)",
"summary": {
"total_time_minutes": "number (raw minutes, e.g. 180)",
"total_volume_lbs": "number (raw tonnage in lbs, e.g. 38400)",
"frequency": "number (active workout days, e.g. 4)"
},
"suggested_edits": ["string (pre-generated AI prompt suggestion)"],
"days": {
"monday": {
"date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. Monday)",
"is_rest_day": "boolean",
"status": "string (enum: scheduled | completed | skipped)",
"completed_log_id": "string (nullable)",
"completed_at": "string (ISO timestamp, nullable)",
"title": "string (e.g. Upper Body Hypertrophy)",
"focus_tag": "string (e.g. Push & Chest Dominant)",
"duration_minutes": "number",
"estimated_volume_lbs": "number",
"coach_note": "string",
"muscle_focus": [{ "muscle": "string", "intensity": "number (0-100)" }],
"suggested_edits": ["string"],
"blocks": [
{
"label": "string (e.g. Main Compound Block)",
"exercises": [
{
"exercise_id": "string",
"name": "string",
"target_sets": "number",
"target_reps": "string or number",
"rest_seconds": "number"
}
]
}
]
}
}
}אוסף: weekly_chats
היררכיה: /users/{userId}/weekly_chats/{week_start_date} • מטרה: "מאגר הצ'אטים השבועי" מכיל את היסטוריית השיחות המלאה של שבוע אימון נתון בקריאת מסמך אחת.
{
"week_start_date": "string (ISO date YYYY-MM-DD, e.g. 2026-09-07)",
"messages": [
{
"id": "string (unique message ID)",
"sender": "string (enum: user | coach)",
"text": "string (conversational message body)",
"created_at": "timestamp (server timestamp)",
"proposal": {
"id": "string (proposal UID, nullable)",
"type": "string (enum: UPDATE_DAY | UPDATE_WEEK, nullable)",
"target_day": "string (e.g. monday, nullable)",
"summary_diff": "string (nullable)",
"status": "string (enum: pending | accepted | dismissed)",
"data": "map (workout or week snapshot payload, nullable)"
}
}
]
}אוסף: workout_logs
היררכיה: /users/{userId}/workout_logs/{log_id} • מטרה: ארכיון היסטורי קבוע ובלתי ניתן לשינוי של אימונים שהושלמו.
{
"id": "string (document ID)",
"plan_reference": {
"week_start_date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. monday)"
},
"title": "string (e.g. Upper Body Hypertrophy)",
"started_at": "timestamp",
"completed_at": "timestamp",
"duration_seconds": "number",
"summary": {
"total_volume_lbs": "number",
"total_sets_completed": "number",
"total_reps_completed": "number"
},
"exercises_logged": [
{
"exercise_id": "string",
"exercise_name": "string",
"sets": [
{
"set_number": "number",
"weight_lbs": "number",
"reps_completed": "number",
"completed": "boolean"
}
]
}
],
"user_notes": "string (nullable)",
"perceived_exertion": "number (1-10 RPE, nullable)"
}אוסף: user_profiles
היררכיה: /users/{userId}/profile/main • מטרה: "תרשים הלקוח החי" שבו מאוחסן פרופיל הזיכרון של המשתמש, שמורכב מ-3 רמות.
{
"user_id": "string (document owner UID)",
"display_name": "string (e.g. Alex)",
"email": "string (e.g. alex@example.com)",
"preferences": {
"units": "string (enum: lbs | kg)",
"haptics": "boolean",
"sound": "boolean"
},
"equipment_ids": ["string (e.g. 'dumbbells', 'bench_adjustable', 'pull_up_bar')"],
"onboarding_status": "string (enum: not_started | in_progress | completed)",
"created_at": "timestamp",
"tier_1_staples": [
{
"id": "string (e.g. mem_01)",
"category": "string (enum: equipment | preference | goal | chronic_condition)",
"fact": "string (e.g. Home gym with dumbbells up to 50 lbs and adjustable bench)",
"source": "string (e.g. conversational_onboarding | manual_entry)"
}
],
"tier_2_physiological": [
{
"id": "string (e.g. mem_02)",
"body_part": "string (e.g. hamstrings, shoulder)",
"condition": "string (e.g. soreness, acute_fatigue, minor_strain)",
"fact": "string (e.g. Hamstrings are moderately sore from hill sprints)",
"effective_date": "string (ISO date YYYY-MM-DD)",
"expires_at": "timestamp"
}
],
"tier_3_situational": [
{
"id": "string (e.g. mem_03)",
"event": "string (e.g. golf_competition, travel)",
"fact": "string (e.g. Major golf tournament on Saturday; avoid excessive arm/lat fatigue on Friday)",
"week_start_date": "string (ISO date YYYY-MM-DD)",
"expires_at": "timestamp"
}
]
}אוסף: prs
היררכיה: /users/{userId}/prs/{exercise_slug} • מטרה: אחסון שיאים אישיים, הערכות של מדדי 1-Rep Max ושיאים ספציפיים לחזרות.
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"exercise_name": "string (e.g. Incline DB Bench Press)",
"estimated_1rm": "number (calculated using Brzycki formula, e.g. 105)",
"max_weight_set": {
"weight_lbs": "number (e.g. 80)",
"reps": "number (e.g. 10)",
"achieved_at": "timestamp"
},
"rep_records": {
"10": 80,
"8": 85,
"5": 95
},
"last_updated": "timestamp"
}4. מחזור החיים של הנתונים וצינור עיבוד הנתונים של הסוכן
4.1 פייפליין 1: אימון, יצירת אימון ושינוי בשלבים
- פעולת המשתמש: המשתמש מזין טקסט חופשי או לוחץ על צ'יפ עריכה לפי הקשר שנוצר מראש.
- הרכבת ההקשר: השרת מאחזר את פרופיל הזיכרון הפעיל בשלוש הרמות ואת התוכנית של השבוע הנוכחי, ומסיר פריטי זיכרון שפג תוקפם.
- הסקת מסקנות של הסוכן: Gemini מעבד את ההנחיה וההקשר באמצעות קריאה מובנית להפעלת פונקציות או כלים.
- שינוי אוטונומי של הזיכרון: עובדות שחולצו על המשתמשים מיושמות ב-
/user_profiles/{userId}ומסונכרנות עםequipment_ids. - הכנת הצעה בשלבים: הצעות לשינויים באימון מוחזרות עם
proposal.status = "pending"בתוך/weekly_chats/{week_start_date}. - החלטת המשתמש ושמירת הנתונים: לחיצה על [אישור התוכנית] שומרת את הנתונים המוצעים ב-
/weekly_plansומסמנת את ההצעה כ"accepted".
4.2 פייפליין 2: ביצוע האימון והעברה לארכיון (אמת קרקע (ground truth))
- הקלטת סשן בזמן אמת: הלקוח עוקב אחרי המשקל שהורם בפועל, החזרות שהושלמו וסימוני ההשלמה במצב ממשק המשתמש המקומי.
- שליחת סשן: הלקוח קורא ל-
POST /api/workouts/logב "סיום אימון". - חישוב ואימות מצטברים: השרת מחשב את נפח האימון המצטבר ובודק אם יש שיאים אישיים באמצעות נוסחת Brzycki.
- שמירה קבועה: כתיבת רשומה קבועה ב-
/workout_logs, סימון היום"completed"ב-/weekly_plans, ועדכון או הוספה (upsert) של בקשות משיכה חדשות ל-/prs.
4.3 צינור עיבוד נתונים 3: הטמעה וביטול תוקף של זיכרון אוטונומי
- הטמעה: עובדות נשלפות באופן רציף מכל האינטראקציות בשיחות.
- סיווג: רמה 1 (מוצרי צריכה בסיסיים, ללא תפוגה), רמה 2 (מצב פיזיולוגי, TTL של 3-4 ימים), רמה 3 (הקשר מצבי, TTL מבוסס-אירוע).
- גיזום וביטול: פריטים שפג תוקפם (
current_timestamp > expires_at) מסוננים מההקשר של ההנחיה ונמחקים בהדרגה.
5. כללי אבטחה ב-Firestore ומפרטים של אינדקסים מורכבים
5.1 כללי אבטחה (firestore.rules)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
match /profile/{document=**} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /weekly_plans/{planId} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /weekly_chats/{chatId} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /prs/{exerciseSlug} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /workout_logs/{logId} {
allow read, create: if request.auth != null && request.auth.uid == userId;
allow update, delete: if false;
}
}
match /exercises/{exerciseId} {
allow read: if request.auth != null;
allow write: if false;
}
}
}5.2 אינדקסים של שאילתות מורכבות (firestore.indexes.json)
{
"indexes": [
{
"collectionGroup": "workout_logs",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "completed_at", "order": "DESCENDING" }]
},
{
"collectionGroup": "weekly_plans",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "week_start_date", "order": "DESCENDING" }]
},
{
"collectionGroup": "prs",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "last_updated", "order": "DESCENDING" }]
}
]
}6. יומן שינויים והחלטות נעולות
| תאריך | רכיב / החלטה | סוג | ננעל על ידי |
|---|---|---|---|
| הגדרה ראשונית | הופעל פיגום מפרט ריק | הגדרת המערכת | קבוצה |
| 2026-09-04 | נוספו אוספי Firestore: exercises ו-weekly_plans | סכימה | קבוצה |
| 2026-09-04 | נוספו נקודת הקצה POST /api/coach/chat והאוסף chat_messages | נקודת קצה (endpoint) וסכימה | קבוצה |
| 2026-09-04 | שינינו את מבנה האחסון של הצ'אטים לסכימה של קטגוריית weekly_chats | סכימה | קבוצה |
| 2026-09-04 | נוספו נקודת הקצה POST /api/workouts/log והאוסף workout_logs | נקודת קצה (endpoint) וסכימה | קבוצה |
| 2026-09-04 | נוספה סכימת זיכרון עם 3 רמות של user_profiles ועדכון של POST /api/coach/chat עם memory_mutations | נקודת קצה (endpoint) וסכימה | קבוצה |
| 2026-09-04 | השלמתם את סעיף 1 (היקף המערכת ועקרונות) וסעיף 4 (מחזור החיים של הנתונים וצינורות עיבוד נתונים) | השלמת המפרט המלא | קבוצה |
| 2026-09-22 | מפרטים מתואמים: סטאק Next.js + Tailwind + Zustand, אימות Bearer, אוסף משנה של PR, כללי Firestore וכתובות URL קנוניות של תרגילים | התאמה מלאה למפרט | בדיקת הארכיטקטורה |