# Fchaty - مراجعة شاملة ودليل النشر والصيانة ## 1. البنية الحالية للمشروع المشروع مؤلف من قسمين رئيسيين: ### 1.1 السيرفر (relay-server/) سيرفر Node.js يعمل كـ relay بين المستخدمين. لا يخزّن الرسائل بشكل دائم (فقط كطابور مؤقت للرسائل أثناء انقطاع الاتصال). **الملفات:** - `src/index.js` — كل كود السيرفر (Express + WebSocket) - `package.json` — التبعيات (express, ws, multer) - `Dockerfile` — بناء Docker image - `docker-compose.yml` — تشغيل مع Traefik كـ reverse proxy - `.env` — متغيرات البيئة (غير موجود بالـ git، لازم تنشئه يدوي) **كيف يشتغل السيرفر:** 1. مستخدم A ينشئ كود اقتران عبر `POST /pairing/create` → يحصل على token + code 2. مستخدم B يدخل الكود عبر `POST /pairing/join` → يحصل على token 3. كلاهما يتصلون بالـ WebSocket على `wss://fchaty.diyaa.de/ws` 4. يرسلون `{type:"auth", token:"..."}` للمصادقة 5. الرسائل تمر عبر السيرفر من peer لـ peer 6. إذا الطرف الآخر offline، الرسالة تتخزّن بالـ queue على الدسك **البيانات المخزنة على السيرفر:** - `/data/files/` — الملفات المرفوعة (تنظّف تلقائياً بعد 30 يوم) - `/data/queue/` — الرسائل المعلقة (تنتهي بعد 7 أيام) - `/data/pairing/` — جلسات الاقتران (الأزواج الدائمة تبقى للأبد) ### 1.2 تطبيق macOS (FchatyApp/) تطبيق Swift/SwiftUI يعمل على macOS 14+. **الملفات الأساسية:** - `Sources/FchatyApp/FchatyApp.swift` — نقطة الدخول + التابات (Chat, Pairing, Settings) - `Sources/FchatyApp/Session/AppSession.swift` — إدارة الجلسة والاتصال - `Sources/FchatyApp/Networking/WSClient.swift` — اتصال WebSocket - `Sources/FchatyApp/Networking/RelayAPI.swift` — HTTP API calls - `Sources/FchatyApp/Storage/MessageStore.swift` — تخزين الرسائل محلياً (JSON file) - `Sources/FchatyApp/Storage/KeychainStore.swift` — تخزين tokens بالـ Keychain - `Sources/FchatyApp/Features/Chat/ChatView.swift` — واجهة المحادثة - `Sources/FchatyApp/Features/Chat/ChatViewModel.swift` — منطق إرسال الرسائل - `Sources/FchatyApp/Features/Pairing/PairingView.swift` — واجهة الاقتران - `Sources/FchatyApp/Features/Pairing/PairingViewModel.swift` — منطق الاقتران - `Sources/FchatyApp/Features/Settings/SettingsView.swift` — الإعدادات - `Sources/FchatyApp/Notifications/NotificationManager.swift` — الإشعارات **كيف يعمل التطبيق:** 1. عند الفتح، يحاول يسترجع الجلسة القديمة من الـ Keychain 2. إذا في token محفوظ، يتصل بالـ WebSocket مباشرة 3. الرسائل تتحفظ محلياً بملف `~/Library/Application Support/de.diyaa.fchaty/messages.json` 4. الـ Keychain يحفظ: authToken, peerID, peerName, installationID --- ## 2. نقاط الضعف والمشاكل المكتشفة ### مشكلة 1: المحادثات القديمة تظهر أحياناً (خطيرة) **السبب:** ملف `messages.json` يحفظ كل الرسائل بدون ربطها بجلسة معينة. لما تعمل unpair ثم pair جديد، الرسائل القديمة تبقى بالملف. التطبيق يحملها كلها عند الفتح بدون تصفية. **الملف:** `AppSession.swift` سطر 146-151 — `loadStoredMessages()` يحمّل كل الرسائل بدون فلتر. **الملف:** `AppSession.swift` سطر 115-124 — `unpair()` ما بيحذف الرسائل المحفوظة! **الحل المطلوب:** عند `unpair()`، يجب حذف ملف `messages.json` أو إضافة `sessionID` لكل رسالة لتصفيتها. ### مشكلة 2: الاقتران (Pairing) يضل loading (خطيرة) **الأسباب المحتملة:** **أ) WebSocket reconnect محدود — بعد 5 محاولات بيوقف:** `WSClient.swift` سطر 244 — `let delays: [UInt64] = [2, 4, 8, 16, 32]` بعد ~62 ثانية من المحاولات، بيوقف نهائياً بدون ما يبلّغ المستخدم! **ب) ما في مؤشر للمستخدم إنو الاتصال فشل نهائياً:** الـ `connectionDidFail` بيحاول يعمل reconnect بس ما بيرجع error للـ UI إذا الـ reconnect فشل. **ج) Auth timeout 10 ثواني:** إذا السيرفر بطيء أو في مشكلة بالشبكة، الاتصال بينقطع بعد 10 ثواني. **د) الـ Creator بيضل "Waiting for the other person..." حتى لو انقطع الـ WebSocket:** `PairingView.swift` — مافي آلية لكشف إنو الاتصال فقد وإعادة عرض حالة الخطأ. ### مشكلة 3: fileRegistry بالذاكرة فقط (متوسطة) **السبب:** `index.js` سطر 42 — `const fileRegistry = new Map()` بعد إعادة تشغيل السيرفر، كل الملفات المرفوعة تصبح غير قابلة للتنزيل (404) رغم إنها لسا موجودة على الدسك. **الحل المطلوب:** تحميل metadata الملفات من الدسك عند بدء السيرفر (مشابه لـ `loadQueueFromDisk`). ### مشكلة 4: ملف .env غير موجود (متوسطة) ملف `.env` مطلوب لتشغيل Docker compose لكنه مستبعد من git. لما تعمل deploy جديد لازم تنشئه يدوي. ### مشكلة 5: أداء MessageStore (منخفضة حالياً) كل عملية save تحمّل كل الرسائل من الدسك، تضيف الرسالة الجديدة، وتكتب الملف كامل من جديد. مع تراكم الرسائل، الأداء رح يسوء. ### مشكلة 6: لا يوجد آلية لمعرفة حالة الطرف الآخر (online/offline) (منخفضة) التطبيق ما بيعرض إذا الشخص الآخر متصل أو لا، إلا لحظة الاتصال الأولى. ### مشكلة 7: الـ installationID vs peerID (مربكة) المرسل يُعرّف بـ `KeychainStore.installationID` (UUID ثابت محلي) لكن السيرفر يستخدم `peerID` (يتغير مع كل pairing جديد). هذا يعني أن `message.from == KeychainStore.installationID` لن يتطابق أبداً مع الرسائل المرسلة لأن السيرفر يعيد كتابة `from` بالـ `peerID`. لكن لأن الرسائل الصادرة تُحفظ محلياً قبل ما تمر بالسيرفر، التطابق يعمل فقط للرسائل المحلية. **ملاحظة مهمة:** هذا يعني إنو إذا الرسالة جاية من الـ queue (رسائل offline)، قد تظهر كرسالة واردة حتى لو أنت المرسل — لأن `from` فيها هو `peerID` مش `installationID`. --- ## 3. سيناريو الديبلوي الكامل (خطوة بخطوة لشخص مبتدئ) ### 3.1 متطلبات السيرفر - سيرفر Linux مع Docker و Docker Compose - Traefik كـ reverse proxy (مع شبكة `traefik-net` جاهزة) - DNS: نطاق `fchaty.diyaa.de` يشير للسيرفر ### 3.2 خطوات نشر السيرفر ```bash # 1. نسخ الملفات للسيرفر scp -r relay-server/ user@server:/path/to/fchaty/ # 2. الدخول للسيرفر ssh user@server cd /path/to/fchaty/relay-server # 3. إنشاء ملف .env cat > .env << 'EOF' PORT=3000 MAX_FILE_SIZE_MB=25 FILE_TTL_DAYS=30 ADMIN_TOKEN=your-secret-admin-token-here TRAEFIK_CERT_RESOLVER=myresolver EOF # 4. بناء وتشغيل docker compose build docker compose up -d # 5. التأكد من التشغيل docker compose logs -f # لازم تشوف: "Fchaty Relay listening on port 3000" # 6. فحص الصحة curl https://fchaty.diyaa.de/health # لازم يرجع: {"ok":true,"connections":0,"queued":0} ``` ### 3.3 تحديث السيرفر بعد تعديلات على الكود ```bash # 1. نقل الملفات المعدلة scp relay-server/src/index.js user@server:/path/to/fchaty/relay-server/src/ # 2. على السيرفر ssh user@server cd /path/to/fchaty/relay-server # 3. إعادة بناء وتشغيل docker compose build docker compose up -d # ملاحظة مهمة: إعادة التشغيل تقطع كل اتصالات WebSocket الحالية. # التطبيقات رح تحاول تعيد الاتصال تلقائياً (5 محاولات). # الرسائل المعلقة والـ pairing sessions تنحفظ على الدسك ولا تضيع. # لكن fileRegistry (الملفات المرفوعة) رح تضيع من الذاكرة! ``` ### 3.4 بناء تطبيق macOS ```bash # على جهاز Mac (يتطلب Xcode Command Line Tools) cd /path/to/f-chaty-native-new # تشغيل السكربت الجاهز (يبني + يثبت + يفتح) chmod +x scripts/run-macos-app.sh ./scripts/run-macos-app.sh # هذا السكربت: # 1. يحذف build artifacts القديمة # 2. يشغل الـ tests # 3. يبني release executable # 4. ينشئ .app bundle مع الأيقونة والـ entitlements # 5. يوقّع التطبيق (ad-hoc signing) # 6. يثبته بـ /Applications/Fchaty.app # 7. يفتحه ``` --- ## 4. هل لازم نعمل بلد بعد كل تعديل؟ ### تعديلات على السيرفر (relay-server/src/index.js): **نعم، لازم نعمل rebuild + restart للـ Docker container.** ```bash docker compose build && docker compose up -d ``` السبب: السيرفر يشتغل داخل Docker container، والكود يُنسخ أثناء الـ build. أي تعديل على `index.js` ما بيتأثر إلا بعد إعادة البناء. ### تعديلات على تطبيق macOS (FchatyApp/): **نعم، لازم نعمل build جديد.** ```bash ./scripts/run-macos-app.sh ``` السبب: التطبيق Swift compiled، أي تعديل بالكود يحتاج إعادة compile. النسخة القديمة لازم تُستبدل. ### تعديلات على الـ .env فقط: **لا حاجة لـ rebuild، فقط restart:** ```bash docker compose down && docker compose up -d ``` ### ملخص: | نوع التعديل | لازم build جديد؟ | الأمر | |---|---|---| | كود السيرفر (index.js) | نعم | `docker compose build && docker compose up -d` | | تطبيق macOS (Swift) | نعم | `./scripts/run-macos-app.sh` | | .env فقط | لا، بس restart | `docker compose down && docker compose up -d` | | docker-compose.yml | لا، بس restart | `docker compose down && docker compose up -d` | --- ## 5. قائمة المهام المطلوبة لاستقرار التطبيق ### أولوية عالية (لازم تنعمل فوراً): - [ ] **إصلاح مشكلة المحادثات القديمة:** حذف `messages.json` عند `unpair()` في `AppSession.swift` - [ ] **إصلاح مشكلة الاتصال اللامحدود:** جعل WebSocket reconnect لانهائي (مع backoff cap عند 60 ثانية) بدل 5 محاولات فقط، وإبلاغ الـ UI عند فقدان الاتصال - [ ] **إصلاح fileRegistry:** تحميل metadata الملفات من الدسك عند بدء السيرفر بدل الاعتماد على الذاكرة فقط - [ ] **إنشاء ملف `.env.example`:** لتسهيل الديبلوي على أي شخص ### أولوية متوسطة: - [ ] **إضافة مؤشر حالة الاتصال بالـ UI:** يظهر بوضوح إذا كان الاتصال مفقود أو يحاول إعادة الاتصال - [ ] **إضافة زر retry يدوي:** لما الاتصال يفشل نهائياً، المستخدم يقدر يحاول يدوياً - [ ] **تحسين أداء MessageStore:** استخدام SQLite بدل JSON file - [ ] **إضافة حد أقصى للرسائل المحلية:** لتجنب استهلاك الذاكرة ### أولوية منخفضة: - [ ] **إضافة مؤشر online/offline للطرف الآخر** - [ ] **إضافة typing indicator** - [ ] **تشفير الرسائل end-to-end** - [ ] **إضافة logging أفضل للسيرفر** - [ ] **إضافة health check أكثر تفصيلاً** --- ## 6. سكربت إنشاء نسخة to-moh السكربت `scripts/build-to-moh.sh` ينشئ نسخة من التطبيق باسم `to-moh` على سطح المكتب. انظر الملف المنفصل: `scripts/build-to-moh.sh`