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:
2026-07-23 16:24:28 +02:00
parent bae08f2759
commit 8a3dfd4592
6 changed files with 276 additions and 66 deletions
+51
View File
@@ -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
+163
View File
@@ -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} &middot; 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."
+50
View File
@@ -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