chore: remove BMAD framework files and IDE configuration artifacts

Clean up unused BMAD workflow, agent, and command files across all IDE
configurations (.agent, .clinerules, .cursor, .gemini, .github, .kilocode,
.opencode) and internal module files (_bmad/bmb, _bmad/bmm).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr
2026-04-25 15:01:09 +02:00
parent 891c4ba436
commit ab5dc7e568
3006 changed files with 279068 additions and 59151 deletions

View File

@@ -0,0 +1,79 @@
# Entropyk Circuit Builder UI
Outil web minimal pour créer des configurations de circuits HVAC et les tester avec le CLI Entropyk (validate + run).
## Prérequis
- Python 3.6+
- Entropyk compilé en release : `cargo build --release -p entropyk-cli`
## Lancement
Depuis **la racine du dépôt** Entropyk :
```bash
cd /chemin/vers/Entropyk
python3 tools/circuit-builder-ui/server.py
```
Puis ouvre un navigateur sur : **http://localhost:8765**
Le serveur sert lUI et exécute le CLI pour les actions « Valider » et « Lancer simulation ». Le binaire attendu est `target/release/entropyk-cli`.
## Utilisation
1. **Scénario** : nom, fluide (ex. R410A), backend (Test ou CoolProp).
2. **Circuits** : ajouter un ou plusieurs circuits ; pour chaque circuit :
- **Composants** : type (Placeholder, Compressor, Condenser, Evaporator, etc.), nom, paramètres.
- **Arêtes** : `from` / `to` au format `composant:port` (ex. `comp:outlet``cond:inlet`).
3. **Solver** : stratégie (fallback, newton, picard), max itérations, tolérance.
4. **Couplages thermiques** (optionnel) : circuit chaud/froid, UA, efficacité.
5. **Charger un exemple** : simple_working.json ou chiller_r410a_minimal.json.
6. **Télécharger JSON** : exporte la config pour usage en ligne de commande.
7. **Valider (CLI)** : envoie la config au serveur qui lance `entropyk-cli validate`.
8. **Lancer simulation (CLI)** : lance `entropyk-cli run` et affiche le résultat.
Tous les types de composants supportés par le CLI peuvent être testés depuis lUI (Placeholder, Compressor, Condenser, Evaporator, ExpansionValve, HeatExchanger, Pump, etc.). La palette inclut aussi **sources/puits** (Source eau, Puits eau, Source réfrig., Puits réfrig.) et **Splitter/Merger** ; exportés en Placeholder pour le CLI. Pour conditions aux limites réelles : API Rust (BrineSource, RefrigerantSource…), voir `docs/migration/boundary-conditions.md`.
---
## Application UI complète plus tard : quelle option technique ?
Tu prévois une vraie application UI à part entière. Voici les options et une recommandation.
### 1. **Application web (front + API)**
- **Stack typique** : React, Vue ou Svelte + backend (Rust actix/axum, ou Node, ou Python FastAPI).
- **Avantages** : un seul front déployable partout (cloud, on-premise), mises à jour centralisées, pas dinstallateur.
- **Inconvénients** : les calculs doivent tourner côté serveur (ou via WebAssembly si tu portes le moteur un jour). Il faut exposer une API (ex. POST /run avec la config JSON) qui appelle le CLI ou la lib Entropyk.
- **Bonne option si** : tu veux un outil accessible via navigateur, multi-utilisateurs ou hébergé.
### 2. **Desktop avec Tauri (Rust + web view)**
- **Stack** : Tauri 2 + front en HTML/JS ou framework (React, Vue, etc.) ; la logique métier et les appels au moteur sont en Rust.
- **Avantages** : réutilisation directe dEntropyk comme crate Rust (pas de sous-process CLI), binaire léger, pas de serveur, offline, performances maximales.
- **Inconvénients** : cest une app desktop (packaging Windows/macOS/Linux), un peu plus de mise en place au début.
- **Bonne option si** : tu veux une application native installable, un seul binaire, et garder tout le calcul dans le même processus Rust. **Cest souvent le meilleur choix pour un outil HVAC pro à long terme.**
### 3. **Desktop avec Electron**
- **Stack** : app Node + moteur de rendu Chromium, front web (React, etc.) ; le CLI peut être lancé en sous-process depuis Node.
- **Avantages** : même stack quen web, écosystème npm riche.
- **Inconvénients** : gros binaire, consommation mémoire, il faut quand même appeler le CLI (ou un binaire natif) pour les calculs.
- **Bonne option si** : tu as déjà une équipe 100 % web et tu veux du desktop sans toucher au Rust en front.
### 4. **Hybride : web UI + petit service local**
- **Principe** : une SPA (React/Vue) qui parle à un petit service local (Rust ou Python) qui exécute le CLI ou utilise la lib Entropyk.
- **Avantages** : UI 100 % web, calculs locaux, pas dhébergement serveur obligatoire.
- **Inconvénients** : il faut installer et lancer le service local en plus du navigateur.
- **Bonne option si** : tu veux une UI web mais que les calculs restent sur la machine de lutilisateur.
---
### Recommandation
- **Pour un outil interne / démo / prototypage rapide** : garder cette UI (Circuit Builder) + serveur Python tel quel, ou remplacer le serveur par un petit backend Rust (axum) qui appelle la lib Entropyk.
- **Pour une application produit complète (HVAC, multi-circuits, résultats structurés)** : viser **Tauri 2 + front web (React/Vue/Svelte)**. Tu réutilises Entropyk en Rust, une seule codebase UI, et tu distribues un installateur par plateforme. Cest la solution la plus propre et la plus pérenne pour une app desktop basée sur Entropyk.
Si tu indiques si tu vises plutôt **desktop** ou **web hébergé**, on peut détailler larchitecture (Tauri vs web API) et les prochaines étapes concrètes.

View File

