Adaptation VigieProcure : Dockerfiles, DeepSeek, déploiement vigiep1 - #1
Open
franckh-stack wants to merge 45 commits into
Open
Adaptation VigieProcure : Dockerfiles, DeepSeek, déploiement vigiep1#1franckh-stack wants to merge 45 commits into
franckh-stack wants to merge 45 commits into
Conversation
Multi-stage build (deps/build/runtime) on oven/bun:1.3.12. Two fixes
verified empirically against the plan's first draft:
- apps/api's postinstall (chmod-trpc-binary.mjs) needs
apps/api/scripts/ present in the deps stage, not just package.json,
or bun install fails.
- @crm/db's postinstall runs `prisma generate`, whose
prisma.config.ts calls env("DATABASE_URL") eagerly and throws if
unset -- even though generate never opens a connection. Deps stage
sets a placeholder DATABASE_URL for install; the real value is
supplied at `docker run` time and overrides it.
Ignore file named apps/api/Dockerfile.dockerignore, not
apps/api/.dockerignore: Task 3's build command uses the repo root as
build context (docker build -f apps/api/Dockerfile ... .), and Docker
only honors a Dockerfile-specific ignore file when it is named
<path-to-dockerfile-relative-to-context>.dockerignore at the context
root. Verified with a canary file: apps/api/.dockerignore was
silently ignored (canary leaked into the image), Dockerfile.dockerignore
excludes node_modules/dist/.git as intended.
Verified: image builds, runs against the real crm_trycompai database
(137.74.172.178), connects, and serves GET /health -> 503
{"status":"error","database":"down"} -- expected since Task 2 only
provisioned an empty database, migrations run in Task 6.
…tual default/deploy branch
…way, add Dockerfile
Vigieproc fork adaptation -- avoids a Vercel account dependency for the
main agent's LLM calls, cf. scripts/SPEC-fork-trycompai-crm.md Task 5.
eve already accepts a raw AI SDK LanguageModel in place of a Gateway
model-id string (PublicAgentStaticModelDefinition = string | LanguageModel),
so no change to eve itself was needed.
deepseekModel() uses @ai-sdk/openai's .chat(...) form explicitly, not the
callable-provider shorthand: the shorthand defaults to the Responses API
(POSTs to /responses), which DeepSeek's OpenAI-compatible endpoint does
not implement -- found empirically via the unit test's provider assertion.
`name: "deepseek"` overrides the default `openai` provider id.
selectedModel()/ModelSelection are kept (not removed as the task's draft
code suggested) because apps/agent/agent/subagents/agent_builder/agent.ts
and test/model.integration.spec.ts still depend on them for the per-run
custom-agent-builder model picker -- out of scope for this task, which
only swaps the main agent's static model. Its db import is now lazy so
importing this module (for deepseekModel(), from the new unit test)
doesn't eagerly require DATABASE_URL/TEST_DATABASE_URL.
The new unit test lives in apps/agent/test/ (not co-located next to
model.ts as first drafted) to match this app's existing convention --
every other bun:test spec lives there, specifically because apps/agent's
tsconfig include glob only covers agent/**, which has no bun:test types
and fails tsc --noEmit otherwise.
Dockerfile mirrors apps/api's and apps/app's (Tasks 3-4): DATABASE_URL
placeholder for @crm/db's postinstall prisma generate, multi-stage
bun build via turbo. Runtime CMD is `cd apps/agent && bun run start`,
not `bun apps/agent/scripts/start.ts` directly -- start.ts spawns the
`eve` CLI via child_process.spawn, which resolves against $PATH, and the
image's system PATH does not include node_modules/.bin (only `bun run`
augments it) -- confirmed empirically ("Executable not found in $PATH:
eve" with the direct form, despite the binary existing on disk).
No docker.io/docker CLI added to the runtime image: eve's sandbox backend
selection (selectDefaultSandbox in its own source) only tries Docker if
isDockerDaemonAvailableSync() is true, and falls back to microsandbox
otherwise. With no docker socket mounted (that lands in Task 6), the
container initializes its sandbox template and starts cleanly without
the CLI -- confirmed by running it standalone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014caYm8q32Dzgm2TiTeKK8G
crm-api (3.65GB) and crm-agent (4.56GB) runtime images copied the FULL monorepo node_modules from the build stage (dev+prod deps for every workspace at once) -- repeatedly exhausted vigiep1's 72GB disk across rebuild cycles in production (29/08/2026). Both Dockerfiles now reinstall with 'bun install --production' in a dedicated stage before the runtime COPY, after removing the full node_modules tree. apps/agent/package.json: moved microsandbox and just-bash from devDependencies to dependencies -- eve's sandbox backend needs at least one importable at runtime even though docker.sock (the preferred backend in this deployment) is mounted; a --production install would otherwise have silently dropped them (carried-forward minor finding from Task 5's review, now directly relevant).
bun install --production re-triggers every workspace's postinstall hook
on a fresh install, including @crm/db's ('prisma generate') -- but prisma
(the CLI) is a devDependency, excluded by --production, so the hook fails
with exit 127. Unnecessary anyway: the Prisma Client was already
generated during the earlier 'build' stage and persists on disk
(packages/db/src/generated/), untouched by the node_modules rm+reinstall.
--ignore-scripts skips re-running it.
…endencies crm-agent crash-looped after the --production slim-down: '[TSCONFIG_ERROR] Failed to load tsconfig @crm/typescript-config/base.json: Tsconfig not found' -- eve's 'start' re-bundles the authored agent module on every boot (not just 'eve build' ahead of time), and needs tsconfig resolution (and presumably the typescript toolchain) present at runtime, not just at build/dev time. Both were devDependencies, excluded by --production.
listMessages() already supported an arbitrary q= filter but nothing called it with a participant clause -- the live incremental sync only ever used listHistory(). searchByParticipant() reuses it unchanged (default q string is byte-identical when query is omitted, pinned by a regression test).
… backfill Same treatment as GmailClient -- listEvents() gains an optional q passthrough (omitted by default, matching today's behavior exactly), searchByParticipant() is the new targeted-search entry point.
Behavior-preserving move of GmailSyncService's former private parse()/ sentAt() -- verbatim logic, now unit-testable on its own and reusable by the contact-history backfill without duplicating parsing logic. gmail-sync.service.ts delegates, no behavior change (no dedicated spec existed for that service before; the extracted pure-function test is the regression net for this move).
…y/contact + relinks Optional 5th param, omitted everywhere in the live incremental sync (byte-identical default behavior, pinned by re-running mailbox-thread-writer.spec.ts unmodified). When passed by the contact history backfill: - a brand-new thread uses the given ids directly, skipping match.resolve() entirely - a thread the live sync already stored by company-only match (companyId set, contactId null -- the common case) gets relinked to the newly-created contact instead of silently staying invisible on its Relationship panel. Relink never touches a thread already pointing at a different contact or a different company.
…backfillForParticipant() apply() and a new buildContext() (extracted from sync(), same pattern as ThreadWriterService.context()) become public so a preresolved company/contact can drive writes directly -- default (preresolved omitted) apply() behavior is unchanged for the live incremental sync's own call. This is the first direct test coverage for this service; flagging the pre-existing gap, not just closing it silently. backfillForParticipant() is the contact-history-backfill entry point: search by participant email, apply() each result with the known contact/company, never touching the live sync's cursor/pagination state.
Given a newly-created contact's email, searches Gmail (via GmailClient.searchByParticipant + parseGmailMessage + ThreadWriterService.store()) and Calendar (via CalendarSyncService.backfillForParticipant()) in parallel, each independently failing without blocking the other. Not wired into ContactsService.create() yet -- that's Unit H. Window/caps as tunable exported constants (24 months, 200 Gmail results, 100 Calendar results), no pagination in v1 (documented limitation, not silent). Capstone test proves the actual point of this feature: a thread the live sync already stored by company-only match becomes visible on the contact's Relationship panel query after backfill, not just 'no error'.
google.module.ts exports CalendarSyncService + GmailClient (previously internal-only). contacts.module.ts imports MailboxModule + GoogleModule and registers ContactHistoryBackfillService. No circular dependency (confirmed: nothing in mailbox/, google/, agent/, companies/, trpc/ imports ContactsModule). Verified by booting the real AppModule (auth.e2e.spec.ts, --timeout 60000 -- the default 5s bun test timeout is too short for this heavy a bootstrap, unrelated to this change).
Router passes ctx.user.id, same pattern as decideFact (contacts.router.ts). Service gains an optional actorId param -- when given and the contact has an email, fires history.run() in the background (.catch()+log, same fire-and-forget pattern already used for fields.queueBackfillForNewRecord). actorId omitted (all 3 pre-existing test call sites) or no email -> never fires, byte- identical to before. bulk.spec.ts, fields.spec.ts, record-delete.spec.ts each get a 7th constructor stub (no behavior change, confirmed by re-running all 39 tests across the three files unmodified otherwise).
…izeImports Reorders 3 files touched by this branch. Remaining 'needs formatting' findings across the repo (this branch included) are the pre-existing CRLF/core.autocrlf Windows artifact already documented in CR-FORK-TRYCOMPAI-CRM-DEPLOYE-20260828.md -- confirmed present on a clean release checkout too (241 pre-existing errors), not introduced here.
feat(contacts): automatic Gmail/Calendar history backfill on contact creation
…ndence Measured on real backfilled data (Damien Vignault, WP crm-enrich, 02/09/2026): 6 threads/13 messages from a decision-makers' club mailing list (26-84 recipients each, sender bruno.hervein@orange.fr, none of them personal) were polluting the contact's business relationship view alongside 3 real 1:1 threads (1 recipient each). parseGmailMessage() now returns null for a message that either carries a List-Unsubscribe header or has more than BULK_MAIL_RECIPIENT_THRESHOLD (5) recipients -- both signals proven against the real data that triggered this fix. Applies to both the live incremental sync and the contact-history backfill, since both share this parser.
fix(gmail): filter mailing-list broadcasts out of the contact relationship view
… email/meeting NOTE_TYPES fed the "Notes" tab's filter clause, but included EMAIL and MEETING alongside NOTE/CALL -- both of which already have their own dedicated tab. Effect: every synced email and every synced meeting also showed up under Notes, which its own empty-state copy describes as "what you write down for the next person to read" -- i.e. manual entries only. Found investigating a report that "all mail lands in the notes channel" after the contact-history backfill made Notes tab traffic visible for the first time (previously the tab was empty because the live sync had never stored anything). NOTE_TYPES narrowed to [NOTE, CALL] -- the two types with no dedicated tab of their own.
fix(activities): Notes tab was showing every synced email and meeting
…hesis Requested after the deployed bulk-mail filter (gmail-message-parser.ts) didn't catch every Club'IT broadcast for Damien Vignault -- some slip under the recipient-count/List-Unsubscribe heuristics. Reps need a manual escape hatch for whatever the automated filter misses. EmailThread gains excludedAt (soft, not a delete -- the row and its rfcMessageId/rootMessageId dedup keys stay in place so the live sync never re-imports an excluded thread). ActivitiesService.timeline()/ timelineCounts() and ContactsService.relationship() now filter it out everywhere the synthesis reads from EmailThread/Activity. New tRPC mutations activities.excludeEmail/restoreEmail (reversible). Known gap, not covered by a dedicated test: ActivityStampService's lastActivityAt is not recomputed on exclude -- if the excluded thread was the most recent activity, lastActivityAt won't fall back to the next one until something else touches it. Acceptable for v1 (matches this session's "backend only, no UI yet" scoping), flagged for anyone picking this up next.
feat(activities): allow excluding an email thread from the CRM synthesis
DEC-C-CRM-10 (Franck, 2026-09-03): inbound direction of F.24 (VigieProcure's CRM gateway) activated. Branches into the same point that already queues agent-worker events (withCrmEvents) -- best-effort, HMAC-SHA256 signed on the raw body, same "no secret = no bridge" rule as the existing agent bridge. New vigieprocure-bridge.ts, VIGIEPROCURE_WEBHOOK_URL/_SECRET env vars (both optional, unset = no-op). createEventTask now returns the AgentTask id, used as a stable event_id for VigieProcure-side deduplication. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLAb4MA3VwGs6CKRhxtsmD
Corrige deux lignes trop longues detectees par le hook pre-push (bun run lint). Aucun changement de comportement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLAb4MA3VwGs6CKRhxtsmD
Notify VigieProcure on CRM deal lifecycle events (DEC-C-CRM-10)
bunx biome check --write . after biome.jsonc's formatter/organizeImports rules. All 12 files are pure line-wrap/reformatting (long object literals, type unions, function signatures split across lines) -- verified diff-by-diff, no logic or values changed. This was the actual scope of the pre-push lint gate failure blocking feat/company-siren-column. The other ~790 files git status flagged were a false positive from local core.autocrlf=true (Windows checkout) vs this repo's LF blobs -- confirmed via `git diff --stat` (empty) and `git ls-files --eol` (i/lf w/lf, no divergence). No commit needed for those; see follow-up note on core.autocrlf / .gitattributes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
…e.failed `no-unknown-parameters` allows an explicit `unknown` parameter only when named `cause` -- the convention every other error-normalizing method in this repo already follows (companies.service.ts, contacts.service.ts, deals.service.ts, fields.ts, main.ts, settings.service.ts all name it `cause`). This was the sole outlier, still named `error`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
…idening
`no-known-value-widening` -- `return { calls }` already carries evidence
for `{ calls: URL[] }`; the explicit anonymous return-type annotation
discarded it for no benefit (callers destructure `{ calls }` either way).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
`no-runtime-typeof` -- bun:test ships a dedicated matcher for exactly this assertion, same intent without a bare typeof check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
…mport no-known-value-widening: gmailCaptured/calendarCaptured were annotated with anonymous object-literal types instead of the named contract already defined by the stub functions that mutate them (GmailStubOptions["captured"], calendarClientStub's own parameter type via Parameters<>) -- satisfies alone doesn't work here since these objects start empty and get filled by the stub's side effect. Also drops MailboxSyncModel (aliased MailboxSync), imported but never referenced -- preexisting on release, caught by biome's noUnusedImports while touching this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
chore: fix lint debt (formatting + anti-slop retyping)
…alue no-unsafe-dictionary-type -- payload and the logger.debug() parameter were Record<string, unknown>, an unsafe unknown escape hatch. The repo already has a concrete owner type for this exact case: Prisma.InputJsonValue /InputJsonObject, used the same way for outbound JSON payloads elsewhere in apps/api/src/agent/ (agent-trigger.service.ts). No behavior change -- both are still structural JSON objects at runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
…e-anti-slop fix(anti-slop): type VigieProcureEvent.payload with Prisma.InputJsonValue
Rapproche la fiche CRM du referentiel SIRENE cote VigieProcure (GET /api/v1/companies/resolve, api_v2). Nullable -- la plupart des fiches n'ont pas encore ete resolues, et les comptes non francais n'en auront jamais. Meme patron que le champ `domain` deja en place : CHAR(9) format SIRENE standard, contrainte unique scopee sur les fiches actives (`archivedAt IS NULL`) pour permettre la reutilisation d'un SIREN apres archivage. Aucun seed/backfill : la migration ajoute la colonne vide, ne resout pas retroactivement les fiches existantes. Cf. plan/CR reports/2026-09/CR/CR-RESOLUTION-SIREN-CRM-20260904.md (repo vigieprocure) pour le contexte complet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
ALTER TABLE company ADD COLUMN siren CHAR(9) + index unique partiel sur (siren) WHERE archivedAt IS NULL. Meme forme que la migration 20260820161500_archive_scoped_uniqueness (patron domain) deja en prod. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
feat(db): add siren column to Company, unique scoped on active records
Ajoute deux procedures tRPC (companies.resolveSiren, companies.setSiren) qui appellent GET /api/v1/companies/resolve cote api_v2 avec un JWT de service (VIGIEPROCURE_API_JWT/VIGIEPROCURE_API_URL, meme doctrine que VIGIEPROCURE_WEBHOOK_URL/_SECRET -- absent = fonctionnalite degradee, jamais un appel non authentifie). Sur la fiche company : affiche le SIREN existant, ou propose de le resoudre sur clic explicite. Un seul candidat "exact" ecrit automatiquement (exception ciblee et reversible, validee par Franck -- jamais depuis un effet au chargement ni un backfill de masse). Sinon, liste les candidats pour choix manuel. Conflit d'unicite (P2002 sur Company.siren) traduit en erreur utilisateur nommant la fiche en conflit, jamais un 500 nu. Inclut la regeneration de apps/api/src/generated/server.ts, qui corrige au passage une dette preexistante (excludeEmail/restoreEmail declares dans activities.router.ts depuis le commit 044e350 mais jamais regeneres). A provisionner separement (hors perimetre de ce chantier) : VIGIEPROCURE_API_URL, VIGIEPROCURE_API_JWT sur crm-api en production. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
- companySetSirenInput.siren: .length(9) laissait passer 9 caracteres non numeriques alors que le message annoncait "9 digits" -- .regex le fait respecter reellement. - companySetSirenOutput (conflict): conflictingCompanyId/Name n'etaient consommes nulle part cote frontend (seul `reason` est lu), et le fallback `conflicting?.id ?? ""` etait un `z.string()` non-nullable rempli d'une chaine vide dans un cas quasi mort (le findFirst est scope par la meme contrainte unique partielle que le P2002 qui declenche ce chemin). Retire les deux champs plutot que de les rendre nullable pour un consommateur qui n'existe pas. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
feat(companies): resolve and set SIREN via VigieProcure
Déplace le build Docker des 3 services (api/app/agent) de vigiep1 vers GitHub Actions. Sur vigiep1, bun install (1771+ paquets) prenait 36-41min par service faute de cache incrémental (Dockerfile réinstalle à froid volontairement, cf. incident disque plein du 29/08) et de disque I/O lent sur /mnt/hermes-extra -- un rebuild complet des 3 services dépassait 2h. Le cache GitHub Actions (type=gha) devrait ramener ça à quelques dizaines de secondes sur les runs suivants, sur des runners dédiés (pas de contention RAM/IO avec dagster/n8n/ vigieproc-api qui tournent sur le même host). vigiep1 passera de `docker compose build` à `docker compose pull` -- changement du docker-compose.yml de production (non versionné dans ce repo) à faire séparément, avec provisionnement d'un PAT read:packages pour l'authentification à ghcr.io (registre privé). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtJccFqpSuP8tQ3Pwg1dYH
feat: release release
release: sync main into release
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Résumé
PR documentaire couvrant l'ensemble du chantier d'adaptation de trycompai/crm pour l'infrastructure VigieProcure (fork+install, cf.
scripts/SPEC-fork-trycompai-crm.mddans le dépôt VigieProcure). Tout est déjà déployé et fonctionnel surcrm.vigieproc.fr— cette PR sert de vue d'ensemble du diff, pas d'étape de merge (releaseest déjà la branche par défaut, tout est déjà dessus).apps/agent/agent/lib/model.ts,agent.ts)bun install --production) après épuisement disque répété en prodBase de comparaison
pre-vigieproc-chantier= état du fork juste avant ce chantier (commit6d4793d, dernier merge amont avant le premier Dockerfile). Branche créée uniquement pour permettre cette vue de diff — pas un point de reprise à maintenir.Test plan
Déjà vérifié en production (vigiep1) :
🤖 Généré avec Claude Code