Manuel d'instructions de cette app. À LIRE avant toute intervention, et à METTRE À JOUR après chaque modification (ajouter une ligne au CHANGELOG en bas).
Dernière génération/MAJ : 2026-08-03 · Statut : en ligne, en attente des comptes externes (Resend + Meta)
/root/workspace/apps/openreplyopenreply-web.service → next start -p 8351 (dashboard + webhook + OAuth callback). Port 8351 (127.0.0.1).openreply-worker.service → tsx worker/dm-worker.ts (consomme la queue d'envoi de DM + reconciler de polling des commentaires). Sans le worker, aucun DM ne part même si les webhooks arrivent.--restart unless-stopped) :openreply-pg : postgres:16, base openreply, user/pass postgres/postgres, port 5432, volume openreply_pgdata.openreply-redis : redis:7-alpine, port 6379, volume openreply_redisdata./etc/nginx/sites-enabled/openreply.panelbay.com.conf → 127.0.0.1:8351, HTTPS Let's Encrypt (exposé via broker DNS + skill expose-webapp).EnvironmentFile=/root/workspace/apps/openreply/.env (les deux services). Le worker NE charge PAS .env seul, d'où l'EnvironmentFile systemd.sudo systemctl restart openreply-web openreply-worker. Après un git pull / changement de code : npm install (si deps) puis npm run build puis restart web ; worker = restart (tsx lit le TS直). Après changement de .env : restart des 2 services.app/ (routes Next 16, dont app/api/health, app/api/webhook, app/api/instagram/callback), worker/dm-worker.ts (worker), lib/, prisma/schema.prisma + prisma/migrations/, .env (secrets, chmod 600, gitignored).docs/setup.md = la doc d'install officielle (Meta pas-à-pas). META_APP_REVIEW.md = justif si un jour ouverture à des tiers.WebhookEvent (réception webhook), DmLog (statut/erreur d'envoi), OperationalEvent (crashs worker + sweeps du reconciler).npm run db:migrate (= prisma migrate deploy). Client généré dans app/generated/prisma.GET /api/health — état DB / Redis / queue / worker (heartbeat). worker.healthy:false = worker down → aucun DM ne partira.POST /api/webhook — reçoit les événements comments de Meta (à configurer côté Meta avec le WEBHOOK_VERIFY_TOKEN).GET /api/instagram/callback — callback OAuth Instagram (à déclarer dans l'app Meta : https://openreply.panelbay.com/api/instagram/callback)./privacy, /terms, /data-deletion.Authorization: Bearer $CRON_SECRET, méthode GET) : /api/cron/refresh-tokens (rafraîchit les tokens IG long-lived à J-10 de l'expiration + reset des compteurs d'usage mensuels), /api/cron/snapshot-followers, /api/cron/attach-next-reel. Un appel non authentifié renvoie 401./etc/systemd/system/openreply-refresh-tokens.service (Type=oneshot) : charge .env (EnvironmentFile) puis curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" http://127.0.0.1:8351/api/cron/refresh-tokens./etc/systemd/system/openreply-refresh-tokens.timer : OnCalendar=*-*-* 04:17:00, Persistent=true (rattrape un run manqué après downtime). Activé (systemctl enable --now openreply-refresh-tokens.timer).systemctl list-timers openreply-refresh-tokens.timer · déclencher à la main : systemctl start openreply-refresh-tokens.service puis journalctl -u openreply-refresh-tokens.service -n 20.snapshot-followers et attach-next-reel ne sont PAS encore câblés (non critiques) : les ajouter en dupliquant ce timer si besoin quand un compte réel est connecté.Tout dans /root/workspace/apps/openreply/.env (jamais les valeurs ici, juste où) :
NEXTAUTH_SECRET, CRON_SECRET, ENCRYPTION_KEY (64 hex, identique web+worker, chiffre les tokens IG), WEBHOOK_VERIFY_TOKEN.DATABASE_URL, REDIS_URL, NEXTAUTH_URL=https://openreply.panelbay.com.RESEND_API_KEY + EMAIL_FROM : compte Resend + domaine expéditeur vérifié. Obligatoire pour se connecter (login = magic link email uniquement).INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET, FACEBOOK_APP_SECRET : app Meta developer (type Business, use case "Manage messaging and content on Instagram"). Voir docs/setup.md étapes 4-9.META_GRAPH_API_VERSION : v23.0 par défaut (ajustable)..env). La changer = tous les comptes IG connectés doivent se reconnecter./api/health → worker.healthy le dit.SENT), il n'apparait juste pas dans la boîte principale. Piège classique en test ("j'ai rien reçu" alors que c'est dans Général). Confirmé 2026-09-27.keywords. Chaque élément est matché tel quel (regex whole-word si wholeWordMatch=true). Mettre plusieurs mots dans une seule case séparés par un espace (ex. "paraguay paraguai") crée UN seul mot-clé = l'expression exacte "paraguay paraguai", qui ne matche jamais un commentaire ne disant que "Paraguay" → aucun DM. Pour plusieurs variantes, les saisir comme entrées séparées ({paraguay,paraguai}). Vu le 2026-09-27 sur la campagne "Paraguay" de mattparaguay7.WebhookEvent → DmLog → OperationalEvent), plus rapide que les logs. Comparer une campagne qui marche à une qui échoue (même compte ou autre) isole vite le champ fautif.docker redémarre, les conteneurs remontent seuls (--restart unless-stopped). Ne pas exposer 5432/6379 publiquement (ils sont bindés 127.0.0.1). Le docker-compose.yml upstream mappait les ports en 0.0.0.0 : corrigé en 127.0.0.1:... + images pinnées par digest (un futur docker compose up n'exposera donc plus Postgres/Redis).next.config.ts renvoie désormais poweredByHeader:false (plus de X-Powered-By) + headers() (HSTS, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP de base). La CSP est volontairement permissive sur script-src/style-src ('unsafe-inline' 'unsafe-eval' https:) pour ne pas casser le dashboard Next/React ; frame-ancestors 'none' verrouille le clickjacking. Toute modif de next.config.ts impose npm run build + restart des 2 services avant de considérer l'app saine.PROCESSED (arthur.k.diary, sylvain.dufayet, etc.), app/worker/compte sains, MAIS 0 DmLog pour mattparaguay7 alors que la campagne "palabra paraguay" de monica.inversiones ({Paraguay}) envoyait 81 DM. Cause : keywords={"paraguay paraguai"} = une seule expression (les 2 mots collés dans une case, séparés par un espace). Avec wholeWordMatch=true le matcher cherche la phrase exacte "paraguay paraguai" → ne matche jamais "Paraguay" seul. Fix : UPDATE Automation SET keywords=ARRAY['paraguay','paraguai']. Restart openreply-worker → le balayage de rattrapage (poll toutes les 5 min, lookback 72h) a enqueue+envoyé 8 DM d'un coup (dont arthur.k.diary et sylvain.dufayet). Piège ajouté en §7. Aucune modif de code.npm install, Postgres 16 + Redis 7 en conteneurs Docker (bindés 127.0.0.1), génération des 4 secrets locaux, .env écrit (infra + secrets faits ; Resend + Meta laissés vides), prisma migrate deploy (toutes migrations appliquées, Prisma 7.8), npm run build OK. 2 services systemd créés (openreply-web :8351 + openreply-worker). Exposé en HTTPS via broker : https://openreply.panelbay.com. /api/health = status ok (db/redis/queue/worker tous verts). RESTE (côté Matt) : compte Resend (login) + app Meta developer (webhooks Instagram) + connexion d'un compte Instagram Business.mattparaguay7 réparée + fix endpoint token IG. Symptôme : à la connexion de mattparaguay7, écran "La conexión con Instagram falló / Unsupported request - method type: get" (IGApiException code 100), reproductible dans 2 workspaces. Les 3 autres comptes du workspace resident.paraguay@gmail.com (mattatma, resident_paraguay, resident_paraguay_en) passent sans souci. Diagnostic pas à pas (logs [Meta debug]/[Meta diag] temporaires, retirés depuis) : (1) l'échange code→token court réussit (token IGAA…, 4 permissions business accordées, user_id présent) ; (2) MAIS tous les appels graph.instagram.com avec CE token échouent, y compris un simple GET /me, en GET comme en POST → ce n'est ni la méthode ni l'URL. (3) Preuve : un token stocké d'un compte qui marche (mattatma) répond 200 OK sur graph.instagram.com/me le même jour → serveur/URL/code OK. (4) Le compte est bien Professionnel (Créateur). Vraie cause : accès Standard / mode dev sur les permissions Instagram → seuls les comptes ayant un rôle sur l'app obtiennent un token FONCTIONNEL ; un compte externe (mattparaguay7) passe l'autorisation mais reçoit un token inerte. Fix : ajouté mattparaguay7 comme Instagram Tester (console Meta → App Roles → Roles → Instagram Testers) + invite acceptée côté Instagram → reconnexion OK (instagramId 17841449139467968, webhook actif). Côté code, correctif secondaire conservé : les endpoints token IG (getLongLivedToken/refreshLongLivedToken dans lib/meta/client.ts) sont désormais bâtis sur https://graph.instagram.com sans préfixe de version (conforme à la doc Meta, via constante INSTAGRAM_GRAPH_HOST) au lieu de …/v23.0/ ; tolérance ajoutée au format data:[] du token court dans lib/meta/oauth.ts (défensif). npm run build + restart openreply-web. Leçon : "Unsupported request - method type: get" à la connexion d'un compte externe = compte pas ajouté en Instagram Tester, PAS un bug de code.Workspace + ensureWorkspaceForUser cree un workspace OWNER par utilisateur au 1er login ; toutes les requetes filtrent par workspaceId derive de la session, jamais du client -> isolation etanche verifiee). Login = magic link email (self-service). Aucune modif de code. (1) Expediteur des emails de login corrige : EMAIL_FROM passe de onboarding@resend.dev (sandbox, n'envoie qu'a soi) a OpenReply <login@panelbay.com>. Domaine panelbay.com cree dans Resend (region eu-west-1, id 1f2278a5-...), 3 enregistrements DNS (DKIM resend._domainkey, MX+SPF send) poses dans Cloudflare (zone panelbay.com), resolvent publiquement ; verification Resend en cours au moment de l'ecriture (DNS OK). Backup .env.bak.* cree. Restart openreply-web, health OK. (2) Path Meta choisi = testers : chaque compte IG d'agent doit etre ajoute comme Instagram Tester dans la console Meta (app en mode Development), l'agent accepte l'invite cote Instagram ; pas d'App Review. (3) Tutoriel agents en espagnol : /root/workspace/OpenReply-tutorial-agentes-ES.md. NB : l'UI est en anglais (traduction ES non faite, chantier separe). RESTE cote Matt : ajouter les IG des agents en testers (console Meta) ; option future = allowlist d'emails a l'inscription (aujourd'hui signup ouvert, mais gate naturelle car sans statut tester l'agent ne peut pas connecter d'IG). MAJ fin de session : domaine panelbay.com verified cote Resend (email test envoye). UI traduite en espagnol : 24 fichiers de la surface agent (app/(dashboard)/*, login, verify-request, composants dashboard) passes en espagnol neutre LatAm (imperatifs "vos"), npm run build OK, restart web, login/verify rendus en ES verifies. Non traduits volontairement : pages marketing/SEO publiques, pages legales (privacy/terms/data-deletion), valeurs d'enum affichees telles quelles (OWNER/ADMIN/MEMBER). Espagnol code en dur, pas de toggle multilingue (chantier separe). (1) Cron refresh tokens IG câblé (item audit #2, P1) : timer systemd openreply-refresh-tokens.timer (quotidien 04:17, Persistent) + .service oneshot appelant /api/cron/refresh-tokens avec Bearer $CRON_SECRET. Testé : {"success":true,...} (auth OK). Empêche la déconnexion silencieuse des comptes IG à 60 jours.
(2) En-têtes de sécurité (item #3) : next.config.ts → poweredByHeader:false + headers() (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP de base permissive scripts/styles). npm run build OK, restart des 2 services, healthcheck vert, X-Powered-By absent, pages Meta /privacy /terms /data-deletion = 200.
(3) docker-compose.yml (item #9) : ports Postgres/Redis passés de 0.0.0.0 à 127.0.0.1:..., images pinnées par digest (postgres:16@sha256:33f923…, redis:7-alpine@sha256:e7723f…). N'affecte pas les conteneurs déjà lancés.
NON fait (reporté à l'orchestrateur/Matt) : item #1 deps vulnérables (npm audit = 17 vulns, 2 critiques dans la couche auth @auth/core/next-auth beta) — npm audit fix NON appliqué (risque sur le flux magic-link d'un fork en prod, un build vert ne valide pas l'auth runtime) ; items nginx #4 rate-limit + en-têtes vhost, #5 DoS écriture DB, #6/#7/#8 (RBAC diagnostics, observabilité, systemd non-root).