Skip to content

feat(bot): נוסח חדש להודעת ZIP, "קבצי גיבוי", אימוג'י מותאם, ואיחוד 🧩 לסקילים - #3200

Merged
amirbiron merged 27 commits into
mainfrom
claude/mcp-codekeeper-webapp-ldnzsg
Jul 27, 2026
Merged

feat(bot): נוסח חדש להודעת ZIP, "קבצי גיבוי", אימוג'י מותאם, ואיחוד 🧩 לסקילים#3200
amirbiron merged 27 commits into
mainfrom
claude/mcp-codekeeper-webapp-ldnzsg

Conversation

@amirbiron

Copy link
Copy Markdown
Owner

What

המשך ישיר ל-#3199 (שני הקומיטים שלא נכנסו ל-squash): (1) נוסח חדש להודעת קליטת ZIP + התווית "📦 קבצי ZIP" → "📦 קבצי גיבוי" בכל המופעים + תשתית אימוג'י מותאם; (2) איחוד אייקון הסקיל ל-🧩 בכל הממשק.

Why

הודעת ה-ZIP: "איפה לשמור אותו?" (יעד, לא שיטה), שם הקובץ בשורת הכותרת, תיאורי קטגוריות (סקילים / גיבויים עם שחזור ריפו בלחיצה), ואייקון 🧩 לסקיל. html_escape נשמר.

"קבצי גיבוי": התווית רוכזה לקבוע BTN_BACKUP_ZIPS (i18n/strings_he.py) הנצרך בכל 6 מופעי הכפתור (תפריט 📚 ×2, Batch, GitHub upload ×2, Drive) + הטקסטים המפנים והתיעוד. אומת שהמחרוזת אינה משמשת כמפתח זיהוי בשום מקום — שום callback_data לא השתנה (אומת ב-diff).

אימוג'י מותאם: tg_emoji(emoji_id, fallback) גנרי ב-utils (גוף הודעה בלבד — כפתורים לא תומכים ב-entities) + CUSTOM_EMOJI_ZIP_ID ב-config (ברירת מחדל None — ה-ID חי רק ב-ENV). על BadRequest: לוג חד-פעמי ושליחה חוזרת עם 📁 — ההודעה מגיעה תמיד.

איחוד 🧩: כל מופעי הסקיל-כישות (תפריט "🧩 סקילים", כותרות רשימה/פרטים, כפתורי פריטים, caption הורדה, הודעות הפניה) — 📝 נשאר רק בתפקיד הערה/עריכה.

✨ תיאור קצר

  • נוסח הודעת קליטת ZIP חדש + כפתור "🧩 סקיל".
  • "📦 קבצי גיבוי" בכל המופעים דרך קבוע מרוכז — תצוגה בלבד.
  • CUSTOM_EMOJI_ZIP_ID + tg_emoji עם fallback בטוח ל-📁.
  • איחוד אייקון הסקיל ל-🧩 בכל הממשק.

📦 שינויים עיקריים

  • קוד (Backend)
  • בוט טלגרם
  • מסד נתונים/מיגרציות
  • תיעוד (docs/)
  • DevOps/CI/CD

פירוט נקודות:

  • handlers/documents.py: נוסח הודעה חדש; ניסיון אימוג'י מותאם עם BadRequest→resend עם 📁.
  • i18n/strings_he.py: קבוע BTN_BACKUP_ZIPS — מקור אמת יחיד לתווית.
  • conversation_handlers.py, github_menu_handler.py, backup_menu_handler.py, handlers/drive/menu.py: צריכת הקבוע + עדכון טקסטים מפנים/כותרות.
  • utils.py + config.py: tg_emoji גנרי + שדה CUSTOM_EMOJI_ZIP_ID (pydantic, Optional).
  • skill_menu_handler.py: איחוד 🧩 (כותרות, כפתורים, caption).
  • docs/environment-variables.rst, services/config_inspector_service.py, GUIDES/BOT_USER_GUIDE.md, docs/handlers/drive_menu.rst: תיעוד המשתנה החדש והתוויות.

🧪 בדיקות

  • איך בדקתם? pytest מקומי; grep+diff שכל שינויי הכפתורים הם תווית בלבד; אומת שנשמר 📝 בתפקיד הערה בלבד.
  • Unit — 85+ טסטים ירוקים בקבצים הרלוונטיים, כולל נעילת הנוסח ("איפה לשמור אותו?", "🧩 סקיל") וטסט tg_emoji
  • Integration
  • Manual — אחרי דיפלוי: להגדיר CUSTOM_EMOJI_ZIP_ID=5069094945915142952 ב-Render ולבדוק את האייקון (דורש פרימיום לבעל הבוט; בלעדיו יוצג 📁)

🧪 בדיקות נדרשות ב‑PR

  • 🔍 Code Quality & Security
  • Unit Tests (3.11)
  • Unit Tests (3.12)

📝 סוג שינוי

  • feat: פיצ'ר חדש
  • fix: תיקון באג
  • docs: שינוי תיעוד בלבד
  • refactor: שינוי קוד ללא שינוי התנהגות
  • perf: שיפור ביצועים
  • chore/ci: תשתית/CI
  • breaking change: שינוי שובר תאימות

Rollback: revert של הקומיטים; אין מיגרציות; CUSTOM_EMOJI_ZIP_ID אופציונלי (ברירת מחדל בטוחה). עיינתי ב-CodeBot – Project Docs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K


Generated by Claude Code

claude and others added 26 commits July 20, 2026 09:18
שלושה כלים חדשים מעל קולקציית sticky_notes הקיימת של הוובאפ:
- codekeeper_list_notes — פתקי הקובץ (קריאה טהורה, בלי ה-backfill של ה-GET בוובאפ)
- codekeeper_create_note — פתק חדש על קובץ קיים; line מעגן לשורת מקור,
  בלעדיו הפתק נוצר צף עם sentinel __floating__ מפורש (אחרת ה-JS מעגן
  אוטומטית לשורה הקרובה ודורס את הכוונה)
- codekeeper_update_note — עדכון חלקי לפי note_id, עם annotation נפרד
  (destructive+idempotent) כי פתק נדרס במקום ואין לו היסטוריית גרסאות

עקרונות:
- כותבים בדיוק את סכמת הוובאפ (scope_id מ-sticky_notes_scope.make_scope_id
  הקנוני, ברירות מחדל בפריטת הקליינט) — פתק מה-MCP מופיע מיד ב-UI
- זהות תמיד מהטוקן; יצירה/עדכון מאחורי require_write
- תוכן >5000 תווים נדחה בשגיאה (לא קיטום שקט — סוכן לא ישים לב לאובדן)
- מגן אנטי-לולאה: עד 200 פתקים לקובץ ביצירה
- אינדקס (user_id, scope_id) שחסר היום נוצר lazy/best-effort — משרת גם את
  שאילתת ה-scope הזהה של הוובאפ
- בלי מחיקה ובלי תזכורות (non-goal מתועד); אפס שינויי קוד בוובאפ —
  דיפלוי לשירות ה-MCP בלבד

בדיקות: 19 טסטים הרמטיים חדשים (סניטציה, ולידציות, sentinel, צבעים,
scope filter מול make_scope_id האמיתי, סריאליזציה) + עדכון טסט הרישום.
188 טסטי MCP עוברים; black/flake8/doc8 נקיים; Sphinx נבנה עם 0 אזהרות.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
הוספת פריט "תיאור" בתפריט 3-הנקודות בעמוד הצפייה בקובץ (view_file),
ראשון ברשימה, שפותח מודאל קטן לעריכת שדה התיאור — חוסך את הכניסה
לעריכת קובץ מלאה רק כדי לשנות תיאור. "נעץ לדשבורד" עלה לשני, "שתף"
לשלישי.

- משתמש ב-endpoint הקיים POST /api/file/<id>/quick-update שמעדכן את
  התיאור in-place (מטא-דאטה, בלי גרסה חדשה) — אין קוד שרת חדש
- מודאל בדפוס מודאל השיתוף הקיים (Escape, לחיצה על הרקע, טוסט הצלחה),
  עם textarea ומונה תווים (עד 500, תואם למגבלת ה-endpoint)
- עדכון חי של התצוגה מתחת לשם ושל תווית התפריט בלי רענון עמוד
- זמין לכל הקבצים (התיאור הוא מטא-דאטה אוניברסלי)
- אין בעיית מודאל-בתוך-מודאל: התפריט (dropdown) נסגר לפני שהמודאל
  נפתח, בדיוק כמו "שתף קובץ" הקיים
- שינוי template בלבד; דורש דיפלוי לוובאפ

בדיקות: Jinja parse תקין, תחביר JS תקין; אין קוד Python שהשתנה.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
באג: אחרי deploy של תבנית, משתמשים שצפו בקובץ לאחרונה המשיכו לראות את
הגרסה הישנה (בלי אלמנטים חדשים) הרבה זמן; אחרים כן ראו חדש. השורש:
ה-ETag של /file/<id> ו-/md/<id> חושב מנתוני הקובץ בלבד (updated_at/תוכן/
version/theme) בלי גרסת ה-deploy — אז קובץ שלא נערך החזיר ETag זהה בין
deploys → הדפדפן קיבל 304 והציג HTML ישן מה-cache. תורם שני: מסלול
If-Modified-Since מבוסס updated_at החזיר 304 בנפרד.

תיקון שורשי (webapp/app.py + cache_manager.py):
- _compute_file_etag כולל עכשיו את _STATIC_VERSION (גרסת deploy) — כל
  deploy מבטל ETags ישנים. קורא מרכזי אחד ⇒ מכסה view_file וגם md_preview.
- שלושת מסלולי If-Modified-Since מכבדים RFC 7232 §3.3: מדלגים כשקיים
  If-None-Match (אחרת 304 מיושן גם אחרי שה-ETag השתנה).
