Replace flowchart.mmd with per-topic diagrams and a print pipeline
Split the single flowchart.mmd into flowchart/platform-overview.mmd (all 5 phases) and flowchart/round-lifecycle.mmd (round/draw detail), plus render-pdf.sh to generate print-ready A4/A3 PDFs with a consistent theme, header/footer, and legible contrast against the page background. Also flip the operational policy in CLAUDE.md: the app now always runs via Docker (dev and prod alike), with the venv reserved for tests, migration authoring, and one-time secret/key-generation scripts.
This commit is contained in:
@@ -8,3 +8,4 @@ master.xprv.enc
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
logs/
|
logs/
|
||||||
data/
|
data/
|
||||||
|
flowchart/*.pdf
|
||||||
|
|||||||
@@ -12,25 +12,24 @@ All 10 build-order stages from `/home/davide/.claude/plans/scalable-mixing-sloth
|
|||||||
|
|
||||||
Real-money verification on mainnet, done so far: registration + address derivation, deposit crediting (1-conf), a real 10 PLM bet (broadcast, confirmed, change credited back), and a full round cycle — close → draw (real block hash) → payout (70/30 split, exact sat math verified against the broadcast tx) → confirmation → round closed → next round auto-opened. Withdrawal and the RBF bump path are unit-tested but have never been exercised against a live broadcast. See "Known gaps" below before treating this as production-ready.
|
Real-money verification on mainnet, done so far: registration + address derivation, deposit crediting (1-conf), a real 10 PLM bet (broadcast, confirmed, change credited back), and a full round cycle — close → draw (real block hash) → payout (70/30 split, exact sat math verified against the broadcast tx) → confirmation → round closed → next round auto-opened. Withdrawal and the RBF bump path are unit-tested but have never been exercised against a live broadcast. See "Known gaps" below before treating this as production-ready.
|
||||||
|
|
||||||
Before writing code, always read [flowchart.mmd](flowchart.mmd) in full: every node in the diagram corresponds to a behavior that must be implemented exactly as described, including the labels on the edges (conditions, retries, loops).
|
Before writing code, always read the "Architecture" section below in full, plus the diagrams in [flowchart/](flowchart/): [platform-overview.mmd](flowchart/platform-overview.mmd) for the whole 5-phase flow, and [round-lifecycle.mmd](flowchart/round-lifecycle.mmd) for the round/draw lifecycle in detail. Every node in these diagrams corresponds to a behavior that must be implemented exactly as described, including the labels on the edges (conditions, retries, loops). Regenerate their companion PDFs with `flowchart/render-pdf.sh <file>.mmd` after editing either one.
|
||||||
|
|
||||||
Human-facing guides live in [docs/](docs/) (Italian, per explicit request — an exception to this file's English-only rule below): [setup.md](docs/setup.md), [running-the-server.md](docs/running-the-server.md), [guida-utente.md](docs/guida-utente.md), [guida-admin.md](docs/guida-admin.md).
|
Human-facing guides live in [docs/](docs/) (Italian, per explicit request — an exception to this file's English-only rule below): [setup.md](docs/setup.md), [running-the-server.md](docs/running-the-server.md), [guida-utente.md](docs/guida-utente.md), [guida-admin.md](docs/guida-admin.md).
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
The server itself — in development and in production alike — always runs via Docker (see "Deployment" below); there is no supported way to run `uvicorn` directly against this codebase. The venv (`.venv/`) is only for local tooling: running tests, authoring Alembic migrations, and running the one-time scripts that generate the secrets/key material that end up referenced from `.env`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source .venv/bin/activate # venv already created at .venv/
|
source .venv/bin/activate # venv already created at .venv/
|
||||||
pip install -e ".[dev]" # install/update deps
|
pip install -e ".[dev]" # install/update deps
|
||||||
|
|
||||||
alembic upgrade head # apply DB migrations
|
alembic revision --autogenerate -m "message" # generate a new migration after editing app/db/models.py (applied automatically by the container's startup command — see Deployment — never run `alembic upgrade head` manually)
|
||||||
alembic revision --autogenerate -m "message" # generate a new migration after editing app/db/models.py
|
|
||||||
|
|
||||||
PYTHONPATH=. python scripts/generate_master_key.py # one-time: create+encrypt the server's master xprv (requires XPRV_ENCRYPTION_KEY in .env)
|
PYTHONPATH=. python scripts/generate_master_key.py # one-time: create+encrypt the server's master xprv (requires XPRV_ENCRYPTION_KEY in .env; see Deployment for where MASTER_KEY_PATH should point)
|
||||||
PYTHONPATH=. python scripts/decrypt_master_key.py # ops recovery: decrypt+print the existing master xprv (asks for confirmation first)
|
PYTHONPATH=. python scripts/decrypt_master_key.py # ops recovery: decrypt+print the existing master xprv (asks for confirmation first)
|
||||||
PYTHONPATH=. python scripts/encrypt_master_key.py # ops bootstrap: bring your own externally-generated xprv instead of generating one (getpass prompt, --overwrite to replace)
|
PYTHONPATH=. python scripts/encrypt_master_key.py # ops bootstrap: bring your own externally-generated xprv instead of generating one (getpass prompt, --overwrite to replace)
|
||||||
|
|
||||||
uvicorn app.main:app --reload --port 8123 # run the dev server
|
|
||||||
|
|
||||||
python -m pytest # run all tests
|
python -m pytest # run all tests
|
||||||
python -m pytest tests/unit/test_hd.py # run one test file
|
python -m pytest tests/unit/test_hd.py # run one test file
|
||||||
python -m pytest tests/unit/test_hd.py::test_derivation_is_deterministic # run a single test
|
python -m pytest tests/unit/test_hd.py::test_derivation_is_deterministic # run a single test
|
||||||
@@ -40,14 +39,15 @@ python -m pytest tests/unit/test_hd.py::test_derivation_is_deterministic # run
|
|||||||
|
|
||||||
## Deployment (Docker + Caddy)
|
## Deployment (Docker + Caddy)
|
||||||
|
|
||||||
`docker-compose.yml` runs two containers: `app` (this codebase, built by `Dockerfile`, runs `alembic upgrade head` then `uvicorn`) and `caddy` (reverse proxy + automatic TLS). `.env` still holds the app secrets; `docker-compose.yml` overrides `DATABASE_URL`/`MASTER_KEY_PATH` to point at the bind-mounted `./data/` (db, encrypted master key, logs — all gitignored, persist across container restarts).
|
The app is always run via Docker — dev and prod alike use the same `docker-compose.yml`, just with a different `SITE_ADDRESS` (see below); there's no separate dev-mode compose file or bare-`uvicorn` workflow. `docker-compose.yml` runs two containers: `app` (this codebase, built by `Dockerfile`, runs `alembic upgrade head` then `uvicorn`) and `caddy` (reverse proxy + automatic TLS). `.env` holds the app secrets; `docker-compose.yml` overrides `DATABASE_URL`/`MASTER_KEY_PATH` inside the container to point at the bind-mounted `./data/` (db, encrypted master key, logs — all gitignored, persist across container restarts). Set `MASTER_KEY_PATH` in `.env` itself to the host-side equivalent, `./data/keys/master.xprv.enc`, so the venv-run key-generation scripts above (see "Commands") write to the exact same file the container reads — one source of truth for the key, whichever way it was generated.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p data/db data/keys data/logs # one-time: host dirs bind-mounted into the app container
|
mkdir -p data/db data/keys data/logs # one-time: host dirs bind-mounted into the app container
|
||||||
|
|
||||||
docker compose run --rm app python scripts/generate_master_key.py # one-time: create+encrypt the master xprv into ./data/keys/
|
# one-time: generate the master key via the venv script above (scripts/generate_master_key.py),
|
||||||
|
# not via `docker compose run` — MASTER_KEY_PATH in .env already points at ./data/keys/
|
||||||
|
|
||||||
docker compose up -d --build # build + start app and caddy
|
docker compose up -d --build # build + start app and caddy — same command for dev and prod
|
||||||
docker compose logs -f app # tail app logs (also written to ./data/logs/app.log)
|
docker compose logs -f app # tail app logs (also written to ./data/logs/app.log)
|
||||||
docker compose down # stop
|
docker compose down # stop
|
||||||
```
|
```
|
||||||
@@ -113,7 +113,7 @@ A periodic-round lottery system built on a Bitcoin-like coin (PLM, mainnet). Eac
|
|||||||
|
|
||||||
## Architecture (from the flowchart subgraphs)
|
## Architecture (from the flowchart subgraphs)
|
||||||
|
|
||||||
The flow is organized into 5 phases, each a subgraph in [flowchart.mmd](flowchart.mmd):
|
The flow is organized into 5 phases (see [flowchart/platform-overview.mmd](flowchart/platform-overview.mmd) for the full-platform diagram, and [flowchart/round-lifecycle.mmd](flowchart/round-lifecycle.mmd) for the round/draw phase in detail):
|
||||||
|
|
||||||
- **REG (Registration)**: on signup the server derives a new P2WPKH address via BIP84 (`m/84'/coin'/0'/0/index`, one index per user) from a master xprv **encrypted at rest**. This address is permanent and serves as both the deposit address and the address that receives winnings and withdrawals.
|
- **REG (Registration)**: on signup the server derives a new P2WPKH address via BIP84 (`m/84'/coin'/0'/0/index`, one index per user) from a master xprv **encrypted at rest**. This address is permanent and serves as both the deposit address and the address that receives winnings and withdrawals.
|
||||||
- **DEP (Balance top-up)**: an ElectrumClient/SPV subscribes to the user's address scripthash. Internal balance (DB) is credited after **1 confirmation only** — the reorg risk at 1-conf is knowingly accepted in v1, with no rollback logic.
|
- **DEP (Balance top-up)**: an ElectrumClient/SPV subscribes to the user's address scripthash. Internal balance (DB) is credited after **1 confirmation only** — the reorg risk at 1-conf is knowingly accepted in v1, with no rollback logic.
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
flowchart TD
|
|
||||||
|
|
||||||
subgraph REG["Registration"]
|
|
||||||
A["User registers: username + password"] --> B["Server derives a new P2WPKH address\n(BIP84, path m/84'/746'/0'/0/index)\nmaster xprv encrypted at rest"]
|
|
||||||
B --> C["Address linked to the user profile in the DB"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph DEP["Balance top-up"]
|
|
||||||
C --> D["User sends PLM to their dedicated address"]
|
|
||||||
D --> E["ElectrumClient/SPV monitors the address\n(subscribe scripthash)"]
|
|
||||||
E --> F{"Tx confirmed\n(1 confirmation)?"}
|
|
||||||
F -- No --> E
|
|
||||||
F -- Yes --> G["User balance credited in the DB\n(balance = confirmed UTXOs on the address)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph PLAY["Bet"]
|
|
||||||
G --> H{"User confirms bet purchase?\n(fixed cost: 10 PLM per round,\nmax 1 active bet at a time,\nacquires per-user DB lock shared with WITHDRAW)"}
|
|
||||||
H -- No --> G
|
|
||||||
H -- "Yes (balance >= bet cost)" --> I["Server builds PSBT:\nuser address -> pool address\n(bet cost) + change -> user address\nfee ~1 sat/vB deducted from the bet amount"]
|
|
||||||
I --> J["Server signs with the user's derived key"]
|
|
||||||
J --> K["Broadcast tx to the network"]
|
|
||||||
K --> L{"Tx confirmed\n(1 confirmation)?"}
|
|
||||||
L -- "No (timeout)" --> K2["Fee bump (RBF) and rebroadcast"]
|
|
||||||
K2 --> K
|
|
||||||
L -- Yes --> M["User registered as a participant\nin the current round (with bet amount)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph DRAW["Periodic draw"]
|
|
||||||
N["Round timer: every X minutes (configurable, default 10)"] --> O{"Are there bets\nalready broadcast but not yet confirmed?"}
|
|
||||||
O -- Yes --> O
|
|
||||||
O -- No --> O2["Close current round"]
|
|
||||||
O2 --> P["List of round participants\n(user address + bet amount),\nordered by broadcast timestamp\n(tie-break for same-block confirmations)"]
|
|
||||||
P --> Q{"Are there participants?"}
|
|
||||||
Q -- No --> N
|
|
||||||
Q -- Yes --> R["Draw winner (simple v1 algorithm):\n1. wait for the first block confirmed after round closing\n2. seed = block hash (hex -> integer)\n3. index = seed mod participant_count\n4. winner = participants[index]\n(anyone can recompute and verify it;\nalgorithm replaceable in the future)"]
|
|
||||||
R --> S["Compute total round prize pool\n(sum of confirmed deposits to the pool address)"]
|
|
||||||
S --> T["70% of the prize pool - payout tx fee\n-> winner's deposit address"]
|
|
||||||
S --> U["30% of the prize pool (unchanged)\n-> fee address (configurable)"]
|
|
||||||
T --> V["Payout tx signed with\nthe pool address key"]
|
|
||||||
U --> V
|
|
||||||
V --> V2{"Tx confirmed\n(1 confirmation)?"}
|
|
||||||
V2 -- "No (timeout)" --> V3["Fee bump (RBF) and rebroadcast"]
|
|
||||||
V3 --> V2
|
|
||||||
V2 -- Yes --> W["Log round\n(winner, amount, txid) for audit"]
|
|
||||||
W --> N
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph WITHDRAW["Withdrawal (simple v1)"]
|
|
||||||
G --> X["User requests withdrawal:\nexternal address + amount <= balance\n(min 1 PLM, acquires per-user DB lock\nshared with PLAY)"]
|
|
||||||
X --> Y["Server builds and signs PSBT:\nuser address -> external address\n+ optional change -> user address\nfee deducted from the withdrawn amount"]
|
|
||||||
Y --> Z["Broadcast + wait for 1 confirmation\n(same RBF-on-timeout pattern)"]
|
|
||||||
Z --> G
|
|
||||||
end
|
|
||||||
|
|
||||||
M --> N
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
flowchart LR
|
||||||
|
|
||||||
|
subgraph REG["FASE 1 - Registrazione"]
|
||||||
|
direction TB
|
||||||
|
A1["L'utente si registra\n(username + password)"] --> A2["Il server genera un indirizzo\ndedicato e permanente per l'utente\n(chiave segreta cifrata,\ncustodita dal server)"]
|
||||||
|
A2 --> A3["L'indirizzo viene collegato\nal profilo utente\n(sara' sia l'indirizzo di deposito\nche quello che ricevera' vincite\ne prelievi)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DEP["FASE 2 - Deposito"]
|
||||||
|
direction TB
|
||||||
|
B1["L'utente invia PLM\nal proprio indirizzo dedicato"] --> B2["Il sistema monitora\nl'indirizzo sulla blockchain"]
|
||||||
|
B2 --> B3{"Transazione\nconfermata?"}
|
||||||
|
B3 -- "No" --> B2
|
||||||
|
B3 -- "Si'" --> B4["Saldo dell'utente\naccreditato nel sistema"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PLAY["FASE 3 - Scommessa"]
|
||||||
|
direction TB
|
||||||
|
C1{"L'utente vuole\nscommettere?\n(costo fisso, es. 10 PLM;\nal massimo una scommessa\nattiva alla volta)"}
|
||||||
|
C1 -- "Si', saldo sufficiente" --> C2["Si prepara la transazione:\ndal suo indirizzo verso\nil conto comune del montepremi\n(con resto che torna a lui)"]
|
||||||
|
C2 --> C3["Transazione firmata\ne inviata alla rete"]
|
||||||
|
C3 --> C4{"Confermata?"}
|
||||||
|
C4 -- "No, troppo tempo" --> C5["Si aumenta la commissione\ne si reinvia"]
|
||||||
|
C5 --> C3
|
||||||
|
C4 -- "Si'" --> C6["L'utente e' ufficialmente\npartecipante al round in corso"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DRAW["FASE 4 - Round ed estrazione"]
|
||||||
|
direction TB
|
||||||
|
D0["(dettaglio completo in\nround-lifecycle.mmd)"] -.-> D1["Il round ha un tempo limite\nper accettare scommesse"]
|
||||||
|
D1 --> D2["Allo scadere, si aspettano\nle scommesse gia' in corso\ne poi il round si chiude"]
|
||||||
|
D2 --> D3["Si estrae un vincitore\nin modo casuale e verificabile\n(hash del primo blocco\ndopo la chiusura)"]
|
||||||
|
D3 --> D4["Il montepremi viene diviso:\n70% al vincitore\n30% alla piattaforma"]
|
||||||
|
D4 --> D5["Pagamento inviato e confermato\nsulla rete\n(stesso schema di riprova\ncon commissione aumentata\nin caso di ritardo)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph WITHDRAW["FASE 5 - Prelievo"]
|
||||||
|
direction TB
|
||||||
|
E1["L'utente richiede un prelievo:\nindirizzo esterno + importo\n(non puo' avvenire insieme\na una scommessa in corso)"] --> E2["Si prepara e firma la transazione:\ndal suo indirizzo verso\nl'indirizzo esterno indicato\n(con resto che torna a lui)"]
|
||||||
|
E2 --> E3["Transazione inviata\nalla rete"]
|
||||||
|
E3 --> E4{"Confermata?"}
|
||||||
|
E4 -- "No, troppo tempo" --> E5["Si aumenta la commissione\ne si reinvia"]
|
||||||
|
E5 --> E3
|
||||||
|
E4 -- "Si'" --> E6["Saldo dell'utente aggiornato"]
|
||||||
|
end
|
||||||
|
|
||||||
|
A3 --> B1
|
||||||
|
B4 --> C1
|
||||||
|
B4 --> E1
|
||||||
|
C6 --> D1
|
||||||
|
D5 -.->|"round successivo"| C1
|
||||||
Executable
+163
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates professional-looking A4 and A3 landscape PDFs from a Mermaid
|
||||||
|
# .mmd flowchart: consistent color theme, legible fonts, a title (read from
|
||||||
|
# the .mmd's own YAML frontmatter) and a footer with the generation date.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./render-pdf.sh [path/to/file.mmd]
|
||||||
|
#
|
||||||
|
# Defaults to round-lifecycle.mmd in this same directory.
|
||||||
|
# Produces <name>-A4.pdf and <name>-A3.pdf next to the .mmd file.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
MMD_FILE="${1:-$SCRIPT_DIR/round-lifecycle.mmd}"
|
||||||
|
|
||||||
|
if [[ ! -f "$MMD_FILE" ]]; then
|
||||||
|
echo "Errore: file non trovato: $MMD_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUT_DIR="$(cd "$(dirname "$MMD_FILE")" && pwd)"
|
||||||
|
BASE="$(basename "$MMD_FILE" .mmd)"
|
||||||
|
SVG_TMP="$OUT_DIR/.${BASE}.tmp.svg"
|
||||||
|
CONFIG_TMP="$OUT_DIR/.${BASE}.tmp-config.json"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$SVG_TMP" "$CONFIG_TMP" "$OUT_DIR/.${BASE}.tmp-A4.html" "$OUT_DIR/.${BASE}.tmp-A3.html"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Consistent, print-friendly color theme (indigo nodes/edges, warm amber phase
|
||||||
|
# clusters, generous font size) instead of mermaid's flat default palette.
|
||||||
|
cat > "$CONFIG_TMP" <<'EOF'
|
||||||
|
{
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"fontFamily": "\"Segoe UI\", Helvetica, Arial, sans-serif",
|
||||||
|
"fontSize": "17px",
|
||||||
|
"primaryColor": "#c9d6f7",
|
||||||
|
"primaryBorderColor": "#3949ab",
|
||||||
|
"primaryTextColor": "#1a1a2e",
|
||||||
|
"lineColor": "#3949ab",
|
||||||
|
"secondaryColor": "#fff8e1",
|
||||||
|
"tertiaryColor": "#ffffff",
|
||||||
|
"clusterBkg": "#fff8e1",
|
||||||
|
"clusterBorder": "#c9a227",
|
||||||
|
"edgeLabelBackground": "#c9d6f7",
|
||||||
|
"titleColor": "#1a1a2e"
|
||||||
|
},
|
||||||
|
"flowchart": {
|
||||||
|
"curve": "basis",
|
||||||
|
"padding": 16,
|
||||||
|
"htmlLabels": true,
|
||||||
|
"nodeSpacing": 100,
|
||||||
|
"rankSpacing": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "-> Rendering diagram to SVG..."
|
||||||
|
npx -y @mermaid-js/mermaid-cli -i "$MMD_FILE" -o "$SVG_TMP" -b white -c "$CONFIG_TMP"
|
||||||
|
|
||||||
|
echo "-> Building print-ready A4/A3 PDFs..."
|
||||||
|
|
||||||
|
# mermaid-cli pulls in puppeteer as a transitive dependency; reuse that install
|
||||||
|
# instead of adding a separate one just for this script.
|
||||||
|
PUPPETEER_DIR="$(dirname "$(find "$HOME/.npm/_npx" -maxdepth 3 -type d -name puppeteer 2>/dev/null | head -n1)")"
|
||||||
|
if [[ -z "$PUPPETEER_DIR" || ! -d "$PUPPETEER_DIR" ]]; then
|
||||||
|
echo "Errore: modulo puppeteer non trovato (serve mermaid-cli gia' eseguito almeno una volta)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export NODE_PATH="$PUPPETEER_DIR"
|
||||||
|
|
||||||
|
GENERATED_AT="$(date '+%d/%m/%Y %H:%M')"
|
||||||
|
|
||||||
|
# Human title for the header banner; falls back to a prettified filename for
|
||||||
|
# any .mmd this script hasn't been told about explicitly.
|
||||||
|
case "$BASE" in
|
||||||
|
round-lifecycle) TITLE="Ciclo di vita di un round" ;;
|
||||||
|
platform-overview) TITLE="Flusso completo della piattaforma" ;;
|
||||||
|
*) TITLE="$(echo "$BASE" | tr '-' ' ' | sed 's/\b\(.\)/\u\1/g')" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
node -e '
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const puppeteer = require("puppeteer");
|
||||||
|
|
||||||
|
const outDir = process.argv[1];
|
||||||
|
const base = process.argv[2];
|
||||||
|
const svgPath = process.argv[3];
|
||||||
|
const generatedAt = process.argv[4];
|
||||||
|
const title = process.argv[5];
|
||||||
|
|
||||||
|
const svg = fs.readFileSync(svgPath, "utf-8");
|
||||||
|
|
||||||
|
function htmlFor(size) {
|
||||||
|
return `<!doctype html>
|
||||||
|
<html><head><meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
html, body { margin:0; padding:0; height:100%; font-family: "Segoe UI", Helvetica, Arial, sans-serif; }
|
||||||
|
body { display:flex; flex-direction:column; height:100%; box-sizing:border-box; padding:4mm 6mm; }
|
||||||
|
.header {
|
||||||
|
flex:0 0 auto;
|
||||||
|
display:flex;
|
||||||
|
align-items:baseline;
|
||||||
|
gap:3mm;
|
||||||
|
border-bottom:1.5pt solid #3949ab;
|
||||||
|
padding-bottom:1.5mm;
|
||||||
|
margin-bottom:2mm;
|
||||||
|
}
|
||||||
|
.header .brand { font-size:12pt; font-weight:700; color:#3949ab; }
|
||||||
|
.header .title { font-size:10pt; font-weight:400; color:#1a1a2e; }
|
||||||
|
.diagram { flex:1 1 auto; min-height:0; display:flex; align-items:flex-start; justify-content:center; }
|
||||||
|
.diagram svg { width:100%; height:auto; max-width:100%; max-height:100%; }
|
||||||
|
.footer {
|
||||||
|
flex:0 0 auto;
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:center;
|
||||||
|
border-top:0.5pt solid #c9c9d6;
|
||||||
|
padding-top:2mm;
|
||||||
|
margin-top:2mm;
|
||||||
|
font-size:8pt;
|
||||||
|
color:#6b6b7a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="header">
|
||||||
|
<span class="brand">PLM Lottery</span>
|
||||||
|
<span class="title">${title}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diagram">${svg}</div>
|
||||||
|
<div class="footer">
|
||||||
|
<span>Diagramma di flusso</span>
|
||||||
|
<span>Generato il ${generatedAt} · formato ${size} orizzontale</span>
|
||||||
|
</div>
|
||||||
|
</body></html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await puppeteer.launch({ args: ["--no-sandbox"] });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
for (const fmt of ["A4", "A3"]) {
|
||||||
|
const htmlPath = path.join(outDir, `.${base}.tmp-${fmt}.html`);
|
||||||
|
fs.writeFileSync(htmlPath, htmlFor(fmt));
|
||||||
|
await page.goto("file://" + htmlPath, { waitUntil: "networkidle0" });
|
||||||
|
const pdfPath = path.join(outDir, `${base}-${fmt}.pdf`);
|
||||||
|
await page.pdf({
|
||||||
|
path: pdfPath,
|
||||||
|
format: fmt,
|
||||||
|
landscape: true,
|
||||||
|
printBackground: true,
|
||||||
|
margin: { top: "6mm", bottom: "6mm", left: "6mm", right: "6mm" },
|
||||||
|
});
|
||||||
|
console.log(" " + pdfPath);
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
})();
|
||||||
|
' "$OUT_DIR" "$BASE" "$SVG_TMP" "$GENERATED_AT" "$TITLE"
|
||||||
|
|
||||||
|
echo "Fatto."
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
flowchart LR
|
||||||
|
|
||||||
|
A["Il round precedente\nsi e' chiuso\n(pagamento vincitore confermato)"] --> B{"Lotteria in pausa\nmanutenzione?"}
|
||||||
|
B -- "Si'" --> B_WAIT["Si attende"]
|
||||||
|
B_WAIT --> B
|
||||||
|
B -- "No" --> C["Breve attesa\n'di raffreddamento'\n(cosi' i giocatori vedono\nil risultato precedente)"]
|
||||||
|
C --> D["Si apre un nuovo round\n(stato: APERTO)\ncon un limite di tempo\nper scommettere"]
|
||||||
|
|
||||||
|
subgraph OPEN["FASE 1 - Round aperto (accetta scommesse)"]
|
||||||
|
direction TB
|
||||||
|
D --> F["Un giocatore\npiazza una scommessa"]
|
||||||
|
F --> G{"E' arrivata prima\ndella scadenza\ndel round?"}
|
||||||
|
G -- "Si'" --> H["Accettata:\ngiocatore aggiunto\nai partecipanti"]
|
||||||
|
G -- "No, troppo tardi" --> F2["Rifiutata:\nil round non e' piu'\nin tempo per accettarla"]
|
||||||
|
H --> F
|
||||||
|
F2 --> F
|
||||||
|
end
|
||||||
|
|
||||||
|
D -.->|"scade il tempo"| I
|
||||||
|
|
||||||
|
subgraph CLOSING["FASE 2 - Chiusura"]
|
||||||
|
direction TB
|
||||||
|
I["Il round raggiunge la sua scadenza\n(indipendentemente dalle scommesse\ngia' in corso, che restano valide):\nda questo momento nessuna\nnuova scommessa e' accettata"] --> J{"Ci sono scommesse\ngia' inviate ma non\nancora confermate?"}
|
||||||
|
J -- "Si'" --> J_WAIT["Si attende qualche secondo\ne si ricontrolla"]
|
||||||
|
J_WAIT --> J
|
||||||
|
J -- "No, tutte confermate" --> K["Il round si chiude:\nsi fotografa lo stato\nattuale della blockchain"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DRAWING["FASE 3 - Estrazione vincitore"]
|
||||||
|
direction TB
|
||||||
|
K --> L{"C'e' almeno\nun partecipante?"}
|
||||||
|
L -- "No" --> M["Round concluso\nsenza vincitore"]
|
||||||
|
L -- "Si'" --> N["Si attende il primo\nnuovo blocco dopo\nla chiusura del round"]
|
||||||
|
N --> O["L'hash del blocco\nsceglie il vincitore\nin modo casuale e verificabile\n(stessa probabilita' per tutti)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PAYING["FASE 4 - Pagamento"]
|
||||||
|
direction TB
|
||||||
|
O --> P["Si calcola\nil montepremi totale"]
|
||||||
|
P --> Q["Si divide:\n70% al vincitore\n30% alla piattaforma"]
|
||||||
|
Q --> R["Si prepara e firma\nla transazione di pagamento"]
|
||||||
|
R --> S["La transazione\nviene inviata"]
|
||||||
|
S --> T{"Confermata\nsulla rete?"}
|
||||||
|
T -- "No, troppo tempo" --> T2["Si aumenta la\ncommissione e si reinvia"]
|
||||||
|
T2 --> S
|
||||||
|
T -- "Si'" --> U["Vincitore registrato\nnel registro di controllo"]
|
||||||
|
end
|
||||||
|
|
||||||
|
U --> V["Round CHIUSO\n(il ciclo ricomincia\ndall'inizio per\nil round successivo)"]
|
||||||
|
M --> V
|
||||||
Reference in New Issue
Block a user