נקודת הכניסה היחידה למפתח חדש. כל מה שצריך כדי לעבוד נמצא כאן — כולל התוכן המלא של מסמכי העומק, המשוכפל בסוף כנספחים א׳–י״ב. אין צורך לצאת מהמסמך כדי להבין את המערכת, להקים מחשב, להעלות שינוי לאוויר או לקלוט לקוח. היעד: פרודוקטיביות תוך יום עבודה אחד. הדיאגרמות שלאורך הפרקים הן חלק מהמסמך — לא קישוט.
איך לקרוא נתיב כמו
docs/DECISIONS.mdאוservices/mis-core/src/…במסמך הזה. נתיב הוא ציטוט מקור בתוך הריפו, ולא הפניה לקריאה חיצונית: הוא שם כדי שתדעו איפה העובדה חיה בקוד ואיפה לתקן אותה, ומרגע שיש לכם clone הוא לחיץ בעורך. אף נתיב כאן אינו תנאי להבנה — כל מה שצריך לעבוד כתוב במסמך במלואו, ומקום שבו התוכן שוכפל לכאן אומר זאת מפורשות בשורת "מקור:". קפיצות פנימיות מסומנות כקישור לפרק או לנספח, למשל פרק 28 — המגבלות.
אם קיבלתם את הקובץ הזה, אתם מצטרפים לפיתוח. הפרק הזה נכתב כדי שתדעו לאן נכנסתם לפני שאתם מתקינים משהו — הוא לא דורש גישה, לא לריפו ולא לענן. קראו אותו עד הסוף, ואז תדעו גם מה לבקש כדי להתחיל בפועל.
MIS26 בונה תוכנת תפעול לרשויות מקומיות ולקבלנים שעובדים מולן. התחום המוביל הוא פסולת: פינוי אשפה, מכולות, תחנות שקילה, קבלנים, פיקוח וחיוב. סביבו יש עוד: ביטחון עירוני (מצלמות על עמודים), דלק, לוגיסטיקה, חקלאות, ובורסות למחזור.
Sentrix הוא שם הפלטפורמה והריפו — כל זה בנוי על Google Cloud, ומחליף שלושה אתרי Wix שהמערכת עבדה עליהם. עיקרון הברזל של ההחלפה: היא נעשית בלי לגעת בשטח. 300+ משאיות, מאות מצלמות, מאזני גשר וחיישני מכולות משדרים לאותן כתובות בדיוק ולא מרגישים שהוחלף להם המוח. הרבה מהאילוצים במסמך הזה נובעים מהמשפט הזה.
ארבע שכבות. פרק 1 מפרק כל אחת עם דיאגרמה; זו התמונה שמספיקה כדי להתמצא:
| השכבה | מה יש בה | מה חשוב לדעת |
|---|---|---|
| השטח | משאיות עם מצלמות ו‑Jetson, עמודי ביטחון, מאזני גשר, חיישני מכולות, מקליטי וידאו | לא משתנה, ומשדר REST. הזיהוי (YOLO) קורה בקצה, ווידאו לא נשמר אצלנו |
| השער | Load Balancer + Cloud Armor + API Gateway | הדלת היחידה פנימה. אין דרך אחרת להגיע לשירות |
| הליבה | 11 שירותי Cloud Run: 7 שירותי backend פרטיים, mcp-wrapper דרך השער, ו‑4 אפליקציות מאחורי ה‑LB | mis-core הוא הגדול; לידו ביטחון, דוחות, מנוע התראות,
שליחה, סוכני AI ו‑MCP. אפליקציית לקוחות אחת מגישה את כל
סוגי הלקוחות |
| הנתונים | Cloud SQL פרטי, Pub/Sub, BigQuery, Cloud Storage | אין IP ציבורי לדאטהבייס. הכל מוגדר כקוד (Terraform) במונו‑ריפו אחד |
וארבעה עקרונות שיחזרו בכל פרק, כי כל אחד מהם נשען על החלטה שנסגרה
(DECISIONS.md):
dev
ו‑prod, מבודדות לגמרי. אין stage.| הפרקים | מה יוצא מזה |
|---|---|
| 0–3 | להבין את המערכת: מה רץ עכשיו, מפת הריפו, וחוק הזהות — לפני שנוגעים בכלום |
| 4 | היום הראשון — אשף גרפי מקים לכם את הסביבה, ובסוף המערכת רצה על המחשב שלכם |
| 5–6 | ענפים, PR, CI ופריסה — ואז השינוי הראשון שלכם עולה לאוויר, מקצה לקצה |
| 7–16 | דאטהבייס, גיבויים, סודות, השער, מולטי‑טננסי, פרונטאנד, טסטים, ניטור — ומה אסור |
| 17–27 | לקלוט לקוח חדש, המעבר מ‑Wix, ותפעול שוטף |
| 28–30 | המגבלות (מה לא עובד ומה לא מגלה לכם לבד), מי מאשר מה, ומשימות פתיחה מומלצות לספרינט הראשון |
| נספחים | הארכיטקטורה המלאה, IAM, DR, תוכנית גדילה, אגם הנתונים, מודל העלות |
היעד: בסוף יום עבודה אחד יש לכם סביבה עובדת ושינוי
אחד שלכם חי ב‑dev. זה לא סיסמה — פרק 6 הוא תרגיל מודרך
שעושה בדיוק את זה, שורה‑שורה.
שני כללים שכדאי שתכירו כבר עכשיו, כי הם משנים את קצב העבודה:
dev ביום שמסיימים — לא
כשהפיצ'ר אושר ללקוחות. אם החשיפה היא החלטה עסקית, הפיצ'ר נכנס
מאחורי דגל כבוי. שלב 11
מסביר.HAND-IN-HAND-EN.md, פרק 26) כתוב
באנגלית בכוונה.את שלושת אלה אתם לא יכולים להסדיר לבד. בקשו אותם לפני שאתם פותחים את פרק 4, אחרת ההתקנה תיראה כאילו עבדה ואז תיפול על "Not Found" מבלבל:
@mis.org.il — הזהות שאיתה נכנסים
לכל דבר (פרק 3).MIS-Make-It-Simple
ב‑GitHub, עם גישה לריפו sentrix.mis26-dev — שלושה
תפקידים, וזה מה שמבקשים בשם: Viewer
(roles/viewer) לראות את הפרויקט, IAP-secured Tunnel
User (roles/iap.tunnelResourceAccessor)
ו‑Compute OS Login (roles/compute.osLogin)
כדי לפתוח את מנהרת ה‑DB דרך ה‑bastion (פרק 7). prod — רק למי שבאמת
צריך.מי שמצרף אתכם מסדיר את שלושתם. עד שיש לכם אותם — הפרקים 0 עד 3 קריאים לגמרי, וכדאי לקרוא אותם.
כל מה שרץ עכשיו, ואיך נכנסים. הכניסה לכל הכתובות זהה: לוחצים
"המשך עם Google" ומזדהים עם המייל הארגוני שלכם — כל
כתובת שמסתיימת ב‑@mis.org.il (אותה זהות ארגונית מפרק 3).
אין משתמש וסיסמה נפרדים — הזהות הארגונית היא המפתח לכל הדשבורדים.
| כתובת (dev) | מי נכנס לשם | מה רואים |
|---|---|---|
https://demo.app-dev.mis-26.com |
קבלנים | הקוקפיט התפעולי: צי, שקילות, התראות, משימות |
https://mgroup.app-dev.mis-26.com |
רשויות | דשבורד הרשות: פיקוח, אזורים, דוחות |
https://netanya-demo.app-dev.mis-26.com |
חלון הראווה | טננט רשות מקומית עם כל המודולים דלוקים — מארח ההדגמה לראש עיר ולמנכ"ל. אם מסך שם מציג "המודול לא כלול", מסך קבלן, או ריק — זה באג |
https://security.app-dev.mis-26.com |
ביטחון | מצלמות, זיהויים, חוקי התראה |
https://admin.app-dev.mis-26.com |
אדמין | קונסולת ניהול: לקוחות, מודולים, דגלי פיצ'רים
(/feature-flags — מי רואה מה, שלב
11), הרשאות, נעילות |
https://driver.app-dev.mis-26.com |
נהג | אפליקציית הנהג: משימות, ניווט, אישורי איסוף |
https://demo-shuttle.app-dev.mis-26.com |
הסעות | קוקפיט מותאם‑סגמנט (shuttle) על אותו קוד |
https://demo-tanker.app-dev.mis-26.com |
מיכליות | קוקפיט מותאם‑סגמנט (tanker) על אותו קוד |
demo-<segment>.app-dev.mis-26.com |
כל 11 הסגמנטים | טננט דמו לכל סגמנט (demo-cargo,
demo-construction...) — אותו שלד, פרופיל מסכים שונה, אפס
דליפה בין סגמנטים (טסט מטריצה) |
https://kedumim.app-dev.mis-26.com |
קדומים (חלון ראווה) | תוכנית פינוי אמיתית "קדומים כתום": 36 עצירות, תוואי OSRM, ניווט במסך הנהג |
https://exchange-waste.app-dev.mis-26.com |
בורסת פסולת | אפליקציית ה‑exchange (מרקטפלייס) |
https://exchange-haulage.app-dev.mis-26.com |
בורסת הובלה | אותה אפליקציה, ורטיקל הובלה |
כל הכתובות חיות על אותה סביבת dev (
app-dev.mis-26.com, מאחורי Load Balancer ו‑Cloud Armor). התת‑דומיין הוא זיהוי הלקוח (tenant): אותו קוד, מיתוג ונתונים שונים לכל כתובת. מסך כניסה = תקין, נכנסים עם Google. 403/401 בלי מסך = האבטחה עובדת; בדקו שאתם בחשבון הארגוני הנכון.הערה כנה על ה‑DNS: הרשומה היא wildcard (
*.app-dev.mis-26.com), אז כל תת‑דומיין פותר ל‑LB. תת‑דומיין שאין מאחוריו לקוח מקבל 404 בארבע שפות עוד לפני מסך הכניסה, ולכן כתובת שעונה 404 היא תשובה אמיתית. כתובת שמגישה מסך כניסה אומרת שיש שורת Tenant מאחוריה, אבל היא עדיין לא מבטיחה שהוגדר לה סגמנט; ראו פרק 20. exchange-waste/haulage הם יוצאי דופן: הם לא טננטים אלא אפליקציה נפרדת, עם host rule מפורש ב‑url-map.ואיפה ה‑DNS הזה חי בפועל: ב‑GoDaddy, ולא בטרהפורם. זה החלק היחיד בסטאק שאין לו קוד ואי אפשר לקרוא אותו מהרפו, ולכן מה שמוגדר שם מתועד ב‑
docs/DNS-RECORDS.md— שש הרשומות, מה לא נוגעים בו (האתר השיווקי), ואיך מאמתים. שימו לב במיוחד לרשומות ה‑_acme-challenge: הן אימות בעלות לתעודה, הערך שלהן אינו קבוע, וכל פעם ש‑dns_authorizationנוצר מחדש נטבע טוקן חדש שחייב להיכנס שם ביד. וכשעורכים אחת מהן,digימשיך להחזיר את הערך הקודם עד שה‑TTL פוגע — כלומר עריכה נכונה נראית כמו כישלון; המסמך מסביר איך לשאול את שרת השמות ישירות ולעקוף את המטמון.אפליקציה אחת מאחורי כל הכתובות האלה. ארבעת סטי המסכים (municipal, contractors, security, driver) מוגשים מ‑
apps/client-app, והוא ה‑default_serviceשל ה‑url map. אין כלל host פר לקוח, ואיןterraform applyבפתיחת לקוח: הכתובת מגיעה לאפליקציה, האפליקציה שואלת את ה‑DB מי הלקוח, ובוחרת את סט המסכים לפי הסגמנט שלו. פתיחת לקוח = שורה ב‑DB. ההסבר המלא, כולל סדר איך זה בנוי ואיך נבחר סט המסכים: פרק צמוד ל‑2. תת‑דומיין שאין מאחוריו טננט — כולל תווית שמורה בלי דלת — מגיע ל‑client-app ומקבל 404 של מארח לא מזוהה, בדיוק כמו כל תת‑דומיין ריק אחר.
בגרסת ה‑HTML (הקובץ בדרייב, או
docs/onboarding-he.html) יש תוכן עניינים צדי וקבוע שנבנה מהמסמך עצמו, עם תיבת סינון (מקש/), פרקים מתקפלים, סימון הפרק שאתה נמצא בו, ופס התקדמות קריאה. הרשימה למטה היא הקפיצות הראשיות; הצדי מכיל כל כותרת, כולל תתי‑סעיפים. ראו פרק 22.
👋 ברוכים הבאים ל‑MIS26 Sentrix — מה זה, איך זה בנוי, ומה צריך לבקש כדי להתחיל
⚡ פרק 0 · המערכת החיה — כל הכתובות והכניסה המהירה
פרקים
dumpdev
/ makeprodנספחים — תוכן מלא מוטמע
הקישורים לעיל הם קפיצות בתוך המסמך. הנספחים הם העתק מלא של מסמכי העומק שבריפו — הובאו לכאן כדי שהמסמך יעמוד בפני עצמו, בלי תלות בקריאה חיצונית. נספחים ט׳ ו‑י״א נכתבו כאן במיוחד למפתח: המסמכים המקוריים שמאחוריהם הם מסמכי הנהלה שמודדים את המוצר מול השוק, ולכן הנספח מביא את החלק שנוגע לקוד.
MIS26 מפתחת פלטפורמת תפעול לרשויות מקומיות: פינוי פסולת, צי משאיות, תחנות שקילה, מצלמות אבטחה על עמודים, זיהויי AI, התראות ודוחות. הפלטפורמה החדשה — Sentrix — נבנתה מחדש על Google Cloud ומחליפה שלושה אתרי Wix. עיקרון הברזל של המעבר: הציוד בשטח (300+ משאיות, מאות מצלמות) ממשיך לשדר לאותן כתובות בדיוק ולא מרגיש כלום.
מבנה בקצרה: שער אחד (Load Balancer + Cloud Armor + API Gateway) ← שירותים פרטיים על Cloud Run (mis-core, security-core, report-engine, mcp-wrapper, דשבורדים) ← Cloud SQL פרטי + Pub/Sub + BigQuery. הכל מוגדר כקוד (Terraform) במונו‑ריפו אחד.
מסמכי העומק: ARCHITECTURE-COMPLETE.md (הארכיטקטורה
המלאה, כולל אנלוגיות לכל רכיב), PROJECT-MAP.md (התכנון),
DECISIONS.md (כל החלטה סגורה, D1–D33), TOUR.md
(סיור מנהלים), VISION-END-STATE.md (לאן זה הולך).
| מה | dev | prod |
|---|---|---|
| פרויקט GCP | mis26-dev |
mis26-prod |
| ענף Git שנפרס אליה | dev |
main |
| הדלת הקדמית (LB + Armor) | https://app-dev.mis-26.com |
https://app.mis-26.com |
| דומיין לקוח | <slug>.app-dev.mis-26.com (למשל holon) |
<slug>.app.mis-26.com |
| API Gateway | https://sentrix-gw-cnz6wcy2.ew.gateway.dev |
https://sentrix-gw-7omgn50c.ew.gateway.dev |
| בדיקת חיים | /health ⇐ 200, /trucks בלי טוקן ⇐ 401 |
אותו דבר |
| Cloud SQL | sentrix-pg (private IP, אין גישה מהאינטרנט) |
sentrix-pg (HA + PITR) |
| בסטיון למסד | sentrix-bastion (IAP בלבד) |
sentrix-bastion (IAP בלבד) |
סטטוס תשתית — מה עוד חי בשתי הסביבות (שורה לרכיב, המסמך המעמיק בסוגריים):
docs/evidence/2026-07-25/prod-promotion.md).sentrix-cache פעיל,
read-through ב‑mis-core
(docs/evidence/2026-07-25/infra-pack.md §1).sentrix_lake + sentrix_marts
חיים, הזנה ישירה מ‑Pub/Sub (נספח ח׳).TruckEvent ו‑CargoEvent
מפורטשים + ג'וב תחזוקה אוטומטי (נספח ז׳).docs/evidence/2026-07-26/dev-green.md).docs/evidence/2026-07-25/ai-agents-layer.md).docs/evidence/2026-07-26/lake-models.md).docs/evidence/2026-07-26/connectors-registry.md).packages/catalog, עם טסט מטריצה שמוכיח אפס דליפה בין
סגמנטים; 5,798 טסטים ירוקים בריפו (פרק 13).עוד כתובות קבועות: הקוד —
https://github.com/MIS-Make-It-Simple/sentrix; הקונסולה —
console.cloud.google.com (לבחור פרויקט למעלה!); האתר
השיווקי — mis-26.com (GoDaddy→Vercel, לא נוגעים); מסמכי
סטטוס גרפיים — Drive חומרים מוויקס/sentrix-status/.
הדומיינים של שתי הסביבות מגישים מסך כניסה (redirect
ל‑/signin); 401/403 מהשער בלי טוקן = האבטחה עובדת, לא
תקלה.
לשמור כ‑bookmarks. אותו קישור, מחליפים
mis26-dev⇄mis26-prod לפי הסביבה:
ברמת הארגון (לא פר פרויקט): Security Command Center — https://console.cloud.google.com/security/command-center?organizationId=264541084239. נוחת בפרויקט הלא נכון? בורר הפרויקטים למעלה משנה הכל — לוודא לפני כל פעולה.
הפרק הקודם נותן את התמונה. הפרק הזה נותן את הרשימה,
כי בפעם הראשונה שמשהו נשבר בענן השאלה היא "מה בכלל קיים כאן", ולחפש את
התשובה בקונסולה תוך כדי תקלה זה הזמן הגרוע לגלות אותה. כל שורה כאן קיימת
כקוד תחת infra/terraform/ — 33 קובצי .tf,
מחולקים לפי תחום ולא לפי סוג משאב, ולכן sql.tf מחזיק את
המסד עם ההרשאות שלו ו‑video-vault.tf מחזיק את הדלי, את
הג'וב ואת ה‑IAM של הכספת באותו מקום.
שתי סביבות, אותו קוד. mis26-dev
ו‑mis26-prod נבנות מאותו ריפו טרפורם עם workspace שונה;
ההבדל היחיד הוא ה‑tfvars. האזור הוא me-west1 (תל אביב) לכל
דבר שיכול לחיות בו, פרט לגיבוי החוץ‑אזורי שיושב
ב‑europe-west1 בכוונה.
| שירות | מה הוא | ingress | Service Account |
|---|---|---|---|
mis-core |
הליבה: שקילה, צי, קליטה, ג'ובים, משתמשים, מדידה | פרטי | sa-mis-core |
security-core |
מצלמות, זיהויים, וידאו חי — מסד נפרד | פרטי | sa-security-core |
report-engine |
PDF/Excel רב‑לשוני (Chromium בתוך האימג') | פרטי | sa-report-engine |
alerts-engine |
צרכן pull של Pub/Sub, min-instances=1 בכוונה |
פרטי | sa-alerts-engine |
notifications |
שליחה בפועל: מייל, וואטסאפ, פוש | פרטי | sa-notifications |
ai-agents |
שער ה‑LLM: ניתוב מודלים ומדידת עלות פר לקוח | פרטי | sa-ai-agents |
mcp-wrapper |
חשיפת אותם endpoints כ‑MCP לסוכני AI | דרך השער | sa-mcp-wrapper |
client-app |
אפליקציית הלקוחות — כל המסכים, כל הלקוחות | LB בלבד | sa-client-app |
admin-console |
הקונסולה שלנו | LB בלבד | sa-admin-console |
exchange |
בורסות פסולת והובלה | LB בלבד | sa-exchange |
partner-portal |
תיעוד ה‑API לאינטגרטורים | LB בלבד | sa-partner-portal |
"פרטי" = --no-allow-unauthenticated בתוך ה‑VPC: מי שלא
מחזיק roles/run.invoker על השירות מקבל 403 לפני שהקוד רץ.
ארבעת ה‑apps נפרסים
--allow-unauthenticated --ingress=internal-and-cloud-load-balancing,
כלומר גוגל מרשה כניסה אבל רק דרך ה‑Load Balancer —
קריאה ישירה לכתובת ה‑run.app שלהם נחסמת ברשת.
ל‑client-app בפרוד יש --min-instances=1 כדי
שלקוח לא ייתקל ב‑cold start.
שני ג'ובים (Cloud Run Jobs, לא services): db-backup —
הגיבוי הלוגי המוצפן, ו‑sentrix-migrate — ריצת ה‑Prisma
migrations, שנקראת מתוך פס הפריסה של mis-core לפני שהתעבורה עוברת
ל‑revision החדש.
sentrix-vpc + sentrix-subnet. אין IP
ציבורי למסד, ולא ל‑Redis.sentrix-bastion — מכונה אחת, נגישה רק
דרך IAP tunnel. זו הדרך היחידה להגיע למסד מהמחשב שלכם (פרק 7).--set-secrets בפס הפריסה, ולא דרך
משתנה סביבה בקוד.sentrix-vault, ובו המפתח
credentials-vault שמצפין את פרטי ההתחברות של המכשירים
והספקים החיצוניים בתוך המסד.| מה | שם | הערה |
|---|---|---|
| Cloud SQL (PostgreSQL) | sentrix-pg |
שני מסדים: mis_core ו‑security_core. פרוד:
HA + PITR |
| Memorystore (Redis) | sentrix-cache |
read-through ב‑mis-core |
| BigQuery — האגם | sentrix_lake |
Pub/Sub כותב אליו ישירות, בלי קוד ETL |
| BigQuery — החיתוכים | sentrix_marts |
scheduled queries: אנומליות דלק, תחזית מילוי, ניצולת צי |
| Cloud Storage | <project>-lift-media |
תמונות פינוי |
<project>-p2p-media |
מסמכי הובלות בין עסקים | |
<project>-cargo-media |
מסמכי מטען | |
<project>-construction-waste-docs |
טופסי פסולת בניין (טופס 4) | |
<project>-video-vault |
כספת קטעי הווידאו של הדמו והביקורת | |
<project>-dr-backups-euw |
הגיבוי החוץ‑אזורי (europe-west1) |
Pub/Sub: sentrix-ingest-events +
sentrix-ingest-events-dlq,
ו‑sentrix-notifications +
sentrix-notifications-dlq. לכל topic יש DLQ כי אירוע שנכשל
חייב להישאר בעולם ולא להיעלם; מה שנכנס ל‑DLQ מופיע בהתראה.
Load Balancer עם תעודת wildcard על *.app.mis-26.com
(ובדב *.app-dev.mis-26.com), Cloud Armor לפניו (WAF,
אנטי‑בוט, rate limiting), ומאחוריו ה‑API Gateway לכל תעבורת ה‑API.
הנקודה החשובה ל‑url_map: client-app הוא
ה‑default_service, ולכן תת‑דומיין חדש מגיע אליו
בלי חוק host ובלי apply.
ארבע כתובות כן מחזיקות חוק host מפורש, כי הן מובילות
לשירות אחר: admin ⇒ admin-console, mcp ⇒
mcp-wrapper, partners ⇒ partner-portal
ו‑exchange-* ⇒ exchange. הן שמורות, ולקוח לא יכול לקבל אותן
כסלאג — אשף הקמת הלקוח בקונסולה קורא את אותה רשימה בדיוק ולכן הוא לא
יכול לחלוק על ה‑LB.
auth הוא מקרה שלישי, וכדאי לא לבלבל אותו עם השניים:
אין לו חוק host. הוא מגיע ל‑client-app כמו כל תווית
אחרת, ושם ה‑middleware מרשה עליו רק את /__/auth/* ומחזיר
404 לכל השאר (פרק Firebase).
| טריגר | על מה הוא מופעל |
|---|---|
deploy-<env>-<service> |
9 שירותי ה‑backend, כל אחד עם
ה‑cloudbuild-<service>.yaml שלו |
deploy-<env>-client-app |
apps/client-app/** או חבילת פרונטאנד משותפת |
deploy-<env>-admin-console |
apps/admin-console/** |
deploy-<env>-exchange |
apps/exchange/** |
deploy-<env>-partner-portal |
apps/partner-portal/** |
הטריגרים הם אזוריים. זו הסיבה שכל פקודת
gcloud builds חייבת --region=me-west1; בלעדיה
גוגל מחזיר את הבניות הגלובליות, כלומר רשימה ישנה עם עמודת טריגר ריקה,
וזה נראה כמו "אין בניות" במקום "שאלת את האזור הלא נכון".
gcloud builds list --region=me-west1 --project=mis26-dev --limit=5 \
--format='table(id,status,substitutions.TRIGGER_NAME,createTime)'
כולם נקראים כ‑POST /jobs/<name> על mis-core עם
x-jobs-secret, ולכן כל אחד מהם אפשר להריץ ביד בדיוק כמו
ש‑Scheduler מריץ אותו. השעות הן Asia/Jerusalem.
| ג'וב | cron | מה הוא עושה |
|---|---|---|
site-offline-detection |
23 23 * * * |
מסמן אתרי שקילה כלא‑מקוונים לפי סף heartbeat |
daily-report |
7 10 * * * |
שורות הדוח היומי פר לקוח |
inside-truck-over-2h |
0 * * * * |
התראה על משאית שנמצאת באתר מעל שעתיים |
over-weight |
21 20 * * * |
דוח חריגות משקל יומי |
usage-rollup |
10 3 * * * |
סוגר את מוני הצריכה של אתמול (טוקנים + LOAD_UNITS) |
ituran-pull |
*/10 * * * * |
ריענון מיקומים חיים לכל לקוח עם FLEET |
ituran-history |
*/5 * * * * |
משיכת היסטוריה צפופה, כדי שקצות מצבי העבודה יהיו מדויקים |
ituran-segment |
2-59/5 * * * * |
מגבש את הזרם לשורות TruckWorkSegment ולסמני
מעבר‑מצב |
statement-generate |
40 3 2 * * |
סוגר את החודש שעבר לחשבוניות בלתי‑ניתנות לשינוי |
partition-maintenance |
17 3 1 * * |
פרטישן לחודש הבא + חלון חם על טבלאות האירועים |
lake-anomalies-pull |
10 4 * * * |
מודל אנומליות הדלק באגם ⇒ התראות FUEL_THEFT |
legacy-pull-tick |
*/20 * * * * |
זורע וממשיך את המשיכות מהמערכת המורשתית |
video-vault-sweep |
35 5-21 * * * |
דואג לקטע וידאו אמיתי אחד לכל משאית‑יום, שבעה ימים אחורה |
signin-failure-pull |
53 * * * * |
כשלי כניסה מ‑Identity Platform ⇒ שורות AuditLog |
openai-costs-pull |
20 7 * * * |
הוצאת OpenAI של אתמול ⇒ מונים + התראות תקציב |
demo-connect-online |
*/15 * * * * |
מכוון את משאית ה‑__live-demo של כל דמו למקליט שמקוון
עכשיו |
demo-refresh |
0 4 * * * |
מריץ מחדש את זריעות יום‑הדמו לתאריך של היום |
camera-health-sweep |
20 * * * * |
לוכד ומסווג פריים מכל מצלמת משאית; פריים שחור = מצלמה מנותקת |
כולם אידמפוטנטיים: הרצה חוזרת של אותו יום מתכנסת לאותה תוצאה ולא מכפילה שורות. זו לא נחמדות — זה מה שמאפשר להריץ ג'וב ביד אחרי תקלה בלי לחשוב.
מה שקוד טרפורם לא אומר לכם. כמה מהמשאבים כאן מסומנים בהערה
INTEGRATOR: NEW entry — takes effect only on the next terraform apply. זה אומר שהם קיימים בקוד וייכנסו לענן ב‑apply הבא, dev קודם. הדרך היחידה לדעת מה כבר עומד שם היאterraform planבסביבה, ומי שמריץ apply הוא שניר (מי מאשר מה). עד ה‑apply הג'וב קיים אבל אף אחד לא קורא לו — ואפשר להריץ אותו ביד עםx-jobs-secret.
מונו‑ריפו אחד (pnpm workspaces). שלוש משפחות קוד — apps
(מה שהמשתמש רואה), services (ה‑backend),
packages (משותף) — וסביבן תשתית, דאטה ותיעוד:
sentrix/
├── AGENTS.md ← כללי הזהב המחייבים (אדם וסוכן AI) — נקרא ראשון
├── README.md ← מפת־על קצרה · CLAUDE.md — הנחיות לסוכני קוד
├── package.json ← שורש ה־monorepo · pnpm-workspace.yaml — רשימת החבילות
├── apps/ ← פרונטאנד Next.js
│ ├── client-app/ ← **אפליקציית הלקוחות היחידה**. בוחרת מסכים לפי
│ │ הסגמנט. products/{municipal,contractors,security,driver,shared}
│ ├── exchange/ ← בורסות פסולת והובלה
│ ├── partner-portal/ ← תיעוד ל-API לאינטגרטורים
│ └── admin-console/ ← הקונסולה שלנו: אישור משתמשים, roles, מודולים פר לקוח
├── services/ ← Fastify על Cloud Run, פרטיים מאחורי השער
│ ├── mis-core/ ← הליבה: שקילה, משאיות, 35 נקודות ingest /_functions/*, /jobs/*
│ │ └── prisma/schema.prisma ← עותק מסונכרן; הקנוני ב־packages/shared-db (פרק 7)
│ ├── security-core/ ← ביטחון: מצלמות ואירועים — מסד נפרד בכוונה
│ ├── report-engine/ ← מנוע דוחות PDF רב־לשוני (he/en/ar/es, מיתוג פר טננט)
│ ├── mcp-wrapper/ ← חשיפת המערכת כ־MCP לסוכני AI
│ ├── alerts-engine/ ← מנוע ההתראות (צרכן Pub/Sub, חי ב-dev) · notifications/
│ └── ai-agents/ ← שער ה-LLM: ניתוב מודלים, מדידה · db-backup/ — ג'וב הגיבוי
├── packages/ ← קוד משותף לכמה אפליקציות/שירותים
│ ├── catalog/ ← קטלוג המוצר: 37 מודולים, 32 סוגי רכב, 11 סגמנטים, פרופילי מסכים
│ ├── ui-kit/ ← מערכת העיצוב: רכיבים + src/theme.css (פרק 12)
│ ├── shared-db/ ← prisma/schema.prisma הקנוני + migrations של mis_core
│ ├── contracts-openapi/ ← החוזים — endpoint נולד קודם כאן (contract-first)
│ ├── api-client/ ← client שנגזר מהחוזים, בו משתמשים הדשבורדים
│ ├── shared-auth/ ← אימות והרשאות משותפים
│ └── shared-i18n/ ← כל מחרוזת, בארבע שפות he/en/ar/es
├── db/guardrails/ ← דרגות הגישה למסד וחוק ה־DDL (פרק 7) + apply.sh
├── infra/ ← הענן כקוד
│ ├── terraform/ ← VPC, SQL, LB+Armor, ניטור — state ב־GCS
│ ├── cloudbuild/ ← cloudbuild-<service>.yaml — פס הפריסה פר שירות (פרק 5)
│ ├── api-gateway/ ← קובצי ה־openapi של השער ונוהל הגלגול (פרק 10)
│ └── env/ ← קונפיג לא־סודי פר סביבה
├── ingestion/ ← truck-ingest, pole-ingest, connectors — צינורות לאגם
├── scripts/ ← db-studio.sh (מנהרה+Studio) · lake-export.sh · ci/ (בדיקות CI)
├── docs/ ← כל התיעוד · docs/conventions/ — המוסכמות המחייבות (פרק 17)
└── archive/ ← קוד ה־Wix הקפוא — רפרנס בלבד, לא מייבאים ממנו
הכלל: לוגיקה עסקית חדשה נכנסת ל‑service לפי תחום; UI — לאפליקציה לפי קהל; מה שמשותף לשניים ומעלה — package. לא בטוחים איפה משהו יושב? README של אותה תיקייה.
זה הפרק שחוסך את הבלבול הגדול ביותר בריפו: יש שירות Cloud Run אחד שמגיש את כל סוגי הלקוחות, ובתוכו ארבעה עצי מסכים. מי שמחפש "האפליקציה של הרשויות" או "האפליקציה של הביטחון" מחפש תיקייה, לא שירות.
apps/client-app/srcsrc/
├── middleware.ts ← מחליט לאיזה טננט ולאיזה מוצר הבקשה שייכת
├── app/
│ ├── layout.tsx · globals.css
│ └── p/ ← ארבעת עצי המסכים. לא נגישים מהאינטרנט (ראו למטה)
│ ├── municipal/ · contractors/ · security/ · driver/
├── products/ ← הקוד של כל מוצר: רכיבים, lib, טיפוסים, fixtures
│ ├── municipal/ (142 קבצים) ← רשויות: פיקוח, מכולות, פסולת בניין
│ ├── contractors/ (149 קבצים) ← קבלנים וצי: קוקפיט, משאיות, שקילה, דלק
│ ├── security/ (48 קבצים) ← חדר בקרה: מצלמות עמודים, זיהויים
│ ├── driver/ (22 קבצים) ← אפליקציית הנהג (PWA)
│ └── shared/ (7 קבצים) ← מה שיותר ממוצר אחד צורך בפועל
├── components/ ← מעטפת האפליקציה, משותפת לכל המוצרים
├── lib/ ← tenant.ts · tenant-runtime.ts · product.ts · session.ts
└── i18n/
products/shared/lib/ הוא הכלל שנוצר מהאיחוד: קובץ עובר
לשם ברגע ששני מוצרים צורכים אותו בפועל, ולא "כי אולי
יהיה שימוש". מה שיושב שם היום: הפרוקסי לאיתוראן, הווידאו החי, ה‑client
של security-core, פיני המשאיות על המפה, ה‑hook של מקליט מקוון, וכיסוי
ה‑i18n של הסיורים.
middleware.tsהסדר הזה הוא ה‑API של האפליקציה. הוא מתועד בקובץ עצמו, וזה התקציר:
| # | צעד | למה הוא שם |
|---|---|---|
| 1 | קובץ סטטי | בקשה לתמונה לא עוברת שער זהות. בלי זה /mis-logo.png
מקבל 307 ל‑/signin |
| 2 | /__/* |
עוזר הכניסה של Firebase, שמוגש מהדומיין שלנו. מדלג על כל השאר בכוונה — פרק Firebase |
| 3 | נתיב פנימי | בקשה שכבר מכילה /p/<product> נדחית
ב‑404. הכתיבה‑מחדש היא הדרך היחידה לעצי המסכים |
| 4 | סלאג הטננט | <slug>.<base> / apex / localhost |
| 5 | החלטת host | חיפוש ב‑DB בזמן ריצה, ורק אם הוא נכשל — רשימת ההיתר שנאפתה לאימג'. host שאין לו בעלים מקבל 404 לפני מסך הכניסה |
| 6 | הפניית רשות | 308 עבור זוג mgroup- יחיד |
| 7 | המוצר | lib/product.ts — הצעד שביטל את חוקי ה‑host בטרפורם |
| 8 | שער זהות | קיום cookie בלבד. המאמת האמיתי הוא השער, לא הדפדפן |
| 9 | כתיבה מחדש | /trucks ⇒ /p/<product>/trucks, עם
כותרות הטננט |
צעד 7 נפתר בשש שכבות, וההיגיון בסדר שלהן הוא כל ההבדל בין "לקוח חדש עובד מיד" לבין "לקוח חדש דורש apply":
driver — אפליקציית הנהג היא
אפליקציית תפקיד, לא אפליקציית לקוח: אותה PWA לנהג של כל
טננט. התווית מכריעה.TENANT_PRODUCT_PINS — רשימה
קפואה של כתובות שקדמו לפתרון בזמן ריצה. היא קיימת כדי
שהאיחוד לא יזיז אף host חי, ובכלל זה demo-security
ו‑demo-authorities, שמדגימים בכוונה את הפרופילים של
אפליקציית הקבלנים. לקוח חדש לא נכנס לרשימה הזו
לעולם.segmentKey, סגמנט ממופה ל‑view-profile
ב‑@sentrix/catalog, וה‑profile מצהיר איזו מעטפת ואילו מסכים
המוצר מקבל. לקוח שנוצר לפני חמש דקות נוחת על המוצר הנכון בלי פריסה של
שום דבר.TENANT_PRODUCT_MAP — גיבוי אפוי לצעד
3, בשימוש רק כשהחיפוש נכשל (תקלה, timeout), ולא כשהוא
ענה. לקוח חדש לא נמצא בו ולא צריך להיות.DEFAULT_CLIENT_PRODUCT כדי ש‑pnpm dev יעבוד
בלי קונפיגורציה.הכתיבה‑מחדש נכנסת ל‑middleware פעם שנייה. על שרת
Node עצמאי (מה שרץ בפרוד), rewrite חוזר לצינור הבקשות, כלומר
ה‑middleware רץ שוב על הנתיב הפנימי. בלי דרך להבדיל בין השניים, שומר הסף
של צעד 3 נדלק על הכתיבה‑מחדש שלנו עצמה ומחזיר 404 לכל עמוד באפליקציה.
הפתרון הוא REWRITE_MARKER — ערך רנדומלי שנטבע פעם אחת
לתהליך ולא נכתב לשום תשובה. הוא חייב להיות לא‑ניחושי
ולא רק "כותרת שקיימת": "תרשה את הנתיב הפנימי כשיש כותרת" היה מאפשר לקורא
לשלוח GET /p/security/cameras עם
x-sentrix-runtime-tenant-id מזויף ולקרוא את חדר הבקרה של
לקוח אחר.
כותרות שנקבעות בפנים נמחקות בכניסה.
x-sentrix-product וכותרות הטננט של זמן ריצה נמחקות מהבקשה
הנכנסת לפני שמשהו קורא אותן. מי שיכול לקבוע אותן בוחר לעצמו טננט
ומוצר.
products/<product>/ ומקבל route תחת
app/p/<product>/. שאר המוצרים לא רואים אותו ולא
מתקמפלים איתו.products/shared/, וכל מוצר מייבא. אין העתקה; יש טסט
shell-parity שמוודא שמעטפת האפליקציה לא נפרדת שוב.segmentKey שלו, לא הפריסה.http://tzvi-cohen.localhost:3010 מגיע לקבלנים, ותת‑דומיין
של טננט רשות מגיע לרשויות. הכתיבה על הנתיב הפנימי
(/p/municipal/...) תמיד תחזיר 404, גם מקומית.you@mis.org.il. אין חשבונות פרטיים ואין חשבונות משותפים.
כלי הסריקה המאושרים הם אלה שרצים ב‑CI — Semgrep, Trivy ו‑gitleaks (פרק
5); כל כלי צד‑שלישי אחר עם גישה לקוד — רק באישור מראש.gh auth status וגם
gcloud config get-value account. לא בטוחים? עוצרים
ובודקים.mis26-dev (לרוב Viewer + הרשאות נקודתיות); ולגישת מסד —
IAP-secured Tunnel User + Compute OS Login על
mis26-dev. prod — רק למי שבאמת צריך.הזהות של הלקוחות (לא שלכם — שלכם היא הזהות הארגונית מהפרק הקודם) מנוהלת ב‑Identity Platform, שהמסכים שלו יושבים בקונסולת Firebase. כדאי להתחיל מהמשפט שחוסך את החיפוש: אנחנו משתמשים ב‑Firebase רק לזהות. אין Firestore, אין Realtime Database, אין Firebase Hosting ואין Cloud Functions. הדאטה יושב ב‑Cloud SQL, האחסון ב‑Cloud Storage והשרתים ב‑Cloud Run. מי שמחפש קולקציית Firestore מחפש משהו שלא קיים.
| מה | dev | prod |
|---|---|---|
| פרויקט Firebase (= פרויקט GCP) | mis26-dev |
mis26-prod |
| מנפיק הטוקן (issuer) | https://securetoken.google.com/mis26-dev |
…/mis26-prod |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN |
auth.app-dev.mis-26.com |
auth.app.mis-26.com |
FIREBASE_AUTH_HANDLER_ORIGIN |
mis26-dev.firebaseapp.com |
mis26-prod.firebaseapp.com |
| מפתח ה‑web API | substitution בטריגר, לא סוד | אותו דבר |
מפתח ה‑web API וה‑authDomain ציבוריים בכוונה — הם
מזהים את הפרויקט ולא מעניקים דבר. האכיפה היא בדיקת ה‑JWT בשער + רשימת
הדומיינים המורשים ב‑Identity Platform. לכן הם יושבים כ‑substitutions
בטריגר ה‑build ולא ב‑Secret Manager, והם נאפים לאימג' בזמן בנייה כמשתני
NEXT_PUBLIC_*.
זה החלק שנראה מבלבל עד שמבינים אותו, ולכן שווה את שלוש הפסקאות:
authDomain הוא הדומיין שהמשתמש רואה
במסך בחירת החשבון של גוגל. כשהוא היה
mis26-prod.firebaseapp.com, לקוח שנכנס דרך גוגל התבקש
"להמשיך אל mis26-prod.firebaseapp.com" — כתובת שנראית ככלי פנימי, לא כמו
המערכת שהוא קנה. לכן authDomain הוא
auth.<app_domain>, כלומר הדומיין
שלנו.
FIREBASE_AUTH_HANDLER_ORIGIN הוא איפה
העוזר האמיתי של Firebase מתארח.
next.config.ts מגדיר rewrite מ‑/__/auth/*
אליו, ולכן הדפדפן מדבר רק עם הדומיין שלנו וגוגל עדיין מקבל את הבקשה
שלו:
// apps/client-app/next.config.ts
{ source: '/__/auth/:path*', destination: `https://${handler}/__/auth/:path*` }
הרווח השני, והחשוב יותר טכנית: הזרימה נהיית
first-party. זה מה שמונע מ‑Safari לבלוע את תוצאת
ה‑redirect בגלל storage partitioning — כל תווית
*.<app_domain> היא אותו site כמו
auth.<app_domain>.
host אחד משותף לכולם, לא אחד לכל לקוח. זו החלטת תכנון ולא קיצור דרך: authDomain פר לקוח היה דורש רשומה ידנית בקונסולת גוגל (דומיין מורשה + redirect URI) בכל קליטת לקוח, ואחת ששוכחים שוברת את הכניסה של אותו לקוח בלי שום דבר בלוגים של השרת. host אחד עולה זוג רשומות, פעם אחת.
auth אינה תווית לקוח: אין לה שורת
Tenant והיא לא ברשימת ההיתר, ולכן כל נתיב עליה חוץ
מ‑/__/auth/* מקבל 404 בשער ה‑host של ה‑middleware (צעד 5
בפרק הקודם). ה‑middleware מדלג עליה מפורשות
בצעד 2 — בלי הדילוג הזה ה‑callback של OAuth, שמגיע מעצם הגדרתו
בלי cookie, היה נזרק ל‑/signin, כלומר לקוח
חוזר מגוגל אל מסך הכניסה שממנו יצא.
signInWithEmailAndPassword.GoogleAuthProvider, עם popup
וירידה ל‑redirect כשהדפדפן חוסם.OAuthProvider('apple.com'),
באותו מסלול.components/totp-challenge.tsx) והכניסה נפתרת מול
ה‑resolver. מי שלא רשם MFA לא רואה את המסך הזה בכלל.sendPasswordResetEmail,
עם continueUrl שמחזיר ללקוח הנכון.ה‑SDK של Firebase מחזיק את ה‑session בדפדפן. אחרי כניסה, ובכל סבב
טוקן שה‑SDK עושה, הלקוח שולח POST /api/session עם ה‑ID
token הנוכחי, והוא נשמר בקוקי httpOnly בשם
sx-session. קוד השרת (עמודים, פרוקסי /api/*)
קורא את הקוקי ומעביר את הטוקן כ‑Bearer אל ה‑API Gateway.
הנקודה שקובעת את כל מודל האבטחה: שרת ה‑Next לא מתייחס לקוקי
כהוכחת זהות לעולם. ה‑middleware משתמש רק בקיום
שלו כדי לנתב (אין קוקי ⇒ /signin), וכל בקשת דאטה מאומתת
מחדש בשער מול JWKS של Identity Platform — בדיוק כמו תעבורת פרודקשן. לפני
שמירה בקוקי נעשית בדיקת צורה בלבד (שלושה מקטעי
base64url, אורך סביר); חתימה ותפוגה נבדקות בשער.
הקוקי חי 60 דקות, כמו תוקף ה‑ID token. ה‑SDK מסבב את הטוקן כחמש דקות
לפני התפוגה ו‑SessionRefresher שולח את הקוקי מחדש בכל סבב,
ולכן שעה שלמה נותנת לסבב מרווח אמיתי במקום להתחרות בו בדקה ה‑55.
signin-failure-pull קורא
את ה‑data-access audit log של identitytoolkit וכותב שורת
AuditLog לכל ניסיון כושל (outcome=FAILED,
actor=svc:identity-platform). הוא מתקדם מ‑watermark משלו,
ולכן ריצה שהוחמצה מתקנת את עצמה.identitytoolkit.googleapis.com בלבד.User שלנו נושאת את
התפקיד, את הסטטוס
(PENDING/ACTIVE/DISABLED) ואת
ה‑accountId, וזה מה שקובע הרשאות. זהות ב‑Identity Platform
בלי שורת User מאושרת מקבלת 403
USER_NOT_APPROVED בשירות, לא ב‑UI. כניסה מוצלחת אינה
אישור.ברירת המחדל: אשף התקנה גרפי, מהיום הראשון. אחרי clone (צעד 4 למטה) מריצים פקודה אחת, והאשף מבצע בפועל את כל צעדי ההקמה — עם התקדמות חיה, לוג מלא לכל צעד וכפתור Retry לצעד שנכשל:
pnpm setup:gui # no pnpm yet? the wizard itself needs nothing beyond Node 20+: node scripts/setup-wizard/server.mjsנפתח דפדפן על
http://localhost:4999(מקומי בלבד). הצעד הראשון הוא שער זהות: האשף מאמת שה‑ADC של gcloud שייך לכתובת@mis.org.ilושמשתמש ה‑GitHub חבר בארגון MIS-Make-It-Simple — ומסרב להמשיך בלעדיהם (זה חוק הזהות מפרק 3, נאכף בקוד). משם הוא מריץ צעד‑צעד: בדיקת דרישות‑קדם, pnpm install, יצירת לקוחות Prisma, קובצי env, בדיקת מנהרת ה‑DB, והרמת הדשבורד עד שהוא עונה עלhttp://tzvi-cohen.localhost:3010. אלה בדיוק צעדיscripts/bootstrap-env.sh, רק עם ממשק; האשף גם מגדיר סביבת התקנה נכונה אוטומטית, אז אין דגלים לזכור.
מעדיפים טרמינל? אותם צעדים, שני סקריפטים. שניהם בטוחים להריץ שוב בכל פעם:
# fresh machine (installs tools, clones, installs, verifies): curl -fsSL https://raw.githubusercontent.com/MIS-Make-It-Simple/sentrix/dev/scripts/new-station.sh | bash # already cloned — bring the repo to a runnable state: ./scripts/bootstrap-env.sh # add --tunnel to also open the dev-DB tunnel
new-station.shמקים מחשב מאפס: מתקין כלים (Homebrew, Node 20+, pnpm דרך corepack, gh, gcloud, Windsurf), מפעיל התחברויות, מושך את הריפו ל‑~/dev/sentrix, מתקין, מריץprisma generateו‑pnpm -r test. bootstrap-env.shלוקח ריפו קיים ומביא אותו למצב "אפשר להריץ אפליקציה מול dev": דרישות‑קדם, התקנה, לקוחות Prisma, תבנית.envל‑DB, ועם--tunnelגם מנהרת IAP ל‑DB. שניהם מגדירים סביבת התקנה נכונה אוטומטית, ובסוף מדפיסים מה נשאר ידני (בקשות הגישה, פרק 3).
למי שמעדיף להתקין ידנית, כלי‑כלי: המסלול המלא נמצא
ב‑docs/conventions/dev-environment.md. חשוב לדעת — יש שם
שני מסלולי התקנה נפרדים, אחד ל‑Mac ואחד ל‑Windows
(Homebrew מול winget/PowerShell). בוחרים את מערכת ההפעלה בתחילת §5
וממשיכים רק במסלול שלה; שאר הסעיפים (התחברויות, clone, הרצה) משותפים, עם
הערות Windows בגוף הטקסט. ברירת המחדל לכולם היא האשף — המסלול הידני נועד
למי שרוצה לראות כל בורג, או לאבחון תקלה.
מוסכמה קבועה בריפו: הוראות שלב‑אחר‑שלב נכתבות באנגלית (כלים משבשים עברית), ההסברים בעברית. המסלול הממוספר שלמטה הוא מה שהאשף מריץ מאחורי הקלעים — שימושי להבנה, לתקלות, ולמי שעובד בלי GUI.
mis26-dev מופיע בבורר הפרויקטים בקונסולה.gh auth login --web +
gh auth setup-git; gcloud auth login +
gcloud auth application-default login;
gcloud config set project mis26-dev.~/dev, לעולם לא
בתוך Google Drive/OneDrive (הורס .git):mkdir -p ~/dev && cd ~/dev
gh repo clone MIS-Make-It-Simple/sentrix
cd sentrix && git checkout dev
git config user.name "Your Name" && git config user.email "you@mis.org.il"
CI=true pnpm install # ~דקה; CI=true משתיק שאלות אינטראקטיביות
הערת שוליים (NODE_ENV): במחשבים מסוימים ה‑shell מייצא
NODE_ENV=production באופן גלובלי, ואז pnpm מדלג על
devDependencies (יופיע tsx: not found). מריצים
env -u NODE_ENV CI=true pnpm install; האשף והסקריפטים
מגדירים את סביבת ההתקנה הנכונה אוטומטית. פירוט:
dev-environment §16 #1.
cp apps/client-app/.env.example apps/client-app/.env.local
pnpm --filter @sentrix/client-app dev
# browser: http://tzvi-cohen.localhost:3010 (subdomain = tenant, segment = screens)
pnpm --filter @sentrix/mis-core exec prisma generate
DATABASE_URL="postgresql://u:p@localhost:5432/db" PORT=8081 pnpm --filter @sentrix/mis-core dev
curl http://localhost:8081/health # {"status":"ok"}
.env.local ריק זה מצב עבודה תקין (מסכי "not
configured"). כל חמשת השירותים, אחד‑אחד — dev-environment §9.
להריץ שירות מול נתוני ה‑dev האמיתיים. שני טרמינלים. בראשון פותחים מנהרת IAP אל ה‑DB הפרטי דרך ה‑bastion (משאירים אותו רץ):
gcloud compute ssh sentrix-bastion --project=mis26-dev --zone=me-west1-a \
--tunnel-through-iap -- -N -L 5439:10.17.0.3:5432 -o StrictHostKeyChecking=no
בשני מושכים את ה‑credential לקריאה בלבד ומריצים את mis-core מולו —
הוא מדבר עם ה‑DB דרך המנהרה על 127.0.0.1:5439:
export DATABASE_URL="$(gcloud secrets versions access latest \
--secret=SENTRIX_READONLY_DATABASE_URL --project=mis26-dev \
| sed 's#@[^/]*/#@127.0.0.1:5439/#')"
PORT=8081 pnpm --filter @sentrix/mis-core dev
curl http://localhost:8081/trucks | head # שורות אמיתיות מ‑dev
sentrix_readonly קורא בלבד בשתי הסביבות; עריכת נתוני דמו
ב‑dev דורשת את ה‑credential של
MIS_CORE_RUNTIME_DATABASE_URL (DML כן, DDL יסורב), ופרוד
לעולם לא נערך ביד. דרגות הגישה המלאות — פרק 7. 7. טסטים
— pnpm -r test (5,798 טסטים ירוקים בכל הריפו, 20 חבילות).
8. לגעת במערכת החיה — פתחו את /health
ו‑/trucks של שער ה‑dev (טבלת פרק 1): 200 ואז 401. זו המערכת
האמיתית שעונה לכם, וה‑401 הוא האבטחה שעובדת. 9. לקרוא את
AGENTS.md — כללי הזהב המחייבים (אדם ו‑AI).
Windsurf/Cursor קוראים אותו אוטומטית דרך
.windsurfrules/.cursorrules. בלי לקרוא אותו לא
פותחים PR.
feat/<scope>-<short> ──PR──► dev ──פריסה אוטומטית──► סביבת dev
│
└──PR (בשל, ירוק, מאושר)──► main ──► prod
auto-merge. ענף שחי שבועיים הוא לא "יותר
זהיר", הוא שתי מיגרציות שנוצרו מול אותו baseline ואיש לא זוכר את הקוד.
מה שמאפשר את זה: פיצ'ר שהחשיפה שלו היא החלטה עסקית מוזג מאחורי
דגל כבוי — שלב 11.main נעול (ruleset): מיזוג רק ב‑PR ירוק עם אישור
בעלי‑קוד — מורן, מוטי או שניר
(CODEOWNERS).feat:,
fix:, docs:, build:,
chore:, refactor:, test: — קצר,
בזמן הווה, אפשר עם scope (mis-core: add ...).git checkout dev && git pull
git checkout -b feat/weighing-report
# ...work, small commits...
git push -u origin HEAD
gh pr create --base dev
| בדיקה | מה היא תופסת |
|---|---|
ci — סודות + מבנה ריפו |
דפוסי מפתח/סיסמה בקוד; קבצים מחייבים חסרים |
ci — משמעת סכמה |
שינוי ב‑schema.prisma בלי migration (פרק 7) |
| Semgrep (SAST) | קוד לא בטוח — SQL ממחרוזות, קלט לא מאומת |
| Trivy (SCA+IaC) | תלות עם CVE; Dockerfile/Terraform לא מוקשחים |
| gitleaks | סוד שקומט, כולל בהיסטוריה |
תוצאה אדומה: לוחצים על ה‑check, רואים קובץ ושורה, מתקנים ודוחפים.
השתקת ממצא — רק נקודתית ועם נימוק, לפי נספח
ד׳. בדיקה מקומית מוקדמת: pnpm -r test ופקודות הסריקה
ב‑dev-environment §14.
מיזוג ל‑dev מפעיל טריגר פר‑שירות (path filters): רק
השירות שהשתנה נבנה ונפרס — install ⇐ build+test ⇐ Docker ⇐ Cloud Run, ~5
דקות. אותם טריגרים קיימים ל‑prod על main.
infra/cloudbuild/cloudbuild-<service>.yaml פר שירות
(12 שירותים); cloudbuild.yaml המלא נשאר לבנייה ידנית של
הכל. מעקב: GCP ⇐ Cloud Build ⇐ History (ירוק = באוויר).
היגיינת תור (skip-if-superseded): צעד 0 בכל אחד מ‑12 קובצי הפריסה בודק אם כבר ממתין build חדש יותר של אותו טריגר — ואם כן מבטל את עצמו. בהיסטוריה זה נראה CANCELLED (אפור), לא FAILURE (אדום): כשממזגים כמה PRים ברצף זה צפוי ותקין — הקומיט האחרון הוא זה שנפרס. ההחלטה המתועדת: אם ה‑build החדש ביותר נכשל, הישן לא נפרס במקומו — השירות פשוט נשאר על ה‑revision הקודם.
אות אחד כש‑dev נשבר: ריצות CI על PR מקובצות
(concurrency) — push עוקב מבטל ריצה שהתיישנה במקום לתת לה להאדים. על
push ל‑dev/main כל ריצה מקבלת verdict אמיתי, וכישלון על dev מדליק את
.github/workflows/dev-broken.yml: שורת לוג מובנית ל‑Cloud
Logging ומייל Monitoring אחד (מוגבל לאחד בשעה) — במקום מבול "Run
failed". מומלץ לכל מפתח לכבות את מייל ה‑Actions הפר‑ריצה: GitHub ⇐
Settings ⇐ Notifications ⇐ Actions ⇐ להוריד את הסימון מ‑Email.
NEXT_PUBLIC_* — וכל ערך אחר ש‑Next צורב לתוך ה‑bundle
בזמן בנייה — לא יכול להיות משתנה סביבה בזמן ריצה. הוא מגיע
כ‑substitution של הטריגר, ומוצהר
ב‑infra/terraform/cicd.tf. משם שני כללים:
terraform apply הבא מחזיר את מה שכתוב
ב‑cicd.tf, בלי שום diff שיסביר לאן הערך נעלם. משנים
ב‑cicd.tf או שזה זמני.merge → terraform apply → re-run של הטריגר → אימות
דילוג על ה‑re-run משאיר את השינוי בקוד ולא מופעל, וזה נראה בדיוק כמו "מה שעשיתי הלך לאיבוד".
הסדר ההפוך הוא המסוכן: קונפיגורציה שהוחלה מול קוד שעוד לא נפרס. ערך
כמו authDomain שמופנה להוסט שהקוד עוד לא הקים מחזיר את
Firebase ל‑404, כלומר אפס כניסות בפרוד. כשה‑apply תלוי
בקידום, makeprod.py דורש --allow-prod-drift
ואומר את זה במילים.
ועוד מלכודת: terraform plan קורא את ה‑checkout
שעל הדיסק שלך, לא את GitHub. No changes
מיד אחרי מיזוג = כמעט תמיד git pull קודם.
קוד רע שנפרס? מזיזים תנועה ל‑revision הקודם ואז מתקנים בגיט:
gcloud run revisions list --service mis-core --region me-west1 --project mis26-prod
gcloud run services update-traffic mis-core --region me-west1 --project mis26-prod \
--to-revisions <PREVIOUS_REVISION>=100
ואז git revert ל‑commit הרע. שני כללים שמשלימים את
התמונה, ובלעדיהם החזרת תעבורה לא מספיקה:
--to-revisions היא אותה פקודה בשני הכיוונים.הפירוט המלא, כולל החלטות חירום:
docs/conventions/rollback-and-db.md §1+§4.
תרגיל חובה ביום־יומיים הראשונים: שינוי UI זעיר, מהעורך ועד שהוא חי אצל לקוח בסביבת dev. בסופו ראיתם את כל מסלול הפריסה פעם אחת בעיניים — ואין יותר "קסם".
1. השינוי — צבע כפתור בדשבורד הקבלנים. הכפתורים מקבלים צבע מ־classes של ui-kit (פרק 12), אז "לשנות צבע" = להחליף class:
git checkout dev && git pull
git checkout -b feat/first-change-<yourname>
# open apps/client-app/src/app/p/contractors/(dashboard)/page.tsx
# find the button with className "sx-btn sx-btn--primary" (line ~304)
# change it to "sx-btn sx-btn--danger" (brand green -> danger red, from theme.css)
pnpm --filter @sentrix/client-app dev
# check http://tzvi-cohen.localhost:3010 - the button is red
2. קומיט ⇐ דחיפה ⇐ PR ל‑dev:
git add -p && git commit -m "feat(dashboard): first-change exercise - button color"
git push -u origin HEAD
gh pr create --base dev --title "first change exercise" --fill
3. הבדיקות רצות על ה‑PR — ci (סודות,
מבנה, סכמה), gitleaks, Semgrep, Trivy (פרק 5). אין דרישת
review. ci ירוק הוא כל השער, והוא
נדרש (ruleset guard-dev) — כלומר כפתור
המיזוג אפור עד שהוא חוזר ירוק, וזה חוסם ולא נימוס. הדרך המומלצת: ללחוץ
Enable auto-merge ולעזוב; GitHub ימזג לבד. אם
dev זז בינתיים תראו "This branch is out-of-date" — לוחצים
Update branch ומחכים לריצה נוספת, וזה המחיר היומי של
מיזוג מאוחר. רק main שמור ודורש אישור בעל‑קוד (בהמשך הפרק).
4. לצפות בפריסה — Cloud Build. המיזוג מדליק בדיוק
טריגר אחד: deploy-dev-client-app — כי השינוי נגע רק
ב‑apps/client-app/** (טריגר פר שירות עם path filters; שירות
שלא השתנה לא נבנה ולא נפרס). פותחים את Builds בקונסולה (טבלת הקישורים
בפרק 1) ורואים את השלבים חיים: install ⇐ build+test ⇐ Docker ⇐ deploy.
~5 דקות עד ירוק.
5. לראות את זה חי אצל "לקוח": https://holon.app-dev.mis-26.com
⇐ מסך הבית — הכפתור אדום. זה היה deployment אמיתי, מקצה לקצה. עכשיו
החזירו את הצבע (git revert באותו מסלול בדיוק) — ה‑revert
הוא חצי מהתרגיל: לתקן קדימה זה הרגל, לא ידע.
ומה הלאה — המסלול לפרוד. PR מ‑dev ל‑main, ושם הרף גבוה יותר:
ci, gitleaks, Semgrep, Trivy) ירוקות,
והענף חייב להיות מעודכן מול main (מדיניות strict — main זז? מעדכנים
ובודקים שוב).CODEOWNERS — מורן, מוטי או שניר; push
חדש לענף מאפס אישור שניתן (dismiss stale reviews).מיזוג ל‑main פורס אוטומטית לפרוד (deploy-prod-*, אותו
מנגנון בדיוק) — לכן ל‑main מגיע רק מה שכבר חי ב‑dev, ירוק ונצפה
עובד.
זה המדריך המודרך, קליק‑אחרי‑קליק, שמלווה את פרקים 5 ו‑6. פרק 6 מראה את המסלול בקצרה; כאן עוברים אותו לאט עם פיצ'ר אמיתי אחד — מהעורך ועד שהוא חי אצל לקוח ב‑dev, ואז המסלול לפרוד. אחרי שעושים את זה פעם אחת, אין יותר "קסם" בפריסה.
הפיצ'ר לדוגמה: להוסיף אריח KPI בשם "מכולות פעילות" לשורת ה‑KPI בקוקפיט הקבלנים. שינוי אמיתי, קטן ונראה לעין — בדיוק מה שצריך בפיצ'ר ראשון. מי שרוצה גרסת שתי דקות: שינוי תווית בלבד בסוף הפרק. ומי שכבר עבר את התרגיל — הרמה השנייה: מסך שלם מאחורי מודול ופרופיל, הדפוס שבו נכנס כמעט כל פיצ'ר אמיתי היום.
pnpm setup:gui) או
scripts/bootstrap-env.sh רצו בהצלחה (פרק 4) — התלויות
מותקנות ולקוחות Prisma קיימים.dev מעודכן. לעולם לא עובדים ישירות על
dev או main (פרק 5).כל משימה מקבלת ענף משלה מ‑dev מעודכן. שתי דרכים, בחרו
אחת:
# דרך א' — ענף פשוט בתוך ה‑clone הקיים:
cd ~/dev/sentrix && git checkout dev && git pull
git checkout -b feat/dashboard-active-containers-kpi
# דרך ב' — worktree מבודד (מומלץ כשעובדים על כמה דברים במקביל; לא נוגע ב‑clone):
git -C ~/dev/sentrix fetch origin
git -C ~/dev/sentrix worktree add ~/dev/sx-kpi -b feat/dashboard-active-containers-kpi origin/dev
cd ~/dev/sx-kpi
2א. אריח ה‑KPI בקובץ
apps/client-app/src/app/p/contractors/(dashboard)/page.tsx.
שורת ה‑KPI היא ה‑grid הזה, עם חמישה StatCard
מ‑@sentrix/ui-kit (פרק 12):
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">
<StatCard label={t('statActiveTrucks')} value={activeTrucks ?? dash} .../>
{/* ...עוד ארבעה אריחים... */}
</div>
מוסיפים אריח שישי לפני סגירת ה‑</div>, ומרחיבים את
מספר העמודות ל‑6:
<StatCard
label={t('statActiveContainers')}
value={fleet ?? dash}
tone="info"
/>
</div>
fleet ו‑dash כבר קיימים בקומפוננטה, אז זה
מתקמפל מיד; חיווט ספירה אמיתית מ‑mis-core הוא follow‑up. שנו גם
xl:grid-cols-5 ל‑xl:grid-cols-6 בשביל שורה של
שישה. מבנה ה‑props של StatCard: label
ו‑value חובה, והשאר אופציונלי
(of/sub/cta/tone/href).
2ב. הטקסט בארבע שפות. המפתח
statActiveContainers חייב להתקיים תחת מרחב השמות
cockpit בכל ארבעת קבצי ההודעות
packages/shared-i18n/src/messages/{he,en,ar,es}.json (מפתח
חסר בשפה אחת שובר את הבנייה):
// he.json → בתוך "cockpit": { ... }
"statActiveContainers": "מכולות פעילות",
// en.json
"statActiveContainers": "Active containers",
// ar.json
"statActiveContainers": "حاويات نشطة",
// es.json
"statActiveContainers": "Contenedores activos",
env -u NODE_ENV CI=true pnpm install # רק אם הוספת/שינית תלות; כאן לא חובה
pnpm --filter @sentrix/client-app dev
# בדפדפן: http://tzvi-cohen.localhost:3010 → האריח "מכולות פעילות" מופיע בשורת ה‑KPI
pnpm --filter @sentrix/client-app test # טסטים ממוקדים לאפליקציית הלקוחות
pnpm -r test # כל הריפו ירוק לפני push
ירוק מקומי חוסך סבב אדום ב‑CI. אם tzvi-cohen.localhost
לא נפתח — dev-environment §16 #9.
Conventional Commits, באנגלית, בזמן הווה (פרק 5):
git add "apps/client-app/src/app/p/contractors/(dashboard)/page.tsx" \
packages/shared-i18n/src/messages/he.json \
packages/shared-i18n/src/messages/en.json \
packages/shared-i18n/src/messages/ar.json \
packages/shared-i18n/src/messages/es.json
git commit -m "feat(dashboard): add active-containers KPI tile to contractor cockpit"
git push -u origin HEAD
gh pr create --base dev --title "feat(dashboard): active-containers KPI tile" --fill
--base dev הוא קריטי: ה‑PR נפתח מול ענף האינטגרציה, לא
מול main.
ברגע שנפתח PR, ארבע בדיקות רצות עליו אוטומטית. כולן חוסמות מיזוג:
| בדיקה (שם ה‑check) | מה היא בודקת | מה תתפוס |
|---|---|---|
ci |
install + prisma generate + typecheck + test בכל ה‑workspaces, + שומרי מבנה/סודות/סכמה | טסט אדום, טייפ שבור, שינוי schema.prisma בלי
migration |
gitleaks (secrets) |
הריפו וכל ההיסטוריה | מפתח/סיסמה שקומטו בטעות |
semgrep (SAST) |
הקוד שלנו | SQL ממחרוזת, endpoint בלי בדיקת קלט |
trivy (dependencies + IaC) |
תלויות ותשתית | תלות עם CVE, Dockerfile/Terraform לא מוקשח |
בדיקה אדומה: לוחצים על שם ה‑check ב‑PR ⇐ רואים קובץ ושורה מדויקים ⇐
מתקנים ודוחפים שוב (כל push מפעיל את הבדיקות מחדש). מהטרמינל:
gh pr checks --watch. לרוץ מקומית לפני push:
pnpm -r test ופקודות הסריקה ב‑dev-environment §14.
אתם. מיזוג ל‑dev דורש דבר אחד: CI ירוק. אין דרישת
review ואין תפקיד שמאשר — מה שמחזיק את האיכות הוא הבדיקות והשומרים
ב‑scripts/ci/, ולכן בדיקה אדומה היא עצירה מלאה ולא הצעה.
הדרך המומלצת: ללחוץ Enable auto-merge ב‑PR ולעזוב אותו;
GitHub ימזג לבד ברגע שהבדיקות ירוקות, בלי לשמור על הדף.
gh pr merge --auto --squash
רק המיזוג ל‑main שמור ודורש אישור בעל‑קוד (שלב 10).
מה dev באמת אוכף (ruleset
guard-dev):
| הכלל | מה זה אומר לכם |
|---|---|
ci הוא בדיקה נדרשת |
כפתור המיזוג אפור עד שהיא ירוקה. זה לא נימוס, זה חוסם |
| Require branches to be up to date | dev זז? תראו "This branch is out-of-date" ותצטרכו ללחוץ
Update branch ולחכות לריצה נוספת. זו הסיבה למזג ביום
שסיימתם — כל יום שעובר מוסיף עוד סבב |
| Block force pushes + אין מחיקה | גם לא בטעות |
| אין דרישת PR | תיקון תיעוד אפשר לדחוף ישר ל‑dev, בלי ענף |
חריג אחד: מי שהוא Repository admin
נמצא ב‑bypass list ולכן יכול למזג בלי לחכות ל‑CI ולדחוף ישר. זה קיים
בכוונה, להחלטה של שניר, ולא כדי לדלג על בדיקה אדומה — bypass חל על כל
ה‑ruleset, כלומר הוא פותח גם force‑push ל‑dev. מי שמשתמש בו שובר סביבה
לשלושה אנשים כשהוא טועה, ו‑dev-broken.yml יצעק על זה.
למה זה בטוח למזג ביום שסיימתם, גם אם הפיצ'ר לא אושר ללקוחות: כי מיזוג ל‑dev הוא החלטה הנדסית (זה נבנה? הטסטים עוברים?), והשאלה אם לקוח רואה את הפיצ'ר היא החלטה נפרדת של המוצר — דגל פיצ'ר, לא ענף שמחזיקים. ראו שלב 11.
המיזוג ל‑dev מדליק טריגר אחד בלבד:
deploy-dev-client-app — כי השינוי נגע רק
ב‑apps/client-app/** (טריגרים פר‑שירות עם path filters;
שירות שלא השתנה לא נבנה ולא נפרס). איפה רואים: קונסולת GCP ⇐
Cloud Build ⇐ History (פרויקט mis26-dev).
השלבים חיים: install ⇐ build+test ⇐ Docker ⇐ deploy ל‑Cloud Run, בערך 5
דקות עד ירוק.
רואים build שלכם ב‑CANCELLED אפור? זה מנגנון ה‑skip-if-superseded (פרק 5): מוזג אחריכם קומיט חדש יותר לאותו שירות, וה‑build הישן פינה לו את התור. הקוד שלכם נפרס בכל זאת — בתוך ה‑build של הקומיט החדש.
פותחים https://holon.app-dev.mis-26.com (לקוח הדמו) או
https://demo.app-dev.mis-26.com, מזדהים עם Google — האריח
"מכולות פעילות" נמצא עכשיו בשורת ה‑KPI. זה היה deployment אמיתי, מקצה
לקצה, על אותה תשתית שהלקוחות רואים.
כשהשינוי בשל ונצפה עובד ב‑dev, מקדמים אותו לפרוד ב‑PR מ‑dev ל‑main. הרף גבוה יותר:
gh pr create --base main --head dev --title "release: active-containers KPI tile" --fill
אותן ארבע בדיקות (ci, gitleaks,
semgrep, trivy) ירוקות, והענף מעודכן מול main
(מדיניות strict — main זז? מעדכנים ובודקים שוב).
אישור בעל‑קוד לפי CODEOWNERS — מורן, מוטי או שניר;
push חדש מאפס אישור שכבר ניתן.
את כפתור ה‑merge לוחצים שניר, מוטי או מורן. force-push ומחיקת ענף חסומים.
מיזוג ל‑main פורס אוטומטית לפרוד
(deploy-prod-client-app, אותו מנגנון) ⇐
https://app.mis-26.com. rollback מיידי = החזרת revision
ב‑Cloud Run (פרק 5).
אם ה‑PR נגע ב‑infra/api-gateway/*.yaml —
לגלגל את השער של prod. עריכת ה‑YAML לא עושה כלום בפני עצמה, ואף
שלב ב‑CI לא מגלגל אותו. הכישלון נראה בדיוק כמו פיצ'ר שבור: טסטים ירוקים,
CI ירוק, פריסה ירוקה, ומסך שאומר "הטעינה נכשלה" כי השער מחזיר 404 לפני
שהבקשה מגיעה לשירות. הפקודות והאימות המלא:
conventions/git-flow.md §Promotion checklist, שלב 4. האימות
בשורה אחת — נתיב חדש עונה 401, לא 404.
לכן ל‑main מגיע רק מה שכבר חי ב‑dev, ירוק, ונראה עובד בעיניים.
עד כאן הקוד. עכשיו השאלה האמיתית של סוף הספרינט.
שלושה מפתחים עובדים במקביל. כל אחד מסיים משימה, בודק על localhost, ודוחף ל‑dev. אחת לשבועיים מקדמים את כל dev ל‑main וזה מגיע לפרודקשן. אז מה קורה כשבסוף הספרינט המוצר אומר שפיצ'ר X מוכן ללקוחות ופיצ'ר Y לא?
התשובה היא לא "נקדם רק את X". PR מ‑dev ל‑main לוקח את כל מה שנמצא על dev, וזה בכוונה:
packages/contracts-openapi, על השירות ועל הקליינט. קוטפים
חלק מהקומיטים ⇐ החוזה לא תואם למימוש.לכן מצב החשיפה יושב בנתונים. הקוד מתקדם בקצב שלו; מי
רואה אותו זו החלטה נפרדת והפיכה, שמתקבלת בקונסולת האדמין במסך
דגלי פיצ'רים (/feature-flags).
שלושה מפתחים, שלושה מתגים — ולא לבלבל ביניהם:
| המתג | השאלה | של מי | לכמה זמן |
|---|---|---|---|
FeatureFlag |
הקוד בשל להיראות? | מוצר | זמני — נמחק אחרי ההשקה |
TenantModule |
הלקוח קנה את זה? | מכירות | כל עוד הלקוח אצלנו |
CapabilityExclusivity |
הלקוח בכלל רשאי? | חוזה | כל עוד ההתחייבות בתוקף |
פיצ'ר מגיע למסך רק כששלושתם אומרים כן. אסור להשתמש
ב‑TenantModule בתור דגל: הוא שורת החיוב
החודשית (priceOverride,
SubscriptionStatus, ו‑TruckModuleUsage מודד
לפי המפתח שלו), כך שהדלקה שלו כדי שמוצר יבדוק משהו פותחת שורת חיוב,
וכיבוי של פיצ'ר לא בשל נראה בדיוק כמו ביטול מנוי.
איך דגל נפתר:
השורה של הלקוח הזה → השורה הגלובלית (tenantId ריק) → false
סגור כברירת מחדל, בכל שכבה: אין שורה, הטבלה לא מוגרה, השאילתה נכשלה — הכול כבוי. ההיפך, "פתוח כשלא ידוע", היה שולח עבודה לא מאושרת ללקוחות בגלל תקלת DB אחת.
וה‑tenantId שיכול להיות ריק הוא כל התרגיל:
FeatureFlag key=route-eta-v2 tenantId=<demo-sanitation> enabled=true ← מוצר בודק כאן, על פרוד
FeatureFlag key=route-eta-v2 tenantId=NULL enabled=false ← כל לקוח אמיתי
אותו קוד בשני המקומות. זו גם הסיבה שאין סביבת stage (D23): מוצר מאמת על dev כשהדגל דלוק שם וכבוי בפרוד, והדגמה אמיתית רצה על טננט הדגמה בפרודקשן עצמו — תשתית אמיתית, בלי Cloud SQL שלישי.
להוסיף דגל — שתי שורות, באותו PR של הפיצ'ר:
packages/catalog/src/feature-flags.ts ומראה
ב‑services/mis-core/src/lib/feature-flags.ts (שירותים רצים
JS מקומפל ולא מייבאים את חבילת הקטלוג; טסט parity נופל אם השניים
מתפצלים). להצהרה יש שם בארבע שפות, אחראי,
ותאריך מחיקה (retireAfter).if (!(await isFeatureEnabled(prisma, 'route-eta-v2', tenantId))) {
return reply.code(404).send({ error: 'Not found' });
}
מפתח שלא הוצהר זורק שגיאה. טעות הקלדה שהייתה מחזירה
בשקט false פירושה פיצ'ר שלא נדלק לעולם, בזמן שבקונסולה הדגל
מוצג דלוק.
מה ש"כבוי" חייב להיות: הפיצ'ר צריך להיות לא
מזיק, לא שבור. שתי דוגמאות שכבר בקוד: partner-api
כבוי ⇐ כל טוקן שותף נדחה ב‑403 והקוד פרוס ולא משרת אף אחד;
alert-dispatch-email כבוי ⇐ המנוע ממשיך לזהות ולכתוב התראות
(הפעמון, מסך ההתראות והקופיילוט לא מושפעים), והשליחה נדלגת
ונרשמת כשורת NotificationDelivery עם הסיבה
channel-disabled.
שינויי סכמה של פיצ'ר מאחורי דגל הולכים ב‑expand‑contract כרגיל (פרק 7): העמודות מגיעות לפרוד באופן אדיטיבי לפני הקוד שקורא אותן, והדגל מחליט מתי מסתכלים עליהן.
ולמחוק את הדגל. דגל הוא זמני. אחרי שפיצ'ר דלוק לכולם
ונשאר דלוק — מוחקים את ההצהרה ואת קריאות isFeatureEnabled.
הטסט ב‑feature-flags.test.ts נופל ברגע
ש‑retireAfter עובר, כך שדגל נשכח הופך לבילד אדום ולא
ל‑if נצחי שאף אחד לא מעז להוציא. ברירת המחדל: שני
ספרינטים.
וזה החלק הטוב: אם בסוף הספרינט המוצר אומר "לא בשל" — הדגל פשוט נשאר כבוי. אין מה להחזיר אחורה, אין מה להוציא מ‑dev, והספרינט הבא ממשיך מעל מה שכבר מוזג. בלי דגלים, "לא בשל" פירושו revert של קומיטים שבועיים אחרי שנכתבו, בקבצים משותפים, אולי עם מיגרציה — וזה בדיוק מה שדוחף אנשים להחזיק ענפים שבועיים, ומשם מגיעות שתי מיגרציות שנוצרו מול אותו baseline.
שלושה כללים שסוגרים את הנושא, וכולם נובעים מ‑D31:
retireAfter מוכרז יחד עם הדגל, וטסט ב‑mis-core
נכשל ברגע שהתאריך עבר. דגל לא נשאר בקוד לנצח כי מישהו
שכח.PARTNER_API_ENABLED ו‑ALERT_DISPATCH_CHANNELS
נקראים כ‑override מפורש (ערך לא‑ריק מנצח), כי מפסק חירום שדורש מסד תקין
הוא לא מפסק חירום.המוסכמה הקנונית, על כל דוגמאותיה, יושבת
ב‑docs/conventions/feature-flags.md.
אריח KPI הוא תרגיל; פיצ'ר אמיתי ב‑Sentrix הוא כמעט תמיד מסך
שנמכר כמודול: הוא מופיע רק אצל לקוח שקנה אותו, רק בסגמנטים שהוא
רלוונטי להם, ורק למשתמשים שהורשו. כל זה קונפיגורציה בקטלוג — לא תנאים
מפוזרים בקוד. ככה נכנסו לאחרונה P2P (הוכחה‑לתשלום) ו‑NAVIGATION — שני
התקדימים הכי טריים להעתיק מהם
(docs/evidence/2026-07-26/p2p-earthworks.md,
navigation-module.md).
נניח שמכרנו מודול חדש בשם "ביקורת אתרים" (SITE_AUDIT) עם
מסך site-audit בדשבורד הקבלנים. תשעה צעדים, לפי הסדר:
שלב א — שורת קטלוג (המודול נולד). מוסיפים
mod(...) אחד
ב‑packages/catalog/src/modules.ts: מפתח, משפחה, שם בארבע
שפות, מחיר, סגמנטים וסוגי רכב ([] = כל הצי). חובה
לשקף את אותה שורה במראה של mis-core —
services/mis-core/src/modules/admin-tenants/registry.ts;
טסט ה‑parity נכשל על כל סטייה בין השניים, אז אי אפשר לשכוח בטעות.
שלב ב — המסך מצטרף לפרופיל.
ב‑packages/catalog/src/view-profiles.ts מוסיפים את
'site-audit' לרשימת ה‑screens של הפרופילים הרלוונטיים (למשל
waste-contractor), וב‑packages/catalog/src/resolve.ts
מוסיפים שורה ל‑SCREEN_MODULE:
'site-audit': 'SITE_AUDIT' — מעכשיו המסך קיים רק כשהמודול
מזוכה לטננט. התראות של המודול נכנסות באותה צורה
ל‑ALERT_KIND_MODULE.
שלב ג — כניסת הניווט.
PROFILE_SCREEN_NAV
ב‑apps/client-app/src/products/contractors/lib/segment-view.ts
מקבל רשומה: href (/site-audit) ואייקון. התפריט נבנה
מהפרופיל — אין הוספת פריט תפריט ידנית פר‑אפליקציה.
שלב ד — העמוד עצמו, עם שומר URL.
// apps/client-app/src/app/p/contractors/(dashboard)/site-audit/page.tsx
import { requireProfileScreen } from '@/lib/catalog-view';
export default async function SiteAuditPage() {
const view = await requireProfileScreen('site-audit'); // 403/redirect אם אין זכאות
// ...הרכבת המסך מרכיבי ui-kit (פרק 12)...
}
הסתרת פריט התפריט היא נוחות; requireProfileScreen הוא
האכיפה — גם מי שמנחש את ה‑URL לא נכנס בלי זכאות. אם יש גם API חדש
ב‑mis-core, הוא נשמר באותו דפוס:
requireModulePermission('SITE_AUDIT','read') על כל endpoint
(403 MODULE_NOT_ENABLED / MODULE_FORBIDDEN),
והחוזה נולד קודם ב‑packages/contracts-openapi (פרק 11).
שלב ה — טקסטים בארבע שפות. כמו בדוגמת ה‑KPI: כל
מחרוזת חדשה בארבעת קובצי
packages/shared-i18n/src/messages/{he,en,ar,es}.json; מפתח
חסר בשפה אחת שובר את הבנייה (טסט ה‑parity של המפתחות).
שלב ו — טסטים לפני PR.
pnpm --filter @sentrix/catalog test מריץ בין השאר את
segment-matrix.test.ts — טסט הדליפה שמוכיח שהמסך מגיע
בדיוק לתאי (סגמנט × סוג רכב) שהגדרתם ולשום תא אחר; ואז
pnpm -r test לכל הריפו.
שלב ז — לזכות לקוח ולבדוק חי. אחרי המיזוג, במסך
Modules של admin-console מדליקים את SITE_AUDIT
לטננט דמו (פרק 20 שלב 3) — או מוסיפים אותו ל‑seed
(packages/shared-db, בדפוס
seed:demo-segments). ההרשאה פר‑משתמש כבר קיימת: מסך "גישה"
באדמין מציג אוטומטית כל מודול שזוכה לטננט, עם
default/read/write/manage/deny — אין UI חדש לבנות.
שלב ח — PR ⇐ פריסה ⇐ אימות. אותם שלבים 5–9 מהדוגמה
הראשונה. כאן יידלקו שני טריגרים (deploy-dev-client-app +
deploy-dev-mis-core, כי נגעתם גם ב‑registry). בדיקת הקצה:
טננט עם המודול רואה את המסך; טננט בלי — לא רואה אותו בתפריט ומקבל סירוב
על ה‑URL; משתמש עם deny — מקבל סירוב גם כשהטננט זכאי.
שלב ט — להכריז. פופאפ "מה חדש" ניזון מלוח monday
(מודול whats-new ב‑mis-core): מוסיפים שם פריט על הפיצ'ר,
ועמודת הקהל (מערכות (whats-new)) דואגת שהוא יוצג רק לקהלים
הרלוונטיים — הכרזה על מסך פסולת לא תופיע אצל צי הסעות.
פיצ'ר ראשון קטן עוד יותר: לשנות טקסט של אריח קיים. עורכים קובץ אחד,
packages/shared-i18n/src/messages/he.json, תחת
"cockpit":
"statActiveTrucks": "משאיות פעילות כרגע", // במקום "משאיות פעילות"
ואז שלבים 3–9 בדיוק כמו למעלה — בלי שינוי קוד ובלי תלות, רק טקסט. זה עדיין deployment מלא ואמיתי מקצה לקצה, והדרך המהירה ביותר לראות את כל הצינור עובד.
אחרי שסיימת:
git revertלשינוי הוא חצי מהתרגיל. לתקן קדימה זה הרגל, לא ידע. נתקעת במשהו שאין לו תשובה כאן? פתור, ואז הוסף אותו למסמך הזה ב‑PR.
שתי בסיסי נתונים ב‑instance אחד פר סביבה: mis_core
(תפעול) ו‑security_core (ביטחון, מבודד בכוונה). הגישה
מדורגת בשלוש רמות, וכל רמה היא credential נפרד ב‑Secret Manager:
| דרגה | מי משתמש | מותר | אסור (ונחסם טכנית) | הסוד |
|---|---|---|---|---|
| קריאה | בני אדם: Windsurf DB Client, psql, Prisma Studio | SELECT בשני המסדים | כל כתיבה, כל DDL | SENTRIX_READONLY_DATABASE_URL |
| ריצה | השירותים (Cloud Run) | DML מלא (קריאה/כתיבה) | כל DDL | MIS_CORE_RUNTIME_DATABASE_URL,
SECURITY_CORE_RUNTIME_DATABASE_URL |
| מיגרציה | ג'וב ה‑CI בלבד (sentrix-migrate,
security-core-migrate) |
DDL מלא במסד שלו | המסד של השירות השני | MIS_CORE_MIGRATOR_DATABASE_URL,
SECURITY_CORE_MIGRATOR_DATABASE_URL |
זה לא נוהל, זו אכיפה בתוך המסד עצמו (שתי שכבות: הרשאות+בעלות, ו‑event trigger). מי שיפתח את התוסף בווינדסרף — או כל כלי אחר — עם הרשאת מפתח וינסה להוסיף עמודה, למחוק טבלה או ליצור אחת, יקבל בדיוק את זה:
ERROR: DDL is locked on this database (role "sentrix_readonly", command ALTER TABLE).
Schema changes go through code only: edit schema.prisma, open a PR, let CI run
the migration job. See docs/conventions/rollback-and-db.md and db/guardrails/README.md.
הכלי לא "יסכים" — המסד מסרב. המסלול הנכון לשינוי מבנה:
schema.prisma (mis_core:
packages/shared-db, והעתק זהה
ב‑services/mis-core; security:
services/security-core).pnpm prisma migrate dev --name <change>), מקמטים
אותו.SCHEMA-PUSH-OK: <סיבה>
בגוף קומיט).שינוי לא‑שביר תמיד (expand‑contract): מוסיפים, מאכלסים, מחליפים
קריאה, ורק בסוף מוחקים — rollback-and-db.md §2. פעולה
הרסנית = אישור + גיבוי לפני. אסון דאטה = PITR (§3).
למסדים אין כתובת אינטרנט. הדרך היחידה מהלפטופ: מנהרת IAP דרך
sentrix-bastion. פורטים מוסכמים: 5439 — עיון יומי ב‑dev;
5438/5440 — סקריפט ה‑guardrails dev/prod.
Open a tunnel (leave the terminal running):
gcloud compute ssh sentrix-bastion --project=mis26-dev --zone=me-west1-a \
--tunnel-through-iap -- -N -L 5439:10.17.0.3:5432 -o StrictHostKeyChecking=no
Get the read-only credential (username sentrix_readonly + password from):
gcloud secrets versions access latest --secret=SENTRIX_READONLY_DATABASE_URL --project=mis26-dev
Connect — Windsurf Database Client / psql / Prisma
Studio על 127.0.0.1:5439, database mis_core או
security_core. המתכון המלא עם צילומי מסך של התוסף:
dev-environment §10.
כללי הדאטה: צפייה — עם sentrix_readonly, בשתי הסביבות.
תיקון נתוני דמו — dev בלבד, במודע, עם ה‑credential של
MIS_CORE_RUNTIME_DATABASE_URL (DML כן, DDL יסורב). prod —
לעולם אין עריכה ידנית, גם לא של שורה אחת.
./scripts/db-studio.sh פותח מנהרה+Studio בפקודה אחת. המודל
המלא של הדרגות, ה‑cutover והאכיפה:
db/guardrails/README.md.
שלוש שכבות הגנה על הדאטה, מהטעות הקטנה ועד אובדן ספק ענן:
| שכבה | מה היא | איפה חיה | מגינה מפני |
|---|---|---|---|
| PITR | transaction logs — שחזור לכל דקה ב‑7 הימים האחרונים | Cloud SQL, שתי הסביבות | נזק לוגי ("מחקתי את הטבלה ב‑10:31") |
| גיבוי יומי | snapshot אוטומטי 02:00 UTC, נשמרים 7 אחרונים | multi‑region eu — שורד גם אובדן מלא של
me-west1 |
אובדן instance או region |
| Offsite | dump מוצפן age — שבועי ל‑bucket חוץ‑אזורי (פעיל) + רגל S3 (ממתינה לסוד) | GCS europe-west1 / AWS מחוץ ל‑Google | אובדן region, ספק או חשבון GCP |
איפה רואים אותם: Cloud SQL ⇐ sentrix-pg ⇐ Backups (קישור בטבלת פרק 1).
שכבת ה‑offsite: ג'וב db-backup מצפין כל
dump עם age ומריץ שתי רגליים: גיבוי שבועי חוץ‑אזורי ל‑GCS (bucket
*-dr-backups-euw ב‑europe-west1 — פעיל בשתי הסביבות), ורגל
S3 (AWS) שנדלקת ברגע שהסוד AWS_BACKUP_CREDENTIALS מאוכלס.
עד אז רגל ה‑S3 מדלגת ומדווחת בלוג — הפעלתה היא הוספת סוד, לא שינוי קוד.
מפתח הפענוח שמור מחוץ לענן (אצל שניר); בלעדיו קובץ הגיבוי הוא רעש חסר
ערך.
שחזור — שלושת התרחישים (לתאם עם שניר, מורן או מוטי לפני נגיעה בפרוד):
# 1. logical damage -> PITR clone to one minute BEFORE the mistake:
gcloud sql instances clone sentrix-pg sentrix-pg-restore \
--point-in-time="2026-07-24T10:30:00Z" --project=mis26-prod
# validate on the clone, then repoint the *_DATABASE_URL secrets to it (new version)
# 2. instance/region loss -> restore a daily backup onto a fresh instance:
gcloud sql backups list --instance=sentrix-pg --project=mis26-prod
# 3. provider loss -> pull from S3, decrypt, load:
# aws s3 cp s3://<offsite-bucket>/<latest>.dump.age .
# age -d -i <offline-key> <latest>.dump.age > latest.dump && pg_restore ...
תרגול שחזור חודשי (חובה, לא רשות): פעם בחודש משחזרים
גיבוי אמיתי ל‑instance זמני ב‑dev, בודקים שפיות (ספירת שורות בטבלאות
מרכזיות מול המקור, חיבור שירות ו‑/health ירוק), מתעדים את
התוצאה ומוחקים את ה‑instance. גיבוי שמעולם לא שוחזר הוא תקווה, לא
גיבוי.
אובדן region (me-west1): הגיבויים ב‑eu
שורדים. משחזרים ל‑instance ב‑region אחר, terraform apply עם
override ל‑region, והטריגרים פורסים את השירותים מחדש — הכל קוד, אין
קונפיגורציה ידנית לשחזר. אובדן ספק: הקוד ב‑GitHub,
הדאטה ב‑S3 מוצפן, התשתית כ‑Terraform — מקימים מחדש אצל ספק אחר מהעותקים
החיצוניים.
הפקודות המלאות, שמות ה‑buckets והתיעוד מוטמעים כאן בנספח ו׳ — פקודות מלאות, שמות buckets ותיעוד תרגולים. הפרק הזה והרנבוק מיושרים לאותו מודל שלוש־השכבות; בפרטי ביצוע — הרנבוק קובע.
.env מקומט, לא
בצ'אט. gitleaks חוסם קומיט שמפר את זה.--set-secrets ב‑cloudbuild).infra/env/,
לא סוד.docs/conventions/how-to-secrets.md.sentrix-vpc):
השירותים, המסד, ה‑bastion. שירות Cloud Run נפרס תמיד
--no-allow-unauthenticated בתוך ה‑VPC./health
פתוח בכוונה; /trucks בלי טוקן = 401./_functions/* — חוזה ה‑Wix של המכשירים בשטח, אחד‑לאחד
(services/mis-core/src/modules/ingest/ +
MAPPING-NOTES.md).גלגול קונפיג של השער: קונפיגים הם immutable. שינוי = קונפיג חדש (v2, v3...) ועדכון ה‑gateway אליו — אין עריכה של קיים:
gcloud api-gateway api-configs create v<N> --api=sentrix-api \
--openapi-spec=gateway-openapi-dev.yaml --project=mis26-dev \
--backend-auth-service-account=sa-api-gateway@mis26-dev.iam.gserviceaccount.com
gcloud api-gateway gateways update sentrix-gw --api=sentrix-api \
--api-config=v<N> --location=<region> --project=mis26-dev
המוקש המתועד: x-google-backend.address
ו‑jwt_audience חייבים להיות בדיוק כתובת ה‑Cloud Run של
mis-core — לוודא אחרי כל פריסת mis-core. הכל
ב‑infra/api-gateway/README.md.
המוקש שצריך להכיר: נתיב חדש נוסף ל‑YAML, הטסטים עוברים, ה‑CI עובר, הפריסה עוברת — ואם הקונפיג לא גולגל, השער מחזיר 404 לפני שהבקשה מגיעה לשירות והמסך אומר "נכשל".
route-contract-drift.test.tsמשווה את הקובץ לטבלת הראוטים ולא יכול לדעת אם הקונפיג גולגל — אין בדיקה אוטומטית על זה. אחרי כל גלגול תריץ את אימות ה‑HTTP: נתיב חדש חייב לענות 401 או 405, לעולם לא 404. פירוט מלא ב‑פרק 27.3.
Tenant = לקוח (קבלן/רשות) עם subdomain משלו.
התעודה היא wildcard, ולכן אין DNS ואין תעודה פר לקוח, וגם אין
ניתוב פר לקוח: apps/client-app הוא
ה‑default_service של ה‑url map, ולכן כל label מגיע אליו בלי
כלל host, והוא בוחר את סט המסכים לפי הסגמנט של הטננט. הכתובת חיה ברגע
שהשורה קיימת. Account = תת‑ארגון בתוך tenant (רשויות
תחת mgroup). Brand = white‑label (mis/mgroup).
Module = יכולת שנמכרת בנפרד (WEIGHING, VIDEO,
NAVIGATION, P2P, משפחת CARGO...), TenantModule =
המתג+המחיר פר לקוח. בידוד: כל שורה עם tenantId
(ו‑accountId אופציונלי); לקוח שדורש מסד פיזי נפרד —
Tenant.isolation = DEDICATED_DB +
databaseSecret.
הסגמנט הוא מה שקובע מוצר.
segmentKey על הטננט נקרא בזמן ריצה
(GET /tenants/by-host/{label}), ופרופיל התצוגה שלו
ב‑packages/catalog אומר לאיזה shell הוא שייך:
authorities⇐מסכי רשות, security⇐חדר בקרה, כל
השאר⇐ המוצר הקבלני/צי שנגזר מהפרופיל (אוטובוס, מיכלית, מכולות, מכרה —
אותו קוד, פרופיל אחר). טננט בלי סגמנט מקבל 503 ולא מוצר
מנוחש; זה מכוון, כי ניחוש מוצר עלול להגיש לרשות קוקפיט של קבלן.
הקטלוג הוא מקור‑האמת של המוצר:
packages/catalog — 37 מודולים, 32 סוגי רכב, 11 סגמנטים
ופרופילי מסכים פר סגמנט. מה שהלקוח רואה נגזר מ(זכאות טננט × סוג רכב ×
פרופיל × הרשאת משתמש); טסט מטריצה מוכיח שאף מסך לא דולף לסגמנט לא לו.
איך מוסיפים מודול ומסך — הרמה השנייה בפרק
הצמוד.
קליטת לקוח (אשף בן 5 צעדים בקונסולת האדמין): לקוח ⇐ רשויות‑משנה ⇐ מודולים שנקנו ⇐ הזמנת המשתמש הראשון ⇐ מסך סיכום. כל חמשת הצעדים בקונסולה, ואין שלב טרהפורם: מסך הסיכום מדווח שלוש עובדות — שהסלאג לא שמור לשירות אחר, ש‑mis-core מזהה את הכתובת, ושלטננט יש סגמנט — וכל אחת מהן מתוקנת מהקונסולה. הפירוט בפרק 20. ההוראה המבצעית, שורה‑שורה: פרק 26. המודל של טננט/מוצר/דומיין: נספח ב׳.
משתמשים ואישורם (D25): נרשמים עם מייל אמיתי
(אימייל+סיסמה / Google / Apple, MFA זמין) ⇐ אימות מייל ⇐ PENDING: מסך
המתנה בלבד וה‑API מחזיר 403 ⇐ אנחנו מאשרים ב‑admin-console ובוחרים role
⇐ ACTIVE. אפשר לחסום (DISABLED). האכיפה בשלוש שכבות: שער (חתימת טוקן) ⇐
שירות (approval gate + role) ⇐ שאילתה (tenantId).
הרשאות: מודל data-driven — roles+permissions
במסד, claims בטוקן, אכיפה בשירות (לעולם לא רק ב‑UI). הוספת
role/permission: docs/conventions/how-to-permissions.md.
מודולים ניתנים גם לאישור פר‑משתמש: מסך "גישה" באדמין
מציג כל מודול של הטננט עם default/read/write/manage/deny — מודול חדש
שמזוכה לטננט מופיע שם אוטומטית.
endpoint חדש — contract-first: קודם
packages/contracts-openapi, ואז מימוש; REST ו‑MCP נגזרים
מאותו חוזה. אסור endpoint גנרי שמקבל שם טבלה מהלקוח (החור של Wix).
מדריך: docs/conventions/how-to-add-endpoint.md.
packages/ui-kit — ספריית הרכיבים המשותפת
(React + Tailwind + shadcn-style): טבלאות, גרפים, כרטיסים, app-shell,
מסך התראות. מסך חדש = הרכבה של רכיבי ui-kit, לא בנייה מחדש של
כפתור.packages/ui-kit/src/theme.css — מחווט
לכל האפליקציות: צבעים, טיפוגרפיה ומשתני עיצוב במקום אחד. שינוי מראה =
variant/prop ב‑ui-kit, פעם אחת. הרפרנס העיצובי:
docs/DESIGN-SPEC.md + המוקאפים בדרייב.packages/shared-i18n בארבע שפות he/en/ar/es, RTL/LTR דרך
CSS לוגי. מדריך: how-to-i18n.md.http://tzvi-cohen.localhost:3010.packages/ui-kit/src/brand.tsx ומוחל בכל
האפליקציות (sidebar, מסכי כניסה, favicon, מסך טעינה); מותג mgroup שומר
על ה‑wordmark שלו (white-label).?tour=<id> מריץ סיור
מחדש, וסיור פתיחה רץ בכניסה הראשונה.Vitest בכל החבילות, ובנוסף שני מקרי קצה שרצים אחרת ומתועדים למטה. אין טסט שדורש מסד: מי שנוגע ב‑Prisma עושה זאת מול mock, כדי ששורת פקודה אחת תריץ הכל בכל מחשב.
pnpm -r test # הכל
pnpm --filter @sentrix/mis-core test # חבילה אחת
pnpm --filter @sentrix/mis-core test -- fleet # קובץ/שם לפי חיפוש
node --test scripts/__tests__/build-partner-spec.test.mjs # ראו "מחוץ ל-vitest"
5,798 טסטים ירוקים ב‑468 קובצי טסט, ב‑20 חבילות,
ועוד 13 טסטים שרצים ב‑node --test. חמישה טסטים מדולגים,
וכולם אותו מקרה אחד: מסלול ה‑PDF, שדורש Chromium אמיתי (למטה).
| חבילה | קבצים | טסטים |
|---|---|---|
services/mis-core |
177 | 2,156 |
apps/client-app |
129 | 1,421 |
services/security-core |
24 | 373 |
apps/admin-console |
28 | 315 |
packages/api-client |
1 | 208 |
packages/catalog |
10 | 206 |
packages/ui-kit |
18 | 199 |
services/alerts-engine |
13 | 140 |
services/report-engine |
12 | 130 (5 מדולגים) |
packages/shared-db |
4 | 120 |
services/notifications |
12 | 95 |
ingestion/connectors |
10 | 93 |
apps/exchange |
8 | 90 |
services/ai-agents |
10 | 64 |
services/mcp-wrapper |
5 | 58 |
packages/shared-i18n |
1 | 49 |
services/db-backup |
2 | 36 |
packages/shared-auth |
1 | 24 |
apps/partner-portal |
3 | 20 |
packages/notifications |
1 | 6 |
1. טסטי יחידה ולוגיקה — הרוב המכריע. חוק אחד: באג שתוקן מקבל טסט שמוכיח שהוא לא יחזור, באותו PR.
2. טסטי ראוט — קוראים ל‑endpoint עם Prisma מוקאפ ובודקים גם את מה שהוא לא מחזיר: שורה של טננט אחר, שדה סוד, מסך בלי entitlement.
3. טסטי חוזה (drift) — הסוג שהחליף ביקורות ידניות. שלושה כאלה:
| טסט | מה הוא משווה | מה הוא תופס |
|---|---|---|
packages/api-client/…/contract-drift.test.ts |
הטיפוסים הכתובים ביד מול openapi.yaml |
סכימה שהשירות שינה והחוזה לא. 114 סכימות מושוות, כל חריגה דורשת נימוק בקוד, וחריגה שהתיישנה נכשלת עד שמוחקים אותה |
services/mis-core/…/route-contract-drift.test.ts |
טבלת הראוטים מול ה‑YAML של השער | ראוט שנרשם בקוד ואין לו נתיב בשער (או ההפך) |
services/security-core/…/route-contract-drift.test.ts |
אותו דבר, לשירות הביטחון | אותו דבר |
4. טסטי מראה (parity) — כל מקום שבו אותה עובדה יושבת פעמיים מקבל טסט שמכריח את שני העותקים להסכים. זה מה שקיים היום:
| טסט | שני העותקים |
|---|---|
admin-tenants/…/catalog-parity |
packages/catalog ⟷ מרשם המודולים ב‑mis-core |
packages/catalog/…/seed-parity |
packages/catalog ⟷ סקריפטי הזריעה |
jobs/…/demo-data-parity |
נתוני הדמו ב‑shared-db ⟷ המראה שלהם ב‑mis-core |
alerts-engine/engine/…/applicability |
טבלת ההתאמה של ההתראות ⟷ הקטלוג |
usage/…/rates.tfvars-parity |
טבלת התעריפים בקוד ⟷ ה‑tfvars |
weighing/…/waste-catalog-parity |
קטלוג סוגי הפסולת ⟷ הקנוניקליזציה |
containers/…/category-parity |
קטגוריות המכולות ⟷ הקטלוג |
partner-api/…/manifest-parity |
מניפסט השותפים ⟷ ה‑spec הנגזר |
security-core/…/internal-claims-parity |
ה‑claims שה‑BFF חותם ⟷ מה שהשירות מאמת |
admin-console/…/feature-flag-mirror |
דגלי הפיצ'רים בקונסולה ⟷ המקור |
client-app/components/shell-parity |
מעטפת האפליקציה, שלא תתפצל שוב בין המוצרים |
5. טסטי כיסוי־בכוח — לא בודקים התנהגות אלא שאין חור:
mis-core/lib/…/gate-coverage — קורא את טבלת הראוטים
מ‑app.ts ונכשל אם prefix כלשהו לא קיבל החלטה מכוונת: או
מאחורי שער האישור, או ברשימת ההיתר עם נימוק ועם שומר שקיים
בקוד. חמישה prefixes חמקו בשני חודשים לפני שהטסט נכתב; מאז
הוספת prefix בלי לגעת באחת הרשימות היא ריצת CI אדומה, לא ממצא לביקורת
הבאה.i18n-parity (ב‑client-app, exchange ו‑partner-portal) —
כל מפתח קיים בכל ארבע השפות, וה‑placeholders של ICU זהים בכולן. תרגום
חסר או {count} שנפל בעברית נכשל בבנייה, לא אצל הלקוח.camera-health/…/coverage — כיסוי סריקת המצלמות.6. טסטי רכיב (React) — @testing-library
ב‑client-app, admin-console,
ui-kit, exchange
ו‑partner-portal. מרנדרים מסך ובודקים מה נראה: תווית בשפה
הנכונה, טבלה שממוינת כמו שהוכרז, מסך שלא מציג פאנל של אוצר מילים שלא
שייך לסגמנט הזה.
7. טסטי כלל־ההתראה — לכל משפחת התראה יש קובץ משלה
(alerts-engine/src/rules/__tests__/): דלק, סרק, עבודה
פרטית, פקיעת רישיון, מטען. כל אחד מוכיח שהכלל נדלק על הקלט שאמור להדליק
אותו, ולא נדלק על מה שדומה לו.
node --test על spec השותפים. 13 טסטים
ב‑scripts/__tests__/build-partner-spec.test.mjs. הם מריצים
את הבנייה של openapi.partner.yaml מהחוזה המלא ובודקים:
שהקובץ שבריפו זהה לרינדור טרי, שאין משטח פנימי שדולף לשותפים, שלכל
operation יש קבוצה, scope ותיאור, ושאין מפתחות נתיב
כפולים בחוזה. הכפילות היא הטסט שהכי שווה להכיר: YAML שומר את
המפתח האחרון בשקט, ולכן נתיב שהוגדר פעמיים יכול לאבד הגדרה שלמה ועדיין
להיפרסר בהצלחה.
node scripts/build-partner-spec.mjs # רינדור מחדש
node --test scripts/__tests__/build-partner-spec.test.mjs # הטסטים
מסלול ה‑PDF.
services/report-engine/src/__tests__/pdf.integration.test.ts
מרנדר PDF אמיתי, ולכן הוא צריך Chromium אמיתי. בלי דפדפן במחשב הוא
מדלג (חמשת המדולגים בטבלה למעלה) — אבל ב‑CI הוא
נכשל בכוונה אם לא נמצא דפדפן, כי "דילגנו" בצינור אוטומטי נקרא
כמו "עבר". CI מתקין Chromium בשלב נפרד. מקומית:
PDF_CHROMIUM_PATH=/path/to/chrome pnpm --filter @sentrix/report-engine test
בכל PR רצים שבעה שומרי סף לפני שמישהו מתקין חבילה — הם זולים, ולכן הם ראשונים:
| שומר | נכשל אם |
|---|---|
| אין סודות בקומיט | diff מכיל מפתח, טוקן, סיסמת RTSP או connection string |
| חוקיות הריפו | מבנה שהוכרז ב‑AGENTS.md נשבר |
| אין נקודות במפתחות i18n | מפתח הודעה מכיל . (שובר את הקינון) |
| spec השער בר‑פריסה | ה‑YAML של השער לא נטען |
| מראת ה‑Prisma מסונכרנת | services/mis-core/prisma/schema.prisma נבדל
מהקנוני |
| חוזה השותפים מסונכרן ומתואר | openapi.partner.yaml לא זהה לרינדור טרי |
| שינוי סכימה נושא migration | schema.prisma השתנה בלי artifact של migration |
אחריהם: התקנה עם lockfile קפוא, prisma generate (מסודר,
כדי ששני תהליכים לא ידרכו זה על ה‑client של זה),
typecheck ו‑test — שניהם רק על החבילות
שהשתנו ועל מי שתלוי בהן. במקביל רצה workflow אבטחה נפרדת: Semgrep
(SAST), Trivy (תלויות + IaC) ו‑gitleaks (סודות), עם סריקה שבועית מלאה
בנוסף ל‑PR, וראיות שנשמרות 400 יום.
קומיט של תיעוד בלבד מדלג על התקנה/typecheck/test במכוון — עץ הקוד זהה להורה, שכבר עבר. שומרי הסף רצים בכל מקרה.
מ‑AGENTS.md: מתקמפל, טסטים ירוקים, i18n בארבע שפות,
tenant scoping נאכף בשירות, אפס סודות ב‑diff, התיעוד עודכן, ונפרס נקי
ל‑dev. endpoint חדש = חוזה קודם, ואז טסט יחידה + טסט חוזה, באותו PR.
אפשר לחיות רק מהטרמינל (pnpm -r test,
gh pr checks --watch), אבל נוח יותר לראות טסטים וריצות CI
בתוך העורך. Windsurf מושך תוספים מ‑Open VSX (לא מחנות מיקרוסופט); שני
התוספים שלמטה קיימים שם ואומתו. הריפו כולל
.vscode/extensions.json עם ההמלצות — Windsurf יציע להתקין
אותן בפתיחה הראשונה של התיקייה.
Run the Vitest suites from the editor (extension id:
vitest.explorer):
Cmd+Shift+X /
Ctrl+Shift+X) → search Vitest (publisher:
vitest) → Install.vitest.config.ts in the monorepo
automatically.זה בדיוק pnpm -r test, רק עם עץ טסטים, ריצה פר‑טסט
וכישלון שמסומן על השורה. הרצת חבילה בודדת מהטרמינל נשארת כשהייתה (פסקה
ראשונה בפרק).
See CI runs in-editor (extension id:
GitHub.vscode-github-actions):
מי שמעדיף בלי תוסף: gh pr checks --watch בטרמינל (פרק 5)
נותן את אותו מידע. שאר התוספים המומלצים (Prisma, Database Client,
Terraform): dev-environment §7.
הג'ובים התפעוליים (ירושה מה‑cron של Wix) חיים כ‑endpoints תחת
/jobs/* ב‑mis-core
(services/mis-core/src/modules/jobs/). Cloud Scheduler קורא
להם בשעות קבועות (UTC — בכוונה, כמו ב‑Wix) עם שתי שכבות אימות: OIDC של
SA ייעודי + header x-jobs-secret מול הסוד
JOBS_SHARED_SECRET. הג'ובים פעילים בשתי הסביבות: דוח יומי,
אתר לא‑מקוון, משאית בפנים >2 שעות, משקל חורג, Ituran pull,
usage-rollup, תחזוקת פרטישנים (חודשי), הפקת דפי חשבון
(statement-generate, חודשי), משיכת אנומליות מהאגם (lake-anomalies-pull —
הופכת שורת mart להתראת FUEL_THEFT בפעמון) וגיבוי לילי. ל‑alerts-engine
יש שלושה ג'ובים משלו ב‑dev (license-expiry, cargo-eod, fuel-loss) —
מוגדרים ב‑infra/terraform/alerts-engine.tf. ג'וב חדש =
endpoint תחת /jobs/* + טסטים + הגדרה ב‑Terraform
(infra/terraform/scheduler.tf).
infra/terraform/observability.tf +
infra/terraform/dashboards/sentrix-overview.json
(Monitoring ⇐ Dashboards).min-instances=1 — צרכן pull של Pub/Sub לא שואב
הודעות כשהשירות ב‑zero (ההחלטה מתועדת
ב‑docs/evidence/2026-07-26/dev-green.md).docs/SCALE-PLAN.md (פרטישונים, retention, מתי read
replica). כלכלת לקוח: docs/COST-PER-TENANT.md +
docs/COSTS.md + docs/AI-PRICING.md.mis26-platform (ה‑Cloud SQL של ramzor — מערכת
חיה, מחוץ לתחום) או באתר השיווקי (GoDaddy→Vercel).tenantId.SECURITY-BASELINE.md).הפרק הזה הוא מפה, לא רשימת קריאה. מסמכי העומק מצורפים במלואם כנספחים א׳–י״ב בסוף המסמך, ולכן אין כאן שום דבר שחייבים לצאת בשבילו. מה שהרשימה למטה נותנת היא איפה כל עובדה חיה בריפו — כדי שתדעו לאן ללכת כשצריך לתקן אותה, ולא כדי להשלים ידע חסר.
בשורש: AGENTS.md — כללי הזהב המחייבים
(לקרוא ראשון) · README.md — מפת הריפו.
מוסכמות עבודה (docs/conventions/):
dev-environment.md — הקמת מחשב, הרצה מקומית, תוספי
Windsurf, תקלות נפוצות · git-flow.md — ענפים ו‑PR ·
environments.md — שתי הסביבות ·
rollback-and-db.md — שינויי DB בטוחים (expand‑contract)
ו‑rollback · how-to-add-endpoint.md ·
how-to-add-table.md · how-to-secrets.md ·
how-to-i18n.md · how-to-permissions.md ·
how-to-frontend.md · gcp-console-guide.md —
איפה כל דבר בקונסולה.
ארכיטקטורה והחלטות:
ARCHITECTURE-COMPLETE.md · PROJECT-MAP.md ·
DECISIONS.md (D1–D30) · tenancy-and-domains.md
— המודל של טננט/מוצר/דומיין · ONE-CLIENT-APP.md —
אפליקציית הלקוחות האחת: למה, איך נבחר סט המסכים, סדר
ה‑apply, ומה מחכה לאישור · RUNTIME-TENANT-RESOLUTION.md —
זיהוי הלקוח מה‑DB בזמן ריצה · VISION-END-STATE.md ·
docs/architecture/target-architecture.html.
תפעול ותשתית: db/guardrails/README.md —
דרגות הגישה למסד וחוק ה‑DDL · infra/README.md +
infra/terraform/README.md — עבודה עם Terraform (state
ב‑GCS, workspaces) · infra/api-gateway/README.md — גלגול
קונפיג השער · infra/cloudscheduler.tf-snippet.md — הג'ובים
· MIGRATION-RUNBOOK.md — קאט‑אובר מ‑Wix (dual-write,
קריטריוני יציאה) · SCALE-PLAN.md — גדילה ·
DATA-LAKE.md — האגם.
אבטחה: SECURITY.md — מדיניות ו‑SLA ·
SECURITY-BASELINE.md — טריאז' הסריקות ·
ISO-secure-dev-tooling-he.md — מסמך הסוקר (ISO
27001/17/18/701).
עלות ומוצר: COSTS.md ·
COST-PER-TENANT.md · AI-PRICING.md ·
CAPABILITY-MATRIX.md · WORK-PLAN.md — התוכנית
שלב‑שלב · TOUR.md.
מורשת (רפרנס בלבד): legacy-mainmis.md,
legacy-mgroupwix.md, legacy-missecurity.md,
legacy-mis-oldsite.md, SCHEMA-GAPS.md, ותיקיית
archive/ (קוד ה‑Wix הקפוא — קריאה בלבד, לא מיובא).
למידה למי שחדש בזה:
LEARN-branches-and-environments.md ·
LEARN-config-driven.md.
mis26-dev לניסויים,
mis26-prod ללקוחות). ההפרדה היא ההבדל בין חזרה להצגה.infra/terraform/): אפשר לשחזר הכל מאפס, ואין "קליק נסתר"
שאף אחד לא זוכר.schema.prisma הוא תוכנית הבניין, migration הוא היתר שיפוץ
מתועד שרץ זהה בכל סביבה.העמקה בקצב שלכם: LEARN-branches-and-environments.md,
ARCHITECTURE-COMPLETE.md §2 (מילון רכיבי הענן עם אנלוגיה
לכל אחד).
הפרק הזה למי שמגיע משלושת אתרי ה‑Wix שלנו. המטרה: להחליף את המודל
המנטלי, לא רק את התחביר. הניתוחים המלאים של הקוד הישן:
docs/legacy-*.md.
באתרי ה‑Wix הקוד לא שמר נתונים בתוך Wix עצמו — הוא דיבר עם
מסד חיצוני משותף, externalMISDB (בפועל
ramzor ו‑priority), דרך "external database
adapter" שהיעד שלו מוגדר בלוח הבקרה של Wix ולא ברפו. שם יושבות שנים של
נתוני אמת.
המסד הזה לא נעלם בקאט‑אובר — וזו נקודת המפתח. הוא נשאר מקור‑האמת לנתונים ההיסטוריים עד שהמעבר מסתיים. מה שקורה בפועל:
ramzor/priority ומעתיק את הרשומות לסכמה
שלנו, מנורמל ומתויג ב‑tenantId. ההיסטוריה זורמת פנימה בלי
שאף מכשיר מרגיש.MIGRATION-RUNBOOK).כלומר "עברנו ל‑Sentrix" לא אומר "מחקנו את הישן ביום אחד", אלא "בנינו סכמה חדשה, זרמנו אליה את הישן דרך גשר, אימתנו — ואז החלפנו את מקור‑האמת".
לפני (Wix) — קריאה ישירה מהמסד החיצוני המשותף:
// backend/http-functions.js (Wix/Velo) — טוקן פר קולקציה, שם קולקציה מהלקוח
import { getRamzorData } from 'backend/DataQueryFromCollections.jsw';
export function get_getRamzorData(request) {
const token = request.headers['auth']; // secret === '<collection>Token'
return getRamzorData(request.query.dataCollection, request.query.filter);
}
אחרי (Sentrix) — גשר ייעודי שמעתיק לסכמה שלנו, מתויג לקוח:
// ingestion/connectors/ramzor-puller.ts (רץ בזמן dual-write בלבד)
const rows = await legacyRamzor.fetchSince(cursor); // קורא מהמסד הישן
for (const r of rows) {
await prisma.weighing.upsert({
where: { legacyId: r.id1 },
create: { legacyId: r.id1, tenantId, net: r.fnet, at: parseWix(r.date, r.time) },
update: { net: r.fnet },
}); // מעתיק לסכמה החדשה
}
ההבדל המהותי: אין יותר "תן לי קולקציה כלשהי לפי שם ממחרוזת". יש מודל
מוגדר, מתויג ב‑tenantId, שהגשר ממלא — והמסד הישן חי עד
הקאט‑אובר.
לפני (Wix): שכבה גנרית אחת
(DataQueryFromCollections.jsw, 5,377 שורות) שקיבלה שם
קולקציה + filter מהלקוח:
import wixData from 'wix-data';
// כל צרכן שלח dataCollection + fieldKey + condition + keysValue
const res = await wixData.query('truckCollection')
.eq('contractor', contractorName)
.find(); // בלי בידוד לקוח מובנה — הכל באותה קולקציה
אחרי (Sentrix): endpoint שנולד מחוזה
(packages/contracts-openapi), ומאחוריו Prisma — תמיד עם
where: { tenantId }:
// services/mis-core/src/modules/fleet/routes.ts
app.get('/trucks', async (req) => {
const { tenantId } = req.auth; // מהטוקן, לא מהלקוח
return prisma.truck.findMany({
where: { tenantId, contractor: req.query.contractor },
});
});
הכלל: אסור endpoint גנרי שמקבל שם טבלה מהלקוח,
ואסורה שאילתה בלי tenantId (פרק 11). זה בדיוק החור של Wix —
סגרנו אותו.
לפני (Wix):
post_externalTruckDataCollection
ב‑http-functions.js — insert גנרי, אימות בהשוואת מחרוזת
טוקן, כתיבה למסד החיצוני בלי ולידציה:
export function post_externalTruckDataCollection(request) {
const token = request.headers['auth'];
if (token !== secretFor('truckDataCollection')) return forbidden();
return request.body.json().then((rec) =>
wixData.insert('externalMISDB/truckDataCollection', rec)); // בלי סכמה
}
אחרי (Sentrix): אותה כתובת
/_functions/* בדיוק (המכשירים לא מרגישים), אבל עם סכמה,
ולידציה, בידוד לקוח, וזרימה לאגם:
// services/mis-core/src/modules/ingest/truck-lift.ts
app.post('/_functions/externalTruckDataCollection', async (req) => {
const rec = TruckLiftSchema.parse(req.body); // Zod — קלט לא תקין נדחה
const row = await prisma.truckLift.create({
data: { ...rec, tenantId: req.tenant.id }, // מתויג לקוח
});
await lake.publish('truck.lift', row); // Pub/Sub → BigQuery
return { ok: true, id: row.id };
});
לפני (Wix): hook סינכרוני על הקולקציה שהריץ התראות רחוב בתוך ה‑insert:
// backend/data.js (Wix) — רץ בתוך הכתיבה, קשה לבדיקה, מפיל את ה-insert אם נכשל
export function truckDataCollection_afterInsert(item, context) {
return sendStreetAlerts(item.street, item.region); // חוסם, לא ניתן לניטור
}
אחרי (Sentrix): ה‑ingest רק כותב ומפרסם אירוע; מנוע ההתראות צורך אותו בנפרד — מנותק, ניתן לבדיקה ולריטריי:
// alerts-engine צורך את האירוע מ-Pub/Sub — לא חלק מה-insert
onMessage('truck.lift', async (evt) => {
await alerts.evaluate(evt); // נכשל? ריטריי, בלי לפגוע בקליטה
});
לפני (Wix): jobs.config +
Jobs.js (למשל דוח יומי ב‑UTC). אחרי: אותו
לו"ז ב‑Cloud Scheduler שקורא ל‑/jobs/* ב‑mis-core (פרק 14)
— עם OIDC + סוד משותף, וכל ג'וב הוא endpoint שנבדק ב‑CI.
| בוויקס | ב‑Sentrix |
|---|---|
| קולקציה ב‑Content Manager | טבלה ב‑PostgreSQL, מוגדרת ב‑schema.prisma |
| הוספת שדה = קליק בעורך על אתר חי | עריכת schema + migration ב‑PR (פרק 7); המסד מסרב לקיצור דרך |
wix-data.query() |
Prisma client, תמיד עם where: { tenantId } |
externalMISDB (ramzor/priority) |
נשאר מקור‑אמת היסטורי עד קאט‑אובר; גשר מעתיק לסכמה שלנו |
backend/http-functions.js |
Fastify ב‑services/mis-core; אותם
/_functions/* |
data.js afterInsert hook |
פרסום ל‑Pub/Sub → מנוע ההתראות (מנותק) |
jobs.config + Jobs.js (cron) |
Cloud Scheduler → /jobs/* (פרק 14) |
| Secrets Manager של Wix | GCP Secret Manager; השירות מקבל סוד כ‑env בפריסה |
עורך גרפי + $w |
קומפוננטות React מ‑ui-kit (פרק 12), hot-reload |
| Publish דוחף לאתר החי | PR → בדיקות → dev → main → prod (פרק 5) |
| מייל פיקטיבי פר לקוח | הרשמה במייל אמיתי + אימות + אישור אדמין (פרק 20) |
בוויקס "לתקן משהו" היה לפתוח את האתר החי ולשנות בו. כאן זה שלוש פקודות ו‑PR — בכוונה. העלות: דקות. התמורה: אי אפשר להפיל פרודקשן בקליק, כל שינוי הפיך, ותמיד יודעים מה רץ איפה. איפה שזה מרגיש "מסובך מוויקס" — שם בדיוק Wix נתן לנו לירות לעצמנו ברגל.
קליטת לקוח חדש היא שורה ב‑DB. הכל קורה באשף בקונסולת
האדמין (apps/admin-console, זמינה ל‑staff בלבד, והאכיפה בצד
השרת — ראו למטה): טננט, רשויות‑משנה, מודולים, והזמנת המשתמש הראשון.
אין שלב טרהפורם בפתיחת לקוח, ואין "פתיחת דלת" ידנית:
apps/client-app הוא ה‑default_service של ה‑url
map, ולכן כל label מגיע לאפליקציה בלי כלל host, והאפליקציה בוחרת את סט
המסכים לפי הסגמנט של הטננט. שמונת צעדי ההכרעה, והמוקשים שבהם: פרק צמוד ל‑2.
ההוראה המבצעית המלאה היא באנגלית וב‑GUI‑first:
docs/HAND-IN-HAND-EN.md§10. הפרק הזה הוא ההסבר; שם נמצאים הצעדים להעתקה.
הדיאגרמה הבאה מראה את מסלול הזהות והאישור שכל משתמש לקוח עובר:
האשף יושב במסך Tenants, מתחת לרשימת הלקוחות.
slug (תת‑הדומיין, נבדק
חי מול הרשימה הקיימת ומול הסלאגים השמורים), סגמנט, מותג. הסגמנט
הוא השדה החשוב בצעד הזה — הוא זה שקובע אילו מסכים הלקוח יקבל,
ולא אנחנו בטרהפורם. חוקי הסלאג בהמשך הפרק.Account לכל רשות תחת
אשכול (M Group). לקוח שנמכר ישירות מדלג על הצעד.TenantInvite.הכתיבה קורית כולה בלחיצה אחת בסוף צעד 4:
POST /admin/tenants { slug, displayName, segment, brandId, moduleKeys[] }
POST /admin/tenants/{slug}/accounts פר רשות‑משנה
POST /admin/tenants/{slug}/invites { email, role }
slug תפוס מחזיר 409; סלאג שמור מחזיר 409 גם הוא; מפתח מודול לא מוכר מחזיר 400 לפני שנפתחת הטרנזקציה, כך שלא נשאר לקוח חצי‑בנוי.
אין ביניהן יחס של גרירה, ולכן האשף מדווח כל אחת בנפרד. שלושתן מטופלות מהקונסולה.
| העובדה | מה נבדק | מה עושים אם היא אדומה |
|---|---|---|
| הסלאג פנוי | שהתווית לא שמורה לשירות אחר (admin, mcp,
partners, exchange-*) |
בוחרים סלאג אחר. mis-core בכלל לא נותן ליצור סלאג כזה, אז זה מצב שקורה רק לשורה היסטורית |
| הכתובת נפתרת | GET /tenants/by-host/{label} מחזיר טננט |
הטננט לא נוצר או לא פעיל — משלימים את היצירה או מפעילים |
| יש סגמנט | לטננט יש segmentKey שהקטלוג מכיר |
קובעים סגמנט במסך הטננט. עד אז הכתובת עונה 503 ולא מגישה מוצר מנוחש |
למה לא בודקים בפתיחת הכתובת בדפדפן. גם לקוח בלי
סגמנט מגיש 200 על /signin עם מסך כניסה ממותג — פתיחת הכתובת
לא מבדילה בין טננט מוכן לטננט חצי‑מוגדר. האשף מציג את קוד ה‑HTTP ליד
שלוש העובדות דווקא כדי שיהיה אפשר לראות שהם סותרים.
הסלאג הוא תווית DNS אחת מתחת
ל‑app.mis-26.com, וזה נובע מהתעודה
(*.app.mis-26.com מכסה בדיוק רמה אחת) ולא מהעדפה. לכן:
a-z, ספרות, ומקף פנימי.
שתי אותיות לפחות, 63 לכל היותר.netanya.mgroup.app.mis-26.com היא רמה שלישית, התעודה לא
מכסה אותה, והיא הייתה דורשת תעודה חדשה ואישור DNS חדש. כשרשות יושבת תחת
אשכול היא מקבלת תחילית: mgroup-netanya, לא
רמה נוספת.הקוד היחיד שמחליט:
services/mis-core/src/modules/admin-tenants/slug.ts.
התוויות השמורות, ולמה כל אחת שמורה:
| התווית | למה |
|---|---|
admin, mcp, partners,
exchange-* |
דלתות מוצר — כלל host מפנה אותן לשירות אחר. לקוח על תווית כזאת לא יגיע לאפליקציה בכלל, אלא יקבל את קונסולת האדמין או 401 מ‑mcp |
classic |
שמורה בלי דלת. אין כלל host על התווית, אבל היא נשארת שמורה: לשחרר תווית זו שורה אחת, לקחת אותה חזרה מלקוח חי זו מיגרציה |
driver |
מארח תפקיד — driver.<domain>
מגיש את מסכי הנהג (מוצר driver בתוך client-app, לפי
DRIVER_HOST_LABELS) לנהג של כל טננט. לקוח עם הסלאג הזה יקבל
מסכי נהג במקום את שלו |
security, contractors,
demo |
סלאגים של טננטים קיימים (ו‑demo הוא כינוי של טננט
הקבלנים). לקוח שני על אחד מהם היה מתנגש עם דאטה אמיתי |
mis, p2p-e2e |
טננטים ללא דלת דפדפן: קליטה מדור קודם וטננט טסטים |
api, app, www,
gateway, health, status,
auth, login, static,
assets, cdn, internal |
תוויות תשתית. אף אחת אינה דלת היום, וכל אחת מהן היא בדיוק סוג המארח שמישהו יפנה אליו כלי בעתיד |
הרשימה הזאת קריטית: אין כלל host פר לקוח ואין
terraform apply שיתפוס סלאג מתנגש, ולכן הבדיקה
ב‑slug.ts היא ההגנה היחידה מפני תווית שדורסת שירות
קיים.
זה לא אותו תהליך כמו פתיחת לקוח, ומבלבלים ביניהם. שני מסלולים, ושניהם מהקונסולה:
POST /admin/tenants/{slug}/invites ממסך הלקוח, עם מייל
ותפקיד. נוצר טוקן חד‑פעמי, נשמר רק הגיבוב שלו, והקישור מוצג פעם אחת. זה
המסלול שמשתמשים בו למשתמש הראשון של לקוח חדש, ולכל מי שרוצים להכניס
מיד.PENDING: הוא רואה מסך המתנה בלבד וה‑API מחזיר לו 403 על כל
בקשת נתונים. אנחנו מאשרים אותו בתור האישורים באדמין ובוחרים לו role,
והוא הופך ל‑ACTIVE. אפשר גם לדחות או לחסום
(DISABLED).בשני המסלולים ההרשאות נאכפות בשלוש שכבות ולא ב‑UI: השער בודק חתימת
טוקן, השירות בודק ACTIVE + role, והשאילתה מסוננת
ב‑tenantId. משתמש שהוזמן לטננט אחד לא רואה טננט אחר גם אם
הוא יודע את הכתובת שלו.
מודולים ניתנים לאישור גם פר משתמש: מסך "גישה" באדמין מציג כל מודול של הטננט עם default/read/write/manage/deny. מודול חדש שמזוכה לטננט מופיע שם אוטומטית.
ההזמנה היא הרשאה למייל אחד.
POST /admin/tenants/{slug}/invites יוצר טוקן חד‑פעמי
(sx_inv_…), שומר רק את ה‑sha‑256 שלו, ומחזיר את הטקסט פעם
אחת בתשובה — יחד עם inviteUrl, קישור הכניסה בכתובת של
הלקוח, ועם delivery שאומר מה קרה למייל.
הקישור הוא המסלול העיקרי. האשף מציג אותו בסיכום עם כפתור העתקה, כדי שאפשר יהיה לשלוח אותו בוואטסאפ. הוא מוצג פעם אחת בלבד: בשורה נשמר רק גיבוב, ולכן לא מסך הרשימה, לא סקריפט תמיכה ולא דאמפ של מסד הנתונים יכולים לשחזר אותו. אין "שליחה חוזרת" — אם הקישור אבד, מבטלים את ההזמנה ומנפיקים חדשה.
המייל הוא הנוחות. האירוע tenant.invited
נכתב לאפיק ההתראות ושירות ה‑notifications מרנדר אותו בשפת המוזמן (עברית
כברירת מחדל, RTL) עם מי הזמין, שם הארגון, התפקיד, הקישור ותאריך התפוגה.
הוא כפוף לאותו דגל alert_dispatch_channels כמו כל ערוץ יוצא
אחר:
delivery.status |
מה כתוב באשף | מה באמת נכון |
|---|---|---|
QUEUED |
"ההזמנה נוצרה והמייל בדרך אל…" | האירוע על האפיק. QUEUED ולא "נשלח" — השליחה קורית
בשירות ההתראות אחרי התשובה |
DISABLED |
"…שליחת מיילים כבויה בסביבה הזו" | אין EMAIL ב‑ALERT_DISPATCH_CHANNELS, או
שאין טופיק. שום דבר לא נשלח ולא יישלח |
FAILED |
"…המייל לא יצא לדרך" | המעטפה נדחתה לפני האפיק |
הזמנה אינה התראת מוצר: היא לא מאחורי העדפת התראות והיא לא מסוננת ברשימת ההשתקה של הצוות — הזמנה לכתובת שלכם, לצורך בדיקה, באמת מגיעה.
המימוש. המוזמן נכנס בכתובת של הלקוח שלו (Google /
Apple / מייל+סיסמה), POST /users/self-register מוצא הזמנה
ממתינה — לפי הטוקן שהגיע מהקישור, או לפי (טננט, המייל המאומת) — ובאותה
טרנזקציה מסמן אותה ACCEPTED ויוצר את המשתמש
ACTIVE עם התפקיד והרשות שנבחרו. ההזמנה היא
האישור — בלי מסך המתנה ובלי תור אישורים. מי שכבר נרשם וישב
PENDING מקודם באותה שורה, לא נוצרת שנייה.
מסלול הטוקן מחווט מקצה לקצה:
/signin?invite=<token> שומר את הטוקן ב‑sessionStorage
ומנקה אותו משורת הכתובת, ו‑/api/register שולח אותו כ‑
inviteToken. זה מה שמאפשר למי שפתח את הכתובת הלא נכונה
לנחות בכל זאת אצל הלקוח הנכון, וזה שורד גם הפניה מלאה של ספק הזהות
(ספארי חוסם פופאפ) וגם את העצירה ב‑/verify-email.
| מצב | מה מקבל המוזמן |
|---|---|
| הזמנה פגה | הרשמה רגילה ⇐ PENDING. עם טוקן מפורש:
403 INVITE_INVALID |
בוטלה (DELETE …/invites/{id}) |
אותו דבר — הסטטוס REVOKED, לא נמצאת |
| כבר מומשה | אותו דבר; החד‑פעמיות נאכפת
ב‑UPDATE … WHERE status = PENDING |
| נכנס עם מייל אחר מזה שבהזמנה | מסורב |
| טוקן לא קיים או של מישהו אחר | 403 INVITE_INVALID — אותו קוד לכל כישלון, בכוונה |
הדיאגרמה שלמעלה (הרשמה ⇐ אימות ⇐ PENDING ⇐ אישור) היא
המסלול של מי שנרשם בלי הזמנה. מוזמן מדלג עליו.
admin.app.mis-26.com נגישה מהאינטרנט, ולכן הגישה נחתכת
בהרשאה, לא בכתובת. חשוב להבין למה עוגיית סשן לבדה לא מספיקה: Identity
Platform הוא פרויקט אחד בלי טננטים לכל סביבה, כלומר לכל לקוח שנכנס
לדשבורד שלו יש טוקן שמייצר עוגייה תקפה גם כאן. לכן האכיפה היא לפי staff,
בצד השרת:
| מי | מה מקבל |
|---|---|
| אנונימי | הפניה ל‑/signin |
| מחובר, לא staff | מסך סירוב אדום במקום הקונסולה, ו‑403 not_staff מכל
/api/admin/* |
| מחובר, staff | הקונסולה |
mis-core לא זמין (או MIS_CORE_API_BASE חסר בפרוד) |
503 והקונסולה נסגרת — נכשל סגור,
בכוונה |
ההחלטה לא נופלת בדפדפן ולא לפי דומיין המייל: הקונסולה שואלת את
mis-core GET /users/staff-tenants, שמחזיר
403 NOT_STAFF לפי הטוקן המאומת של הקורא. שתי נקודות אכיפה —
ה‑layout של הקונסולה, וכל route handler בנפרד
(src/lib/staff-guard.ts), כי layout לא מגן על route.
| פעולה במסך | Route בקונסולה | מה קורה ב‑mis-core |
|---|---|---|
| יצירת טננט (אשף, צעד 1+3) | POST /api/admin/tenants |
שורת Tenant + כל ה‑TenantModule בטרנזקציה
אחת |
| רשות‑משנה (צעד 2) | POST /api/admin/tenants/{slug}/accounts |
שורת Account |
| הזמנת המשתמש הראשון (צעד 4) | POST /api/admin/tenants/{slug}/invites |
TenantInvite + הטוקן פעם אחת בתשובה |
| ביטול הזמנה | DELETE /api/admin/tenants/{slug}/invites/{id} |
REVOKED + revokedBy (השורה נשארת) |
| מצב הדלת (צעד 5) | GET /api/admin/onboarding/door-state |
קריאה בלבד: המפה המיושמת + GET /tenants/by-host/{label}
+ probe |
| הדלקת/כיבוי מודול | PUT /api/admin/tenant-modules |
upsert ל‑TenantModule (enabled/status/מחיר) |
| אישור משתמש שנרשם בלי הזמנה | POST /api/admin/users/{id}/approve |
PENDING → ACTIVE + role +
statusChangedBy |
| חסימת משתמש | POST /api/admin/users/{id}/reject |
DISABLED |
כל אחת מהפעולות האלה כותבת רשומת audit. בידוד: כל שורה נושאת
tenantId; לקוח שדורש מסד פיזי נפרד מסומן
Tenant.isolation = DEDICATED_DB (נספח ב׳).
מעבר לרשימת ה"אסור" ההתנהגותית (פרק 16), יש נעילות שהן אכיפה טכנית: לא בקשה יפה, אלא המערכת מסרבת. לכל אחת יש "שחרור חירום" (break‑glass) יחיד — רק שניר. אין דרך לעקוף בלי בקשה מפורשת שלו, וכל שחרור מתועד, תחום בזמן, ומוחזר אחריו.
| הנעילה | איך נאכפת טכנית | שחרור (break‑glass) |
|---|---|---|
| DDL על המסד (שינוי מבנה) | event trigger בכל DB + grants/ownership — לתפקידים הרגילים אין בעלות ואין CREATE (פרק 7) | שניר בלבד: מנהרת bastion כתפקיד הבעלים
ו‑DROP EVENT TRIGGER זמני, מתועד |
| שינוי סכמה מה‑UI | אותה נעילה — המסד מסרב ל‑ALTER/CREATE/DROP מכל כלי | דרך קוד בלבד: schema.prisma + migration + PR |
| שינוי ישיר בפרוד (קונסולה/מסד/תשתית) | IAM least‑privilege — לבני אדם אין write על משאבי prod; הכול דרך Cloud Build | שניר (org owner) מעניק הרשאה נקודתית וזמנית, ומסיר אחרי |
| push/merge ל‑main | ruleset: PR ירוק + אישור CODEOWNERS; ממזגים — שניר, מוטי או מורן | אין bypass קבוע (bypass_actors ריק); בחירום org admin
עורך את ה‑ruleset זמנית, מתועד ומחזיר |
| force‑push / מחיקת ענף | חסום ב‑ruleset | org admin בלבד |
| סוד מחוץ ל‑Secret Manager | gitleaks חוסם את הקומיט (כולל בהיסטוריה) | אין שחרור — מעבירים את הסוד ל‑Secret Manager |
| יצירת משאב ענן בתשלום | מדיניות + התראות תקציב (AGENTS כלל 9) | אישור עלות מול שניר לפני |
./db/guardrails/apply.sh <env> שמחזיר את ה‑trigger
וה‑grants.מי שנתקל ב‑ERROR: DDL is locked on this database — זה
עובד כמתוכנן, לא תקלה. המסלול הנכון תמיד: קוד → PR → CI (פרק 7).
המסמך הזה הוא כעת מקור‑האמת היחיד לקליטת מפתחים —
הפרקים והנספחים באותו קובץ, docs/onboarding-he.md. כדי
שיישאר עדכני:
docs/onboarding-he.md. הכול פרק אחד ברור — לא צריך לנחש
איזה מסמך עומק לגעת בו.docs/*
המקוריים. עורכים את קטע הנספח כאן (הוא הקנוני למסמך הזה); אם המקור
החיצוני עדיין בשימוש נפרד — מיישרים גם אותו.גרסת ה‑HTML הכהה (RTL, קובץ עצמאי אחד שנפתח בכל מקום, גם בלי אינטרנט) נבנית מקומית:
./scripts/build-onboarding-html.sh # -> docs/onboarding-he.html; מאמת את עצמו
חוויית הניווט. לצד התוכן, גרסת ה‑HTML נותנת:
| רכיב | מה הוא עושה |
|---|---|
| תוכן עניינים צדי | נבנה מהמסמך עצמו בזמן ריצה (כל h2
ו‑h3), ולכן פרק חדש ב‑Markdown מופיע בניווט בלי לגעת
בבנייה |
| תיבת סינון | סינון חי לפי מילה. קיצור: מקש / |
| פרקים מתקפלים | לחיצה על כותרת פרק מקפלת אותו. כפתורי "פתח הכל" / "סגור הכל" מימין למטה. קישור פנימי אל תוך פרק מקופל פותח אותו — אחרת הקפיצה נוחתת בלא‑כלום |
| סימון מקום | הפרק הנוכחי מסומן בצד, ושמו מופיע בשורה העליונה |
| פס התקדמות | קו דק בראש העמוד שמראה איפה אתה במסמך |
| מגירה למובייל | מתחת ל‑1100px התוכן נפתח כמגירה; Esc סוגר |
| הדפסה | ה‑CSS מסתיר את הניווט ופותח כל הפרקים, כדי שהדפסה תהיה מסמך שלם |
הבנייה עצמה עברה לסקריפט גנרי שמשמש כל מסמך עברי בריפו:
./scripts/build-doc-html.sh docs/<doc>.md <out>.html "כותרת" "כותרת משנה"
build-onboarding-html.sh נשאר כמעטפת דקה סביבו, כדי
שהפקודה שכולם מכירים תמשיך לעבוד. הקוד: scripts/doc-nav.css
+ scripts/doc-nav.js. בלי JavaScript המסמך עדיין נקרא
במלואו עם כל העוגנים — הניווט הוא תוספת, לא תנאי.
PDF לכל מסמך עברי, בפקודה אחת (אותו צינור בדיוק כמו ה‑Action, ואותה בחירה שמנוע הדוחות עושה — pandoc ל‑HTML RTL, ואז Chromium מדפיס):
./scripts/build-doc-pdf.sh docs/<doc>.md <out>.pdf "כותרת"
ה‑PDF (docs/onboarding-he.pdf) מופק ידנית
בלבד, כדי שעריכות שוטפות של התוכן לא יריצו בניית PDF על כל
push: GitHub ⇐ Actions ⇐ workflow onboarding-pdf ⇐
Run workflow. ההפעלה היחידה היא
workflow_dispatch — אין טריגר push, ולכן עריכת תוכן לא
מריצה בניית PDF. הצינור: pandoc ל‑HTML RTL, ואז Chrome headless מדפיס
PDF, כך שהעברית וה‑SVG נשמרים נכון. עותק ה‑HTML נשמר גם ב‑Drive:
חומרים מוויקס/sentrix-status/sentrix-onboarding-final.html.
pnpm setup:gui — האשף עובר את שער הזהות ומריץ התקנה,
Prisma, env והרמת הדשבורד. mis-core מקומית — המתכון בפרק 4 צעד 6.pnpm -r test ירוק; /health 200
ו‑/trucks 401 מול שער ה‑dev.AGENTS.md, הפרקים 5–8 כאן,
ו‑rollback-and-db.md.נתקעתם על משהו שאין לו תשובה כאן או במסמכי העומק? תפתרו, ואז תוסיפו את התשובה למסמך המתאים ב‑PR — ככה המדריך הזה נשאר המדריך היחיד שצריך.
המסגרת של שניר: רצים במקביל עם הגשר עוד כמה חודשים, מבצעים backfill מדי פעם, ובקאט‑אובר הסופי מעדכנים את אפליקציית ה‑Median. הפרק הזה הוא סדר הפעולות של אותו קאט‑אובר, מה מאמתים לפני כל צעד, ומה נשבר אם מדלגים.
הכלל היחיד שאין ממנו יציאה: ברגע שאתר Wix כבוי, האוספים שלו — כולל ה‑
*Archive— הופכים בלתי‑קריאים. מה שלא נמשך עד אז אבד לתמיד. זו הסיבה שהצעד הראשון בפרק הזה הוא לא כיבוי אלא שאיבה (§24.3).
כתיבה בוויקс
│
├──► האוסף בוויקס (מקבל את השורה בדיוק כמו קודם)
│
└──► hook יוצא ──► /_functions/bridge/<collection> ──► mis-core
│ │
סוד משותף mapper אידמפוטנטי
(401 בלי סוד) על (tenantId, legacyId)
| מה | מצב |
|---|---|
| אתרי Wix שמשדרים | 4 — main, mgroup, security, oldsite |
| אוספים עם נתיב גשר | 72 |
| יעדי מסירה | 2 בשורת קונפיג אחת (baseUrl + baseUrl2) —
dev ופרוד מקבלים כל כתיבה |
| אימות | סוד משותף. בוויקס בשם sentrixBridgeSecret, אצלנו
wix-bridge-secret → WIX_BRIDGE_SECRET.
fail‑closed |
| כשלים | שורות retry פר‑יעד; מה שלא מזוהה נכנס להסגר
(legacy-inbox) ולא נעלם |
*Archive |
בכוונה בלי hook — §24.3 |
| מסמכי העזר | docs/WIX-BRIDGE-GO-LIVE.md ·
docs/LEGACY-COVERAGE.md ·
integration/wix-bridge/ |
למה אין hook על הארכיונים. שורה מגיעה לאוסף ארכיון
רק אחרי שג'וב בוויקס העביר אותה מהאוסף החי — כלומר היא כבר עברה ב‑hook
החי וכבר אצלנו, לפי (tenantId, legacyId). hook על ארכיון
היה שולח שוב שורות שכבר יש לנו. כל ה‑mappers אידמפוטנטיים ולכן שום דבר
לא היה נהרס, אבל תעבורת הגשר ועומק התור היו מוכפלים בתמורה לאפס מידע
חדש.
הדבר האחד שהארכיונים כן מחזיקים ואנחנו לא: כל מה שנכתב לפני שהגשר עלה ב‑26–27.7.2026. זו בעיית קריאה, לא בעיית כתיבה, ו‑hook הוא הכלי הלא נכון לה.
המקור: docs/plans/HISTORY-GATE.md. הרעיון: משיכה
לפי דרישה במקום זרימה.
המכניקה כבר קיימת: modules/wix-pull/runner.ts מדפדף אוסף
מורשתי דרך הייצוא לקריאה‑בלבד ב‑sentrix-bridge.jsw ומנחית
כל שורה דרך אותם mappers שה‑hooks החיים משתמשים בהם.
אידמפוטנטי על (tenantId, legacyId), ולכן משיכה יכולה להצטלב
עם תעבורה חיה בכל סדר. הוא כבר מקבל plateScope
ו‑siteScope — כך תוחמה הדגמת נתניה לתחנה 10.
ו‑onScanWrite מוגדר בכוונה כלא‑מוגדר בנתיב המשיכה, כדי
ששורות שנמשכו אחורנית לא ידליקו התראות רחוב או פוליגון על אירועים
ישנים.
מה שחסר זו רק הדלת הקדמית, ארבעה פריטים:
| # | מה נדרש | למה |
|---|---|---|
| 1 | להוסיף את שמות ה‑*Archive לרשימת ההיתר של ה‑runner |
ה‑mappers כבר מכסים את התאומים החיים שלהם — שורת משאית מארכיון היא באותה צורה בדיוק כמו חיה |
| 2 | משטח בקשה: "תן לי לוחית X / תחנה Y בין תאריך A ל‑B" | להגיש קודם מהטבלאות שלנו; רק כשהחלון קודם לשורה הראשונה שיש לנו למפתח הזה — לירות משיכה תחומה ואז לענות |
| 3 | לזכור שחלון כבר נמשך | הבקשה השנייה לאותה תקופה היא קריאת טבלה רגילה |
| 4 | סקופ חובה. בלי כפתור "תמשוך הכל" | תחום לפי לוחית/תחנה/תאריך זה קטן. לא תחום על
RamzorStatsArchive זה לא |
האוספים שבהם מדובר: camerasCollectionArchive (security)
· truckCollectionArchive (main) ·
RamzorStatsArchive · priorityold ·
ramzorold (oldsite).
הדדליין: השער חייב להיות קיים לפני
כיבוי Wix. ברגע הכיבוי הארכיונים הופכים בלתי‑קריאים ומה שלא נמשך אבד. זו
הסיבה שהפריט יושב ב‑docs/plans/ ולא ב‑backlog.
שבעה שלבים. שלב אינו מתחיל לפני שהאימות של קודמו עבר, וכל שלב אומר במפורש מה נשבר אם מדלגים עליו.
./scripts/dumpdev.py --env prod --i-know # קריאה בלבד; ה-MANIFEST הוא הראיה
אימות: MANIFEST.md בתיקיית הפלט מציג
מספרי שורות שאינם אפס בטבלאות העוגן. אם מדלגים: אין
מספר "לפני". כל דיון על "חסרות לי שורות" יהפוך לדיון בלי נתונים.
# 72 האוספים המוצהרים בשער, בשתי הסביבות
grep -oE "/_functions/bridge/[A-Za-z0-9_+-]+" infra/api-gateway/gateway-openapi-dev.yaml | sort -u | wc -l
grep -oE "/_functions/bridge/[A-Za-z0-9_+-]+" infra/api-gateway/gateway-openapi-prod.yaml | sort -u | wc -l
ואז לעבור על docs/LEGACY-COVERAGE.md
עמודה‑עמודה: לכל אוסף בייצוא, האם יש לו מודל אצלנו,
והאם יש לו וו חי.
מה שידוע חסר כרגע, ולכן צריך הכרעה בשלב הזה:
| אוסף | מצב | ההכרעה הנדרשת |
|---|---|---|
supervisorAlertsCollection (146 שורות ב‑mgroup) |
יובא פעם אחת, בלי וו חי | התראות פיקוח שנוצרו בוויקס אחרי 26.7 לא זורמות אלינו. או להוסיף hook, או להכריע שהמסך החדש הוא מקור האמת מכאן והלאה |
userRequestsCollection (153 שורות) |
הוכרז ש‑monday הוא מקור האמת | 153 הבקשות ההיסטוריות לא הועברו לשום מקום. לייצא ל‑CSV או להכריע שהן נזרקות |
containersSensors,
weighStationDrivers/Trucks/Containers |
בלי מודל, היו ריקים או כמעט ריקים בייצוא | לפתוח את ה‑CMS החי ולוודא שהם באמת ריקים ולא שהייצוא פספס |
| רשימת ה‑CMS של main | מעולם לא צולמה | פעולה של דקה, וזו הבדיקה היחידה שסוגרת את השאלה "מה לא ידענו שקיים" |
אימות: אין שורה ב‑LEGACY-COVERAGE.md עם
דאטה חי, בלי מודל ובלי הכרעה כתובה. אם מדלגים: מגלים
אוסף חסר אחרי הכיבוי, כשאין ממה לשחזר.
ארבעת הפריטים מ‑§24.2. ואז, לכל אתר, למשוך את החלונות שמישהו עוד עשוי לבקש — תחום לפי לוחית/תחנה/תאריך.
אימות: לבחור 5 לוחיות ו‑3 תחנות, לבקש חלון שקודם ל‑26.7.2026, ולראות שהתשובה מגיעה מהטבלאות שלנו ולא משגיאה. ואז לבקש את אותו חלון שוב ולראות שהוא לא ירה משיכה שנייה (פריט 3). אם מדלגים: זה הצעד הבלתי הפיך. אחרי הכיבוי אין מאיפה למשוך.
מדידה, לא תחושה. לאורך כמה שבועות, בכל שבוע:
# ההסגר חייב להיות ריק, או שכל שורה בו מוסברת
curl -s -H "authorization: Bearer $TOK" -H "x-tenant-id: <tenant>" \
"https://sentrix-gw-7omgn50c.ew.gateway.dev/legacy-inbox?status=quarantined" | python3 -m json.tool | head -40
ובמסך legacy-intake בקונסולת האדמין: dry‑run על תקופה,
ולראות ש"מה שהיה נכנס" תואם למה שכבר בפנים.
אימות שלושה שבועות רצוף: (א) ההסגר ריק או מוסבר; (ב) ספירת שורות שבועית בטבלאות העוגן זזה באותו קצב שהאוספים בוויקס זזים; (ג) אפס שורות retry שנתקעו. אם מדלגים: מכבים על סמך "נראה שעובד". פער שיטתי — אתר שהודבק בו hook לא נכון, טננט שלא נפתר — מתגלה רק כשמישהו מחפש דאטה שלא קיים.
לפני שהאתר הישן כבוי, המשתמשים צריכים להיות כבר על החדש.
main / mgroup /
security תחת mis.org.il) נשמרים כ‑alias
ומופנים לטננט הנכון, כדי שהלקוח לא יראה שינוי בכתובת ביום המעבר.אימות: לכל לקוח פעיל, אפס משתמשים ב‑PENDING שהיו פעילים במערכת הישנה, וכל משתמש כזה מקושר לשורה המורשתית שלו. אם מדלגים: ביום הכיבוי מוקד השירות מקבל טלפונים מאנשים שאין להם חשבון.
הציוד בשטח משדר לאותן 35 כתובות בדיוק. בקאט‑אובר מפנים אותן לשרת החדש והוא לא יודע שמשהו השתנה. SEC‑1 דו‑מצבי: הטוקן הישן ממשיך לעבוד בזמן שטוקנים פר‑מכשיר מונפקים, ולכן ההפניה וההחלפה אינן צריכות לקרות באותו יום.
# כל 35 הנקודות המורשתיות קיימות בשער (405 ל-GET = הנתיב קיים, המתודה נדחית)
curl -o /dev/null -w '%{http_code}\n' https://sentrix-gw-7omgn50c.ew.gateway.dev/_functions/post_ramzor
אימות: אחרי ההפניה, לבחור 3 משאיות ו‑2 תחנות ולראות שורות נכנסות בטבלאות בזמן אמת, וגם שההתראות שאמורות להידלק נדלקות. אם מדלגים או ממהרים: אירועים מהשטח נופלים בשקט. ה‑DLQ תופס הודעות שנכשלו, אבל מכשיר שמדבר לכתובת שכבר לא עונה לא מגיע ל‑DLQ בכלל.
מעטפת ה‑Median (webview) נשארת. מה שמשתנה זה לאן היא מצביעה.
| מה | פעולה |
|---|---|
| URL בסיס | לעדכן ב‑appConfig.json מהמארח הישן למארח החדש
(<slug>.app.mis-26.com) |
| JS bridge | לשמור. הפוש עובר דרכו |
| פוש (OneSignal) | אותו App ID. לוודא שמשתמש שנרשם בשלב 4 מקבל את אותו external id, אחרת פוש ילך למכשיר של מישהו אחר או לאף אחד |
| Apple Sign‑In | דרך אותו bundle של ה‑webview. הדומיין החדש חייב להיות ב‑Authorized domains של Identity Platform |
| בדיקה | לפני הפרסום: build פנימי, כניסה עם Apple ועם Google, פוש אחד לבדיקה, ועיבור מסך אחד עם דאטה אמת |
| פרסום | App Store ו‑Play. לתכנן 3–7 ימי סקירה ולא לכבות שום דבר עד שהגרסה זמינה בפועל להורדה |
אימות: מכשיר שהוריד את הגרסה החדשה מהחנות (לא build פנימי) נכנס, רואה דאטה אמת, ומקבל פוש בדיקה. אם מדלגים: האתר הישן כבוי והאפליקציה בטלפון של הנהג עוד מצביעה עליו. מבחינת הנהג המערכת מתה, והוא לא יכול לתקן את זה לבד.
לא בבת אחת. אתר‑אתר, עם חלון של לפחות שבוע בין אתרים.
jobs.config),
אחרון ה‑hooks. הזרם החי נעצר.מעבר ל-GCP/07 - טבלת מיפוי סודות.mis.org.il לפי לוח זמנים שלנו, לא בו‑ביום.אימות אחרי כל אתר: 24 שעות בלי 404 חדש בלוגים של השער, בלי שורה חדשה בהסגר, ובלי טלפון מלקוח. אם מדלגים על ההמתנות: מכבים ארבעה אתרים ביום אחד, ואז מנסים לאבחן איזה מהם היה זה שהחזיק את הדבר שנשבר.
[ ] 0 dumpdev prod — MANIFEST עם מספרים
[ ] 1 LEGACY-COVERAGE נסקר, כל אוסף חי בלי מודל קיבל הכרעה כתובה
[ ] 1b רשימת ה-CMS של main צולמה
[ ] 2 שער ההיסטוריה בנוי · *Archive ברשימת ההיתר · חלונות נמשכו · משיכה כפולה לא קורית
[ ] 3 שלושה שבועות רצוף: הסגר ריק, ספירות זזות יחד, אפס retry תקוע
[ ] 4 כל משתמש פעיל קיים ומאושר בחדש · קושר לשורה המורשתית · alias-ים מופנים
[ ] 5 הקצה מופנה · 3 משאיות + 2 תחנות מאומתות חי · התראות נדלקות
[ ] 6 Median פורסם בחנויות ואומת ממכשיר אמת (לא build פנימי)
[ ] 7 אתר-אתר: UI כבוי → שבוע → jobs כבויים → האתר מורד → סודות בסבב → alias-ים מוסרים
הצעד הבא בפרק הזה: להוסיף את חמשת שמות
ה‑*Archive לרשימת ההיתר של ה‑runner ולפתוח PR. זה הפריט
היחיד בכל הרנבוק שיש לו תאריך אחרון שנקבע על ידי מישהו אחר.
מיושר לסיכום האודיט (docs/audit/2026-07-26/full-audit.md
§6 ולראיות docs/evidence/2026-07-27/). שלוש רשימות, כי הן
שלושה דברים שונים: מה שנדחה בהחלטה, מה שאחרי המסירה, ומה שפער אמיתי בלי
החלטה.
הפרק הזה מול פרקים 28–30, כדי שלא תחפשו במקום הלא נכון. כאן נמצא מה שהוחלט לדחות ומי החליט — פריט עם החלטה מתועדת מאחוריו. פרק 28 הוא המגבלות הטכניות שחיות איתנו גם אחרי שהכל הודלק, ופרק 30 הוא מה שראוי לעשות בספרינט הראשון. פריט יכול להופיע בשניים מהם משתי זוויות שונות; מה שלא יקרה הוא שהוא יופיע רק כאן ויעלם מהתמונה הטכנית.
| פריט | ההחלטה | מה בנוי בזמן ההמתנה | מה נדרש להדליק |
|---|---|---|---|
| גיבוי offsite ל‑AWS | לסוף. "בסדר שלפרוד אין גיבוי חוץ עדיין, אבל כשיידלק חייב להיות מנגנון אימות" | רגל ה‑S3 מדלגת, מדווחת בלוג ויוצאת ב‑0. מנגנון האימות נבנה
ורדום: BACKUP_MODE=verify מאמת קיום, טריות (8 יום
לשבועי / 36 ש' ליומי), קריאות וכותרת הצפנה — ועליו התראת
היעדרות על "אין אימות מוצלח ב‑36 שעות", שתופסת scheduler מושהה
או ג'וב שנמחק |
סוד AWS_BACKUP_CREDENTIALS +
backup_verify_enabled = true
ב‑prod.tfvars |
| דיספאץ' התראות לערוצים | להזכיר ולהחליט יחד. שניר נוטה להדליק | הכל בנוי: טבלאות העדפה פר‑משתמש ופר‑סוג, תור Pub/Sub, שירות ומתאמי
מייל/פוש/ווטסאפ, ופופאפ+סירנה לחמ"ל. ברירת המחדל [] = מזהה
וכותב, לא משגר |
alert_dispatch_channels = ["popup"] ב‑tfvars.
ההמלצה: שלב 1 בלבד, שבוע על dev, ולראות כמה פעמים
הפופאפ קפץ. המספר יחליט אם שלב 2 מוצדק —
docs/FEATURE-alert-dispatch.md |
| grok | אחרי המסירה, מחוץ לסקופ | הראוטר אגנוסטי; ספק נוסף הוא מתאם ולא ארכיטקטורה | החלטה + מפתח |
| רוטציית 7 סודות המורשת | לסוף | הערכים מוצערים ב‑HEAD ומוכרים לפי fingerprint עם הערת רוטציה | אחרי כיבוי Wix (פרק 24 שלב 7.5). רוטציה לפני זה משביתה את האתר החי |
ANTHROPIC_API_KEY |
לא נדרש — מתקננים על OpenAI | אינו בפריסה, ב‑IAM או בסדר ברירת המחדל. המתאם עצמו קיים בעץ ונגיש בהצמדה מפורשת | — |
| הטבלה הדחוסה המורשתית של סטטוס משאיות | apps/client-app/src/products/contractors/components/screens/classic-truck-status-screen.tsx
בריפו ולא מנותב |
העמודות מכוסות בשני מסכי /trucks |
שורה אחת, אם שניר יגיד שהוא רוצה אותה כטאב רביעי |
holon ו‑kedumim |
אינם בסט ההדגמה ("משכפלים את רעיון נתניה") | השורות קיימות בכוונה: holon הוא יעד
הקליטה המורשתית החי לאתר שקילה 270270270, ו‑kedumim מחזיק
את "קדומים כתום" ואת מסלול ה‑GPX האמיתי. הדלתות שלהם מצביעות על
המוצר הנכון |
— |
| פריט | למה זה יושב שם |
|---|---|
שער ההיסטוריה
(docs/plans/HISTORY-GATE.md) |
הוחלט בעקרון, לא תוזמן. Wix חי עד המסירה ולכן הארכיונים נגישים. חייב להיות קיים לפני הכיבוי — פרק 24 §24.2 |
| אכיפת VPC‑SC | ✅ נאכף על שני הפרויקטים. גישת SA לזרימות הדאטה החוצות-פרויקט + גישת צוות לפי זהות נקובה (SSO/MFA, ללא access level מבוסס-IP); התראת חסימת-אכיפה פעילה + גלגול-אחורה בפקודה אחת |
AuditLog רוחבי |
החלטת סכמה. קיים AuditEvent (שינויי סיסמה) +
/credentials/audit + יומני ענן |
| נוהל מחיקה לפי בקשת נושא מידע | 27701. המידע האישי הישיר מרוכז במעט טבלאות, ולכן זו כתיבת נוהל ומחיקה ממוקדת |
| אכיפת TLS בחיבור למסד | דורש קודם sslmode=require בכל סודות החיבור ואז שינוי
המופע. תפוגה מוצהרת: 31.10.2026. המופע private‑IP בלבד, ולכן החשיפה
נמוכה |
| מפתח OpenAI ייעודי לפרוד | פרוד חולק כרגע את מפתח ה‑dev. זו הייתה הדרך המהירה להחזיר את ה‑Copilot לפני ההדגמה, והיא מטשטשת ייחוס צריכה |
| MCP מעל Median API | תוכנן ב‑PROJECT-MAP, לא נבנה, לא נדרש עדיין |
| איחוד חמש מראות ה‑Prisma לחבילה אחת | עובד וזול היום; מתיישן רע. לעשות לפני השירות השישי |
| # | פער | חומרה | מה נדרש |
|---|---|---|---|
| 1 | מודול תקלות (Fault) — אין תיקייה
ב‑mis-core, אין נתיב בשום שער, ו‑api/faults/route.ts מחזיר
503 במכוון |
החור הגדול ביותר במוצר. §3.8 הוא מסך חתום עם 503 מאחוריו | החלטת סכמה: Fault עם חומרה, תחום, משאית, סוג,
פתיחה/סגירה, סטטוס טכנאי וקישור תקלה חוזרת. כל שאר משפחת התקלות מחכה
לזה |
| 2 | העדפות התראה מרובות‑ערוצים —
/notification-preferences/me קיים, ומסך הקבלנים עוד מדבר עם
/api/team/.../email-prefs הישן |
מסך חי מחובר ל‑endpoint הלא‑נכון | הפניית הפאנל. הפריט בעל התשואה הגבוהה
ב‑CAPABILITY-MATRIX §A.2 |
| 3 | externalMISDB — הקוד המורשתי מפנה אליו
1,270+ פעם |
הליבה החיה | שליטה בפרויקט שמריץ אותו, ואז חיבור ישיר. הגשר מכסה את הזרם החי אבל לא את ההיסטוריה שכבר שם |
| 4 | supervisorAlertsCollection בלי וו
חי |
התראות פיקוח מ‑26.7 והלאה לא זורמות | פרק 24 שלב 1 |
| 5 | פערי מפה — 24 YES / 12 PARTIAL / 44 NO מתוך 81 | שלושה אשכולות: ארבע שכבות נקודות שמציירות ואף אחד לא מזין · replay מסלול עם מהירות · שמירת פוליגונים (ציור עובד, הצורה מתה ברענון) | MAP-PARITY.md. השכבות הן הזול ביותר: שורת checkbox +
זריעה |
| 6 | auto-refresh על המפה | לא הועבר מקלאסיק | poller משותף עם backoff. setInterval פר‑מסך הוא מה שיצר
את סופת ה‑429 |
| 7 | תבניות ה‑PDF מ‑pdfgeneratorapi | report-engine בנה חדשות | לא חוסם. אבל אם יש דוח שלקוח מכיר מהעין — התבנית הישנה היא ה‑spec, ואף אחד לא הוריד אותה |
| 8 | מילוי טופס 1 / דיווח למפ"ס | הדוח מפיק כל שדה שהטופס מבקש, לא ממלא את ה‑PDF הרשמי | לטופס אין API והוא משתנה. מילוי אוטומטי של גרסה מיושנת גרוע מהדבקה ידנית של מספרים נכונים |
| 9 | יכולות קצה — זיהוי זיהום, גלישה, שתי תמונות מנוף, הצלבת טונות מול נפח, אותות מטאטא, ראש שקילה, Proof of Service לביוב, חיישני מפלס לטמונים | כולן דורשות סוג אירוע קליטה ומכשיר שמשדר אותו | נכון שהן לא מוצגות. מסך על אירוע שאף מכשיר לא שולח הוא כפתור מת, לא תיקון |
7 פריטים נדחו בהחלטה · 8 אחרי המסירה · 9 פערים אמיתיים, מהם
שלושה חוסמים: מודול התקלות (#1 — מסך חתום עם 503), שער
ההיסטוריה (§25.2 — הדדליין היחיד שנקבע בידי מישהו אחר), והשליטה
ב‑externalMISDB (#3 — הליבה החיה).
הצעד הבא: לפתוח את החלטת הסכמה על
Fault. היא חוסמת חמישה פריטים אחרים במשפחת התקלות, והיא
ההחלטה היחידה כאן שאף אחד לא יכול לקבל במקומך.
הפרק הזה מצביע במקום לשכפל, כי המדריך עצמו באנגלית בכוונה: כל שורה בו נועדה להדבקה בטרמינל, ועברית בטרמינל מתהפכת.
docs/HAND-IN-HAND-EN.md — תשע מקטעים
שאפשר לרוץ עליהם שורה‑שורה, ואז ללמד מהם:
| # | מה יש שם |
|---|---|
| 1 | clone ופתיחה ב‑Windsurf: מה מותקן קודם, gh auth,
pnpm install, .env, ומה לצפות בפעם
הראשונה |
| 2 | התמצאות: איזה מארח מגיש איזו אפליקציה, ואיפה יושבים המסך, נתיב ה‑API והשירות שמאחוריו — ארבעת הקפיצים |
| 3 | שינוי קוד קטן וראייתו מקומית: pnpm -F <app> dev
פר אפליקציה עם הפורט שלה |
| 4 | שינוי מסד קטן בבטחה: שלוש דרגות הגישה, חוק ה‑DDL, ומתי
SCHEMA-PUSH-OK: הוא התשובה הנכונה ומתי לא |
| 5 | dumpdev.py ו‑makeprod.py
— הוורקפלואו המומלץ |
| 6 | פרסום ל‑dev: ענף, קומיט, PR, מה כל בדיקת CI אומרת ומה עושים כשהיא
אדומה, מה guard-dev אוכף — ו‑6.7: לשלוח פיצ'ר
כבוי (הצהרת דגל, מה "כבוי" חייב להיות, ומחיקתו
ב‑retireAfter) |
| 7 | קידום לפרוד — המקטע שאפשר לתת למורן כמו שהוא: מה היא מאשרת, חמש הבדיקות שלה (החמישית: אם הדיף נגע ב‑YAML של השער, לוודא שמישהו מגלגל), ומה קורה ברגע שהיא מממזגת |
| 8 | troubleshooting לכשלים שנתקלנו בהם באמת — 8.1 הוא זה שתפס אותנו פעמיים |
| 9 | גרסת עמוד אחד להעתקה |
./scripts/dumpdev.py --schema-only # לפני שנוגעים: תמונת מצב + MANIFEST עם מספרים
./scripts/makeprod.py # חזרה יבשה של הקידום — קוראים אותה
./scripts/makeprod.py --go # מריץ, ועוצר בשער האישור
./scripts/makeprod.py --go --from-step 6 # אחרי שהמאשר מיזג: סכמה, בילדים, smoke
dumpdev.py — פותח מנהרת IAP, קורא את
ה‑credential לקריאה בלבד, מוציא pg_dump של
שני המסדים ל‑.dumps/<env>-<timestamp>/, וכותב
MANIFEST.md עם גדלים וספירות שורות. לא כותב לשום
מסד (המסד מסרב DML ו‑DDL מה‑role הזה), ולא מעלה לשום מקום. מה
לשנות בו: ENVS ו‑ANCHOR_TABLES בראש
הקובץ.makeprod.py — שמונה שלבים: preflight
(עץ נקי, על dev, מסונכרן, מראות Prisma זהות, terraform drift אפס בפרוד,
ארבע הבדיקות ירוקות) → PR → בדיקות → עצירה בשער האישור
→ מיזוג → ג'ובי סכמה בפרוד → 13 טריגרי הבילד → smoke + פקודת ה‑rollback.
ברירת המחדל היא חזרה יבשה. הוא לא מעביר דאטה מ‑dev לפרוד, ואין
בו נתיב קוד שיכול. מה לשנות בו: PROD,
REQUIRED_CHECKS, PROD_TRIGGERS,
SMOKE_HOSTS — שתי האחרונות הן היחידות שמתיישנות, ולכן הן
הדרך היחידה שבה הסקריפט יכול לשקר.עותק של המדריך יושב גם ב‑Drive:
חומרים מוויקס/sentrix-hand-in-hand-EN-2026-07-27.md.
הפרק הזה מכסה את החלקים שנוגעים למערכות חיצוניות — נגן ה‑CMS, שער המדיה, שער ה‑API, ה‑CMS של המקליטים ו‑Cloud Scheduler. באזורים האלה טסט ירוק אינו הוכחה שהדבר עובד: ההוכחה היא הרצה מול השרת האמיתי, קריאת לוגים אמיתיים ופענוח פריים אמיתי. אם אתה נוגע בווידאו, בבריאות מצלמות, בשער או בדמו — קרא את זה קודם.
שני מסלולי וידאו חי חיים במקביל, והם לא מתחלפים זה בזה:
| מה | דרך מה | איך זה עובד |
|---|---|---|
| משאית (מקליט DVR) | נגן ה‑CMS של 808gps | dvr.mis.org.il/808gps/open/player/video.html?devIdno=<did>&jsession=<…>
— נדרשת התחברות לפלטפורמה, וה‑URL נושא סוד
(jsession). |
| מצלמת עמוד | שער מדיה (MediaMTX) | dvr.mis.org.il/security/<שם המצלמה>/index.m3u8 —
HLS, ודף נגן live.html?cam=… שעוטף אותו. אין
jsession, ה‑URL לא נושא סוד. |
למה זה מהותי: מצלמת עמוד (Hikvision) אינה מכשיר CMS.
אם דוחפים Camera.externalId שלה לנגן ה‑CMS מתקבל מסך כחול,
כי הנגן הזה מדבר רק עם מקליטי DVR. הראוט חייב לפצל לפי סוג המכשיר.
איך הראוט מפצל:
services/security-core/src/vault/hls-gateway.ts —
cameraId → שער המדיה,
truckRef/deviceRef → נגן ה‑CMS. התשובה נושאת
kind: 'cms-player' | 'hls-gateway' כדי שהצד הקדמי ידע מה
הוא מקבל.
שרשרת הבקשה של מצלמת עמוד במלואה: לחיצה →
live.html ב‑nginx → video.js מקומי →
GET /security/<cam>/index.m3u8 → nginx proxy
ל‑MediaMTX על 127.0.0.1:8888 → sourceOnDemand
פותח RTSP אל ה‑NVR בעמוד → MediaMTX אורז HLS.
sourceOnDemand: yes — MediaMTX פותח את ה‑RTSP רק כשמבקשים,
ולכן עמוד שהמצלמה שלו מתה עונה 500 אחרי ~10.3 שניות,
בזמן שה‑redirect שלפניו עונה ב‑0.2. תקציב ה‑probe הוא 20 שניות בגלל זה.
אם תקטין אותו — כל מצלמה מתה תדווח "השרת לא זמין", וזה
שולח את הטכנאי לכיוון הלא נכון.SameSite=None; Secure; Partitioned (CHIPS), וגם בלי עוגייה
בכלל ה‑playlist נטען. לכן הבחירה ב‑iframe של live.html היא
נכונה, ולא למשוך את ה‑m3u8 חוצה‑מקור.status הוא לא האמת. עמודי גליל‑עמקים
אין להם edge‑PC שמדווח, ולכן הם תמיד UNKNOWN. הכפתור מוצג
ל‑ONLINE וגם ל‑UNKNOWN, וההחלטה נופלת על ה‑probe בצד
השרת. אם תגייט על status === 'ONLINE' — סגרת את הלייב על כל
עמודי גליל.מתוך 373 עמודים 4 מפורסמים בשער (כולם
Shomron-*). השאר לא — וזו פעולה על ה‑DVR אצל מורן,
לא בקוד. אשקלון (198) על רשת פנימית 10.0.x.x עם
סיסמאות בתוך כתובת ה‑RTSP: לא מגיעה מהענן, וגם השמות בעברית ש‑MediaMTX
דוחה מפורשות. שם מחשב‑קצה מושך RTSP מקומית ומעלה סטילס וקליפים בלבד;
לייב באתר כזה ידרוש שרת מדיה בשטח.
הפיצ'ר: איזו משאית אונליין אבל עם מצלמה שהפריים שלה שחור לגמרי.
services/mis-core/src/modules/camera-health/.
הכיול נעשה על צילומים אמיתיים, ואל תשנה אותו בלי לחזור על זה:
| ערוץ | מרכז התמונה (crop 50%) |
|---|---|
| חי, לילה | ממוצע 109–124, פער 185–198 |
| מנותק | ממוצע 16, פער 0 |
שני דברים שאסור לפספס:
=== 0 מפספסת כל מצלמה מתה בצי.OFF CH3 למטה) מרים את המקסימום של הפריים המלא
ל‑243, ולכן פריים מת "נראה" כאילו יש בו אות. במרכז הוא שטוח
לחלוטין.הסף: ממוצע ≤32 וגם פער ≤10. שניהם נדרשים — פריים בהיר ואחיד (עדשה מול קיר ביום) הוא שטוח אבל לא שחור, ואינו תקלת חיווט.
איך צולם הפריים:
StandardApiAction_capturePicture.action?…&Type=1 מחזיר
DownUrl עם JPEG של הפריים הנוכחי. שני דברים: המקליט עונה גם
על ערוץ מנותק (הוא מרנדר שחור עם OSD) — וזה בדיוק האות;
וה‑DownUrl חוזר עם IP גולמי על פורט 16611, שאנחנו ממפים
בחזרה לשם המתועד.
הרצה: POST /jobs/camera-health-sweep,
מתוזמן כל שעה בדקה 20 ב‑dev וב‑prod. מתוקצב פר‑ריצה
(הפחות‑נבדקים קודם), ולכן צי בכל גודל מתכסה בכמה שעות. מסך: קונסולת
ניהול → בריאות מצלמות
(/admin/camera-health).
תבנית שכדאי לחפש בתוצאות: כמה משאיות שמחזירות בדיוק ימנית + אחורית שחורות אינן בהכרח מצלמות שנשרפו — זו החתימה של התקנה שבה שני הערוצים האחרונים לא חוברו. תשאל את המתקין לפני שמזמינים חומרה.
ואל תקרא את המספר הזה כמספר הצי. הסריקה מתוקצבת
ל‑120 מכשירים לריצה (הכפתור במסך: 60) מול כ‑200 משאיות עם מקליט, ונבדקות
רק משאיות שה‑CMS מדווח אונליין באותו רגע. מכשיר שלא
מותאם לשורת Truck נספר ב‑devicesUnmatched
ונדלג. כל ריצה היא מדגם, לא מפקד — summary.total / 4 הוא
מספר המשאיות שבאמת נבדקו.
demo-connect-online מכוון את משאית הדמו למקליט
אמיתי שאונליין כרגע, ודורס את הלוחית שלה במספר המקליט
(externalId: did, plate: did).
ו‑TruckCameraHealth הוא
@@unique([devIdno, channel]) — שורה אחת לכל ערוץ
מקליט בכל הפלטפורמה. כלומר שתי המשאיות לא מייצרות שתי שורות; הן
נלחמות על אותה שורה, ומי שהסריקה התאימה הוא הבעלים שלה. בלי טיפול, תקלה
של לקוח אמיתי מוצגת בטננט דמו בתור "רכב <סריאל>", ובמסך זה נראה
כאילו הדוח פשוט לא מסנן דמו.
שני חצאים לתיקון, ושניהם נחוצים:
matchTruck מעדיף את המשאית האמיתית על
המשאית המושאלת על אותו did — העדפה, לא החרגה, כדי שתקלה
אמיתית שרק השורה המושאלת מצביעה על המקליט שלה תמשיך להירשם./admin/camera-health מחריג את השורה המושאלת
כברירת מחדל (?includeDemo=true כדי לכלול). הספירות
מסוננות באותו אופן כדי שה‑KPI יתאים לטבלה,
ו‑summary.demoExcluded אומר כמה הוסתרו. דוח המייל מחריג בלי
opt‑in.ואל תסנן לפי טננט. החרגה לפי צורת ה‑slug (
demo,demo-*,*-demo) מסתירה תקלות אמיתיות:netanya-demoמחזיק משאיות נתניה אמיתיות (RamsaZC בייבוא המורשת, לוחיות אמיתיות, בלי כינוי של live‑demo), ולכל אחת עלולה להיות ימנית ואחורית מנותקות. ה‑slug אומר דמו, המשאיות לא.
מה שמזוהה הוא השורה המושאלת עצמה, לפי שני סימנים שכל
אחד מהם מספיק: הכינוי הוא סמן ה‑live‑demo, או שהלוחית
היא מספר המקליט — מה שלא קורה באף משאית אמיתית. הכל
במקום אחד:
services/mis-core/src/lib/live-demo-truck.ts.
קונסולת ה‑CMS היא רגיסטר נפרד. העץ שלה
(Monitoring Center (online/total)) סופר מכשירים רשומים מול
מחוברים באותו רגע, ומספרם משתנה כל הזמן. הסריקה בודקת רק מה
שאונליין באותו רגע, ולכן מקליט שחונה כל היום בלתי נראה לה גם
אחרי מאה ריצות.
אל תסיק גודל צי מ‑Truck.externalId.
מספר שורות ה‑Truck אינו מספר המקליטים, ואינו אפילו מספר הרכבים הייחודי:
אותו צי מיובא ליותר מטננט אחד, כך שלוחיות זהות חוזרות על פני
mis/contractors. המקליטים שלנו אינם נשמרים על
שורת ה‑Truck בכלל.
Truck.externalId מחזיק את הלוחית, לא את מספר
המקליט. לכן הענף המתועד ב‑matchTruck
(externalId === did) לא מתאים משאיות לקוח, וכל ההתאמות
עוברות דרך ה‑fallback של vid.startsWith(plate). מכשיר
שתווית ה‑CMS שלו לא מתחילה בלוחית נספר ב‑devicesUnmatched
ונעלם.
מכשיר שה‑CMS מכיר והפלטפורמה לא הוא כמעט תמיד רכב שלא
יובא, לא באג התאמה: הלוחית שבתווית ה‑CMS לא מוצאת שורת Truck,
והסריאל לא מוצא nvrId בייבוא המורשת. עמודת
recorderSerial על ה‑Truck — התיקון שצורת
matchTruck מזמינה לחשוב עליו — לא תתקן אותם. זה ייבוא, לא
לוגיקה.
צילום מצב מלא של המבנה, כולל עץ הקבוצות ב‑CMS והפילוח לפי ענף:
docs/evidence/2026-08-04/cms-fleet-structure-he.md. מתוארך בכוונה — העץ משתנה, אז תרעננו ואל תסתמכו על המספרים בלי בדיקה.
GET /admin/camera-health?coverage=true עונה על זה
במספרים חיים במקום בהשערות: cmsDevices /
cmsOnlineNow מהרגיסטר, matchedToTruck לפי אותו
matchTruck שהסריקה מריצה, ו‑שתי סיבות
נפרדות לחוסר — neverScanned (לא היה אונליין באף
סריקה, בעיה של רכב) מול unmatchedToTruck (ה‑CMS מכיר,
הפלטפורמה לא, בעיה של שורת Truck), ושני הדליים חוזרים
בשמם — neverScannedSample עם לוחית וטננט,
unmatchedSample עם תווית ה‑CMS, כל אחד עם דגל truncation
נפרד, ומכשיר מופיע באחד מהם בלבד. זה פרמטר על הנתיב הקיים ולא ראוט חדש —
ראוט חדש היה דורש גלגול קונפיג של השער לפני שהוא מפסיק להחזיר 404
(27.3).
ו‑
POST /admin/camera-health/sweepמוגבל ל‑60 מכשירים, לא 500. deviceLimit: 120מחזיר504 upstream request timeout— השער מוותר הרבה לפני Cloud Run. סריקה רחבה היא עבודה של ה‑scheduler, שקורא ל‑runner ישירות.
זה כתוב בפרק 10, וזה חוזר בכל נתיב חדש: מוסיפים אותו לספק, הטסטים עוברים, ה‑CI עובר, הפריסה עוברת — ואם הקונפיג לא גולגל, השער מחזיר 404 לפני שהבקשה בכלל מגיעה ל‑mis‑core, והמסך אומר "נכשל".
למה הטסטים לא תופסים את זה:
route-contract-drift.test.ts משווה את
הקובץ לטבלת הראוטים. הוא לא יכול לדעת אם הקונפיג
גולגל. אין שום בדיקה אוטומטית על זה.
הכלל: נתיב חדש בשער = שני צעדים, תמיד, בשתי הסביבות:
python3 scripts/ci/validate-gateway-spec.py \
infra/api-gateway/gateway-openapi-dev.yaml infra/api-gateway/gateway-openapi-prod.yaml
# ~2-4 דק' ליצירה, ~5-8 דק' לעדכון, ורק אחד בכל רגע לכל API
gcloud api-gateway api-configs create sentrix-api-config-v<N>-$(date +%Y%m%d) \
--api=sentrix-api --openapi-spec=infra/api-gateway/gateway-openapi-dev.yaml \
--project=mis26-dev --backend-auth-service-account=sa-api-gateway@mis26-dev.iam.gserviceaccount.com
gcloud api-gateway gateways update sentrix-gw --api=sentrix-api \
--api-config=sentrix-api-config-v<N>-$(date +%Y%m%d) --location=europe-west1 --project=mis26-dev
האימות שאסור לדלג עליו — נתיב חדש חייב לענות 401 (מוצהר, נעצר באימות) או 405 (מוצהר, שיטה אחרת), לעולם לא 404:
GW=$(gcloud api-gateway gateways describe sentrix-gw --location=europe-west1 \
--project=mis26-dev --format='value(defaultHostname)')
curl -s -o /dev/null -w "%{http_code}\n" https://$GW/admin/camera-health # 401, לא 404
מה demo-refresh עושה.
/jobs/demo-refresh שומר את שבוע הצי של כל טננט דמו טרי —
הוא מזריע שבוע של היסטוריה כדי שהמסכים ייראו מלאים בכל כניסה, ולשני
הדמואים המובילים (demo-agriculture, demo-port)
הוא מרכיב יום עסקים מלא. הג'וב מדווח בכל ריצה
ב‑coverage אילו טננטים רועננו ואילו לא — לא כל טננט דמו
נכלל, וטננט כמו netanya-demo (דמו ראש העיר, רץ בפרודקשן על
netanya-demo.app.mis-26.com) אינו מתרענן. הסטים אינם
מתחלפים בין טננטים: משאית תערובות בחברת אוטובוסים היא לא דמו, היא
באג.
היום הוא יום ישראלי. הג'וב בונה את חלון "היום"
ב‑Asia/Jerusalem, לא משעון ה‑UTC של Cloud Run — אחרת בין
חצות לארבע לפנות בוקר אין "היום" בכלל והריצה כותבת את יום האתמול.
משתמשים ב‑yardDayKey/yardDayWindow הקיימים
ב‑modules/ituran/segment.ts (DST‑תקינים). אל תכתוב
עוזר יום שלישי — יש שניים, אחד מהם
ב‑modules/cargo/board.ts.
מסך הסיכום היומי הוא תצוגת חריגים. מסך הצי היומי מסודר סביב שורה פר רכב שמציגה ק"מ מול תוכנית, שעות מנוע/עבודה/סרק, ניצולת, מהירות שיא ותגי חריגה; קווי הזמן מקופלים בתוך כל שורה ונפתחים לפי דרישה. הכיוון: קודם מה חריג, ורק אז הפירוט.
עמודים חיים בדמו. העמודים המשדרים מוזרעים לטננטי
הדמו בשתי הסביבות
(services/security-core/scripts/seed-live-poles-into-demos.ts --env dev|prod).
הטריק: ה‑externalId של השורה הוא נתיב הזרם
בשער, ולכן עותק בטננט אחר משדר בדיוק כמו המקור.
--env הוא חובה — למזהי הטננטים יש uuid
שונה בכל סביבה.
מעבר שעון. החלון DST‑תקין, אבל ההיסטים בדקות שבתוכו לא: ביום המעבר באביב היום הישראלי הוא 23 שעות וארבע שורות נופלות ליום הבא, וביום המעבר בסתיו השעות זזות שעה. פעם בשנה, דמו בלבד.
ffmpeg כאן בלי fribidi. drawtext בלי
fontfile נותן ריבועי טופו על עברית — אבל פונט עברי
לבדו לא פותר: ה‑build הזה מקושר ל‑harfbuzz ולא ל‑fribidi, ולכן
drawtext מסדר עברית בסדר לוגי משמאל לימין, כלומר הפוך.
libass כן מיישם bidi — עברית עוברת דרך מסנן
ass, ו‑drawtext נשאר ל‑ASCII בלבד (טיימקוד,
תוויות). בונוס: ב‑drawbox הביטוי t ב‑x/y הוא
עובי המסגרת, לא הזמן — אנימציה שם נכשלת בשקט.
סודות לא נכתבים לקובץ. אף פעם. קל להיגרר לכתוב מחרוזת חיבור עם סיסמה או מפתח חתימה לקובץ עבודה זמני כשמריצים משהו ידני. אל תעשו את זה. הכלל: מעבירים סוד ישר לפקודה הצורכת באותה הרצה, דרך משתנה סביבה, ולא מדפיסים ולא שומרים:
DATABASE_URL=$(gcloud secrets versions access latest --secret=MIS_CORE_RUNTIME_DATABASE_URL \
--project=mis26-dev | sed 's/?schema=.*//') psql "$DATABASE_URL" -c '...'
באגים שנוגעים למערכות חיצוניות נשארים ירוקים בטסטים ונתפסים רק בהרצה — מול השרת האמיתי, מול לוגים אמיתיים, מול פריים אמיתי. אם השינוי שלך נוגע ב‑CMS, בשער מדיה, בשער API או ב‑Cloud Scheduler — הטסט הוא לא ההוכחה. ההרצה היא ההוכחה.
הפרק הזה קיים כדי שלא תגלו את הדברים האלה תוך כדי תקלה. כל שורה כאן היא מצב קיים ומוכר, ולא באג פתוח; מה שראוי לטיפול מופיע בפרק 30 כמשימה.
| המגבלה | מה זה אומר בפועל |
|---|---|
| גלגול קונפיג השער אינו אוטומטי | נתיב חדש ב‑YAML של השער נכנס לתוקף רק אחרי
api-configs create + gateways update.
route-contract-drift משווה קוד ל‑YAML ואינו יכול
לדעת אם הקונפיג גולגל. הסימן: השער מחזיר 404 לפני שהבקשה מגיעה
לשירות, והמסך אומר "נכשל". האימות היחיד הוא HTTP — נתיב חדש חייב לענות
401 או 405, לעולם לא 404 |
| אין טסט מול מסד אמיתי | כל הטסטים מריצים Prisma מוקאפ. שגיאת migration, index חסר או התנהגות
של ON CONFLICT נתפסות רק בהרצה מול dev |
| מסלול ה‑PDF מדלג מקומית | בלי PDF_CHROMIUM_PATH חמשת טסטי ה‑PDF מדלגים. CI נכשל
אם אין דפדפן, כדי שהדילוג לא ייקרא כמו "עבר" |
| מערכות חיצוניות | CMS הווידאו, MediaMTX ואיתוראן חיים מחוץ לענן שלנו. טסט לא מדמה אותם; פרק 27 הוא הפרק שכתוב מהניסיון הזה |
| המגבלה | מה זה אומר בפועל |
|---|---|
| קוד טרפורם ≠ ענן | חלק מהמשאבים ב‑infra/terraform/ מסומנים
INTEGRATOR: NEW entry — הם ייכנסו ב‑apply הבא, dev קודם. עד
אז הג'וב קיים ואף אחד לא קורא לו. terraform plan הוא הדרך
היחידה לדעת מה עומד שם |
alerts-engine ו‑ai-agents חיים
ב‑dev |
בפרוד הם טרם נפרסו. משפחות ההתראות מונחות‑האירועים רצות מקצה לקצה ב‑dev |
| ייצוא החיוב של GCP לא מופעל | ולכן כל מספר כספי במסכי העלות הוא הערכה, והוא מסומן
כך. ה‑provenance של כל figure אומר את זה במפורש —
list-price הוא לא measured |
| מד האחסון לא מחובר | storage ב‑provenance מדווח unpriced, והמסך
לא מציג מספר במקום להציג ניחוש |
| רגל הגיבוי ל‑AWS מדלגת | עד שקיים סוד AWS_BACKUP_CREDENTIALS, רגל ה‑S3 מדווחת
בלוג ויוצאת ב‑0. מנגנון האימות בנוי ורדום:
BACKUP_MODE=verify מאמת קיום, טריות, קריאות וכותרת הצפנה,
ומעליו התראת היעדרות |
| המגבלה | ההחלטה שמאחוריה |
|---|---|
| הווידאו לא נשמר אצלנו | D5. הוא נשאר ב‑NVR ונצפה לפי דרישה. משמעות: צפייה חיה תלויה במקליט שמקוון עכשיו — ולכן קיים הג'וב שמכוון את משאית הדמו למקליט חי |
packages/api-client כתוב ביד |
8,016 שורות שמשקפות את openapi.yaml. יש שומר
סחף שמשווה 114 סכימות ונכשל על כל פער בלי נימוק כתוב, אבל
ה‑codegen עצמו לא הונחת — משימה 1 בפרק 30 |
| חמישה עותקים של סכימת Prisma | הקנוני ב‑packages/shared-db; שומר CI מכשיל PR שבו מראה
נבדלה. עובד וזול, ומתיישן רע — ראוי לאיחוד לפני השירות השישי |
| שני מסדים | mis_core ו‑security_core מופרדים בכוונה.
משמעות: אין JOIN ביניהם. הגשר הוא קריאת HTTP עם claims חתומים |
| הדמו נזרע ליום | demo-refresh מריץ את זריעות יום‑הדמו לתאריך של היום,
אידמפוטנטית ומצטברת. סביבה טרייה בלי הרצה אחת ⇒ מסכי דמו ריקים |
supervisorAlertsCollection יובא פעם
אחת |
146 שורות מהמערכת המורשתית, בלי hook חי. התראות פיקוח שנוצרו שם אחרי הייבוא לא זורמות אלינו. צריך הכרעה: להוסיף hook, או להכריע שהמסך החדש הוא מקור האמת מכאן והלאה |
ההיסטוריה ב‑externalMISDB |
הגשר מכסה את הזרם החי ולא את ההיסטוריה שכבר יושבת שם |
הרשימה הזו היא התשובה ל"האם אני יכול פשוט לעשות את זה". ברירת המחדל: כן, ואת היוצאים מן הכלל צריך להכיר בשמם.
dev.x-jobs-secret,
קריאה בכל הלוגים.| הפעולה | מי מאשר | למה |
|---|---|---|
מיזוג ל‑dev |
ביקורת עמית + CI ירוק | הזרימה הרגילה |
קידום dev ⇒ main |
שניר | main הוא פרודקשן. אין cherry-pick של חלק
מ‑dev — או הכל, או כלום (כלל 10) |
terraform apply |
שניר | הענן הוא כספת. plan מותר לכולם, apply לא |
| כל דבר שנוצר בתשלום ב‑GCP | שניר, לפני היצירה | כלל 9: קודם אומדן ב‑me-west1, ואז עוצרים לאישור. גם
ג'וב Scheduler אחד נוסף הוא ~$0.10 לסביבה לחודש, וגם זה נאמר במספר ולא
ב"זניח" |
| מחיקת דאטה או תשתית | שניר, מפורשות | כלל 8. אין "מחקתי כדי לנקות" |
| DDL ידני על המסד | אסור לחלוטין | כלל ה‑DDL: שינוי מבנה עובר רק דרך migration בקוד (פרק 7) |
שינוי AGENTS.md, DECISIONS.md,
docs/conventions/ |
שניר | אלה כללי המשחק |
שינוי החוזה (packages/contracts-openapi/) |
שניר (CODEOWNERS) | החוזה קודם לקוד, ולכן הוא לא משתנה בשקט |
שינוי infra/, packages/shared-auth/,
packages/shared-db/ |
שניר (CODEOWNERS) | תשתית, זהות וסכימה |
| הוספת כלי צד‑שלישי עם גישה לקוד | שניר, מראש | הכלים המאושרים הם אלה שרצים ב‑CI: Semgrep, Trivy, gitleaks |
| השתקת סריקת אבטחה או בדיקת CI | דורש טריאז' מתועד | בייסליין האבטחה (נספח ד׳) |
| פתיחת לקוח חדש בפרוד | שניר | אשף בקונסולה, לא קוד. פרק 20 |
| הדלקת דגל פיצ'ר ללקוח | החלטה מוצרית — שניר | הקוד מוזג ל‑dev ביום שהוא נגמר; מי רואה אותו זו החלטה
נפרדת (כלל 10) |
אין להוסיף מבקרים אנושיים ל‑PR — לא
ב‑--reviewer ולא ב‑API. בקשת ביקורת הופכת אדם
למשתתף בשרשור, וגיטהאב מתחיל לשלוח לו כל בדיקה אדומה
וכל הרצה חוזרת, לנצח. .github/CODEOWNERS כבר מכסה את
main. הסלמה נעשית בהודעה לשניר (כלל 11).
main בשלב הבנייה מקודם בידי אדם אחד.
CODEOWNERS מצמצם היום את הבעלות לשניר בלבד, וה‑ruleset מרשה ל‑Repository
admin לקדם בלי אישור. זו החלטה מכוונת של שלב הבנייה, והדרך להחזיר אכיפת
שני‑אנשים מתועדת בתוך CODEOWNERS עצמו תחת
RESTORE AT HANDOVER — שלוש פעולות ב‑PR אחד.
הרשימה הזו נכתבה כדי שהספרינט הראשון לא יתחיל בשאלה "במה מתחילים". כל משימה כאן היא מצב קיים מוכר, לא באג שנפל בין הכיסאות: הן מסודרות לפי סיכון, וכל אחת אומרת מה המצב היום, מה חסר, ואיך יודעים שנגמר.
packages/api-client — הפער המסוכן ביותרהמצב היום: 8,016 שורות טיפוסים כתובים ביד
ב‑packages/api-client/src/index.ts, שמשקפים את
packages/contracts-openapi/openapi.yaml. יש שומר
סחף שרץ בכל PR
(src/__tests__/contract-drift.test.ts): הוא טוען את החוזה,
מוציא את הטיפוסים דרך ה‑TS compiler API ומשווה 114
סכימות — שדה חסר, שדה מיותר, required מול optional ו‑nullable.
כל חריגה דורשת נימוק כתוב בקוד, וחריגה שהתיישנה מכשילה את הבנייה
עד שמוחקים אותה, כדי שרשימת ההיתר לא תהפוך לשטיח.
מה שעוד פתוח: הטיפוסים עצמם נכתבים ביד. endpoint שמשנה שדה דורש שני עדכונים במקומות שונים, והשומר תופס את הפער אחרי שהוא נוצר במקום למנוע אותו.
מה לעשות: להנחית codegen מ‑openapi.yaml
לטיפוסים, ולהשאיר את השכבה הכתובה ביד רק למה שאינה סכימה (ה‑client עצמו,
ה‑ApiError, שמות הפונקציות). זה שינוי ב‑build של כל הריפו,
ולכן הוא משימה מתוכננת ולא תיקון אגב.
איך יודעים שנגמר: packages/api-client
נבנה מהחוזה, שומר הסחף נשאר בירוק ומאבד את הצורך ברשימת היתר לסכימות,
ושינוי שדה בחוזה שובר קומפילציה לפני שמישהו מריץ
טסט.
המצב היום: route-contract-drift מוודא
שהראוטים בקוד וה‑YAML של השער מסכימים. אף בדיקה לא יודעת אם הקונפיג
גולגל לשער, וזה בדיוק המקרה שנראה כמו באג באפליקציה:
404 לפני שהבקשה מגיעה לשירות.
מה לעשות: שלב אחרי הפריסה שמריץ את אימות ה‑HTTP על הנתיבים החדשים בדיף — כל נתיב חייב לענות 401 או 405, לעולם לא 404 — ומכשיל את הבנייה אחרת.
איך יודעים שנגמר: PR שמוסיף נתיב לשער בלי גלגול נהיה אדום, ולא "עובד עד שמישהו יפתח את המסך".
המצב היום: הסכימה הקנונית
ב‑packages/shared-db, ולה מראות מסונכרנות; שומר CI מכשיל PR
שבו מראה נבדלה, ו‑scripts/ci/sync-schema-copies.sh
מסנכרן.
מה לעשות: לצרוך את החבילה במקום להעתיק אותה. לפני שנוסף שירות שישי שקורא למסד — כל מראה נוספת מכפילה את עלות האיחוד.
איך יודעים שנגמר: קיים עותק אחד של
schema.prisma בריפו, ושומר הסנכרון נמחק כי אין מה
לסנכרן.
המצב היום: supervisorAlertsCollection
יובא פעם אחת (146 שורות) בלי hook חי, ולכן התראת פיקוח שנוצרה במערכת
הישנה אחרי הייבוא לא מגיעה אלינו.
מה לעשות: אחת משתיים, והכרעה מתועדת: להוסיף hook חי, או להכריע שהמסך החדש הוא מקור האמת מכאן והלאה ולסמן את הייבוא כחד‑פעמי בכוונה.
איך יודעים שנגמר: DECISIONS.md מחזיק את
ההכרעה, והתיעוד לא מתאר שני מקורות אמת בלי לומר מי מנצח.
המצב היום: ה‑provenance של האחסון מדווח
unpriced, והמסך לא מציג מספר. מהות ההחלטה נכונה — לא להציג
ניחוש — אבל היא זמנית.
מה לעשות: ג'וב לילי שמודד prefix בדלי וכותב מונה
STORAGE_BYTES.
איך יודעים שנגמר: ה‑provenance מדווח
measured, והמסך מציג מספר עם מקור.
alerts-engine ול‑ai-agentsהמצב היום: שניהם חיים ב‑dev, כולל צרכן ה‑pull של
Pub/Sub עם min-instances=1 (מנוי pull לא נשאב
ב‑scale-to-zero — זה לא בזבוז, זה תנאי).
מה לעשות: פריסה לפרוד עם אותו קונפיג, ואימות שה‑DLQ ריק אחרי סבב אחד.
איך יודעים שנגמר: התראה שמונעת אירוע ב‑dev נדלקת גם בפרוד, ומגיעה ליעד ההתראה בפועל.
AWS_BACKUP_CREDENTIALS ⇒ הרגל מפסיקה לדלג,
ו‑backup_verify_enabled = true מדליק את מנגנון האימות שכבר
בנוי.שתיהן דורשות פעולה של שניר בקונסולה, ולכן הן משימות של תיאום ולא של קוד.
מקור:
docs/ARCHITECTURE-COMPLETE.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.
נכתב 24.7.2026, ריצת הסיום. מיועד לכל אחד: מנהל, איש מכירות, מפתח חדש, או שניר בעוד חצי שנה. ההנחה: הקורא לא נגע מעולם ב-Google Cloud. כל מושג מוסבר, לכל רכיב יש אנלוגיה. הגרסה הגרפית (אותו תוכן + 4 דיאגרמות SVG, RTL) יושבת בדרייב:
חומרים מוויקס/sentrix-status/sentrix-architecture-complete-2026-07-24.html. מסמכי עומק: PROJECT-MAP (התכנון), DECISIONS (ההחלטות), WORK-PLAN (השלבים), TOUR (סיור מנהלים).
ללקוחות שלנו יש ציוד בשטח: משאיות אשפה עם מצלמות ומחשב קטן, תחנות שקילה עם משקל גשר, מצלמות אבטחה על עמודים, וחיישנים על מכולות. כל הציוד הזה שולח כל היום דיווחים קטנים: "הרמתי פח ברחוב הרצל", "משאית 12-345-67 שקלה 8 טון", "זיהיתי אדם בגדר הדרומית".
Sentrix היא המערכת שקולטת את כל הדיווחים האלה, שומרת אותם מסודר, ומציגה אותם לכל לקוח בדשבורד משלו: הקבלן רואה את הצי שלו, הרשות רואה את הניקיון בעיר, קצין הביטחון רואה את המצלמות שלו. כל לקוח רואה רק את שלו, בכתובת משלו, עם הלוגו שלו.
עד היום זה רץ על Wix (שלושה אתרים שנבנו לאורך שנים). עכשיו זה נבנה מחדש על Google Cloud, הענן של גוגל, בישראל (region תל אביב, me-west1). היתרון: מערכת אחת מסודרת במקום שלוש, אבטחה אמיתית, יכולת לגדול פי מאה בלי לבנות מחדש, וכל שורת קוד מתועדת ב-GitHub.
עיקרון הברזל של המעבר: הציוד בשטח לא מרגיש כלום. 300+ המשאיות ומאות המצלמות ממשיכות לשדר לאותן כתובות בדיוק (35 נקודות קליטה ששוחזרו אחת-לאחת מ-Wix). ביום המעבר מפנים אותן לשרת החדש, והן לא יודעות שמשהו השתנה.
| רכיב | מה הוא בפועל | האנלוגיה |
|---|---|---|
| Cloud Run | מריץ את התוכנות שלנו (השרתים והדשבורדים) בלי שננהל מחשב. גדל כשיש עומס, נכבה כשאין | שכירת אולם אירועים לפי שעה: פתוח כשיש אורחים, לא משלמים כשהוא ריק |
| Cloud SQL (PostgreSQL) | בסיס הנתונים: טבלאות מסודרות של כל המידע. אצלנו instance בשם
sentrix-pg בכל סביבה, בלי כתובת אינטרנט ציבורית |
ארון תיוק ענק בחדר נעול, שרק העובדים שלנו מחזיקים מפתח אליו |
| Pub/Sub | לוח מודעות פנימי: שירות אחד מפרסם "קרה אירוע", וכמה שירותים קוראים אותו במקביל בלי להפריע זה לזה | קבוצת וואטסאפ של אירועים: הקולט מפרסם פעם אחת, כל המנויים מקבלים |
| BigQuery (אגם הנתונים) | מחסן היסטוריה: כל אירוע שאי פעם קרה, זול לאחסון, מהיר לחיתוכים ("כמה טון בשנה?") | הארכיון העירוני: לא ניגשים אליו כל יום, אבל הכל שמור ואפשר לחפש בו בשניות |
| Cloud Storage | אחסון קבצים: תמונות מהמשאיות, snapshots מהמצלמות, דוחות PDF. וידאו בכוונה לא אצלנו (נשאר ב-NVR בשטח) | מחסן קרטונים ממוספרים; בטבלה שומרים רק את מספר הקרטון, לא את התוכן |
| Secret Manager | הכספת: סיסמאות, מפתחות API וטוקנים. הקוד מקבל אותם בזמן ריצה, הם לא כתובים בשום קובץ | כספת בנק: כל שירות מקבל גישה רק לתאים שלו |
| API Gateway | הדלת הקדמית ל-API: כל בקשה עוברת בה, היא בודקת תעודת זהות (טוקן) לפני שמעבירה פנימה | שומר בכניסה לבניין: בלי תג עובד לא נכנסים |
| Load Balancer + Cloud Armor | הכתובת הציבורית (app.mis-26.com) + חומת אש חכמה שחוסמת התקפות, בוטים וסריקות | דלת הכניסה הראשית של הקניון, עם מאבטח שמזהה מי בא לעשות בעיות |
| Identity Platform | מערכת ההזדהות: אימייל+סיסמה, Google, Apple, ואימות דו-שלבי (MFA). מנוהלת דרך מסכי Firebase | משרד הפנים של המערכת: מנפיק תעודות זהות ובודק אותן |
| Cloud Build | פס הייצור: כל דחיפת קוד מפעילה בנייה, בדיקות ופריסה אוטומטית | מכונת מטבעות: מכניסים קוד, יוצא שירות רץ. אותו תהליך בדיוק בכל פעם |
| Artifact Registry | מדף האריזות: שומר את גרסאות התוכנה הארוזות (Docker images) שכבר נבנו | מחסן מוצרים מוגמרים, כל גרסה עם תאריך ומספר |
| Terraform | כל התשתית כתובה כקוד ב-infra/terraform/. אין "קליק
נסתר" בקונסולה שאף אחד לא זוכר |
מתכון כתוב: אפשר לבשל את כל הענן מחדש מאפס, זהה בדיוק |
| Bastion | מחשב זעיר (e2-micro, sentrix-bastion) שדרכו מתחברים
בבטחה ל-DB הפרטי מהלפטופ |
דלת שירות אחורית עם אינטרקום: הדרך היחידה פנימה למי שמורשה |
| Cloud Scheduler | שעון מעורר לענן: מפעיל ג'ובים בשעות קבועות (דוח יומי, בדיקת אתר לא מקוון) | מזכירה שמתקשרת כל בוקר ב-6:00 להזכיר משימה |
| MCP | פרוטוקול שמחבר סוכני AI (כמו קלוד) למערכת דרך אותם endpoints ואותן הרשאות בדיוק | מתורגמן: הסוכן שואל בשפה שלו, המערכת עונה, בלי דלת אחורית |
(הציור המלא בגרסת ה-HTML בדרייב. כאן הגרסה הטקסטואלית.)
שטח ענן (GCP me-west1) לקוחות
┌──────────┐ HTTPS ┌──────────────────────────────────────┐ ┌─────────────────┐
│ משאיות │ ────────► │ שער: LB + Cloud Armor + API Gateway │◄──│ דפדפן/מובייל │
│ משקלים │ אותן │ │ 35 נקודות קליטה (חוזה ה-Wix) │ │ <לקוח>.app.mis- │
│ מצלמות │ כתובות │ ▼ │ │ 26.com + Median │
│ עמודים │ כמו │ שירותים על Cloud Run: │ └─────────────────┘
│ חיישנים │ היום │ mis-core │ security-core │ client-app │
└──────────┘ │ report-engine │ mcp-wrapper │
│ │ │
│ ▼ Pub/Sub (sentrix-ingest-events) │
│ ├── mis-core (עדכון state) │
│ ├── alerts-engine (התראות) │
│ └── BigQuery lake (היסטוריה, ישירות) │
│ │
│ דאטה: Cloud SQL (mis_core, │
│ security_core) · Cloud Storage │
│ (תמונות/דוחות) · BigQuery (אגם+marts) │
│ · Secret Manager (סודות) │
└──────────────────────────────────────┘
מה כל שירות עושה:
| שירות | תפקיד | סטטוס |
|---|---|---|
| mis-core | הליבה התפעולית: משאיות, שקילות, סריקות פחים, מכולות, משימות, נהגים,
ייבוא ספקים, התאמות, ג'ובים, משתמשים ואישורם, מנוע ההתראות הראשון
(פוליגון). כולל 35 נקודות הקליטה /_functions/* |
חי ב-dev+prod |
| security-core | דומיין הביטחון: מצלמות, עמודים (EdgePc), מסכות יום/לילה, זיהויים
(Detections), כללי התראה, אישורי צפייה. DB נפרד
(security_core) לבידוד |
חי ב-dev+prod, 92 טסטים |
| client-app | אפליקציית הלקוחות היחידה (Next.js): מסכי רשות, קבלן/צי לפי סגמנט, חדר בקרה ואפליקציית נהג באותו שירות; 4 שפות RTL/LTR, זיהוי לקוח וזיהוי מוצר לפי ה-Host בזמן ריצה | המבנה וההכרעה: פרק צמוד ל‑2 |
| report-engine | מחולל דוחות: PDF עברית RTL + Excel, 4 שפות, לוגו MIS + לוגו לקוח | קוד+טסטים; רץ כספרייה/ג'וב, טרם כשירות פרוס |
| mcp-wrapper | חשיפת ה-API ככלים לסוכני AI (קלוד וכו') באותן הרשאות | חי ב-dev+prod |
| alerts-engine | מנוע התראות כשירות נפרד: צרכן pull של Pub/Sub, שער ישימות לפי סוג רכב (fail-closed), כללי FUEL/IDLE/PRIVATE_WORK/CARGO/P2P + ג'ובים מתוזמנים | חי ב-dev (min-instances=1, E2E מוכח ממכשיר עד פעמון); פרוד — טרם נפרס |
| ai-agents | שער LLM אגנוסטי (Claude/OpenAI): ניתוב מודלים, caching, מדידת עלות פר-לקוח | חי ב-dev |
| notifications | דיספאץ' התראות לערוצים, על topic sentrix-notifications | קוד+טסטים, פס פריסה קיים |
| exchange (אפליקציה) | בורסות פסולת והובלה — מרקטפלייס בין לקוחות | חי ב-dev: exchange-waste / exchange-haulage.app-dev.mis-26.com |
| client-app (אפליקציה) | אפליקציית הלקוח היחידה: קוקפיט קבלנים וצי, קוקפיט רשויות, חדר בקרה ו-PWA לנהג — שירות אחד, והסגמנט של הטננט בוחר את סט המסכים בזמן ריצה | חי ב-dev+prod; default_service של ה-LB, כלומר כל <לקוח>.app.mis-26.com |
| אפליקציות נוספות | admin-console (אדמין: תור אישורים, מודולים ללקוח), partner-portal (תיעוד לאינטגרטורים) | פרוסות ב-dev+prod |
הבעיה שפתרנו: בוויקס כל לקוח קיבל "מייל פיקטיבי" משותף, ולא באמת ידענו מי נכנס. עכשיו (החלטה D25) כל משתמש נרשם עם המייל האמיתי שלו, מאמת אותו, ומחכה לאישור שלנו.
הזרימה, צעד אחרי צעד:
נרשם עם מייל אמיתי ──► מקבל לינק אימות למייל ──► נכנס למצב PENDING
│ │
▼ ▼
(או Google / Apple) רואה מסך "ממתין לאישור" בלבד.
ה-API מחזיר לו 403 על הכל.
│
אנחנו מאשרים ב-admin-console (+בחירת role)
│
▼
ACTIVE ──► רואה רק את ה-tenant שלו,
לפי ה-role שלו. אפשר גם לחסום (DISABLED)
tenantId (כלל זהב 5).Tenant.brandId.מה קורה כשמשאית מרימה פח:
/_functions/post_ramzor), עם
אותו header הזדהות ישן. שום שינוי בקצה.sentrix-ingest-events).alerts-engine (subscription) — מריץ כללים:
פוליגון-חריגה, אתר לא מקוון וכו'. כלל שנדלק = שורת Alert, שמופיעה בפעמון
בדשבורד.lake-sink (BigQuery subscription) — מוסיף את האירוע
לאגם ההיסטורי, בלי שום קוד.הפרדת חם/קר (העיקרון שכל התכנון בנוי עליו): Cloud SQL מחזיק את ההווה
(מה שהדשבורד שואל כל שנייה), BigQuery מחזיק את העבר (כל מה שאי פעם קרה,
זול, ערוך לחיתוכים). תמונות ב-Cloud Storage עם reference בטבלה; וידאו
נשאר ב-NVR בשטח ונצפה דרך dvr לפי דרישה (החלטה D5). ה-DLQ
(sentrix-ingest-events-dlq) תופס הודעות שנכשלו, כדי ששום
אירוע לא ילך לאיבוד בשקט.
שתי סביבות מבודדות לגמרי, כל אחת פרויקט GCP משלה עם DB, סודות ורשת משלה:
dev (mis26-dev) |
prod (mis26-prod) |
|
|---|---|---|
| ייעוד | פיתוח וניסויים. שוברים פה | לקוחות אמיתיים. לא נוגעים ידנית |
| ענף git | dev |
main (מוגן, רק דרך PR) |
| כתובת | app-dev.mis-26.com (IP 34.160.36.137) | app.mis-26.com (IP 8.233.86.186) |
| DB | sentrix-pg (db-g1-small, Private IP 10.17.0.3) | sentrix-pg (זהה, מוקשח) |
הזרימה של שינוי קוד:
feature branch ──PR──► dev ──(אוטומטית)──► פריסה ל-mis26-dev ──בדיקה──►
PR dev→main (סריקות אבטחה חוסמות + review) ──מיזוג──► פריסה ל-mis26-prod
infra/terraform/ (main, frontdoor, pubsub, datalake,
cicd, bastion). state בענן (bucket מוורסן). שינוי תשתית = קוד = PR, כמו
כל שינוי אחר.שני מסדים על אותו instance (בידוד לוגי מלא): mis_core
(הליבה התפעולית) ו-security_core (ביטחון). הסכמות:
packages/shared-db/prisma/schema.prisma (זהה ל-mis-core) ו-
services/security-core/prisma/schema.prisma. כל טבלה נושאת
tenantId — אין שורה בלי שיוך ללקוח. "סדר גודל" = צפי שורות
בעומס של היום (300 משאיות, מאות מצלמות); הטיפול בטבלאות שגדלות למיליונים
מפורט בנספח ז׳.
עדכון 26.7.2026 — טבלאות שנוספו עם מודולי המוצר החדשים (כולן עם
tenantId): RoutePlan(+עמודתgeometry— תוואי OSRM/קו-ישר) ו-Mission.route— מנוע המסלולים · Station— מרשם 298 התחנות הארצי עם geofence PostGIS · CargoEvent(מפורטש חודשית) — משפחת CARGO · אירועי דלק + FuelSite— משפחת FUEL · P2pTicket/ P2pRateCard/ ChargeLine— הוכחה-לתשלום (עבודות עפר) · BillingLine— הנהלת חשבונות ודפי חשבון · TruckModuleUsage— מנייה פר (משאית × מודול × חודש), הבסיס לחיוב פר-משאית · MondayLeadPush— תור לידים עמיד. הסכמה הקנונית נשארת ב-packages/shared-db, עם ארבע מראות byte-identical (mis-core, alerts-engine, notifications, ai-agents) שטסט סנכרון שומר עליהן.
| טבלה | בשביל מה | עמודות מפתח | סדר גודל |
|---|---|---|---|
| Tenant | הלקוח. גבול הבידוד: subdomain, לוגו, מותג, אזור, ורמת בידוד (SHARED או DB נפרד) | slug (→ |
עשרות |
| Account | תת-ארגון בתוך לקוח (כל רשות תחת mgroup). לקוח ישיר = account אחד | tenantId, name, slug | עשרות-מאות |
| User | משתמש אנושי. כולל מנגנון האישור (D25) | email, role (ADMIN/MANAGER/VIEWER), status (PENDING/ACTIVE/DISABLED), statusChangedBy/At, permissions (Json), preferences (Json) | מאות-אלפים |
| Brand | מותג white-label: mis, mgroup | key, name, logoUrl, primaryColor, marketingDomain | בודדים |
| Module | קטלוג המוצרים הנמכרים (WEIGHING, VIDEO, NAVIGATION, P2P, משפחת
CARGO...) עם מחיר מחירון; מקור-האמת: packages/catalog +
מראת mis-core (טסט parity) |
key, name, monthlyPrice, currency, vehicleTypes, active | 37 |
| TenantModule | מה הלקוח קנה: המתג + שורת החיוב החודשית | tenantId, moduleId, enabled, status, priceOverride, config | עשרות-מאות |
| טבלה | בשביל מה | עמודות מפתח | סדר גודל |
|---|---|---|---|
| Truck | רישום המשאיות: מי קיימת, של מי, מאיזה סוג | externalId (מזהה ה-NVR), plate, nickname, kind, contractorId, remoteAccessId (AnyDesk) | מאות |
| TruckEvent | אירועי משאית גולמיים: טלמטריה, GPS, סטטוסים. append-only | truckId, type, payload (Json), occurredAt | הגדולה במערכת — מיליונים בשנה כשהטלמטריה תחובר. partitioning חובה (SCALE-PLAN) |
| ScanEvent | הרמת פח / סריקה — "רמזור" הישן. כל הרמה: אילו פחים, צבע, כמה, איפה, תמונות | licensePlate, scannedTags (ארוקו), binType/2, binCount/2, weightKg, region, street, lat/lng, images, scannedAt | ~20 אלף ביום ← 7 מיליון+ בשנה. במורשת: עשרות מיליונים |
| TruckDailySummary | שורת הסיכום היומית פר משאית — מה שכל הדשבורדים מציגים | truckId, day, totalBins, binsPerHour, workMinutes, mileageKm, netWeightKg, binsByType (Json), statuses (Json) | 1 למשאית ליום — ~110 אלף בשנה |
| Driver / Mission / Route / BinPlan | נהגים, משימות (כולל שבועיות + תמונות לפני/אחרי), מסלולים בנויים ותוכניות פינוי | driverId, status, isWeekly, route (Json), stops (Json), plan (Json) | מאות-אלפים |
| WorkZone / WorkPlan / ContractorInput | אזורי עבודה (פוליגונים), תוכניות עבודה של פיקוח טיאוט, וקלטי קבלן (משמרות, קנסות) | polygon (Json), plan+polygons (Json), isActive, payload (Json) | עשרות-מאות |
| טבלה | בשביל מה | עמודות מפתח | סדר גודל |
|---|---|---|---|
| WeighSite | תחנת שקילה / מאזני גשר (חולון = 270270270), כולל heartbeat אונליין/אופליין | siteNumber, name, lat/lng, status, lastHeartbeatAt, contractExpiresAt | עשרות |
| WeighTicket | תעודת השקילה — הרשומה העסקית המרכזית. מאחדת את priority (התחנות שלנו) + קבצי ספקים | direction (IN/OUT), source (STATION/SUPPLIER_IMPORT), misNumber, licensePlate, grossKg/tareKg/netKg, wasteTypeId, contractorId, destinationId, weighedAt, exitAt, images, raw | ~1,500 ביום ← 550 אלף בשנה; מורשת: מיליונים |
| StationVisit | מי בפנים עכשיו: משאית שנכנסה וטרם שקלה החוצה. מזין את התראת "מעל שעתיים בפנים" | siteNumber, misNumber, licensePlate, enteredAt, clearedAt | מאות פתוחות; מצטבר כמו tickets |
| WasteType / Contractor / Destination | טבלאות מאסטר: סוגי פסולת, קבלנים מובילים, יעדים (מטמנה/טיפול/מקור, כולל כינויים) | code+name / name+externalRef / name+kind+aliases | עשרות פר לקוח |
| Container / BinTag / ContainerEvent | מכולות וכלי אצירה, תגי ארוקו שעליהם, ודגימות חיישן (מיקום+סוללה מ-Digital Matter) | externalId, sensorDeviceId; tagNumber; lat/lng, batteryPct, recordedAt | מאות; אירועים: אלפים ביום |
| טבלה | בשביל מה | עמודות מפתח | סדר גודל |
|---|---|---|---|
| AlertRule | כלל התראה כקונפיגורציה (לא קוד): פוליגון-חריגה עם חלון שעות, dedupe | type, config (Json: polygon/schedule/mode), severity, dedupeMinutes, enabled | עשרות פר לקוח |
| Alert | ההתראה עצמה: מה קרה, ממי, מי אישר. מוזנת מהג'ובים ומהמנוע; הפעמון בדשבורד קורא מכאן | kind, ruleId, severity, status (OPEN/ACK/RESOLVED), refId, truckId, dedupeKey, firedAt, ackedBy/At | עשרות-מאות ביום |
| ReportRow | שורת אגרגט מוכנה פר (לקוח, דוח, יום) — ה-report-engine מרנדר ממנה, לא מחשב מחדש | report, day, payload (Json) | 1 פר דוח פר יום |
| FormSubmission / PaymentRecord | טפסי בקשות (פסולת בניין וכו') + רשומות תשלום Grow/Meshulam שמגיעות ב-webhook | formNumber, status, pdfUrl; provider, reference (אסמכתא), payload | מאות-אלפים |
| SecurityEvent | אירועי VCA/ANPR שנדחפים מה-NVR דרך נקודות הקליטה של main (נפרד מ-security-core) | channelName, eventType, plate, vehicleBrand/Color, startAt/endAt, images | אלפים-עשרות אלפים ביום |
| טבלה | בשביל מה | עמודות מפתח | סדר גודל |
|---|---|---|---|
| EdgePc | מחשב הקצה על העמוד (מריץ YOLO): ספים, סטטיסטיקות, heartbeat | externalId, siteName, settings (Json: ספי יום/לילה), lastStats, status | עשרות |
| Camera | רישום המצלמות: איפה, של מי, LPR או לא, תמונה אחרונה | externalId, edgePcId, name, lat/lng, rtspPath (בלי סיסמאות!), isLpr, latestPicture (Json), status | מאות |
| CameraMask | המסכות מה-YOLO labeler: אזורי זיהוי פר מצלמה, פרופיל יום/לילה, עם version לזיהוי drift | cameraId, kind (DAY/NIGHT), polygons (Json), version | 2 פר מצלמה |
| CameraRtspError | יומן תקלות סטרימינג שה-watchdog בקצה דוחף | cameraId, errorCode, occurredAt, resolvedAt | אלפים |
| Detection | הזיהוי החי: מה המצלמה ראתה (תגיות, צבעים, ספירות, לוחית), עם הפניות למדיה. חלון חם בלבד — ההיסטוריה עוברת לאגם | cameraId, label, tags[], colors[], classCount (Json), licensePlate, recordId, media (Json refs), detectedAt | עשרות אלפים ביום בפריסה מלאה. מורשת: עשרות מיליונים. partitioning + ייצוא לאגם (SCALE-PLAN) |
| AlertRule (security) | כללי התראה על זיהויים (מחליף את alertLogicCollection): אילו תגיות, אילו מצלמות, באילו שעות, כמה | name, config (Json), severity, enabled | עשרות |
| Alert (security) | התראת ביטחון: מי קיבל, באיזה ערוץ (פופאפ/פעמון/מייל), עם dedupe | ruleId, detectionId, targetUser, delivery (Json), dedupeKey, firedAt, status | מאות ביום |
| Acknowledgment | מי אישר איזו התראה ומתי (audit) | alertId, ackedBy, ackedAt, note | כמו Alerts |
| טבלה | בשביל מה | מבנה |
|---|---|---|
| weigh_tickets_history | כל שקילה אי פעם. מקור לדוחות חודשיים והתאמות ספקים | partition יומי על weighed_at, clustering לפי tenant_id+site_number, חובה לסנן לפי תאריך (require_partition_filter) |
| detections_history | כל זיהוי אי פעם. מקור לתחקורים וניתוחי עומס | partition יומי על start_at, clustering לפי tenant_id+event_type |
| truck_events_history | ארכיון TruckEvent — מזין את מודלי הדלק והניצולת ואת אימות ניתוק הפרטישנים | partition יומי, clustering לפי tenant_id |
| marts | אגרגטים ומודלים מוכנים: weigh_monthly, fuel_anomalies_daily (חשד לאובדן דלק), fill_predictions (תחזית מילוי), fleet_utilization_daily. הדוחות והסוכנים קוראים מפה, לא מה-raw | scheduled queries יומיים; הסוכנים מוגבלים ל-marts בלבד |
| מה | כתובת |
|---|---|
| דשבורד dev (לקוח דמו חולון, יש דאטה) | https://holon.app-dev.mis-26.com — משתמש demo@mis-26.com, סיסמה בסוד DEMO_USER_CREDENTIALS |
| דשבורד dev (לקוח ריק) | https://tzvi-cohen.app-dev.mis-26.com |
| קדומים — חלון הראווה (מסלול אמיתי) | https://kedumim.app-dev.mis-26.com — תוכנית "קדומים כתום", 36 עצירות, תוואי OSRM |
| בורסות (dev) | https://exchange-waste.app-dev.mis-26.com · https://exchange-haulage.app-dev.mis-26.com |
| טננטי דמו סגמנטליים (dev) | demo- |
| כניסה כללית dev / prod | https://app-dev.mis-26.com · https://app.mis-26.com |
| API דרך השער (dev) | https://sentrix-gw-cnz6wcy2.ew.gateway.dev — /health פתוח, /trucks מחזיר 401 בלי טוקן (בכוונה) |
| האתר השיווקי (לא נגענו) | https://mis-26.com (GoDaddy→Vercel) |
| מה | כתובת |
|---|---|
| הריפו | https://github.com/MIS-Make-It-Simple/sentrix (ענפים dev/main) |
| התיעוד | docs/ בריפו: PROJECT-MAP, DECISIONS, WORK-PLAN, COSTS, AI-PRICING, DATA-LAKE, SECURITY-BASELINE, onboarding-he, tenancy-and-domains, COVERAGE-SWEEP-24-7, COST-PER-TENANT, SCALE-PLAN |
| מסמכים גרפיים לשיתוף | Drive חומרים מוויקס/sentrix-status/ (סטטוס, ארכיטקטורה,
היררכיית משתמשים) |
בשני הפרויקטים: APPLE_SIWA_PRIVATE_KEY · A_S_BINA_TOKEN · DATABASE_URL · SECURITY_CORE_DATABASE_URL · DVR_CREDENTIALS · DIGITAL_MATTER_CREDENTIALS · GOOGLE_GEOCODING_API_KEY · HTTPFUNCTIONSECURE (ה-auth הישן של הקצה) · JOBS_SHARED_SECRET · MONDAY_API_TOKEN · ONESIGNAL_REST_KEY · OPENAI_API_KEY · ROUTIFIC_API_KEY · SENDGRID_API_KEY (+ ב-dev בלבד: DEMO_USER_CREDENTIALS). ITURAN/POINTER/HIKVISION אינם ברשימה בכוונה: פרטי ההתחברות שלהם יושבים בכספת פר-לקוח, מוצפנים ב-KMS (סעיף 9.4).
git checkout dev && git pull # מתחילים מ-dev מעודכן
git checkout -b feature/my-task # ענף למשימה
# ... עובדים, קומיטים ...
git push -u origin feature/my-task
gh pr create --base dev # PR + סריקות; מיזוג = פריסה אוטומטית ל-dev
קידום לפרוד: PR מ-dev ל-main. הסריקות הן required checks; מיזוג מפעיל
את טריגרי deploy-prod-*. אין דחיפה ישירה ל-main (חסום ב-ruleset). פירוט
מלא למצטרף חדש: docs/onboarding-he.md +
docs/conventions/.
Account) ⇐
מודולים שנקנו (TenantModule, כולל priceOverride) ⇐ הזמנת
המשתמש הראשון (TenantInvite) ⇐ סיכום. הכל כותב
ל‑mis-core.<slug>.app.mis-26.com מכוסה ב‑wildcard cert, אין DNS
פר לקוח, ומאז מיזוג הדשבורדים ל‑apps/client-app גם אין כלל
host פר לקוח: השירות הזה הוא ה‑default_service, ולכן כל
label מגיע אליו והוא בוחר את סט המסכים לפי הסגמנט של הטננט.
הסגמנט הוא מה שצריך להיות נכון — טננט בלי סגמנט מקבל
503 ולא מוצר מנוחש, והאשף אומר את זה במסך הסיכום.ACTIVE
— ההזמנה היא האישור. תור האישורים (PENDING) נשאר למי שנרשם
בלי הזמנה.Tenant.customDomain — זה עדיין טרהפורם, פר דומיין ואניטי
ולא פר לקוח. רשות שדורשת DB פיזי נפרד:
isolation=DEDICATED_DB + databaseSecret.--set-secrets), לא
"שולף" ידנית.docs/conventions/how-to-secrets.md.סודות פלטפורמה (SendGrid, OpenAI...) הם גלובליים ויושבים ב-Secret
Manager. אבל קרדנצ'לס של איתוראן/פוינטר הם של הלקוח, וסיסמת RTSP היא של
מצלמה ספציפית. לאלה נבנית כספת credentials פר-tenant:
טבלת TenantCredential (tenantId, provider, scope כמו
truckId/cameraId, ciphertext) כשהערך מוצפן ב-KMS envelope encryption,
נגיש רק לשירות הקונקטורים, עם audit log על כל קריאה. ברמת PII. מסך
"הגדרות אינטגרציות" ללקוח מזין אותם דרך ה-admin. עד שהיא חיה — שום
קרדנצ'ל לקוח לא נשמר בכלל.
חוזה קודם (OpenAPI לפני קוד) · אין גישת DB גנרית · אימות בשער, הרשאות בשירות · סודות רק בכספת · כל שאילתה מסוננת tenant · i18n מהיום הראשון (he/en/ar/es) · לא מוחקים כלום בלי אישור.
docs/COVERAGE-SWEEP-24-7.md;
הראיות המפורטות של מה שנסגר לאחרונה:
docs/evidence/2026-07-25/ +
docs/evidence/2026-07-26/.הצעד הממשי הבא: לפתוח את הגרסה הגרפית מהדרייב מול המסמך הזה, לעבור מסך-מסך על holon.app-dev.mis-26.com, ולסמן כל אי-התאמה בין המסמך למציאות כ-issue בריפו.
מקור:
docs/tenancy-and-domains.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.
Decision (2026-07-22): separate WHO (tenant) from WHAT (products/modules). Products are never separate domains — they are modules toggled per customer.
mis-26.com.app.mis-26.com behind an
external HTTPS load balancer (Cloud Armor + Google-managed
wildcard cert *.app.mis-26.com). The LB
reads the Host header, resolves the tenant, and injects the
tenant id; mis-core scopes every query.<slug>.app.mis-26.com (e.g.
holon.app.mis-26.com). No per-customer DNS, no per-customer
certificate — the wildcard covers every label — and since 4.8.2026 no
per-customer routing either: one service (apps/client-app)
is the url_map's default_service, so every label reaches it
with no host rule and the screen set comes from the tenant's segment at
request time. A new tenant works the moment its row exists. The eight
resolution steps are in the companion chapter to chapter 2 of this
document.Tenant.customDomain (mGroup's own domain,
or legacy security.mis.org.il) → add a host alias + managed
cert once.*.mis.org.il hosts (main, mgroup,
security) kept as aliases mapped to the right tenant so
customers see no change; retired on our schedule.WEIGHING — שקילה ותחנות מעברVIDEO — וידאו/מצלמות (משאיות; גם לקבלנים)ALERTS — התראות ודוחותROUTE_SURVEY — סקר קוMISSIONS — משימות ומסלולים (כולל אופטימיזציה)CONTAINERS — מכולות/פחים + תגי ארוקוRAMZOR — ניטור רמזור (סטטוס פחים)FLEET — מעקב צי / DVRSECURITY — סקיוריטי (מנוע ומסד נפרד; נגיש כמתג)ACCOUNTING — חיוב / הנהלת חשבונותModule.monthlyPrice +
currency is the list price; a per-customer negotiated price
lives on TenantModule.priceOverride.TenantModule(tenant, module, enabled, status, priceOverride, startedAt, config)
is both the per-customer switch and the monthly billing line. "Video
only" = a tenant with just VIDEO. An authority with trucks
that also wants security = WEIGHING + SECURITY
both on.Each contractor and authority is its own tenant
under the main app domain, with its own switches. mGroup is not special
— it is simply a tenant that happens to hold several
accounts (the authorities under it).
security-core serves any tenant with the
SECURITY module on, not only mGroup.
Brand(key, name, logo, color, marketingDomain):
mis (default), mgroup, etc.Tenant.brandId sets the look and which domain the
customer sees. mGroup selling to an authority abroad =
Tenant{ brand: mgroup, region: INTL, modules: [...] }. The
same authority sold directly by MIS = brand: mis. Switching
the brand is a field change, not new code.Tenant.region (IL | INTL)
drives geography-specific behavior (language, compliance,
mGroup-abroad).Step-by-step, GUI-first: docs/HAND-IN-HAND-EN.md §10.
What each step writes:
| # | Step | Where | Automatic? |
|---|---|---|---|
| 1 | Tenant row: name, slug, segment, brand, optional parent
tenant |
wizard step 1 → POST /admin/tenants |
yes |
| 2 | Account rows (sub-authorities under a cluster) |
wizard step 2 →
POST /admin/tenants/{slug}/accounts |
yes |
| 3 | TenantModule rows = what they bought |
wizard step 3, same transaction as step 1 | yes |
| 4 | TenantInvite for the first administrator |
wizard step 4 → POST /admin/tenants/{slug}/invites |
the row, yes. Delivery to the customer, no — nothing emails it |
| 5 | The door — removed 4.8.2026 | nothing | n/a |
Step 5 used to be a load-balancer host rule in
local.app_host_doors plus a terraform apply,
and it was deliberately manual: a console that can apply terraform
against the load balancer can take every customer offline by mistake.
The step is gone because the question is gone — one client app on
default_service means there is no per-customer rule for
anyone to apply.
What the wizard reports instead: the slug is not a reserved
label (one of the five product doors), mis-core
resolves the host, and the tenant has a
segment. All three are fixable from the console.
Opening the URL is still not a check: a tenant with no segment answers
200 on /signin and has no screens, exactly as a label with
no door used to (measured on prod, 28.7).
Some tenants hold many sub-organizations. Example: the
mGroup tenant has one subdomain
(mgroup.app.mis-26.com), and each authority under
mGroup is an Account inside that one tenant, with its own users
and its own operational data. A tenant sold directly (e.g. Holon) uses a
single default account.
Account(tenantId, name, slug) — a sub-org within a
tenant.User, Truck, WeighSite,
WeighTicket, Container, ScanEvent
carry an optional accountId, so data is scoped tenant →
account → row.By default every tenant shares one database; each row carries
tenantId (and accountId) and queries are
always scoped, so no customer sees another's data. Some authorities
require a physically separate database today — set
Tenant.isolation = DEDICATED_DB and point
Tenant.databaseSecret at that tenant's own
DATABASE_URL secret. The app resolves the connection per
tenant at request time; the schema is identical in every database. Same
mechanism covers large overseas customers later.
מקור:
docs/IAM-PLAN.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.עדכון 26.7.2026: Talya צורפה —
talyap@mis.org.il(GitHub: talyape) מחזיקה בדיוק את הסט של דניאל: dev — editor + secretAccessor + IAP tunnel + OS Login; prod — viewer + logging.viewer (terraform, additive). מורן מחזיקה write בריפו ונמנית עם ממזגי main (שניר, מוטי, מורן); ה‑bypass הקבוע על main בוטל (bypass_actorsריק). הפירוט: docs/IAM-PLAN.md§9–§10 + docs/evidence/2026-07-25/external-signup.md.
Who can touch what, across GCP (org 264541084239 /
mis.org.il) and GitHub (MIS-Make-It-Simple/sentrix).
Mapping was read-only. The target model ships in two disjoint
halves:
infra/terraform/iam.tf, applied with
-target=google_project_iam_member.human. Non-authoritative
members only: can never remove or overwrite an existing binding, zero
lockout risk.gcloud commands, ordered; only Snir runs them, one at
a time.Service-account bindings are context only — this run does not touch any SA.
| Principal | Org role | Notes |
|---|---|---|
| snir@mis.org.il | roles/owner, roles/securitycenter.admin | effective owner of dev+prod via inheritance |
| daniel@mis.org.il | roles/owner | effective owner of dev+prod via inheritance |
| MIS@smart26.org | roles/owner, roles/resourcemanager.organizationAdmin | shared/legacy account; also direct owner of mis26-platform |
| yehiamc@wix.com | roles/orgpolicy.policyAdmin, roles/resourcemanager.projectCreator | external (Wix-era) |
| domain:mis.org.il (everyone) | roles/resourcemanager.projectCreator, roles/billing.creator | any workspace user can create projects + billing accounts |
| Project | Principal | Role | How |
|---|---|---|---|
| mis26-dev | snir@mis.org.il | roles/owner | direct (console bootstrap) |
| mis26-prod | admin@mis.org.il | roles/owner | direct — the only direct human on prod |
| mis26-platform (legacy, untouched) | MIS@smart26.org | roles/owner | direct; project runs the ramzor Cloud SQL — do not touch |
No other human appears in any project binding (all 23 org projects scanned).
moran* principal in the org policy or in any of the 23
projects (incl. dev, prod, mis26-platform).moranzeevi, org
member, repo role read (pull only —
cannot push, cannot merge).@moranzeevi as a
required approver, but GitHub ignores code owners without
write — and read-role reviews do not count toward
required approvals. Her approver seat is currently inert.| User | Org role | Repo role |
|---|---|---|
| SnirNisimMIS | admin | admin |
| Moti-MIS26 | admin | admin |
| Daniel-Gove | member | read |
| moranzeevi | member | write (עדכון 26.7) |
| talyape | member | read (GCP dev+prod הוענקו 26.7 — ראו העדכון בראש הנספח) |
Branch rules today: protect-main = PR required, 1
approval, code-owner review, require_last_push_approval,
checks (ci, gitleaks, semgrep, trivy), no standing
bypass (bypass_actors: [], tightened 24.7).
guard-dev = no delete / no force-push only — merging to dev
needs no special role. Effective main-mergers today: Snir + Moti +
Moran.
Per env: sa-cicd (run.admin, artifactregistry.writer,
logging.logWriter) deploys via Cloud Build triggers; runtime SAs
(sa-mis-core, sa-ai-agents,
sa-mcp-wrapper) hold bigquery.jobUser; Google service
agents as usual. Two flags for a later, separate pass (needs a runtime
audit first):
secretmanager.admin for the
Cloud Build agent lingers on both envs (expired 22.7 — inert, cleanup in
§5).| Layer | Who | Gets | Why |
|---|---|---|---|
| mis26-dev | every developer | editor + secretmanager.secretAccessor + iap.tunnelResourceAccessor + compute.osLogin | "dev לכולם": full dev loop incl. secrets, bastion tunnel, DB |
| mis26-prod | every developer | viewer + logging.viewer — nothing else | "prod צפייה בלבד"; humans read, sa-cicd writes |
| mis26-prod | exactly one break-glass owner | roles/owner | today admin@mis.org.il; Snir picks which account stays (§5.4) |
| org | snir@ only | owner (+securitycenter.admin) | shrink inherited-owner surface (§5) |
| GitHub main | approved mergers only | merge via PR + code-owner approval, no admin bypass | §6 — Snir picks the names |
snir@'s owner on dev and all SA bindings stay exactly as they are.
infra/terraform/iam.tf →
google_project_iam_member.human:
terraform apply per
workspace → done, both envs, all roles. (עדכון 26.7: tal@ אומתה והוחלה —
talyap@mis.org.il מחזיקה את מלוא הסט בשתי הסביבות; moran@
עדיין ממתינה לאימות כתובת.)Apply recipe (what was run):
cd infra/terraform && terraform init
terraform plan -var-file=dev.tfvars -target=google_project_iam_member.human
terraform apply -var-file=dev.tfvars -target=google_project_iam_member.human
terraform workspace select prod
terraform plan -var-file=prod.tfvars -target=google_project_iam_member.human
terraform apply -var-file=prod.tfvars -target=google_project_iam_member.human
terraform workspace select default
שורות ה‑
workspace selectהן לא סדר — הן הגנה. ה‑tfvars קובע איזה פרויקט התוכנית מתארת, ה‑workspace קובע לאיזה state היא נכתבת, ואם הם לא תואמים טרהפורם ישכתב סביבה אחת לתוך השנייה. יש בדיקה בקוד — infra/terraform/workspace-guard.tfמפיל את ה‑plan על צמד לא תואם, לפני שהוצעה הריסה אחת. אבל השורות למעלה מריצות-target, ותוכנית ממוקדת לא כוללת את המגן ולכן לא נבדקת. במתכון הזה ובכל-targetאחר, שורת ה‑select היא עדיין הדבר היחיד שעומד בינך ובין השכתוב.
Each step is independent and reversible
(add-iam-policy-binding with the same arguments restores
it). Run only after §4 grants are verified live.
# anyone-in-domain can create projects / billing accounts — close both
gcloud organizations remove-iam-policy-binding 264541084239 \
--member='domain:mis.org.il' --role='roles/resourcemanager.projectCreator'
gcloud organizations remove-iam-policy-binding 264541084239 \
--member='domain:mis.org.il' --role='roles/billing.creator'
gcloud organizations remove-iam-policy-binding 264541084239 \
--member='user:yehiamc@wix.com' --role='roles/orgpolicy.policyAdmin'
gcloud organizations remove-iam-policy-binding 264541084239 \
--member='user:yehiamc@wix.com' --role='roles/resourcemanager.projectCreator'
Org owners inherit owner on prod, bypassing everything. Only after daniel@'s §4 grants are verified (dev editor-grade + prod viewer):
gcloud organizations remove-iam-policy-binding 264541084239 \
--member='user:daniel@mis.org.il' --role='roles/owner'
MIS@smart26.org (shared legacy account) — keep organizationAdmin as the org-level recovery identity, drop full owner; it stays direct owner of mis26-platform (ramzor) either way, so nothing there breaks:
gcloud organizations remove-iam-policy-binding 264541084239 \
--member='user:MIS@smart26.org' --role='roles/owner'
Today prod's only direct owner is admin@mis.org.il (snir@ owns it via org inheritance). Option A (recommended): make snir@ the direct prod owner and retire admin@'s binding — one identity, MFA'd, auditable:
gcloud projects add-iam-policy-binding mis26-prod \
--member='user:snir@mis.org.il' --role='roles/owner'
gcloud projects remove-iam-policy-binding mis26-prod \
--member='user:admin@mis.org.il' --role='roles/owner'
Option B: keep admin@ as-is (documented break-glass). Do NOT run both halves of A separately — add first, verify login, then remove.
for P in mis26-dev mis26-prod; do
gcloud projects remove-iam-policy-binding $P \
--member="serviceAccount:$(gcloud projects describe $P --format='value(projectNumber)' | xargs -I{} echo service-{}@gcp-sa-cloudbuild.iam.gserviceaccount.com)" \
--role='roles/secretmanager.admin' \
--condition='title=cloudbuild-connection-setup,expression=request.time < timestamp("2026-07-22T09:45:10.722Z")'
done
# prod's condition timestamp differs: 2026-07-22T22:17:03.176Z — adjust.
# Expired conditions are inert; this is hygiene, zero urgency.
Deferred (separate change, needs runtime audit): tighten the default compute SA's roles/editor on both envs. Explicitly out of scope — SA bindings frozen.
Goal: merging dev→main only by approved mergers, no self-merge, no silent admin bypass. Suggested roster — mergers: Snir, Moti, Moran; developers (push branches + PR, no main merge): Daniel, Tal.
Write access so devs can actually work (read can't push a branch) — and without write, Moran's CODEOWNERS seat stays ignored:
for U in moranzeevi Daniel-Gove talyape; do
gh api -X PUT repos/MIS-Make-It-Simple/sentrix/collaborators/$U \
-f permission=push; doneTighten protect-main (ruleset 19483445):
require_last_push_approval: true — whoever pushed last
cannot be the approver, so nobody merges their own unreviewed work
(GitHub already blocks approving your own PR; this closes the
push-to-someone-else's-PR hole). Keep 1 required approval + code-owner
review + the 4 checks.bypass_actors: drop
OrganizationAdmin/always (or downgrade to
pull_request bypass only) — today Snir+Moti can push main
directly.guard-dev stays as-is (direct push to dev is the
intended dev flow).
Apply via:
gh api -X PUT repos/MIS-Make-It-Simple/sentrix/rulesets/19483445 --input ruleset.json
after Snir signs off the roster — not executed in this run (names are
his call).
Verification snapshot (24.7.2026, post-apply): see PR checks +
gcloud projects get-iam-policy mis26-{dev,prod} —
daniel@/moti@ hold exactly the §3 roles; no binding was removed anywhere
(etag-diff = adds only).
הבייסליין בקצרה: על כל PR רצות שלוש סריקות חוסמות — Semgrep (SAST,
כולל p/security-audit), Trivy (תלויות + IaC ברמת
CRITICAL/HIGH) ו‑gitleaks (סודות, על מלוא ההיסטוריה) — לצד בדיקות
ה‑ci (פרק 5). ממצא חדש: ברירת המחדל היא תיקון; השתקה רק
נקודתית (nosemgrep / .trivyignore עם מזהה, נימוק ותאריך
תפוגה) ומתועדת. מפתח חדש מתחיל מאפס — אין צורך להכיר את היסטוריית
התיקונים.
היומן המלא — כל טריאז', כל פסיקה, החוב הפתוח ומיפוי בקרות ה‑ISO — חי
ב‑docs/SECURITY-BASELINE.md. מדיניות ו‑SLA:
docs/SECURITY.md.
מקור:
docs/MIGRATION-RUNBOOK.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.
גרסה 1.0 · 20.7.2026. מלווה את שלב 6-7 ב-
WORK-PLAN.md. מטרה: מעבר מ-Wix ל-GCP בלי סיכון לפרודקשן החי (300+ משאיות, מאות מצלמות), עם קריטריוני-יציאה פורמליים ו-rollback. לא "שלב מעבר" פתוח בזמן.
המשאיות והעמודים בשטח ממשיכים לשדר בדיוק כמו היום (REST, אותו חוזה). מוסיפים כתיבה כפולה (Wix + GCP), מאמתים זהות נתונים לאורך חלון מוגדר, ורק אז מפנים את הקצה ל-GCP ומכבים את Wix.
מקור:
docs/DR-RUNBOOK.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.
v1.0 · 24.7.2026. Owner: Snir. Covers both Sentrix databases (
mis_core,security_coreon Cloud SQLsentrix-pg, per env). Code:services/db-backup,infra/terraform/sql.tf+backup.tf. Restore requires the age private key that exists ONLY offline with Snir (password manager) — see §6.
| Layer | What | Where | Survives |
|---|---|---|---|
| Internal | Cloud SQL automated backups (7 kept, nightly 02:00 UTC) + PITR (7 days of WAL) | GCP eu multi-region (not
me-west1) |
zone loss, region loss, bad deploy / data mistake (PITR) |
| External | Nightly pg_dump per DB → age-encrypted
→ S3 (AWS) |
Snir's AWS account, suggested eu-central-1 |
losing the GCP project/account/org itself, GCP-wide incident |
The external job runs 02:45 Asia/Jerusalem (Cloud Scheduler
db-backup-nightly → Cloud Run job db-backup,
per env). While AWS_BACKUP_CREDENTIALS is empty the run
logs exactly backup target not configured and exits 0 —
silence is intentional until the AWS side exists (§3).
mis26-dev / mis26-prod), or
gcloud sql backups list --instance=sentrix-pg.s3://<bucket>/sentrix/<env>/<db>/<YYYY>/<MM>/<db>-<UTC stamp>.dump.age
e.g.
s3://mis26-sentrix-backups/sentrix/prod/mis_core/2026/07/mis_core-2026-07-25T00-45-12Z.dump.agepg_dump --format=custom --no-owner --no-acl,
then age -r <public key>. Public key (also in Secret
Manager AGE_BACKUP_PUBLIC_KEY, both projects):
age14lwzfrrmdgcyr2kcfy29xq83nwzw7e99mp7yy5un9pq3mzjumcvshuhr5rEverything below is OUTSIDE GCP on purpose — separate provider, separate credentials, so no single account compromise reaches both copies.
Account: use a dedicated AWS account (or a clean account in an AWS Organization) with MFA on the root user. Nothing else runs in it.
Bucket: name suggestion
mis26-sentrix-backups, region eu-central-1
(Frankfurt — deliberately NOT il-central-1: me-west1 is also Tel Aviv, a
metro-level event must not take both copies). Keep all "block public
access" settings ON. Enable versioning (an
overwritten/deleted object keeps its previous version).
Lifecycle (bucket → Management → Lifecycle rule, applies to all objects):
IAM user (not root): e.g.
sentrix-backup-writer, programmatic access only, with this
inline policy — PUT-only, single prefix, least privilege:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "SentrixBackupPutOnly",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mis26-sentrix-backups/sentrix/*"
}]
}
No Get/List/Delete: a leaked key can add objects but can never read or destroy the backup history (versioning catches malicious overwrites).
Wire the credentials into BOTH projects — the
payload is one JSON object (all four fields required;
prefix optional):
{"accessKeyId":"<ACCESS_KEY_ID>","secretAccessKey":"<SECRET_ACCESS_KEY>","region":"eu-central-1","bucket":"mis26-sentrix-backups"}
printf '%s' '{"accessKeyId":"...","secretAccessKey":"...","region":"eu-central-1","bucket":"mis26-sentrix-backups"}' \
| gcloud secrets versions add AWS_BACKUP_CREDENTIALS --project=mis26-dev --data-file=-
# repeat with --project=mis26-prodVerify the first backup landed (no need to wait for the night):
gcloud run jobs execute db-backup --region=me-west1 --project=mis26-dev --wait
gcloud logging read 'resource.type="cloud_run_job" resource.labels.job_name="db-backup"' \
--project=mis26-dev --limit=20 --format="value(textPayload)"
Expect two uploaded s3://... lines +
backup completed: 2 database(s), then the objects in the S3
console (Frankfurt). Repeat for prod. A malformed JSON payload FAILS the
run on purpose (a typo must not look like "not configured").
Practice §5 before you ever need this section. Never restore over prod "to see if it works" — restore to a fresh instance and cut over.
gcloud sql instances clone sentrix-pg sentrix-pg-restore \
--point-in-time "2026-07-24T22:30:00Z" --project=<proj>gcloud sql backups list --instance=sentrix-pg --project=<proj>
gcloud sql backups restore <BACKUP_ID> --restore-instance=sentrix-pg-restore --project=<proj>weigh_tickets timestamps,
then repoint the DATABASE_URL +
SECURITY_CORE_DATABASE_URL secrets at the new private IP
and redeploy (or update the secrets and restart revisions). Old instance
stays until sign-off.Needs: the S3 objects, the age private key (§6), any Postgres 18 host.
# 1. fetch (any machine with the AWS creds that can read — root/admin, not the PUT-only user)
aws s3 cp s3://mis26-sentrix-backups/sentrix/prod/mis_core/2026/07/mis_core-<stamp>.dump.age .
# 2. decrypt — paste the private key (AGE-SECRET-KEY-1...) into a file first
age -d -i age-backup.key -o mis_core.dump mis_core-<stamp>.dump.age
# 3. restore into an empty database
createdb mis_core
pg_restore --no-owner --no-acl -d mis_core mis_core.dump
Repeat per database. Then run Prisma migrations check + app smoke test.
A backup that was never restored is a hope, not a backup.
mis_core object from S3 (dev prefix
is fine).pg_restore into a scratch DB (bastion or local Docker
postgres:18).SELECT count(*) FROM "WeighTicket"; + latest
timestamp within the last day; one spot query on
security_core too every quarter.docs/DECISIONS.md or the ops log; fix anything that
dragged.Also monthly: confirm S3 shows ~30 new objects/env/DB and the lifecycle is transitioning old ones (cost stays flat).
AGE_BACKUP_PUBLIC_KEY secret). The PRIVATE half
(AGE-SECRET-KEY-1...) lives ONLY offline: password manager
entry "Sentrix backup age key" (+ optional printed copy in a safe).
It must never enter GCP, the repo, chat logs, or CI —
an attacker with GCP access must not be able to read the off-cloud
copy.gcloud secrets versions add AGE_BACKUP_PUBLIC_KEY (both
projects) with the new public key — from the next night new objects are
recoverable again; internal GCP backups still cover the gap.Impact: Cloud Run, the LB and the SQL PRIMARY are regional — down.
The backups are not: eu multi-region (internal) + AWS
(external).
gcloud sql instances create sentrix-pg --region=europe-west1 ...
(match sql.tf settings), then restore the latest eu backup
into it (§4A works cross-region because the backup is
multi-region).terraform apply with a region
override is the long path; the short path is
gcloud run deploy of the existing images (Artifact Registry
sentrix repo is regional — pull via
me-west1-docker.pkg.dev if reachable, else rebuild from the
repo, which is on GitHub, not in the region).The S3 dumps + the GitHub repo are the complete recovery kit. No GCP access is assumed at any step.
services/*/Dockerfile, plain Node 20 images) —
docker build + run with env from §2 of each service README.
The only GCP-isms to replace: Secret Manager reads (env vars), Pub/Sub
(ingest bus), BigQuery lake, Identity Platform. That is a rebuild
project measured in days, not a data-loss event: the businesses'
weighing/detection history is in the dumps.backup target not configured (grep-stable; the string is
pinned by a unit test). No alert fires — this is the expected state
until §3 is done.gcloud run jobs execute db-backup --region=me-west1 --project=<proj> --wait.מקור:
docs/SCALE-PLAN.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.
נכתב 24.7.2026. הרקע: בטבלאות המורשת (ramzor, אירועי עמודים) יש כבר עשרות מיליוני שורות. הטבלאות החדשות יגיעו לשם: ScanEvent ~7M לשנה כבר בעומס של היום, TruckEvent יעבור 100M לשנה ברגע שטלמטריה רציפה תחובר, Detection בפריסת עמודים מלאה — עשרות אלפים ביום. המסמך הזה קובע איך PostgreSQL נשאר מהיר וזול כשזה קורה, ומתי בדיוק פועלים.
DROP TABLE scan_event_2026_01) היא מיידית
ובחינם. לכן: partitioning חודשי לפי זמן על כל טבלת
אירועים, ו-retention = ניתוק הפרטישנים שיצאו מהחלון.| טבלה | עמודת הזמן | חלון חם ב-SQL | ארכוב | הערות |
|---|---|---|---|---|
| TruckEvent | occurredAt | 90 יום | כבר באגם דרך ה-bus | לפרטש עכשיו, כשהיא קטנה — לפני חיבור הטלמטריה. הזולה ביותר לטפל בה היום |
| ScanEvent | scannedAt | 13 חודשים (השוואה שנה-על-שנה בדשבורד) | כבר באגם | הראשונה שתגיע ל-5M — לפרטש בסבב הזה |
| Detection (security_core) | detectedAt | 90 יום | detections_history באגם | המדיה ממילא ב-Cloud Storage — השורה קלה |
| SecurityEvent (mis_core) | startAt | 90 יום | כבר באגם | אותו טיפול כמו Detection |
| WeighTicket | weighedAt | 25 חודשים | weigh_tickets_history באגם | רשומה עסקית (חיוב/התאמות) — חלון נדיב יותר, נפח נמוך (0.5M לשנה) |
| ContainerEvent | recordedAt | 13 חודשים | ג'וב לילי → אגם | נפח קטן; פרטישן רק כשעוברים סף |
| StationVisit | enteredAt | 90 יום לשורות סגורות | לא נדרש (נגזרת של tickets) | שורות פתוחות לעולם לא נמחקות |
| Alert / CameraRtspError | createdAt / occurredAt | 13 חודשים / 90 יום | ייצוא לילי אם יידרש | Alert נשאר נגיש לתחקור שנה |
| TruckDailySummary / ReportRow | day | ללא מחיקה (שורה ליום, זעיר) | — | אלה הטבלאות שהדשבורד אמור לשאול במקום ה-raw |
retention בפועל = החלטת עסק (חוזה לקוח + חוק הגנת הפרטיות, D17). המספרים כאן הם ברירת המחדל הטכנית; שינוי = עדכון פרמטר בג'וב התחזוקה, לא שינוי קוד.
prisma migrate dev --create-only ואז עורכים את ה-SQL), פעם
אחת.(id)
ל-(id, scannedAt). אין לזה השפעה על Prisma (findUnique לפי
id מוחלף ב-findFirst או מקבל את שני השדות) — נקודת שינוי הקוד היחידה,
והיא קטנה כי בטבלאות אירועים כמעט אין שליפה-לפי-id./jobs/partition-maintenance) שמריץ CREATE/DETACH. מתחילים
בג'וב שלנו (שקוף, נבדק, באותו מנגנון jobs קיים), עוברים ל-pg_partman אם
יימאס.| מה | למה |
|---|---|
btree (tenantId, <time> DESC) — האינדקס המוביל
בכל טבלת אירועים |
כל שאילתת דשבורד היא "הלקוח הזה, החלון הזה, מהחדש לישן" |
btree ייעודי לנתיב החם השני:
(tenantId, truckId, occurredAt) /
(tenantId, cameraId, detectedAt) /
(tenantId, licensePlate) |
מסך "משאית בודדת" / "מצלמה בודדת" / חיפוש לוחית |
| BRIN על עמודת הזמן בטבלאות append-only ענקיות (TruckEvent) | אינדקס של קילובייטים על מיליוני שורות; מצוין לסריקות טווח, כמעט חינם בכתיבה |
partial index:
(tenantId, createdAt) WHERE status='OPEN' על Alert,
WHERE clearedAt IS NULL על StationVisit |
הפעמון ומסך "מי בפנים" קוראים רק את הפתוחות — אינדקס זעיר במקום סריקה |
| לא מוסיפים אינדקסים "ליתר ביטחון" על טבלאות ingest | כל אינדקס מאט את כל ה-inserts; 65 אלף כתיבות ביום סופרים את זה |
| PK UUIDv4 נשאר בינתיים | הפיזור האקראי מנפח מעט את ה-btree; לא שווה שבירת תאימות עכשיו. UUIDv7 = שיפור עתידי בקוד בלבד |
(time, id) < האחרון שהוצג — לא OFFSET, שנהיה איטי
ליניארית עם העומק.| מדד | סף | פעולה |
|---|---|---|
| שורות בטבלת אירועים | > 5M או > 10GB | לפרטש (סעיף 7). TruckEvent: לא מחכים — מפרטשים לפני חיבור הטלמטריה |
| p95 של שאילתת דשבורד | > 300ms מתמשך | EXPLAIN, לוודא pruning ואינדקס מוביל; לשקול cache (Redis, D15) |
| Cloud SQL storage | > 60% מהדיסק | לוודא שה-retention רץ; auto-grow דלוק, אבל דיסק רק גדל ב-GCP — retention הוא החיסכון האמיתי |
| CPU > 70% מתמשך / connections > 80% | לפי COSTS.md | tier למעלה / read replica / pooler. db-g1-small של היום הוא נקודת פתיחה בלבד |
| INSERT latency ב-ingest עולה | p95 > 100ms | לבדוק אינדקסים עודפים, לעבור ל-batch insert בקונקטורים |
מיגרציית SQL אחת (expand-contract, בחלון שקט):
BEGIN;
ALTER TABLE "TruckEvent" RENAME TO "TruckEvent_old";
CREATE TABLE "TruckEvent" (
LIKE "TruckEvent_old" INCLUDING DEFAULTS INCLUDING INDEXES,
PRIMARY KEY ("id", "occurredAt")
) PARTITION BY RANGE ("occurredAt");
-- פרטישן פתיחה + הבא, ואז הג'וב החודשי ממשיך לבד
CREATE TABLE "TruckEvent_2026_07" PARTITION OF "TruckEvent"
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
CREATE TABLE "TruckEvent_2026_08" PARTITION OF "TruckEvent"
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
INSERT INTO "TruckEvent" SELECT * FROM "TruckEvent_old"; -- קטנה היום, שניות
DROP TABLE "TruckEvent_old"; -- אחרי אימות ספירות; או משאירים שבוע ליתר ביטחון
COMMIT;
CREATE INDEX ON "TruckEvent" USING brin ("occurredAt");
כשהטבלה כבר גדולה, INSERT...SELECT אחד נועל יותר מדי. עושים את זה בשלבים:
-- 1) יוצרים טבלה מפורטשת חדשה לצד הישנה (אותו מבנה, PK כולל scannedAt)
CREATE TABLE "ScanEvent_p" (LIKE "ScanEvent" INCLUDING DEFAULTS,
PRIMARY KEY ("id","scannedAt")) PARTITION BY RANGE ("scannedAt");
-- + פרטישנים לכל חודש קיים ולחודשיים קדימה
-- 2) גב מילוי בנתחים חודשיים, מהישן לחדש (כל נתח בטרנזקציה קצרה משלו)
INSERT INTO "ScanEvent_p" SELECT * FROM "ScanEvent"
WHERE "scannedAt" >= '2026-01-01' AND "scannedAt" < '2026-02-01';
-- 3) חלון קצר: משלימים את הזנב מאז תחילת הגב-מילוי, ומחליפים שמות
BEGIN;
LOCK TABLE "ScanEvent" IN EXCLUSIVE MODE;
INSERT INTO "ScanEvent_p" SELECT * FROM "ScanEvent" s
WHERE NOT EXISTS (SELECT 1 FROM "ScanEvent_p" p
WHERE p.id = s.id AND p."scannedAt" = s."scannedAt");
ALTER TABLE "ScanEvent" RENAME TO "ScanEvent_retired";
ALTER TABLE "ScanEvent_p" RENAME TO "ScanEvent";
COMMIT; -- שניות של נעילה, לא שעות
ה-ingest ממשיך לכתוב לאותו שם טבלה; Prisma לא מרגיש. "ScanEvent_retired" נשארת שבוע לאימות (ספירות פר יום מול האגם) ואז DROP.
לכל טבלה מפורטשת, לפי טבלת קונפיג קטנה (table, timeColumn, hotMonths):
אין להן BigQuery subscription, אז לפני DROP: ג'וב לילי מייצא את החודש
הסגור לאגם (INSERT ל-BQ דרך ה-API בצ'אנקים, או EXPORT ל-GCS ו-load). רק
אחרי אישור ספירות — ניתוק. אותו קוד ג'וב, דגל
exportFirst=true בקונפיג.
require_partition_filter דלוק על ה-raw: אי אפשר לשרוף
כסף בשאילתה בלי טווח.הצעד הממשי הבא: מיגרציית 7.1 (TruckEvent) + endpoint
/jobs/partition-maintenance עם טבלת הקונפיג — PR אחד, לפני
שקונקטור הטלמטריה של איתוראן נדלק.
מקור:
docs/DATA-LAKE.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.
סטטוס: חי בשתי הסביבות.
sentrix_lake(כוללingest_events, שמוזן ישירות מ-Pub/Sub ב-BigQuery subscription בשםlake-bq) ו-sentrix_marts(mart ראשון:weigh_monthly) פעילים ב-dev וב-prod, מנוהלים ב-Terraform. ראיות ופקודות אימות:docs/evidence/2026-07-25/infra-pack.md§2.עדכון 26.7.2026 — שכבת המודלים חיה (
infra/terraform/lake-models.tf, שתי הסביבות): שלושה scheduled queries יומיים בונים marts חדשים — fuel_anomalies_daily(חשד לאובדן דלק: ירידות מפלס לא-מוסברות מול המדיאן של המשאית עצמה, סטטיסטיקה כנה בלי טענת ML), fill_predictions(תחזית מילוי לפי קצב פינוי פר אזור) ו-fleet_utilization_daily(ניצולת צי). נוספה גם truck_events_history. שורות סימולציה מסומנות isDemoוהדשבורד מציג עליהן תג "דמו". הג'וב lake-anomalies-pullהופך אנומליה מה-mart להתראת FUEL_THEFT בפעמון. הראיות:docs/evidence/2026-07-26/lake-models.md.
Cloud SQL מחזיק את ההווה — מה שהדשבורדים שואלים כל שנייה. BigQuery מחזיק את העבר — כל אירוע שאי פעם קרה, זול, ערוך לחיתוכים. מפרידים כדי שההיסטוריה לא תנפח את ה-DB התפעולי ולא תאט אותו (אותו עיקרון כמו truck_state מול truck_telemetry בשלב 1 של תוכנית העבודה).
| שכבה | מה גר בה | מי קורא | סדר גודל |
|---|---|---|---|
| Cloud SQL (חם) | state נוכחי: משאיות, אתרים, טיקטים אחרונים, משתמשים | דשבורדים, mis-core | חודשים אחרונים |
BigQuery — sentrix_lake |
היסטוריה גולמית append-only: שקילות, זיהויים | jobs של marts בלבד | שנים |
BigQuery — sentrix_marts |
אגרגטים מוכנים: פר רשות/אתר/חודש | דוחות, סוכני AI (דרך MCP) | קטן |
קצה (משאיות/עמדות/מצלמות)
│ REST כרגיל — הקצה לא משתנה
▼
ingestion (truck-ingest / pole-ingest)
│ נרמול ופרסום
▼
Pub/Sub topic ──► subscription: mis-core (עדכון state ב-Cloud SQL)
──► subscription: alerts-engine (כללים והתראות)
──► BigQuery subscription (append ל-sentrix_lake, בלי קוד)
ה-fan-out הוא הנקודה: אף שירות לא בנתיב הכתיבה של אחר. ההזנה לאגם היא subscription מסוג BigQuery — Pub/Sub כותב ישירות לטבלה, אין שירות ETL לתחזק. מדיה (תמונות/וידאו) לעולם לא עוברת בהודעות — Cloud Storage + reference בלבד (גבול קשיח מ-AGENTS.md). עד ששלב 5 יקום, אפשר להתחיל גם בלעדיו: job לילי ב-mis-core שמעתיק שורות שסגרו יום מ-Cloud SQL לאגם. שתי הדרכים כתובות בקובץ ה-Terraform.
שתי טבלאות פתיחה, סכמות נגזרות מ-Prisma
(services/mis-core/prisma/schema.prisma):
weigh_tickets_history — מקור: מודל
WeighTicket. partition יומי על weighed_at,
clustering על tenant_id, site_number. דנורמליזציה אחת
מכוונת: site_number נוסע עם השורה כדי שחיתוך פר-אתר לא
ידרוש join.
detections_history — מקור: מודל
SecurityEvent (זיהויי VCA/ANPR). partition יומי על
start_at, clustering על tenant_id, event_type.
בשלב 4.1 הבעלות עוברת ל-security-core; העמודות נשארות, רק הכותב
מתחלף.
שתיהן עם require_partition_filter — שאילתה בלי טווח
תאריכים נחסמת. זה שומר העלות קרוב לאפס גם כשמישהו טועה, ומחנך את כולם
לשאול נכון מהיום הראשון. tenant_id חובה בכל שורה (חוק 5) —
אין שורת אגם בלי שיוך רשות.
הסיפור של האגם הוא לא "לאחסן הכל", אלא לענות מהר וזול על החיתוכים שחוזרים בכל דוח ובכל שאלה לסוכן:
-- טונאז' חודשי: רשות ⨯ אתר ⨯ כיוון (הבסיס לדוח החודשי לרשות)
SELECT tenant_id, site_number,
FORMAT_TIMESTAMP('%Y-%m', weighed_at, 'Asia/Jerusalem') AS month,
direction, SUM(net_kg)/1000 AS tons, COUNT(*) AS tickets
FROM sentrix_lake.weigh_tickets_history
WHERE weighed_at BETWEEN @from AND @to
GROUP BY 1, 2, 3, 4;
-- השוואת ספקים לתחנה (הבסיס להתאמות compareMonth מהאתר הישן)
SELECT site_number, source, FORMAT_TIMESTAMP('%Y-%m', weighed_at) AS month,
SUM(net_kg) AS net_kg
FROM sentrix_lake.weigh_tickets_history
WHERE weighed_at BETWEEN @from AND @to AND tenant_id = @tenant
GROUP BY 1, 2, 3;
-- עומס זיהויים פר מצלמה בחודש (תחקור ביטחון)
SELECT channel_name, event_type, COUNT(*) AS events
FROM sentrix_lake.detections_history
WHERE start_at BETWEEN @from AND @to AND tenant_id = @tenant
GROUP BY 1, 2 ORDER BY events DESC;
שאילתות כאלה רצות ב-scheduled query ומטריאליזציה שלהן נשמרת
ב-sentrix_marts (דוגמה חתומה בקובץ ה-Terraform:
weigh_monthly). מכאן:
services/mcp-wrapper). הסוכן לא כותב SQL חופשי על ה-raw:
הוא קורא לכלי בסגנון get_reconciliation_summary, הכלי פונה
ל-endpoint שקורא mart, וההרשאה היא של המשתמש שמאחורי הסוכן
(pass-through, בלי עקיפה). ל-SA של הסוכנים יש קריאה על
sentrix_marts בלבד — ה-raw סגור בפניו.tenant_id,
וכל גישה עוברת דרך שירות שאוכף אותו. אין גישת BI ישירה של לקוח
לאגם.מחירון BigQuery (סדרי גודל, on-demand): אחסון פעיל 0.02/GBלחודש(יורדלחציאחרי90יוםבלישינוי), סריקה 6.25/TB. חינם כל חודש: 10GB אחסון + 1TB סריקות.
הערכת נפח אצלנו (2KB לשורת שקילה כולל raw, 1.5KB לזיהוי):
| זרם | קצב משוער | לחודש | לשנה |
|---|---|---|---|
| שקילות (כל התחנות) | ~1,500 ליום | ~0.1GB | ~1.1GB |
| זיהויים/סריקות (כל הרשויות) | ~20,000 ליום | ~0.9GB | ~11GB |
| סה"כ | ~1GB | ~12GB |
מסקנה: בקנה המידה הנוכחי האגם עולה בפועל 0 ש"ח, וגם בפי-10 נפח מדובר בעשרות שקלים בחודש. העלות האמיתית היחידה היא משמעת שאילתות — והיא נאכפת בקוד.
בוצע — האגם חי בשתי הסביבות: datasets, טבלאות ההיסטוריה, subscription
lake-bq (Pub/Sub כותב ישירות ל-BigQuery, בלי קוד ETL)
וה-mart הראשון. הרחבת האגם מכאן = עוד טבלה או mart ב-Terraform, דרך PR
רגיל.
תמחור, break-even והחלטות מכירה הם דיון הנהלה. מה שמופיע כאן הוא החלק שנוגע לקוד: איך המערכת מודדת צריכה, ואיזו אמת כל מספר מתיימר להיות. מי שנוגע במסכי הצריכה, במדידה או בחשבונאות — זה החוזה שלו.
עלות_לקוח_בחודש = Σ(יחידות × עלות_משתנה_ליחידה) [ישיר — מיוחס]
+ (יחידות_עומס_לקוח / יחידות_עומס_כלל) × F [משותף — מוקצה]
+ AI(טוקנים בפועל × מחירון) [מיוחס]
F הוא העלות המשותפת: מה שרץ בין אם יש לקוח אחד או מאה (מסד, Redis, LB, bastion, gateway). היא לא מיוחסת ללקוח אלא מוקצית לפי חלקו בעומס.
לקוח מחזיק משאיות, מצלמות, תחנות וחיישנים, ולכל אחד מהם עלות שונה. LU
מנרמל את כולם למטבע אחד כדי שיהיה אפשר לחלק את F. המשקלות
יושבות ב‑rates.ts (LU_MILLI_WEIGHTS):
| נכס | LU | ההנמקה |
|---|---|---|
| משאית | 1.0 | הבסיס: אירועים, תמונות, טלמטריה |
| מצלמת עמוד | 0.5 | זיהויים רבים, תמונות קטנות, בלי טלמטריה רציפה |
| תחנת שקילה | 2.0 | תעודות + תמונות + ingest רציף + התאמות |
| חיישן מכולה | 0.05 | דגימות בודדות ביום |
| משתמש דשבורד פעיל | 0.2 | קריאות API ו‑cache; ה‑AI מחויב בנפרד |
המשקלות האלה הן מדיניות הקצאה, לא מדידה. הן אומרות שתחנת שקילה עולה לנו בערך פי שתיים ממשאית, וזה הכל. מי שמשנה אותן משנה את ההקצאה של כל הלקוחות ולכן הן מכוילות מול ייצוא החיוב, לא מנוחשות מחדש.
טבלת UsageCounter היא בגרעין של (טננט × יום × מטריקה ×
feature), ולכן כתיבה חוזרת של אותו יום דורסת ולא מסכמת
— זה מה שהופך את ה‑rollup לבטוח להרצה חוזרת. MeterMetric
מחזיק היום:
| קבוצה | מטריקות | מי מגדיל |
|---|---|---|
| קליטה ותעבורה | INGEST_EVENTS, API_REQUESTS,
DB_ROWS_WRITTEN |
תפרים אוטומטיים: כתיבות Prisma, ו‑hook על כל תשובה |
| AI | AI_TOKENS_IN, AI_TOKENS_OUT |
rollup לילי מיומן AiUsage |
| gauges | LOAD_UNITS, STORAGE_BYTES |
rollup לילי (האחסון טרם מחובר — פרק 28) |
| פיצ'רים בתשלום | REPORT_RENDERS, VIDEO_SESSIONS,
ROUTE_OPTIMIZATIONS, WEIGH_CERTIFICATES |
הפיצ'ר עצמו, בזמן השימוש |
| חיוב | TRUCK_MODULE_UNITS |
rollup חודשי מ‑TruckModuleUsage — הגרעין שההצעות
מתמחרות |
| וידאו | VIDEO_EGRESS_BYTES |
בייטים שיוצאים מהכספת — הרגל של הווידאו שעולה כסף בפועל |
| פלטפורמה | OPENAI_ORG_COST |
הג'וב שמושך את הוצאת OpenAI, על טננט הפלטפורמה |
DB_ROWS_WRITTEN היא קירוב מוצהר, וזה
כתוב במקום שבו היא נמדדת. מי שקורא את המסכים דרך ה‑API לא יכול לנפח את
המספרים: כתיבה למונים האוטומטיים נדחית ב‑400.
מספר בלי נייר עבודה הוא קישוט, ולכן תשובת הצריכה נושאת
provenance עם ארבע עובדות נפרדות, כי אין
להן אותה דרגת אמון:
| העובדה | ה‑source שלה | למה |
|---|---|---|
tokens |
measured |
הספק החזיר את הספירות, והן נשמרו פר קריאה |
money |
list-price (ה‑worst-of של המודלים שחויבו) |
הספירות מדודות, אבל המחיר הוא מחירון פומבי. לא
ראינו חשבונית, ולכן זה לעולם לא measured |
usageCounters |
measured |
תפרים אוטומטיים |
storage |
unpriced |
מד האחסון טרם מחובר, ולכן לא מוצג מספר במקום ניחוש |
הסולם המלא: measured ⟵ list-price ⟵
inventory ⟵ configured ⟵
unpriced, ובנוסף declared בפני עצמו במסך המנה
של הטוקנים — "מספר שהכרזנו עליו", לא מדוד ולא מתומחר. יש טסט מראה שמכריח
את טבלת התעריפים בקוד ואת ה‑tfvars להסכים (פרק 13).
F נפתרת בזמן ריצה בשלושה שלבים, בסדר הזה:
measured — הסכום בפועל לחודש מייצוא החיוב;
configured — מה שה‑ops הגדיר;
inventory — הקבוע שבקוד. ייצוא החיוב טרם מופעל, ולכן
בפועל אנחנו בשלב 2–3 וכל סכום כספי במסכים הוא הערכה
מסומנת (פרק 28, ומשימה 7 בפרק 30).
הקצאת טוקנים מוקדמת (TokenAllocation) מול יומן
AiUsage נותנת אחוז ורמה: 70% אזהרה, 90% התראה, 100% עצירה.
ב‑100% ה‑AI נעצר — צ'אט מחזיר 429 ai_quota_exhausted עד
טעינה מחדש. אין הקצאה ⇒ אין שער, ואף פעם לא עצירה.
הכלל שגובר על הכל הוא D18: המוצר עצמו לא נעצר. פיצ'ר
שהוא פלט מחויב — תעודת שקילה, קודם כל — מציג צריכה אבל
לעולם לא מרים באנר אזהרה ולא נחסם. תחנת שקילה שעברה את
מספר התעודות שלה עדיין חייבת למסור לנהג תעודה. בחוזה זה השדה
enforceable: false, והבאנר העליון מחושב מה‑worst-of של
הפיצ'רים הניתנים לאכיפה בלבד.
עלויות, תקציבים וכלל אישור‑העלות (כלל 9 — אומדן
ב‑me-west1 לפני יצירת כל דבר בתשלום) נמצאים בפרק 15, ומי
מאשר — בפרק 29.
מקור:
docs/CAPABILITY-MATRIX.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו.
הוכן 23.7.2026 לבקשת שניר: ריכוז כל היכולות שהמוצר מוכר/משווק, מיפוי לסוג משאית, הפרדה בין מה שרץ בענן (הדשבורד שלנו) למה שרץ בקצה (על המשאית), והצלבה מול מה שכבר בנוי בריפו. בסוף: (1) פערי ענן לשילוב בתוכנית העבודה; (2) "תוכנית מערכות המשאיות" — רשימת יכולות הקצה לריכוז.
עדכון סטטוס 26.7.2026 — הטבלאות למטה משקפות את מיפוי 23.7; מאז נסגרו בקוד ונפרסו ל-dev (שורה ליכולת, הראיות ב-
docs/evidence/2026-07-25..26/): משפחת CARGO — 8 מודולים (דלת, משטחים, חבילות, ברקוד, POD, צילום אספקה, פריקה, החזרות) עם מנייה פר-משאית · מודולי דלק — תיעוד פריקה, יומן שטח, אנליטיקת אובדן · P2P עבודות עפר — צילום תעודה ⇐ OCR אמיתי (Document AI) ⇐ שורת חיוב · מנוע מסלולים (תכנון מול ביצוע, אופטימיזציה, geometry) + מודול NAVIGATION לכל סוגי הרכב, משולב במשימות ובר-אישור פר-משתמש · מנוע התראות כשירות (alerts-engine, dev) עם כללי עמידה ועבודות-פרטיות מודעי-סוג-רכב · מרשם תחנות ארצי (298 תחנות, geofence אוטומטי, אירועי כניסה) · דוח רגולטורי Proof-of-Service (ביוביות/הגנ"ס, PDF RTL) · הנהלת חשבונות (BillingLine, דפי חשבון) · וידאו — תחקור ארוך-טווח · מודלי אגם (אנומליות דלק, תחזית מילוי) + מסכי צריכה 70/90/100% · בורסות פסולת והובלה · קונקטור לידים מ-monday חי; hikvision/pointer/digital-matter/svision מוכני-קרדנצ'לס · כל 11 הסגמנטים חיים עם טסט אפס-דליפה. הסטטוס החי:docs/WORK-PLAN.md.
| מקור | מה נלקח ממנו |
|---|---|
| הצעת מחיר פיילוט מיכליות דלק (שמעון, 23.7.2026) | מערכת בסיס + 15 מודולי AI למיכלית, כולל מחירים |
| הצעת מחיר פיילוט Cargo AI (יוסי, חברת הפצה, 22.7.2026) | מערכת בסיס + 13 מודולי AI למשאית קרגו |
דרייב Snir/mis_marketing_site/brochures-inbox/ (~40
קבצים) |
דפי מוצר פר סוג משאית: משאיות חכמות (פסולת), ביובית, רמסע, מנוף גזם, מנוף טמונים, טיאוט, עבודות עפר (Proof-to-Payment), TEMPO POD, שקילה, ביטחון (3 פתרונות), SuperVision |
| האתר השיווקי mis-26.com | web_fetch מחזיר shell של JS (Next על Vercel) — נקרא
מעותק הריפו בדרייב: mis_marketing_site/mis26-site/src/data/
(4 ורטיקלים, 32 עמודי מוצר) |
| ניתוחי legacy בריפו | docs/legacy-mainmis.md,
legacy-mgroupwix.md, legacy-mis-oldsite.md,
legacy-missecurity.md — מה רץ היום בפועל |
| הריפו עצמו | docs/WORK-PLAN.md, מודולים ב-services/,
apps/, ingestion/, סכמות Prisma |
סוגי המשאיות/הנכסים בעמודת "סוג": דחס · רמסע · מנוף גזם · מנוף טמונים/מונחים · טיאוט · ביובית · מיכלית דלק · הפצה/קרגו (POD) · עבודות עפר/צמ"ה · תחנת שקילה · עמוד/אתר ביטחון · מכולות וכלי אצירה · כולם = רוחבי לכל צי. באתר מפורסמים עוד 18 סוגים עתידיים (מערבלי בטון, מזון לבע"ח, כרייה, חקלאות, חומ"ס, מיכליות גז, אספלט, קירור, מכולות, פסולת רפואית, בע"ח, מובילי רכב, ציוד כבד, חבילות, מיכליות מים, גרירה, מיכליות חלב) — כולם ממופים לאותה ליבה + מודול ייעודי.
מקרא ריצה: ☁️ ענן (הדשבורד/שירותים שלנו) · 🚚 קצה (על המשאית/בתחנה/בעמוד) · 🔀 היברידי (קצה אוסף, ענן מעבד/מציג).
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| 4–8 מצלמות חכמות (מעטפת 360) | כולם | הצעות מחיר + דפי מוצר | 🚚 | חומרת קצה קיימת (DVR/NVR + Jetson); ענן:
truckCollection.data.truckStatus (מיפוי legacy) |
ניהול מצלמות-רכב כ-entity בענן → שלב 3.1/5.3 |
| וידאו LIVE + הקלטה | כולם | הצעות + דפי מוצר | 🔀 | קונקטור dvr/808gps: ingestion/connectors/src/dvr-808gps
(jsession, סטטוס, URL וידאו חי) |
סשן LIVE בדשבורד → שלב 3.1; צינור CMS→Cloud Storage → 5.3 |
| Playback / תחקור לאחור (3–6 חודשים) | כולם | דפי מוצר | 🔀 | legacy דרך CMS; טרם בדשבורד החדש | מסך תחקור → שלב 4.2 (ביטחון) + 3 (משאיות) |
| GPS + מסלולי נסיעה | כולם | הצעות + דפי מוצר | 🔀 | ingest 35 נקודות
ב-services/mis-core/src/modules/ingest; טלמטריה בסכמה |
מסך מפה חיה → שלב 3.4 |
| קישוריות סלולרית + עדכוני תוכנה מרחוק | כולם | הצעות | 🚚 | רישום יחידות AnyDesk (legacy jetsonOrinAnyDeskIds) |
מודול ניהול יחידות קצה בענן → חדש (ר' פערים #8) |
| פורטל ניהול + אפליקציה + אחסון ענן | כולם | הצעות | ☁️ | apps/client-app (מסכי קבלן, רשות, חדר בקרה ונהג באותו
שירות) + Median wrapper (שלב 6.3) |
השלמת 9 מסכים → שלב 3.4 |
| אבטחת מידע (Cyber) + הרשאות משתמשים | כולם | הצעות + דפי מוצר | ☁️ | Gateway+JWT+RBAC חיים; Identity Platform עם MFA | — |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| נתוני רכב: ק"מ יומי, שעות מנוע, זמן נסיעה/עמידה | כולם | שתי ההצעות (₪19.90) | 🔀 | שדות קיימים בסכמה (mileage, workTime במיפוי legacy) | ווידג'ט בדשבורד → שלב 3.4 |
| התראת עמידה מעל זמן מוגדר | כולם | שתי ההצעות (₪9.90) | ☁️ | אין מנוע התראות עדיין | Alerts Engine → שלב 3.2 |
| Geofence — יציאה מאזורים מאושרים | כולם | שתי ההצעות (₪19.90) | ☁️ | legacy updatePolygonExceptionAlerts (mgroup) ממופה;
בריפו רק תכנון |
כלל פוליגון ראשון ב-Alerts Engine → שלב 3.2 |
| התראה על עבודות פרטיות (שימוש מחוץ לשעות/מסלול) | מיכלית דלק, מנוף גזם, כולם | הצעת דלק + דף מנוף גזם | ☁️ | — | כלל שעות+מסלול → שלב 3.2 |
| ניהול תקלות + סטטוס רכיבים (מצלמה/GPS/כיול/הקלטה) | כולם | דפי מוצר + legacy | 🔀 | truckStatus ממופה; NVR watchdog ב-legacy |
מסך תקלות (תקלה חוזרת) → שלב 3.4 |
| הרשאות/תצוגות פר תפקיד ולקוח | כולם | דפי מוצר + legacy | ☁️ | Tenancy חי (tenant_id בכל טבלה, subdomain פר לקוח) | תצוגות פר-תפקיד → שלב 4.3 |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| AI/CV — ספירה וזיהוי לפי אפיון | כולם | שתי ההצעות (₪49.90) + דפי מוצר | 🚚 | הקצה מזהה; הענן קולט דרך ingest (truckDataCollection,
detections) |
— (קליטה קיימת) |
| ספירת פחים לפי צבע/גודל בדיוק מקסימלי | דחס | אתר + legacy (ramzor: color, pah*) | 🚚 | ingest + סכמה קיימים | דשבורד כמויות → שלב 3.4 |
| זיהוי חד-ערכי של כלי אצירה (ARUCO) | דחס, רמסע, מכולות | אתר + legacy (isAruco, aruco2Container) | 🚚 | endpoint קיים ב-ingest | — |
| זיהוי זיהום מיחזור (Contamination) | דחס | אתר (products.ts) | 🚚 | — | קליטת סוג אירוע חדש → שלב 5.1 |
| זיהוי העמסת יתר וגלישה (Overload) | דחס, מנוף | אתר | 🚚 | — | כנ"ל |
| ספירת מכולות + תמונה לפני/אחרי פריקה | רמסע | דף מוצר (רמסע) | 🚚 | חלקית ב-ramzor | שדות אירוע פריקה → שלב 5.1 |
| זיהוי אוטומטי של כניסה לכל תחנה בארץ | רמסע, ביובית | דפי מוצר | 🔀 | geofence על מאגר תחנות | מאגר תחנות ארצי → חדש (פערים #6) |
| צילום אוטומטי מ-2 צידי המנוף + טמון באוויר | מנוף טמונים | דף מוצר | 🚚 | — | קליטת אירוע ייעודי → 5.1 |
| צילום ארגז לווידוא סוג פסולת + חריגת טון/קוב | מנוף גזם | דף מוצר | 🔀 | שקילות קיימות; אין הצלבת נפח | כלל טון/קוב ב-Alerts → שלב 4 |
| טיאוט: מעקב מברשות/גרניק, אדם מחוץ לרכב, אפוד זוהר, נתוני רכב (מים/סלד) | טיאוט | דף מוצר + מודול mgroup | 🚚 | supervisorWorkPlan ממופה (legacy) | מסכי טיאוט → שלב 4.3 |
| LPR — זיהוי לוחית רישוי | תחנה, ביטחון, כולם | דפי מוצר + legacy | 🚚 | security-core: Detection.licensePlate + חוקי LPR
✅ |
— |
| זיהוי ברקודים (שיוך מטען למסמכים) | הפצה/קרגו | הצעת קרגו (₪49.90) | 🚚 | — | מודול קרגו → חדש (פערים #2) |
| ספירת משטחים / חבילות וארגזים | הפצה/קרגו | הצעת קרגו (₪59.90/₪69.90) + POD | 🚚 | — | כנ"ל |
| אינדיקציה+תיעוד פתיחת דלת לפי מיקום (כולל תמונה) | הפצה/קרגו | הצעת קרגו (₪29.90/₪39.90) | 🚚 | — | כנ"ל + סוג אירוע ב-ingest |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| שקילה אוטומטית במאזני גשר: LPR + מחסומים + צג נהג | תחנת שקילה | דף מוצר שקילה + אתר | 🚚 | תוכנת התחנה קיימת (legacy, חולון 270270270); ענן: ingest Prioriti* ✅ | ניהול תחנה בדשבורד → שלב 3.4 (מסך שקילה) |
| תעודות שקילה, ברוטו/טרה/נטו, in/out | תחנת שקילה | legacy (priority) | ☁️ | סכמה + ingest ✅; reconciliation
ב-modules/reconciliation ✅ |
— |
| ייבוא אקסל ספקים + השוואה חודשית | תחנת שקילה | legacy oldsite | ☁️ | modules/imports (5 פרסרים) ✅ |
מסך פערים → שלב 3.4 |
| ראש משקל KWS (alibi, fgross/ftare/fnet) | משאיות שקילה | legacy | 🚚 | token ב-legacy; ingest ממופה | אימות צרכן KWS → שלב 6 |
| ממשק ERP (Priority) + דיווח למשרד להגנת הסביבה | תחנת שקילה | דף מוצר + legacy | ☁️ | constERP ingest ✅; דיווח רגולטורי אין | דוח רגולטורי → report-engine שלב 4.5 |
| משאיות בפנים >2ש' + תחנה אונליין/אופליין | תחנת שקילה | legacy | ☁️ | ingest ✅; jobs ב-modules/jobs ✅ |
התראה במסך → שלב 3.2 |
| OCR על תעודת שקילה (צילום → קריאה אוטומטית) | עבודות עפר | דף Proof-to-Payment | 🔀 | — | מודול P2P → חדש (פערים #3) |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| הוכחת אספקה (POD) — מיקום/שעה/וידאו/דוח | מיכלית דלק, הפצה | הצעות (₪39.90) + TEMPO POD | 🔀 | — | מודול POD → חדש (פערים #2) |
| Proof of Service לכל מחזור שאיבה-פריקה | ביובית | דף ביוביות | 🔀 | — | וריאנט POD לביוביות |
| הוכחת ביצוע לכל פריקה לאורך מסלול | עבודות עפר | דף P2P | 🔀 | — | מודול P2P |
| חיוב לקוח בזמן אמת (מהשטח לחיוב) | עבודות עפר, הפצה | דף P2P | ☁️ | — | P2P + לוגיקת חיוב (מתחבר למסך הצריכה מ"תוספות 22.7") |
| בקרת החזרות ומשטחים ריקים | הפצה | TEMPO POD | 🔀 | — | מודול קרגו |
| תיעוד תדלוקי שטח (מיקום/זמן/צילום) | מיכלית דלק | הצעת דלק (₪39.90) | 🔀 | — | מודול דלק → חדש (פערים #4) |
| צילום אספקה בשטח ("ראיה לכל ליטר") | מיכלית דלק | הצעת דלק (₪29.90) | 🚚 | — | מודול דלק |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| זיהוי חשד לאובדן/גניבת דלק (מפלס מול נסיעה) | כולם (בדגש קרגו/דלק) | שתי ההצעות (₪49.90) + אתר | 🔀 | — | אנליטיקת דלק בענן → פערים #4 |
| בקרה חכמה סביב נקודות פריקה + מניעת מיקס סוגי דלק | מיכלית דלק | הצעת דלק (₪49.90) | 🔀 | — | מודול דלק |
| התראה על פריקה במיקום לא מאושר | מיכלית דלק, הפצה | הצעות (₪39.90) | ☁️ | — | כלל ב-Alerts Engine על אירועי פריקה/דלת |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| סידור עבודה + תכנון מול ביצוע | כולם | הצעות (₪39.90) + דפי מוצר | ☁️ | legacy missions ממופה; מסך "ניהול משימות" בתכנון | שלב 3.4 (מסך משימות) |
| בניית מסלולים חכמה (נקודות, חלונות זמן, קיבולת) | הפצה | הצעת קרגו (₪69.90) | ☁️ | legacy: routific | מנוע מסלולים → פערים #5 |
| אופטימיזציית מסלולים (קיצור ק"מ/זמן) | כולם | הצעות (₪99.90) + דפי מוצר | ☁️ | — | כנ"ל; פיצ'ר מתומחר → מנייה פר-שימוש |
| My Service — פניות/תלונות → משימה → נהג קרוב + ניווט | דחס, מנוף, רשויות | אתר (my-service) | ☁️ | legacy userRequests+Monday ממופה | אפליקציית משימות → שלב 4.3 |
| קריאות מוקד עירוני (מוקד בינה) אצל הנהג | רשויות (mgroup) | legacy | ☁️ | קונקטור מתוכנן (5.3, A-S-Bina) | שלב 5.3 |
| אוטומציות ותובנות תפעוליות | כולם | הצעות (₪29.90) | ☁️ | — | Alerts Engine + אוטומציות → שלב 3.2/4.5 |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| דשבורד תפעולי + דוחות (אירוע: מיקום/שעה/תמונה) | כולם | הצעות (₪29.90) | ☁️ | דשבורד קבלנים חי (3/12 מסכים) | השלמה → שלב 3.4 |
| מחולל דוחות מתקדם (חיתוכים, התאמה אישית) | כולם | הצעת קרגו (₪29.90) | ☁️ | report-engine: PDF RTL 4 שפות + Excel ✅ (samples
ב-docs/samples) |
חיבור לסוכן AI → שלב 4.5 |
| דוחות רגולטוריים + Audit Log + API | ביובית | דף ביוביות | ☁️ | — | תבנית רגולטורית → שלב 4.5 |
| ניתוח פעילות צי + זיהוי אנומליות | ביובית, כולם | דף ביוביות | ☁️ | האגם חי (raw+marts, שתי הסביבות) | מודל אנומליות → שלב 4.5 |
| מנוע התראות: כללים (אזור/תג/צבע/לוחית/כמות/שעות/ימים) | כולם | legacy security+mgroup | ☁️ | security-core rules.ts ✅ (labels/cameras/schedule/dedupe); alerts-engine ריק | הרחבת תנאים + שירות ייעודי → שלב 3.2 |
| ערוצי התראה: פופאפ, פעמון, מייל, סאונד + acknowledgments | כולם | legacy | ☁️ | סכמת Alert+Acknowledgment ✅; אין דיספאץ' | notification dispatch → שלב 3.2 |
| דשבורד רשויות פר-תפקיד (ראש עיר/גזבר/שפ"ע) + SuperVision | רשויות | brochures supervision + legacy | ☁️ | apps/client-app src/products/municipal | שלב 4.3 |
| מסך צריכה/מנייה פר מודול ומשתמש | כולם | תוספות 22.7 ל-WORK-PLAN | ☁️ | — | metering → שלבים 3-4 (כבר מאושר) |
| Copilot / סוכני AI / שאילתות בשפה חופשית | כולם | WORK-PLAN + אתר | ☁️ | mcp-wrapper ✅ (כולל lake-tools); ai-agents שלד | שלב 4.5 |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| צינור זיהוי: PC קצה (YOLO) → אירוע+תמונה → חוקים → התראה פר-משתמש | עמוד/אתר | legacy security | 🔀 | security-core מלא ✅ (Detection, recordId, tags/colors, classCount) | אימות payload מול תוכנת הקצה → לפני קאט-אובר |
| מסכות יום/לילה (YOLO labeler) + ספי PC | עמוד/אתר | legacy | 🔀 | CameraMask + EdgePc.settings ✅ | מסך labeler → שלב 4.2 |
| וידאו התראות (480p/HLS/הורדה) | עמוד/אתר | legacy | 🔀 | Detection.media ✅ | נגן במסך → שלב 4.2 |
| עמדת תצפית חכמה Plug&Play (סולארי+סלולרי/RF) | אתר מבודד | חוברת ביטחון | 🚚 | — (מוצר קצה) | ניהול העמדה בענן = אותו security-core |
| מערכת RealTime לחמ"ל — On-Prem, מצלמה תרמית, בידוד רעשים | חמ"ל | חוברת ביטחון + אתר | 🚚 | — (מוצר On-Prem, בלי ענן בכוונה) | מחוץ לענן; לתעד ב"תוכנית מערכות" בלבד |
| מערכת תחקור Web — ציר זמן, קשרים בין ישויות, שליפה בדקות | אתר/רשות | חוברת ביטחון + אתר | ☁️ | חלקית: detections + media בסכמה | מסך תחקור/אירועים + XLSX/PDF → שלב 4.2 |
| עמדת אנליטיקה נתיקה/מתקדמת | עמוד | דף מוצר (דפי מוצר/עמדת אנליטיקה נתיקה) |
🚚 | — | וריאנט של עמדת תצפית; קליטה זהה |
| התראות עמודים (כיום רק חוף אשקלון) | עמוד | WORK-PLAN דרישה #6 | ☁️ | pole-ingest קיים ב-ingestion/pole-ingest |
העברה ל-Security → שלב 4.2 |
| יכולת | סוג | מקור | ריצה | כיסוי היום | פער → שלב |
|---|---|---|---|---|---|
| מיקום+סוללה למכולות (Digital Matter) + geocoding | מכולות | legacy + אתר (bin-sensors) | 🔀 | קונקטור מתוכנן (5.3); סכמה ממופה | שלב 5.3 |
| חיישני מילוי לטמונים/מונחים + תחזית מילוי | טמונים | אתר (bin-sensors) | 🔀 | — | ingest חיישנים + תחזית → שלב 5 + 4.5 |
| דחסנים/דחסניות (חיישנים, ורידיס) | דחסנים | הצעות מחיר בדרייב + אפיון אלון | 🔀 | DahasDatas ב-legacy ממופה | סוג נכס בסכמה → שלב 4 |
| שליטה על מכולות / מכולות בחוץ | מכולות | legacy oldsite | ☁️ | ingest ✅ | מסך מכולות → שלב 4.3 |
הרשימה קופלה לתוכנית העבודה והיא מנוהלת שם:
docs/WORK-PLAN.md — פריטים 4.6–4.10
(מיכליות דלק, קרגו/הפצה, Proof-to-Payment עפר, מנוע מסלולים, ניהול
יחידות קצה), והיתר מופו לשלבים קיימים (3.2, 4.2, 4.5). חלק מהפריטים כבר
בביצוע; הסטטוס החי — בתוכנית העבודה, לא כאן.
מה שרץ על המשאית/בתחנה/בעמוד — הרשימה ששניר ביקש לרכז. 22 פריטים, לפי נכס:
על כל משאית (ליבה):
פר סוג משאית (תוספות קצה): 7. דחס: זיהוי פחים צבע/גודל, ARUCO, contamination, overload, lid_match 8. רמסע: זיהוי מכולות + תמונה לפני/אחרי, זיהוי כניסה לתחנות 9. מנוף טמונים: צילום דו-צדי של המנוף, תמונת כלי באוויר, ספירת כלים 10. מנוף גזם: צילום ארגז, זיהוי עבודות פרטיות 11. טיאוט: חיווי מברשות/גרניק, אדם מחוץ לרכב + אפוד זוהר, ממשק נתוני רכב (מים/סלד) 12. ביובית: תיעוד מחזורי שאיבה/פריקה (Proof of Service) 13. מיכלית דלק: חיישן/אינדיקציית פריקה, מניעת מיקס, צילום אספקה, מפלס דלק 14. הפצה/קרגו: חיישן דלת + תמונה, ספירת משטחים/חבילות, קריאת ברקודים 15. עבודות עפר: צילום תעודת שקילה (ל-OCR), סימון פריקה ע"י נהג 16. שקילה על המשאית: ראש משקל KWS (alibi, gross/tare/net)
בתחנות שקילה: 17. LPR בכניסה/יציאה + מחסומים + צג נהג + תיעוד מצולם (תוכנת התחנה)
בעמודים/אתרי ביטחון: 18. PC קצה עם YOLO (מסכות יום/לילה, ספים לפי שעות), דחיפת אירוע+תמונה+וידאו 19. עמדת תצפית Plug&Play: סולארי, סלולרי/RF, מצלמות, AI מובנה 20. עמדת אנליטיקה נתיקה/מתקדמת (וריאנט נייד) 21. מערכת RealTime לחמ"ל — On-Prem מלא (תרמית, בידוד רעשים, UI מקומי; בלי ענן בכוונה)
כלי אצירה: 22. חיישני Digital Matter (מיקום/סוללה) + חיישני מילוי לטמונים
הערת ריכוז: הפריטים 1-6 הם "מערכת הבסיס" שבהצעות המחיר (₪5,775 + ₪1,750 התקנה + ₪260/חודש); 7-16 הם המודולים שנדלקים פר-משאית. מסמך "תוכנית מערכות המשאיות" המלא צריך להוסיף לכל פריט: חומרה מדויקת, ספק, גרסת תוכנת קצה, payload מול ה-ingest, ותלות ברישיון.
הצעד הבא: לאשר את שלבים 4.6-4.10 המוצעים מול שניר ולקבוע איזה משני הפיילוטים (דלק/קרגו) נכנס ראשון לתוכנית — שניהם על אותה תבנית "אירוע פריקה מתועד", אז הראשון סולל את השני.
סריקת הכיסוי המלאה, כלומר כל מה שתוכנן אי פעם מול מה שקיים, היא סריקה
שיווקית והנהלתית: היא מודדת את המוצר מול מה שהובטח
בשוק, ולכן היא מסמך נפרד ומתוחזק בנפרד
(docs/COVERAGE-SWEEP-24-7.md, וגרסת HTML מופקת ממנו).
לצורכי פיתוח אין צורך בה, ולא בגלל שהיא לא חשובה — אלא כי מפתח שואל שאלה אחרת. שלוש השאלות שמפתח באמת שואל, וכל אחת נענית בתוך המסמך הזה:
| השאלה | איפה התשובה |
|---|---|
| מה המערכת לא עושה, ומה לא מגלה לי לבד? | פרק 28 — המגבלות |
| מה פתוח וראוי לספרינט הראשון? | פרק 30 — משימות פתיחה |
| איזו יכולת קיימת לאיזה סוג רכב ולאיזה סגמנט? | נספח י׳ — מטריצת היכולות, והמקור
הבר‑הרצה הוא packages/catalog עם טסט המטריצה שמוכיח אפס
דליפה בין סגמנטים |
הכלל שמאחורי החלוקה הזו: הקוד הוא מקור האמת לגבי מה
קיים. packages/catalog מצהיר על 37 המודולים, 32
סוגי הרכב ו‑11 הסגמנטים, ולכל מראה שלו יש טסט שמכריח אותה להסכים איתו
(פרק 13). מסמך שיווקי לא יכול להיות אדום ב‑CI; הקטלוג יכול, ולכן עליו
סומכים.
מקור:
docs/conventions/how-to-frontend.md— משוכפל כאן במלואו כדי שהמסמך יעמוד בפני עצמו. ההקשר המלא של המעבר מ‑Wix: פרק 19; מערכת העיצוב: פרק 12.
In Wix you dragged visual components in an editor and changed their settings. In real code development the equivalent is a component library + compose in code + live preview. You keep the speed, gain flexibility and version control.
packages/ui-kit) —
build once the reusable UI pieces: buttons, tables, cards, map, charts,
forms, the alerts screen, layout shells. Built in React + Tailwind + a
component kit (shadcn/ui). This is your "visual components"
catalog.npm run dev; the
browser hot-reloads on every save. You edit code (or Claude does) and
see it instantly — that's the "drag/resize and see it" feel, just faster
and reversible.git checkout -b feat/<screen> off
dev.npm run dev → tweak with hot-reload until it looks
right; responsive on all breakpoints (incl. mobile / Median shell).shared-i18n
(he/en/ar/es, RTL/LTR).main → prod.A new screen reuses ui-kit components; you don't rebuild a button or a table per dashboard. New look = new props/variant in ui-kit, once.