- מפתח ה-cache צד-שרת של md_preview כולל את גרסת ה-deploy (אחרת HTML
  מרונדר ישן מוגש עד 30 דק').
- נלווה: invalidate_file_related מבטל עכשיו גם את המפתח האמיתי
  web:md_preview:user:*:{file_id}:* (היה prefix שגוי — עריכת קובץ לא
  ביטלה את cache ה-md שלו).

ב-Render גרסת ה-deploy מגיעה מ-RENDER_GIT_COMMIT (משתנה לכל commit).

אימות: py_compile + flake8 + 9 טסטי cache/invalidation עוברים. את לוגיקת
ה-ETag אימתתי בבידוד (הרצת הפונקציה האמיתית: גרסת deploy משנה את ה-ETag,
יציב לאותו קלט, ותוכן עדיין משנה). טסט יחידה שמייבא webapp.app לא ישים —
טסטי ה-webapp מדולגים ב-CI ("צינור הבוט") ו-flask לא זמין בסביבה.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
מתחת לכל קובץ באוסף שיש לו תיאור מופיע אייקון ℹ️; לחיצה פותחת מודאל
קטן (קריאה בלבד) עם התיאור — בלי להיכנס לקובץ.

- Backend: get_collection_items מצרף עכשיו את ה-description של הקובץ
  לכל פריט, דרך אותו batch שכבר מחשב is_file_active (הרחבת ה-projection
  ל-file_name+description) — בלי N+1 ובלי שדות כבדים (Smart Projection
  נשמר; description ≤500 תווים). קובץ בלי תיאור/לא-פעיל ⇒ "".
- Frontend: אייקון ℹ️ ב-.collection-card__meta רק אם יש תיאור;
  openDescriptionModal בדפוס .collection-modal הקיים (Escape/רקע סוגרים,
  textContent — בטוח מ-XSS).
- מצב workspace לא נכלל בשלב זה.

בדיקות: 3 טסטי enrichment חדשים (fakes שתומכים ב-projection) + 49 טסטי
collections_manager עוברים; node --check ל-JS; py_compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
אייקון התיאור בכרטיס קובץ עבר משורת ה-meta אל צד שם הקובץ (משמאל, RTL) באותה
שורה, עם רווח. כשהשם ארוך ונשבר לשתי שורות — האייקון "קופץ" לשורת 4 הכפתורים,
משמאל להם (פונקציה layoutDescIcons שמנצלת את זיהוי is-wrapped של autoFitText,
בסדר reset→autoFit→move כדי למנוע oscillation).

ארכיון לאוספים: שדה is_archived חדש (נפרד מ-is_active), toggle 🗄️ "הצג ארכיון"
בסיידבר, וכפתור ארכב/שחזר בכותרת האוסף (מגודר ל-non-workspace). list_collections
קיבל archived_only ו-include_archived; ברירת המחדל מחריגה מאורכבים (ne:True מכסה
גם אוספים ישנים ללא השדה). הקאש כבר מבחין לפי querystring, וה-PUT מנקה את שתי
התצוגות. הגיבוי האישי משתמש ב-include_archived=True כדי לא לפספס אוספים בארכיון.

- database/collections_manager.py: doc-build, allow-list, list filter, serializer, index+backfill
- webapp/collections_api.py: פרמטר archived ב-GET, דילוג על "שולחן עבודה" בתצוגת ארכיון
- webapp/static/js/collections.js + collections.css: אייקון, toggle, כפתורי ארכב/שחזר
- services/personal_backup_service.py: include_archived=True בגיבוי ובבדיקת כפילות
- tests/test_collections_archive.py: ארכוב/שחזור/include/legacy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
main קלט את אייקון-התיאור הבסיסי דרך squash (#3191), ולכן נוצר קונפליקט מול
העבודה החדשה בענף (הזזת האייקון + ארכיון). מיזגתי את main לענף ופתרתי:
- collections.css: נשמרה הגרסה שלי (superset — בסיס .desc-info + flex + Slot 2 + toggle ארכיון).
- collections.js: הוחזר לגרסה שלי כדי למנוע שכפול של כפתור התיאור שהמיזוג האוטומטי יצר.
תוצאת המיזוג זהה בדיוק לעבודה שכבר נבדקה (36 טסטים ירוקים).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
לפי theming_and_css.rst אסורים צבעים קשיחים בקבצי רכיבים — רק var(--token).
שתי חריגות הומרו:
- מודאל "ערוך תיאור" (view_file.html): הרקע הכהה הקבוע (#1f2a44, #fff, rgba לבנים,
  focus בצבע primary קשיח) הוחלף בטוקנים סמנטיים — bg-secondary/tertiary,
  text-primary/secondary/muted, glass-border, primary; ה-scrim וה-shadow קיבלו
  טוקן-רכיב עם fallback (var(--modal-backdrop, ...), var(--solid-surface-shadow, ...)).
  כך המודאל מקבל את צבעי הערכה גם בערכות בהירות (rose-pine-dawn, classic).
- כפתור "הצג ארכיון" במצב לחוץ (collections.css): rgba לבנים קשיחים הוחלפו
  בטוקני glass קיימים (glass-hover/glass-border/glass).

מודאל ה-ℹ️ באוספים נבדק ונמצא תקין (יורש var(--collections-modal-*)) — ללא שינוי.
מודאל השיתוף הסמוך הוא legacy קיים ולא נכלל (חוב נפרד).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
…קט collections.css

main קלט את הארכיון והזזת האייקון דרך squash (#3192), והענף ממשיך עם תיקון
הטוקנים (991b1ad) שנגע באותה שורה. נשמרה גרסת הטוקנים של #toggleArchivedBtn
(var(--glass-*)) — ההבדל היחיד מול main, שהוחלף בכוונה. תוצאת המיזוג זהה
לחלוטין לעץ שכבר נבדק.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
באג שורש: המחרוזת `סה""כ` פיצלה f-strings לשתי מחרוזות סמוכות — השנייה (בלי
קידומת f) הציגה placeholders כטקסט מילולי (למשל "{len(items)}"). תוקן ב-3 מקומות
(handlers/documents.py, conversation_handlers.py, handlers/save_flow.py) ע"י
מעבר ל-f-string רציף אחד עם מרכאות בודדות. כך "✅ נוסף: X (סה"כ N קבצים)" מציג
את המספר בפועל, וכן כותרת רשימת ה-ZIP השמורים ומסך איסוף הקוד הארוך.

פיצ'ר: שלב בחירת שם ל-ZIP. אחרי "✅ סיום" הבוט מבקש שם (או "⏭️ דלג" לשם אוטומטי):
- conversation_handlers.py: helper משותף finalize_zip_create + _cleanup_zip_state;
  zip_create_finish מציב awaiting_zip_name ומבקש שם; callback חדש zip_create_skip_name.
- main.py: hook בראש handle_text_message שתופס את השם (נבדק ראשון כדי שלא ייבלע/ייחשב קוד).
- שם מנוקה דרך TextUtils.clean_filename + סיומת .zip; fallback ל-my-files-<timestamp>.zip.

טסט: חיזוק test_handle_document_collects_zip_items לאימות שהמספר מוצג בפועל (הגנת רגרסיה).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
…hread, מסלול ביטול

מענה ל-review findings ב-PR #3194:
- ניקוי שם רשומת ZIP (utils.safe_zip_entry_name): basename בלבד, דחיית נתיב מוחלט/
  מקונן ו-"."/".." — הגנת Zip-Slip. משמש בבניית הארכיון במקום השם הגולמי.
- בניית ה-ZIP חולצה ל-utils.build_zip_bytes (טהור) ורצה תחת asyncio.to_thread כדי
  לא לחסום את לולאת האירועים (בהתאם לכלל ה-Performance ב-CLAUDE.md).
- אכיפת מגבלות איסוף (ZIP_CREATE_MAX_FILES=50, ZIP_CREATE_MAX_TOTAL_BYTES=45MB)
  בזמן צבירת הקבצים ב-handlers/documents.py, עם הגנה כפולה גם בשלב הבנייה.
- מסלול ביטול במצב "המתנה לשם": zip_create_cancel עובר דרך _cleanup_zip_state
  (מנקה גם awaiting_zip_name), ונוסף כפתור "❌ ביטול" למקלדת בקשת השם — כך שמשתמש
  שמתחרט לא ישלח ארכיון בטעות בהודעת טקסט כלשהי.
- טסטים: tests/test_zip_bundle_utils.py (ניקוי שמות + מגבלות, נבדק ע"י פענוח ה-ZIP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
…ון docstring

מענה ל-review (סבב 2) ב-PR #3194:
- handlers/documents.py: בדיקות מגבלת ה-ZIP (מספר קבצים + גודל לפי document.file_size)
  הוזזו לפני get_file()/download_to_memory() — לא מורידים לזיכרון קובץ שנדחה מראש.
  בדיקת len(raw) נשמרה כאימות סופי לפער אפשרי מול הגודל המוצהר.
- utils.build_zip_bytes: מניעת שמות רשומה כפולים — הראשון נשמר, הבאים מקבלים סיומת
  ממספרת (x.txt, x_2.txt) עם שמירת הסיומת.
- utils: תיקון docstring (שורה ריקה אחרי רשימת ה-bullets) — מבטל אזהרת docutils/RTD.
- tests: חיזוק test_...skips (file_1/file_2 מדויק) + regression לכפילות שמות.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
…ך-יתר של URL

שלושה תיקונים שורשיים ב-Config Inspector (שרץ בתהליך ה-webapp וקורא os.getenv):

1) הפרדה לפי שירות: הצלבנו את כל 236 המשתנים מול עמודת "רכיב" ב-
   docs/environment-variables.rst וגם מול הקוד עצמו (import-closure סטטי של
   webapp/bot/mcp + חיפוש הקוראים של כל KEY). 41 משתנים שנקראים רק בבוט (34,
   כולל ה-webserver הפנימי שרץ בתוך תהליך הבוט), ב-MCP (6) או בסקריפטים (1)
   הוסטו מהעמוד הראשי ל"עמוד 2" חדש (טאב "שירותים אחרים") שמציג מטא-דאטה בלבד
   — בלי Status ובלי Active Value, כי ערכיהם חיים בתהליכים אחרים ואינם נגישים
   מה-webapp. שדה service חדש ב-ConfigDefinition + get_other_services_entries().
   OTEL_EXPORTER_* נשארו בעמוד webapp (ה-SDK קורא אותם מה-env בכל תהליך);
   BOT_USERNAME נשאר webapp ו-BOT_TOKEN נוסף כהגדרה חסרה (נקרא ב-auth_routes).

2) סטטוס "Set" חדש: ערך שהוגדר בסביבה (למשל ברנדר) כשאין ברירת מחדל בקוד אינו
   "Modified" — אין דיפולט שממנו סטינו. determine_status מחזיר Set במקרה זה
   (MCP_SERVER_URL, GITHUB_TOKENS, GITHUB_WEBHOOK_SECRET,
   ALERTMANAGER_WEBHOOK_SECRET, ALERT_TELEGRAM_BOT_TOKEN ודומיהם). נוספו
   set_count לסקירה, ספירה בכרטיסי הקטגוריות ו-pill כחול (טוקן --info) ב-UI.

3) ביטול מיסוך-יתר: "URL" הוסר מ-SENSITIVE_PATTERNS — כתובת ציבורית
   (MCP_SERVER_URL, WEBAPP_URL, PROMETHEUS_URL, PUBLIC_BASE_URL...) אינה סוד.
   URL שמגלם credentials (MONGODB_URL) נשאר ממוסך דרך sensitive=True מפורש,
   ו-TOKEN/SECRET/URI/KEY ממשיכים להיתפס בתבניות.

בנוסף: תוקנו 5 שורות "רכיב" שגויות ב-docs/environment-variables.rst שהתגלו
בהצלבה (ENABLE_INTERNAL_SHARE_WEB, SENTRY_WEBHOOK_SECRET,
SENTRY_WEBHOOK_DEDUP_WINDOW_SECONDS, DUMMY_BOT_TOKEN → Bot; BOT_TOKEN → Bot/WebApp).

טסטים: 32 ב-test_config_inspector_service.py (כולל 3 מחלקות חדשות: סטטוס Set,
מיסוך URL, הפרדת שירותים) — ירוקים. תחביר Jinja אומת.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
…פים בעמוד 2, ותיקון העתק-הכל

שלושה תיקונים בהמשך למשוב:
- ה-webserver הוא שירות Render נפרד (ההרצה הפנימית בתוך תהליך הבוט בוטלה):
  ה-closure שלו חושב בנפרד מהבוט, ומשתנים שנקראים בו (SENTRY_WEBHOOK_SECRET,
  SENTRY_WEBHOOK_DEDUP_WINDOW_SECONDS) מסומנים service=webserver. עודכן גם
  "רכיב" ב-environment-variables.rst (Webserver) ותואר ENABLE_INTERNAL_SHARE_WEB.
