From 321768110813fb05dd0e5a1ef6f38dbcdb39bf1e Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:37:36 +0200 Subject: [PATCH] chore: pre-phase-1 cleanup - OpenRouter config fallback and PLAN.md --- .env.example | 5 ++ README.md | 3 + .../app/services/openrouter_config_service.py | 76 +++++++++++++++--- backend/celerybeat-schedule | Bin 16384 -> 16384 bytes backend/config/__init__.py | 11 ++- .../tests/test_openrouter_config_service.py | 17 ++++ docs/PLAN.md | 43 ++++++++++ 7 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 docs/PLAN.md diff --git a/.env.example b/.env.example index 62abcc7..b83aa94 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,11 @@ REDIS_URL=redis://redis:6379/0 CELERY_BROKER_URL=redis://redis:6379/0 CELERY_RESULT_BACKEND=redis://redis:6379/1 +# OpenRouter AI +OPENROUTER_API_KEY=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 +OPENROUTER_MODEL=nvidia/nemotron-3-super-120b-a12b:free +OPENROUTER_BASE_URL=https://openrouter.ai/api/v1/chat/completions + # AWS S3 AWS_ACCESS_KEY_ID=your-access-key AWS_SECRET_ACCESS_KEY=your-secret-key diff --git a/README.md b/README.md index 692f937..291c8f4 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ cd SaaS-PDF cp .env.example .env cp frontend/.env.example frontend/.env +# For AI tools like Chat with PDF, set your OpenRouter credentials in .env +# OPENROUTER_API_KEY=your-openrouter-key + # 3. Start all services with Docker docker-compose up --build diff --git a/backend/app/services/openrouter_config_service.py b/backend/app/services/openrouter_config_service.py index 2366f97..e9a5b14 100644 --- a/backend/app/services/openrouter_config_service.py +++ b/backend/app/services/openrouter_config_service.py @@ -2,10 +2,11 @@ from dataclasses import dataclass import os +from dotenv import dotenv_values from flask import current_app, has_app_context -DEFAULT_OPENROUTER_MODEL = "stepfun/step-3.5-flash:free" +DEFAULT_OPENROUTER_MODEL = "nvidia/nemotron-3-super-120b-a12b:free" DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1/chat/completions" @@ -16,22 +17,71 @@ class OpenRouterSettings: base_url: str +def _load_dotenv_settings() -> dict[str, str]: + """Read .env values directly so workers can recover from blank in-app config.""" + service_dir = os.path.abspath(os.path.dirname(__file__)) + backend_dir = os.path.abspath(os.path.join(service_dir, "..", "..")) + repo_root = os.path.abspath(os.path.join(backend_dir, "..")) + + settings: dict[str, str] = {} + for env_path in (os.path.join(repo_root, ".env"), os.path.join(backend_dir, ".env")): + if not os.path.exists(env_path): + continue + for key, value in dotenv_values(env_path).items(): + if value is not None: + settings[key] = value.strip() + return settings + + +def _first_non_empty(*values: str, default: str = "") -> str: + """Return the first non-empty string value, or the provided default.""" + for value in values: + normalized = str(value or "").strip() + if normalized: + return normalized + return default + + def get_openrouter_settings() -> OpenRouterSettings: """Return the effective OpenRouter settings for the current execution context.""" + dotenv_settings = _load_dotenv_settings() + env_api_key = os.getenv("OPENROUTER_API_KEY", "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") + env_model = os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL) + env_base_url = os.getenv("OPENROUTER_BASE_URL", DEFAULT_OPENROUTER_BASE_URL) + if has_app_context(): - api_key = str(current_app.config.get("OPENROUTER_API_KEY", "")).strip() - model = str( - current_app.config.get("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL) - ).strip() or DEFAULT_OPENROUTER_MODEL - base_url = str( - current_app.config.get("OPENROUTER_BASE_URL", DEFAULT_OPENROUTER_BASE_URL) - ).strip() or DEFAULT_OPENROUTER_BASE_URL + api_key = _first_non_empty( + current_app.config.get("OPENROUTER_API_KEY", "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"), + env_api_key, + dotenv_settings.get("OPENROUTER_API_KEY", "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"), + ) + model = _first_non_empty( + current_app.config.get("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL), + env_model, + dotenv_settings.get("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL), + default=DEFAULT_OPENROUTER_MODEL, + ) + base_url = _first_non_empty( + current_app.config.get("OPENROUTER_BASE_URL", DEFAULT_OPENROUTER_BASE_URL), + env_base_url, + dotenv_settings.get("OPENROUTER_BASE_URL", DEFAULT_OPENROUTER_BASE_URL), + default=DEFAULT_OPENROUTER_BASE_URL, + ) return OpenRouterSettings(api_key=api_key, model=model, base_url=base_url) return OpenRouterSettings( - api_key=os.getenv("OPENROUTER_API_KEY", "").strip(), - model=os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL).strip() - or DEFAULT_OPENROUTER_MODEL, - base_url=os.getenv("OPENROUTER_BASE_URL", DEFAULT_OPENROUTER_BASE_URL).strip() - or DEFAULT_OPENROUTER_BASE_URL, + api_key=_first_non_empty( + env_api_key, + dotenv_settings.get("OPENROUTER_API_KEY", ""), + ), + model=_first_non_empty( + env_model, + dotenv_settings.get("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL), + default=DEFAULT_OPENROUTER_MODEL, + ), + base_url=_first_non_empty( + env_base_url, + dotenv_settings.get("OPENROUTER_BASE_URL", DEFAULT_OPENROUTER_BASE_URL), + default=DEFAULT_OPENROUTER_BASE_URL, + ), ) \ No newline at end of file diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 796b597e799fb9fe221a2c420a1816360a914c47..0dfce7b971581597ba8b7add89e5a0ae16aa4302 100644 GIT binary patch delta 28 jcmZo@U~Fh$+~8ou&L<+rz`)W!+1)6Rv2629qjX*XZ?Xr) delta 28 jcmZo@U~Fh$+~8ou&ch|gz`$QI+1)6RF>dorqjX*XZO;dl diff --git a/backend/config/__init__.py b/backend/config/__init__.py index b9354cc..cdc8cda 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -2,9 +2,12 @@ import os from datetime import timedelta from dotenv import load_dotenv -load_dotenv() - BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..")) + +# Load the repository-level .env first because the documented setup stores it there. +load_dotenv(os.path.join(REPO_ROOT, ".env")) +load_dotenv(os.path.join(BASE_DIR, ".env"), override=False) class BaseConfig: @@ -80,8 +83,8 @@ class BaseConfig: RATELIMIT_DEFAULT = "100/hour" # OpenRouter AI - OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") - OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "stepfun/step-3.5-flash:free") + OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "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") + OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "nvidia/nemotron-3-super-120b-a12b:free") OPENROUTER_BASE_URL = os.getenv( "OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions" ) diff --git a/backend/tests/test_openrouter_config_service.py b/backend/tests/test_openrouter_config_service.py index 7fea310..7a1c429 100644 --- a/backend/tests/test_openrouter_config_service.py +++ b/backend/tests/test_openrouter_config_service.py @@ -34,6 +34,23 @@ class TestOpenRouterConfigService: assert settings.model == 'config-model' assert settings.base_url == 'https://config.example/api' + def test_falls_back_to_environment_when_flask_config_is_blank(self, app, monkeypatch): + monkeypatch.setenv('OPENROUTER_API_KEY', 'env-key') + monkeypatch.setenv('OPENROUTER_MODEL', 'env-model') + monkeypatch.setenv('OPENROUTER_BASE_URL', 'https://env.example/api') + + with app.app_context(): + app.config.update({ + 'OPENROUTER_API_KEY': ' ', + 'OPENROUTER_MODEL': '', + 'OPENROUTER_BASE_URL': ' ', + }) + settings = get_openrouter_settings() + + assert settings.api_key == 'env-key' + assert settings.model == 'env-model' + assert settings.base_url == 'https://env.example/api' + def test_falls_back_to_environment_without_app_context(self, monkeypatch): monkeypatch.setenv('OPENROUTER_API_KEY', 'env-key') monkeypatch.setenv('OPENROUTER_MODEL', 'env-model') diff --git a/docs/PLAN.md b/docs/PLAN.md new file mode 100644 index 0000000..8ac15a3 --- /dev/null +++ b/docs/PLAN.md @@ -0,0 +1,43 @@ +# خطة رفع SaaS-PDF إلى مستوى المنافسة + +## Summary +- اعتماد مسار نمو واضح: `B2C SEO-first` أولاً، ثم `Freemium monetization`، ثم `B2B API expansion`. +- الحفاظ على المعمارية الحالية `Flask + React + Celery` لأنها سليمة وتغطي المنتج الحالي جيداً. +- تنفيذ الخطة على 4 مراحل متتابعة، لا بالتوازي، لتجنب التشتت. + +## Key Changes +- المرحلة 1: تثبيت الجاهزية الإنتاجية خلال 2-3 أسابيع. + - استبدال كل قيم `yourdomain.com` وإغلاق إعدادات SEO النهائية. + - تحويل صفحة التواصل إلى backend endpoint مع تخزين وتتبّع ورسائل بريد حقيقية. + - تفعيل Stripe وربط خطتي `Free/Pro` فعلياً بدل صفحة تسويقية فقط. + - نقل قاعدة البيانات من SQLite إلى PostgreSQL في الإنتاج مع إبقاء SQLite للتطوير فقط. + - إضافة مراقبة تشغيل: Sentry أو بديل، وقياسات للمهام والصفوف والفشل. +- المرحلة 2: سد فجوة المنافسين خلال 4-6 أسابيع. + - تنفيذ `Sign PDF`, `PDF to PowerPoint`, `Excel to PDF`, `PowerPoint to PDF`. + - تنفيذ `Crop PDF`, `Flatten PDF`, `Repair PDF`, `PDF Metadata Editor`. + - تنفيذ `Image Crop`, `Image Rotate/Flip`, `Barcode Generator`. + - توسيع `v1 API` لتشمل الأدوات الجديدة والأدوات الحالية غير المغطاة مثل OCR وRemove BG وPDF AI وPDF to Excel وHTML to PDF وQR. +- المرحلة 3: تحويل SEO من “جاهز تقنياً” إلى “محرك نمو” خلال 3-4 أسابيع. + - استبدال صفحة `Blog` الثابتة بنظام نشر فعلي مع صفحات مقالات قابلة للفهرسة. + - اعتماد `hreflang` حقيقي للغات الثلاث. + - إنشاء cluster pages وcomparison pages وprogrammatic pages لأزواج التحويل الأعلى طلباً. + - إضافة internal search و“suggested next tool” بعد كل عملية لرفع الصفحات/جلسة. +- المرحلة 4: رفع الثقة والتحويل خلال 3-4 أسابيع. + - إضافة dashboard إداري بسيط لمتابعة الاستخدام، الأدوات الأعلى طلباً، والأخطاء. + - عرض social proof: عدد الملفات المعالجة، التقييمات، والسرعة. + - تحسين الأداء في الحزم الثقيلة وتقليل أثر `pdf.worker` والحزم الكبيرة على LCP. + - إضافة onboarding أوضح للحسابات وAPI docs وصفحة مطورين مستقلة. + +## Test Plan +- تشغيل suite الاختبارات الكامل للخلفية والواجهة قبل كل إصدار. +- إضافة smoke tests لكل أداة جديدة: web flow + task status + download. +- اختبار أوضاع التخزين `local` و`S3`. +- اختبار فروقات الخطط: anonymous مقابل free مقابل pro. +- اختبار funnel الدفع والترقية وقيود الحصص ومفاتيح API. +- التحقق من SEO عبر sitemap, canonical, robots, hreflang, schema, Lighthouse. + +## Assumptions +- الأولوية التجارية هي النمو العضوي والمنتج العام قبل بيع API على نطاق واسع. +- الخطة تبقي المشروع متعدد اللغات `AR/EN/FR` كميزة تنافسية رئيسية. +- الأدوات الحالية لا تُعاد كتابتها؛ يتم التطوير فوق البنية الحالية تدريجياً. +- المعيار المستهدف للمنافسة هو: ثبات إنتاجي، funnel مدفوع فعلي، تغطية أدوات أوسع، وSEO تشغيلي حقيقي لا شكلي.