diff --git a/Encelado/.vscode/launch.json b/Encelado/.vscode/launch.json index 493f479..6b541c1 100644 --- a/Encelado/.vscode/launch.json +++ b/Encelado/.vscode/launch.json @@ -1,8 +1,12 @@ { // F5 avvia il server (motore + interfaccia web) come processo locale, fuori dal - // container, con la cartella dati in Documenti\Encelado. Il browser si apre da solo - // appena Kestrel è in ascolto. "Encelado (campione)" serve l'interfaccia con dati - // finti, senza chiavi né mercato: è quello che si usa per lavorare sulle pagine. + // container, con configurazione e dati nella sandbox deploy/local (ignorata da git, + // stesse cartelle che il compose monta come /config e /data). Il browser si apre da + // solo appena Kestrel è in ascolto; senza token il server ascolta solo su localhost e + // non chiede il login. "Encelado (campione)" serve l'interfaccia con dati finti, senza + // chiavi né mercato: è quello che si usa per lavorare sulle pagine. + // Le chiavi eToro: variabili ETORO_API_KEY/ETORO_USER_KEY nell'ambiente di VS Code, + // oppure Impostazioni ▸ Chiavi eToro con ENCELADO_KEY_PASSPHRASE impostata qui sotto. "version": "0.2.0", "configurations": [ { @@ -17,6 +21,8 @@ "stopAtEntry": false, "env": { "ENCELADO_WEB_PORT": "8080", + "ENCELADO_CONFIG_DIR": "${workspaceFolder}/deploy/local/config", + "ENCELADO_DATA_DIR": "${workspaceFolder}/deploy/local/data", "DOTNET_ENVIRONMENT": "Development" }, "serverReadyAction": { diff --git a/Encelado/.vscode/tasks.json b/Encelado/.vscode/tasks.json index 3ab954a..89491fc 100644 --- a/Encelado/.vscode/tasks.json +++ b/Encelado/.vscode/tasks.json @@ -112,10 +112,37 @@ "problemMatcher": "$msCompile" }, { - "label": "docker compose up", - "detail": "Avvia il container dalla docker-compose.yml alla radice (immagine locale, cartelle ./deploy/local/config e ./deploy/local/data).", + "label": "avvia in locale (script)", + "detail": "scripts/run-dev.ps1: compila e avvia il server nella sandbox deploy/local, senza container. Ctrl+C per fermare.", "type": "shell", - "command": "docker compose up --build", + "command": "powershell -ExecutionPolicy Bypass -File scripts/run-dev.ps1", + "options": { "cwd": "${workspaceFolder}" }, + "presentation": { "reveal": "always", "panel": "dedicated" }, + "problemMatcher": [] + }, + { + "label": "avvia in docker", + "detail": "scripts/run-docker.ps1: costruisce l'immagine e avvia il container dalla docker-compose.yml (deploy/local come /config e /data), poi segue il log.", + "type": "shell", + "command": "powershell -ExecutionPolicy Bypass -File scripts/run-docker.ps1", + "options": { "cwd": "${workspaceFolder}" }, + "presentation": { "reveal": "always", "panel": "dedicated" }, + "problemMatcher": [] + }, + { + "label": "ferma docker", + "detail": "docker compose down: SIGTERM al bot, stop pulito, container rimosso (cartelle in deploy/local conservate).", + "type": "shell", + "command": "docker compose down", + "options": { "cwd": "${workspaceFolder}" }, + "presentation": { "reveal": "always", "panel": "dedicated" }, + "problemMatcher": [] + }, + { + "label": "screenshot", + "detail": "scripts/screenshots.ps1: server in modalità campione e Edge headless, rigenera docs/img/*.png.", + "type": "shell", + "command": "powershell -ExecutionPolicy Bypass -File scripts/screenshots.ps1", "options": { "cwd": "${workspaceFolder}" }, "presentation": { "reveal": "always", "panel": "dedicated" }, "problemMatcher": [] diff --git a/Encelado/CHANGELOG.md b/Encelado/CHANGELOG.md index ec05f40..84924a9 100644 --- a/Encelado/CHANGELOG.md +++ b/Encelado/CHANGELOG.md @@ -10,6 +10,7 @@ Formato: una voce per sessione di lavoro, con data. Le voci più recenti in alto - **Strumento**: comando `learn` (ciclo di apprendimento in ombra su un ledger esportato). - **Pulizia**: rimossi WPF, DPAPI, i test di rendering e di binding, ~45 membri mai usati nel Core e nell'Engine (statistiche di ricerca, `Psi`, `EmptyContextProvider`, `EnvironmentKeyStore`, display helper dello snapshot, `INotifyPropertyChanged`), `build/Encelado.iss`. `PROMPT.md` → `docs/PROMPT_5.0.md`. - **Documenti**: ADR-0007, ADR-0008, `docs/DOCKER.md`, `docs/UI_GUIDELINES.md`, `README.md`, `CLAUDE.md`, ARCHITECTURE, RUNBOOK, KNOWN_ISSUES, GLOSSARY, DATA_SOURCES, ML_AND_LEARNING, QUESTIONS (D-28, D-29, D-36, D-37), POSTMORTEM (gli accrediti del demo erano uno per ordine ridotto), skill, `build/README.md`. +- **Avvio in locale**: sandbox `deploy/local/` con la stessa disposizione del container (`ENCELADO_DATA_DIR` vale anche fuori dal container), F5 in VS Code sulla sandbox, script `scripts/run-dev`, `run-docker`, `screenshots` (`.ps1` e `.sh`), attività di VS Code per avviare, fermare e fotografare. Arresto pulito su SIGTERM (`PosixSignalRegistration`) verificato con `docker stop`; `/api/health` in `--sample` non finge un motore acceso. - Test nuovi: `EmbeddedUiTests`, `WebHostTests` (token, cookie, stream, storico), `HistoryBuilderTests`, `KeyStoreTests`; 210 verdi, anche su Linux dentro `docker build`. Versione di sviluppo 5.0.0. ## 2026-09-23 — 5.0, apprendimento in ombra (ADR-0006) e skill di progetto diff --git a/Encelado/CLAUDE.md b/Encelado/CLAUDE.md index e351f97..d5b2da7 100644 --- a/Encelado/CLAUDE.md +++ b/Encelado/CLAUDE.md @@ -45,10 +45,10 @@ deploy/docker, deploy/unraid entrypoint del container, template Unraid - **Tempo**: UTC ovunque; conversione solo in UI. `CultureInfo.InvariantCulture` per ogni parsing e formattazione su file. - **Concorrenza**: un solo thread di decisione; I/O asincrono; `Channel` fra ingestion, strategia, esecuzione e UI. - **Riproducibilità**: seed fisso 42 per ogni componente stocastica; ogni run scrive `run_id`, hash della configurazione e versione del codice nel ledger. -- **Cartelle a runtime**: nel container `/config` (encelado.json, strategy.json, instruments.json, `etoro.keys.enc`, file `STOP`) e `/data` (`data/`, `knowledge/`, `reports/`, `results/`, `logs/`); fuori dal container (solo sviluppo) `Documenti\Encelado\` o `ENCELADO_CONFIG_DIR`. Credenziali solo nelle variabili `ETORO_API_KEY`/`ETORO_USER_KEY` o nel file cifrato (AES-256-GCM, passphrase `ENCELADO_KEY_PASSPHRASE`); token Telegram solo in `TELEGRAM_BOT_TOKEN` (chat in `TELEGRAM_CHAT_ID`); token dell'interfaccia in `ENCELADO_WEB_TOKEN`. +- **Cartelle a runtime**: nel container `/config` (encelado.json, strategy.json, instruments.json, `etoro.keys.enc`, file `STOP`) e `/data` (`data/`, `knowledge/`, `reports/`, `results/`, `logs/`); fuori dal container (solo sviluppo) la sandbox `deploy/local/` (`ENCELADO_CONFIG_DIR` + `ENCELADO_DATA_DIR`, usata da F5 e dagli script) o, senza variabili, `Documenti\Encelado\`. Credenziali solo nelle variabili `ETORO_API_KEY`/`ETORO_USER_KEY` o nel file cifrato (AES-256-GCM, passphrase `ENCELADO_KEY_PASSPHRASE`); token Telegram solo in `TELEGRAM_BOT_TOKEN` (chat in `TELEGRAM_CHAT_ID`); token dell'interfaccia in `ENCELADO_WEB_TOKEN`. - **Esecuzione (5.0)**: ogni ordine entra nel registro `data/state/pending_orders.json` prima dell'HTTP e l'esito si legge per `orderId` (il server non registra il `referenceId`); una gamba senza esito porta il basket in `PendingA`/`PendingB`, mai in un rifiuto; ogni posizione del conto è `basket`, `orfana-bot` (adottata e chiusa) o `esterna` (mai toccata); la size è il minimo fra rischio e margine (`strategy.json` → `risk`); il kill-switch verifica la piattezza sul conto e dichiara `Halted-Residuo` se resta qualcosa; heartbeat e recupero dopo inattività (`recovery`); un solo bot per cartella dati (`instance.lock`). - **Interfaccia**: navigation rail a sinistra (Dashboard, Storico ordini, Log, Impostazioni), barra in alto (titolo, ora UTC e nel fuso, valuta, AVVIA/FERMA, KILL-SWITCH), pagine sotto. Nella dashboard solo le informazioni principali; i dettagli nei tooltip e nel log. La UI non decide niente: legge lo snapshot via SSE e manda comandi via `POST /api/commands/`. Gli orari a schermo sono nel fuso scelto (`ui.timeZone`, `TZ`); il log porta l'offset, il ledger è UTC. Regole complete in `docs/UI_GUIDELINES.md`. -- **Verifica visiva**: `dotnet run --project src/Encelado.Server -- --sample --port 8085` e screenshot con Edge headless (`docs/UI_GUIDELINES.md`); i test `EmbeddedUiTests` e `WebHostTests` coprono HTML, JSON, token e stream. +- **Verifica visiva**: `scripts\screenshots.ps1` (server `--sample` + Edge headless, `docs/UI_GUIDELINES.md`); i test `EmbeddedUiTests` e `WebHostTests` coprono HTML, JSON, token e stream. ## Comandi @@ -57,6 +57,9 @@ dotnet build Encelado.slnx # compilazione dotnet test tests/Encelado.Tests --no-restore # test (xunit) dotnet msbuild build/Release.proj -t:Verifica # compilazione + test nella cartella di verifica dotnet run --project src/Encelado.Server -- [--no-autostart] [--sample] [--port 8080] [--minutes 240] [--bonifica] [--confirm-live "CONFERMO LIVE"] # server locale; comandi da tastiera: status, close, kill, residuo, preset, reset, bonifica, stop +scripts\run-dev.ps1 [-Sample] [-Autostart] # server locale nella sandbox deploy/local (= F5 in VS Code); .sh per Git Bash +scripts\run-docker.ps1 [-Down] # il container dalla compose, stessa sandbox, token "sviluppo" +scripts\screenshots.ps1 # docs/img dal server campione dotnet run --project tools/Encelado.Backtest -- ticks --data "A:\Download\Trading" --out "%USERPROFILE%\Documents\Encelado\data\market" dotnet run --project tools/Encelado.Backtest -- baskets --data "%USERPROFILE%\Documents\Encelado\data\market" --out results dotnet run --project tools/Encelado.Backtest -- falsify --data "%USERPROFILE%\Documents\Encelado\data\market" --out reports [--costs api] diff --git a/Encelado/README.md b/Encelado/README.md index 9ddc286..4bbbce3 100644 --- a/Encelado/README.md +++ b/Encelado/README.md @@ -30,18 +30,27 @@ Senza token il server ascolta solo su localhost del container. La modalità `Liv | ![Impostazioni](docs/img/impostazioni.png) | ![Telefono](docs/img/mobile.png) | | **Impostazioni**: configurazione validata, chiavi eToro cifrate, ripristino in cinque passi, Ricerca (modello in ombra, ciclo di apprendimento), Diagnostica (percorsi, quote, bonifica), Informazioni (versione, build, commit). | Sotto i 600 px il rail diventa un menu e i basket diventano cards. | -## Sviluppo +## Provarlo in locale + +Tre modi, tutti sulla stessa sandbox `deploy/local/` (ignorata da git: `config/` è il `/config` del container, `data/` il `/data`), così configurazione e chiavi salvate valgono ovunque: + +| Modo | Come | Cosa ottieni | +|---|---|---| +| **F5 in VS Code** | configurazione `Encelado (server)` | server locale su `http://localhost:8080/`, browser aperto da solo, motore fermo finché non premi **AVVIA**; `Encelado (campione)` mostra le pagine con dati finti senza chiavi | +| **Script** | `scripts\run-dev.ps1` (`-Sample`, `-Autostart`, `-Port`) o `sh scripts/run-dev.sh` | lo stesso processo da terminale, con i comandi da tastiera (`status`, `kill`, `stop`…) | +| **Container** | `scripts\run-docker.ps1` (o `sh scripts/run-docker.sh`, o `docker compose up -d --build`) | l'immagine vera su `http://localhost:8080/`, token `sviluppo`; `-Down` per fermare con SIGTERM | + +Le chiavi eToro: variabili `ETORO_API_KEY`/`ETORO_USER_KEY` nell'ambiente (per il compose in un file `.env` accanto alla `docker-compose.yml`), oppure **Impostazioni ▸ Chiavi eToro** nella pagina con `ENCELADO_KEY_PASSPHRASE` impostata (vengono verificate e salvate cifrate in `deploy/local/config/etoro.keys.enc`). Senza chiavi il motore non parte e la pagina lo dice; lo storico mostra solo il ledger. ```powershell dotnet build Encelado.slnx # compilazione dotnet test tests/Encelado.Tests --no-restore # test (xunit) -dotnet run --project src/Encelado.Server -- --no-autostart # server locale (Documenti\Encelado), poi http://localhost:8080/ -dotnet run --project src/Encelado.Server -- --sample # interfaccia con dati finti, senza chiavi dotnet msbuild build/Release.proj -t:Verifica # compilazione + test nella cartella di verifica dotnet msbuild build/Release.proj -t:Docker # l'immagine, con i test dentro la build +scripts\screenshots.ps1 # rigenera docs/img dal server campione ``` -In VS Code: **F5** (`Encelado (server)` apre il browser da solo; `Encelado (campione)` per lavorare sulle pagine), attività `verifica`, `backtest`, `immagine docker`, `pacchetto`, `rilascia su Gitea`. +Attività di VS Code: `verifica`, `backtest`, `avvia in locale (script)`, `avvia in docker`, `ferma docker`, `screenshot`, `immagine docker`, `pacchetto`, `rilascia su Gitea`. ``` src/Encelado.Core logica pura: basket, cross sintetici, decisore, cost gate, sizing, esecutore, registro ordini, kill-switch, apprendimento, notifiche @@ -50,7 +59,8 @@ src/Encelado.Engine motore, ledger, feed, configurazione, impostazioni, stori src/Encelado.Server l'eseguibile: Kestrel, API JSON, SSE, interfaccia incorporata (Material 3, vanilla) tools/Encelado.Backtest ticks, baskets, falsify, learn tests/Encelado.Tests xunit -deploy/ docker (entrypoint), unraid (template), local (ignorato: volumi del compose) +deploy/ docker (entrypoint), unraid (template), local (ignorato: sandbox di F5 e del compose) +scripts/ run-dev, run-docker, screenshots (.ps1 e .sh) build/ catena di verifica, pacchetto e rilascio (MSBuild) docs/ STATE, ARCHITECTURE, STRATEGY, RUNBOOK, DOCKER, UI_GUIDELINES, ADR… ``` diff --git a/Encelado/docker-compose.yml b/Encelado/docker-compose.yml index 69101e9..39a9a3b 100644 --- a/Encelado/docker-compose.yml +++ b/Encelado/docker-compose.yml @@ -1,10 +1,11 @@ # Encelado in locale con Docker Compose: immagine costruita dalla Dockerfile alla # radice, cartelle di configurazione e dati sotto deploy/local (ignorate da git). # -# docker compose up --build # costruisce e avvia, log a video -# docker compose up -d # in background +# docker compose up --build # costruisce e avvia, log a video (Ctrl+C = stop pulito) +# docker compose up -d --build # in background → http://localhost:8080/ (token "sviluppo") # docker compose logs -f encelado # segue il log # docker compose down # ferma (SIGTERM: stop pulito del motore) +# Oppure: scripts/run-docker.ps1 (o .sh), che fa le stesse cose e stampa l'indirizzo. # # Le chiavi eToro e il token Telegram NON stanno qui: mettili in un file .env # accanto a questo (ignorato da git) con ETORO_API_KEY=…, ETORO_USER_KEY=…, @@ -38,8 +39,9 @@ services: ENCELADO_CONFIRM_LIVE: ${ENCELADO_CONFIRM_LIVE:-} ENCELADO_DISPLAY_CURRENCY: ${ENCELADO_DISPLAY_CURRENCY:-USD} # Senza token l'interfaccia è raggiungibile solo da localhost del container: - # per usarla dal browser il token serve. - ENCELADO_WEB_TOKEN: ${ENCELADO_WEB_TOKEN:-} + # per usarla dal browser il token serve. "sviluppo" è il default delle prove + # in locale; in .env mettine uno vero. + ENCELADO_WEB_TOKEN: ${ENCELADO_WEB_TOKEN:-sviluppo} ENCELADO_KEY_PASSPHRASE: ${ENCELADO_KEY_PASSPHRASE:-} ETORO_API_KEY: ${ETORO_API_KEY:-} ETORO_USER_KEY: ${ETORO_USER_KEY:-} diff --git a/Encelado/docs/DOCKER.md b/Encelado/docs/DOCKER.md index e971849..0892b0a 100644 --- a/Encelado/docs/DOCKER.md +++ b/Encelado/docs/DOCKER.md @@ -67,6 +67,16 @@ Il server ascolta su tutte le interfacce **solo** quando `ENCELADO_WEB_TOKEN` è Il Gitea dell'utente è raggiunto in HTTP: chi fa `docker pull` deve dichiararlo `insecure-registry` (Docker Desktop: *Settings ▸ Docker Engine* → `"insecure-registries": ["192.168.30.23:3000"]`; Unraid: vedi `deploy/unraid/README.md`). Il push dalla catena di rilascio (`-t:Rilascia`) fa `docker login` con il token di `build/gitea.json` e `docker logout` subito dopo. -## Esecuzione senza container +## Prove in locale: la sandbox `deploy/local` -Sconsigliata e non supportata in produzione, ma utile per lo sviluppo: F5 in VS Code (`Encelado (server)`) o `dotnet run --project src/Encelado.Server -- --no-autostart`. Configurazione e dati vanno in `Documenti\Encelado` (o `ENCELADO_CONFIG_DIR`). `Encelado (campione)` avvia il server con `--sample`: snapshot finto, nessuna chiave, per lavorare sulle pagine e fotografarle. +`deploy/local/config` e `deploy/local/data` (ignorate da git) sono il `/config` e il `/data` delle prove sul PC, in tutti e tre i modi di avvio: + +| Modo | Comando | Note | +|---|---|---| +| F5 in VS Code | `Encelado (server)` | processo locale, `ENCELADO_CONFIG_DIR`/`ENCELADO_DATA_DIR` puntati alla sandbox, browser aperto da solo, motore fermo finché non premi AVVIA (senza token: solo localhost, nessun login) | +| script | `scripts\run-dev.ps1` / `sh scripts/run-dev.sh` (`--sample`, `--autostart`) | come F5 ma da terminale, con i comandi da tastiera | +| container | `scripts\run-docker.ps1` / `sh scripts/run-docker.sh` / `docker compose up -d --build` | l'immagine vera; token `sviluppo` salvo `ENCELADO_WEB_TOKEN` in `.env`; `--down` ferma con SIGTERM | + +`ENCELADO_DATA_DIR` vale anche fuori dal container: separa i dati dalla configurazione come fa `/data`. Le chiavi salvate da Impostazioni finiscono in `deploy/local/config/etoro.keys.enc` e valgono per F5 e per il container (stessa `ENCELADO_KEY_PASSPHRASE`). `Encelado (campione)` e `--sample` servono lo snapshot finto: nessuna chiave, per lavorare sulle pagine e fotografarle (`scripts\screenshots.ps1`). + +Fuori dalla sandbox, senza variabili, il processo locale usa `Documenti\Encelado`: sconsigliato, è la cartella dell'installazione 4.0.0. diff --git a/Encelado/docs/RUNBOOK.md b/Encelado/docs/RUNBOOK.md index 60cc7eb..1697fc0 100644 --- a/Encelado/docs/RUNBOOK.md +++ b/Encelado/docs/RUNBOOK.md @@ -12,6 +12,10 @@ Aggiornato: 2026-09-23 (5.0). Come si avvia, si ferma, si sblocca e si ripara il Per Telegram: `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` (vedi la sezione Telegram). +## Sviluppo sul PC + +Per vedere e provare il bot senza Unraid: **F5** in VS Code (`Encelado (server)`), `scripts\run-dev.ps1` o `scripts\run-docker.ps1` (il container dalla compose). Tutti usano la sandbox `deploy/local/` come `/config` e `/data`; dettagli in `docs/DOCKER.md`. Con `--sample` (o `Encelado (campione)`) la pagina mostra dati finti senza chiavi. + ## Modalità | Modalità | Ordini | Conferma all'avvio | diff --git a/Encelado/docs/STATE.md b/Encelado/docs/STATE.md index 535039f..6beeea1 100644 --- a/Encelado/docs/STATE.md +++ b/Encelado/docs/STATE.md @@ -10,22 +10,21 @@ Aggiornato: 2026-09-23 (sessione 5.0, Fasi 6-9 concluse: il piano 5.0 è complet - **Fase 6 — Engine + Server**: `src/Encelado.Bot` → `src/Encelado.Engine` (libreria, senza UI); `src/Encelado.Server` (Sdk.Web, nessun NuGet) con `WebHost` (token in cookie HttpOnly o Bearer, senza token solo localhost), API JSON scritta a mano, SSE `/api/stream` a uno snapshot al secondo, `LogBuffer`, `HistoryService`; chiavi da variabili o file cifrato `etoro.keys.enc` (AES-256-GCM, `ENCELADO_KEY_PASSPHRASE`) al posto di DPAPI; `AppPaths` con `/config` e `/data`; `HeadlessRunner` ridotto a console (stato, comandi, bonifica); `--sample`, `--health`, `--no-autostart`, `ENCELADO_AUTOSTART`, `ENCELADO_CONFIRM_LIVE`. - **Fase 7 — interfaccia web Material 3** (`Web/wwwroot`, vanilla): navigation rail 80/256 px (drawer sotto 600 px), barra con orologi UTC e fuso, selettore della valuta (USD, EUR, GBP, CHF, JPY, AUD, CAD, NZD con tasso dalle quotazioni e tooltip in USD), AVVIA/FERMA, KILL-SWITCH con «chiudi anche le esterne»; dashboard con sei KPI (incluso il margine), tabella dei basket (cards da telefono), card evento/eToro/Telegram, attività, banner; **Storico ordini** (ordini, posizioni classificate, profitti per periodo con curva SVG, CSV); Log con filtri; Impostazioni con configurazione validata, chiavi eToro, ripristino in cinque passi, Ricerca, Diagnostica (con bonifica), Informazioni (versione, build, commit da `AssemblyMetadata`); temi scuro/chiaro. `HistoryBuilder` nel Core; `SnapshotJson`, `SampleSnapshot`, `SettingsService` nell'Engine; comando `learn` nello strumento (`Learn.cs`); `ui.displayCurrency/theme/navExpanded` e coppie di conversione nel polling. -- **Fase 8 — Docker e Unraid**: `Dockerfile` multi-stage (build, **test**, publish, runtime `aspnet:10.0` + gosu + tzdata), `deploy/docker/entrypoint.sh` (TZ, PUID/PGID), `docker-compose.yml`, `.dockerignore`, `HEALTHCHECK --health`; `deploy/unraid/encelado.xml` (Container v2, icona `assets/encelado.png` raw da Gitea, D-29) e README; `build/Release.proj` senza Inno Setup: `Pubblica` framework-dependent, `Docker`, `Pacchetto` (zip + template + tag), `Rilascia` (push dell'immagine sul registro di Gitea + release); VS Code con F5 sul server (`Encelado (server)`, `Encelado (campione)`, backtest) e attività Docker. Immagine `encelado:dev` costruita in locale con i test verdi nello stadio Linux e provata (avvio, semina di `/config`, permessi 99:100, `/api/health`, token, healthcheck). Il primo arresto con `docker stop` **non** era pulito (SIGKILL dopo la grazia): corretto con `PosixSignalRegistration` per SIGTERM in `Program.cs`; la correzione compila ed è committata ma **non è stata riprovata nel container** perché il disco C: del PC si è riempito (0 byte liberi) e il demone Docker ha iniziato a dare errori di I/O. +- **Fase 8 — Docker e Unraid**: `Dockerfile` multi-stage (build, **test**, publish, runtime `aspnet:10.0` + gosu + tzdata), `deploy/docker/entrypoint.sh` (TZ, PUID/PGID), `docker-compose.yml`, `.dockerignore`, `HEALTHCHECK --health`; `deploy/unraid/encelado.xml` (Container v2, icona `assets/encelado.png` raw da Gitea, D-29) e README; `build/Release.proj` senza Inno Setup: `Pubblica` framework-dependent, `Docker`, `Pacchetto` (zip + template + tag), `Rilascia` (push dell'immagine sul registro di Gitea + release); VS Code con F5 sul server (`Encelado (server)`, `Encelado (campione)`, backtest) e attività Docker. Immagine `192.168.30.23:3000/alby96/encelado:5.0.0` (346 MB) costruita dalla catena (`-t:Docker -p:Versione=5.0.0`, senza tag) con 210 test verdi nello stadio Linux e provata: avvio, semina di `/config`, permessi 99:100, `/api/health`, token, sonda `--health`, nessun avviso di Kestrel o di `useradd`. Il primo arresto con `docker stop` **non** era pulito (SIGKILL dopo la grazia): corretto con `PosixSignalRegistration` per SIGTERM in `Program.cs` e **verificato**: `docker stop` chiude in 1 s con «SIGTERM: arresto» ed exit 0. +- **Avvio in locale (richiesta dell'utente)**: sandbox `deploy/local/` (`config/` = `/config`, `data/` = `/data`, ignorata da git) usata da F5 (`Encelado (server)`, `ENCELADO_CONFIG_DIR` + `ENCELADO_DATA_DIR`, che ora vale anche fuori dal container), dagli script `scripts/run-dev.ps1|.sh`, `run-docker.ps1|.sh` (compose, token `sviluppo`), `screenshots.ps1|.sh` e dalle attività di VS Code (`avvia in locale (script)`, `avvia in docker`, `ferma docker`, `screenshot`); provati tutti e tre i modi e lo script degli screenshot. `/api/health` in `--sample` non finge più un motore acceso. - **Fase 9 — documenti e pulizia**: ADR-0007, ADR-0008, `docs/DOCKER.md`, `docs/UI_GUIDELINES.md`, `README.md` con gli screenshot (`docs/img/`), `CLAUDE.md`, ARCHITECTURE, RUNBOOK, KNOWN_ISSUES, GLOSSARY, DATA_SOURCES, ML_AND_LEARNING, QUESTIONS (D-28, D-29, D-36, D-37 risposte), POSTMORTEM (gli «accrediti» erano crediti demo per ogni ordine ridotto), skill aggiornate, `build/README.md`. **Codice morto rimosso**: progetto WPF, DPAPI, test WPF, `UiRenderTests`, ~45 membri mai usati (statistiche di ricerca, `Psi`, `EmptyContextProvider`, `EnvironmentKeyStore`, display helper dello snapshot, `INotifyPropertyChanged` in `SettingField`, …). Test nuovi: `EmbeddedUiTests`, `WebHostTests`, `HistoryBuilderTests`, `KeyStoreTests`; suite a **210 verdi**. ## Prossimi passi -1. Liberare spazio su C:, riavviare Docker, ricostruire l'immagine e verificare l'arresto pulito con `docker stop` (vedi problemi aperti). -2. **Rilascio 5.0.0** (utente): push del ramo, `-t:Rilascia -p:Versione=5.0.0` (richiede Docker acceso, `build/gitea.json` con permesso `package`, registro `192.168.30.23:3000` dichiarato `insecure-registry`), poi installazione su Unraid dal template. -3. Riaccendere il Demo nel container per 24 ore di verifica: contatore «orfane» a 0, `orders.jsonl` senza `Unknown` irrisolti, `sizing_bound = margin` sui primi ingressi; kill-switch a mano con verifica di piattezza; fermare il container un'ora con un basket aperto per vedere il recupero; messaggi Telegram reali. -4. Revisione visiva dell'interfaccia con l'utente (gli screenshot sono dal campione `--sample`); eventuali ritocchi ai testi e ai tooltip. -5. Avviso Telegram sui feed fermi da più di un'ora (resta in KNOWN_ISSUES). -6. Rifare il backtest (`backtest baskets`) con i limiti di margine e aggiornare §3 di `docs/STRATEGY.md`. +1. **Rilascio 5.0.0** (utente): push del ramo, `-t:Rilascia -p:Versione=5.0.0` (richiede Docker acceso, `build/gitea.json` con permesso `package`, registro `192.168.30.23:3000` dichiarato `insecure-registry`), poi installazione su Unraid dal template. +2. Riaccendere il Demo nel container per 24 ore di verifica: contatore «orfane» a 0, `orders.jsonl` senza `Unknown` irrisolti, `sizing_bound = margin` sui primi ingressi; kill-switch a mano con verifica di piattezza; fermare il container un'ora con un basket aperto per vedere il recupero; messaggi Telegram reali. +3. Revisione visiva dell'interfaccia con l'utente (gli screenshot sono dal campione `--sample`); eventuali ritocchi ai testi e ai tooltip. +4. Avviso Telegram sui feed fermi da più di un'ora (resta in KNOWN_ISSUES). +5. Rifare il backtest (`backtest baskets`) con i limiti di margine e aggiornare §3 di `docs/STRATEGY.md`. ## Problemi aperti -- **Il disco C: del PC di sviluppo è pieno** (0 byte liberi a fine sessione): Docker Desktop ha dato errori di I/O e l'ultima immagine non si è costruita. Liberare spazio, riavviare Docker Desktop, rifare `dotnet msbuild build/Release.proj -t:Docker` e riprovare `docker stop` (deve loggare «SIGTERM: arresto» e uscire con 0). - +- Il disco C: del PC di sviluppo si era riempito (0 byte) a metà sessione; ora ha 12 GB liberi. Le immagini e la cache di build di Docker pesano: `docker system prune` ogni tanto. - Backtest negativo: la strategia non regge i costi (`docs/STRATEGY.md`). - Il conto reale vale 224,90 USD: il Live non è praticabile. - Google News blocca le ricerche RSS; RBA 403 a intermittenza; Fed 404 a tratti. diff --git a/Encelado/docs/UI_GUIDELINES.md b/Encelado/docs/UI_GUIDELINES.md index 92b569b..2d5e125 100644 --- a/Encelado/docs/UI_GUIDELINES.md +++ b/Encelado/docs/UI_GUIDELINES.md @@ -55,8 +55,8 @@ Angoli 12 px (card, banner), 16 px (dialoghi grandi 28 px come da M3), pulsanti | Classe | Larghezza | Layout | |---|---|---| | compact | < 600 px | rail nascosto (menu ☰), KPI su due colonne, basket a cards, orologi nascosti | -| medium | 600-839 px | rail 80 px, KPI su tre colonne, contesto su una colonna, impostazioni su una colonna | -| expanded | ≥ 840 px | rail 80/256 px, KPI su sei colonne, contesto su tre, impostazioni su due | +| medium | 600-839 px | rail 80 px, KPI su due colonne, contesto su una colonna, impostazioni su una colonna | +| expanded | ≥ 840 px | rail 80/256 px, KPI su tre colonne (due righe), contesto su tre, impostazioni su due | ### Accessibilità @@ -76,7 +76,7 @@ Tastiera completa (Tab su rail, pulsanti, tabelle, dialoghi; Esc chiude drawer e ## Screenshot -`docs/img/` (dashboard, storico, log, impostazioni, mobile), presi dal server in modalità campione: +`docs/img/` (dashboard, storico, log, impostazioni, mobile), presi dal server in modalità campione con `scripts\screenshots.ps1` (o `sh scripts/screenshots.sh`): avvia il server con `--sample` su una porta libera, apre ogni pagina con Edge headless (`?once=1`, 1440×900 e 420×860) e lo ferma. A mano, una pagina: ```powershell dotnet run --project src/Encelado.Server -- --sample --port 8085 diff --git a/Encelado/docs/img/dashboard.png b/Encelado/docs/img/dashboard.png index 5a0bb0d..5df138b 100644 Binary files a/Encelado/docs/img/dashboard.png and b/Encelado/docs/img/dashboard.png differ diff --git a/Encelado/docs/img/impostazioni.png b/Encelado/docs/img/impostazioni.png index feb2f16..71b2675 100644 Binary files a/Encelado/docs/img/impostazioni.png and b/Encelado/docs/img/impostazioni.png differ diff --git a/Encelado/docs/img/log.png b/Encelado/docs/img/log.png index 3a0cb51..957c5e9 100644 Binary files a/Encelado/docs/img/log.png and b/Encelado/docs/img/log.png differ diff --git a/Encelado/docs/img/storico.png b/Encelado/docs/img/storico.png index eda2c25..5e96d41 100644 Binary files a/Encelado/docs/img/storico.png and b/Encelado/docs/img/storico.png differ diff --git a/Encelado/scripts/run-dev.ps1 b/Encelado/scripts/run-dev.ps1 new file mode 100644 index 0000000..24ef74c --- /dev/null +++ b/Encelado/scripts/run-dev.ps1 @@ -0,0 +1,42 @@ +<# +.SYNOPSIS + Compila e avvia Encelado in locale, senza container, nella sandbox deploy/local. + +.DESCRIPTION + Stessa disposizione del container: deploy/local/config (encelado.json, strategy.json, + chiavi cifrate) e deploy/local/data (ledger, stato, log). La cartella è ignorata da git. + Senza ENCELADO_WEB_TOKEN il server ascolta solo su localhost e non chiede il login. + Le chiavi eToro: variabili ETORO_API_KEY/ETORO_USER_KEY già nell'ambiente, oppure + Impostazioni ▸ Chiavi eToro con ENCELADO_KEY_PASSPHRASE impostata. + + scripts\run-dev.ps1 # motore fermo: premi AVVIA nella pagina + scripts\run-dev.ps1 -Autostart # motore avviato subito (servono le chiavi) + scripts\run-dev.ps1 -Sample # dati finti, nessuna chiave: per guardare le pagine + scripts\run-dev.ps1 -Port 8090 +#> +param( + [int] $Port = 8080, + [switch] $Autostart, + [switch] $Sample, + [switch] $NoBrowser +) +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +Set-Location $root + +$env:ENCELADO_CONFIG_DIR = Join-Path $root "deploy\local\config" +$env:ENCELADO_DATA_DIR = Join-Path $root "deploy\local\data" +$env:ENCELADO_WEB_PORT = "$Port" +New-Item -ItemType Directory -Force $env:ENCELADO_CONFIG_DIR, $env:ENCELADO_DATA_DIR | Out-Null + +dotnet build src/Encelado.Server -c Debug -nologo -v q +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +$args = @() +if ($Sample) { $args += "--sample" } elseif (-not $Autostart) { $args += "--no-autostart" } +$url = "http://localhost:$Port/" +Write-Host "Encelado in locale: $url (configurazione in deploy\local\config, dati in deploy\local\data)" -ForegroundColor Cyan +Write-Host "Comandi da tastiera: status, close , kill, residuo, preset , reset , bonifica, stop. Ctrl+C = arresto." -ForegroundColor DarkGray +if (-not $NoBrowser) { Start-Job -ScriptBlock { param($u) Start-Sleep -Seconds 3; Start-Process $u } -ArgumentList $url | Out-Null } + +dotnet src/Encelado.Server/bin/Debug/net10.0/Encelado.Server.dll @args diff --git a/Encelado/scripts/run-dev.sh b/Encelado/scripts/run-dev.sh new file mode 100644 index 0000000..e9064be --- /dev/null +++ b/Encelado/scripts/run-dev.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh +# Builds and starts Encelado locally, without the container, in the deploy/local sandbox +# (deploy/local/config as /config, deploy/local/data as /data; ignored by git). +# sh scripts/run-dev.sh # engine stopped: press AVVIA in the page +# sh scripts/run-dev.sh --sample # fake data, no keys: to look at the pages +# sh scripts/run-dev.sh --autostart # engine started at once (keys needed) +# Without ENCELADO_WEB_TOKEN the server listens on localhost only and asks no login. +set -eu +cd "$(dirname "$0")/.." +export ENCELADO_CONFIG_DIR="$(pwd)/deploy/local/config" +export ENCELADO_DATA_DIR="$(pwd)/deploy/local/data" +export ENCELADO_WEB_PORT="${ENCELADO_WEB_PORT:-8080}" +mkdir -p "$ENCELADO_CONFIG_DIR" "$ENCELADO_DATA_DIR" + +MODE="--no-autostart" +for a in "$@"; do + case "$a" in + --sample) MODE="--sample" ;; + --autostart) MODE="" ;; + esac +done + +dotnet build src/Encelado.Server -c Debug -nologo -v q +echo "Encelado in locale: http://localhost:$ENCELADO_WEB_PORT/ (config in deploy/local/config, dati in deploy/local/data)" +exec dotnet src/Encelado.Server/bin/Debug/net10.0/Encelado.Server.dll $MODE diff --git a/Encelado/scripts/run-docker.ps1 b/Encelado/scripts/run-docker.ps1 new file mode 100644 index 0000000..d035c7b --- /dev/null +++ b/Encelado/scripts/run-docker.ps1 @@ -0,0 +1,40 @@ +<# +.SYNOPSIS + Costruisce l'immagine e avvia Encelado nel container con la docker-compose.yml alla radice. + +.DESCRIPTION + Le cartelle deploy/local/config e deploy/local/data vengono montate come /config e /data: + sono le stesse di scripts\run-dev.ps1, quindi configurazione e chiavi salvate valgono + in entrambi i modi. Il token dell'interfaccia è "sviluppo" salvo ENCELADO_WEB_TOKEN in + un file .env accanto alla compose (dove vanno anche ETORO_API_KEY, ETORO_USER_KEY, + TELEGRAM_*). Segue il log finché non premi Ctrl+C; il container resta acceso: + scripts\run-docker.ps1 -Down per fermarlo (SIGTERM, stop pulito). + + scripts\run-docker.ps1 # build + up -d + log + scripts\run-docker.ps1 -NoBuild # senza ricostruire l'immagine + scripts\run-docker.ps1 -Down # docker compose down +#> +param( + [switch] $NoBuild, + [switch] $Down, + [switch] $NoBrowser +) +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +Set-Location $root + +if ($Down) { docker compose down; exit $LASTEXITCODE } + +New-Item -ItemType Directory -Force "deploy\local\config", "deploy\local\data" | Out-Null +$env:GIT_COMMIT = (git rev-parse --short=12 HEAD 2>$null); if (-not $env:GIT_COMMIT) { $env:GIT_COMMIT = "n/d" } +$env:PUID = "1000"; $env:PGID = "1000" + +if ($NoBuild) { docker compose up -d } else { docker compose up -d --build } +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +$port = if ($env:ENCELADO_WEB_PORT) { $env:ENCELADO_WEB_PORT } else { "8080" } +$token = if ($env:ENCELADO_WEB_TOKEN) { "(da ENCELADO_WEB_TOKEN)" } else { "sviluppo" } +$url = "http://localhost:$port/" +Write-Host "Encelado nel container: $url token: $token (docker compose down per fermare)" -ForegroundColor Cyan +if (-not $NoBrowser) { Start-Job -ScriptBlock { param($u) Start-Sleep -Seconds 4; Start-Process $u } -ArgumentList $url | Out-Null } +docker compose logs -f encelado diff --git a/Encelado/scripts/run-docker.sh b/Encelado/scripts/run-docker.sh new file mode 100644 index 0000000..1b0da4d --- /dev/null +++ b/Encelado/scripts/run-docker.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env sh +# Builds the image and starts Encelado in the container with the docker-compose.yml at the root. +# deploy/local/config and deploy/local/data are mounted as /config and /data (the same +# folders scripts/run-dev.sh uses). Token "sviluppo" unless ENCELADO_WEB_TOKEN is in .env. +# sh scripts/run-docker.sh # build + up -d + follow the log (Ctrl+C leaves it running) +# sh scripts/run-docker.sh --no-build +# sh scripts/run-docker.sh --down # docker compose down (SIGTERM, clean stop) +set -eu +cd "$(dirname "$0")/.." +case "${1:-}" in + --down) exec docker compose down ;; +esac +mkdir -p deploy/local/config deploy/local/data +export GIT_COMMIT="$(git rev-parse --short=12 HEAD 2>/dev/null || echo n/d)" +export PUID="${PUID:-1000}" PGID="${PGID:-1000}" +if [ "${1:-}" = "--no-build" ]; then docker compose up -d; else docker compose up -d --build; fi +echo "Encelado nel container: http://localhost:${ENCELADO_WEB_PORT:-8080}/ token: ${ENCELADO_WEB_TOKEN:-sviluppo} (sh scripts/run-docker.sh --down per fermare)" +exec docker compose logs -f encelado diff --git a/Encelado/scripts/screenshots.ps1 b/Encelado/scripts/screenshots.ps1 new file mode 100644 index 0000000..5202d9b --- /dev/null +++ b/Encelado/scripts/screenshots.ps1 @@ -0,0 +1,47 @@ +<# +.SYNOPSIS + Rigenera docs/img/*.png: server in modalità campione (--sample) e Edge headless su ogni pagina. + +.DESCRIPTION + Niente chiavi, niente mercato: lo snapshot è finto (SampleSnapshot). Serve Microsoft Edge. + La pagina viene aperta con ?once=1 (una sola lettura dello snapshot, nessuno stream), altrimenti + il browser headless non considera mai il caricamento concluso. + + scripts\screenshots.ps1 [-Port 58085] +#> +param([int] $Port = 58085) +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +Set-Location $root +$edge = @("$env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1 +if (-not $edge) { throw "Microsoft Edge non trovato" } + +dotnet build src/Encelado.Server -c Debug -nologo -v q +$work = Join-Path $env:TEMP ("encelado-shot-" + [guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Force "$work\config", "docs\img" | Out-Null +$env:ENCELADO_CONFIG_DIR = "$work\config" +$server = Start-Process -FilePath "dotnet" -ArgumentList "src/Encelado.Server/bin/Debug/net10.0/Encelado.Server.dll", "--sample", "--port", "$Port" -PassThru -WindowStyle Hidden +try { + $ready = $false + for ($i = 0; $i -lt 30 -and -not $ready; $i++) { Start-Sleep -Seconds 1; try { $ready = (Invoke-WebRequest -UseBasicParsing "http://127.0.0.1:$Port/api/health" -TimeoutSec 2).StatusCode -eq 200 } catch {} } + if (-not $ready) { throw "il server campione non risponde sulla porta $Port" } + $shots = @( + @{ name = "dashboard"; page = "dashboard"; size = "1440,900" }, + @{ name = "storico"; page = "storico"; size = "1440,900" }, + @{ name = "log"; page = "log"; size = "1440,900" }, + @{ name = "impostazioni"; page = "impostazioni"; size = "1440,900" }, + @{ name = "mobile"; page = "dashboard"; size = "420,860" } + ) + foreach ($s in $shots) { + $out = Join-Path $root ("docs\img\" + $s.name + ".png") + $profile = Join-Path $work ("edge-" + $s.name) + # Start-Process: Edge scrive avvisi su stderr e in PowerShell 5.1 diventerebbero errori bloccanti. + $edgeArgs = @("--headless=new", "--disable-gpu", "--no-first-run", "--hide-scrollbars", "--user-data-dir=$profile", "--window-size=$($s.size)", "--virtual-time-budget=5000", "--screenshot=$out", "http://127.0.0.1:$Port/?once=1#/$($s.page)") + Start-Process -FilePath $edge -ArgumentList $edgeArgs -Wait -NoNewWindow -RedirectStandardError "$profile.err" -RedirectStandardOutput "$profile.out" + if (Test-Path $out) { Write-Host (" {0,-14} {1,7:N0} byte" -f $s.name, (Get-Item $out).Length) } else { Write-Warning "$($s.name): screenshot non scritto" } + } +} +finally { + if ($server -and -not $server.HasExited) { Stop-Process -Id $server.Id -Force } + Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/Encelado/scripts/screenshots.sh b/Encelado/scripts/screenshots.sh new file mode 100644 index 0000000..6e6509c --- /dev/null +++ b/Encelado/scripts/screenshots.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh +# Regenerates docs/img/*.png: the server in sample mode (--sample) and a headless Edge/Chrome on every page. +# No keys, no market: the snapshot is fake. Pages are opened with ?once=1 (one snapshot, no stream), +# otherwise the headless browser never considers the page loaded. +# sh scripts/screenshots.sh [port] +set -eu +cd "$(dirname "$0")/.." +PORT="${1:-58085}" +BROWSER="${ENCELADO_BROWSER:-}" +for c in "/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" "/c/Program Files/Microsoft/Edge/Application/msedge.exe" "$(command -v microsoft-edge || true)" "$(command -v google-chrome || true)" "$(command -v chromium || true)"; do + [ -n "$BROWSER" ] && break + [ -n "$c" ] && [ -x "$c" ] && BROWSER="$c" +done +[ -n "$BROWSER" ] || { echo "nessun browser Chromium trovato (imposta ENCELADO_BROWSER)" >&2; exit 1; } + +dotnet build src/Encelado.Server -c Debug -nologo -v q +WORK="$(mktemp -d)" +trap '[ -n "${PID:-}" ] && kill "$PID" 2>/dev/null; rm -rf "$WORK"' EXIT +mkdir -p "$WORK/config" docs/img +ENCELADO_CONFIG_DIR="$WORK/config" dotnet src/Encelado.Server/bin/Debug/net10.0/Encelado.Server.dll --sample --port "$PORT" >/dev/null 2>&1 & +PID=$! +i=0; until curl -sf "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; do i=$((i+1)); [ "$i" -lt 30 ] || { echo "il server campione non risponde" >&2; exit 1; }; sleep 1; done + +shot() { # shot + "$BROWSER" --headless=new --disable-gpu --no-first-run --hide-scrollbars "--user-data-dir=$WORK/edge-$1" "--window-size=$3" --virtual-time-budget=5000 "--screenshot=$(pwd)/docs/img/$1.png" "http://127.0.0.1:$PORT/?once=1#/$2" >/dev/null 2>&1 || true + [ -s "docs/img/$1.png" ] && echo " $1: $(wc -c < "docs/img/$1.png") byte" || echo " $1: screenshot non scritto" >&2 +} +shot dashboard dashboard 1440,900 +shot storico storico 1440,900 +shot log log 1440,900 +shot impostazioni impostazioni 1440,900 +shot mobile dashboard 420,860 diff --git a/Encelado/src/Encelado.Engine/Configuration/AppPaths.cs b/Encelado/src/Encelado.Engine/Configuration/AppPaths.cs index 15a37de..86c39c3 100644 --- a/Encelado/src/Encelado.Engine/Configuration/AppPaths.cs +++ b/Encelado/src/Encelado.Engine/Configuration/AppPaths.cs @@ -9,9 +9,10 @@ namespace Encelado.Engine.Configuration; /// instruments.json and the encrypted keys; everything the bot produces /// (data/, knowledge/, reports/, logs/) resolves against /// it unless the configuration says otherwise. In the container it is /config -/// (and the data folder /data); anywhere else it is ENCELADO_CONFIG_DIR, -/// or Documenti\Encelado for a developer on Windows, where a backup catches it -/// and a reinstall cannot overwrite it. +/// (and the data folder /data); anywhere else it is ENCELADO_CONFIG_DIR +/// (with ENCELADO_DATA_DIR for a separate data folder, as the local sandbox +/// under deploy/local does), or Documenti\Encelado for a developer on +/// Windows, where a backup catches it and a reinstall cannot overwrite it. /// /// public static class AppPaths @@ -63,11 +64,15 @@ public static class AppPaths Config = ConfigLoader.Load(ConfigPath, out List warnings); ConfigWarnings = warnings; - // In the container the data volume is separate from the configuration volume: - // relative folders in the file resolve against /data, not /config. - if (InContainer && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ENCELADO_CONFIG_DIR"))) + // A data folder separate from the configuration folder: /data in the container, + // ENCELADO_DATA_DIR anywhere (the local sandbox in deploy/local mirrors the + // container's layout, so F5 and docker compose write the same tree). + string? dataDir = Environment.GetEnvironmentVariable("ENCELADO_DATA_DIR"); + bool separateData = !string.IsNullOrWhiteSpace(dataDir) + || (InContainer && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ENCELADO_CONFIG_DIR"))); + if (separateData) { - string data = Environment.GetEnvironmentVariable("ENCELADO_DATA_DIR") is { Length: > 0 } d ? d : ContainerDataDirectory; + string data = !string.IsNullOrWhiteSpace(dataDir) ? dataDir.Trim() : ContainerDataDirectory; Config.Run.BaseDirectory = ConfigDirectory; if (!Path.IsPathRooted(Config.Run.DataDirectory)) { Config.Run.DataDirectory = Path.Combine(data, Config.Run.DataDirectory); } if (!Path.IsPathRooted(Config.Run.KnowledgeDirectory)) { Config.Run.KnowledgeDirectory = Path.Combine(data, Config.Run.KnowledgeDirectory); } diff --git a/Encelado/src/Encelado.Server/Web/WebHost.cs b/Encelado/src/Encelado.Server/Web/WebHost.cs index 537f99b..22966e9 100644 --- a/Encelado/src/Encelado.Server/Web/WebHost.cs +++ b/Encelado/src/Encelado.Server/Web/WebHost.cs @@ -597,7 +597,7 @@ public static class WebHost string heartbeatPath = Path.Combine(ctx.Config.Run.DataPath, "state", "heartbeat.json"); Heartbeat? hb = HeartbeatFile.Read(heartbeatPath); double age = hb is null ? -1 : (DateTime.UtcNow - hb.Utc).TotalSeconds; - bool running = s.State == BotState.Running; + bool running = !ctx.Sample && s.State == BotState.Running; // the sample snapshot says "running" with no engine behind it bool heartbeatFresh = !running || (hb is not null && age < 3 * 30 + 15); bool disk = DiskWritable(ctx.Config.Run.DataPath); bool ok = disk && heartbeatFresh;