- משתנים משותפים: השדה service הוחלף ב-services (tuple) — משתנה יכול להשתייך
  לכמה שירותים. עמוד 2 מציג עכשיו את כל 220 המשתנים ששייכים לשירות שאינו webapp,
  כולל המשותפים (למשל MONGODB_URL: bot + mcp + webserver), עם ציון השירותים בכל
  שורה ותג "גם Webapp" למשתנים שערכיהם מוצגים בעמוד הראשון. עמוד 1 נשאר 196.
- תיקון P2 מה-review: "העתק הכל" הוגבל ל-#inspectorPageWebapp — טבלת השירותים
  האחרים (מטא-דאטה בלי ערכים) לא מייצרת יותר שורות KEY= ריקות בייצוא ה-.env.

טסטים: 33 ב-test_config_inspector_service.py (עודכנו לסמנטיקת services + טסט
משתנה-משותף-בשני-העמודים) — ירוקים. תחביר Jinja אומת.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
סקילים (ZIP עם SKILL.md) נכנסו בטעות למסלול הגיבויים, מה שגרם לשלוש בעיות:
save_backup_bytes דוחס מחדש ומזריק metadata.json (לא byte-for-byte),
cleanup_expired_backups מוחק לפי retention, ו-restore עם purge הרסני.

הפתרון — אחסון עצמאי לחלוטין:
- SkillManager חדש (file_manager.py): קולקציית GridFS "skills" נפרדת, שמירת
  bytes as-is (fs.put ישיר), תמיד מונגו בלי תלות ב-BACKUPS_STORAGE ובלי env var.
  skill_id ייחודי (timestamp+uuid) מונע התנגשות; שם קובץ עם סיומת ייחוד מונע דריסה.
- ניתוב בהעלאה: _maybe_store_zip_copy מציג שני כפתורים "סקיל"/"גיבוי" במקום
  שמירה אוטומטית; ה-bytes נשמרים זמנית עד לבחירה מפורשת (עזרי stash ב-utils.py,
  מחיקות מוגבלות ל-allowlist ייעודי).
- SkillMenuHandler חדש (prefix skill_): רשימה + הורדה/מחיקה/תיוג/הערה, כפתור
  "📝 סקילים" בתפריט "הצג את כל הקבצים שלי". תיוג/הערה דרך ה-facade הגנרי הקיים.

טסטים: שמירה+הורדה byte-for-byte, בידוד מ-cleanup של הגיבויים, ושני סקילים
עם אותו שם שאינם דורסים. טסטי הגיבויים הקיימים נשארים ירוקים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
תיקון כשלי CI/RTD ויישום ממצאי code review על פיצ'ר הסקילים.

CI/RTD:
- עדכון test_documents להתנהגות "בחירה מפורשת" (כפתורים) במקום שמירה אוטומטית
- עדכון סדר תפריט "הצג את כל הקבצים" בשני טסטי patch_coverage (skill_list)
- תיקון docstring שהכשיל את RTD (הסרת * חשוף שנפרש כ-emphasis)

אבטחה/נכונות:
- main: logger.exception בשמירת הערות, בלי חשיפת שגיאת DB למשתמש
- file_manager: נרמול user_id ל-int לפני שמירת סקיל (עקביות שאילתת list_skills)
- config_inspector: מיסוך URL עם credentials מוטמעים (user:pass@) גם בשם לא-רגיש
- utils: אימות token בטוח לשם קובץ + הרשאות 0o700 לתיקיית ה-pending
- conversation: ניקוי ה-pending רק אחרי שמירה מוצלחת (מאפשר retry בכשל)

ביצועים/עקביות:
- skill_menu: איסוף דירוגים ב-thread, הורדה בסריקת GridFS אחת, safe_edit_message_text
- conversation: load ל-to_thread + safe_edit על תשובות ה-routing
- documents: קבוע PENDING_ZIP_TTL_SECONDS + הגבלת ZIP ממתינים למשתמש

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
הבאג בפרודקשן ("save_skill_bytes: GridFS 'skills' לא זמין"): ב-_get_skills_gridfs
נעשתה בדיקה ``if not mongo_db``, אבל get_mongo_db() מחזיר אובייקט Database אמיתי של
pymongo — ו-bool()/not עליו זורק NotImplementedError. החריגה נבלעה ב-except הכללי,
המתודה החזירה None, והסקיל לא נשמר. חיבור המונגו עצמו תקין (קטעי קוד כן נשמרים).

השורש: pymongo אוסר truth-value testing על Database/Collection/MongoClient. שאר
מתודות ה-facade כבר משוות נכון עם ``is None`` — רק שני עוזרי ה-GridFS השתמשו ב-not.

התיקון (root-cause, שני המקומות):
- SkillManager._get_skills_gridfs: ``if not mongo_db`` → ``if mongo_db is None``.
- BackupManager._get_gridfs: אותו באג רדום (מוסתר בפרודקשן ע"י BACKUPS_STORAGE=fs,
  שמחזיר None עוד קודם) — תוקן גם הוא כדי שלא יתפוצץ במצב BACKUPS_STORAGE=mongo.

טסטים: הטסטים הקיימים מוקים את _get_skills_gridfs ולכן פספסו את המסלול האמיתי.
נוספו טסטי רגרסיה שקוראים למתודה האמיתית עם Database אמיתי של pymongo (connect=False,
בלי שרת) ומוודאים שמוחזר GridFS ולא None. אומת red→green. 11 טסטים ירוקים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
… קונפליקטים לטובת תיקון הבאג של הסקילים)
…וך שירותים

שלושה תיקונים בעקבות פידבק על העמוד:

1. רווחים שנעלמו: עטיפת הטאבים (.inspector-page) היא display:block, ולכן הילדים
   (כרטיסי סיכום, שורת סינון, קטגוריות, טבלה) איבדו את ה-gap של ה-flex ההורה
   (.config-page). העמוד הפעיל הוא עכשיו flex column עם אותו gap — הרווחים חזרו.

2. שורת סינון לעמוד 2 (שירותים אחרים): קטגוריה + שירות (אין שם Status כי אין
   ערכים חיים), עם סנן/איפוס/העתק הכל. הסינון client-side; "העתק הכל" מעתיק
   KEY=default עבור השורות הגלויות ומציין בטוסט שאלה ערכי ברירת מחדל.

3. דיוק שיוך שירותים (68 משתנים): מיפוי אוטומטי של צריכה בפועל — לכל משתנה נבדק
   באילו קבצים הוא נקרא (getenv/config.X) והאם הקובץ שייך לשירות או נטען בסגירת
   ה-imports שלו (entry: mcp_server/app.py, services/webserver.py). משתנים שסווגו
   ל-MCP/Webserver בלי שימוש אמיתי הוסרו משם (21 מ-MCP, 65 מ-Webserver — למשל
   UPTIME_*, PUSH_*, VAPID_*, MAINTENANCE_*, ALERTMANAGER_*). חריגים ידניים:
   PORT נשאר (נצרך בפקודת ההרצה), PROFILER_* נשארו ב-webserver (profiler_handler
   נטען שם בפועל).

   בפרט BACKUPS_STORAGE/BACKUPS_DIR עברו ל-Bot בלבד (גם ב-rst): רק הבוט טוען את
   file_manager; לוובאפ מנגנון גיבוי נפרד (WEBAPP_BACKUPS_DIR). הגיבויים האלה הם
   ZIP של קטעי הקוד (get_user_files) + גיבויי GitHub/Drive — לא אוספים/סימניות.

טסטים: 67 ירוקים (config_inspector, skill_manager, mcp_docs, rst_parser),
Jinja template מתקמפל.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
…לתות ממוקדות

תיקונים בעקבות code review (כל הממצאים אומתו מול הקוד):

אבטחה/פרטיות:
- file_manager: אזהרת user_id לא תקין רושמת רק את סוג הערך, לא את הערך עצמו (PII).
- utils: הערה מבהירה ש-_SAFE_TOKEN_RE ([A-Za-z0-9_-]) מונע path traversal ב-stash
  (false positive של static analysis — אין תיקון קוד).

נכונות:
- documents: stash_pending_zip_bytes ו-cleanup_stale_pending_zips רצים ב-to_thread
  (I/O של דיסק בתוך handler אסינכרוני); אם שליחת הודעת הבחירה נכשלת — הקובץ
  והרשומה שנוצרו עבור אותו ZIP מנוקים (בלי כפתורים אין דרך להשלים את הבחירה).
- skill_menu: query.answer() לפני עריכה גם ב-delete_confirm/delete_execute
  וב-send_rating_prompt; ב-skill_rate המענה לפני עריכת ההצלחה בלבד — answer מוקדם
  גורף היה מבטל את ה-show_alert של הודעות השגיאה הקיימות.
- file_manager: נרמול user_id ל-int בכל המתודות (list/get/info/delete) — קלט str
  לא היה מוצא כלום מול שמירה כ-int.

מכסות ואינדקסים (לסקילים אין retention — בלי מכסה האחסון גדל ללא גבול):
- SKILLS_MAX_PER_USER (ברירת מחדל 100) ו-SKILLS_MAX_TOTAL_BYTES (ברירת מחדל 1GB,
  0 = כיבוי) נאכפים לפני fs.put; תועדו ב-rst וב-config inspector (שירות Bot).
- אינדקסים על skills.files: (user_id, skill_id) ו-(skill_id) — best-effort פעם אחת.

ביצועים/מבנה:
- get_skill_info חדש: שליפת סקיל בודד בשאילתה ממוקדת; _find_skill ופרטי/הורדת
  סקיל כבר לא סורקים את כל הרשימה. delete_skills מסנן ב-DB עם $in.
- conversation: ספירת קבצי ה-ZIP אוחדה עם השמירה להלפר אחד שרץ ב-to_thread.
- main: זרימות הערה לגיבוי/סקיל אוחדו להלפר משותף; save_backup_note ב-to_thread.
- SkillInfo הומר ל-dataclass; send_rating_prompt מלוגג כשל ומודיע במקום pass.

טסטים: fixture עם client.close(); fake תומך $in; טסטי מכסות, נרמול str,
ו-get_skill_info; ניקוי קבצי pending בשני טסטים שהשאירו קבצים ב-tmp (אומת 0
שאריות); עדכון הערת הסדר ב-test_patch_coverage. 77 טסטים ירוקים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
…ואייקון מותאם

שלושה שינויים ברמת התצוגה (שום callback_data או מפתח לוגי לא השתנה — אומת שהתווית
אינה משמשת כמפתח זיהוי בשום handler/regex):

1. הודעת קליטת ZIP (documents.py): "איפה לשמור אותו?" במקום "איך", שם הקובץ בשורת
   הכותרת, תיאורי הקטגוריות (סקילים / גיבויים עם אזכור שחזור ריפו בלחיצה), ואייקון
   הסקיל 🧩 בטקסט ובכפתור. html_escape על שם הקובץ נשמר.

