5.0: avvio in locale nella sandbox deploy/local, script di avvio e screenshot, arresto pulito del container verificato

Per vedere e provare il bot senza Unraid: F5 in VS Code, scripts/run-dev e
scripts/run-docker usano tutti deploy/local come /config e /data, con la stessa
disposizione del container, così chiavi e configurazione valgono in ogni modo.
ENCELADO_DATA_DIR separa i dati anche fuori dal container. scripts/screenshots
rigenera docs/img dal server campione; /api/health in --sample non finge più un
motore acceso. docker stop ora termina in un secondo con «SIGTERM: arresto»
(PosixSignalRegistration), provato sull'immagine 5.0.0 costruita dalla catena.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 13:59:56 +02:00
co-authored by Claude Fable 5.1
parent d7559d36a5
commit e9ace47501
22 changed files with 310 additions and 39 deletions
+9 -3
View File
@@ -1,8 +1,12 @@
{ {
// F5 avvia il server (motore + interfaccia web) come processo locale, fuori dal // 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 // container, con configurazione e dati nella sandbox deploy/local (ignorata da git,
// appena Kestrel è in ascolto. "Encelado (campione)" serve l'interfaccia con dati // stesse cartelle che il compose monta come /config e /data). Il browser si apre da
// finti, senza chiavi né mercato: è quello che si usa per lavorare sulle pagine. // 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", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
@@ -17,6 +21,8 @@
"stopAtEntry": false, "stopAtEntry": false,
"env": { "env": {
"ENCELADO_WEB_PORT": "8080", "ENCELADO_WEB_PORT": "8080",
"ENCELADO_CONFIG_DIR": "${workspaceFolder}/deploy/local/config",
"ENCELADO_DATA_DIR": "${workspaceFolder}/deploy/local/data",
"DOTNET_ENVIRONMENT": "Development" "DOTNET_ENVIRONMENT": "Development"
}, },
"serverReadyAction": { "serverReadyAction": {
+30 -3
View File
@@ -112,10 +112,37 @@
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"label": "docker compose up", "label": "avvia in locale (script)",
"detail": "Avvia il container dalla docker-compose.yml alla radice (immagine locale, cartelle ./deploy/local/config e ./deploy/local/data).", "detail": "scripts/run-dev.ps1: compila e avvia il server nella sandbox deploy/local, senza container. Ctrl+C per fermare.",
"type": "shell", "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}" }, "options": { "cwd": "${workspaceFolder}" },
"presentation": { "reveal": "always", "panel": "dedicated" }, "presentation": { "reveal": "always", "panel": "dedicated" },
"problemMatcher": [] "problemMatcher": []
+1
View File
@@ -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). - **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`. - **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`. - **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. - 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 ## 2026-09-23 — 5.0, apprendimento in ombra (ADR-0006) e skill di progetto
+5 -2
View File
@@ -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. - **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<T>` fra ingestion, strategia, esecuzione e UI. - **Concorrenza**: un solo thread di decisione; I/O asincrono; `Channel<T>` 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. - **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`). - **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/<nome>`. 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`. - **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/<nome>`. 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 ## Comandi
@@ -57,6 +57,9 @@ dotnet build Encelado.slnx # compilazione
dotnet test tests/Encelado.Tests --no-restore # test (xunit) dotnet test tests/Encelado.Tests --no-restore # test (xunit)
dotnet msbuild build/Release.proj -t:Verifica # compilazione + test nella cartella di verifica 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 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 -- 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 -- 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] dotnet run --project tools/Encelado.Backtest -- falsify --data "%USERPROFILE%\Documents\Encelado\data\market" --out reports [--costs api]
+15 -5
View File
@@ -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](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. | | **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 ```powershell
dotnet build Encelado.slnx # compilazione dotnet build Encelado.slnx # compilazione
dotnet test tests/Encelado.Tests --no-restore # test (xunit) 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:Verifica # compilazione + test nella cartella di verifica
dotnet msbuild build/Release.proj -t:Docker # l'immagine, con i test dentro la build 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 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) src/Encelado.Server l'eseguibile: Kestrel, API JSON, SSE, interfaccia incorporata (Material 3, vanilla)
tools/Encelado.Backtest ticks, baskets, falsify, learn tools/Encelado.Backtest ticks, baskets, falsify, learn
tests/Encelado.Tests xunit 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) build/ catena di verifica, pacchetto e rilascio (MSBuild)
docs/ STATE, ARCHITECTURE, STRATEGY, RUNBOOK, DOCKER, UI_GUIDELINES, ADR… docs/ STATE, ARCHITECTURE, STRATEGY, RUNBOOK, DOCKER, UI_GUIDELINES, ADR…
``` ```
+6 -4
View File
@@ -1,10 +1,11 @@
# Encelado in locale con Docker Compose: immagine costruita dalla Dockerfile alla # Encelado in locale con Docker Compose: immagine costruita dalla Dockerfile alla
# radice, cartelle di configurazione e dati sotto deploy/local (ignorate da git). # 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 --build # costruisce e avvia, log a video (Ctrl+C = stop pulito)
# docker compose up -d # in background # docker compose up -d --build # in background → http://localhost:8080/ (token "sviluppo")
# docker compose logs -f encelado # segue il log # docker compose logs -f encelado # segue il log
# docker compose down # ferma (SIGTERM: stop pulito del motore) # 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 # 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=…, # 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_CONFIRM_LIVE: ${ENCELADO_CONFIRM_LIVE:-}
ENCELADO_DISPLAY_CURRENCY: ${ENCELADO_DISPLAY_CURRENCY:-USD} ENCELADO_DISPLAY_CURRENCY: ${ENCELADO_DISPLAY_CURRENCY:-USD}
# Senza token l'interfaccia è raggiungibile solo da localhost del container: # Senza token l'interfaccia è raggiungibile solo da localhost del container:
# per usarla dal browser il token serve. # per usarla dal browser il token serve. "sviluppo" è il default delle prove
ENCELADO_WEB_TOKEN: ${ENCELADO_WEB_TOKEN:-} # in locale; in .env mettine uno vero.
ENCELADO_WEB_TOKEN: ${ENCELADO_WEB_TOKEN:-sviluppo}
ENCELADO_KEY_PASSPHRASE: ${ENCELADO_KEY_PASSPHRASE:-} ENCELADO_KEY_PASSPHRASE: ${ENCELADO_KEY_PASSPHRASE:-}
ETORO_API_KEY: ${ETORO_API_KEY:-} ETORO_API_KEY: ${ETORO_API_KEY:-}
ETORO_USER_KEY: ${ETORO_USER_KEY:-} ETORO_USER_KEY: ${ETORO_USER_KEY:-}
+12 -2
View File
@@ -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. 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.
+4
View File
@@ -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). 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à
| Modalità | Ordini | Conferma all'avvio | | Modalità | Ordini | Conferma all'avvio |
+8 -9
View File
@@ -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 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 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**. - **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 ## Prossimi passi
1. Liberare spazio su C:, riavviare Docker, ricostruire l'immagine e verificare l'arresto pulito con `docker stop` (vedi problemi aperti). 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. **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. 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. 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. 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`.
6. Rifare il backtest (`backtest baskets`) con i limiti di margine e aggiornare §3 di `docs/STRATEGY.md`.
## Problemi aperti ## 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`). - Backtest negativo: la strategia non regge i costi (`docs/STRATEGY.md`).
- Il conto reale vale 224,90 USD: il Live non è praticabile. - 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. - Google News blocca le ricerche RSS; RBA 403 a intermittenza; Fed 404 a tratti.
+3 -3
View File
@@ -55,8 +55,8 @@ Angoli 12 px (card, banner), 16 px (dialoghi grandi 28 px come da M3), pulsanti
| Classe | Larghezza | Layout | | Classe | Larghezza | Layout |
|---|---|---| |---|---|---|
| compact | < 600 px | rail nascosto (menu ☰), KPI su due colonne, basket a cards, orologi nascosti | | 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 | | 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 sei colonne, contesto su tre, impostazioni su due | | expanded | ≥ 840 px | rail 80/256 px, KPI su tre colonne (due righe), contesto su tre, impostazioni su due |
### Accessibilità ### Accessibilità
@@ -76,7 +76,7 @@ Tastiera completa (Tab su rail, pulsanti, tabelle, dialoghi; Esc chiude drawer e
## Screenshot ## 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 ```powershell
dotnet run --project src/Encelado.Server -- --sample --port 8085 dotnet run --project src/Encelado.Server -- --sample --port 8085
Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

+42
View File
@@ -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 <basket>, kill, residuo, preset <nome>, reset <motivazione>, 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
+25
View File
@@ -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
+40
View File
@@ -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
+18
View File
@@ -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
+47
View File
@@ -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
}
+32
View File
@@ -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 <name> <page> <size>
"$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
@@ -9,9 +9,10 @@ namespace Encelado.Engine.Configuration;
/// <c>instruments.json</c> and the encrypted keys; everything the bot produces /// <c>instruments.json</c> and the encrypted keys; everything the bot produces
/// (<c>data/</c>, <c>knowledge/</c>, <c>reports/</c>, <c>logs/</c>) resolves against /// (<c>data/</c>, <c>knowledge/</c>, <c>reports/</c>, <c>logs/</c>) resolves against
/// it unless the configuration says otherwise. In the container it is <c>/config</c> /// it unless the configuration says otherwise. In the container it is <c>/config</c>
/// (and the data folder <c>/data</c>); anywhere else it is <c>ENCELADO_CONFIG_DIR</c>, /// (and the data folder <c>/data</c>); anywhere else it is <c>ENCELADO_CONFIG_DIR</c>
/// or <c>Documenti\Encelado</c> for a developer on Windows, where a backup catches it /// (with <c>ENCELADO_DATA_DIR</c> for a separate data folder, as the local sandbox
/// and a reinstall cannot overwrite it. /// under <c>deploy/local</c> does), or <c>Documenti\Encelado</c> for a developer on
/// Windows, where a backup catches it and a reinstall cannot overwrite it.
/// </para> /// </para>
/// </summary> /// </summary>
public static class AppPaths public static class AppPaths
@@ -63,11 +64,15 @@ public static class AppPaths
Config = ConfigLoader.Load(ConfigPath, out List<string> warnings); Config = ConfigLoader.Load(ConfigPath, out List<string> warnings);
ConfigWarnings = warnings; ConfigWarnings = warnings;
// In the container the data volume is separate from the configuration volume: // A data folder separate from the configuration folder: /data in the container,
// relative folders in the file resolve against /data, not /config. // ENCELADO_DATA_DIR anywhere (the local sandbox in deploy/local mirrors the
if (InContainer && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ENCELADO_CONFIG_DIR"))) // 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; Config.Run.BaseDirectory = ConfigDirectory;
if (!Path.IsPathRooted(Config.Run.DataDirectory)) { Config.Run.DataDirectory = Path.Combine(data, Config.Run.DataDirectory); } 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); } if (!Path.IsPathRooted(Config.Run.KnowledgeDirectory)) { Config.Run.KnowledgeDirectory = Path.Combine(data, Config.Run.KnowledgeDirectory); }
+1 -1
View File
@@ -597,7 +597,7 @@ public static class WebHost
string heartbeatPath = Path.Combine(ctx.Config.Run.DataPath, "state", "heartbeat.json"); string heartbeatPath = Path.Combine(ctx.Config.Run.DataPath, "state", "heartbeat.json");
Heartbeat? hb = HeartbeatFile.Read(heartbeatPath); Heartbeat? hb = HeartbeatFile.Read(heartbeatPath);
double age = hb is null ? -1 : (DateTime.UtcNow - hb.Utc).TotalSeconds; 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 heartbeatFresh = !running || (hb is not null && age < 3 * 30 + 15);
bool disk = DiskWritable(ctx.Config.Run.DataPath); bool disk = DiskWritable(ctx.Config.Run.DataPath);
bool ok = disk && heartbeatFresh; bool ok = disk && heartbeatFresh;