Admin : créer un ticket Forgejo depuis un message de contact #81
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Résumé
Depuis la page d'administration des messages de contact
(
/admin/messages, voirissue #80),
permettre à un admin de créer une issue Forgejo à partir d'un message
utilisateur en un geste : un bouton ouvre une modale pré-remplie (titre +
corps éditables), l'admin ajuste et confirme, l'app crée l'issue dans le
dépôt via l'API Forgejo et mémorise le lien sur le message.
Ce serait la première intégration Forgejo dans
apps/api(aucuneaujourd'hui — vérifié). Elle suit la même posture que les autres
intégrations externes du repo (
INTENT_SERVICE_*,INTERNAL_WORKER_SECRET) :config par variables d'environnement, fail-closed si incomplète (le
bouton n'apparaît pas, l'endpoint répond
503).Dépendance
Nécessite #80 livré (modèle
ContactMessage, moduleadmin-contact.{routes,service}.ts, page/admin/messages). Ce ticketajoute un delta dessus : 2 colonnes sur
ContactMessage, 2 routes admin,1 service
ForgejoService, l'UI de la modale. Si #80 est encore en cours aumoment de démarrer, intégrer directement les 2 colonnes à sa migration.
Décisions de conception (validées avec l'utilisateur)
forgejoIssueNumber+forgejoIssueUrlsurContactMessage) — la ligne affiche#N, le bouton disparaîtModèle de données — delta sur
ContactMessage(#80)Migration
prisma migrate dev --name contact_message_forgejo_link— 2colonnes nullables, aucun backfill.
Configuration (
apps/api/src/config/env.ts)Trois variables, toutes
.optional()dans le schéma ; le service etl'endpoint refusent tant que les trois ne sont pas là (même posture que
ADMIN_JWT_SECRET/INTENT_SERVICE_SECRETqui « fail closed ») :FORGEJO_BASE_URL— ex.https://git.dev.kyuno.fr(.url(), pas dedéfaut : propre au déploiement).
FORGEJO_TOKEN— jeton d'accès personnel Forgejo avec le scopewrite:issue(ouissue). Secret —.env.examplene montre qu'unchangeme.FORGEJO_REPO—owner/repo, ex.kyuno/batchCooking(regex
^[^/\s]+/[^/\s]+$).Documenter les trois dans
apps/api/.env.examplesous « seulement pour lacréation d'issue depuis un message ; le bouton est masqué tant que non
configuré ».
Backend
apps/api/src/lib/forgejo/forgejo.service.ts—ForgejoService(unevraie
class+ singletonexport const forgejo = new ForgejoService(),cf.
logger/analytics; conventions de dev) :isConfigured(): boolean— les trois vars présentes.createIssue({ title, body }): Promise<{ number: number; htmlUrl: string }>—
POST {FORGEJO_BASE_URL}/api/v1/repos/{FORGEJO_REPO}/issues, en-têteAuthorization: token {FORGEJO_TOKEN}, corps{ title, body }; lit{ number, html_url }de la réponse. Viafetchglobal (même approcheque les adaptateurs de sources ;
the-meal-db.tsest le précédent d'unmodule qui parle à un vrai réseau externe, et ses tests réassignent
globalThis.fetch). Réseau KO / réponse non-2xx ⇒ erreur typée(
HttpError 502 FORGEJO_REQUEST_FAILED).admin-contact.service.ts(#80) — ajoutercreateForgejoIssueFromMessage(messageId, input):503 FORGEJO_NOT_CONFIGUREDsi!forgejo.isConfigured().ContactMessage—404 CONTACT_MESSAGE_NOT_FOUNDsinon.409 CONTACT_MESSAGE_ALREADY_LINKEDsiforgejoIssueNumber !== null.forgejo.createIssue({ title, body })(aucun label — décision).prisma.contactMessage.update(... { forgejoIssueNumber, forgejoIssueUrl }),renvoyer le
ContactMessageAdminViewà jour.502, base non touchée (pas d'étatpartiel).
Plus
getForgejoStatus(): { enabled: boolean }.admin-contact.routes.ts(#80) — ajouter, derrièrerequireAdmin:POST /admin/api/contact/:id/forgejo-issue— corpscreateForgejoIssueSchema.GET /admin/api/contact/forgejo/status—{ enabled }.Contrat partagé (
packages/shared/src) :schemas/contact.ts(#80) —createForgejoIssueSchema:{ title: z.string().trim().min(1).max(255), body: z.string().trim().min(1).max(60000) }(255 = limite de titre Forgejo).
types/contact.ts(#80) — étendreContactMessageAdminViewavecforgejoIssueNumber: number | null+forgejoIssueUrl: string | null;ajouter
ForgejoIntegrationStatus { enabled: boolean }.errors/error-codes.ts—FORGEJO_NOT_CONFIGURED,FORGEJO_REQUEST_FAILED,CONTACT_MESSAGE_ALREADY_LINKED(+
CONTACT_MESSAGE_NOT_FOUNDs'il n'existe pas déjà via #80).Frontend (
apps/web)api/admin-client.ts—createForgejoIssue(id, { title, body }),getForgejoStatus().pages/admin/messages/MessagesPage.tsx(#80) :getForgejoStatus(); si!enabled, pas de bouton (petitlibellé « intégration Forgejo non configurée »).
#NversforgejoIssueUrl(
target="_blank"), pas de bouton.CreateForgejoIssueModal.tsx) + helpers purs dans unmessages.ts(hors.tsx, cf. conventions) :buildIssueTitle(message)— ex.[Bug] <première ligne, tronquée>.buildIssueBody(message)— le texte, puis un bloc---:Signalé par : <nom> <email>·Page : <pageUrl>·Version : <appVersion>·Navigateur : <userAgent>·Reçu le : <date>, + un pied🤖 Créé depuis le formulaire de contact.createForgejoIssue→ succès :fermer, la ligne montre
#N; erreur : message i18n(
ErrorMessageService, jamaiserr.messagebrut).admin.messages.forgejo.*(bouton, titre modale, libellés,succès, erreurs, « non configuré ») + entrées code d'erreur →
message dans
ErrorMessageService.Tests (obligatoires par PR, cf. specs/dev-conventions.md)
forgejo.service—globalThis.fetchréassigné :createIssuetape labonne URL avec l'en-tête
Authorization: tokenet{title, body};lit
{number, html_url}; réponse non-2xx / échec réseau ⇒ erreurtypée ;
isConfigured()suit l'env.admin-contact.service(étendu) —createForgejoIssueFromMessage:chemin nominal (stocke le lien, renvoie la vue à jour) ;
503nonconfiguré ;
404message inconnu ;409déjà lié ; échec Forgejo ⇒502et ligne intacte.admin-messages.cy.ts:forgejo/statusmockéenabled:true⇒ bouton visible, clic ⇒ modalepré-remplie,
POST .../forgejo-issuemocké ⇒ la ligne affiche#N;enabled:false⇒ pas de bouton.Documentation à mettre à jour
specs/backend-architecture.md—ForgejoService, les 2 routes admin,les 3 variables
FORGEJO_*.specs/batch-cooking-modele.md— les 2 colonnesforgejoIssue*deContactMessage.Découpage proposé
Une PR cohérente suffit, ou deux courtes stackées si on préfère
(cf. « PRs plutôt petites et scopées ») :
feat(admin): ForgejoService + POST .../forgejo-issue— config env,ForgejoService, delta modèle + migration, service/route admin, contratpartagé, tests API.
feat(admin): modale de création de ticket depuis un message—admin-client, bouton + modale + helpers, affichage du lien, i18n,Cypress.
Hors scope
aucun label auto).
en lecture seule).
répercutés sur le message — le lien est unidirectionnel.
FORGEJO_REPO, ou choix du dépôt.négligeable).
🤖 Filed by Claude Code