@@ -0,0 +1,93 @@
# Circuit Builder UI — Référence (basée sur le code, sans supposition)
Ce document décrit **ce qui existe réellement** dans le CLI, la lib et lUI. Aucune supposition : uniquement ce qui est implémenté et utilisé.
---
## 1. Types de composants que le CLI crée (`run.rs` → `create_component`)
Le CLI instancie **uniquement** les types suivants. Tout autre type dans le JSON provoque une erreur « Unknown component type ».
| Type dans le JSON | Création réelle |
|-------------------|------------------|
| `ScrewEconomizerCompressor`, `ScrewCompressor` | Compresseur vis avec courbes, économiseur |
| `MchxCondenserCoil`, `MchxCoil` | Condenseur à air (UA, T_air, fan_speed, n_air) |
| `FloodedEvaporator` | Évaporateur noyé (UA, target_quality, refrigerant, secondary_fluid) |
| `Condenser`, `CondenserCoil` | Condenseur simple (UA, optionnel t_sat_k) |
| `Evaporator`, `EvaporatorCoil` | Évaporateur simple (UA, optionnel t_sat_k, superheat_k) |
| `HeatExchanger` | Échangeur LMTD avec conditions hot/cold (hot_fluid, hot_t_inlet_c, hot_pressure_bar, hot_mass_flow_kg_s, cold_*) |
| `Compressor` | Compresseur AHRI 540 (speed_rpm, displacement_m3, efficiency, fluid, m1m10) |
| `ExpansionValve` | Détendeur (fluid, opening) |
| `Pump` | Pompe (name) — équations 0, composant pass-through |
| `Placeholder` | Composant générique (n_equations) pour compléter la topologie |
**Ce que le CLI ne crée pas** (même si la lib les a) :
`AirSource`, `AirSink`, `BrineSource`, `BrineSink`, `WaterSource`, `WaterSink`, `Fan`.
Ils napparaissent nulle part dans le `match component_type` de `run.rs`.
---
## 2. Modèle de configuration (JSON)
- **Un scénario** : `name`, `fluid`, `circuits[]`, optionnel `thermal_couplings[]`, `solver`.
- **Un circuit** : `id`, `components[]`, `edges[]`. Chaque circuit est un graphe de composants (un fluide par circuit dans lusage actuel).
- **Une arête** : `from: "nom:port"`, `to: "nom:port"`. Ports par défaut : `inlet` (0), `outlet` (1). Pour le screw : `suction`, `discharge`, `economizer`.
- **Couplage thermique** : `hot_circuit`, `cold_circuit`, `ua`, `efficiency`. Lie deux circuits (ex. réfrigérant → eau).
Exemple réel à 2 circuits : `crates/cli/examples/heat_pump_r410a.json` (circuit 0 = réfrigérant, circuit 1 = eau, 1 thermal_coupling).
---
## 3. Ce que lUI fait aujourdhui (code dans `index.html`)
### Export (`buildConfig()`)
- **Un seul circuit** : `circuits: [circuit]` avec `id: 0`. Pas de circuit 1, pas de `thermal_couplings`.
- **Mapping des types UI → JSON** :
- `WaterSource`, `WaterSink`, `AirSource`, `AirSink`, `RefrigerantSource`, `RefrigerantSink`, `FlowSplitter`, `FlowMerger`**toujours** `type: "Placeholder"` + `n_equations: 2` (et autres params conservés). Cest obligatoire car le CLI ne connaît pas ces types.
- `CondenserWater``type: "HeatExchanger"` avec hot = fluide projet (réfrigérant), cold = Water + conditions secondaires.
- `CondenserAir``type: "MchxCondenserCoil"` avec `ua_nominal_kw_k`, `air_inlet_temp_c`, `fan_speed`, `n_air_exponent`, `coil_index: 0`.
- `EvaporatorWater`, `EvaporatorAir``type: "HeatExchanger"` avec hot/cold cohérents (secondaire / réfrigérant).
- `Compressor`, `ExpansionValve`, `HeatExchanger`, `Pump` → type inchangé + params.
- **Arêtes** : `from: "nom:outlet"`, `to: "nom:inlet"` (noms dérivés des nœuds).
### Limites actuelles de lUI
1. **Un seul circuit exporté** : pas de deuxième circuit (ex. eau), pas de `thermal_couplings`. Donc on ne peut pas produire un JSON du type `heat_pump_r410a.json` depuis lUI.
2. **Sources/puits** : Source eau, Puits eau, Source air, Puits air, etc. sont exportés en **Placeholder**. Le solver les traite comme des blocs à `n_equations` sans physique (pas de vraie source air/eau). Les placer sur le schéma permet de représenter visuellement un circuit, mais ce circuit nest pas simulé comme « circuit air » ou « circuit eau ».
3. **Ventilateur** : il nexiste pas comme composant séparé dans le CLI. Le condenseur à air est modélisé par **MchxCondenserCoil** (T_air entrée + vitesse ventilateur en paramètres). LUI reflète ça en exportant un seul nœud « Condenseur à air » en `MchxCondenserCoil`.
---
## 4. Lib vs CLI (composants)
| Composant | Dans la lib (crates/components) | Créé par le CLI |
|-----------|--------------------------------|------------------|
| AirSource, AirSink | Oui (`air_boundary.rs`) | Non |
| BrineSource, BrineSink | Oui (`brine_boundary.rs`) | Non |
| RefrigerantSource, RefrigerantSink | (existent ou équivalent) | Non (utiliser Placeholder) |
| Fan | Oui (`fan.rs`) | Non |
| Pump | Oui | Oui (composant pass-through, 0 équations) |
| HeatExchanger | Oui | Oui (avec hot/cold conditions) |
| MchxCondenserCoil | Oui | Oui |
| Condenser / Evaporator (simples) | Oui | Oui |
Donc : **loutil UI est cohérent avec le CLI** pour tout ce qui est exporté en Compressor, Condenser*, Evaporator*, HeatExchanger, MchxCondenserCoil, ExpansionValve, Pump, Placeholder. Les « sources air/eau » et le « ventilateur » ne sont pas des approximations de lUI : le CLI ne propose tout simplement pas ces types.
---
## 5. Vision claire de loutil (sans approximation)
**Aujourdhui, loutil est :**
- Un **éditeur visuel pour un seul circuit** (en pratique le circuit réfrigérant).
- Les nœuds « Condenseur à eau/air », « Évaporateur à eau/air » sont convertis en **HeatExchanger** ou **MchxCondenserCoil** avec les bons côtés (réfrigérant / eau ou air) et conditions aux limites. Cest aligné avec le CLI.
- Les nœuds « Source air », « Puits air », « Source eau », etc. sont des **Placeholders** : ils servent à dessiner un schéma et à garder la topologie (nombre darêtes, n_equations), mais le fluide « air » ou « eau » nest pas simulé comme tel par le CLI.
**Pour aller vers une machine complète (ex. PAC air-eau) sans approximation :**
1. **Multi-circuit dans lUI** : pouvoir définir au moins 2 circuits (ex. 0 = réfrigérant, 1 = eau) et des arêtes par circuit.
2. **Export des thermal_couplings** : quand deux circuits existent, exporter un bloc `thermal_couplings` (hot_circuit, cold_circuit, ua, efficiency).
3. **Support CLI pour sources/puits** (optionnel) : si un jour le CLI crée `AirSource`/`AirSink` ou `BrineSource`/`BrineSink`, lUI pourra alors exporter ces types au lieu de Placeholder pour les circuits air/eau.
Ce document peut servir de base pour implémenter ces évolutions ou pour expliquer précisément le comportement actuel à un ingénieur HVAC.

