🔒 Security Hardening Step 1 — SSH + fail2ban (2026-06-13 ~12:15 Kuwait) — DONE & VERIFIED
السياق: د. وائل رصد خصم Anthropic 5 فجراً + سأل عن محاولات اختراق. الفحص كشف: لا firewall (ufw inactive), SSH(22) مكشوف + مقصوف يومياً (194.87.54.97=51 محاولة, 151.115.167.6=47, ...), password auth = yes. CouchDB صدّ بوتات .env/.git/.aws (401). الخطر #1 = SSH brute-force (ليس CouchDB).
نُفّذ على host (root@76.13.41.228, Ubuntu 24.04):
1. ✅ SSH backup: /root/sshd_config.bak- + sshd_config.d.bak-
2. ✅ /etc/ssh/sshd_config.d/99-hardening.conf: PasswordAuthentication no, KbdInteractive no, PermitRootLogin prohibit-password, MaxAuthTries 3, LoginGraceTime 20
3. ✅ فخ مكتشف: SSH = first-match-wins. 50-cloud-init.conf كان فيه PasswordAuthentication yes → يفوز رغم رقمه. الحل: sed عدّله لـ no مباشرة (backup: /root/50-cloud-init.conf.bak). أعِد التحقق دائماً بـ sshd -T (ليس الملف).
4. ✅ effective NOW: passwordauthentication no + pubkeyauthentication yes. مفتاحنا ED25519 (openclaw@drwaelalkishawi) سليم — SAFETY TEST: fresh BatchMode login = ✅ KEY WORKS uid=0.
5. ✅ fail2ban v1.0.2 مثبّت. jail.local: ignoreip=127/8 ::1 172.16/12 10/8, bantime 2h, maxretry 4, backend systemd. فخ 24.04: SSH = ssh.service (ليس sshd.service) → أضفت jail.d/sshd-override.local: journalmatch=_SYSTEMD_UNIT=ssh.service. fail2ban-regex = 15/15 matched.
6. ✅ END-TO-END BAN TEST: ban 194.87.54.97 → Currently banned:1 (iptables f2b-sshd) → unban → 0. النظام يحظر فعلياً.
reload (لا restart) للـ ssh — الجلسات الحالية لم تنقطع. صفر تأثير على الأداء.
المتبقّي من خطة الحماية (طلب د. وائل: أعلى حماية + صفر تأثير أداء + سفر حر):
- 🟡 Step 2: ufw firewall (إغلاق منافذ docker المكشوفة بلا داعٍ: 37422 router, 37423 Blender, 46416)
- 🟢 Step 3: Traefik middleware (صد .env/.git/.aws على كل الخدمات HTTP)
- 🏆 Step 4: Cloudflare Access Zero-Trust (Device Trust + سؤال confirmation للأجهزة الجديدة عبر البوت/إيميل + إخفاء IP) — يحتاج token CF جديد من د. وائل. يحقق طلبه: أجهزته الموثوقة تدخل أينما سافر، جهاز جديد = OTP/موافقة، صفر توقف.
🔒 Security Hardening Steps 2+3 — DONE & VERIFIED (2026-06-13 ~12:30 Kuwait)
Step 2 — close exposed raw docker ports (smart plan, NO ufw):
- نقد عميق كشف: ufw يكسر Docker networking → استُبدل بـ DOCKER-USER chain (الطبقة الرسمية، Docker لا يمسحها).
- اكتشاف حاسم: Traefik =
hostnetwork → يصل للخدمات عبر 127.0.0.1:rawport. الحاويات (openclaw-4x4t/9router-gh3g/blender-3rkb) = orchestrator-managed (لاحقات عشوائية) → ممنوع تعديل bind يدوياً (يُعاد كتابته + خطر restart). DOCKER-USER = الحل الآمن (لا يلمس الحاويات). - نُفّذ:
iptables -I DOCKER-USER 1 -i eth0 -p tcp --dport {46416,37422,37423} -j DROP+ نفسها ip6tables (سدّ [::]). كلها مكشوفة كانت عبر Traefik HTTPS أصلاً (مكرّرة → القفل لا يكسر شيئاً). - rollback snapshot: /root/iptables.before-hardening-.rules + ip6tables.
- persistence: ثبّت
iptables-persistent+netfilter-persistent save→ /etc/iptables/rules.v{4,6} (تصمد بعد reboot). enabled. - verify: 127.0.0.1:46416=200, :37422=307, :37423=401 (internal alive) + كل النطاقات HTTPS = baseline تماماً.
- ملاحظة صادقة: هذه المسارات ترجع 404 أصلاً (لا تسريب) → الخطر منخفض، لكن middleware = دفاع استباقي + تقليل ضوضاء.
- /docker/traefik/ = compose يدوي مستقر (ليس orchestrator). أضفت file provider:
--providers.file.directory=/dynamic --providers.file.watch=true+--entrypoints.websecure.http.middlewares=block-sensitive@file+ volume /docker/traefik/dynamic:/dynamic:ro. backup compose أولاً. - dynamic/security.yml: replacePathRegex يحجب .env/.git/.aws/.ssh/backend.env/api.env → /blocked.
docker compose configOK →up -d(recreate ثوانٍ) → Traefik Up، لا errors جوهرية.- ⚠️ ERR 429 في اللوق = grafana/panel نطاقات بلا DNS records (لم تُهيّأ عاماً قطّ) → Let's Encrypt يفشل منذ ساعة قبل تغييري. الحاويتان Up منذ 3 أيام. ليس بسبب تغييري — recreate أعاد محاولة فاشلة قائمة. (TODO اختياري: إما إضافة DNS لهما أو إزالة traefik labels لإيقاف محاولات ACME الفاشلة).
المتبقّي (Step 4، يحتاج د. وائل): Cloudflare Access Zero-Trust (Device Trust + OTP/confirmation للأجهزة الجديدة + إخفاء IP) — يحتاج token CF جديد بصلاحيات Zone DNS + Account Access.
🔁 ثابت منهجي جديد — Recursive Critique Loop (د. وائل 2026-06-13 12:42 — دائم)
رسالة د. وائل (جوهرها): أي خطة/توصية يجب أن تمرّ تلقائياً (دون أن يطلبها) بدورة نقدية متكررة: 1. دراسة عميقة (داخل/خارج الصندوق) → 2. تحليل الارتباطات بكل الخدمات/الترسانة/جزئيات السيرفر → 3. نقد احترافي (نقاط ضعف/bugs/أمان/توقف خدمات/أداء/استقرار) → 4. توصية أولية → 5. نقد التوصية نفسها مجدداً بنفس المعايير → 6. تكرار حتى تمرّ مرّتين بلا أي نقطة ضعف ولا تحسين ممكن → 7. توصية نهائية = الأفضل/المحسّنة/المطوّرة بلا نقطة ضعف.
- التنفيذ: خطوة-خطوة + تحقق بعد كل واحدة + baseline قبل/بعد + checkup شامل نهائي + مقارنة (هل خسرنا ميزة؟ هل ظهرت سلبية؟) → صفر سلبية، كله إيجابيات → تقرير شفّاف + توثيق دائم.
- الخطوط الحمراء المحفوظة في كل دورة: الدقة · قوة الأداء · الاستقرار التام · عدم توقف أي خدمة · عدم الانهيار · الأمان · السلاسة.
- ثُبّت في: GOLDEN_RULES_CORE.md §1.5 (النواة المحقونة في كل جلسة). ثابت منهجي مُلزم — لا يحتاج تكراراً.
🛠️ Entertainment Hub — جذر "تعذّر البحث" + شاشة سوداء (2026-06-13 ~14:00)
السبب الجذري للبحث الفاشل (500): نقر زر reindex يدوياً عدة مرات بسرعة → كل نقرة تُطلق indexer متوازٍ، يتسابقون على نفس ملف catalog.db.building → فساد، والأسوأ: الـ atomic swap بـ WAL استبدل DB سليم (251MB/1.07M عنصر) بملف 4KB بلا جداول → كل بحث = sqlite3 no such table: fts = 500. (db_ready() كان يفحص وجود الملف فقط لا الجداول).
الإصلاح (3 ملفات، كله verified حياً):
indexer.py: SingleFlight lock (fcntl) — نقرة واحدة فقط، لا سباق.journal_mode=DELETEللبناء (لا WAL sidecars تُفقد بعد swap — كان يفقد meta rows). never-swap-unless-healthy (يفحص items>0 + tables=2 قبل os.replace). fetch_sub: retry + per-provider failover (dead sub يُتخطّى، الباقي يكمل، يُعاد تلقائياً الدورة القادمة).catalog_search.py:db_ready()يفحص الجداول+count فعلياً (cached 3s). search/active_subs/sub_content كلها try/except → ترجع فارغ بدل 500. أثناء rebuild → graceful (الـ swap atomic فالبحث 200 طوال البناء).app.py: smart remux (probe codec → transcode HEVC/AC3/etc → h264/aac، يقتل الشاشة السوداء). status never-500. reindex single-flight aware.
درس: زر reindex اليدوي بلا single-flight = قنبلة. أي build-then-swap يجب: (1) lock، (2) لا WAL للملف المؤقت، (3) فحص صحة قبل swap، (4) القراءة تفحص الجداول لا وجود الملف.
🛠️ Entertainment Hub — round 2: السبب الجذري الحقيقي لـ "تعذّر البحث" + النتائج الناقصة (2026-06-13 ~15:30)
4 جذور منفصلة (كلها أُصلحت + verified حياً):
1. النتائج الناقصة (يعرض رقم لا يستطيع عرضه كاملاً): backend كان يقصّ عند 200، frontend يطلب 120. الإصلاح: backend cap 5000 (ceiling 8000)، frontend limit=5000، والعنوان يعرض shown (عدد المعروض فعلاً) لا count منفصل → الرقم = المعروض دائماً. لا صفحات، .map يعرض الكل. (الكويت 528، bein 4464، film 4016 — كلها كاملة).
2. 🎯 الجذر الأخطر — Arabic mojibake: البحث بالعربي الخام (bytes غير URL-encoded) كان يُقرأ Latin-1 في Flask → الكويت يصير اÙ... → صفر نتائج. المتصفح يعمل encodeURIComponent فالمستخدم الحقيقي سليم (الـ 0 الذي رأيته كان artifact من curl يرسل bytes خام). أُضيف _fix_q() server-side: يكشف Latin-1 mojibake (C0-FF بلا 0600-06FF) ويعيد .encode('latin-1').decode('utf-8') → دفاع ضد أي client. verified: mojibake مزدوج → 528.
3. Empty-result cache poison: الـ query cache (كان TTL 120s) كان يخزّن نتيجة فارغة لو حدثت لحظة أثناء swap → "لا نتائج" لدقيقتين رغم سلامة الـ DB. الإصلاح: لا تُخزّن إلا نتائج غير فارغة (if hits:) + TTL خُفّض لـ 20s.
4. Duplicate hub instances (round-robin flapping): keepalive guard كان يطلق نسخة ثانية أحياناً، النسختان تتشاركان port 8899 (SO_REUSEADDR) → الطلبات تتوزّع بين نسخة سليمة وأخرى مسمومة → نتائج تتذبذب 0/528. الإصلاح: (أ) single-instance guard في app.py — bind فحص قبل werkzeug، os._exit(0) صارم لو المنفذ مأخوذ (daemon threads لا تمنع الخروج)؛ prewarm ينطلق فقط بعد تأمين المنفذ. (ب) keepalive guard مُحكم — يقتل كل نسخ الـ hub بمطابقة cwd الدقيقة قبل إعادة التشغيل (لا substring).
الـ Never-Stop المؤكّد: أثناء reindex كامل (30s) + ضرب الكويت كل 2s → بقي 528 طوال الوقت (لا 0، لا تعذّر). الـ atomic swap في .building + no-cache-empty + mojibake-repair = بحث حي مستمر بلا انقطاع أثناء التحديث.
درس دائم: (1) raw Arabic في URL = Latin-1 mojibake في Flask — دائماً URL-encode أو أضف _fix_q. (2) لا تُخزّن نتائج فارغة في أي cache. (3) أي خدمة port-bound تحتاج single-instance guard صارم (os._exit) + keepalive يقتل-بـ-cwd قبل التشغيل. (4) curl بـ raw bytes في URL يكذب — اختبر دائماً بـ urllib.parse.quote.
🛠️ Obsidian Viewer down — السبب الجذري: fd-leak عطّل keepalive guards كاملاً (2026-06-13 ~15:50)
العَرَض: obsidian.drwaelalkishawi.com = 502، port 8908 = down، العملية غير موجودة. لم يُعَد تشغيلها تلقائياً رغم وجود guard لها.
السبب الجذري (خطير + systemic): في daemons_keepalive.sh يُفتح flock على fd 9 (exec 9>/tmp/openclaw-1000/keepalive.lock). الـ daemons طويلة العمر (inotify_nexos_guard/wrapper_guard/L1_pinger، تعمل منذ Jun 7) كانت تُطلق بـ nohup setsid ... & بدون إغلاق fd 9 → ورِثت الـ lock fd → ظلّت ممسكة بالـ flock للأبد. النتيجة: كل دورة keepalive منذ Jun 7 رأت "already running — skip" وتخطّت كل فحوص الـ guards (obsidian-viewer + أي خدمة تموت لم يُعَد تشغيلها). الـ guard موجود لكنه لم يُنفَّذ أبداً.
الدليل الحاسم: tuning log من 14:15→14:35 كله "keepalive already running — skip" + 0 محاولات restart لـ obsidian. lsof على fd 9 أظهر pid 582/585/591/594/595 (Jun 7) ممسكة بـ keepalive.lock، وأطفالها (inotifywait) ترث منها.
الإصلاح (3 طبقات، verified):
1. أضفت 9>&- (إغلاق fd الـ lock) لكل spawn مُخلفي في keepalive (perl batch على كل < /dev/null & + الـ 3 الأساسية يدوياً) → النسخ الجديدة لا ترث الـ lock أبداً.
2. force-kill (SIGKILL) لشجرة 582 الـ Jun7 leaker + كل ممسك بـ fd 9 (SIGTERM لم يكفِ — daemon يتجاهلها/يعيد التشغيل).
3. أعدت تشغيل keepalive → daemons رجعت fd-clean، اللوك حُرِّر تماماً، الفحوص تعمل كل دورة الآن (لا "skip").
النتيجة: obsidian viewer = 200 (local + HTTPS)، نسخة واحدة (pid 319720 تملك 8908)، keepalive guards فعّالة مجدداً لكل الخدمات.
درس دائم (M-candidate): أي exec N>lockfile; flock N يجب أن تُغلق fd N (N>&-) في كل child مُخلَّف، وإلا الـ daemon طويل العمر يمسك الـ lock للأبد فيُعطّل كل المنطق المحمي بالـ flock صامتاً. هذا عطّل نظام الـ keepalive guards بالكامل منذ Jun 7 دون أي إنذار. الفحص: readlink /proc//fd/9 | grep lock يكشف المتسرّبين.
🎬 Entertainment Hub — Player Jump-Bar + Per-Source Freshness Widget (2026-06-13 ~15:00 الكويت)
طلب د. وائل (صوتي) تحسينين: 1. مدة الفيديو + الانتقال لطابع زمني — بعض اشتراكاته يبثّ متقطعاً ولا يعرض المدة الكلية. الحل: شريط "اذهب إلى" تحت كل مشغّل (live/VOD/series/channels) يقبل صيغ 21:15 و 1:21:15 و 90 (دقائق) + عرض الوقت الحالي/الكلي + أزرار ⏪10ث / 30ث⏩. fallback شامل حتى عندما لا يعرض المشغّل المدة. 2. freshness في الزاوية اليسرى العليا — تحديث الفهرس SQL (24س دورياً) + EPG (3س دورياً) كلٌّ مستقل، مع وقت آخر تحديث لكل مصدر + تفصيل لكل مزوّد + تحديث manual يحترم المدة الدورية.
التنفيذ (entertainment-hub):
- catalog_search.py: أُضيف
freshness_detail()— يرجع built_at/epg_built_at epochs + subs_summary + provider_status. - app.py:
/api/catalog/statusوسّع (catalog_built_at, epg_built_at, epg_age_sec, providers[], provider_status{}, epg_rebuilding). أُضيف endpoint منفصل/api/catalog/reindex_epg+ lock مستقل.epg.lock+_epg_reindex_running()(flock، لا يصطدم مع cron EPG). - templates/index.html:
playerHtml() صار يضمّ jumpbar (jtNow/jtDur/jtInput/jt-go/skip) + دوال _fmtT/_parseT/_wireJump/jumpTo/skipBy. mountPlayer يستدعي _wireJump + يخفي input للبث المباشر.
- widget freshness غني: سطر مضغوط + popover (فهرس/EPG منفصلين بألوان حالة + per-provider counts + زرّا تحديث منفصلان) + _ago() عربي + toggle/outside-click.
- القاعدة المطبّقة: الـ cron الدوري (catalog 24س
a52c22e6, EPG 3سc0ff96fb) لم يُمَس — التحديث الـ manual on-demand فقط.
التحقق الحي (production hub.drwaelalkishawi.com):
- status endpoint: 10 مزوّدين كلهم ok، catalog/epg ages صحيحة، build timestamps موجودة.
- jumpbar: حمّل فيديو اختبار → jtDur=00:10 ✅، jumpTo('0:05')→currentTime=5 ✅، parse 21:15→1275 ✅، 1:21:15→4875 ✅.
- EPG manual refresh: spawn
indexer.py --epg→ epg_rebuilding=true (catalog rebuilding=false) → اكتمل → epg age=0.0س، الفهرس بقي 0.6س ✅ (الفصل التام يعمل). - backups: app.py/index.html/catalog_search.py .bak-jumpdur-1781355714.
- restart: قتلت hub PID 316460 صراحةً (تركت obsidian-viewer 319720) → relaunch start_hub flow → PID جديد، 200 OK.
💰 Invest Dashboard — الذهب والفضة بالجرام + إصلاح FX (2026-06-13 ~15:25 الكويت)
طلب د. وائل: لوحة الاستثمار (invest.drwaelalkishawi.com) تعرض الذهب والفضة بالجرام (سعر الجرام) بدل الأونصة.
التنفيذ (projects/domain-setup/invest/index.html):
- loadAssets(): كان يعرض الذهب بالأونصة فقط، بلا فضة. الآن: ذهب + فضة لكل جرام (USD + د.ك). الثابت
TROY_OZ_G=31.1034768(1 أونصة trf = 31.1g exact). gold-api.com/price/XAU + XAG (USD/oz) ÷ 31.1034768.
- 🐛 إصلاح جذري لـ FX (أثّر على اللوحة كلها): Frankfurter API صار يرجع 301 redirect (نقل endpoint) → "تعذّر تحميل العملات" +
_kwdundefined. أضفت_fetchFX()resilient: open.er-api.com أولاً (موثوق، فيه KWD/TRY) ثم Frankfurter backup. الآن FX يعمل + KWD/gram يظهر.
النشر + التحقق الحي:
- معماري مهم: invest container = nginx:alpine يخدم من host
/docker/invest/site/index.html(volume). تعديل workspace لا يكفي — يجب scp للـ host عبرssh -i /data/.ssh/openclaw_host_ed25519 root@76.13.41.228. (نفس النمط لكل static subdomains: code/wiki/go/status/eng/studio/obsidian = nginx proxies على host /docker/<name>/site). - scp تم → host file: 0× أونصة، 1× "سعر جرام الذهب" + 1× "سعر جرام الفضة".
- browser حي: ذهب $135.69·41.83د.ك، فضة $2.191·0.676د.ك، fxOk=true، _kwd=0.308313 ✅.
- backup: index.html.bak-gram-<ts>.
🎬 Entertainment Hub — 5 ميزات إبداعية (resume/bookmarks/speed/episode-jump/failover) (2026-06-13 ~15:30 الكويت)
طلب د. وائل: إضافة الأفكار الخمسة كلها لترسانة الترفيه، مع معايير الإضافة (تأكّد → تأكّد لا تؤثر على الأداء لأي خدمة مربوطة → نفّذ → ارجع واختبر حياً).
المعمارية المؤكّدة (مهم للمستقبل):
- hub.drwaelalkishawi.com =
hub-proxy(nginx) → proxy_pass http://172.16.1.2:8899 = Flask app داخل حاوية OpenClaw. أي تعديل app.py/index.html هنا + restart الـ Flask المحلي = نشر إنتاجي مباشر (لا scp). index.html يعيد التحميل تلقائياً (TEMPLATES_AUTO_RELOAD)، app.py يحتاج restart. - تمييز عن invest: invest = nginx static يخدم من host
/docker/invest/site/→ يحتاج scp للـ host. الـ hub = proxy للـ Flask → لا.
الميزات الخمس (كلها client-side عدا alternates endpoint):
1. استئناف المشاهدة (Resume): localStoragehub_resume (key=hash مستقر من URL). يحفظ الموضع كل 5ث (بعد 10ث تشغيل)، يعرض "توقفت عند MM:SS — استئناف/من البداية" عند >15ث، يمسح عند الانتهاء. cap 200 عنصر.
2. علامات/نقاط (Bookmarks): localStorage hub_bm. زر 🔖 احفظ نقطة + chips قابلة للضغط/الحذف.
3. سرعة العرض: select 0.5×–2× (playbackRate).
4. اذهب لحلقة (Episode jump): في modal المسلسل — إدخال موسم/حلقة → يحل ويشغّل مباشرة (match بـ episode_num ثم index، رسالة خطأ للنطاق).
5. مؤشّر صحة البث + failover عبر المزوّدين: نقطة 🟢/🟡/🔴 (playing/waiting/stall). عند انقطاع فعلي (HLS/mpegts fatal أو stall >12ث) → endpoint جديد /api/catalog/alternates (DB-only، FTS، cap 8) يجد نفس العنوان عند مزوّدين آخرين بمطابقة اسم قوية (normalized، يزيل HD/VIP/4K...) → أزرار تبديل + switchProvider() (يحل VOD/series عبر resolve hop).التنفيذ:
- app.py:
_norm_name()+/api/catalog/alternates(يعيد sub/kind/sid/name/play/resolve/series). backups .bak-features-<ts>. - templates/index.html: playerHtml وسّع (health dot + resume bar + speed + bookmark + skip60 + failbar). دوال: _mediaKey/_resumeStore/_bmStore/setMedia/setRef/resumeNow/setSpeed/addBookmark/delBookmark/gotoBookmark/renderBookmarks/_wireHealth/_setHealth/offerFailover/switchProvider/gotoEpisode. setRef مُضاف في playIptv/playHit(live+vod). CSS كامل.
التحقق الحي (production):
- node --check JS = OK، braces balanced، app.py ast OK.
- alternates: MBC1 (exclude MH new) → 6 بدائل عبر Adam vip new/K8/Lion، كلها play=True ✅.
- player حي: health dot أخضر أثناء التشغيل، duration 00:10، bookmark@00:06، speed=1.5×، resume logic (movie@1275s/3600s→offers، fmt 21:15) ✅.
- episode jump: S2E1→e21, S1E3→e3, S1E9→رسالة "غير موجودة (المتاح 1–3)" ✅.
- failover UI حي: "🩹 مزوّدون بدائل (اضغط للتبديل): Adam vip new/K8/Lion..." ✅.
- صفر تأثير على الأداء: 4 ميزات localStorage/UI بحتة، الـ5 = FTS DB-only (cap 8، auto-search مرة واحدة/جلسة). regression: search count=3 ready=True، status 10 providers سليم، crons (catalog 24h a52c22e6 + EPG 3h c0ff96fb) لم تُمَس.
- restart: استخدمت kill by cwd (/proc/<pid>/cwd match) لتجنّب pgrep-self-match (M-052 trap — pattern النسبي app.py لا يطابق full-path).
🎬 Entertainment Hub — 3 ميزات إضافية: grouping+ping / continue-watching / universal-controls (2026-06-13 ~15:55 الكويت)
طلب د. وائل (دمج فكرتي #1+#3، + #2، + تحكم شامل): 1. تجميع نتائج البحث المكررة + ترتيب بالأسرع (ping): القناة المكررة = صف واحد، كل المصادر تحته، مرتّبة بالأسرع، وكلهم موجودون. 2. أكمل المشاهدة: بدون أي تأثير على التسجيل/DVR. 3. تحكم شامل: كيبورد + لمس (Android) + ريموت TV.
التنفيذ:
- app.py: endpoint جديد
/api/catalog/ping?subs=a,b,c— يقيس زمن استجابة المزوّدين (cached ~5min، parallel ≤8، cap 16، read 64 bytes فقط) → خفيف، لا يصبح مصدر حمل. - templates/index.html:
renderGroupedSearch() + _gnorm() (يزيل HD/VIP/4K/H265/AR/RAW + العلامات الزخرفية). صف واحد لكل عنوان فريد + badge "N مصادر" + زر توسيع. toggleSources() → _orderSources() (ping → fastest-first، unreachable=null تبقى مدرجة آخراً). playGroup() يشغّل الأسرع تلقائياً، playSource() للاختيار اليدوي.
- Continue Watching: _continueList() من hub_resume (mid-watch فقط: t>15 و t<dur-20). قسم "⏯ أكمل المشاهدة" أعلى tab المفضّلة + شريط تقدّم % + إزالة. resumeWatch() يبحث+يشغّل (resume bar يظهر تلقائياً). localStorage-only، صفر كتابة backend، صفر تأثير على DVR. (resume save وُسّع ليحفظ name/sub/kind/dur للـ replay).
- Universal controls: keydown handler عام (نشط فقط والـ modal مفتوح): Space/Enter=play/pause, Arrows=±10ث/volume, F=fullscreen, M=mute, 0-9=قفز %, Escape/Back=إغلاق, Media* keys. + toast feedback. + @media(pointer:coarse) أزرار أكبر للمس/TV. التحكم = نفس مفاتيح ريموت TV (D-pad→arrows, OK→Enter) + Android touch (أزرار onscreen).التحقق الحي (production):
- ping: K8=214ms, Lion=214ms, MH new=220ms (cached call=15ms) ✅.
- grouping: MBC1 → 7 hits صار 2 عنوان فريد، top group 6 مصادر، header "2 عنوان فريد (7 مصدر إجمالاً)" ✅.
- source list: مرتّب fastest-first (K8 214ms=الأسرع، ثم Lion، ثم MH new 220ms)، كل المصادر مدرجة، ⚡ badges ✅.
- continue-watching: Oppenheimer @21:15/1:30:00 (24%)، إزالة، فوق المفضّلة ✅.
- keyboard: →+10s, ←−10s, m=mute, 5=50% — كلها صحيحة ✅.
- صفر تأثير على الأداء/DVR: search ready=True، rec/jobs+rec/scheduled+status=200، 18 DVR routes untouched، 23 dvr refs كلها pre-existing، continue-watching localStorage-only. ping bounded+cached.
- backups .bak-v3-<ts>. restart عبر kill-by-cwd (تجنّب M-052 self-match). hub PID 330169.
الميزات الكلية الآن في الترسانة (8): jumpbar+duration · per-source freshness · resume · bookmarks · speed · episode-jump · health+failover · grouping+ping · continue-watching · universal-controls.
🎬 Entertainment Hub — Ping Sort أثناء البحث (background-warmed, zero-load) + تأكيدات (2026-06-13 ~16:06 الكويت)
د. وائل أكّد: الترتيب بالـ ping يكون أثناء البحث (نتائج مرتّبة بالأفضل فوراً) لكن صفر تأثير على الأداء/الاستقرار/التقطيع. + أكّد أن resume = option (موجود)، jump-to-minute = موجود.
الحل الاحترافي (المعضلة: ping أثناء البحث ضد الأداء):
Background ping warmer — بما أن المزوّدين 10 فقط وثابتون:- app.py:
_ping_all_background()= daemon thread يقيس كل المزوّدين كل 3 دقائق (cache TTL 5min → دائماً warm). يُشغّل في_start_prewarm()(الـ instance الناجي فقط، بعد single-instance bind). + endpoint?warm=1= يقرأ الكاش فقط (لا قياس حي إطلاقاً). - النتيجة: البحث يرتّب من الكاش المُسخّن مسبقاً = فوري + صفر حمل لكل بحث. القياس يحدث out-of-band.
- index.html:
_pingSort()(cache-only، instant) يرتّب مصادر كل group عند الـ render (15ms لـ 300 hit→90 group)._warmPing()غير حاجب (non-blocking) يحدّث الكاش ويعيد الترتيب صامتاً._orderSources()يفضّل الكاش، قياس حي فقط لو لا شيء معروف. prewarm عند load (1.5s).
التحقق الحي (production) + دراسة الأداء/الاستقرار:
- warm endpoint: 10 مزوّدين (Maven=43ms أسرع، Adam vip new=259ms)، المكالمة=39ms (cache-only) ✅.
- render grouped search = 15ms لـ 300 hit→90 group، غير محجوب بالـ ping ✅.
- source list مرتّب fastest-first (xiptv 62ms→Adam gold 91ms→...)، كل المصادر مدرجة ✅.
- الاستقرار: threads ثابتة عند 3 (2 Flask + 1 warmer daemon) حتى بعد 5 مكالمات متتالية — لا نمو. FDs=4 لا تسرّب. RSS ~2.5GB ثابت (catalog.db mmap). صفر errors في log ✅.
- عدم التأثير على البث/DVR: الـ warmer يقرأ 64 bytes فقط، timeout 8s، كل 3 دقائق. regression: search/rec-jobs/rec-scheduled/status/iptv-subs كلها 200 ✅.
- backups .bak-v4-<ts>. hub PID 330991.
تأكيدات د. وائل (موجودة سلفاً، تحقّقت):
- resume = option صريح: شريط فيه زرّان "استئناف من هنا" / "من البداية" ✅.
- jump-to-minute input: خانة "اذهب إلى 21:15" في jumpbar تحت كل مشغّل ✅.
🎬 Ping Sort — "الأفضل دائماً في القمة" + failover sorted (2026-06-13 ~16:10 الكويت)
د. وائل (تنبيه): الأقل ping (الأسرع/الأفضل) دائماً في القمة، ثم التالي، في كل شيء (قنوات/أفلام/...).- تأكيد:
_pingSortascending (ma-mb، null آخراً) = دالة واحدة مستخدمة في channels/movies/series/expand/default-play/warm-resort. لا descending في أي مكان. اختبار حي: [50,90,150,300,null]→[B50,E90,C150,A300,Dnull] ✅. - إضافة (في كل شيء): failover alternates صارت ping-sorted أيضاً — اختبار حي MBC1: K8 52ms⚡→Lion 104ms→Adam vip 259ms، header "الأسرع أولاً". (كانت بترتيب الباك-إند الافتراضي). zero-load (warm cache).
- backup .bak-sortfix-<ts>. template auto-reload (لا restart). regression: search/rec-jobs/status 200، threads ثابتة.
🎬📺 Entertainment Hub APK — إصلاح تنقّل ريموت Android TV (D-pad) (2026-06-13 ~17:05 الكويت)
المشكلة (د. وائل، صوت): APK ترسانة الترفيه على Android TV — الريموت (D-pad) لا يصل لقائمة التبويبات (الأزرار)؛ up/down يقفز فقط بين "تحديث" و"البحث"، يتجاوز القائمة الأهم → التطبيق عديم الفائدة على التلفزيون.
التشخيص:
- APK (
projects/entertainment-apk/) = thin WebView wrapper يحمّلhttps://hub.drwaelalkishawi.comحياً (DEFAULT_URL، loadUrl). ليس assets مجمّعة. - السبب الجذري = web-layer focus: WebView spatial-nav الافتراضي يفشل مع RTL + tabs أفقية scroll + grid، فلا يصل للتبويبات/المحتوى.
الإصلاح (web-layer = يصل للـ APK المثبّت فوراً):
- index.html: محرّك TV spatial-navigation كامل (self-contained قبل
</script>): D-pad arrows تحرّك focus للأقرب هندسياً (nearest-neighbor، cross-axis weighted ×2)، tabs↔search↔content↔player-controls. Enter/OK = click. يتنحّى تلقائياً حين يكون مشغّل الفيديو مفتوحاً (player handler له الأولوية). MutationObserver للمحتوى المتغيّر. focus ring CSS واضح (أصفر للتبويبات/البطاقات، scale 1.04 للبطاقة). - APK MainActivity.java (لـ rebuild مستقبلي):
injectTvCssمحسّن — على TV يضع focus على أول tab بعد 600ms + scroll-margin للعناصر. (تغيير اختياري؛ الإصلاح الفعلي web).
التحقق الحي (production hub.drwaelalkishawi.com):
- ArrowDown→أول tab (📡 بحث قنوات) لا search/refresh ✅. ArrowRight يتنقّل التبويبات ✅. ArrowDown→ينزل للبطاقات (CUKESIM...) ✅.
- Enter على "⭐ المفضّلة" → cur=fav + المحتوى تحمّل ✅.
- لا تعارض مع المشغّل: ArrowRight داخل الفيديو يقدّم (2→10s)، TV-nav يتنحّى ✅.
- backup index.html.bak-tvnav-<ts>.
النتيجة المهمة:
الإصلاح live في الـ APK المثبّت فوراً (WebView يحمّل URL الحي) — د. وائل يكفيه إعادة فتح التطبيق على التلفزيون، بلا إعادة تثبيت. (الـ video الكبير >20MB لم يصل أصلاً — قيد Telegram Bot API؛ منفصل عن هذه المشكلة).📺 TV Nav — Universal cross-browser/cross-device hardening (2026-06-13 ~17:10 الكويت)
د. وائل: الحل الجذري من صفحة الويب (لا APK جديد) ممتاز — لكن تأكّد يعمل على كل المتصفّحات وكل الأجهزة (mouse+touch+keyboard+TV remote)، وأي تطوير مستقبلي يظهر تلقائياً. "هذا هو الذكاء."- التقوية: أعدت كتابة محرّك TV-nav + player handler ليفكّا المفاتيح من e.key الحديث AND keyCode القديم (Tizen/webOS/Android-TV القديم/Chromium قديم يرسلون keyCode فقط). دعم: arrows 37-40، OK = Enter(13)/DPAD_CENTER(23)/Space(32)، TV BACK = Escape(27)/461 webOS/10009 Tizen، أرقام 48-57 + numpad 96-105. + pointerdown parity للماوس. + try/catch حول focus للمتصفّحات القديمة.
- التحقق الحي (production، محاكاة بيئات): legacy keyCode-only (Down→tab→content) ✅ · modern e.key ✅ · DPAD_CENTER 23 يفعّل التبويب (cur=fav) ✅ · داخل المشغّل: legacy ArrowRight يقدّم، 23 play/pause، Tizen BACK 10009 يغلق ✅ · mouse click يبدّل التبويب بلا تعارض ✅ · pointerdown يجعل العنصر focusable ✅.
- النتيجة: حل ويب واحد = كل البيئات (mouse/touch/keyboard/remote، كل المتصفّحات/الأجهزة)، live فوراً في APK المثبّت (WebView)، وأي تحديث مستقبلي يظهر تلقائياً بلا APK. backup .bak-tvnav2-<ts>. regression: hub 200 + single instance + threads ثابتة.
📺 TV Nav fix #2 — OK/Enter على البطاقة لم يفتح القناة (2026-06-13 ~17:18 الكويت)
المشكلة (د. وائل + صورة تثبت التنقّل يعمل): اختيار قناة + OK/Enter بالريموت لا يفتحها على TV.- الجذر: في grouped search و
_cardHtml، الـonclickعلى div داخلي وليس على.cardنفسها. الـ engine يركّز.cardويستدعي.click()عليها → لا handler عليها مباشرة → لا شيء يحدث. - الإصلاح:
activate()ذكّي — لو العنصر المركّز بلا handler مباشر (onclick/A/BUTTON)، يبحث عن أول[onclick]descendant ويضغطه (= div الـ playGroup/playHit، أول عنصر في DOM order قبل toggle/fav). - تحقق حي: focus card + Enter → فتح المشغّل "📺 MBC DRAMA 4K" ✅. legacy DPAD_CENTER 23 على card → نفس الفتح ✅.
- backup .bak-okfix-<ts>. template auto-reload.
📺 TV Nav fix #3 — السبب الحقيقي: WebView caching HTML قديم (2026-06-13 ~17:21 الكويت)
المشكلة: OK لا يفتح الأفلام/المسلسلات على TV — رغم أن الإصلاح يعمل في متصفّحي الطازج.- الجذر الحقيقي: الـ HTML shell كان بلا cache-control headers → WebView (LOAD_DEFAULT) يكاش نسخة index.html قديمة (قبل إصلاحات TV-nav/OK) → التلفزيون يشغّل كود قديم. لهذا "يعمل عندي ويفشل عنده".
- الإصلاح (app.py after_request): HTML (
/+.html) →Cache-Control: no-cache, no-store, must-revalidate+ Pragma + Expires:0. الـ API search يبقى max-age=60 (الأداء محفوظ). الآن كل فتح للتطبيق = أحدث UI تلقائياً (جوهر تصميم thin-WebView: إصلاح ويب واحد → حي في كل مكان). - تحقق حي: prod HTML headers = no-cache ✅، search API = max-age=60 ✅. OK على فيلم Oppenheimer → فتح المشغّل ✅. OK على مسلسل Grendizer → فتح الحلقات ✅.
- restart hub PID 338066. backup .bak-nocache-<ts>. regression 200 + threads ثابتة + single instance.
- ملاحظة للمستخدم: التلفزيون يحتاج إعادة فتح/refresh مرة واحدة ليلتقط الـ headers الجديدة، وبعدها دائماً محدّث تلقائياً.
📺 TV fixes #4+#5 — Source Picker + Back-navigation stack (2026-06-13 ~17:25 الكويت)
د. وائل (صوت): (1) بطاقة لها 4-5 مصادر تفتح مصدراً واحداً فقط — أريد رؤية كل المصادر مرتّبة بالـ ping، كل واحد بجانبه زمنه، احترافي. (2) زر Back بالريموت يخرج من التطبيق ("قمة البلاهة") — يجب أن يرجع خطوة (مشغّل→نتائج)، ولا يخرج إلا بسؤال تأكيد.Fix #4 — Source Picker:
playGroup: single source → play مباشر. multi →openSourcePicker= modal يعرض كل المصادر ping-sorted (رقم + اسم المزوّد + ms + ★الأسرع)، يشغّل الأسرع inline تلقائياً (pickPlayer)، والقائمة تبقى للتبديل بضغطة.pickSourceيحل VOD/series عبر resolve hop + mountPlayer inline.- تحقق حي: MBC1 → 6 مصادر (K8 63ms★ → Lion 99ms → MH new 106ms)، auto-play الأسرع، الأول highlighted ✅.
Fix #5 — Back stack (history.pushState/popstate):
navStack+ seed history entry. popstate يقرّر "خطوة واحدة": (1) exitDlg مفتوح→يغلقه (2) modal/player مفتوح→closeM يعود للنتائج (3) داخل tab غير home→switchTab('library:movies') (4) home→_confirmExitdialog (لا/نعم).- أصلحت bug كامن:
switchTab()كان مُستدعى (سطر ~230) وغير معرّف أبداً → عرّفته. - تحقق حي: Back من picker → عاد لنتائج «MBC1» (لم يخرج) ✅. Back ثانية في home → exit dialog ظهر ✅. "لا البقاء" يلغي ويبقى ✅.
- backup .bak-picker-back-<ts>. (لا app.py change — template auto-reload؛ HTML no-cache من fix#3 يضمن وصوله للـ TV).
- regression: hub 200 + threads ثابتة.
🔼 Ask RAG embedding upgrade: gemini-embedding-001 → gemini-embedding-2 (2026-06-14 ~00:30 الكويت)
الطلب: د. وائل وافق على ترقية محرّك التضمين للـ4 أنظمة (Ask RAG أولاً كـ A/B)، بشرط: أفضل نتائج + أداء + سلاسة + استقرار + عدم توقف + أمان، صفر سلبيات، ثم توصية.
التحقق الحي قبل أي شيء:
gemini-embedding-2حقيقي وحيّ (+-2-preview). أبعاد = 3072 = نفس 001.- cosine(نفس النص تحت 001 vs 2) = 0.04 → فضاءان متعامدان → الخلط ممنوع، يجب reindex كامل atomic (لا جزئي).
knowledge.lance كان مملوكاً root بصلاحية 600 (من reindex يدوي شُغّل كـ root في 10 يونيو) → خدمة node لا تقرأه → /api/semantic يفشل صامتاً (Permission denied، os error 13) → grep-only منذ 3 أيام. الترقية أصلحته (إعادة بناء node-owned).A/B (نسخة موازية، لم تُلمس الحيّة):
- عيّنة 160 doc: ظهرت سلبية وهمية (avg_rel@5 −6.7) — artifact من العيّنة الصغيرة.
- الكامل 950 doc (الحاسم): embedding-2 ≥ 001 في كل مقياس، صفر تراجع:
التطبيق (atomic، بحرفية):
semantic_index.py+app.py→EMBED_MODEL = "gemini-embedding-2"(السطر موثّق بنتيجة الـA/B).- swap ذرّي: live lancedb → 950 row embedding-2، كله node-owned (0 ملف root).
- restart عبر setsid (pid 366820 على :8902). keepalive (node، §114-117) يحرسه بـ
9>&-+/usr/bin/python3→ الكود الجديد يُعاد تشغيله تلقائياً لو مات. - refresh الدوري (keepalive §404، كل 24h، node) يبني embedding-2 + node-owned → العطل لا يتكرر.
Rollback: projects/ask-rag/lancedb.old-001-1781389250 (11MB، 001 backup) محفوظ.
الباقي (لم يُطبّق بعد — بانتظار قرار د. وائل لكل واحد): Legal RAG (index.json فيه 0 entries — يحتاج بناء محتوى أولاً + فكرة voyage-law-2) · RAG Knowledge Base (Voyage-3 primary) · OpenClaw Memory Index (lancedb plugin، معطّل حالياً برسالة metadata mismatch — مشكلة منفصلة).