Commit graph

351 commits

Author SHA1 Message Date
68216cf852 Merge pull request 'fix(ci): corrige le bit exécutable de gradlew et le fiabilise en CI' (#70) from fix/gradlew-executable-bit into main
Some checks failed
CI / lint (push) Successful in 2m11s
CI / build (push) Successful in 2m26s
CI / intent-service-test (push) Successful in 4m2s
CI / e2e (push) Successful in 7m29s
CI / test (push) Successful in 16m30s
CI / build-android (push) Failing after 4m54s
2026-09-06 15:27:32 +02:00
9c6feab064 fix(ci): corrige le bit exécutable de gradlew et le fiabilise en CI
All checks were successful
CI / build (push) Successful in 3m5s
CI / lint (push) Successful in 3m12s
CI / intent-service-test (push) Successful in 4m16s
CI / e2e (push) Successful in 9m3s
CI / test (push) Successful in 17m43s
CI / build-android (push) Has been skipped
Le tout premier run réel du job build-android (après le merge de #64)
échouait avec './gradlew: Permission denied' — gradlew était suivi par
git en 100644 (non exécutable) au lieu de 100755, probablement perdu au
commit initial du projet Android natif (#63) sur cette machine Windows.

Corrige le mode suivi par git (git update-index --chmod=+x) et ajoute un
chmod +x défensif juste avant l'exécution dans ci.yml, puisque ce bit
n'a aucun sens sous Windows et peut être reperdu silencieusement par un
futur commit sur cette machine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:09:21 +02:00
c0a07e8adc Merge pull request 'feat(ci): build, teste et empaquette un APK Android signé sur chaque push main' (#67) from feat/mobile-ci-android-apk into main
Some checks failed
CI / lint (push) Successful in 2m48s
CI / build (push) Successful in 2m51s
CI / intent-service-test (push) Successful in 4m9s
CI / e2e (push) Successful in 9m14s
CI / test (push) Successful in 17m18s
CI / build-android (push) Failing after 4m15s
2026-09-06 14:44:35 +02:00
92f364bc1f chore: relance la CI (9e collision infra sur le Postgres partagé)
All checks were successful
CI / build (push) Successful in 3m9s
CI / lint (push) Successful in 3m40s
CI / intent-service-test (push) Successful in 5m11s
CI / e2e (push) Successful in 7m29s
CI / test (push) Successful in 17m34s
CI / build-android (push) Has been skipped
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:26:31 +02:00
c662d02da6 chore: relance la CI (8e collision infra sur le Postgres partagé)
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 4m7s
CI / build (push) Successful in 4m13s
CI / test (push) Failing after 12m19s
CI / intent-service-test (push) Successful in 9m10s
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:12:46 +02:00
e9c0a61aaf Merge pull request 'feat(mobile): vérification automatique de mise à jour de version' (#69) from feat/mobile-update-check into main
Some checks failed
CI / lint (push) Successful in 1m23s
CI / build (push) Successful in 4m37s
CI / intent-service-test (push) Successful in 4m42s
CI / test (push) Failing after 18m10s
CI / e2e (push) Successful in 17m29s
2026-09-06 14:11:54 +02:00
7bafe39d72 chore: relance la CI (collision infra — violation de clé étrangère pendant le seed Postgres partagé)
All checks were successful
CI / lint (push) Successful in 3m37s
CI / build (push) Successful in 3m22s
CI / intent-service-test (push) Successful in 5m42s
CI / e2e (push) Successful in 10m15s
CI / test (push) Successful in 18m22s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 12:25:05 +02:00
907a68eb8a chore: relance la CI (run précédent annulé — cause inconnue, pas un échec de test)
Some checks failed
CI / lint (push) Successful in 4m25s
CI / intent-service-test (push) Successful in 4m43s
CI / build (push) Successful in 4m5s
CI / test (push) Failing after 11m5s
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 12:09:19 +02:00
b3581b2fb5 chore: relance la CI (7e collision infra sur le Postgres partagé)
Some checks failed
CI / build (push) Successful in 4m5s
CI / lint (push) Successful in 4m25s
CI / intent-service-test (push) Successful in 6m47s
CI / e2e (push) Successful in 13m15s
CI / test (push) Failing after 20m7s
CI / build-android (push) Has been skipped
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 12:06:53 +02:00
cf7e519883 chore: relance la CI (4e collision infra sur le Postgres partagé)
Some checks failed
CI / intent-service-test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 12:00:53 +02:00
85dd1b6255 chore: relance la CI (6e collision infra sur le Postgres partagé)
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 4m33s
CI / build (push) Successful in 3m50s
CI / intent-service-test (push) Successful in 5m28s
CI / test (push) Failing after 10m28s
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:54:51 +02:00
59352563c4 chore: relance la CI (3e collision infra — course d'upsert concurrente sur le seed Postgres partagé)
Some checks failed
CI / lint (push) Successful in 4m38s
CI / build (push) Successful in 3m28s
CI / intent-service-test (push) Successful in 6m34s
CI / test (push) Failing after 10m22s
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:49:11 +02:00
4f7a452924 chore: relance la CI (5e collision infra — course d'upsert concurrente sur le seed Postgres partagé)
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 3m34s
CI / build (push) Successful in 3m17s
CI / intent-service-test (push) Successful in 5m16s
CI / test (push) Failing after 8m39s
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:45:08 +02:00
f37a61266f chore: relance la CI (4e collision infra — cette fois sur le cache d'actions du runner)
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 4m7s
CI / build (push) Successful in 4m43s
CI / intent-service-test (push) Successful in 7m56s
CI / test (push) Failing after 11m11s
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:31:39 +02:00
09f6d426d3 chore: relance la CI (2e collision infra sur le Postgres partagé)
Some checks failed
CI / lint (push) Successful in 2m46s
CI / intent-service-test (push) Successful in 4m47s
CI / build (push) Successful in 4m31s
CI / e2e (push) Successful in 13m54s
CI / test (push) Failing after 17m45s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:29:51 +02:00
275cc2d500 chore: relance la CI (3e collision infra sur le Postgres partagé)
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 1m6s
CI / build (push) Failing after 35s
CI / e2e (push) Has been cancelled
CI / intent-service-test (push) Has been cancelled
CI / test (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:27:25 +02:00
a3a949ce08 chore: relance la CI (job test cassé par la course d'infra connue sur le Postgres partagé)
Some checks failed
CI / lint (push) Successful in 5m32s
CI / intent-service-test (push) Successful in 5m49s
CI / build (push) Successful in 4m29s
CI / test (push) Failing after 15m13s
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:08:16 +02:00
bf994c396a chore: relance la CI (2e collision infra sur le Postgres partagé, connexion coupée en cours de run)
Some checks failed
CI / lint (push) Successful in 2m15s
CI / build (push) Successful in 3m54s
CI / intent-service-test (push) Successful in 6m41s
CI / e2e (push) Successful in 16m11s
CI / test (push) Failing after 17m33s
CI / build-android (push) Has been skipped
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:04:32 +02:00
e90cadcc41 chore: relance la CI (#68)
Some checks failed
CI / lint (push) Successful in 4m15s
CI / intent-service-test (push) Successful in 5m49s
CI / build (push) Successful in 4m1s
CI / test (push) Failing after 10m15s
CI / e2e (push) Successful in 12m4s
Le job `test` de la run précédente a échoué sur une saturation du runner
(P1002 Prisma + timeout Docker), pas sur le code — le même job échouait
au même moment sur toutes les branches, `main` compris. Diff 100 % frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:02:53 +02:00
5d4d6be2dc feat(mobile): vérification automatique de mise à jour de version
Some checks failed
CI / lint (push) Successful in 6m25s
CI / intent-service-test (push) Successful in 5m1s
CI / build (push) Successful in 2m38s
CI / test (push) Failing after 11m54s
CI / e2e (push) Has been cancelled
Réutilise l'API Releases de Forgejo (publique, sans authentification) comme
source de vérité pour "quelle est la dernière version" — zéro nouvel
endpoint backend : useAppUpdateCheck compare App.getInfo().version
(@capacitor/app, nouvelle dépendance) au tag_name de
GET .../releases/latest, et pointe "Mettre à jour" vers l'URL stable de
téléchargement (issue #65).

Comparaison de version en logique pure (version-compare.ts), séparée du
hook effectful — testée isolément via un test de composant Cypress
(version-compare.cy.tsx, même patron que highlight-tech-steps.cy.tsx :
aucun montage, juste describe/it/expect sur des fonctions pures).

Natif uniquement (Capacitor.isNativePlatform()) : @capacitor/app et
@capacitor/preferences restent en import dynamique dans le hook, jamais
dans le bundle web (même patron que capacitor-token-store.ts) — vérifié
dans le build (deux petits chunks séparés, pas dans le bundle principal).

Vérification au démarrage à froid + à la reprise depuis l'arrière-plan
(appStateChange), avec un throttle de 6h et une mémorisation de la version
déjà rejetée dans @capacitor/preferences pour ne jamais re-harceler sur la
même version. Bannière discrète (pas une Dialog modale), bouton "Mettre à
jour" ouvrant le lien de téléchargement dans le navigateur système plutôt
qu'un flux PackageInstaller in-app.

Vérifié : pnpm lint, pnpm --filter web build, le test de composant (9/9),
et layout/smoke/planning-page e2e (18/18, la bannière étant un no-op sur
le web ne devait rien casser — confirmé).

Closes #66

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:52:35 +02:00
548cee721f chore: relance la CI (job test cassé par la course d'infra connue sur le Postgres partagé)
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 2m22s
CI / intent-service-test (push) Successful in 5m35s
CI / build (push) Successful in 4m17s
CI / test (push) Failing after 10m27s
CI / e2e (push) Has been cancelled
18 échecs éparpillés (timeouts 10s sur des suites sans rapport entre elles,
PrismaClientKnownRequestError, un mismatch d'ID façon ré-seed concurrent) sur
373 passing — signature exacte de la course documentée dans
ci-forgejo-runner-constraints (postgres partagé + contention RAM/CPU du
runner), pas un vrai échec de code : ce PR ne touche que ci.yml/le projet
Android/le README, rien que la suite Mocha exerce différemment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:45:05 +02:00
76a99f0e77 feat(ci): build, teste et empaquette un APK Android signé sur chaque push main
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / build (push) Successful in 3m21s
CI / lint (push) Successful in 3m31s
CI / intent-service-test (push) Successful in 4m51s
CI / e2e (push) Successful in 10m34s
CI / test (push) Failing after 14m37s
Nouveau job build-android dans ci.yml, gardé par needs: sur les 5 jobs
existants (lint/test/intent-service-test/build/e2e) et limité à main via
if: github.ref == 'refs/heads/main' — c'est ce qui garantit que l'APK n'est
empaqueté qu'une fois la suite de tests existante verte, pas juste construit
en parallèle d'elle.

APK signé avec un vrai keystore de release (jamais généré par la CI —
Android refuse une mise à jour signée par une clé différente de celle déjà
installée) : ajoute signingConfigs.release à build.gradle, lu depuis
l'environnement (ANDROID_KEYSTORE_PATH/_PASSWORD, ANDROID_KEY_ALIAS/_PASSWORD),
jamais codé en dur. *.jks/*.keystore désormais réellement ignorés par git
(étaient commentés dans le .gitignore généré par Capacitor).

README : documente les 5 secrets Forgejo requis et la génération du
keystore. Aucune suite de tests instrumentés Android n'existe (zéro code
natif au-delà de la coquille Capacitor) — "testée" est satisfait par le
gate sur la suite existante, pas par une suite native à inventer.

Risque documenté, pas résolu par avance : aucun build Android n'a jamais
tourné sur ce runner Forgejo auto-hébergé, contrairement aux 4 autres jobs.

Closes #64

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:27:55 +02:00
503781c95f feat(landing): lien de téléchargement de l'app Android (#65)
Some checks failed
CI / lint (push) Successful in 5m4s
CI / build (push) Successful in 3m38s
CI / intent-service-test (push) Successful in 6m41s
CI / test (push) Failing after 11m29s
CI / e2e (push) Successful in 14m56s
Sur la page vitrine `/`, sous la rangée de CTA du hero (pas un 3e bouton :
un compte reste nécessaire quelle que soit la plateforme), un lien discret
vers l'APK Android + une ligne d'aide.

- Cible : l'asset `batchcooking.apk` de la dernière release taguée Forgejo
  (`/releases/latest/download/…`, nom de fichier stable) — pas un artefact
  de CI. 404 tant que #64 + la correction de release.yml ne sont pas faits :
  état transitoire assumé, ce ticket n'ajoute que le lien.
- `mobileHint` : rappel honnête que c'est un .apk à sideloader, pas une
  distribution Play Store.
- i18n `landing.hero.mobileDownload` / `mobileHint` ; style discret dans
  landing-page.scss ; scénario Cypress de plus (présence + href du lien).

Vérifs : `pnpm build` + `pnpm lint` OK. `landing.feature` non lancé en
local (le hoist `tslib` de `@cypress/vite-dev-server` dans ce node_modules
est cassé par un `pnpm install --force` interrompu + un verrou concurrent
sur `@esbuild/win32-x64` ; `pnpm-lock.yaml` inchangé, la CI n'est pas
affectée) → couvert par le job `e2e` de la CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:26:50 +02:00
dcfb86d6c9 Merge pull request 'feat(mobile): projet natif Android Capacitor (#39)' (#63) from feat/capacitor-android-native into main
Some checks failed
CI / lint (push) Successful in 1m15s
CI / build (push) Successful in 1m9s
CI / intent-service-test (push) Successful in 3m31s
CI / e2e (push) Successful in 7m2s
CI / test (push) Failing after 14m7s
2026-09-06 10:09:13 +02:00
fd5841d045 feat(mobile): ajoute le projet natif Android Capacitor (#39)
All checks were successful
CI / lint (push) Successful in 4m59s
CI / build (push) Successful in 3m15s
CI / intent-service-test (push) Successful in 6m5s
CI / e2e (push) Successful in 11m41s
CI / test (push) Successful in 20m16s
Dernière étape du portage mobile #39 : génère et versionne le projet natif
Android (`npx cap add android`) que les phases 1 à 3 (jeton Bearer, passe
responsive, plomberie web) préparaient déjà.

- `apps/web/android/` — projet natif Capacitor 8.5. Permission `INTERNET`
  présente par défaut, `applicationId` = `fr.kyuno.batchcooking`.
- Thème natif aligné sur la palette « Mise en Place » (`_theme.scss`) :
  `colors.xml` redéfinit `colorPrimary`/`Dark`/`Accent` (basilic/vermillon)
  par-dessus les défauts Material de la lib capacitor-android ; barre de
  statut basilic foncé, fond de fenêtre « porcelaine » pour ne pas flasher
  blanc au démarrage à froid ; `postSplashScreenTheme` pour la transition.
- Icône adaptative + splash (clair et sombre, toutes densités) générés
  depuis `apps/web/assets/logo.svg` (monogramme « bC », géométrie seule, pas
  de `<text>`) via `@capacitor/assets` ; sources et commande de régénération
  documentées dans `apps/web/assets/README.md`. `@capacitor/assets` n'est pas
  ajouté aux dépendances (il tire `sharp`, hors `onlyBuiltDependencies`) —
  lancé via `npx`.
- `.gitignore` racine : ignore `/.idea/` (Android Studio ouvre désormais le
  dépôt comme projet). `biome.json` : exclut `apps/web/android` du lint.
- README : section « Application mobile (Android) » (prérequis + flux de
  build `build:capacitor` → `cap sync` → Android Studio).

Le bundle web copié sous `android/app/src/main/assets/public/` reste
git-ignoré (régénéré par `pnpm build:capacitor && npx cap sync android`).
Build de l'APK + test sur appareil : à finir dans Android Studio — la CI
self-hosted ne build pas Android.

Vérifs : `pnpm build` + `pnpm lint` OK ; `npx cap sync android` OK ; tous
les XML natifs bien formés.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 09:32:24 +02:00
ab1cf421d9 Merge pull request 'fix(recipes): rend StepDescription tolérant à un techSteps absent' (#62) from claude/practical-swartz-648594 into main
All checks were successful
CI / lint (push) Successful in 3m51s
CI / build (push) Successful in 3m55s
CI / intent-service-test (push) Successful in 5m9s
CI / e2e (push) Successful in 9m1s
CI / test (push) Successful in 17m55s
2026-09-06 09:24:28 +02:00
8e55f3d3f9 fix(recipes): rend StepDescription tolérant à un techSteps absent
All checks were successful
CI / lint (push) Successful in 4m3s
CI / build (push) Successful in 4m36s
CI / intent-service-test (push) Successful in 5m53s
CI / e2e (push) Successful in 8m56s
CI / test (push) Successful in 18m54s
StepDescription faisait `useState(techSteps)` puis `liveTechSteps.flatMap(...)` :
quand la prop `techSteps` arrivait `undefined`, `.flatMap` levait
`TypeError: Cannot read properties of undefined (reading 'flatMap')`, remontée
par Cypress comme une erreur applicative non capturée. Observé de façon
intermittente sur le parcours « modifier une recette » : après enregistrement,
`RecipeFormPage` navigue vers `/recettes/:id`, dont le détail se rendait parfois
avant que `techSteps` ne soit peuplé.

- `StepDescription` : `techSteps` devient optionnel et `liveTechSteps` retombe
  sur `[]` (dans le `useState` et le `useEffect` de resync), pour que tous les
  consommateurs (`.flatMap`, `splitDescriptionSegments`,
  `findTechStepForTemperature`, lookup `activeStepTechStep`) aient toujours un
  vrai tableau — même posture « tolérer la donnée manquante, ne pas planter »
  que le reste du composant.
- `recipe-form.ts` : le mock de la recette 7 envoyait une étape sans `techSteps`,
  contrairement au vrai `StepView` et à tous les autres mocks de la suite —
  corrigé en `techSteps: []`.
- `recipes.cy.ts` : nouveau test de non-régression déterministe — un payload
  d'étape sans `techSteps` doit rendre la description en texte brut. Vérifié
  qu'il échoue bien (`flatMap` undefined) sans le correctif du composant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 09:04:36 +02:00
713d6c520d Merge pull request 'feat(web): page admin de CRUD des didacticiels (#44)' (#61) from feat/admin-tour-page into main
All checks were successful
CI / lint (push) Successful in 1m17s
CI / build (push) Successful in 1m10s
CI / intent-service-test (push) Successful in 3m33s
CI / e2e (push) Successful in 6m57s
CI / test (push) Successful in 16m11s
2026-09-06 07:27:16 +02:00
b39105ac27 feat(web): page admin de CRUD des didacticiels (#44)
All checks were successful
CI / lint (push) Successful in 2m10s
CI / build (push) Successful in 2m19s
CI / intent-service-test (push) Successful in 4m41s
CI / e2e (push) Successful in 7m19s
CI / test (push) Successful in 16m20s
L'UI par-dessus l'API `/admin/api/tours*`. Nouvelle entrée
`/admin/didacticiels` dans `AdminLayout` (i18n `admin.nav.tours`), route
sous `RequireAdmin`+`AdminLayout` dans `App.tsx`.

`ToursPage.tsx` : liste des scénarios à gauche (nom, état activé/désactivé,
nombre d'étapes) ; à droite l'éditeur du scénario sélectionné —
- renommer, activer/désactiver ;
- éditer la liste d'étapes (route, cible via `<select>` alimenté par
  `GET /tours/targets`, titre, contenu), réordonner (↑/↓), ajouter,
  retirer ;
- « Enregistrer » → un seul `PATCH /:id` qui remplace tout (name/enabled +
  étapes rénumérotées `order` = index), erreurs API affichées telles quelles ;
- « Supprimer le scénario » (confirmation `window.confirm`) ;
- « Nouveau scénario » → formulaire clé + nom → `POST /`.

`admin-client.ts` gagne `getTourScenarios` / `getTourTargets` /
`createTourScenario` / `updateTourScenario` / `deleteTourScenario`.

Tests Cypress (`admin-tours.cy.ts`, 4) : liste (activés + désactivés,
nombre d'étapes) ; édition nom + réordonnancement d'étapes → un seul PATCH
avec la liste complète réordonnée ; création → POST `{key,name}` ;
suppression après confirmation → DELETE. 4/4 en local ; `pnpm build` +
`pnpm lint` verts.

Closes #44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 06:53:38 +02:00
d5ac6c4661 Merge pull request 'feat(web): moteur de tutoriel guidé driver.js (#42)' (#60) from feat/tour-engine into main
All checks were successful
CI / lint (push) Successful in 2m23s
CI / build (push) Successful in 2m29s
CI / intent-service-test (push) Successful in 4m5s
CI / e2e (push) Successful in 7m56s
CI / test (push) Successful in 16m29s
2026-09-06 06:53:23 +02:00
38fcf03417 feat(web): moteur de tutoriel guidé driver.js (#42)
All checks were successful
CI / lint (push) Successful in 3m55s
CI / build (push) Successful in 4m53s
CI / intent-service-test (push) Successful in 5m52s
CI / e2e (push) Successful in 10m30s
CI / test (push) Successful in 18m43s
Consomme le socle backend de #42/#44 (`GET /reference/tours`,
`GET/POST /profile/tours`). Deux tours : orientation générale (auto sur
`/`) et correction des métadonnées de recette (auto à l'ouverture du
premier popover de correction).

- Dépendance `driver.js` (~5 ko, MIT, zéro dépendance).
- `features/tour/tour-engine.ts` : wrapper vanilla autour de `driver.js` —
  gère ce que la lib ne fait pas seule : la navigation React Router entre
  étapes situées sur des routes différentes, et un `onFinish` unique
  (`"completed"` seulement si on atteint la fin, `"skipped"` pour la croix
  / Échap / clic sur l'overlay).
- `features/tour/TourContext.tsx` : `TourProvider` (charge scénarios +
  progression, keyed sur `user?.id` comme `ThemeContext`) et `useTour()`
  (`startTour(key)` / `isTourCompleted(key)` / `tours` / `isReady`).
  `isReady` est **calculé au render** contre l'id utilisateur chargé — pas
  stocké — pour qu'un changement de `user` le repasse à `false`
  immédiatement, avant qu'un effet enfant (l'auto-déclenchement) ne tourne
  sur des données de complétion périmées.
- `main.tsx` : `<TourProvider>` sous `ThemeProvider`.
- Attributs `data-tour-*` : liens de nav (`AppLayout`), grille / bouton `+`
  du 1er créneau / bouton Cuisiner (`PlanningPage`), page liste de courses
  (`ShoppingListPage`), description d'étape + section technique du popover
  (`StepDescription` / `TechStepCorrectionPopover`).
- Déclenchement auto : tour `general` sur `/` (`PlanningPage`, ref-gardé),
  tour `techStepMetadata` à `activeCorrection` non-null (`StepDescription`).
- Section « Revoir un tutoriel » dans `/parametres/preferences-utilisateur`
  — un `map` sur `tours`, un bouton « Revoir » par scénario.
- `tour.scss` re-skinne le popover `driver.js` sur les tokens du thème.
- i18n `tour.*` (chrome uniquement : boutons du popover, section « Revoir » ;
  le contenu des étapes vient de la base).

Tests Cypress (`tour.feature`, 5 scénarios) : auto-démarrage + élément
surligné pour un nouveau venu ; « Terminé » → complétion enregistrée
`skipped:false` ; croix → `skipped:true` ; un tour déjà vu ne se relance
pas ; « Revoir » le relance depuis les paramètres. Vérifié 5/5 en local.
`pnpm build` + `pnpm lint` verts.

Refs #42
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 06:17:06 +02:00
c21adeda72 Merge pull request 'feat(api): CRUD admin des scénarios de didacticiel (#44 — API)' (#59) from feat/admin-tour-crud into main
All checks were successful
CI / lint (push) Successful in 1m15s
CI / build (push) Successful in 1m11s
CI / intent-service-test (push) Successful in 3m35s
CI / e2e (push) Successful in 6m30s
CI / test (push) Successful in 16m13s
2026-09-06 06:16:49 +02:00
2beff139a3 feat(api): CRUD admin des scénarios de didacticiel (#44)
All checks were successful
CI / lint (push) Successful in 2m57s
CI / build (push) Successful in 2m59s
CI / intent-service-test (push) Successful in 4m22s
CI / e2e (push) Successful in 9m4s
CI / test (push) Successful in 17m32s
Deuxième volet backend du tutoriel guidé, sur le socle de #42/#44 : permet
à un admin de créer/éditer/supprimer les scénarios que
`GET /reference/tours` sert au moteur front.

`/admin/api/tours` (agrégateur `admin.routes.ts`, tout derrière
`requireAdmin`) :
- `GET /` — tous les scénarios (activés **et** désactivés) + étapes
  ordonnées.
- `GET /targets` — le catalogue `TourTarget` pour le sélecteur de cible de
  l'éditeur.
- `POST /` — crée un scénario vide (`key` + `name`) ; 409
  `TOUR_KEY_ALREADY_IN_USE` (nouveau code 4025) sur une clé prise.
- `PATCH /:id` — édite `name`/`enabled` **et remplace toute la liste
  d'étapes en bloc** (delete-all puis recreate avec `order` = index), même
  convention que `updateRecipe` pour les étapes d'une recette. `key`
  immuable. 404 `TOUR_NOT_FOUND` (id inconnu), 404 `TOUR_TARGET_NOT_FOUND`
  (nouveau code 4054, `targetKey` d'une étape hors catalogue — vérifié
  avant d'écrire, les étapes réelles restent intactes si la requête est
  rejetée).
- `DELETE /:id` — 204 ; les étapes et les `TourProgress` des utilisateurs
  cascadent.

Tests Mocha (`admin-tours.test.ts`) : 401 sans session admin sur chaque
route ; list (activés+désactivés) ; targets ; create→patch pour remplir
les étapes ; remplacement en bloc ; 409 clé dupliquée ; 404 cible
inconnue (sans toucher aux étapes existantes) ; 404 id inconnu ; delete +
cascade vérifiée ; un scénario désactivé disparaît de `/reference/tours`
mais reste dans la liste admin.

Refs #44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:39:37 +02:00
d1948954a6 Merge pull request 'feat(api): modèle de données + API des scénarios de didacticiel (#42/#44 — socle backend)' (#58) from feat/tour-scenarios-backend into main
All checks were successful
CI / lint (push) Successful in 2m52s
CI / build (push) Successful in 3m7s
CI / intent-service-test (push) Successful in 4m54s
CI / e2e (push) Successful in 8m31s
CI / test (push) Successful in 17m21s
2026-09-06 05:39:22 +02:00
cd8ce42df7 feat(api): modèle de données + API des scénarios de didacticiel (#42/#44)
All checks were successful
CI / lint (push) Successful in 1m0s
CI / build (push) Successful in 1m16s
CI / intent-service-test (push) Successful in 3m34s
CI / e2e (push) Successful in 6m38s
CI / test (push) Successful in 15m49s
Socle backend, entièrement testable en CI, pour le tutoriel guidé. Le
moteur front (driver.js), l'instrumentation `data-tour-*` et le CRUD admin
viendront ensuite.

Schéma (`schema.prisma` + migration `20260906120000_tour_scenarios`) — les
4 modèles de #44, qui révisent le registre statique prévu par #42 :
- `TourTarget` (`key` PK + `description`) : catalogue des cibles de
  surlignage valides, seedé, comme `TechStep`/`Unit`.
- `TourScenario` (`key` stable, `name`, `enabled`) : un scénario rejouable.
- `TourScenarioStep` (`order`, `route?`, `targetKey`, `title`, `content`) :
  `title`/`content` sont du **texte libre**, pas des clés i18n (#44 :
  contenu administrable → texte, comme le nom d'une recette).
- `TourProgress` (`userProfileId` + `scenarioId`, PK composite) : présence
  d'une ligne = scénario vu (terminé *ou* passé).

Seed (`reference-seed-data.ts`) : les 9 `TourTarget` correspondant aux
`data-tour-*` à poser côté front, + les 2 scénarios de #42 (`general`,
`techStepMetadata`) **créés seulement s'ils sont absents** (un admin en
devient propriétaire ensuite). `techStepMetadata` reste court pour
l'instant — #36 (popover qui se ferme en pleine sélection) est ouvert, v1
ne couvre que les gestes qui marchent déjà.

API :
- `GET /reference/tours` (public) → scénarios **activés** + étapes
  ordonnées, forme consommée directement par le futur `useTour()`.
- `GET /profile/tours` → `{ completedTourKeys: string[] }`.
- `POST /profile/tours/:tourKey/complete` → upsert idempotent ; corps vide
  = terminé, `{ skipped: true }` = passé. Émet `analytics.recordEvent`
  `tour.completed` / `tour.skipped` (le dashboard admin agrège déjà tout
  type d'`AnalyticsEvent` génériquement — analytics de #44 quasi gratuites).
  404 `TOUR_NOT_FOUND` (nouveau code 4053) sur une clé inconnue/désactivée.

`reset-db.ts` truncate les 4 nouvelles tables (reseedées juste après).

Tests Mocha : `GET /reference/tours` (scénarios + étapes ordonnées,
exclusion d'un scénario désactivé) ; `/profile/tours` GET+POST (upsert
idempotent, `skipped`, 404 clé inconnue, 404 tour désactivé, isolation
par profil). Pas de base Postgres locale → exécutés en CI.

Refs #42, #44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:08:43 +02:00
9174f80282 Merge pull request 'feat(web): page vitrine publique à la racine (#41)' (#57) from feat/landing-page into main
All checks were successful
CI / lint (push) Successful in 3m6s
CI / build (push) Successful in 3m18s
CI / intent-service-test (push) Successful in 4m29s
CI / e2e (push) Successful in 8m51s
CI / test (push) Successful in 16m12s
2026-09-06 05:06:18 +02:00
da7b077098 feat(web): page vitrine publique à la racine (#41)
All checks were successful
CI / lint (push) Successful in 2m1s
CI / build (push) Successful in 2m16s
CI / intent-service-test (push) Successful in 3m47s
CI / e2e (push) Successful in 7m45s
CI / test (push) Successful in 14m48s
Un visiteur déconnecté sur `/` voit désormais une vraie page de
présentation avant qu'on lui demande de se connecter, au lieu d'être
renvoyé directement vers `/login`.

- `RequireAuth.tsx` devient sensible au chemin : dans sa branche `!user`,
  `pathname === "/"` rend `<LandingPage />` ; toutes les autres routes
  protégées redirigent vers `/login` exactement comme avant. Aucun
  changement de la table de routes — `/` reste `<PlanningPage>` niché sous
  `RequireAuth`+`AppLayout`, et `AppLayout` (donc la sidebar) ne monte
  jamais pour la vitrine.
- `pages/landing/LandingPage.tsx` + `landing-page.scss` : hero (accroche +
  illustration SVG dessinée à la main dans le langage de `lucide-react`,
  zéro dépendance ni asset externe) + deux CTA, grille de 4
  fonctionnalités (icônes lucide), section « aperçu », CTA final. Copie
  entièrement sous le namespace i18n `landing.*`.
- `index.html` : favicon SVG (monogramme « bC ») + `<meta description>` —
  `/` est maintenant une page publique partageable.
- Les 3 captures d'écran réelles de la section « aperçu » sont servies
  statiquement depuis `public/landing/` (un fichier manquant retombe sur
  son `alt`, ne casse pas le build) ; elles restent à produire par un
  humain avec l'app lancée sur des données réelles — cf.
  `public/landing/README.md`.
- Tests Cypress `landing.feature` : vitrine visible déconnecté sur `/`,
  CTA vers `/login` et `/signup`, autre route protégée toujours redirigée,
  visiteur connecté voit bien le planning. `smoke.cy.ts` mis à jour pour
  tester la redirection depuis `/recettes` (puisque `/` ne redirige plus).

Closes #41
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:33:41 +02:00
961aa151d4 Merge pull request 'feat(web): plomberie Capacitor Android — dépendances, config, routeur, token store (portage mobile #39, phase 2)' (#56) from feat/capacitor-android-shell into main
All checks were successful
CI / lint (push) Successful in 2m41s
CI / build (push) Successful in 3m30s
CI / intent-service-test (push) Successful in 4m21s
CI / e2e (push) Successful in 7m44s
CI / test (push) Successful in 16m19s
2026-09-06 04:33:25 +02:00
0ba6b2f687 chore: relance la CI (run 109 : échec du checkout git côté runner, pas du code)
All checks were successful
CI / lint (push) Successful in 2m31s
CI / build (push) Successful in 2m40s
CI / intent-service-test (push) Successful in 4m18s
CI / e2e (push) Successful in 9m12s
CI / test (push) Successful in 16m48s
Les 5 jobs de la run 109 ont échoué tôt sur « git clone: some refs were not
updated » pendant l'étape de checkout, avant même `pnpm install`/`pnpm lint`.
Rien à voir avec cette branche : `pnpm install --frozen-lockfile`, `pnpm lint`
et `pnpm build` sont verts en local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:15:50 +02:00
ff36cefcde feat(web): plomberie web pour la coquille Capacitor Android (#39 phase 2)
Some checks failed
CI / lint (push) Failing after 1m59s
CI / build (push) Successful in 3m55s
CI / test (push) Has been cancelled
CI / intent-service-test (push) Has been cancelled
CI / e2e (push) Has been cancelled
Tout ce qui est vérifiable en CI de la phase 2 du portage mobile. La
génération du projet natif (`npx cap add android`), le build de l'APK et
le test sur appareil restent une étape manuelle (pas d'Android Studio ici)
— cf. le corps de l'issue #39.

- Dépendances : `@capacitor/core`, `@capacitor/android`,
  `@capacitor/preferences` (+ `@capacitor/cli` en dev), toutes en 8.5.x.
- `capacitor.config.ts` : `appId` `fr.kyuno.batchcooking`, `webDir: "dist"`.
- `apps/web/src/api/capacitor-token-store.ts` : implémentation `TokenStore`
  adossée à `@capacitor/preferences`, branchée via `setTokenStore()` (le
  point d'injection posé en phase 1). Importée dynamiquement dans
  `main.tsx` uniquement quand `Capacitor.isNativePlatform()` — le bundle
  web ne charge jamais `@capacitor/preferences` (vérifié : chunk séparé de
  0,5 ko jamais requis côté navigateur).
- `main.tsx` : `HashRouter` en natif (le bundle packagé sert `dist/`
  depuis l'origine locale de la WebView, sans réécriture des liens
  profonds vers `index.html`), `BrowserRouter` inchangé sur le web ;
  bootstrap async pour poser le token store avant le premier rendu.
- `build:capacitor` (`vite build --mode capacitor`) + `.env.capacitor.example` :
  le build packagé a besoin de l'URL absolue de l'API en dur (pas de
  "même origine que la page" possible).

Vérifié : `pnpm build` + `pnpm lint` verts ; Cypress smoke/layout/sidebar
14/14 en local (le refactor du bootstrap et le choix de routeur ne
cassent rien côté web).

Refs #39
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 03:56:15 +02:00
5ad9d644bd Merge pull request 'feat(web): passe responsive téléphone (portage mobile #39, phase 3)' (#55) from feat/mobile-responsive-pass into main
Some checks failed
CI / lint (push) Successful in 1m58s
CI / build (push) Successful in 2m15s
CI / intent-service-test (push) Successful in 4m20s
CI / e2e (push) Failing after 5m53s
CI / test (push) Successful in 14m59s
2026-09-06 03:56:00 +02:00
a8d86c7099 feat(web): passe responsive téléphone pour le contenu des pages (#39 phase 3)
All checks were successful
CI / lint (push) Successful in 2m28s
CI / build (push) Successful in 2m42s
CI / intent-service-test (push) Successful in 4m27s
CI / e2e (push) Successful in 8m24s
CI / test (push) Successful in 15m28s
Réutilise la philosophie déjà en place pour la sidebar (défilement
horizontal plutôt que compression illisible) au lieu d'un nouveau patron
par page. Tout est strictement additif sous 640px — le rendu desktop est
inchangé.

- `AppLayout.scss` : `.app-content` passe de `--space-xl` (32px) à
  `--space-md` (16px) sous 640px — 64px de largeur utile récupérés sur un
  écran de ~375px.
- `planning-page.scss` : la grille 7 jours défile déjà horizontalement
  (`overflow: auto` + `min-width: 62rem`) ; ce qui casse au tactile, c'est
  le `✕` de suppression d'une recette (`opacity: 0` hors survol) — épinglé
  visible + cible tactile agrandie sous 640px, idem le bouton « + ».
- `dialog.scss` : `.dialog-panel` garde une gouttière latérale et gagne en
  hauteur utile sous 640px (le `--space-2xl` vertical est trop cher sur un
  écran court).
- `recipe-picker-dialog.scss` : le split 2 colonnes forcé du catalogue
  (plus spécifique que le `@media` de recipes.scss) repasse à une colonne
  sous 640px.

Tests : `layout.cy.ts` gagne un bloc « page content under 640px » —
padding de `.app-content`, grille sur son propre scroll sans déborder la
page, `✕` visible au tactile. 10/10 en local.

Refs #39
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 03:24:13 +02:00
b0ac42441f Merge pull request 'feat(auth): jeton Bearer en repli du cookie de session (portage mobile #39, phase 1)' (#54) from feat/mobile-bearer-auth into main
All checks were successful
CI / lint (push) Successful in 2m29s
CI / build (push) Successful in 2m42s
CI / intent-service-test (push) Successful in 4m1s
CI / e2e (push) Successful in 8m25s
CI / test (push) Successful in 15m30s
2026-09-06 03:23:56 +02:00
5d7e4d6f6a chore: relance la CI (job test cassé par une course d'infra avec la run de main)
All checks were successful
CI / lint (push) Successful in 2m42s
CI / build (push) Successful in 2m45s
CI / intent-service-test (push) Successful in 4m22s
CI / e2e (push) Successful in 8m26s
CI / test (push) Successful in 15m35s
La run #104 a échoué au `beforeAll` racine (`seedReferenceData`, P2002 sur
`key`) parce que la run `main` #103 exécutait son propre job `test` au même
moment sur le conteneur Postgres `--name postgres` partagé. Runner idle,
aucune run concurrente cette fois. Code inchangé — build+lint verts en
local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 03:07:17 +02:00
1d8c001035 feat(auth): accepte un jeton Bearer en repli du cookie de session
Some checks failed
CI / lint (push) Successful in 2m6s
CI / build (push) Successful in 4m0s
CI / intent-service-test (push) Successful in 6m47s
CI / test (push) Failing after 8m58s
CI / e2e (push) Successful in 9m28s
Phase 1 du portage mobile (#39) : purement additif côté navigateur, qui
continue d'utiliser le cookie `session` httpOnly inchangé.

- `POST /auth/login` et `POST /auth/signup` renvoient aussi le JWT dans le
  corps de la réponse (`AuthResponse` = `SafeUserProfile` + `token`), en
  plus de poser le cookie. Un client dont l'origine ne peut pas recevoir
  un cookie `SameSite=Lax` (WebView Capacitor) le stocke et le renvoie.
- `requireAuth` lit le cookie en priorité, puis se rabat sur un en-tête
  `Authorization: Bearer <jwt>` quand le cookie est absent. Les deux
  mécanismes coexistent, aucun n'est retiré.
- `apps/web` : `ApiClient` gagne une abstraction `TokenStore`
  (`get`/`set`/`clear`) ; l'implémentation par défaut est un no-op
  navigateur (le cookie fait tout). `_request` ajoute l'en-tête
  `Authorization` quand un jeton existe ; `login`/`signup` le stockent,
  `logout`/`deleteAccount` l'effacent. `setTokenStore` est le point
  d'injection de la version Capacitor (`@capacitor/preferences`), branchée
  en phase 2 quand la dépendance sera ajoutée.

Tests Mocha : `requireAuth` authentifie via l'en-tête `Authorization` sans
cookie, rejette un jeton Bearer invalide ou un schéma non-Bearer ;
`login`/`signup` exposent bien le jeton dans le corps.

Refs #39
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 02:47:57 +02:00
f8ec373691 Merge pull request 'feat(sources): ajoute ohmonbento.com comme source de recettes importable' (#53) from feat/ohmonbento-recipe-source into main
Some checks failed
CI / lint (push) Successful in 3m25s
CI / build (push) Successful in 3m24s
CI / intent-service-test (push) Successful in 5m12s
CI / e2e (push) Failing after 11m18s
CI / test (push) Successful in 19m0s
2026-09-06 02:47:32 +02:00
802f3d251b chore: relance la CI (job test de la run précédente bloqué par une course d'infra)
All checks were successful
CI / lint (push) Successful in 1m55s
CI / build (push) Successful in 1m51s
CI / intent-service-test (push) Successful in 3m52s
CI / e2e (push) Successful in 6m18s
CI / test (push) Successful in 14m43s
Le job `test` de la run #101 a lu un état de base incohérent pendant que
le `seedReferenceData` d'une run `main` concurrente (run #100, restée
bloquée) tournait encore — décalage de +5 sur les ids `tech_step`. Le code
de cette branche est inchangé et vérifié localement (9/9 Mocha, sans DB).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 02:32:02 +02:00
2251aa93d7 feat(sources): ajoute ohmonbento.com comme source de recettes importable
Some checks failed
CI / lint (push) Successful in 4m8s
CI / build (push) Successful in 3m5s
CI / intent-service-test (push) Successful in 6m16s
CI / e2e (push) Successful in 8m15s
CI / test (push) Failing after 15m27s
6e adaptateur, le plus simple des scrapés : `fetchDetail`/`parse`
délèguent tels quels à `jsonLdRecipeAdapter` — une page recette porte un
`Recipe` WP Recipe Maker dans un `@graph` Yoast, déjà géré nativement par
`findRecipeNode`. `list()` : recherche WordPress `GET /?s={query}&paged={N}`
— **même URL que la requête soit vide (tout le catalogue) ou non**, aucun
branchement (contrairement à `750g.ts`). Scrape de cartes
`<div class="entry-thumb"><a href="/<slug>/"><img alt src>` — image dans
`src` (pas de lazy-load `data-src` ici), `href` relatif résolu contre
l'origine. Une page au-delà de la dernière renvoie une vraie 404 → fin de
liste propre (comme `marmiton.ts`). `rekeySourceError` dupliqué localement.

Enregistré dans `registerAllRecipeSources()`.

Tests : `ohmonbento.test.ts` (`stubFetchHtml`, pas de base) — scrape des
cartes, 404 = fin de liste, URL `/?s=` identique pour requête vide, query
+ page dans `/?s={q}&paged={n}`, erreurs re-labellisées `ohMonBento`,
`parse` sur un `@graph` Yoast + WPRM (`recipeYield` en tableau géré).
Vérifié localement, 9/9. + `sources-index.test.ts` couvre
l'enregistrement.

Docs : section `ohmonbento.ts` dans `backend-architecture.md`.

Closes #40

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 01:30:44 +02:00
8a29889175 Merge pull request 'feat(sources): ajoute quitoque.fr comme source de recettes importable' (#52) from feat/quitoque-recipe-source into main
Some checks failed
CI / lint (push) Successful in 2m40s
CI / build (push) Successful in 2m44s
CI / intent-service-test (push) Successful in 4m20s
CI / e2e (push) Successful in 12m15s
CI / test (push) Has been cancelled
2026-09-06 01:28:02 +02:00
4906accd85 feat(sources): ajoute quitoque.fr comme source de recettes importable
All checks were successful
CI / build (push) Successful in 3m28s
CI / lint (push) Successful in 3m33s
CI / intent-service-test (push) Successful in 4m54s
CI / e2e (push) Successful in 7m20s
CI / test (push) Successful in 15m33s
5e adaptateur, calqué sur `marmiton.ts` (le plus fin des wrappers
scrapés) : `fetchDetail`/`parse` délèguent tels quels à
`jsonLdRecipeAdapter` — une page recette quitoque.fr porte un `Recipe`
schema.org, seul élément d'un tableau JSON-LD de premier niveau, déjà géré
nativement par `findRecipeNode`. Seul `list()` est spécifique :
`GET /recettes?page=N` (page 1 implicite si curseur absent), scrape des
cartes `<a class="card__content_header" href="/recettes/…">` (image réelle
dans `data-src`, `src` n'étant qu'un placeholder lazy-load ; `href`
relatif résolu contre l'origine). Extraction agnostique à l'ordre des
attributs.

Pas de vraie recherche serveur sur le site → `query` est un filtre
sous-chaîne « best effort » sur les titres de la page courante (même
lacune assumée que `750g.ts`). Fin de liste = page sans aucune carte
(comme `marmiton.ts`). `rekeySourceError` dupliqué localement (750g /
manger-bouger en ont chacun une copie).

Enregistré dans `registerAllRecipeSources()`.

Tests : `quitoque.test.ts` (`stubFetchHtml`, pas de base) — scrape des
cartes, pagination `?page=N`, filtre par query, fin de liste, erreurs
re-labellisées `quitoque`, `parse` sur un JSON-LD tableau-de-premier-
niveau. Vérifié localement, 9/9. + `sources-index.test.ts` couvre
l'enregistrement.

Docs : section `quitoque.ts` dans `backend-architecture.md` (+ correction
de la ligne « n'enregistre que TheMealDB », devenue fausse).

Closes #37

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 01:11:48 +02:00