refactor: consolidate ui_freenove_allinone to hardware root level

- Move hardware/firmware/ui_freenove_allinone → hardware/ui_freenove_allinone
- All UI hardware now at hardware/ root level
- Single coherent hardware structure complete

Structure now:
- hardware/firmware/       ← Code source
- hardware/ui_freenove_allinone/ ← UI at root level
- hardware/projects/       ← Secondary projects
- hardware/shared/libs/    ← Shared libraries
- hardware/{bom,enclosure,wiring}/ ← Reference docs
This commit is contained in:
Clément SAILLANT
2026-03-01 19:10:27 +01:00
parent 5ad66a755c
commit 620b27de6d
267 changed files with 8490 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
manifest_id: zacus_v2_audio
version: 1
scenario_id: zacus_v2
description: "Sequence audio canon pour le scenario Zacus V2"
tracks:
- id: intro
title: "Alerte du professeur"
source: "game/prompts/audio/intro.md"
cues: "début du jeu, mise en immersion"
- id: incident
title: "Incident thermique"
source: "game/prompts/audio/incident.md"
cues: "après la première exploration, déclenchement de la porte verrouillée"
- id: accusation
title: "Accusation"
source: "game/prompts/audio/accusation.md"
cues: "début de la synthèse, demande de conclusion"
- id: solution
title: "Solution dévoilée"
source: "game/prompts/audio/solution.md"
cues: "fin, révélation et remerciement"
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
echo "=== Hardware Consolidation Checker ==="
echo ""
echo "Docs to move:"
ls -1 hardware/firmware/*.md 2>/dev/null | sed 's|^| - |' | grep -v README.md
echo ""
echo "Projects to reorganize:"
ls -d hardware/firmware/esp32_SLIC_phone 2>/dev/null && echo " - esp32_SLIC_phone"
ls -d hardware/firmware/hardware/RTC_SLIC_PHONE 2>/dev/null && echo " - RTC_SLIC_PHONE (nested)"
echo ""
echo "Would move to:"
echo " hardware/docs/ ← all .md files"
echo " hardware/projects/slic-phone/ ← phone variants"
echo " hardware/shared/libs/ ← shared libraries"
+18
View File
@@ -0,0 +1,18 @@
# Agent Contract Docs & Knowledge Copilot
## Scope
Docs agents govern `docs/`, `README.md`, `esp32_audio/README.md`, and any onboarding / briefing files (`.github/agents/`, `docs/_generated/`).
## Doit
- Avant toute mise à jour, lire `docs/AGENT_TODO.md` pour ne pas dupliquer les efforts de coordination.
- Refléter tout changement dorganisation ou de procédure dans `docs/TEST_SCRIPT_COORDINATOR.md`, `docs/TEST_COHERENCE_AUDIT_RUNBOOK.md`, et les AGENTS concernés.
- Maintenir `docs/_generated/COCKPIT_COMMANDS.md` en cohérence avec `tools/dev/cockpit_commands.yaml` via `python3 tools/dev/gen_cockpit_docs.py`.
- Documenter les nouveaux artefacts (gates/tests) dans la section adéquate du runbook et mentionner les chemins dans le reporting template.
- Mentionner toute dépendance externe (depuis `docs/RTOS_WIFI_HEALTH.md`, `docs/WIFI_RECOVERY_AND_HEALTH.md`, etc.) pour aider tranche QA.
## Artefacts
- Les docs nintègrent pas dartefacts binaires ; citez plutôt les dossiers `artifacts/` lorsque vous décrivez un protocole ou un gate réussi.
- Pour les instructions “TODO”, mettez à jour `docs/AGENT_TODO.md` (bloc 4/5) en parallèle des docs.
## Bonnes pratiques
- Chaque mise à jour de runbook ou guide doit inclure un rappel sur la nécessité de respecter `docs/AGENT_TODO.md` comme tracker unique.
+19
View File
@@ -0,0 +1,19 @@
# Agent Contract Firmware Copilot
## Scope
Firmware agents cover `esp32_audio/`, `ui/esp8266_oled/`, `ui/rp2040_tft/`, and shared `protocol/` code that drives the MCU binaries.
## Doit
- Lire puis mettre à jour `docs/AGENT_TODO.md` avant dagir (le tracker unique garde la trace de l’état de chaque gate).
- Respecter le contrat global (`AGENTS.md`) et les règles doutillage de `tools/dev/AGENTS.md` (gates/scripts centralisés, artefacts hors git).
- Sassurer que tout build se passe via `platformio.ini`/`build_all.sh` ou `Makefile fast-*`, et documenter les artefacts dans `docs/AGENT_TODO.md`.
- Pour les modifications de structure, ajouter un commentaire dans `docs/AGENT_TODO.md` + mentionner le commit dans `docs/TEST_SCRIPT_COORDINATOR.md`.
- Toujours signaler l’état des UI Link / LittleFS / I2S dans `docs/AGENT_TODO.md:6-14` lorsquon manipule ces composants.
## Reporting
- Chaque run doit produire des artefacts `artifacts/<phase>/<timestamp>` ; les chemins doivent figurer dans la case correspondante du TODO.
- Données hardware (logs, ports) restent hors git : référencer les fichiers dans `docs/AGENT_TODO.md` ou le rapport final.
## References
- Branche principale : `docs/SPRINT_RECOMMENDATIONS.md`, `README.md`, `protocol/ui_link_v2.md`
- Evidence tracker : `docs/TEST_SCRIPT_COORDINATOR.md`, `docs/TEST_COHERENCE_AUDIT_RUNBOOK.md`
+18
View File
@@ -0,0 +1,18 @@
# Agent Contract Tests Copilot
## Scope
Tests agents focus on files under `tools/dev/`, `tools/test/`, `esp32_audio/tests/`, and the artifacts/log directories (`artifacts/`, `logs/`).
## Doit
- Toujours lire/mettre à jour `docs/AGENT_TODO.md` pour annoncer les gates en cours ou bloqués (ex. smoke, stress, audit).
- Exécuter les calibrations/gates via les scripts officiels (`tools/dev/run_matrix_and_smoke.sh`, `tools/dev/run_smoke_tests.sh`, `tools/dev/run_stress_tests.py`, `tools/test/audit_coherence.py`), puis consigner les artefacts sous `artifacts/`.
- Tenir `docs/TEST_SCRIPT_COORDINATOR.md` à jour avec les résultats et les chemins d’évidence (section “Exécutions récentes” + reporting template).
- Valider les commandes cockpit via `tools/dev/cockpit_commands.yaml` et `docs/_generated/COCKPIT_COMMANDS.md` (regénération avec `python3 tools/dev/gen_cockpit_docs.py` si besoin).
- Mentionner les échecs réseau (HTTP/WebSocket/WiFi) dans `docs/AGENT_TODO.md` et `docs/TEST_SCRIPT_COORDINATOR.md` pour la coordination.
## Artefacts
- Les logs exigent `meta.json`, `git.txt`, `commands.txt`, `summary.md` (au minimum) ; vérifiez les répertoires demandés dans `docs/TEST_SCRIPT_COORDINATOR.md`.
- Pas de commit dartefacts ; les chemins doivent être mentionnés dans le TODO.
## Bonnes pratiques
- En cas de panne persistante (UI link, stress panic, scénario manquant), capturez les traces dans `artifacts/rc_live/<timestamp>` et notez la prochaine action dans `docs/AGENT_TODO.md`.
+199
View File
@@ -0,0 +1,199 @@
# Changelog Firmware
Tous les changements notables du firmware sont documentés ici.
Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/),
et ce projet adhère au [Semantic Versioning](https://semver.org/lang/fr/).
## [Unreleased]
### En cours (branche `hardware/firmware`)
#### Added
- Documentation complète architecture UML (diagrammes classes/séquence)
- Documentation état des lieux firmware (métriques, tests, structure)
- Documentation recommandations sprint (roadmap actions)
- Index navigation documentation (docs/INDEX.md)
- Skill local embarqué: `docs/skills/EMBEDDED_CPP_OO_ESP32S3_PIO_ARDUINO.md`
- Observabilité Freenove: boot report + runtime metrics (`audio_underrun`, `sd_errors`, `ui_fps_approx`, reset reason)
- Nouveau socle display: `DisplayHal` + `SpiBusManager` + hooks backend LGFX/TFT_eSPI
- Interfaces phase-2 préparatoires: `audio_pipeline`, `storage_prefetch`, `camera_pipeline`, `task_topology`, `ui/fx/fx_engine`
#### Changed
- Structure nettoyée : suppression symlink `esp32 → esp32_audio`
- Structure nettoyée : suppression duplication `hardware/firmware/hardware/`
- Paths corrigés dans AGENTS.md, READMEs, tools/qa docs
- Port mapping ESP8266 OLED : marker ready timeout étendu
- Port mapping ESP32/ESP8266 : swap 20-6.4.1/20-6.4.2
- Migration modulaire Freenove: `src/{app,ui,audio,storage,camera,drivers,system}` avec headers façade legacy
- UI stack: draw buffers PSRAM unifiés via `UI_DRAW_BUF_IN_PSRAM` + mapping legacy `FREENOVE_PSRAM_UI_DRAW_BUFFER`
- SCENE_WIN_ETAPE: logs périodiques enrichis (`fx_fps`, heap internal/psram, largest dma)
#### Fixed
- Documentation cohérente (esp32/ vs esp32_audio/ résolu)
---
## [0.9.0-beta] - 2026-02-15 (à venir après merge PR #86)
### Summary
**Refonte complète architecture firmware** avec introduction multi-MCU (ESP32 + ESP8266 OLED + RP2040 TFT), Story Engine V2, protocole UI Link v2, dual-channel audio.
### Added
#### Architecture multi-MCU
- ESP32 Audio Kit : Firmware principal C++ (18.6K lignes)
- ESP8266 NodeMCU : UI OLED SSD1306 (1.2K lignes)
- RP2040 Pico : UI TFT tactile LVGL (285 lignes)
- Protocol UI Link v2 : UART 19200 baud, frame-based ASCII
#### Story Engine V2
- Machine à états déclarative (ScenarioDef YAML)
- Event queue asynchrone (BTN, TOUCH, timers, conditions)
- Transitions implicites (timeout, LA detection, MP3 gate)
- Actions modulaires via ActionRegistry
- Story Apps pluggables : LaDetectorApp, Mp3GateApp, ScreenSceneApp, AudioPackApp
- StoryAppHost : Lifecycle apps (activate/deactivate par step)
#### Audio subsystem
- Dual-channel audio : Base (main) + Overlay (effects)
- AsyncAudioService : MP3 decoder ESP8266Audio
- FmRadioScanFx : Effets I2S (sweep, noise, beep) via arduino-audio-tools
- Mp3Player : Lecteur SD synchrone, TrackCatalog
- I2sJinglePlayer : RTTTL ringtones
- CodecES8388Driver : I2C/I2S hardware config
- Timeouts explicites : Base 30s, Overlay 5s
- Fallback FX si MP3 manquant/corrupt
#### Services layer
- AudioService : Dual-channel orchestration
- RadioService : Web streaming, station presets
- WifiService : AP mode, network management
- WebUiService : AsyncWebServer REST API
- InputService : Analog keypad, debounce, long-press
- ScreenSyncService : UI Link coordination, ScreenFrame serialization
- SerialRouter : USB terminal, debug commands
- CatalogScanService : SD track indexing
- LaDetectorRuntimeService : 440Hz detection
#### Controllers layer
- StoryControllerV2 : Story mode orchestration (142 lignes)
- Mp3Controller : SD player mode
- InputController : Keyboard/touch routing
- BootProtocolRuntime : Boot sequence (Serial, WiFi, Codec, I2C, SD)
#### UI Link Protocol v2
- Frame types : HELLO, ACK, KEYFRAME, STAT, BTN, TOUCH, PING, PONG
- Fields : mode, track, track_tot, vol, tune_ofs, tune_conf, uptime_ms, seq, scene
- CRC8 validation, 320 byte max line, 40 fields max
- Timeouts : HEARTBEAT_MS=1000, TIMEOUT_MS=1500
- Reconnection automatique : session counter, KEYFRAME resync
#### PlatformIO workspace
- 5 environments : esp32dev, esp32_release, esp8266_oled, ui_rp2040_ili9488, ui_rp2040_ili9486
- Shared protocol/ directory (ui_link_v2.h/md)
- Fast build Makefile targets : `make fast-esp32`, `make fast-ui-oled`, `make fast-ui-tft`
- `build_all.sh` : Build 5/5 environments
#### Testing & Automation
- `tools/dev/run_matrix_and_smoke.sh` : Build matrix + smoke tests hardware
- Port mapping by LOCATION (macOS CP2102 stable)
- Smoke gates stricts : panic/reboot markers → FAIL
- UI Link status gate : `connected==1` requis
- RC Live sessions : `./tools/dev/zacus.sh rc`
- Artifacts : `artifacts/rc_live/{timestamp}/` (logs + rapport _rc.md)
#### Documentation
- docs/QUICKSTART.md : Getting started dev
- docs/HW_NOW.md : Hardware status rapide
- docs/RC_FINAL_BOARD.md : Tests RC dashboard
- docs/protocols/PROTOCOL.md : Story Engine V2 format YAML
- protocol/ui_link_v2.md : Spéc protocole UART
- esp32_audio/WIRING.md : Câblage ESP32/UI
- ui/rp2040_tft/WIRING.md : Câblage TFT
### Changed
#### Runtime modes flexibles
- Mode STORY_V2 (défaut) : Quête ETAPE1 → unlock → ETAPE2 → MP3 libre
- Mode MP3_PLAYER : Lecteur SD pur (skip story)
- Mode RADIO : Streaming web (skip story + MP3)
- Sélection boot via config.h flags
#### Architecture refactor
- Séparation claire Controllers / Services / Drivers
- Event-driven architecture (non-blocking)
- Dependency injection via constructeurs
- Error handling via Result enum (Success/Timeout/Failed)
- Timeouts explicites partout (audio, UI link, network)
#### Build improvements
- PlatformIO cache : `$HOME/.platformio` (pas dans repo)
- Artifacts deterministes : `artifacts/rc_live/<timestamp>/`
- Environment overrides : ZACUS_REQUIRE_HW, ZACUS_WAIT_PORT, ZACUS_NO_COUNTDOWN, ZACUS_SKIP_SMOKE, ZACUS_ENV, ZACUS_FORCE_BUILD
### Deprecated
- Ancien Story Engine V1 (remplacé par V2)
- Protocole UI Link v1 (remplacé par v2 frames)
- Single-channel audio (remplacé par dual-channel)
- Hardcoded scénarios (remplacé par YAML loader)
### Removed
- Code legacy Story V1 (src/story_old/)
- Anciens tests manuels (remplacés par automation)
- Symlink `esp32 → esp32_audio` (confusion docs)
- Duplicate `hardware/firmware/hardware/` (artifacts)
### Fixed
- UI Link reconnection robuste (session counter, timeout recovery)
- Audio timeout fallback (MP3 bloqué → FX I2S)
- ESP8266 OLED heap stability (pooled buffers)
- Port mapping macOS CP2102 (by LOCATION stable)
- Panic detection smoke tests (strict gates)
### Security
- Aucun secret committé (tokens exclus .gitignore)
- WiFi credentials via config.h (local only, .gitignore)
- Web UI authentication stub (TODO prod)
---
## [0.1.0] - 2024-XX-XX
### Initial Release
- Proof-of-concept ESP32 Audio Kit
- Simple MP3 player SD
- Basic web UI control
- Single MCU architecture
---
## Conventions de versioning
- **MAJOR** : Breaking changes architecture/API
- **MINOR** : Ajout features backward compatible
- **PATCH** : Bug fixes backward compatible
- **Suffix** :
- `-alpha` : Fonctionnalités incomplètes, instable
- `-beta` : Fonctionnalités complètes, tests en cours
- `-rc.N` : Release candidate (pre-production)
- Rien : Release stable production
**Exemples :**
- `0.9.0-beta` : Refonte v2 beta (pre-production)
- `1.0.0-rc.1` : Release candidate 1 (pre-prod final)
- `1.0.0` : First stable production release
- `1.1.0` : Ajout feature (ex: nouveau runtime mode)
- `1.1.1` : Bug fix
- `2.0.0` : Breaking change (ex: nouveau protocole incompatible)
---
**Dernière mise à jour :** 15 février 2026
**Maintenu par :** Firmware team (@electron-rare)
+87
View File
@@ -0,0 +1,87 @@
# Structure & Configuration Fixes (2026-03-01)
## Summary
Fixed the double `hardware/firmware` directory duplication and locked `cockpit.sh` to use ONLY PlatformIO CLI.
## Changes Made
### 1. Directory Structure Correction
**Problem:** Double nesting `hardware/firmware/hardware/firmware/` contained actual sources
**Solution:**
- Moved `esp32_audio/`, `ui_freenove_allinone/`, `ui/` to correct location
- Removed empty `hardware/firmware/hardware/` directory
- New clean structure: `hardware/firmware/{esp32_audio/, ui_freenove_allinone/, ui/, lib/, ...}`
### 2. platformio.ini Fixes
**Problem:** Paths referenced `hardware/firmware` double, wrong `libs` path
**Changes:**
```ini
# Before:
src_dir = hardware/firmware
build_flags = -Ihardware/firmware/esp32_audio/src -I$PROJECT_DIR/hardware/libs/story
# After:
src_dir = .
build_flags = -I./esp32_audio/src -I$PROJECT_DIR/lib/story
```
**All corrections:**
- `src_dir`: `hardware/firmware``.` (current directory only)
- Include paths: Removed all `hardware/firmware` double-references
- Library paths: `libs/``lib/` (correct singular form)
- Relative includes: `./lib/story` instead of complex relative paths
### 3. cockpit.sh Locked to PIO-Only
**Problem:** Complex script with hardware detection, artifact management, build orchestration
**Solution:** Rewritten as pure PlatformIO CLI wrapper
**Removed:**
- ❌ Hardware auto-detection
- ❌ build_all.sh integration
- ❌ Artifact collection/logging
- ❌ Complex FX/graphics verification
- ❌ 600+ lines of legacy code
**Added:**
- ✅ Direct pio run / pio run -t upload / pio device monitor
- ✅ Environment listing (envs) and port detection (ports)
- ✅ Locked contract: "ONLY uses PIO (PlatformIO CLI)"
- ✅ Clean 113-line script with help, examples, clear contract
**New usage:**
```bash
./tools/dev/cockpit.sh build [env] # pio run -e $env
./tools/dev/cockpit.sh flash [env] # pio run -e $env -t upload
./tools/dev/cockpit.sh monitor [env] # pio device monitor -e $env
./tools/dev/cockpit.sh go [env] # Build + Flash + Monitor combo
./tools/dev/cockpit.sh envs # List available environments
./tools/dev/cockpit.sh ports # List connected serial ports
./tools/dev/cockpit.sh help # Show help with contract
```
## Files Modified
1. `platformio.ini` - Fixed all path references
2. `tools/dev/cockpit.sh` - Rewrote to locked PIO-only contract
## Verification
```bash
# Verify directory structure
ls -1d hardware/firmware/{esp32_audio,ui_freenove_allinone,ui,lib}
# Output: All 4 dirs present ✓
# Verify cockpit.sh works
./tools/dev/cockpit.sh envs
# Output: Lists 9+ environments ✓
# Verify no more hardware/firmware double-refs
grep -c 'hardware/firmware' platformio.ini
# Output: 0 ✓
```
## Notes
- Build may show missing headers (e.g., `boot_protocol_runtime.h`) - this is a separate issue in source files, not related to the path corrections
- cockpit.sh now has a strict locked contract: zero custom logic, zero shell complexity
- All future cockpit.sh changes must use ONLY `pio` commands, no custom detection/scripts
## Rollback (if needed)
Original files backed up in git; use `git checkout hardware/firmware/platformio.ini tools/dev/cockpit.sh` to revert.
+111
View File
@@ -0,0 +1,111 @@
# Générer un scénario STORY V2 (firmware ESP32)
Ce guide explique **comment créer/modifier un scénario** pour le firmware ESP32 sans toucher au moteur C++.
## Prérequis
Depuis ce dossier:
```bash
cd hardware/firmware/esp32_audio
```
## 1) Créer (ou dupliquer) un scénario YAML
Point de départ recommandé:
- template: `docs/protocols/story_specs/templates/scenario.template.yaml`
- exemple existant: `docs/protocols/story_specs/scenarios/default_unlock_win_etape2.yaml`
- schéma de référence: `docs/protocols/story_specs/schema/story_spec_v1.yaml`
Crée un nouveau fichier dans `docs/protocols/story_specs/scenarios/`, par exemple:
`docs/protocols/story_specs/scenarios/mon_scenario.yaml`
Exemple prêt à l'emploi dans le repo:
- `docs/protocols/story_specs/scenarios/example_unlock_express.yaml`
- `docs/protocols/story_specs/scenarios/example_unlock_express_done.yaml`
## 2) Définir la structure minimale
Dans le YAML, renseigne au minimum:
- `id` (identifiant unique)
- `version` (utiliser la version supportée par le générateur; dans ce repo les exemples sont en `2`)
- `initial_step`
- `app_bindings`
- `steps`
- `transitions`
Règle pratique:
- chaque `target_step_id` doit pointer vers une étape existante,
- les IDs doivent être uniques,
- les transitions doivent permettre datteindre une fin logique (`DONE` ou équivalent).
## 3) Valider le scénario
```bash
make story-validate
```
Cette commande vérifie:
- la conformité de structure,
- les IDs / références,
- la cohérence des transitions,
- les bindings des mini-apps.
Corrige les erreurs jusqu’à obtenir une validation propre.
## 4) Générer le code C++
```bash
make story-gen
```
Le générateur produit les fichiers utilisés par le runtime V2:
- `src/story/generated/scenarios_gen.h`
- `src/story/generated/scenarios_gen.cpp`
- `src/story/generated/apps_gen.h`
- `src/story/generated/apps_gen.cpp`
## 5) Compiler et flasher
```bash
pio run -e esp32dev
```
Puis upload selon ton port série:
```bash
pio run -e esp32dev -t upload --upload-port /dev/ttyUSB0
```
## 6) Tester côté série
Dans le moniteur série, commandes utiles:
- `STORY_V2_ENABLE ON`
- `STORY_V2_LIST`
- `STORY_V2_SCENARIO <id>`
- `STORY_V2_STATUS`
- `STORY_V2_EVENT <name>`
- `STORY_V2_VALIDATE`
## Workflow court (copier-coller)
```bash
cd hardware/firmware/esp32_audio
make story-validate
make story-gen
pio run -e esp32dev
```
## Conseils pour éviter les blocages
- Commence simple: 3 à 4 étapes max au début.
- Najoute quun seul nouveau type d’événement à la fois.
- Teste chaque transition clé au moniteur série (`STORY_V2_EVENT ...`).
- Garde un scénario « de secours » valide dans `docs/protocols/story_specs/scenarios/`.
+233
View File
@@ -0,0 +1,233 @@
# Git Write Operations Implementation - Summary
## ✅ Implementation Complete
### Objective
Save token credits by executing git write operations through shell scripts instead of agents, with:
- Safe guards (require `ZACUS_GIT_ALLOW_WRITE=1`)
- Confirmation prompts (skippable with `ZACUS_GIT_NO_CONFIRM=1`)
- Evidence integration
- Cockpit registry management
---
## 📦 Livrables
### 1. Implementation in agent_utils.sh
**File**: `tools/dev/agent_utils.sh`
Added functions:
```bash
git_write_check() # Validates safeguards (env vars + confirmation)
git_add() # Stage files (requires ZACUS_GIT_ALLOW_WRITE=1)
git_commit() # Commit changes (requires ZACUS_GIT_ALLOW_WRITE=1)
git_stash() # Stash working tree (requires ZACUS_GIT_ALLOW_WRITE=1)
git_push() # Push to remote (requires ZACUS_GIT_ALLOW_WRITE=1)
```
### 2. Implementation in cockpit.sh
**File**: `tools/dev/cockpit.sh`
Extended `run_git()` function to support:
- Read actions: status, diff, log, branch, show ✓ (existing)
- Write actions: add, commit, stash, push ✓ (NEW)
All git actions dispatch through `cockpit.sh git <action> [args...]`
### 3. Cockpit Command Registry
**File**: `tools/dev/cockpit_commands.yaml`
Added entries:
```yaml
- id: git-add # Stage files
- id: git-commit # Commit staged changes
- id: git-stash # Stash working tree
- id: git-push # Push to remote
```
Also updated main `git` entry to list all supported actions (read + write)
### 4. Generated Documentation
**File**: `docs/_generated/COCKPIT_COMMANDS.md`
Auto-generated from cockpit_commands.yaml:
- Full command registry table (ID, Description, Entrypoint, Args, Runbook, Evidence)
- Includes all 4 new git write commands
- Runbook references to Git Operations Policy
### 5. Core Documentation Updates
**File**: `docs/TEST_SCRIPT_COORDINATOR.md`
Enhanced "Git Operations Policy" section with:
- Clear separation: read vs. write operations
- Safeguard explanation (`ZACUS_GIT_ALLOW_WRITE=1`)
- Confirmation prompt details
- Usage examples for common scenarios
- Script integration patterns
### 6. Complete Git Write Operations Guide
**File**: `docs/GIT_WRITE_OPS.md` (NEW)
Comprehensive guide covering:
- Quick start (3 steps)
- Architecture diagram
- All files modified
- Environment variables reference
- 3 usage patterns (interactive, CI/CD, evidence)
- Safeguards & error handling
- Evidence integration details
- 5 use cases with code examples
- Testing instructions
- Limitations & design notes
- Token credit savings benefits
- Troubleshooting guide
### 7. Usage Examples Script
**File**: `tools/dev/examples_git_write_ops.sh` (NEW)
8 runnable examples:
1. Stage files with git add
2. Commit with confirmation prompt (interactive)
3. Commit silently for CI/CD
4. Stash changes
5. Push to remote
6. Git operations with evidence tracking
7. Error case - missing ZACUS_GIT_ALLOW_WRITE
8. Release automation pattern
---
## 🔧 How It Works
### Safeguards
```
User wants to run: ./tools/dev/cockpit.sh git add .
run_git() dispatches to git_add()
git_write_check() validates:
├─ ZACUS_GIT_ALLOW_WRITE=1 ? (fail if not)
└─ ZACUS_GIT_NO_CONFIRM=1 ? (skip prompt if yes)
If confirmation needed: prompt user
git_cmd add . (records in evidence, executes)
```
### Evidence Logging
```
When evidence_init() is called:
EVIDENCE_COMMANDS = artifacts/<phase>/<timestamp>/commands.txt
Then git_cmd() automatically appends to EVIDENCE_COMMANDS:
git add .
git commit -m "message"
git push origin main
```
---
## 📊 Feature Matrix
| Feature | Read Ops | Write Ops |
|---------|----------|-----------|
| Environment var required | No | `ZACUS_GIT_ALLOW_WRITE=1` |
| Confirmation prompt | No | Yes (unless `ZACUS_GIT_NO_CONFIRM=1`) |
| Logged in evidence | Yes | Yes |
| Cockpit.sh supported | Yes | Yes |
| Registry documented | Yes | Yes |
| Examples provided | Yes | Yes |
---
## 🚀 Usage Quick Reference
### Read operations (no restrictions)
```bash
./tools/dev/cockpit.sh git status
./tools/dev/cockpit.sh git diff
./tools/dev/cockpit.sh git log 10
./tools/dev/cockpit.sh git branch
./tools/dev/cockpit.sh git show HEAD
```
### Write operations (require ZACUS_GIT_ALLOW_WRITE=1)
```bash
export ZACUS_GIT_ALLOW_WRITE=1
# Interactive (with confirmation)
./tools/dev/cockpit.sh git add src/
./tools/dev/cockpit.sh git commit -m "My changes"
# Automated (skip prompts)
export ZACUS_GIT_NO_CONFIRM=1
./tools/dev/cockpit.sh git commit -m "Auto commit"
./tools/dev/cockpit.sh git push origin main
```
### In scripts with evidence
```bash
source tools/dev/agent_utils.sh
evidence_init "my_phase"
export ZACUS_GIT_ALLOW_WRITE=1
export ZACUS_GIT_NO_CONFIRM=1
git_add src/
git_commit -m "staged changes"
# Verify logged commands
cat "$EVIDENCE_COMMANDS"
```
---
## 📋 Verification Checklist
- ✅ agent_utils.sh: git_add(), git_commit(), git_stash(), git_push() implemented
- ✅ agent_utils.sh: git_write_check() validates safeguards
- ✅ cockpit.sh: run_git() extended with write actions
- ✅ cockpit_commands.yaml: 4 new git-* entries added
- ✅ docs/_generated/COCKPIT_COMMANDS.md: Auto-regenerated successfully
- ✅ docs/TEST_SCRIPT_COORDINATOR.md: Git Operations Policy section updated
- ✅ docs/GIT_WRITE_OPS.md: Complete guide created
- ✅ examples_git_write_ops.sh: 8 usage examples provided
- ✅ cockpit_registry.py: Fixed YAML parsing (handles multiline content)
- ✅ gen_cockpit_docs.py: Python 3.8+ compatible (uses List[str] not list[str])
- ✅ PyYAML: Installed in .venv for robust YAML parsing
- ✅ All functions tested and verified working
---
## 💾 Token Credit Savings
By executing git operations via cockpit shell scripts instead of agents:
| Operation | Without Script | Via Cockpit |
|-----------|----------------|------------|
| git add + commit | ~4-5 agent calls | 1 shell command |
| Release: tag + push | ~6-8 agent calls | 1-2 shell commands |
| Per operation | 50-200 tokens | ~5-10 tokens |
**Estimated savings**: 80-90% reduction in tokens for git workflows
---
## 🔗 References
- **Main guide**: [docs/GIT_WRITE_OPS.md](docs/GIT_WRITE_OPS.md)
- **Policy**: [docs/TEST_SCRIPT_COORDINATOR.md#git-operations-policy](docs/TEST_SCRIPT_COORDINATOR.md#git-operations-policy)
- **Command registry**: [docs/_generated/COCKPIT_COMMANDS.md](docs/_generated/COCKPIT_COMMANDS.md)
- **Implementation**: `tools/dev/agent_utils.sh`, `tools/dev/cockpit.sh`
- **Examples**: `tools/dev/examples_git_write_ops.sh`
---
## Next Steps (Optional Enhancements)
- [ ] Add git branch/tag creation commands
- [ ] Git merge/rebase operations with safeguards
- [ ] Automated diff preview before commit
- [ ] Git workflow templates (release, hotfix, feature)
- [ ] Integration with CI/CD systems (GitHub Actions, etc.)
+121
View File
@@ -0,0 +1,121 @@
# Quick Reference: Git Write Ops via Cockpit
## 🎯 One-Liner Commands
### Basic Operations
```bash
# Enable write mode
export ZACUS_GIT_ALLOW_WRITE=1
# Stage all changes
./tools/dev/cockpit.sh git add .
# Commit with message
./tools/dev/cockpit.sh git commit -m "Your commit message"
# Stash changes
./tools/dev/cockpit.sh git stash save "description"
# Push to remote
./tools/dev/cockpit.sh git push origin main
```
### CI/CD Automation (no prompts)
```bash
export ZACUS_GIT_ALLOW_WRITE=1
export ZACUS_GIT_NO_CONFIRM=1
./tools/dev/cockpit.sh git add .
./tools/dev/cockpit.sh git commit -m "Auto-commit"
./tools/dev/cockpit.sh git push origin main
```
### With Evidence Tracking
```bash
source tools/dev/agent_utils.sh
evidence_init "my_phase"
export ZACUS_GIT_ALLOW_WRITE=1
export ZACUS_GIT_NO_CONFIRM=1
git_add .
git_commit -m "changes"
# All logged to: $EVIDENCE_COMMANDS
```
## 📖 Read Operations (no guards needed)
```bash
./tools/dev/cockpit.sh git status
./tools/dev/cockpit.sh git diff
./tools/dev/cockpit.sh git log 20
./tools/dev/cockpit.sh git branch
./tools/dev/cockpit.sh git show HEAD
```
## 🔒 Safeguard Errors
```bash
# Error: Missing ZACUS_GIT_ALLOW_WRITE
$ ./tools/dev/cockpit.sh git add .
[AGENT][FAIL] Git write operation requires ZACUS_GIT_ALLOW_WRITE=1
# Solution:
export ZACUS_GIT_ALLOW_WRITE=1
./tools/dev/cockpit.sh git add .
```
## 🧪 Test Examples
```bash
./tools/dev/examples_git_write_ops.sh 1 # Stage files
./tools/dev/examples_git_write_ops.sh 2 # Commit interactive
./tools/dev/examples_git_write_ops.sh 3 # Commit silent
./tools/dev/examples_git_write_ops.sh 4 # Stash
./tools/dev/examples_git_write_ops.sh 5 # Push
./tools/dev/examples_git_write_ops.sh 6 # With evidence
./tools/dev/examples_git_write_ops.sh 7 # Error demo
./tools/dev/examples_git_write_ops.sh 8 # Release pattern
```
## 📚 Full Documentation
- **Main Guide**: `docs/GIT_WRITE_OPS.md`
- **Policy**: `docs/TEST_SCRIPT_COORDINATOR.md#git-operations-policy`
- **Registry**: `docs/_generated/COCKPIT_COMMANDS.md`
- **Implementation**: `GIT_WRITE_OPS_IMPLEMENTATION.md`
## 🚀 Key Points
1. **Always set**: `export ZACUS_GIT_ALLOW_WRITE=1` for write ops
2. **Get prompted**: By default (unless `ZACUS_GIT_NO_CONFIRM=1`)
3. **Check status**: `./tools/dev/cockpit.sh git status`
4. **In scripts**: Use `git_add()`, `git_commit()`, etc. from `agent_utils.sh`
5. **Evidence**: All commands auto-logged when evidence is initialized
## 💡 Pro Tips
- Use `git status` before committing to verify changes
- Test with `ZACUS_GIT_NO_CONFIRM=1` in isolated branch first
- Review `docs/GIT_WRITE_OPS.md` for full examples
- Run `examples_git_write_ops.sh` to see all patterns
- Check `EVIDENCE_COMMANDS` file after operations
## 🔗 Related Commands
```bash
# Other cockpit commands
./tools/dev/cockpit.sh rc # RC live gate
./tools/dev/cockpit.sh flash # Flash firmware
./tools/dev/cockpit.sh build # Build all
./tools/dev/cockpit.sh ports # Watch ports
# Git read operations
./tools/dev/cockpit.sh git log 10
./tools/dev/cockpit.sh git diff HEAD~1
./tools/dev/cockpit.sh git branch -vv
```
+395
View File
@@ -0,0 +1,395 @@
# 🚀 Git Write Operations via Cockpit - COMPLETED
## Status: ✅ FULLY IMPLEMENTED & TESTED
All git write operations (add, commit, stash, push) are now available via `cockpit.sh` with safeguards and evidence integration.
---
## 📋 What's New
### New Features
- **git add** - Stage files via `./tools/dev/cockpit.sh git add <pathspec>`
- **git commit** - Commit via `./tools/dev/cockpit.sh git commit -m "<msg>"`
- **git stash** - Stash changes via `./tools/dev/cockpit.sh git stash [action]`
- **git push** - Push commits via `./tools/dev/cockpit.sh git push [remote] [branch]`
### Safeguards
- **Required**: `ZACUS_GIT_ALLOW_WRITE=1` for all write operations
- **Confirmation prompt**: Shown by default (skip with `ZACUS_GIT_NO_CONFIRM=1`)
- **Evidence integration**: All commands logged to `commands.txt`
### Token Credit Savings
- **Before**: 4-8 agent calls for git operations (200-400+ tokens)
- **After**: 1-2 shell commands via cockpit (5-10 tokens)
- **Savings**: ~95% reduction in token usage
---
## 📦 Deliverables (7 items)
### 1️⃣ Core Implementation
**File**: `tools/dev/agent_utils.sh`
Added functions:
```bash
git_write_check() # Guard: requires ZACUS_GIT_ALLOW_WRITE=1 + confirmation
git_add(pathspec) # Stage files
git_commit(-m "msg") # Commit changes
git_stash([action]) # Stash working tree
git_push([remote] [br]) # Push to remote
```
**Verification**: ✅ All functions tested and working
---
### 2️⃣ Cockpit Dispatcher Update
**File**: `tools/dev/cockpit.sh`
Extended `run_git()` function to handle:
- Read actions (status, diff, log, branch, show) - existing ✓
- Write actions (add, commit, stash, push) - NEW ✓
Uses same guard pattern for all write operations.
**Verification**: ✅ Dispatcher properly routes all actions
---
### 3️⃣ Command Registry
**File**: `tools/dev/cockpit_commands.yaml`
Added entries:
```yaml
- id: git-add
- id: git-commit
- id: git-stash
- id: git-push
```
Each entry includes: description, args, runbook reference, safety notes.
**Verification**: ✅ Registry validated and parsed
---
### 4️⃣ Generated Documentation
**File**: `docs/_generated/COCKPIT_COMMANDS.md`
Auto-generated table with:
- 18 total cockpit commands (5 new git entries)
- ID | Description | Entrypoint | Args | Runbook | Evidence
- All git commands with safeguard notes
**Verification**: ✅ Successfully generated from YAML registry
---
### 5️⃣ Policy Documentation
**File**: `docs/TEST_SCRIPT_COORDINATOR.md`
Updated "Git Operations Policy" section with:
- Clear read vs. write command lists
- Safeguard explanation (`ZACUS_GIT_ALLOW_WRITE=1`)
- Confirmation flow description
- Usage examples:
- Interactive (with confirmation)
- Silent (for CI/CD)
- With evidence tracking
- Script integration patterns
**Verification**: ✅ Section expanded and documented
---
### 6️⃣ Complete Usage Guide
**File**: `docs/GIT_WRITE_OPS.md` (8.9 KB)
Comprehensive manual covering:
- Quick start (3 steps)
- Architecture diagram
- Implementation details
- 4 environment variables reference
- 3 usage patterns (interactive, CI/CD, evidence)
- Safeguards & error handling with examples
- Evidence integration workflow
- 5 real-world use cases with code
- Testing instructions
- Design notes & limitations
- Token credit savings calculation
- Troubleshooting guide
**Verification**: ✅ Complete guide created
---
### 7️⃣ Usage Examples Script
**File**: `tools/dev/examples_git_write_ops.sh` (5.9 KB)
8 runnable examples:
```bash
./tools/dev/examples_git_write_ops.sh 1 # Stage files
./tools/dev/examples_git_write_ops.sh 2 # Commit with prompt
./tools/dev/examples_git_write_ops.sh 3 # Commit silently
./tools/dev/examples_git_write_ops.sh 4 # Stash changes
./tools/dev/examples_git_write_ops.sh 5 # Push to remote
./tools/dev/examples_git_write_ops.sh 6 # With evidence tracking
./tools/dev/examples_git_write_ops.sh 7 # Error case
./tools/dev/examples_git_write_ops.sh 8 # Release automation
```
Each example is documented and runnable.
**Verification**: ✅ Script created and ready to run
### 8️⃣ Implementation Summary
**File**: `GIT_WRITE_OPS_IMPLEMENTATION.md`
Summary document covering:
- Implementation objectives
- Complete deliverables checklist
- Architecture explanation
- Feature matrix
- Quick reference
- Verification checklist
- Token credit savings
- Next steps for enhancement
**Verification**: ✅ Summary document complete
---
## 🔍 Quick Verification
All components verified working:
```bash
✅ Helper functions loaded
✅ All git write functions exist
✅ cockpit.sh has git dispatcher
✅ Registry has 5 git entries
✅ Documentation generated
✅ Guide created
✅ Examples provided
✅ PyYAML installed for robust YAML parsing
```
---
## 🎯 Key Features
### 1. Safeguarded Operations
```bash
# Fails without ZACUS_GIT_ALLOW_WRITE=1
$ ./tools/dev/cockpit.sh git add .
[AGENT][FAIL] Git write operation requires ZACUS_GIT_ALLOW_WRITE=1
# Works with safeguard
$ export ZACUS_GIT_ALLOW_WRITE=1
$ ./tools/dev/cockpit.sh git add .
[WARN] git add .
Continue? (y/N): _
```
### 2. CI/CD Automation
```bash
export ZACUS_GIT_ALLOW_WRITE=1
export ZACUS_GIT_NO_CONFIRM=1
./tools/dev/cockpit.sh git commit -m "automated commit"
./tools/dev/cockpit.sh git push origin main
```
### 3. Evidence Integration
```bash
source tools/dev/agent_utils.sh
evidence_init "release"
git_add src/
git_commit -m "release changes"
git_push origin main
# All logged to: artifacts/release/<timestamp>/commands.txt
```
---
## 📊 Usage Statistics
### File Changes
- Modified: 3 files (agent_utils.sh, cockpit.sh, cockpit_commands.yaml, TEST_SCRIPT_COORDINATOR.md)
- Created: 4 files (GIT_WRITE_OPS.md, examples_git_write_ops.sh, GIT_WRITE_OPS_IMPLEMENTATION.md, this README)
- Generated: 1 file (docs/_generated/COCKPIT_COMMANDS.md)
### Code Lines
- agent_utils.sh: +65 lines (git write operations)
- cockpit.sh: +15 lines (extended dispatcher)
- cockpit_commands.yaml: +20 lines (new entries + descriptions)
- Documentation: 1000+ lines (guides + examples)
### Dependencies
- PyYAML: Installed (robust YAML parsing)
- Python 3.8+: Compatible (uses List[str] not list[str])
- Shell: Standard bash (no new dependencies)
---
## 🚀 Getting Started
### 1. Read the Guide
```bash
cat docs/GIT_WRITE_OPS.md
```
### 2. Try an Example
```bash
./tools/dev/examples_git_write_ops.sh 1
```
### 3. Use in Your Script
```bash
export ZACUS_GIT_ALLOW_WRITE=1
export ZACUS_GIT_NO_CONFIRM=1
./tools/dev/cockpit.sh git add .
./tools/dev/cockpit.sh git commit -m "changes"
```
---
## 📖 Documentation Map
```
docs/
├── GIT_WRITE_OPS.md ← Start here (usage guide)
├── TEST_SCRIPT_COORDINATOR.md ← Policy section updated
└── _generated/
└── COCKPIT_COMMANDS.md ← Auto-generated registry table
tools/dev/
├── agent_utils.sh ← Git write functions
├── cockpit.sh ← Dispatcher updated
├── cockpit_commands.yaml ← Registry with new entries
├── examples_git_write_ops.sh ← 8 runnable examples
├── cockpit_registry.py ← Fixed YAML parser
└── gen_cockpit_docs.py ← Doc generator (Python 3.8+)
Root:
└── GIT_WRITE_OPS_IMPLEMENTATION.md ← Full implementation summary
```
---
## ✅ Acceptance Criteria
All requirements met:
-**Git write actions**: add, commit, stash, push implemented
-**Safeguards**: Require `ZACUS_GIT_ALLOW_WRITE=1`
-**Confirmation prompts**: Shown by default, skippable
-**Evidence integration**: Commands logged to evidence
-**Cockpit integration**: All via `cockpit.sh` dispatcher
-**Registry updated**: cockpit_commands.yaml with new entries
-**Docs generated**: Automated from registry
-**Documentation**: Complete guide + examples
-**Testing**: Smoke tests passed
-**Token savings**: ~95% reduction for git workflows
---
## 🔗 Related Commands
### Cockpit Git Commands
```bash
./tools/dev/cockpit.sh git status # Read
./tools/dev/cockpit.sh git diff # Read
./tools/dev/cockpit.sh git log 10 # Read
./tools/dev/cockpit.sh git branch # Read
./tools/dev/cockpit.sh git show HEAD # Read
./tools/dev/cockpit.sh git add . # Write (needs safeguard)
./tools/dev/cockpit.sh git commit -m "" # Write (needs safeguard)
./tools/dev/cockpit.sh git stash # Write (needs safeguard)
./tools/dev/cockpit.sh git push origin # Write (needs safeguard)
```
### Other Cockpit Commands
```bash
./tools/dev/cockpit.sh rc # RC live gate
./tools/dev/cockpit.sh flash # Flash firmware
./tools/dev/cockpit.sh build # Build all
./tools/dev/cockpit.sh audit # Full audit
...
```
---
## 💡 Design Principles
1. **Save tokens**: Shell scripts instead of agent calls
2. **Safe by default**: Guards prevent accidental modifications
3. **Auditable**: All operations logged to evidence
4. **Automated**: CI/CD friendly with bypass options
5. **Documented**: Complete guides and examples
6. **Testable**: Smoke tests + usage examples
7. **Integrated**: Single cockpit.sh dispatcher
8. **Extensible**: Easy to add more git commands
---
## 🎓 Learning Resources
### For Users
- Start: `docs/GIT_WRITE_OPS.md`
- Examples: `./tools/dev/examples_git_write_ops.sh`
- Policy: `docs/TEST_SCRIPT_COORDINATOR.md`
### For Developers
- Implementation: `tools/dev/agent_utils.sh` (git functions)
- Dispatcher: `tools/dev/cockpit.sh` (run_git function)
- Registry: `tools/dev/cockpit_commands.yaml` (metadata)
- Summary: `GIT_WRITE_OPS_IMPLEMENTATION.md`
---
## 🐛 Debugging
### "Git write operation requires ZACUS_GIT_ALLOW_WRITE=1"
→ Set: `export ZACUS_GIT_ALLOW_WRITE=1`
### "Cancelled by user"
→ Confirm with `y` or set: `export ZACUS_GIT_NO_CONFIRM=1`
### "Command not found" for git_add, etc.
→ Source agent_utils: `source tools/dev/agent_utils.sh`
### Check registry loading
→ Run: `python3 tools/dev/gen_cockpit_docs.py`
---
## 📝 Notes
- All git write operations require explicit `ZACUS_GIT_ALLOW_WRITE=1` for safety
- Confirmation prompts are shown unless suppressed for CI/CD
- All operations are recorded in evidence when evidence tracking is initialized
- PyYAML is installed for robust YAML parsing (fallback simple parser available)
- Python 3.8+ compatibility ensured (using `List[str]` instead of `list[str]`)
---
## 🎉 Summary
Git write operations are now available via cockpit.sh with:
- **Full safeguards** (requires explicit environment variables)
- **Evidence integration** (all commands logged)
- **Token savings** (~95% reduction)
- **Complete documentation** (guides + examples)
- **Cockpit integration** (single entry point)
Ready for production use! 🚀
---
**Last Updated**: 2026-02-16
**Status**: ✅ COMPLETE & TESTED
**Ready for**: Production, CI/CD, Evidence-tracked scripts
+26
View File
@@ -0,0 +1,26 @@
Configuration PlatformIO centralisee (hardware/firmware)
=========================================================
Ce depot utilise un seul `platformio.ini` a la racine.
Environnements:
- esp32dev
- esp32_release
- esp8266_oled
- ui_rp2040_ili9488
- ui_rp2040_ili9486
Commandes utiles:
- Build complet: `pio run`
- Build cible: `pio run -e <env>`
- Upload: `pio run -e <env> -t upload --upload-port <PORT>`
- Monitor: `pio device monitor -e <env> --port <PORT>`
- Matrix locale: `./build_all.sh`
- Upload via cockpit: `./tools/dev/cockpit.sh flash`
- Override ports: `ZACUS_PORT_ESP32=... ZACUS_PORT_ESP8266=... ZACUS_PORT_RP2040=...`
- Envs flash: `ZACUS_FLASH_ESP32_ENVS="esp32dev esp32_release"` etc.
Rappels:
- `src_dir = esp32_audio/src` (sources ESP32 par defaut)
- Chaque env isole ses sources via `build_src_filter`
- Header protocole partage: `protocol/ui_link_v2.h` via `-Iprotocol`
+27
View File
@@ -0,0 +1,27 @@
# STOP REQUIRED
Date: 2026-03-01 11:49 Europe/Paris
## Trigger condition
- Build/test regression not fixed quickly.
## What happened
- Firmware build succeeded for `freenove_esp32s3_full_with_ui`.
- Flash succeeded on `/dev/cu.usbmodem5AB90753301`.
- Serial validation failed at boot with repeated panic/reboot loop before scenario checks.
## Serial evidence
- `E (...) I2S: i2s_alloc_dma_buffer(741): Error malloc dma buffer`
- `E (...) I2S: i2s_driver_install(2027): I2S set clock failed`
- `Guru Meditation Error: Core 1 panic'ed (LoadProhibited)`
- Backtrace includes:
- `Audio::Audio(...)` (`ESP32-audioI2S/src/Audio.cpp:231`)
- `AudioManager::ensurePlayer()` (`hardware/firmware/ui_freenove_allinone/src/audio/audio_manager.cpp:395`)
- `AudioManager::begin()` (`hardware/firmware/ui_freenove_allinone/src/audio/audio_manager.cpp:412`)
- `setup()` (`hardware/firmware/ui_freenove_allinone/src/app/main.cpp:7212`)
## Impact
- Cannot complete runtime verification for `SCENE_GOTO SCENE_CREDIT` / `UI_SCENE_STATUS` because system reboots during startup.
## Requested next action
- Triage/fix audio boot allocation regression first, then rerun scene-credit serial validation.
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+62
View File
@@ -0,0 +1,62 @@
# Zacus Scratch Frontend V2 (greenfield)
Frontend "from scratch" pour designer un scenario Zacus avec des blocs type Scratch,
generer du YAML, puis appeler les endpoints Story V2.
## Objectif
- Ne pas reutiliser le frontend legacy du repo.
- Construire un socle neuf, orientee OSS/libre.
- Garder la compatibilite API avec `STORY_V2_WEBUI.md`.
## Stack OSS retenue
Validation rapide faite le 2026-03-01 (npm registry):
| Brique | Version | Licence | Role |
| --- | --- | --- | --- |
| blockly | 12.4.1 | Apache-2.0 | editeur blocs |
| yaml | 2.8.2 | ISC | serialisation YAML |
| zod | 4.3.6 | MIT | validation locale de schema |
| ajv | 8.18.0 | MIT | JSON schema (next step) |
| @monaco-editor/react | 4.7.0 | MIT | vue YAML |
Note: `scratch-gui` / `scratch-vm` sont AGPL-3.0-only, donc non retenus ici pour garder un front permissif.
## Demarrage
```bash
cd frontend-scratch-v2
npm install
npm run dev
```
Option API:
```bash
VITE_STORY_API_BASE=http://<esp_ip>:8080 npm run dev
```
## Fonctionnalites livrees
- Workspace Blockly avec bloc `step`.
- Generation YAML live depuis les blocs.
- Preview/edit YAML dans Monaco.
- Actions runtime:
- GET `/api/story/list`
- GET `/api/story/status`
- POST `/api/story/validate`
- POST `/api/story/deploy`
## Structure
- `src/components/BlocklyDesigner.tsx`: editeur blocs + generation YAML.
- `src/components/RuntimeControls.tsx`: actions HTTP Story V2.
- `src/lib/scenario.ts`: mapping blocs -> document scenario -> YAML.
- `src/types.ts`: types du document scenario.
## Prochain lot recommande
1. Ajouter un mapping complet `steps + transitions` (FSM) avec validation croisee.
2. Ajouter import/export Blockly JSON.
3. Ajouter tests unitaires de generation YAML et smoke E2E API mock.
+23
View File
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend-scratch-v2</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "frontend-scratch-v2",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"ajv": "^8.18.0",
"blockly": "^12.4.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"yaml": "^2.8.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+110
View File
@@ -0,0 +1,110 @@
.app-shell {
min-height: 100vh;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.app-header h1 {
margin: 0;
font-size: 1.6rem;
}
.app-header p {
margin: 0.25rem 0 0;
opacity: 0.82;
}
.app-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.panel {
border: 1px solid #d0d7de;
border-radius: 0.75rem;
padding: 0.75rem;
background: #ffffff;
display: flex;
flex-direction: column;
gap: 0.75rem;
min-height: 300px;
}
.panel h2 {
margin: 0;
font-size: 1.05rem;
}
.blockly-host {
height: 48vh;
min-height: 340px;
border: 1px solid #d0d7de;
border-radius: 0.5rem;
overflow: hidden;
}
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.toolbar input {
min-width: 220px;
border: 1px solid #d0d7de;
border-radius: 0.4rem;
padding: 0.4rem 0.6rem;
}
.toolbar button {
border: 1px solid #d0d7de;
border-radius: 0.4rem;
background: #f6f8fa;
padding: 0.4rem 0.7rem;
}
.status-line {
font-size: 0.85rem;
opacity: 0.9;
}
.status-line.error {
color: #b42318;
}
.runtime-panel {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}
.runtime-panel .actions {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.runtime-panel pre {
margin: 0;
min-height: 120px;
border: 1px solid #d0d7de;
border-radius: 0.5rem;
background: #f6f8fa;
padding: 0.6rem;
overflow: auto;
font-size: 0.8rem;
}
@media (max-width: 980px) {
.app-grid {
grid-template-columns: 1fr;
}
.runtime-panel {
grid-template-columns: 1fr;
}
}
+43
View File
@@ -0,0 +1,43 @@
import { useState } from 'react';
import Editor from '@monaco-editor/react';
import { BlocklyDesigner } from './components/BlocklyDesigner';
import { RuntimeControls } from './components/RuntimeControls';
import './App.css';
function App() {
const [yaml, setYaml] = useState('');
return (
<div className="app-shell">
<header className="app-header">
<h1>Zacus Scratch Frontend V2</h1>
<p>Nouvelle base from scratch, orientee Blockly + YAML + API Story V2.</p>
</header>
<main className="app-grid">
<section className="panel">
<h2>Designer blocs</h2>
<BlocklyDesigner onYamlChange={setYaml} />
</section>
<section className="panel">
<h2>YAML runtime</h2>
<Editor
height="40vh"
defaultLanguage="yaml"
value={yaml}
onChange={(value) => setYaml(value ?? '')}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 13,
}}
/>
<RuntimeControls yaml={yaml} />
</section>
</main>
</div>
);
}
export default App;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,228 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import * as Blockly from 'blockly';
import 'blockly/blocks';
import {
buildScenarioFromBlocks,
scenarioToYaml,
validateScenarioDocument,
} from '../lib/scenario';
import type { ScenarioStep } from '../types';
const TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Scenario',
colour: '#3b82f6',
contents: [
{
kind: 'block',
type: 'zacus_step',
},
],
},
],
};
let blocksRegistered = false;
function ensureCustomBlocks(): void {
if (blocksRegistered) {
return;
}
Blockly.defineBlocksWithJsonArray([
{
type: 'zacus_step',
message0: 'step %1 scene %2',
args0: [
{
type: 'field_input',
name: 'STEP_ID',
text: 'STEP_NEW',
},
{
type: 'field_input',
name: 'SCENE_ID',
text: 'SCENE_NEW',
},
],
previousStatement: null,
nextStatement: null,
colour: 210,
},
]);
blocksRegistered = true;
}
function readScenarioSteps(workspace: Blockly.WorkspaceSvg): ScenarioStep[] {
const steps: ScenarioStep[] = [];
const seenBlocks = new Set<string>();
const collectChain = (startBlock: Blockly.Block): void => {
let cursor: Blockly.Block | null = startBlock;
while (cursor) {
if (seenBlocks.has(cursor.id)) {
break;
}
seenBlocks.add(cursor.id);
if (cursor.type === 'zacus_step') {
steps.push({
stepId: cursor.getFieldValue('STEP_ID') ?? '',
sceneId: cursor.getFieldValue('SCENE_ID') ?? '',
});
}
cursor = cursor.getNextBlock();
}
};
for (const block of workspace.getTopBlocks(true)) {
if (block.type === 'zacus_step') {
collectChain(block);
}
}
if (steps.length === 0) {
for (const block of workspace.getAllBlocks(false)) {
if (block.type === 'zacus_step') {
collectChain(block);
}
}
}
return steps;
}
function addStarterBlocks(workspace: Blockly.WorkspaceSvg): void {
const first = workspace.newBlock('zacus_step');
first.setFieldValue('STEP_U_SON_PROTO', 'STEP_ID');
first.setFieldValue('SCENE_U_SON_PROTO', 'SCENE_ID');
first.initSvg();
first.render();
first.moveBy(40, 40);
const second = workspace.newBlock('zacus_step');
second.setFieldValue('STEP_LA_DETECTOR', 'STEP_ID');
second.setFieldValue('SCENE_LA_DETECTOR', 'SCENE_ID');
second.initSvg();
second.render();
if (first.nextConnection && second.previousConnection) {
first.nextConnection.connect(second.previousConnection);
}
}
type BlocklyDesignerProps = {
onYamlChange: (yaml: string) => void;
};
export function BlocklyDesigner({ onYamlChange }: BlocklyDesignerProps) {
const hostRef = useRef<HTMLDivElement | null>(null);
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
const [scenarioId, setScenarioId] = useState('zacus_v2_new');
const [steps, setSteps] = useState<ScenarioStep[]>([]);
const [copyInfo, setCopyInfo] = useState('');
useEffect(() => {
if (!hostRef.current) {
return;
}
ensureCustomBlocks();
const workspace = Blockly.inject(hostRef.current, {
toolbox: TOOLBOX,
trashcan: true,
grid: {
spacing: 20,
length: 3,
colour: '#d0d7de',
snap: true,
},
zoom: {
controls: true,
wheel: true,
startScale: 0.95,
},
});
workspaceRef.current = workspace;
addStarterBlocks(workspace);
const onChange = () => {
setSteps(readScenarioSteps(workspace));
setCopyInfo('');
};
workspace.addChangeListener(onChange);
onChange();
return () => {
workspace.removeChangeListener(onChange);
workspace.dispose();
workspaceRef.current = null;
};
}, []);
const generated = useMemo(() => {
const scenarioDocument = buildScenarioFromBlocks(scenarioId, steps);
return {
yaml: scenarioToYaml(scenarioDocument),
validation: validateScenarioDocument(scenarioDocument),
};
}, [scenarioId, steps]);
useEffect(() => {
onYamlChange(generated.yaml);
}, [generated.yaml, onYamlChange]);
const handleReset = () => {
if (!workspaceRef.current) {
return;
}
workspaceRef.current.clear();
addStarterBlocks(workspaceRef.current);
setSteps(readScenarioSteps(workspaceRef.current));
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(generated.yaml);
setCopyInfo('YAML copied.');
} catch {
setCopyInfo('Copy failed.');
}
};
return (
<>
<div className="toolbar">
<input
type="text"
value={scenarioId}
onChange={(event) => setScenarioId(event.target.value)}
aria-label="scenario id"
placeholder="scenario id"
/>
<button type="button" onClick={handleReset}>
Reset blocks
</button>
<button type="button" onClick={handleCopy}>
Copy YAML
</button>
</div>
<div ref={hostRef} className="blockly-host" />
<div
className={`status-line ${generated.validation.ok ? '' : 'error'}`}
role="status"
>
{generated.validation.ok
? `Ready - ${steps.length} step(s) detected. ${copyInfo}`
: `Invalid draft: ${generated.validation.error}`}
</div>
</>
);
}
@@ -0,0 +1,127 @@
import { useState } from 'react';
const DEFAULT_BASE_URL =
(import.meta.env.VITE_STORY_API_BASE as string | undefined) ??
'http://localhost:8080';
type RuntimeControlsProps = {
yaml: string;
};
type ResponseView = {
label: string;
payload: string;
};
async function readResponsePayload(response: Response): Promise<string> {
const text = await response.text();
if (!text) {
return '<empty response>';
}
try {
return JSON.stringify(JSON.parse(text), null, 2);
} catch {
return text;
}
}
export function RuntimeControls({ yaml }: RuntimeControlsProps) {
const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL);
const [result, setResult] = useState<ResponseView>({
label: 'No request yet',
payload: '',
});
const [busy, setBusy] = useState(false);
const runRequest = async (
label: string,
path: string,
init?: RequestInit,
) => {
setBusy(true);
try {
const response = await fetch(`${baseUrl}${path}`, init);
const payload = await readResponsePayload(response);
setResult({
label: `${label} -> HTTP ${response.status}`,
payload,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setResult({
label: `${label} -> FAILED`,
payload: message,
});
} finally {
setBusy(false);
}
};
return (
<div className="runtime-panel">
<div>
<div className="toolbar">
<input
type="text"
value={baseUrl}
onChange={(event) => setBaseUrl(event.target.value.trim())}
aria-label="story api base url"
placeholder="http://esp32:8080"
/>
</div>
<div className="actions">
<button
type="button"
disabled={busy}
onClick={() => runRequest('list', '/api/story/list')}
>
List
</button>
<button
type="button"
disabled={busy}
onClick={() => runRequest('status', '/api/story/status')}
>
Status
</button>
<button
type="button"
disabled={busy || yaml.trim().length === 0}
onClick={() =>
runRequest('validate', '/api/story/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ yaml }),
})
}
>
Validate
</button>
<button
type="button"
disabled={busy || yaml.trim().length === 0}
onClick={() =>
runRequest('deploy', '/api/story/deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/x-yaml',
},
body: yaml,
})
}
>
Deploy
</button>
</div>
</div>
<pre aria-live="polite">
{result.label}
{'\n'}
{result.payload}
</pre>
</div>
);
}
+21
View File
@@ -0,0 +1,21 @@
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
Helvetica, Arial, sans-serif;
line-height: 1.4;
font-weight: 400;
color: #0f172a;
background: #f5f7fa;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
}
+109
View File
@@ -0,0 +1,109 @@
import YAML from 'yaml';
import { z } from 'zod';
import type { ScenarioDocument, ScenarioStep } from '../types';
const scenarioSchema = z.object({
id: z.string().min(1),
version: z.number().int().positive(),
title: z.string().min(1),
players: z.object({
min: z.number().int().min(1),
max: z.number().int().min(1),
}),
duration: z.object({
total_minutes: z.number().int().positive(),
}),
canon: z.object({
introduction: z.string().min(1),
stakes: z.string().min(1),
}),
stations: z.array(z.record(z.string(), z.unknown())),
puzzles: z.array(z.record(z.string(), z.unknown())),
steps_reference_order: z.array(z.string().min(1)),
steps_narrative: z.array(
z.object({
step_id: z.string().min(1),
scene: z.string().min(1),
narrative: z.string().min(1),
}),
),
});
function normalizeId(raw: string, fallback: string): string {
const cleaned = raw
.trim()
.replace(/[^a-zA-Z0-9_]+/g, '_')
.replace(/^_+|_+$/g, '')
.toUpperCase();
return cleaned || fallback;
}
export function buildScenarioFromBlocks(
scenarioId: string,
steps: ScenarioStep[],
): ScenarioDocument {
const normalizedSteps = steps.map((step, index) => {
const position = index + 1;
return {
stepId: normalizeId(step.stepId, `STEP_${position}`),
sceneId: normalizeId(step.sceneId, `SCENE_${position}`),
};
});
const fallbackStep =
normalizedSteps.length > 0
? normalizedSteps
: [{ stepId: 'STEP_BOOT', sceneId: 'SCENE_BOOT' }];
return {
id: normalizeId(scenarioId, 'ZACUS_V2_NEW'),
version: 1,
title: 'Nouveau scenario Zacus',
players: {
min: 6,
max: 14,
},
duration: {
total_minutes: 90,
},
canon: {
introduction: 'Scenario genere depuis frontend scratch-like.',
stakes: 'Valider les transitions et deployer via API Story V2.',
},
stations: [],
puzzles: [],
steps_reference_order: fallbackStep.map((step) => step.stepId),
steps_narrative: fallbackStep.map((step) => ({
step_id: step.stepId,
scene: step.sceneId,
narrative: `Etape ${step.stepId} en scene ${step.sceneId}.`,
})),
};
}
export function validateScenarioDocument(
document: ScenarioDocument,
): { ok: true } | { ok: false; error: string } {
const parsed = scenarioSchema.safeParse(document);
if (!parsed.success) {
return {
ok: false,
error: parsed.error.issues[0]?.message ?? 'schema validation error',
};
}
if (parsed.data.players.min > parsed.data.players.max) {
return { ok: false, error: 'players.min must be <= players.max' };
}
const uniqueStepIds = new Set(parsed.data.steps_reference_order);
if (uniqueStepIds.size !== parsed.data.steps_reference_order.length) {
return { ok: false, error: 'duplicate step ids in steps_reference_order' };
}
return { ok: true };
}
export function scenarioToYaml(document: ScenarioDocument): string {
return YAML.stringify(document);
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+29
View File
@@ -0,0 +1,29 @@
export interface ScenarioStep {
stepId: string;
sceneId: string;
}
export interface ScenarioDocument {
id: string;
version: number;
title: string;
players: {
min: number;
max: number;
};
duration: {
total_minutes: number;
};
canon: {
introduction: string;
stakes: string;
};
stations: Array<Record<string, unknown>>;
puzzles: Array<Record<string, unknown>>;
steps_reference_order: string[];
steps_narrative: Array<{
step_id: string;
scene: string;
narrative: string;
}>;
}
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

Some files were not shown because too many files have changed in this diff Show More