View File

@@ -0,0 +1,640 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Entropyk — Éditeur visuel de machine</title>
<style>
:root { --bg: #0f1419; --surface: #1a1f26; --card: #252d38; --accent: #58a6ff; --green: #3fb950; --orange: #d29922; --red: #f85149; --text: #e6edf3; --muted: #8b949e; }
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.topbar { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1rem; background: var(--surface); border-bottom: 1px solid #30363d; flex-shrink: 0; }
.topbar h1 { font-size: 1rem; font-weight: 600; margin: 0; color: var(--accent); }
.topbar input, .topbar select { padding: 0.35rem 0.6rem; border: 1px solid #30363d; border-radius: 6px; background: var(--card); color: var(--text); }
.topbar .spacer { flex: 1; }
.topbar button { padding: 0.4rem 0.8rem; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; }
.topbar button.primary { background: var(--accent); color: #fff; }
.topbar button.secondary { background: var(--card); color: var(--text); border: 1px solid #30363d; }
.topbar button:hover { filter: brightness(1.1); }
.main { display: flex; flex: 1; min-height: 0; }
.palette { width: 200px; background: var(--surface); border-right: 1px solid #30363d; padding: 0.75rem; overflow-y: auto; flex-shrink: 0; }
.palette h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
.palette-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem; margin-bottom: 0.25rem; background: var(--card); border-radius: 8px; cursor: grab; border: 2px solid transparent; font-size: 0.875rem; }
.palette-item:hover { border-color: var(--accent); }
.palette-item .icon { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.75rem; }
.palette-item.Compressor .icon { background: #23863633; color: var(--green); }
.palette-item.Condenser .icon, .palette-item.CondenserWater .icon, .palette-item.CondenserAir .icon { background: #1f6feb33; color: var(--accent); }
.palette-item.Evaporator .icon, .palette-item.EvaporatorWater .icon, .palette-item.EvaporatorAir .icon { background: #8957e533; color: #a371f7; }
.palette-item.ExpansionValve .icon { background: #d2992233; color: var(--orange); }
.palette-item.Placeholder .icon { background: #6e768133; color: var(--muted); }
.palette-item.Pump .icon { background: #388bfd33; color: #58a6ff; }
.palette-item.HeatExchanger .icon { background: #da363633; color: var(--red); }
.palette-item.WaterSource .icon { background: #23863633; color: var(--green); }
.palette-item.WaterSink .icon { background: #da363633; color: var(--red); }
.palette-item.AirSource .icon { background: #58a6ff33; color: #79c0ff; }
.palette-item.AirSink .icon { background: #6e768133; color: var(--muted); }
.palette-item.RefrigerantSource .icon { background: #1f6feb33; color: var(--accent); }
.palette-item.RefrigerantSink .icon { background: #8957e533; color: #a371f7; }
.palette-item.FlowSplitter .icon { background: #d2992233; color: var(--orange); }
.palette-item.FlowMerger .icon { background: #d2992233; color: var(--orange); }
.canvas-wrap { flex: 1; position: relative; overflow: hidden; background: #0d1117; }
#canvas { display: block; width: 100%; height: 100%; cursor: default; }
.canvas-wrap.mode-add #canvas { cursor: crosshair; }
.canvas-wrap.mode-connect #canvas { cursor: pointer; }
.props { width: 280px; background: var(--surface); border-left: 1px solid #30363d; padding: 0.75rem; overflow-y: auto; flex-shrink: 0; }
.props h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
.props label { display: block; margin-top: 0.5rem; font-size: 0.8rem; color: var(--muted); }
.props input, .props select { width: 100%; padding: 0.4rem; border: 1px solid #30363d; border-radius: 6px; background: var(--card); color: var(--text); margin-top: 0.2rem; }
.props .empty { font-size: 0.875rem; color: var(--muted); margin-top: 1rem; }
.props button.danger { background: var(--red); color: #fff; border: none; padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer; margin-top: 0.75rem; font-size: 0.8rem; }
.result { padding: 0.5rem; margin-top: 0.5rem; border-radius: 6px; font-size: 0.8rem; white-space: pre-wrap; max-height: 120px; overflow: auto; }
.result.ok { background: #23863622; border: 1px solid var(--green); }
.result.err { background: #f8514922; border: 1px solid var(--red); }
.mode-btns { display: flex; gap: 0.25rem; margin-bottom: 0.5rem; }
.mode-btns button { padding: 0.35rem 0.6rem; border: 1px solid #30363d; border-radius: 6px; background: var(--card); color: var(--text); cursor: pointer; font-size: 0.8rem; }
.mode-btns button.active { background: var(--accent); color: #fff; border-color: var(--accent); }
</style>
</head>
<body>
<div class="topbar">
<h1>Entropyk — Machine thermodynamique</h1>
<input type="text" id="projectName" placeholder="Nom du projet" style="width:180px" />
<label style="font-size:0.8rem;color:var(--muted)">Fluide</label>
<select id="fluid" style="width:100px"><option value="R410A">R410A</option><option value="R134a">R134a</option><option value="R744">R744</option></select>
<div class="spacer"></div>
<select id="exampleSelect" style="width:200px">
<option value="">Charger un exemple…</option>
<option value="/examples/simple_working.json">Cycle simple (placeholders)</option>
<option value="/examples/chiller_r410a_minimal.json">Chiller 2 circuits</option>
</select>
<button type="button" id="btnValidate" class="secondary">Valider</button>
<button type="button" id="btnRun" class="primary">Lancer simulation</button>
<button type="button" id="btnExport" class="secondary">Télécharger JSON</button>
</div>
<div class="main">
<aside class="palette">
<h3>Composants</h3>
<p style="font-size:0.8rem;color:var(--muted);margin:0 0 0.5rem">Cliquez pour sélectionner, puis cliquez sur le schéma pour placer. Ou glissez-déposez.</p>
<div class="palette-info" style="font-size:0.7rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:0.4rem 0.5rem;margin-bottom:0.5rem;line-height:1.35">
<strong>Logique HVAC</strong> : Condenseur = réfrigérant (chaud) + eau/air (froid). Évaporateur = eau/air (chaud) + réfrigérant (froid). Compresseur = réfrigérant uniquement. Condenseur à air = ventilateur intégré (vitesse 01). Circuit air = Source air → … → Puits air (export Placeholder tant que le CLI na pas de circuit air dédié).
</div>
<div class="mode-btns">
<button type="button" id="modeMove" class="active">Déplacer</button>
<button type="button" id="modeAdd">Ajouter</button>
<button type="button" id="modeConnect">Relier</button>
</div>
<p id="modeHint" style="font-size:0.75rem;color:var(--muted);margin:0.25rem 0 0.5rem">Déplacer : glissez les blocs.</p>
<p id="connectStatus" style="font-size:0.8rem;font-weight:600;color:var(--accent);margin:0.25rem 0 0.5rem;min-height:1.2em;"></p>
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Sources &amp; puits</h3>
<div class="palette-item WaterSource" data-type="WaterSource" draggable="true"><span class="icon">↓S</span> Source eau</div>
<div class="palette-item WaterSink" data-type="WaterSink" draggable="true"><span class="icon">S↑</span> Puits eau</div>
<div class="palette-item AirSource" data-type="AirSource" draggable="true"><span class="icon">↓A</span> Source air</div>
<div class="palette-item AirSink" data-type="AirSink" draggable="true"><span class="icon">A↑</span> Puits air</div>
<div class="palette-item RefrigerantSource" data-type="RefrigerantSource" draggable="true"><span class="icon">↓R</span> Source réfrig.</div>
<div class="palette-item RefrigerantSink" data-type="RefrigerantSink" draggable="true"><span class="icon">R↑</span> Puits réfrig.</div>
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Circuit frigorifique</h3>
<div class="palette-item Compressor" data-type="Compressor" draggable="true"><span class="icon">C</span> Compresseur (réfrigérant)</div>
<div class="palette-item CondenserWater" data-type="CondenserWater" draggable="true"><span class="icon">Co</span> Condenseur à eau</div>
<div class="palette-item CondenserAir" data-type="CondenserAir" draggable="true"><span class="icon">Co</span> Condenseur à air</div>
<div class="palette-item EvaporatorWater" data-type="EvaporatorWater" draggable="true"><span class="icon">E</span> Évaporateur à eau</div>
<div class="palette-item EvaporatorAir" data-type="EvaporatorAir" draggable="true"><span class="icon">E</span> Évaporateur à air</div>
<div class="palette-item ExpansionValve" data-type="ExpansionValve" draggable="true"><span class="icon">V</span> Détendeur</div>
<div class="palette-item HeatExchanger" data-type="HeatExchanger" draggable="true"><span class="icon">H</span> Échangeur générique (2 fluides)</div>
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Circuit eau / secondaire</h3>
<div class="palette-item Pump" data-type="Pump" draggable="true"><span class="icon">P</span> Pompe</div>
<div class="palette-item FlowSplitter" data-type="FlowSplitter" draggable="true"><span class="icon"></span> Splitter</div>
<div class="palette-item FlowMerger" data-type="FlowMerger" draggable="true"><span class="icon"></span> Merger</div>
<h3 style="margin:0.5rem 0 0.25rem;font-size:0.7rem;color:var(--muted)">Divers</h3>
<div class="palette-item Placeholder" data-type="Placeholder" draggable="true"><span class="icon">?</span> Placeholder</div>
</aside>
<div class="canvas-wrap" id="canvasWrap">
<svg id="canvas" viewBox="0 0 1200 700" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<aside class="props">
<h3>Propriétés</h3>
<div id="propsEmpty" class="empty">Cliquez sur un composant sur le schéma pour modifier son nom et ses paramètres.</div>
<div id="propsForm" style="display:none">
<label>Nom (pour les connexions)</label>
<input type="text" id="propName" placeholder="ex: comp" />
<label>Type</label>
<input type="text" id="propType" readonly style="opacity:0.8" />
<div id="propParams"></div>
<button type="button" class="danger" id="btnDeleteNode">Supprimer le composant</button>
</div>
<div id="resultBox" style="display:none" class="result"></div>
</aside>
</div>
<script>
const COMPONENT_DEFAULTS = {
Placeholder: { n_equations: 2 },
WaterSource: { n_equations: 2 },
WaterSink: { n_equations: 2 },
AirSource: { n_equations: 2, t_air_c: 35, pressure_bar: 1.01, rh_percent: 50 },
AirSink: { n_equations: 2, pressure_bar: 1.01 },
RefrigerantSource: { n_equations: 2 },
RefrigerantSink: { n_equations: 2 },
FlowSplitter: { n_equations: 2 },
FlowMerger: { n_equations: 2 },
Compressor: { fluid: "R410A", speed_rpm: 2900, displacement_m3: 0.000025, efficiency: 0.82, m1: 0.88, m2: 2.2, m3: 450, m4: 1400, m5: -2, m6: 1.5, m7: 550, m8: 1500, m9: -2.5, m10: 1.8 },
CondenserWater: { ua: 5000, t_sat_cond_c: 45, ref_pressure_bar: 26, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 30, sec_pressure_bar: 2, sec_mass_flow_kg_s: 1.2 },
CondenserAir: { ua: 5000, t_sat_cond_c: 45, ref_pressure_bar: 26, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 35, sec_pressure_bar: 1.01, sec_mass_flow_kg_s: 2.5, fan_speed: 1, n_air_exponent: 0.5 },
EvaporatorWater: { ua: 6000, t_sat_evap_c: 5, superheat_k: 5, ref_pressure_bar: 7, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 12, sec_pressure_bar: 2, sec_mass_flow_kg_s: 1.0 },
EvaporatorAir: { ua: 6000, t_sat_evap_c: 5, superheat_k: 5, ref_pressure_bar: 7, ref_mass_flow_kg_s: 0.05, sec_t_inlet_c: 7, sec_pressure_bar: 1.01, sec_mass_flow_kg_s: 2.0 },
ExpansionValve: { fluid: "R410A", opening: 1 },
HeatExchanger: { ua: 6000, hot_fluid: "Air", hot_t_inlet_c: 7, hot_pressure_bar: 1.01, hot_mass_flow_kg_s: 0.8, cold_fluid: "R410A", cold_t_inlet_c: 0, cold_pressure_bar: 7, cold_mass_flow_kg_s: 0.04 },
Pump: {}
};
const EXPORT_AS_PLACEHOLDER = ["WaterSource", "WaterSink", "AirSource", "AirSink", "RefrigerantSource", "RefrigerantSink", "FlowSplitter", "FlowMerger"];
const NODE_LABEL = {
WaterSource: "Src eau", WaterSink: "Puits eau", AirSource: "Src air", AirSink: "Puits air",
RefrigerantSource: "Src réf.", RefrigerantSink: "Puits réf.",
FlowSplitter: "Split", FlowMerger: "Merge",
CondenserWater: "Cond. eau", CondenserAir: "Cond. air", EvaporatorWater: "Évap. eau", EvaporatorAir: "Évap. air"
};
const PARAM_META = {
n_equations: { type: "number", label: "n_equations" },
ua: { type: "number", label: "UA (W/K)" },
t_sat_k: { type: "number", label: "t_sat_k" },
t_sat_cond_c: { type: "number", label: "T sat cond. (°C)" },
t_sat_evap_c: { type: "number", label: "T sat évap. (°C)" },
superheat_k: { type: "number", label: "Surchauffe (K)" },
fluid: { type: "text", label: "Réfrigérant" },
opening: { type: "number", label: "Ouverture" },
speed_rpm: { type: "number", label: "Vitesse (tr/min)" },
displacement_m3: { type: "number", label: "Cylindrée (m³)" },
efficiency: { type: "number", label: "Rendement" },
ref_pressure_bar: { type: "number", label: "P réfrig. (bar)" },
ref_mass_flow_kg_s: { type: "number", label: "Débit réfrig. (kg/s)" },
sec_t_inlet_c: { type: "number", label: "T entrée secondaire (°C)" },
sec_pressure_bar: { type: "number", label: "P secondaire (bar)" },
sec_mass_flow_kg_s: { type: "number", label: "Débit secondaire (kg/s)" },
fan_speed: { type: "number", label: "Vitesse ventilateur (01)" },
n_air_exponent: { type: "number", label: "Exposant n_air (loi débit)" },
t_air_c: { type: "number", label: "T air (°C)" },
pressure_bar: { type: "number", label: "P (bar)" },
rh_percent: { type: "number", label: "Humidité relative (%)" },
m1: { type: "number", label: "m1" }, m2: { type: "number", label: "m2" },
m3: { type: "number", label: "m3" }, m4: { type: "number", label: "m4" },
m5: { type: "number", label: "m5" }, m6: { type: "number", label: "m6" },
m7: { type: "number", label: "m7" }, m8: { type: "number", label: "m8" },
m9: { type: "number", label: "m9" }, m10: { type: "number", label: "m10" },
hot_fluid: { type: "text", label: "Fluide chaud" }, cold_fluid: { type: "text", label: "Fluide froid" },
hot_t_inlet_c: { type: "number", label: "T entrée chaud (°C)" }, cold_t_inlet_c: { type: "number", label: "T entrée froid (°C)" },
hot_pressure_bar: { type: "number", label: "P chaud (bar)" }, cold_pressure_bar: { type: "number", label: "P froid (bar)" },
hot_mass_flow_kg_s: { type: "number", label: "Débit chaud (kg/s)" }, cold_mass_flow_kg_s: { type: "number", label: "Débit froid (kg/s)" }
};
const PARAM_ORDER = {
CondenserWater: ["ua", "t_sat_cond_c", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s"],
CondenserAir: ["ua", "t_sat_cond_c", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s", "fan_speed", "n_air_exponent"],
EvaporatorWater: ["ua", "t_sat_evap_c", "superheat_k", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s"],
EvaporatorAir: ["ua", "t_sat_evap_c", "superheat_k", "ref_pressure_bar", "ref_mass_flow_kg_s", "sec_t_inlet_c", "sec_pressure_bar", "sec_mass_flow_kg_s"]
};
let state = { mode: "move", nodes: [], edges: [], selectedId: null, connectFrom: null, dragNode: null, dragOffset: null, addType: null, mouseX: null, mouseY: null };
let nodeIdSeq = 0;
const NODE_W = 100, NODE_H = 52;
function getNode(id) { return state.nodes.find(n => n.id === id); }
function getNodeByName(name) { return state.nodes.find(n => n.name === name); }
function addNode(type, x, y) {
const name = type.toLowerCase().slice(0, 3) + "_" + (++nodeIdSeq);
const params = { ...COMPONENT_DEFAULTS[type] };
state.nodes.push({ id: "n" + nodeIdSeq, type, name, x, y, params });
render();
}
function deleteNode(id) {
state.nodes = state.nodes.filter(n => n.id !== id);
state.edges = state.edges.filter(e => e.from !== id && e.to !== id);
if (state.selectedId === id) state.selectedId = null;
render();
showProps();
}
function addEdge(fromId, toId) {
const from = getNode(fromId), to = getNode(toId);
if (!from || !to || fromId === toId) return;
if (state.edges.some(e => e.from === fromId && e.to === toId)) return;
state.edges.push({ from: fromId, to: toId });
state.connectFrom = null;
setModeHint();
render();
}
function buildConfig() {
const fluid = document.getElementById("fluid").value.trim() || "R410A";
const name = document.getElementById("projectName").value.trim() || "Machine";
const circuit = {
id: 0,
components: state.nodes.map(n => {
if (EXPORT_AS_PLACEHOLDER.includes(n.type)) {
return { type: "Placeholder", name: n.name, ...n.params };
}
const p = n.params;
if (n.type === "CondenserWater") {
return {
type: "HeatExchanger",
name: n.name,
ua: p.ua,
hot_fluid: fluid,
hot_t_inlet_c: (p.t_sat_cond_c != null ? p.t_sat_cond_c : 45) + 5,
hot_pressure_bar: p.ref_pressure_bar,
hot_mass_flow_kg_s: p.ref_mass_flow_kg_s,
cold_fluid: "Water",
cold_t_inlet_c: p.sec_t_inlet_c,
cold_pressure_bar: p.sec_pressure_bar,
cold_mass_flow_kg_s: p.sec_mass_flow_kg_s
};
}
if (n.type === "CondenserAir") {
return {
type: "MchxCondenserCoil",
name: n.name,
ua_nominal_kw_k: (p.ua != null ? p.ua : 5000) / 1000,
n_air_exponent: p.n_air_exponent != null ? p.n_air_exponent : 0.5,
air_inlet_temp_c: p.sec_t_inlet_c != null ? p.sec_t_inlet_c : 35,
fan_speed: p.fan_speed != null ? p.fan_speed : 1,
coil_index: 0
};
}
if (n.type === "EvaporatorWater") {
return {
type: "HeatExchanger",
name: n.name,
ua: p.ua,
hot_fluid: "Water",
hot_t_inlet_c: p.sec_t_inlet_c,
hot_pressure_bar: p.sec_pressure_bar,
hot_mass_flow_kg_s: p.sec_mass_flow_kg_s,
cold_fluid: fluid,
cold_t_inlet_c: p.t_sat_evap_c != null ? p.t_sat_evap_c : 5,
cold_pressure_bar: p.ref_pressure_bar,
cold_mass_flow_kg_s: p.ref_mass_flow_kg_s
};
}
if (n.type === "EvaporatorAir") {
return {
type: "HeatExchanger",
name: n.name,
ua: p.ua,
hot_fluid: "Air",
hot_t_inlet_c: p.sec_t_inlet_c,
hot_pressure_bar: p.sec_pressure_bar,
hot_mass_flow_kg_s: p.sec_mass_flow_kg_s,
cold_fluid: fluid,
cold_t_inlet_c: p.t_sat_evap_c != null ? p.t_sat_evap_c : 5,
cold_pressure_bar: p.ref_pressure_bar,
cold_mass_flow_kg_s: p.ref_mass_flow_kg_s
};
}
return { type: n.type, name: n.name, ...n.params };
}),
edges: state.edges.map(e => {
const a = getNode(e.from), b = getNode(e.to);
return a && b ? { from: a.name + ":outlet", to: b.name + ":inlet" } : null;
}).filter(Boolean)
};
return { name, fluid, circuits: [circuit], solver: { strategy: "fallback", max_iterations: 100, tolerance: 1e-6 } };
}
function render() {
const svg = document.getElementById("canvas");
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
defs.innerHTML = '<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#58a6ff"/></marker>';
svg.innerHTML = "";
svg.appendChild(defs);
const gEdges = document.createElementNS("http://www.w3.org/2000/svg", "g");
state.edges.forEach(e => {
const a = getNode(e.from), b = getNode(e.to);
if (!a || !b) return;
const x1 = a.x + NODE_W; const y1 = a.y + NODE_H / 2;
const x2 = b.x; const y2 = b.y + NODE_H / 2;
const mid = (x1 + x2) / 2;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", `M ${x1} ${y1} C ${mid} ${y1} ${mid} ${y2} ${x2} ${y2}`);
path.setAttribute("fill", "none");
path.setAttribute("stroke", "#58a6ff");
path.setAttribute("stroke-width", "2.5");
path.setAttribute("marker-end", "url(#arrow)");
gEdges.appendChild(path);
});
if (state.connectFrom && state.mouseX != null && state.mouseY != null) {
const a = getNode(state.connectFrom);
if (a) {
const x1 = a.x + NODE_W; const y1 = a.y + NODE_H / 2;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
const mid = (x1 + state.mouseX) / 2;
path.setAttribute("d", `M ${x1} ${y1} C ${mid} ${y1} ${mid} ${state.mouseY} ${state.mouseX} ${state.mouseY}`);
path.setAttribute("fill", "none");
path.setAttribute("stroke", "#58a6ff");
path.setAttribute("stroke-width", "2");
path.setAttribute("stroke-dasharray", "6 4");
path.setAttribute("marker-end", "url(#arrow)");
gEdges.appendChild(path);
}
}
svg.appendChild(gEdges);
const gNodes = document.createElementNS("http://www.w3.org/2000/svg", "g");
state.nodes.forEach(n => {
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttribute("data-id", n.id);
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", n.x);
rect.setAttribute("y", n.y);
rect.setAttribute("width", NODE_W);
rect.setAttribute("height", NODE_H);
rect.setAttribute("rx", "8");
rect.setAttribute("fill", state.selectedId === n.id ? "#388bfd44" : "#21262d");
rect.setAttribute("stroke", state.connectFrom === n.id ? "#58a6ff" : "#30363d");
rect.setAttribute("stroke-width", state.selectedId === n.id || state.connectFrom === n.id ? "2" : "1");
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", n.x + NODE_W / 2);
label.setAttribute("y", n.y + 20);
label.setAttribute("text-anchor", "middle");
label.setAttribute("fill", "#e6edf3");
label.setAttribute("font-size", "12");
label.setAttribute("pointer-events", "none");
label.textContent = NODE_LABEL[n.type] || n.type.slice(0, 2);
const nameText = document.createElementNS("http://www.w3.org/2000/svg", "text");
nameText.setAttribute("x", n.x + NODE_W / 2);
nameText.setAttribute("y", n.y + 38);
nameText.setAttribute("text-anchor", "middle");
nameText.setAttribute("fill", "#8b949e");
nameText.setAttribute("font-size", "10");
nameText.setAttribute("pointer-events", "none");
nameText.textContent = n.name;
g.appendChild(rect);
g.appendChild(label);
g.appendChild(nameText);
const hitRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
hitRect.setAttribute("x", n.x);
hitRect.setAttribute("y", n.y);
hitRect.setAttribute("width", NODE_W);
hitRect.setAttribute("height", NODE_H);
hitRect.setAttribute("fill", "transparent");
hitRect.setAttribute("cursor", state.mode === "connect" ? "pointer" : state.mode === "move" ? "grab" : "default");
hitRect.setAttribute("data-id", n.id);
g.appendChild(hitRect);
gNodes.appendChild(g);
});
svg.appendChild(gNodes);
if (state.mode === "connect") setModeHint();
}
function showProps() {
const empty = document.getElementById("propsEmpty");
const form = document.getElementById("propsForm");
if (state.selectedId) {
const n = getNode(state.selectedId);
if (n) {
empty.style.display = "none";
form.style.display = "block";
document.getElementById("propName").value = n.name;
document.getElementById("propType").value = n.type;
const container = document.getElementById("propParams");
container.innerHTML = "";
const order = PARAM_ORDER[n.type];
const keys = order ? order.filter(k => n.params.hasOwnProperty(k)).concat(Object.keys(n.params).filter(k => !order.includes(k))) : Object.keys(n.params);
let didRefHeader = false;
keys.forEach((k) => {
if (PARAM_ORDER[n.type]) {
if (!didRefHeader && (k === "ua" || k === "t_sat_cond_c" || k === "t_sat_evap_c")) {
didRefHeader = true;
const refHeader = document.createElement("div");
refHeader.className = "prop-section";
refHeader.style.cssText = "grid-column:1/-1;font-size:0.7rem;color:var(--muted);margin-bottom:0.2rem;";
refHeader.textContent = "Côté réfrigérant";
container.appendChild(refHeader);
}
if (k === "sec_t_inlet_c") {
const sep = document.createElement("div");
sep.className = "prop-section";
sep.style.cssText = "grid-column:1/-1;font-size:0.7rem;color:var(--muted);margin:0.4rem 0 0.2rem;padding-top:0.3rem;border-top:1px solid var(--border);";
sep.textContent = "Côté secondaire (eau / air)";
container.appendChild(sep);
}
if (n.type === "CondenserAir" && k === "fan_speed") {
const fanSep = document.createElement("div");
fanSep.className = "prop-section";
fanSep.style.cssText = "grid-column:1/-1;font-size:0.7rem;color:var(--muted);margin:0.4rem 0 0.2rem;padding-top:0.3rem;border-top:1px solid var(--border);";
fanSep.textContent = "Ventilateur (export MchxCondenserCoil)";
container.appendChild(fanSep);
}
}
const meta = PARAM_META[k] || { type: "number", label: k };
const label = document.createElement("label");
label.textContent = meta.label;
const input = document.createElement("input");
input.type = meta.type === "number" ? "number" : "text";
input.dataset.key = k;
input.value = n.params[k];
input.addEventListener("input", () => { n.params[k] = meta.type === "number" ? parseFloat(input.value) || 0 : input.value; });
container.appendChild(label);
container.appendChild(input);
});
return;
}
}
empty.style.display = "block";
form.style.display = "none";
}
function pt(svg, e) {
const p = svg.createSVGPoint();
p.x = e.clientX;
p.y = e.clientY;
const ctm = svg.getScreenCTM();
if (!ctm) return { x: 0, y: 0 };
const tp = p.matrixTransform(ctm.inverse());
return { x: tp.x, y: tp.y };
}
function nodeAtPoint(x, y) {
for (let i = state.nodes.length - 1; i >= 0; i--) {
const n = state.nodes[i];
if (x >= n.x && x <= n.x + NODE_W && y >= n.y && y <= n.y + NODE_H) return n.id;
}
return null;
}
const canvas = document.getElementById("canvas");
const wrap = document.getElementById("canvasWrap");
canvas.addEventListener("mousedown", e => {
e.preventDefault();
const p = pt(canvas, e);
const id = nodeAtPoint(p.x, p.y);
if (state.mode === "connect") {
if (id) {
if (state.connectFrom) {
if (state.connectFrom === id) {
state.connectFrom = null;
} else {
addEdge(state.connectFrom, id);
}
} else {
state.connectFrom = id;
}
} else {
state.connectFrom = null;
}
setModeHint();
render();
return;
}
if (state.mode === "add" && state.addType) {
if (!id) addNode(state.addType, Math.max(0, p.x - NODE_W / 2), Math.max(0, p.y - NODE_H / 2));
return;
}
if (id) {
state.selectedId = id;
state.dragNode = id;
const n = getNode(id);
state.dragOffset = { x: p.x - n.x, y: p.y - n.y };
showProps();
render();
} else {
state.selectedId = null;
showProps();
render();
}
});
function onMouseMove(e) {
const p = pt(canvas, e);
state.mouseX = p.x;
state.mouseY = p.y;
if (state.connectFrom) {
render();
return;
}
if (!state.dragNode) return;
const n = getNode(state.dragNode);
n.x = Math.max(0, p.x - state.dragOffset.x);
n.y = Math.max(0, p.y - state.dragOffset.y);
render();
}
canvas.addEventListener("mousemove", onMouseMove);
window.addEventListener("mousemove", onMouseMove);
canvas.addEventListener("mouseup", () => { state.dragNode = null; });
window.addEventListener("mouseup", () => { state.dragNode = null; });
canvas.addEventListener("dragover", e => { e.preventDefault(); });
canvas.addEventListener("drop", e => {
e.preventDefault();
const type = e.dataTransfer.getData("type");
if (!type) return;
const p = pt(canvas, e);
addNode(type, Math.max(0, p.x - NODE_W / 2), Math.max(0, p.y - NODE_H / 2));
});
document.querySelectorAll(".palette-item").forEach(el => {
el.addEventListener("click", () => {
state.addType = el.dataset.type;
document.querySelectorAll(".palette-item").forEach(x => x.style.borderColor = "transparent");
el.style.borderColor = "var(--accent)";
});
el.addEventListener("dragstart", e => { e.dataTransfer.setData("type", el.dataset.type); });
});
function setModeHint() {
const h = document.getElementById("modeHint");
const s = document.getElementById("connectStatus");
if (state.mode === "move") {
h.textContent = "Déplacer : glissez les blocs.";
s.textContent = "";
} else if (state.mode === "add") {
h.textContent = "Ajouter : choisissez un composant ci-dessus, puis cliquez sur le schéma.";
s.textContent = "";
} else {
h.textContent = "Relier : cliquez sur un bloc (sortie), puis sur un autre (entrée).";
s.textContent = state.connectFrom ? "→ Maintenant cliquez sur le bloc cible (entrée)." : "Cliquez sur le 1er bloc.";
}
}
document.getElementById("modeMove").onclick = () => { state.mode = "move"; state.connectFrom = null; wrap.className = "canvas-wrap"; document.querySelectorAll(".mode-btns button").forEach(b => b.classList.remove("active")); document.getElementById("modeMove").classList.add("active"); setModeHint(); render(); };
document.getElementById("modeAdd").onclick = () => { state.mode = "add"; state.connectFrom = null; wrap.className = "canvas-wrap mode-add"; document.querySelectorAll(".mode-btns button").forEach(b => b.classList.remove("active")); document.getElementById("modeAdd").classList.add("active"); setModeHint(); };
document.getElementById("modeConnect").onclick = () => { state.mode = "connect"; wrap.className = "canvas-wrap mode-connect"; document.querySelectorAll(".mode-btns button").forEach(b => b.classList.remove("active")); document.getElementById("modeConnect").classList.add("active"); state.connectFrom = null; setModeHint(); render(); };
document.getElementById("propName").addEventListener("input", e => {
const n = getNode(state.selectedId);
if (n) n.name = e.target.value.trim() || n.name;
render();
});
document.getElementById("btnDeleteNode").onclick = () => { if (state.selectedId) deleteNode(state.selectedId); };
document.getElementById("exampleSelect").addEventListener("change", async function() {
const path = this.value;
if (!path) return;
const res = await fetch(path);
const config = await res.json();
state.nodes = [];
state.edges = [];
state.selectedId = null;
nodeIdSeq = 0;
const c = config.circuits[0];
if (!c) return;
const nameToId = {};
(c.components || []).forEach((comp, i) => {
const id = "n" + (++nodeIdSeq);
nameToId[comp.name] = id;
const params = { ...comp };
delete params.type;
delete params.name;
state.nodes.push({
id,
type: comp.type,
name: comp.name,
x: 150 + (i % 4) * 220,
y: 80 + Math.floor(i / 4) * 120,
params
});
});
(c.edges || []).forEach(edge => {
const fromName = (edge.from || "").split(":")[0];
const toName = (edge.to || "").split(":")[0];
const fromId = nameToId[fromName];
const toId = nameToId[toName];
if (fromId && toId) state.edges.push({ from: fromId, to: toId });
});
document.getElementById("projectName").value = config.name || "";
document.getElementById("fluid").value = config.fluid || "R410A";
this.value = "";
render();
showProps();
});
async function postCli(endpoint) {
const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildConfig()) });
const data = await res.json();
const box = document.getElementById("resultBox");
box.style.display = "block";
box.className = "result " + (data.ok ? "ok" : "err");
box.textContent = (data.stdout || "") + (data.stderr ? "\n" + data.stderr : "");
}
document.getElementById("btnValidate").onclick = () => postCli("/validate");
document.getElementById("btnRun").onclick = () => postCli("/run");
document.getElementById("btnExport").onclick = () => {
const blob = new Blob([JSON.stringify(buildConfig(), null, 2)], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "entropyk-config.json";
a.click();
URL.revokeObjectURL(a.href);
};
render();
</script>
</body>
</html>

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Minimal backend for Circuit Builder UI.
Serves the static UI and runs entropyk-cli validate/run on POST.
Run from repository root: python tools/circuit-builder-ui/server.py
Then open http://localhost:8765
"""
import json
import os
import subprocess
import tempfile
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
from urllib.parse import parse_qs, urlparse
PORT = 8765
# Path to entropyk-cli from repo root (run server from repo root)
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
CLI_PATH = REPO_ROOT / "target" / "release" / "entropyk-cli"
UI_DIR = Path(__file__).resolve().parent
def run_cli(subcommand, config_json_str, run_output=None):
"""Run entropyk-cli validate or run; config_json_str written to temp file."""
if not CLI_PATH.exists():
return False, f"CLI not found. Build first: cargo build --release -p entropyk-cli\nExpected: {CLI_PATH}", ""
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
f.write(config_json_str)
tmp_path = f.name
try:
cmd = [str(CLI_PATH), subcommand, "--config", tmp_path]
if run_output is not None:
cmd.extend(["-o", run_output])
r = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60,
cwd=str(REPO_ROOT),
)
stdout = r.stdout or ""
stderr = r.stderr or ""
if r.returncode != 0:
return False, stdout or stderr or f"Exit code {r.returncode}", stderr
return True, stdout, stderr
except subprocess.TimeoutExpired:
return False, "Timeout (60s)", ""
except Exception as e:
return False, str(e), ""
finally:
try:
os.unlink(tmp_path)
except Exception:
pass
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
path = urlparse(self.path).path
if path == "/" or path == "/index.html":
self.serve_file(UI_DIR / "index.html", "text/html")
elif path == "/examples/simple_working.json":
self.serve_file(REPO_ROOT / "crates" / "cli" / "examples" / "simple_working.json", "application/json")
elif path == "/examples/chiller_r410a_minimal.json":
self.serve_file(REPO_ROOT / "crates" / "cli" / "examples" / "chiller_r410a_minimal.json", "application/json")
else:
self.send_error(404)
def serve_file(self, filepath, content_type):
try:
with open(filepath, "rb") as f:
data = f.read()
except FileNotFoundError:
self.send_error(404)
return
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_POST(self):
path = urlparse(self.path).path
if path != "/validate" and path != "/run":
self.send_error(404)
return
content_len = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_len)
try:
config_str = body.decode("utf-8")
json.loads(config_str)
except Exception as e:
self.send_json(400, {"ok": False, "error": f"Invalid JSON: {e}"})
return
if path == "/validate":
ok, out, err = run_cli("validate", config_str)
else:
ok, out, err = run_cli("run", config_str, run_output=os.path.devnull)
self.send_json(200, {"ok": ok, "stdout": out, "stderr": err})
def send_json(self, status, obj):
body = json.dumps(obj).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
print(f"[{self.log_date_time_string()}] {format % args}")
def main():
os.chdir(REPO_ROOT)
print(f"Entropyk Circuit Builder UI")
print(f" UI: http://localhost:{PORT}/")
print(f" CLI: {CLI_PATH} {'(found)' if CLI_PATH.exists() else '(not found — run: cargo build --release -p entropyk-cli)'}")
print(f" Root: {REPO_ROOT}")
server = HTTPServer(("", PORT), Handler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nBye.")
server.server_close()
if __name__ == "__main__":
main()