2. התווית "📦 קבצי ZIP" → "📦 קבצי גיבוי" — רוכזה לקבוע BTN_BACKUP_ZIPS
   (i18n/strings_he.py, מקור אמת יחיד) הנצרך בכל 6 מופעי הכפתור: תפריט 📚 (message
   + callback), עיבוד Batch, GitHub upload (כולל המקלדת המשוכפלת), ותפריט Drive.
   בהיקף שאושר עודכנו גם הטקסטים המפנים לכפתור (עזרה, הודעת אישור גיבוי, כותרות
   רשימה, הודעות Drive) והתיעוד (BOT_USER_GUIDE, drive_menu.rst). מופעי "ZIP/גדולים"
   בניסוח אחר, docstrings, וה-webapp — במפורש מחוץ להיקף.

3. אייקון ZIP מותאם: tg_emoji(emoji_id, fallback) גנרי ב-utils (לגוף הודעה עם
   parse_mode=HTML בלבד — כפתורים לא תומכים ב-entities), שדה CUSTOM_EMOJI_ZIP_ID
   ב-config (ברירת מחדל None — ה-ID חי רק ב-ENV, משאב צד ג'). בהודעה: ניסיון עם
   האייקון המותאם; על BadRequest — לוג חד-פעמי לתהליך ושליחה חוזרת עם 📁 (המשתמש
   מקבל את ההודעה בכל מקרה). fallback הוא 📁 בדיוק (שתי יחידות UTF-16). תועד
   ב-rst וב-config inspector (display/Bot).

טסטים: נעילת הנוסח החדש ("איפה לשמור", כפתור "🧩 סקיל") + טסט tg_emoji.
85 טסטים ירוקים; אומת ב-diff שכל שינויי הכפתורים הם תווית בלבד.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
בהמשך לשינוי אייקון הסקיל בהודעת קליטת ה-ZIP — איחוד לכל שאר המופעים של
הסקיל-כישות (📝 → 🧩): כפתור "🧩 סקילים" בתפריט 📚 (שתי הגרסאות), כותרת
הרשימה, כפתורי הפריטים, כותרת פרטי הסקיל, caption בהורדה, הודעת "אין סקילים",
והודעת ההצלחה שמפנה לתפריט. שינוי תצוגה בלבד — שום callback_data לא השתנה.

אייקון ה-📝 בתפקידיו האחרים (הערה / ערוך הערה / שנה שם) נשאר בכוונה — הוא
מציין פעולת כתיבה, לא את ישות הסקיל.

51 טסטים ירוקים; אומת שלא נותר "📝 סקיל/סקילים" בקוד.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

🧯 Dangerous deletes guard report

Policy: see .cursorrules — dangerous deletions are blocked unless wrapped safely.

Summary:

  • Flagged findings (blocking): 0
    0
  • Excluded matches (not blocking): 15
  • Total matches (all files): 129

Flagged findings (file:line:snippet):
(none)

Excluded matches (by path pattern)
./node_modules/mermaid/dist/mermaid.js.map:4:  "sourcesContent": ["/**\n* Default values for dimensions\n*/\nconst defaultIconDimensions = Object.freeze({\n\tleft: 0,\n\ttop: 0,\n\twidth: 16,\n\theight: 16\n});\n/**\n* Default values for tr … [truncated]
./node_modules/mermaid/dist/mermaid.min.js.map:4:  "sourcesContent": ["/**\n* Default values for dimensions\n*/\nconst defaultIconDimensions = Object.freeze({\n\tleft: 0,\n\ttop: 0,\n\twidth: 16,\n\theight: 16\n});\n/**\n* Default values fo … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.core/chunk-KS23V3DP.mjs.map:4:  "sourcesContent": ["{\n  \"name\": \"mermaid\",\n  \"version\": \"11.12.0\",\n  \"description\": \"Markdown-ish syntax for generating flowcharts, mindmaps, sequence  … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.esm/chunk-2M32CCKP.mjs.map:4:  "sourcesContent": ["{\n  \"name\": \"mermaid\",\n  \"version\": \"11.12.0\",\n  \"description\": \"Markdown-ish syntax for generating flowcharts, mindmaps, sequence d … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.esm.min/chunk-4HFYJGYH.mjs.map:4:  "sourcesContent": ["{\n  \"name\": \"mermaid\",\n  \"version\": \"11.12.0\",\n  \"description\": \"Markdown-ish syntax for generating flowcharts, mindmaps, sequen … [truncated]
./node_modules/mermaid/dist/chunks/mermaid.esm.min/chunk-4HFYJGYH.mjs:1:var r={name:"mermaid",version:"11.12.0",description:"Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graph … [truncated]
./node_modules/mermaid/dist/mermaid.min.js:1524:`,"getStyles"),c1e=RQe});var h1e={};dr(h1e,{diagram:()=>NQe});var NQe,f1e=N(()=>{"use strict";$ge();a1e();l1e();u1e();NQe={parser:Fge,db:n1e,renderer:o1e,styles:c1e}});var m1e,g1e=N(()=>{"use  … [truncated]
./node_modules/katex/package.json:153:    "build": "rimraf dist/ && mkdirp dist && cp README.md dist && rollup -c --failAfterWarnings && webpack && node update-sri.js package dist/README.md",
./node_modules/katex/src/fonts/Makefile:139:	rm -rf pfa ff otf ttf woff woff2
./Dockerfile:42:    rm -rf /var/lib/apt/lists/*
./Dockerfile:121:    rm -rf /var/lib/apt/lists/*
./webapp/static/js/md_preview.bundle.js.map:4:  "sourcesContent": ["// Markdown-it plugin to render GitHub-style task lists; see\n//\n// https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments\n// https://github.com/blog/1825-t … [truncated]
./docs/DOCUMENTATION_GUIDE.md:453:rm -rf _build
./docs/Makefile:24:	rm -rf $(BUILDDIR)
./README.md:842:find . -name "__pycache__" -exec rm -rf {} +

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@amirbiron, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c5dcd31f-257a-4825-a39a-1edde19da3a0

📥 Commits

Reviewing files that changed from the base of the PR and between 81cc87d and 4909cb3.

📒 Files selected for processing (4)
  • github_menu_handler.py
  • handlers/documents.py
  • tests/config.py
  • tests/handlers/test_documents.py
📝 Walkthrough

Walkthrough

העדכון מאחד את תוויות אזור הגיבויים, מוסיף הגדרת אימוג׳י Telegram מותאם עם fallback, ומעדכן את תיעוד ותצוגת הסקילים.

Changes

זרימת אימוג׳י מותאם ושמירת ZIP

Layer / File(s) Summary
הגדרת אימוג׳י, helper וטיפול ב-fallback
config.py, services/config_inspector_service.py, utils.py, handlers/documents.py, tests/test_utils.py, tests/handlers/test_documents.py
נוסף CUSTOM_EMOJI_ZIP_ID, helper בשם tg_emoji, וטיפול בשליחה חוזרת עם אימוג׳י רגיל לאחר BadRequest מ-Telegram. בדיקות חדשות מאמתות את הפלט ואת כפתורי בחירת היעד.

אחידות תוויות הגיבויים בממשקים

Layer / File(s) Summary
קבוע תרגום ושילוב במסכי גיבוי
i18n/strings_he.py, backup_menu_handler.py, conversation_handlers.py, github_menu_handler.py, handlers/drive/menu.py
נוסף BTN_BACKUP_ZIPS, והוא משמש בכותרות, כפתורים, הודעות שגיאה, מסכי Drive, Batch, GitHub ורשימות הקבצים.

תיעוד ואייקוני סקילים

Layer / File(s) Summary
עדכון ניסוחים ותצוגת סקילים
GUIDES/BOT_USER_GUIDE.md, docs/environment-variables.rst, docs/handlers/drive_menu.rst, skill_menu_handler.py
התיעוד משתמש ב“קבצי גיבוי”, משתנה הסביבה מתועד, ותוויות הסקילים עודכנו מ-📝 ל-🧩.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant משתמש
  participant _maybe_store_zip_copy
  participant Telegram
  משתמש->>_maybe_store_zip_copy: בחירת יעד ל-ZIP
  _maybe_store_zip_copy->>Telegram: שליחת הודעה עם אימוג׳י מותאם
  Telegram-->>_maybe_store_zip_copy: BadRequest
  _maybe_store_zip_copy->>Telegram: שליחה חוזרת עם אימוג׳י רגיל
Loading

Possibly related PRs

  • amirbiron/CodeBot#3197: משנה את אותה זרימת בחירת יעד ZIP ואת רכיבי הממשק של סקיל מול גיבוי.
  • amirbiron/CodeBot#3199: נוגע באותה זרימת שמירה מושהית ובבחירת יעד ל-ZIP.

Poem

ZIP קטן בוחר לו יעד,
גיבוי מקבל שם מאוחד,
אימוג׳י נוצץ — ואם לא, fallback,
סקיל לובש 🧩 בחיוך,
Claude Code קודד — והבוט ממשיך לרקוד.
CodeKeeper forever 💫

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed הכותרת משקפת היטב את עיקר השינוי: נוסח ZIP, תווית גיבוי, אימוג'י מותאם ואיחוד 🧩 לסקילים.
Description check ✅ Passed ה-PR description כולל What/Why/Tests, פירוט שינויים, סוג שינוי ו-Rollback, ורק חסרים חלקים משניים מהתבנית.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/mcp-codekeeper-webapp-ldnzsg

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

⏱️ Performance report

(No performance test durations collected. Mark tests with @pytest.mark.performance.)

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

📖 Documentation Preview

The documentation has been built successfully!

To view locally:

  1. Download the artifacts
  2. Extract the zip file
  3. Open index.html in your browser

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.79487% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
handlers/documents.py 81.25% 1 Missing and 2 partials ⚠️
handlers/drive/menu.py 62.50% 3 Missing ⚠️
github_menu_handler.py 33.33% 2 Missing ⚠️
skill_menu_handler.py 0.00% 2 Missing ⚠️
conversation_handlers.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/handlers/test_documents.py (1)

299-308: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

הוסיפו בדיקה לנתיב ה-fallback.

Claude Code, נעילה של הטקסט והכפתורים כאן טובה. הוסיפו תרחיש עם CUSTOM_EMOJI_ZIP_ID שבו השליחה הראשונה נכשלת, וודאו שהשליחה השנייה משתמשת ב-📁 ושקובץ ה-ZIP עדיין נשאר ממתין לבחירת המשתמש.
CodeKeeper forever 💫

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/handlers/test_documents.py` around lines 299 - 308, הוסיפו תרחיש בדיקה
לנתיב ה-fallback סביב CUSTOM_EMOJI_ZIP_ID: גרמו לשליחה הראשונה להיכשל, ודאו
שהניסיון השני משתמש בתווית 📁, ובדקו שקובץ ה-ZIP נשאר במצב ממתין לבחירת המשתמש.
שמרו על בדיקות הטקסט והכפתורים הקיימות דרך replies.messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@github_menu_handler.py`:
- Line 1589: Replace the direct query.edit_message_text call in the
error-handling path with TelegramUtils.safe_edit_message_text, preserving the
existing Hebrew error text and query context. Ensure the wrapper suppresses only
“message is not modified” while propagating other BadRequest errors.

In `@handlers/documents.py`:
- Around line 1084-1097: עדכנו את בלוק ה־BadRequest סביב שליחת הודעת
_zip_prompt_text כך שיבצע fallback רק כאשר השגיאה מאומתת כנובעת מהאימוג׳י המותאם
שהוחזר על ידי tg_emoji(). אל תסמנו את _custom_emoji_warned ואל תבצעו retry עבור
שגיאות BadRequest אחרות; לחלופין, קבעו את מצב האזהרה רק לאחר ששליחת fallback עם
📁 הצליחה.

---

Nitpick comments:
In `@tests/handlers/test_documents.py`:
- Around line 299-308: הוסיפו תרחיש בדיקה לנתיב ה-fallback סביב
CUSTOM_EMOJI_ZIP_ID: גרמו לשליחה הראשונה להיכשל, ודאו שהניסיון השני משתמש בתווית
📁, ובדקו שקובץ ה-ZIP נשאר במצב ממתין לבחירת המשתמש. שמרו על בדיקות הטקסט
והכפתורים הקיימות דרך replies.messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eebca8e0-4594-4ec8-bd57-58517cba4471

📥 Commits

Reviewing files that changed from the base of the PR and between 0e15099 and 81cc87d.

📒 Files selected for processing (15)
  • GUIDES/BOT_USER_GUIDE.md
  • backup_menu_handler.py
  • config.py
  • conversation_handlers.py
  • docs/environment-variables.rst
  • docs/handlers/drive_menu.rst
  • github_menu_handler.py
  • handlers/documents.py
  • handlers/drive/menu.py
  • i18n/strings_he.py
  • services/config_inspector_service.py
  • skill_menu_handler.py
  • tests/handlers/test_documents.py
  • tests/test_utils.py
  • utils.py

Comment thread github_menu_handler.py Outdated
Comment thread handlers/documents.py
…דויק לאימוג'י

שלושת הממצאים אומתו מול הקוד ותוקנו:

- github_menu_handler: שתי עריכות ההודעה בנתיבי השגיאה של רשימת הגיבויים
  ("רכיב גיבוי לא זמין" / "שגיאה בטעינת קבצי גיבוי") עברו ל-
  TelegramUtils.safe_edit_message_text — בולע רק "message is not modified"
  (ו-parse fallbacks), מפיץ כל שגיאה אחרת. תוקנו שתי הקריאות הצמודות באותו
  בלוק (ה-review ציין את השנייה; הראשונה זהה בדפוס).

- documents: סימון _custom_emoji_warned והלוג עברו לאחרי הצלחת שליחת ה-fallback
  עם 📁 — רק אז מוכח שהדחייה נבעה מהאימוג'י המותאם. BadRequest ממקור אחר כבר
  לא מסמן/מלוגג בטעות; ה-retry נשאר (כשל אחר ייכשל שוב ויתגלגל ל-except החיצוני
  שמנקה את ה-pending).

- טסטים: נתיב ה-fallback (טלגרם דוחה tg-emoji → ההודעה נשלחת שוב עם 📁, ה-ZIP
  נשאר ממתין לבחירה, הדגל מסומן) + המסלול החיובי (ID מוגדר → ההודעה מכילה את
  התג, הדגל לא מסומן). ל-tests/config.py (ה-config החלופי של הטסטים) נוסף השדה
  CUSTOM_EMOJI_ZIP_ID כמו בפרודקשן.

41 טסטים ירוקים, אפס קבצי pending שיוריים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K
@amirbiron
amirbiron merged commit cf98d5f into main Jul 27, 2026
26 checks passed
amirbiron added a commit that referenced this pull request Jul 30, 2026
* feat(mcp): פתקים דביקים — צפייה, יצירה ועריכה דרך ה-MCP

שלושה כלים חדשים מעל קולקציית sticky_notes הקיימת של הוובאפ:
- codekeeper_list_notes — פתקי הקובץ (קריאה טהורה, בלי ה-backfill של ה-GET בוובאפ)
- codekeeper_create_note — פתק חדש על קובץ קיים; line מעגן לשורת מקור,
  בלעדיו הפתק נוצר צף עם sentinel __floating__ מפורש (אחרת ה-JS מעגן
  אוטומטית לשורה הקרובה ודורס את הכוונה)
- codekeeper_update_note — עדכון חלקי לפי note_id, עם annotation נפרד
  (destructive+idempotent) כי פתק נדרס במקום ואין לו היסטוריית גרסאות

עקרונות:
- כותבים בדיוק את סכמת הוובאפ (scope_id מ-sticky_notes_scope.make_scope_id
  הקנוני, ברירות מחדל בפריטת הקליינט) — פתק מה-MCP מופיע מיד ב-UI
- זהות תמיד מהטוקן; יצירה/עדכון מאחורי require_write
- תוכן >5000 תווים נדחה בשגיאה (לא קיטום שקט — סוכן לא ישים לב לאובדן)
- מגן אנטי-לולאה: עד 200 פתקים לקובץ ביצירה
- אינדקס (user_id, scope_id) שחסר היום נוצר lazy/best-effort — משרת גם את
  שאילתת ה-scope הזהה של הוובאפ
- בלי מחיקה ובלי תזכורות (non-goal מתועד); אפס שינויי קוד בוובאפ —
  דיפלוי לשירות ה-MCP בלבד

בדיקות: 19 טסטים הרמטיים חדשים (סניטציה, ולידציות, sentinel, צבעים,
scope filter מול make_scope_id האמיתי, סריאליזציה) + עדכון טסט הרישום.
188 טסטי MCP עוברים; black/flake8/doc8 נקיים; Sphinx נבנה עם 0 אזהרות.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* feat(webapp): קיצור דרך לעריכת תיאור מתפריט ⋮ בעמוד הקובץ

הוספת פריט "תיאור" בתפריט 3-הנקודות בעמוד הצפייה בקובץ (view_file),
ראשון ברשימה, שפותח מודאל קטן לעריכת שדה התיאור — חוסך את הכניסה
לעריכת קובץ מלאה רק כדי לשנות תיאור. "נעץ לדשבורד" עלה לשני, "שתף"
לשלישי.

- משתמש ב-endpoint הקיים POST /api/file/<id>/quick-update שמעדכן את
  התיאור in-place (מטא-דאטה, בלי גרסה חדשה) — אין קוד שרת חדש
- מודאל בדפוס מודאל השיתוף הקיים (Escape, לחיצה על הרקע, טוסט הצלחה),
  עם textarea ומונה תווים (עד 500, תואם למגבלת ה-endpoint)
- עדכון חי של התצוגה מתחת לשם ושל תווית התפריט בלי רענון עמוד
- זמין לכל הקבצים (התיאור הוא מטא-דאטה אוניברסלי)
- אין בעיית מודאל-בתוך-מודאל: התפריט (dropdown) נסגר לפני שהמודאל
  נפתח, בדיוק כמו "שתף קובץ" הקיים
- שינוי template בלבד; דורש דיפלוי לוובאפ

בדיקות: Jinja parse תקין, תחביר JS תקין; אין קוד Python שהשתנה.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(webapp): תבנית חדשה מגיעה מיד אחרי deploy (ETag כולל גרסת deploy)

באג: אחרי deploy של תבנית, משתמשים שצפו בקובץ לאחרונה המשיכו לראות את
הגרסה הישנה (בלי אלמנטים חדשים) הרבה זמן; אחרים כן ראו חדש. השורש:
ה-ETag של /file/<id> ו-/md/<id> חושב מנתוני הקובץ בלבד (updated_at/תוכן/
version/theme) בלי גרסת ה-deploy — אז קובץ שלא נערך החזיר ETag זהה בין
deploys → הדפדפן קיבל 304 והציג HTML ישן מה-cache. תורם שני: מסלול
If-Modified-Since מבוסס updated_at החזיר 304 בנפרד.

תיקון שורשי (webapp/app.py + cache_manager.py):
- _compute_file_etag כולל עכשיו את _STATIC_VERSION (גרסת deploy) — כל
  deploy מבטל ETags ישנים. קורא מרכזי אחד ⇒ מכסה view_file וגם md_preview.
- שלושת מסלולי If-Modified-Since מכבדים RFC 7232 §3.3: מדלגים כשקיים
  If-None-Match (אחרת 304 מיושן גם אחרי שה-ETag השתנה).
- מפתח ה-cache צד-שרת של md_preview כולל את גרסת ה-deploy (אחרת HTML
  מרונדר ישן מוגש עד 30 דק').
- נלווה: invalidate_file_related מבטל עכשיו גם את המפתח האמיתי
  web:md_preview:user:*:{file_id}:* (היה prefix שגוי — עריכת קובץ לא
  ביטלה את cache ה-md שלו).

ב-Render גרסת ה-deploy מגיעה מ-RENDER_GIT_COMMIT (משתנה לכל commit).

אימות: py_compile + flake8 + 9 טסטי cache/invalidation עוברים. את לוגיקת
ה-ETag אימתתי בבידוד (הרצת הפונקציה האמיתית: גרסת deploy משנה את ה-ETag,
יציב לאותו קלט, ותוכן עדיין משנה). טסט יחידה שמייבא webapp.app לא ישים —
טסטי ה-webapp מדולגים ב-CI ("צינור הבוט") ו-flask לא זמין בסביבה.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* feat(webapp): אייקון תיאור על קבצים באוסף + מודאל תצוגה

מתחת לכל קובץ באוסף שיש לו תיאור מופיע אייקון ℹ️; לחיצה פותחת מודאל
קטן (קריאה בלבד) עם התיאור — בלי להיכנס לקובץ.

- Backend: get_collection_items מצרף עכשיו את ה-description של הקובץ
  לכל פריט, דרך אותו batch שכבר מחשב is_file_active (הרחבת ה-projection
  ל-file_name+description) — בלי N+1 ובלי שדות כבדים (Smart Projection
  נשמר; description ≤500 תווים). קובץ בלי תיאור/לא-פעיל ⇒ "".
- Frontend: אייקון ℹ️ ב-.collection-card__meta רק אם יש תיאור;
  openDescriptionModal בדפוס .collection-modal הקיים (Escape/רקע סוגרים,
  textContent — בטוח מ-XSS).
- מצב workspace לא נכלל בשלב זה.

בדיקות: 3 טסטי enrichment חדשים (fakes שתומכים ב-projection) + 49 טסטי
collections_manager עוברים; node --check ל-JS; py_compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* feat(webapp): הזזת אייקון תיאור בכרטיס אוסף + ארכיון לאוספים

אייקון התיאור בכרטיס קובץ עבר משורת ה-meta אל צד שם הקובץ (משמאל, RTL) באותה
שורה, עם רווח. כשהשם ארוך ונשבר לשתי שורות — האייקון "קופץ" לשורת 4 הכפתורים,
משמאל להם (פונקציה layoutDescIcons שמנצלת את זיהוי is-wrapped של autoFitText,
בסדר reset→autoFit→move כדי למנוע oscillation).

ארכיון לאוספים: שדה is_archived חדש (נפרד מ-is_active), toggle 🗄️ "הצג ארכיון"
בסיידבר, וכפתור ארכב/שחזר בכותרת האוסף (מגודר ל-non-workspace). list_collections
קיבל archived_only ו-include_archived; ברירת המחדל מחריגה מאורכבים (ne:True מכסה
גם אוספים ישנים ללא השדה). הקאש כבר מבחין לפי querystring, וה-PUT מנקה את שתי
התצוגות. הגיבוי האישי משתמש ב-include_archived=True כדי לא לפספס אוספים בארכיון.

- database/collections_manager.py: doc-build, allow-list, list filter, serializer, index+backfill
- webapp/collections_api.py: פרמטר archived ב-GET, דילוג על "שולחן עבודה" בתצוגת ארכיון
- webapp/static/js/collections.js + collections.css: אייקון, toggle, כפתורי ארכב/שחזר
- services/personal_backup_service.py: include_archived=True בגיבוי ובבדיקת כפילות
- tests/test_collections_archive.py: ארכוב/שחזור/include/legacy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(webapp): המרת צבעים קשיחים לטוקני ערכה במודאל התיאור ובכפתור הארכיון

לפי theming_and_css.rst אסורים צבעים קשיחים בקבצי רכיבים — רק var(--token).
שתי חריגות הומרו:
- מודאל "ערוך תיאור" (view_file.html): הרקע הכהה הקבוע (#1f2a44, #fff, rgba לבנים,
  focus בצבע primary קשיח) הוחלף בטוקנים סמנטיים — bg-secondary/tertiary,
  text-primary/secondary/muted, glass-border, primary; ה-scrim וה-shadow קיבלו
  טוקן-רכיב עם fallback (var(--modal-backdrop, ...), var(--solid-surface-shadow, ...)).
  כך המודאל מקבל את צבעי הערכה גם בערכות בהירות (rose-pine-dawn, classic).
- כפתור "הצג ארכיון" במצב לחוץ (collections.css): rgba לבנים קשיחים הוחלפו
  בטוקני glass קיימים (glass-hover/glass-border/glass).

מודאל ה-ℹ️ באוספים נבדק ונמצא תקין (יורש var(--collections-modal-*)) — ללא שינוי.
מודאל השיתוף הסמוך הוא legacy קיים ולא נכלל (חוב נפרד).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(bot): תיקון מספר קבצים בהודעות ZIP + שלב בחירת שם ל-ZIP

באג שורש: המחרוזת `סה""כ` פיצלה f-strings לשתי מחרוזות סמוכות — השנייה (בלי
קידומת f) הציגה placeholders כטקסט מילולי (למשל "{len(items)}"). תוקן ב-3 מקומות
(handlers/documents.py, conversation_handlers.py, handlers/save_flow.py) ע"י
מעבר ל-f-string רציף אחד עם מרכאות בודדות. כך "✅ נוסף: X (סה"כ N קבצים)" מציג
את המספר בפועל, וכן כותרת רשימת ה-ZIP השמורים ומסך איסוף הקוד הארוך.

פיצ'ר: שלב בחירת שם ל-ZIP. אחרי "✅ סיום" הבוט מבקש שם (או "⏭️ דלג" לשם אוטומטי):
- conversation_handlers.py: helper משותף finalize_zip_create + _cleanup_zip_state;
  zip_create_finish מציב awaiting_zip_name ומבקש שם; callback חדש zip_create_skip_name.
- main.py: hook בראש handle_text_message שתופס את השם (נבדק ראשון כדי שלא ייבלע/ייחשב קוד).
- שם מנוקה דרך TextUtils.clean_filename + סיומת .zip; fallback ל-my-files-<timestamp>.zip.

טסט: חיזוק test_handle_document_collects_zip_items לאימות שהמספר מוצג בפועל (הגנת רגרסיה).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(bot): הקשחת יצירת ZIP — ניקוי שמות (Zip-Slip), מגבלות איסוף, to_thread, מסלול ביטול

מענה ל-review findings ב-PR #3194:
- ניקוי שם רשומת ZIP (utils.safe_zip_entry_name): basename בלבד, דחיית נתיב מוחלט/
  מקונן ו-"."/".." — הגנת Zip-Slip. משמש בבניית הארכיון במקום השם הגולמי.
- בניית ה-ZIP חולצה ל-utils.build_zip_bytes (טהור) ורצה תחת asyncio.to_thread כדי
  לא לחסום את לולאת האירועים (בהתאם לכלל ה-Performance ב-CLAUDE.md).
- אכיפת מגבלות איסוף (ZIP_CREATE_MAX_FILES=50, ZIP_CREATE_MAX_TOTAL_BYTES=45MB)
  בזמן צבירת הקבצים ב-handlers/documents.py, עם הגנה כפולה גם בשלב הבנייה.
- מסלול ביטול במצב "המתנה לשם": zip_create_cancel עובר דרך _cleanup_zip_state
  (מנקה גם awaiting_zip_name), ונוסף כפתור "❌ ביטול" למקלדת בקשת השם — כך שמשתמש
  שמתחרט לא ישלח ארכיון בטעות בהודעת טקסט כלשהי.
- טסטים: tests/test_zip_bundle_utils.py (ניקוי שמות + מגבלות, נבדק ע"י פענוח ה-ZIP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(bot): הקשחת ZIP סבב 2 — מגבלות לפני הורדה, מניעת שמות כפולים, תיקון docstring

מענה ל-review (סבב 2) ב-PR #3194:
- handlers/documents.py: בדיקות מגבלת ה-ZIP (מספר קבצים + גודל לפי document.file_size)
  הוזזו לפני get_file()/download_to_memory() — לא מורידים לזיכרון קובץ שנדחה מראש.
  בדיקת len(raw) נשמרה כאימות סופי לפער אפשרי מול הגודל המוצהר.
- utils.build_zip_bytes: מניעת שמות רשומה כפולים — הראשון נשמר, הבאים מקבלים סיומת
  ממספרת (x.txt, x_2.txt) עם שמירת הסיומת.
- utils: תיקון docstring (שורה ריקה אחרי רשימת ה-bullets) — מבטל אזהרת docutils/RTD.
- tests: חיזוק test_...skips (file_1/file_2 מדויק) + regression לכפילות שמות.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(webapp): Config Inspector — הפרדת שירותים, סטטוס Set, וביטול מיסוך-יתר של URL

שלושה תיקונים שורשיים ב-Config Inspector (שרץ בתהליך ה-webapp וקורא os.getenv):

1) הפרדה לפי שירות: הצלבנו את כל 236 המשתנים מול עמודת "רכיב" ב-
   docs/environment-variables.rst וגם מול הקוד עצמו (import-closure סטטי של
   webapp/bot/mcp + חיפוש הקוראים של כל KEY). 41 משתנים שנקראים רק בבוט (34,
   כולל ה-webserver הפנימי שרץ בתוך תהליך הבוט), ב-MCP (6) או בסקריפטים (1)
   הוסטו מהעמוד הראשי ל"עמוד 2" חדש (טאב "שירותים אחרים") שמציג מטא-דאטה בלבד
   — בלי Status ובלי Active Value, כי ערכיהם חיים בתהליכים אחרים ואינם נגישים
   מה-webapp. שדה service חדש ב-ConfigDefinition + get_other_services_entries().
   OTEL_EXPORTER_* נשארו בעמוד webapp (ה-SDK קורא אותם מה-env בכל תהליך);
   BOT_USERNAME נשאר webapp ו-BOT_TOKEN נוסף כהגדרה חסרה (נקרא ב-auth_routes).

2) סטטוס "Set" חדש: ערך שהוגדר בסביבה (למשל ברנדר) כשאין ברירת מחדל בקוד אינו
   "Modified" — אין דיפולט שממנו סטינו. determine_status מחזיר Set במקרה זה
   (MCP_SERVER_URL, GITHUB_TOKENS, GITHUB_WEBHOOK_SECRET,
   ALERTMANAGER_WEBHOOK_SECRET, ALERT_TELEGRAM_BOT_TOKEN ודומיהם). נוספו
   set_count לסקירה, ספירה בכרטיסי הקטגוריות ו-pill כחול (טוקן --info) ב-UI.

3) ביטול מיסוך-יתר: "URL" הוסר מ-SENSITIVE_PATTERNS — כתובת ציבורית
   (MCP_SERVER_URL, WEBAPP_URL, PROMETHEUS_URL, PUBLIC_BASE_URL...) אינה סוד.
   URL שמגלם credentials (MONGODB_URL) נשאר ממוסך דרך sensitive=True מפורש,
   ו-TOKEN/SECRET/URI/KEY ממשיכים להיתפס בתבניות.

בנוסף: תוקנו 5 שורות "רכיב" שגויות ב-docs/environment-variables.rst שהתגלו
בהצלבה (ENABLE_INTERNAL_SHARE_WEB, SENTRY_WEBHOOK_SECRET,
SENTRY_WEBHOOK_DEDUP_WINDOW_SECONDS, DUMMY_BOT_TOKEN → Bot; BOT_TOKEN → Bot/WebApp).

טסטים: 32 ב-test_config_inspector_service.py (כולל 3 מחלקות חדשות: סטטוס Set,
מיסוך URL, הפרדת שירותים) — ירוקים. תחביר Jinja אומת.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(webapp): Config Inspector v2 — webserver כשירות נפרד, משתנים משותפים בעמוד 2, ותיקון העתק-הכל

שלושה תיקונים בהמשך למשוב:
- ה-webserver הוא שירות Render נפרד (ההרצה הפנימית בתוך תהליך הבוט בוטלה):
  ה-closure שלו חושב בנפרד מהבוט, ומשתנים שנקראים בו (SENTRY_WEBHOOK_SECRET,
  SENTRY_WEBHOOK_DEDUP_WINDOW_SECONDS) מסומנים service=webserver. עודכן גם
  "רכיב" ב-environment-variables.rst (Webserver) ותואר ENABLE_INTERNAL_SHARE_WEB.
- משתנים משותפים: השדה service הוחלף ב-services (tuple) — משתנה יכול להשתייך
  לכמה שירותים. עמוד 2 מציג עכשיו את כל 220 המשתנים ששייכים לשירות שאינו webapp,
  כולל המשותפים (למשל MONGODB_URL: bot + mcp + webserver), עם ציון השירותים בכל
  שורה ותג "גם Webapp" למשתנים שערכיהם מוצגים בעמוד הראשון. עמוד 1 נשאר 196.
- תיקון P2 מה-review: "העתק הכל" הוגבל ל-#inspectorPageWebapp — טבלת השירותים
  האחרים (מטא-דאטה בלי ערכים) לא מייצרת יותר שורות KEY= ריקות בייצוא ה-.env.

טסטים: 33 ב-test_config_inspector_service.py (עודכנו לסמנטיקת services + טסט
משתנה-משותף-בשני-העמודים) — ירוקים. תחביר Jinja אומת.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* feat(bot): סוג קובץ חדש "סקיל" — אחסון נפרד מגיבויים

סקילים (ZIP עם SKILL.md) נכנסו בטעות למסלול הגיבויים, מה שגרם לשלוש בעיות:
save_backup_bytes דוחס מחדש ומזריק metadata.json (לא byte-for-byte),
cleanup_expired_backups מוחק לפי retention, ו-restore עם purge הרסני.

הפתרון — אחסון עצמאי לחלוטין:
- SkillManager חדש (file_manager.py): קולקציית GridFS "skills" נפרדת, שמירת
  bytes as-is (fs.put ישיר), תמיד מונגו בלי תלות ב-BACKUPS_STORAGE ובלי env var.
  skill_id ייחודי (timestamp+uuid) מונע התנגשות; שם קובץ עם סיומת ייחוד מונע דריסה.
- ניתוב בהעלאה: _maybe_store_zip_copy מציג שני כפתורים "סקיל"/"גיבוי" במקום
  שמירה אוטומטית; ה-bytes נשמרים זמנית עד לבחירה מפורשת (עזרי stash ב-utils.py,
  מחיקות מוגבלות ל-allowlist ייעודי).
- SkillMenuHandler חדש (prefix skill_): רשימה + הורדה/מחיקה/תיוג/הערה, כפתור
  "📝 סקילים" בתפריט "הצג את כל הקבצים שלי". תיוג/הערה דרך ה-facade הגנרי הקיים.

טסטים: שמירה+הורדה byte-for-byte, בידוד מ-cleanup של הגיבויים, ושני סקילים
עם אותו שם שאינם דורסים. טסטי הגיבויים הקיימים נשארים ירוקים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(bot,webapp): סבב review לסקילים — כשלי CI, אבטחה וביצועים

תיקון כשלי CI/RTD ויישום ממצאי code review על פיצ'ר הסקילים.

CI/RTD:
- עדכון test_documents להתנהגות "בחירה מפורשת" (כפתורים) במקום שמירה אוטומטית
- עדכון סדר תפריט "הצג את כל הקבצים" בשני טסטי patch_coverage (skill_list)
- תיקון docstring שהכשיל את RTD (הסרת * חשוף שנפרש כ-emphasis)

אבטחה/נכונות:
- main: logger.exception בשמירת הערות, בלי חשיפת שגיאת DB למשתמש
- file_manager: נרמול user_id ל-int לפני שמירת סקיל (עקביות שאילתת list_skills)
- config_inspector: מיסוך URL עם credentials מוטמעים (user:pass@) גם בשם לא-רגיש
- utils: אימות token בטוח לשם קובץ + הרשאות 0o700 לתיקיית ה-pending
- conversation: ניקוי ה-pending רק אחרי שמירה מוצלחת (מאפשר retry בכשל)

ביצועים/עקביות:
- skill_menu: איסוף דירוגים ב-thread, הורדה בסריקת GridFS אחת, safe_edit_message_text
- conversation: load ל-to_thread + safe_edit על תשובות ה-routing
- documents: קבוע PENDING_ZIP_TTL_SECONDS + הגבלת ZIP ממתינים למשתמש

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(bot): שמירת סקילים נכשלה — pymongo Database לא תומך ב-bool()

הבאג בפרודקשן ("save_skill_bytes: GridFS 'skills' לא זמין"): ב-_get_skills_gridfs
נעשתה בדיקה ``if not mongo_db``, אבל get_mongo_db() מחזיר אובייקט Database אמיתי של
pymongo — ו-bool()/not עליו זורק NotImplementedError. החריגה נבלעה ב-except הכללי,
המתודה החזירה None, והסקיל לא נשמר. חיבור המונגו עצמו תקין (קטעי קוד כן נשמרים).

השורש: pymongo אוסר truth-value testing על Database/Collection/MongoClient. שאר
מתודות ה-facade כבר משוות נכון עם ``is None`` — רק שני עוזרי ה-GridFS השתמשו ב-not.

התיקון (root-cause, שני המקומות):
- SkillManager._get_skills_gridfs: ``if not mongo_db`` → ``if mongo_db is None``.
- BackupManager._get_gridfs: אותו באג רדום (מוסתר בפרודקשן ע"י BACKUPS_STORAGE=fs,
  שמחזיר None עוד קודם) — תוקן גם הוא כדי שלא יתפוצץ במצב BACKUPS_STORAGE=mongo.

טסטים: הטסטים הקיימים מוקים את _get_skills_gridfs ולכן פספסו את המסלול האמיתי.
נוספו טסטי רגרסיה שקוראים למתודה האמיתית עם Database אמיתי של pymongo (connect=False,
בלי שרת) ומוודאים שמוחזר GridFS ולא None. אומת red→green. 11 טסטים ירוקים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(webapp): Config Inspector — שחזור רווחים, סינון בעמוד 2, ודיוק שיוך שירותים

שלושה תיקונים בעקבות פידבק על העמוד:

1. רווחים שנעלמו: עטיפת הטאבים (.inspector-page) היא display:block, ולכן הילדים
   (כרטיסי סיכום, שורת סינון, קטגוריות, טבלה) איבדו את ה-gap של ה-flex ההורה
   (.config-page). העמוד הפעיל הוא עכשיו flex column עם אותו gap — הרווחים חזרו.

2. שורת סינון לעמוד 2 (שירותים אחרים): קטגוריה + שירות (אין שם Status כי אין
   ערכים חיים), עם סנן/איפוס/העתק הכל. הסינון client-side; "העתק הכל" מעתיק
   KEY=default עבור השורות הגלויות ומציין בטוסט שאלה ערכי ברירת מחדל.

3. דיוק שיוך שירותים (68 משתנים): מיפוי אוטומטי של צריכה בפועל — לכל משתנה נבדק
   באילו קבצים הוא נקרא (getenv/config.X) והאם הקובץ שייך לשירות או נטען בסגירת
   ה-imports שלו (entry: mcp_server/app.py, services/webserver.py). משתנים שסווגו
   ל-MCP/Webserver בלי שימוש אמיתי הוסרו משם (21 מ-MCP, 65 מ-Webserver — למשל
   UPTIME_*, PUSH_*, VAPID_*, MAINTENANCE_*, ALERTMANAGER_*). חריגים ידניים:
   PORT נשאר (נצרך בפקודת ההרצה), PROFILER_* נשארו ב-webserver (profiler_handler
   נטען שם בפועל).

   בפרט BACKUPS_STORAGE/BACKUPS_DIR עברו ל-Bot בלבד (גם ב-rst): רק הבוט טוען את
   file_manager; לוובאפ מנגנון גיבוי נפרד (WEBAPP_BACKUPS_DIR). הגיבויים האלה הם
   ZIP של קטעי הקוד (get_user_files) + גיבויי GitHub/Drive — לא אוספים/סימניות.

טסטים: 67 ירוקים (config_inspector, skill_manager, mcp_docs, rst_parser),
Jinja template מתקמפל.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(bot): סבב review לסקילים — PII בלוגים, ניקוי pending, מכסות, ושאילתות ממוקדות

תיקונים בעקבות code review (כל הממצאים אומתו מול הקוד):

אבטחה/פרטיות:
- file_manager: אזהרת user_id לא תקין רושמת רק את סוג הערך, לא את הערך עצמו (PII).
- utils: הערה מבהירה ש-_SAFE_TOKEN_RE ([A-Za-z0-9_-]) מונע path traversal ב-stash
  (false positive של static analysis — אין תיקון קוד).

נכונות:
- documents: stash_pending_zip_bytes ו-cleanup_stale_pending_zips רצים ב-to_thread
  (I/O של דיסק בתוך handler אסינכרוני); אם שליחת הודעת הבחירה נכשלת — הקובץ
  והרשומה שנוצרו עבור אותו ZIP מנוקים (בלי כפתורים אין דרך להשלים את הבחירה).
- skill_menu: query.answer() לפני עריכה גם ב-delete_confirm/delete_execute
  וב-send_rating_prompt; ב-skill_rate המענה לפני עריכת ההצלחה בלבד — answer מוקדם
  גורף היה מבטל את ה-show_alert של הודעות השגיאה הקיימות.
- file_manager: נרמול user_id ל-int בכל המתודות (list/get/info/delete) — קלט str
  לא היה מוצא כלום מול שמירה כ-int.

מכסות ואינדקסים (לסקילים אין retention — בלי מכסה האחסון גדל ללא גבול):
- SKILLS_MAX_PER_USER (ברירת מחדל 100) ו-SKILLS_MAX_TOTAL_BYTES (ברירת מחדל 1GB,
  0 = כיבוי) נאכפים לפני fs.put; תועדו ב-rst וב-config inspector (שירות Bot).
- אינדקסים על skills.files: (user_id, skill_id) ו-(skill_id) — best-effort פעם אחת.

ביצועים/מבנה:
- get_skill_info חדש: שליפת סקיל בודד בשאילתה ממוקדת; _find_skill ופרטי/הורדת
  סקיל כבר לא סורקים את כל הרשימה. delete_skills מסנן ב-DB עם $in.
- conversation: ספירת קבצי ה-ZIP אוחדה עם השמירה להלפר אחד שרץ ב-to_thread.
- main: זרימות הערה לגיבוי/סקיל אוחדו להלפר משותף; save_backup_note ב-to_thread.
- SkillInfo הומר ל-dataclass; send_rating_prompt מלוגג כשל ומודיע במקום pass.

טסטים: fixture עם client.close(); fake תומך $in; טסטי מכסות, נרמול str,
ו-get_skill_info; ניקוי קבצי pending בשני טסטים שהשאירו קבצים ב-tmp (אומת 0
שאריות); עדכון הערת הסדר ב-test_patch_coverage. 77 טסטים ירוקים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* feat(bot): נוסח חדש להודעת קליטת ZIP, "קבצי גיבוי" במקום "קבצי ZIP", ואייקון מותאם

שלושה שינויים ברמת התצוגה (שום callback_data או מפתח לוגי לא השתנה — אומת שהתווית
אינה משמשת כמפתח זיהוי בשום handler/regex):

1. הודעת קליטת ZIP (documents.py): "איפה לשמור אותו?" במקום "איך", שם הקובץ בשורת
   הכותרת, תיאורי הקטגוריות (סקילים / גיבויים עם אזכור שחזור ריפו בלחיצה), ואייקון
   הסקיל 🧩 בטקסט ובכפתור. html_escape על שם הקובץ נשמר.

2. התווית "📦 קבצי ZIP" → "📦 קבצי גיבוי" — רוכזה לקבוע BTN_BACKUP_ZIPS
   (i18n/strings_he.py, מקור אמת יחיד) הנצרך בכל 6 מופעי הכפתור: תפריט 📚 (message
   + callback), עיבוד Batch, GitHub upload (כולל המקלדת המשוכפלת), ותפריט Drive.
   בהיקף שאושר עודכנו גם הטקסטים המפנים לכפתור (עזרה, הודעת אישור גיבוי, כותרות
   רשימה, הודעות Drive) והתיעוד (BOT_USER_GUIDE, drive_menu.rst). מופעי "ZIP/גדולים"
   בניסוח אחר, docstrings, וה-webapp — במפורש מחוץ להיקף.

3. אייקון ZIP מותאם: tg_emoji(emoji_id, fallback) גנרי ב-utils (לגוף הודעה עם
   parse_mode=HTML בלבד — כפתורים לא תומכים ב-entities), שדה CUSTOM_EMOJI_ZIP_ID
   ב-config (ברירת מחדל None — ה-ID חי רק ב-ENV, משאב צד ג'). בהודעה: ניסיון עם
   האייקון המותאם; על BadRequest — לוג חד-פעמי לתהליך ושליחה חוזרת עם 📁 (המשתמש
   מקבל את ההודעה בכל מקרה). fallback הוא 📁 בדיוק (שתי יחידות UTF-16). תועד
   ב-rst וב-config inspector (display/Bot).

טסטים: נעילת הנוסח החדש ("איפה לשמור", כפתור "🧩 סקיל") + טסט tg_emoji.
85 טסטים ירוקים; אומת ב-diff שכל שינויי הכפתורים הם תווית בלבד.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* style(bot): איחוד אייקון הסקיל ל-🧩 בכל המופעים

בהמשך לשינוי אייקון הסקיל בהודעת קליטת ה-ZIP — איחוד לכל שאר המופעים של
הסקיל-כישות (📝 → 🧩): כפתור "🧩 סקילים" בתפריט 📚 (שתי הגרסאות), כותרת
הרשימה, כפתורי הפריטים, כותרת פרטי הסקיל, caption בהורדה, הודעת "אין סקילים",
והודעת ההצלחה שמפנה לתפריט. שינוי תצוגה בלבד — שום callback_data לא השתנה.

אייקון ה-📝 בתפקידיו האחרים (הערה / ערוך הערה / שנה שם) נשאר בכוונה — הוא
מציין פעולת כתיבה, לא את ישות הסקיל.

51 טסטים ירוקים; אומת שלא נותר "📝 סקיל/סקילים" בקוד.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(bot): סבב review על PR #3200 — wrapper לעריכות שגיאה ו-fallback מדויק לאימוג'י

שלושת הממצאים אומתו מול הקוד ותוקנו:

- github_menu_handler: שתי עריכות ההודעה בנתיבי השגיאה של רשימת הגיבויים
  ("רכיב גיבוי לא זמין" / "שגיאה בטעינת קבצי גיבוי") עברו ל-
  TelegramUtils.safe_edit_message_text — בולע רק "message is not modified"
  (ו-parse fallbacks), מפיץ כל שגיאה אחרת. תוקנו שתי הקריאות הצמודות באותו
  בלוק (ה-review ציין את השנייה; הראשונה זהה בדפוס).

- documents: סימון _custom_emoji_warned והלוג עברו לאחרי הצלחת שליחת ה-fallback
  עם 📁 — רק אז מוכח שהדחייה נבעה מהאימוג'י המותאם. BadRequest ממקור אחר כבר
  לא מסמן/מלוגג בטעות; ה-retry נשאר (כשל אחר ייכשל שוב ויתגלגל ל-except החיצוני
  שמנקה את ה-pending).

- טסטים: נתיב ה-fallback (טלגרם דוחה tg-emoji → ההודעה נשלחת שוב עם 📁, ה-ZIP
  נשאר ממתין לבחירה, הדגל מסומן) + המסלול החיובי (ID מוגדר → ההודעה מכילה את
  התג, הדגל לא מסומן). ל-tests/config.py (ה-config החלופי של הטסטים) נוסף השדה
  CUSTOM_EMOJI_ZIP_ID כמו בפרודקשן.

41 טסטים ירוקים, אפס קבצי pending שיוריים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* docs: עמוד תיעוד ל-Config Inspector + עדכון הנחיית Sphinx ב-CLAUDE.md

הידע על ה-Config Inspector חי עד היום רק בהיסטוריית ה-PRים, ולכן מי שמוסיף משתנה
חדש חוזר על אותן טעויות: סטטוס Modified שגוי, שיוך שירות מנוחש, ותיאור שלא תואם
בין הקוד ל-rst. העמוד החדש מרכז את המודל ואת נוהל העבודה.

docs/webapp/config-inspector.rst (חדש) — לצד cache-inspector, אותו דפוס:
- למה עמוד 1 מציג רק משתני Webapp: ה-inspector רץ בתוך תהליך ה-webapp ורואה רק את
  ה-ENV שלו; הצגת Status למשתנה של שירות אחר הייתה מטעה (מידע שגוי גרוע מהיעדר מידע).
- מה בעמוד 2 (Bot/MCP/Webserver): מטא-דאטה בלבד, בלי Status/Active Value, עם תגית
  "גם Webapp" למשתנים משותפים והפניה ל-Render Dashboard לערכים בפועל.
- ארבעת הסטטוסים + הכלל השורשי למניעת Modified שגוי: ה-default בהגדרה חייב להיות
  זהה תו-בתו לדיפולט שבקוד; אין דיפולט בקוד ⇒ להשאיר ריק כדי לקבל Set.
- איך בודקים שיוך שירות לפני הוספה: grep על הצריכה בפועל + טבלת נקודות כניסה
  (main.py/webapp/mcp_server/webserver/scripts), וקובץ משותף לפי שרשרת ה-imports.
- משתנה לכמה שירותים: מתועד בכל המקומות — services מלא, עמודת "רכיב" תואמת ב-rst,
  ותיאור זהה בשני המקומות.
- מיסוך רגישים (כולל למה URL הוסר ו-URI נשאר) וצ'קליסט הוספת משתנה.

קישורים: רישום ב-docs/index.rst (חובה — בלי זה RTD נכשל על אזהרה), ו-seealso
ב-environment-variables.rst שמפנה לכללים מתוך ההנחיה המחייבת שבראש העמוד.

CLAUDE.md: השורה על החרגת examples.rst הייתה מיושנת (העמוד כבר ב-toctree ואינו
ב-exclude_patterns). הוחלפה בכלל השימושי — כל עמוד חדש חייב להירשם ב-docs/index.rst.

ולידציה: בניית Sphinx עם -W (בדיוק כמו RTD) עברה עם 0 אזהרות; אומת שהעמוד נבנה,
מופיע ב-toctree, וכל ההפניות ההדדיות נפתרו. 34 טסטים ירוקים. שינויי תיעוד בלבד.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(webapp): קישורי תפריט פנימיים במסמכי Markdown לא גללו לסעיף

השורש (אומת בשחזור מלא מול markdown-it האמיתי): מסמכים רבים מגדירים עוגן לכותרת
בתחביר HTML — `## 1. תקשורת <a id="communication"></a>` — ומפנים אליו מתוכן
העניינים. ה-renderer רץ עם html:false (הגנת XSS על תוכן שמשתמשים מעלים), ולכן
התגית לא הופכת לאלמנט. שלושה נזקים בפועל:
  1. אין אלמנט עם id="communication" → הקישור לא מוביל לשומקום (קישורים בתוך
     התוכן נשענים על התנהגות דפדפן טבעית — אין להם handler).
  2. התגית מוצגת כטקסט זבל גלוי בתוך הכותרת.
  3. הטקסט הזה נבלע ל-id האוטומטי ומייצר slug מעוות
     (id="1-תקשורת-a-idcommunicationa").

התיקון שומר על html:false — לא נפתחה פרצה:
- webapp/static/js/md-anchors.js (חדש): שליפת העוגנים ממקור ה-Markdown לפני
  הרינדור וניקויָם מהטקסט, ואחרי הרינדור הוספת אלמנט יעד בלתי-נראה לפני הכותרת.
  תומך גם בעוגן בשורה נפרדת לפני הכותרת (דפוס GitHub), ב-name= לצד id=, ומדלג
  על בלוקי קוד. ה-id מוצב דרך element.id (DOM property) ולא innerHTML — אין
  וקטור הזרקה, ובנוסף יש סניטציה.
- מזהי הכותרות עצמם לא נדרסים: הם משמשים לסימניות על כותרות, ודריסה הייתה
  שוברת סימניות קיימות. לכן יעד נפרד ולא שינוי של heading.id.
- חובר ל-md_preview.html (הצופה הראשי) ול-live-preview.js (תצוגה מקדימה בעורך,
  אותו באג), עם fallback שקט אם המודול לא נטען.

באג נוסף שתוקן אגב: md_preview.html השתמש ב-querySelector(hash) לגלילה לפי
עוגן בכתובת. מזהה שמתחיל בספרה (כמו "1-תקשורת" מכותרת ממוספרת) אינו סלקטור CSS
חוקי והקריאה זרקה — הוחלף ב-getElementById עם decodeURIComponent.

טסטים: tests/md-anchors.test.js (חדש, `node tests/md-anchors.test.js`) — 21
בדיקות ירוקות: שליפה, דפוס GitHub, דילוג על בלוקי קוד, כמה עוגנים לכותרת,
סניטציה, והחלה על DOM (כולל אי-דריסת מזהה תפוס). 81 טסטי פייתון ירוקים; לא
נגעתי בשום קובץ .py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

* fix(webapp): כותרות Setext בעוגני Markdown + ניתוק מצב מודולרי בתצוגה החיה

סבב review — כל הממצאים אומתו מול הקוד ותוקנו:

1. כותרות Setext (באג נכונות אמיתי, אומת בשחזור): extractExplicitAnchors ספר
   רק כותרות ATX, אבל markdown-it מייצר h1/h2 גם מכותרות Setext (שורת טקסט
   ואחריה === או ---). התוצאה: האינדקסים זזו והעוגן הוחל על הכותרת הלא נכונה —
   במסמך "מבוא\n====" ואחריו "## תקשורת <a id=...>" העוגן נחת על "מבוא".
   נוסף זיהוי Setext עם שמירה על ההתנהגות ל-ATX, ועם בדיקה שמרנית שמונעת זיהוי
   שגוי: קו מפריד (hr) אחרי שורה ריקה, שורת הפרדה של טבלה, פריט רשימה, ציטוט,
   וקוד מוזח — כולם אינם כותרת. נוסף פרמטר setext (ברירת מחדל true) לכיבוי
   כש-md.disable('lheading') פעיל; אומת שאף צרכן בריפו לא מנטרל אותו.

2. ניתוק המצב המודולרי ב-live-preview: pendingExplicitAnchors המשותף בין
   render() ל-enhance() אפשר לשני רינדורים עוקבים לדרוס זה את העוגנים של זה —
   וזה החמיר בגלל ש-repo-browser.js הוא צרכן נוסף של אותו זוג. עכשיו
   renderWithAnchors() מחזיר { html, anchors } והם מועברים במפורש ל-
   enhance(root, anchors). render() נשאר עם החתימה הקיימת (מחזיר HTML) כדי לא
   לשבור קוראים.

3. repo-browser: מנוצל התיקון — כבר היה שם handler תקין לגלילת עוגנים
   (setupMarkdownAnchorScrolling עם getElementById), רק שהעוגנים מעולם לא נוצרו.
   מעבר ל-renderWithAnchors + טעינת md-anchors.js ב-base_repo.html משלימים את
   התמונה גם בעיון בקבצי GitHub.

4. תיעוד config-inspector: פקודות החיפוש הוחלפו ב-rg עם -w (מילה שלמה) ו-
   -g '!tests/**' — הן תופסות עכשיו גם os.getenv('X') בגרש בודד וגם config.X
   בפקודה אחת, בלי להיתפס ל-X_OTHER. הודגם בפועל לפני העדכון. בנוסף הובהר
   שהמיסוך חל רק על ערכים שסווגו כרגישים, ולא על כל ערך.

טסטים: 33 ירוקים (נוספו 12 ל-Setext, כולל מקרי אי-זיהוי), ובנוסף הצלבה של כל
תשעת המקרים מול ה-DOM ש-markdown-it באמת מייצר — 9/9 תואמים. Sphinx -W עבר,
81 טסטי פייתון ירוקים.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNFuSyshwpRcxozVZEui5K

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants