Compare commits

...

7 Commits

Author SHA1 Message Date
Matthieu Tholot e4b8df105e docs(catalog) : mark CatalogModule REQUIRED=true after Tristan review #12
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m35s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 10m13s
2026-05-26 16:15:16 +02:00
Matthieu Tholot 723a220ec1 docs(catalog) : add Timestampable + Blamable Shared pattern (v1.2) 2026-05-26 16:13:39 +02:00
Matthieu Tholot 3c9eaf5d69 docs(catalog) : add M0 categories specs (back + front)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m18s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 10m11s
2026-05-26 15:07:45 +02:00
gitea-actions 33599db5a3 chore: bump version to v0.1.39
Auto Tag Develop / tag (push) Successful in 7s
Build & Push Docker Image / build (push) Successful in 22s
2026-05-19 13:56:08 +00:00
matthieu 34e75a35fb ci : add pull_request quality gate workflow (#11)
Auto Tag Develop / tag (push) Has been cancelled
2026-05-19 13:55:59 +00:00
gitea-actions 1696602abb chore: bump version to v0.1.38
Auto Tag Develop / tag (push) Successful in 7s
Build & Push Docker Image / build (push) Successful in 22s
2026-05-19 06:38:13 +00:00
Matthieu cacd8718e5 chore(prod) : ajuster conf prod pour HTTP en reseau local
Auto Tag Develop / tag (push) Has been cancelled
- .env.prod.example : JWT_COOKIE_SECURE=0, CORS_ALLOW_ORIGIN en http
- prompt-rename-prod.md : retirer etape certbot/Let's Encrypt, verifier la resolution locale a la place
- deployment-docker.md : aligner DEFAULT_URI, CORS et JWT_COOKIE_SECURE sur HTTP
2026-05-19 08:38:03 +02:00
8 changed files with 1305 additions and 30 deletions
+119
View File
@@ -0,0 +1,119 @@
name: Pull Request — Quality gate
# Lance les tests + lint + build sur chaque PR ciblant develop.
# Deux jobs en parallele (backend / frontend) pour reduire le temps de feedback.
# E2E volontairement hors scope (cf. regle d'or testing.md).
on:
pull_request:
branches:
- develop
# Annule les runs obsoletes quand on repush sur la meme PR.
concurrency:
group: pr-${{ gitea.event.pull_request.number }}
cancel-in-progress: true
jobs:
backend:
name: Backend (PHP CS + PHPUnit)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
# Doivent matcher la DATABASE_URL ci-dessous. Le suffixe `_test`
# est applique automatiquement par Doctrine en APP_ENV=test.
POSTGRES_USER: app
POSTGRES_PASSWORD: '!ChangeMe!'
POSTGRES_DB: app
# Pas de `ports:` host mapping — le runner partage l'hote avec la
# prod (Postgres deja sur 5432) et les jobs Gitea Actions tournent
# en container sur un reseau Docker dedie : le service est joignable
# via son nom (`postgres`), pas via 127.0.0.1.
options: >-
--health-cmd "pg_isready -U app"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
APP_ENV: test
APP_SECRET: ci-secret-not-used
APP_DEBUG: 0
DEFAULT_URI: http://localhost/
DATABASE_URL: postgresql://app:!ChangeMe!@postgres:5432/app?serverVersion=16&charset=utf8
JWT_SECRET_KEY: '%kernel.project_dir%/config/jwt/private.pem'
JWT_PUBLIC_KEY: '%kernel.project_dir%/config/jwt/public.pem'
JWT_PASSPHRASE: change_me_in_env_local
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: pdo, pdo_pgsql, intl, opcache, zip, mbstring, sodium
coverage: none
tools: composer:v2
- name: Cache Composer
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: |
composer-
- name: Install PHP dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Generate JWT keypair
run: php bin/console lexik:jwt:generate-keypair --skip-if-exists --no-interaction
- name: PHP CS Fixer (dry-run)
run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes --dry-run --diff
- name: Bootstrap test database
run: |
php bin/console doctrine:database:create --env=test --if-not-exists --no-interaction
php bin/console doctrine:migrations:migrate --env=test --no-interaction
php bin/console doctrine:schema:update --env=test --force --no-interaction
php bin/console doctrine:fixtures:load --env=test --no-interaction
php bin/console app:sync-permissions --env=test --no-interaction
- name: Run PHPUnit
run: php -d memory_limit=512M vendor/bin/phpunit
frontend:
name: Frontend (lint + Vitest + build)
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node 22
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install Node dependencies
run: npm ci
- name: ESLint
run: npm run lint
- name: Unit tests (Vitest)
run: npm run test
- name: Build production (nuxt build)
run: npm run build:dist
+1 -1
View File
@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.37'
app.version: '0.1.39'
+2 -2
View File
@@ -152,7 +152,7 @@ DATABASE_URL="postgresql://malio:password@host.docker.internal:5432/starseed_pro
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=<generer avec: openssl rand -hex 32>
JWT_COOKIE_SECURE=1
JWT_COOKIE_SECURE=0
JWT_COOKIE_SAMESITE=lax
JWT_TOKEN_TTL=86400
JWT_COOKIE_TTL=86400
@@ -161,7 +161,7 @@ JWT_COOKIE_TTL=86400
CORS_ALLOW_ORIGIN='^https?://starseed\.malio-dev\.fr$'
# App
DEFAULT_URI=https://starseed.malio-dev.fr
DEFAULT_URI=http://starseed.malio-dev.fr
```
### 6. Generer les cles JWT
+20 -25
View File
@@ -2,8 +2,9 @@
Copier-coller integralement dans une session Claude lancee **sur le serveur de prod** apres que :
- le push develop + build CI ont publie l'image `gitea.malio.fr/malio-dev/starseed:latest`,
- le DNS `starseed.malio-dev.fr` resout vers ce serveur,
- un certificat Let's Encrypt existe (ou est pret a etre genere) pour `starseed.malio-dev.fr`.
- la resolution reseau local (DNS interne ou `/etc/hosts` des postes clients) pour `starseed.malio-dev.fr` est en place.
> Setup : HTTP en reseau local, pas de TLS. Pas de Let's Encrypt.
---
@@ -11,7 +12,7 @@ Copier-coller integralement dans une session Claude lancee **sur le serveur de p
Tu es sur le serveur de production d'une app Symfony+Nuxt qui s'appelait **Coltura** et qui doit etre renommee en **Starseed**. Le rename cote code est deja fait et merge. Le repo Gitea s'appelle deja `starseed`. L'image `gitea.malio.fr/malio-dev/starseed:latest` est publiee.
Le DNS `starseed.malio-dev.fr` resout vers ce serveur. Le certificat Let's Encrypt pour ce nom de domaine est gere a la main par l'admin (a confirmer avant l'etape nginx).
L'app est servie en **HTTP sur reseau local** (pas de TLS, pas de Let's Encrypt). La resolution `starseed.malio-dev.fr` est faite via DNS interne ou `/etc/hosts` cote postes clients — pas de certificat a gerer.
Objectif : basculer la prod sur le nouveau nom (registry, container, DB, path FS, vhost) **sans perdre les donnees** et avec downtime minimal (mode maintenance pendant la migration).
@@ -30,9 +31,6 @@ ls -la /var/www/starseed/ 2>/dev/null | head -5
# 4. Vhost nginx system
sudo ls -la /etc/nginx/sites-enabled/ | grep -E "coltura|starseed"
# 5. Cert Let's Encrypt
sudo ls /etc/letsencrypt/live/ | grep -E "coltura|starseed"
```
**Apres confirmation de l'etat, executer dans cet ordre, en demandant validation utilisateur AVANT chaque etape destructive (DB drop, rm -rf, certificat) :**
@@ -43,7 +41,7 @@ sudo ls /etc/letsencrypt/live/ | grep -E "coltura|starseed"
cd /var/www/coltura
touch maintenance.on
# Verifier qu'une requete renvoie 503
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://coltura.malio-dev.fr/
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://coltura.malio-dev.fr/
```
Doit renvoyer `503`.
@@ -100,17 +98,18 @@ sudo test -f /var/www/starseed/.env && echo ".env OK"
Editer `/var/www/starseed/.env` :
- `DATABASE_URL` : remplacer `/coltura_prod` -> `/starseed_prod` (et user si renomme a etape 3)
- `CORS_ALLOW_ORIGIN` : remplacer `coltura.malio-dev.fr` -> `starseed.malio-dev.fr`
- `DEFAULT_URI` : `https://starseed.malio-dev.fr`
- `DEFAULT_URI` : `http://starseed.malio-dev.fr`
- `JWT_COOKIE_SECURE` : doit etre `0` (HTTP, pas de TLS) — verifier qu'il l'est deja
Diff attendu :
```diff
- DATABASE_URL="postgresql://malio:xxx@host.docker.internal:5432/coltura_prod?..."
+ DATABASE_URL="postgresql://malio:xxx@host.docker.internal:5432/starseed_prod?..."
- CORS_ALLOW_ORIGIN='^https?://coltura\.malio-dev\.fr$'
+ CORS_ALLOW_ORIGIN='^https?://starseed\.malio-dev\.fr$'
- DEFAULT_URI=https://coltura.malio-dev.fr
+ DEFAULT_URI=https://starseed.malio-dev.fr
- CORS_ALLOW_ORIGIN='^http://coltura\.malio-dev\.fr$'
+ CORS_ALLOW_ORIGIN='^http://starseed\.malio-dev\.fr$'
- DEFAULT_URI=http://coltura.malio-dev.fr
+ DEFAULT_URI=http://starseed.malio-dev.fr
```
### Etape 6 — Stopper et supprimer l'ancien container
@@ -144,9 +143,9 @@ sudo docker compose exec -T -u www-data app php bin/console cache:clear --env=pr
sudo docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
```
### Etape 9 — Vhost nginx system + certificat
### Etape 9 — Vhost nginx system (HTTP only)
Copier le nouveau vhost (a jour avec `server_name starseed.malio-dev.fr` et `root /var/www/starseed/public`) :
Copier le nouveau vhost (a jour avec `server_name starseed.malio-dev.fr` et `root /var/www/starseed/public`, `listen 80` uniquement) :
```bash
sudo cp /var/www/starseed/infra/prod/nginx-proxy.conf /etc/nginx/sites-available/starseed.conf
@@ -155,14 +154,10 @@ sudo rm -f /etc/nginx/sites-enabled/coltura.conf
sudo nginx -t
```
**Avant de reload nginx**, generer le certificat Let's Encrypt pour le nouveau domaine (l'utilisateur doit confirmer ; certbot peut casser temporairement le vhost actuel pendant la challenge) :
Verifier la resolution reseau local avant reload :
```bash
# Verifier le DNS d'abord
dig +short starseed.malio-dev.fr
# Generer le cert (l'utilisateur valide)
sudo certbot --nginx -d starseed.malio-dev.fr --non-interactive --agree-tos -m matthieu@malio.fr
getent hosts starseed.malio-dev.fr || echo "ATTENTION : starseed.malio-dev.fr ne resout pas localement"
```
Puis :
@@ -176,9 +171,9 @@ sudo systemctl reload nginx
```bash
rm -f /var/www/starseed/maintenance.on
# Tests externes
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://starseed.malio-dev.fr/
curl -s https://starseed.malio-dev.fr/api/version
# Tests externes (HTTP local)
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://starseed.malio-dev.fr/
curl -s http://starseed.malio-dev.fr/api/version
```
`/api/version` doit renvoyer du JSON avec la version courante.
@@ -229,8 +224,8 @@ rm -f /var/www/coltura/maintenance.on
## Regles de comportement pour le Claude prod
- **Ne jamais skipper le backup** (etape 2).
- **Demander confirmation utilisateur** avant : `DROP DATABASE`, `rm -rf`, `certbot`, et avant de lever le mode maintenance final.
- **Demander confirmation utilisateur** avant : `DROP DATABASE`, `rm -rf`, et avant de lever le mode maintenance final.
- **Une seule operation destructive a la fois**, attendre le retour utilisateur entre chaque.
- **Logger systematiquement** la sortie des commandes critiques (pg_dump, docker compose up, certbot).
- **Logger systematiquement** la sortie des commandes critiques (pg_dump, docker compose up, nginx -t / reload).
- **Si une etape echoue**, NE PAS continuer — declencher le rollback.
- **Ne commit rien** sur le repo depuis le serveur prod.
File diff suppressed because it is too large Load Diff
+113
View File
@@ -0,0 +1,113 @@
---
# === IDENTITÉ ===
module: M0
nom: "Gestion des catégories"
ecran: gestion-categories
owner_spec: Matthieu
backup_spec: Tristan
version: V0
date_redaction: 2026-05-22
# === LIENS ===
maquette_figma: null # pas de Figma — UI admin standard
regles_metier: [RG-1.01, RG-1.02, RG-1.03, RG-1.04, RG-1.05, RG-1.06, RG-1.07, RG-1.08, RG-1.09, RG-1.10, RG-1.11, RG-1.12, RG-1.13]
roles: [Admin, Bureau, Compta, Commerciale, Usine]
lien_spec_back: ./spec-back.md
# === VALIDATION CLIENT #1 ===
client_validation_1:
statut: validee # V0 client validée le 22/05/2026
date: 2026-05-22
canal: ecrit
valide_par: "Matthieu (CP MALIO) — validation implicite, périmètre projet"
resume: "Module 0 — Gestion des catégories. Page admin (datatable + drawer). 2 champs (Nom + Type), 3 actions (Ajouter / Consulter / Modifier). Admin only."
trace_archivee: "uploads/c4ebb6b4-M0categories.docx (V0 d'origine .docx) — restitué ci-dessous en Markdown."
# === LIEN LESSTIME ===
lesstime_taskgroup_id: 22
lesstime_project_id: 6 # ERP / Starseed
statut_global: en_dev # tickets créés en backlog Lesstime le 2026-05-26
---
# Module 0 — Gestion des catégories (V0 front)
> **Origine** : spec front V0 livrée le 22/05/2026 (`c4ebb6b4-M0categories.docx` + `f665acfb-M0categoriesV0.pdf`). Restitution Markdown fidèle pour intégration au workflow MALIO. Le contenu original n'est pas modifié — toute reformulation et précision (en particulier côté back) vit dans [`spec-back.md`](./spec-back.md).
## But
Permettre à un administrateur Starseed de gérer un référentiel de **catégories** depuis l'interface admin du logiciel. Ces catégories seront utilisées plus tard pour classifier les tiers (clients, fournisseurs, prestataires).
## Accès
- **Depuis** : menu principal → **Administration** → entrée « Gestion des catégories »
- **Rôles autorisés** : **Admin uniquement** (Bureau / Compta / Commerciale / Usine n'ont **aucun** accès, ni lecture ni écriture).
## Navigation
L'écran est la page d'entrée du Module **Administration**. Titre de la page : « **Gestion des catégories** ».
- Affichage principal : un **datatable** listant toutes les catégories existantes.
- **Clic sur une ligne** → ouverture d'un **drawer** latéral en mode **consultation / modification** (cf. § Action « Consulter »).
- **Bouton « + Ajouter »** (en haut à droite du datatable) → ouverture d'un **drawer** en mode **création** (cf. § Action « Ajouter »).
- Pas d'onglet, pas de pagination explicite (volumétrie cible faible).
## Actions
| Action | Déclencheur | Comportement |
|---|---|---|
| **Ajouter** | Clic sur le bouton « + Ajouter » | Ouvre le drawer en mode création, formulaire vide. Validation → POST → la catégorie apparaît dans le datatable. |
| **Consulter** | Clic sur une ligne du datatable | Ouvre le drawer avec les champs pré-remplis en lecture (et passage en édition si l'utilisateur modifie un champ). |
| **Modifier** | Modification d'un champ dans le drawer ouvert en consultation | Validation → PATCH → la ligne du datatable se met à jour. |
> **Note V0** : la **suppression** n'était pas mentionnée dans la V0 client. Côté workflow MALIO, suite à la revue back (cf. `spec-back.md` § Q3), un soft delete est ajouté (corbeille logique). L'UI peut intégrer ce point lors d'une V1 — au M0 le bouton « Supprimer » n'est pas obligatoire, mais doit être facilement ajoutable.
## Formulaire — Champs
Le formulaire (drawer) contient **2 champs**, tous deux obligatoires :
| Champ | Type | Obligatoire | Contenu / valeur par défaut | Règle |
|---|---|---|---|---|
| **Nom** | Texte libre | **Oui** | vide à la création | Pas de règle métier détaillée en V0. Détails côté back : RG-1.02 / RG-1.03 / RG-1.04 (obligatoire, trim, longueur 2120). |
| **Type de catégorie** | Select | **Oui** | vide à la création | Le contenu du Select n'était pas précisé en V0. Décision back : entité de référence `CategoryType` séparée (RG-1.05 / RG-1.06). Le référentiel sera alimenté plus tard (cf. HP-1 dans `spec-back.md`). |
> **Note V0** : la V0 ne précisait ni si le `Type de catégorie` est un enum hardcodé ni si c'est une autre entité. Décision tranchée côté back avant découpe en tickets : **entité de référence** (`category_types`), table créée vide au M0.
## Permissions par rôle
| Rôle | Vue (`GET`) | Création (`POST`) | Édition (`PATCH`) | Suppression (`DELETE`) |
|---|---|---|---|---|
| **Admin** | ✅ | ✅ | ✅ | ✅ (soft delete — ajout post-V0) |
| Bureau | ❌ | ❌ | ❌ | ❌ |
| Compta | ❌ | ❌ | ❌ | ❌ |
| Commerciale | ❌ | ❌ | ❌ | ❌ |
| Usine | ❌ | ❌ | ❌ | ❌ |
→ Les rôles non-Admin ne voient **pas** l'entrée de menu et reçoivent **403** sur toute requête vers les endpoints `/api/categories/*` (cf. RG-1.01 dans `spec-back.md`).
## Composants UI à utiliser (Starseed / `@malio/layer-ui`)
- **Datatable** : `<MalioDataTable>` (avec colonnes `Nom` + `Type` + actions, tri par défaut sur Nom).
- **Drawer** : drawer latéral standard `@malio/layer-ui` (à confirmer côté front avec le composant exact).
- **Input texte** : `<MalioInputText>` pour le champ Nom.
- **Select** : `<MalioSelect>` pour le champ Type de catégorie, alimenté par `GET /api/category_types`.
- **Bouton** : `<MalioButton>` (« + Ajouter », « Enregistrer », « Annuler »).
- **Toasts succès / erreur** : standards via `useApi()`.
## Points laissés ouverts par la V0 (résolus côté back)
| # | Zone d'ombre V0 | Résolution (cf. `spec-back.md`) |
|---|---|---|
| 1 | Suppression non mentionnée | **Soft delete** ajouté (RG-1.12 + RG-1.13). UI peut ajouter le bouton plus tard. |
| 2 | Unicité du nom non précisée | **Unicité sur `(name, type)` case-insensitive**, parmi non-soft-deleted (RG-1.07). |
| 3 | Nature du `Type de catégorie` (enum vs entité) | **Entité de référence** `CategoryType` (table vide au M0, créée par migration). |
| 4 | Volumétrie & pagination | **300 max** → pagination front (`<MalioDataTable>`), pas de pagination serveur. Tri serveur `name ASC` par défaut. |
| 5 | Audit / traçabilité | Pattern `#[Auditable]` Starseed standard. Trace dans la table `audit_log` (qui / quoi / quand / diff). **Pas** de colonnes `created_by` / `updated_by` sur l'entité (cohérent avec User / Role dans Starseed). Historique consultable via `/api/audit-log?entityType=Category&entityId={id}`. |
| 6 | Référencement par d'autres entités | **Aucune FK entrante au M0.** Les modules Tiers (M-Clients / M-Fournisseurs / M-Prestas) ajouteront leur propre `category_id` plus tard. |
---
## 📦 Tickets Lesstime générés
**TaskGroup Lesstime** : `#22 — M0 — Gestion des catégories` (projet `ERP / Starseed`, projectId=6)
> Détail complet, table des tickets et action manuelle dans Lesstime → voir [`spec-back.md § Tickets Lesstime générés`](./spec-back.md#-tickets-lesstime-générés).
+3 -2
View File
@@ -5,8 +5,9 @@ APP_SECRET=CHANGE_ME_IN_PRODUCTION
DATABASE_URL="postgresql://starseed:CHANGE_ME@host.docker.internal:5432/starseed_prod?serverVersion=16&charset=utf8"
JWT_PASSPHRASE=CHANGE_ME_IN_PRODUCTION
JWT_COOKIE_SECURE=1
# HTTP en reseau local => cookie non secure
JWT_COOKIE_SECURE=0
JWT_TOKEN_TTL=86400
JWT_COOKIE_TTL=86400
CORS_ALLOW_ORIGIN='^https://starseed\.malio-dev\.fr$'
CORS_ALLOW_ORIGIN='^http://starseed\.malio-dev\.fr$'
+6
View File
@@ -70,6 +70,7 @@ help:
@printf " \033[36m%-28s\033[0m %s\n" "install-e2e-deps" "One-time : Chromium + libs systeme (sudo)"
@printf "\n \033[1;33mQualite code\033[0m\n"
@printf " \033[36m%-28s\033[0m %s\n" "php-cs-fixer-allow-risky" "Fix code style PHP (utilise par le pre-commit)"
@printf " \033[36m%-28s\033[0m %s\n" "php-cs-fixer-check" "Dry-run du fixer (CI / verif avant push)"
@printf "\n Plus de details : \033[4mREADME.md\033[0m, \033[4mCLAUDE.md\033[0m\n\n"
env-init:
@@ -258,6 +259,11 @@ php-cs-fixer-allow-risky:
@echo "Fixing files: $(FILES)"
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes $(FILES)
# Dry-run du fixer : echec si au moins un fichier n'est pas conforme.
# Utilise par la CI (Gitea pull_request) et avant un push manuel.
php-cs-fixer-check:
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes --dry-run --diff $(FILES)
test:
$(EXEC_PHP) php -d memory_limit="512M" vendor/bin/phpunit $(FILES)