38 Commits
Author SHA1 Message Date
david-spacil c0272f4f70 Merge pull request 'Zaříznout stará jména kostky' (#20) from agents/claude/zariznout-stara-jmena into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 1s
testy / pytest (3.13) (push) Successful in 1s
testy / pytest (3.14) (push) Successful in 1s
binárka / linux (push) Successful in 10s
Reviewed-on: #20
2026-08-23 23:14:58 +02:00
gitea-actions a6c94e239b Zaříznout stará jména kostky
binárka / linux (pull_request) Successful in 11s
image / image (pull_request) Successful in 18s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 0s
testy / pytest (3.13) (pull_request) Successful in 0s
testy / pytest (3.14) (pull_request) Successful in 1s
Druhý krok po #16, rovnou celý. Dvojkolejnost dávala smysl pro projekt
s neznámými uživateli; tady běží jedna instance a ta je přemigrovaná.

Pryč je:

  - deploy/pve-kostky.sh (přesměrování na nový skript)
  - stěhování ze starého jména v instalátoru včetně bloku STARY_*
  - záložní jméno přílohy kostky-* v instalátoru
  - věšení příloh pod dvěma jmény v obou workflow
  - záloha na ~/.local/share/kostky/dice.db ve storage.py
  - odstavce o přechodu v README

Jediné, co ve zdrojácích zůstalo, je docstring "Pravidla hry v kostky"
v core.py — to je česky pojmenovaná hra, ne identifikátor.

POZOR na pořadí: instalátor teď hledá jen dice-counter-*, a poslední
vydání (v1.2.1) má na releasu pouze kostky-*. Do otagování v1.2.2 tedy
`update` ani čerstvá instalace nemají co stáhnout. Merge a tag patří
k sobě.

Ověřeno v debianím kontejneru: čistá instalace, druhé spuštění za sebou,
a vydání bez nových jmen spadne se srozumitelnou hláškou místo toho, aby
sáhlo po starém. Binárka postavená lokálně zakládá databázi v
…/dice-counter/ a starou vedle sebe ignoruje. 118 testů.

Closes #19
2026-08-23 23:09:15 +02:00
david-spacil 8e6e4a8fe9 Merge pull request 'Sjednotit pojmenování: kostky → dice-counter' (#18) from agents/claude/prejmenovani into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 0s
testy / pytest (3.13) (push) Successful in 0s
testy / pytest (3.14) (push) Successful in 1s
Reviewed-on: #18
2026-08-23 22:49:47 +02:00
david-spacil 4e526e8841 Merge branch 'main' into agents/claude/prejmenovani
binárka / linux (pull_request) Successful in 11s
image / image (pull_request) Successful in 7s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
2026-08-23 22:44:44 +02:00
david-spacil 01dc7a0504 Merge pull request 'Připnout image k repozitáři přes OCI label' (#17) from agents/claude/oci-labely into main
testy / pytest (3.11) (push) Successful in 0s
testy / pytest (3.12) (push) Successful in 0s
testy / pytest (3.13) (push) Successful in 1s
testy / pytest (3.14) (push) Successful in 1s
Reviewed-on: #17
2026-08-23 22:44:30 +02:00
gitea-actions 182d0a3fdc Upřesnit komentář: nezmizí tři řádky, ale celý blok
binárka / linux (pull_request) Successful in 10s
image / image (pull_request) Successful in 7s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 0s
testy / pytest (3.13) (pull_request) Successful in 0s
testy / pytest (3.14) (pull_request) Successful in 1s
2026-08-23 22:33:40 +02:00
gitea-actions 6443a93955 Nenechat zkoušku binárky viset na zbytku z minulého běhu
binárka / linux (pull_request) Successful in 10s
image / image (pull_request) Successful in 14s
testy / pytest (3.11) (pull_request) Successful in 2s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 0s
testy / pytest (3.14) (pull_request) Successful in 1s
Krok padl na `test -f /tmp/zkouska/dice-counter/dice.db`. Runner si /tmp
mezi běhy drží, takže tam po dřívějších stavbách zůstalo
/tmp/zkouska/kostky/dice.db — a binárka po něm nově sáhne, dokud nová
databáze neexistuje. Zkouška tedy hledala soubor, který se schválně
nezaložil.

Chová se to správně, jen se to nedá zkoušet na adresáři, do kterého si
sype každý běh. Nově dostane každý svůj vlastní přes mktemp a po sobě ho
uklidí.

Reprodukováno lokálně: se zbytkem po staru krok padne, s vlastním
adresářem projde.
2026-08-23 22:32:39 +02:00
gitea-actions 7449c01b14 Sjednotit pojmenování: kostky → dice-counter
binárka / linux (pull_request) Failing after 10s
image / image (pull_request) Successful in 7s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Projekt se jmenuje dice-counter, ale skoro všude vystupoval jako kostky.
Tohle je první ze dvou kroků podle #16: nová jména se zavádějí vedle
starých, aby běžící instalace nic nerozbilo.

Nově:

    binárka a spec       dice-counter.spec, dist/dice-counter
    přílohy releasu      dice-counter-linux-x86_64 a spol.
    služba v LXC         dice-counter.service
    cesty v LXC          /opt/dice-counter, /var/lib/dice-counter
    uživatel             dice-counter
    hostname LXC         dice-counter
    instalátor           deploy/pve-dice-counter.sh
    datový adresář       ~/.local/share/dice-counter a obdoby
    co appka hlásí       "dice-counter <verze>" v banneru i v patičce

Stará jména zatím fungují dál:

  - Přílohy se na release věší pod oběma jmény.
  - Instalátor umí stáhnout i kostky-*, takže starší vydání jdou
    nainstalovat pořád.
  - deploy/pve-kostky.sh zůstává jako přesměrování — příkaz `update`
    v už založených kontejnerech chodí pro instalátor právě tam.
  - Kontejnery z dřívějška se při aktualizaci přestěhují samy: služba
    se přejmenuje a databáze se přesune do /var/lib/dice-counter, takže
    o historii her nikdo nepřijde.
  - Binárka sáhne po ~/.local/share/kostky/dice.db, dokud nová databáze
    neexistuje.

Při zkoušení přechodu vyplavala chyba, která tu byla už předtím: příkaz
`update` si nový instalátor uloží do $HOME_DIR/install.sh a odtud ho
pustí, takže `install -m 755 "$0" "$HOME_DIR/install.sh"` kopírovalo
soubor sám na sebe. install to odmítne, se `set -e` spadl celý update.
Ošetřeno testem `-ef`.

Ověřeno v debianím kontejneru: přechod ze staré instalace (včetně běhu
instalátoru z adresáře, který se během něj maže), čistá instalace, běh
proti releasu, kde je jen stará příloha, a druhé spuštění za sebou.

Refs #16
2026-08-23 22:29:58 +02:00
gitea-actions 5feb3cdd23 Připnout image k repozitáři přes OCI label
binárka / linux (pull_request) Successful in 11s
image / image (pull_request) Successful in 7s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Balíček se po publikaci objevil jen pod uživatelem, záložka Packages
u repozitáře zůstala prázdná. Gitea páruje kontejnerový balíček s repozitářem
podle labelu org.opencontainers.image.source a image dosud neměl labely
vůbec žádné.

Kromě source ještě title, description, url a version — ta se plní z build
argumentu, takže sedí s tím, co hlásí --version.
2026-08-23 22:07:14 +02:00
david-spacil 96f2fc1028 Merge pull request 'Publikovat image do registru balíčků Gitey' (#15) from enhancement/docker-compose into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 0s
testy / pytest (3.13) (push) Successful in 0s
testy / pytest (3.14) (push) Successful in 1s
binárka / linux (push) Successful in 11s
Reviewed-on: #15
2026-08-23 20:46:46 +02:00
gitea-actions 826f243af7 Sjednotit jména i v dockerové cestě
binárka / linux (pull_request) Successful in 11s
image / image (pull_request) Successful in 16s
testy / pytest (3.11) (pull_request) Successful in 0s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Služba, container_name, svazek a uživatel uvnitř image se jmenovaly kostky.
Teď je nejlevnější chvíle to srovnat: compose se dá použít teprve od tohohle
PR, takže svazek kostky-data zatím nikde neexistuje a není co migrovat. Po
vydání by to už chtělo přesun dat.

Uživateli zůstává uid 1000, takže vlastnictví souborů v /data nezávisí na
jménu a bind mounty se nerozbijí.

LXC strana (kostky.service, /opt/kostky, /var/lib/kostky) zůstává na #16 —
tam přejmenování znamená migraci běžícím instalacím.
2026-08-23 20:45:36 +02:00
gitea-actions b1b7d1fad1 Nezavádět další "kostky" tam, kde na to není důvod
binárka / linux (pull_request) Successful in 10s
image / image (pull_request) Successful in 13s
testy / pytest (3.11) (pull_request) Successful in 0s
testy / pytest (3.12) (pull_request) Successful in 0s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Značka image ve zkoušce, testovací kontejner a builder buildx se jmenovaly
kostky. Všechno to vzniká a mizí uvnitř jednoho běhu CI, nikam se to
nepropisuje a nic to nestojí — jen jsem to zkopíroval ze zvyku.

Jména v docker-compose.yaml (služba, container_name, svazek kostky-data)
zůstávají; ta se dotýkají uložených dat a patří k tomu úklidu ve #16.
2026-08-23 20:41:03 +02:00
gitea-actions b6066e2be8 Image vydávat z GitHubu, doma ho jen zkoušet
binárka / linux (pull_request) Successful in 10s
image / image (pull_request) Successful in 26s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Domácí runner běží v LXC. binfmt_misc je vlastnost jádra hostitele a do toho
se kvůli počitadlu kostek vrtat nebude — je produkční. tonistiigi/binfmt sice
hlásil úspěch, ale na hostiteli po něm nezůstala žádná registrace, takže
buildkit arm64 mezi platformami nikdy neuvidí.

Stejná dělba jako u binárek: doma se hlídá, na GitHubu se vydává. Zdejší
workflow staví nativní amd64 obyčejným `docker build` a zkouší ho spustit nad
každým pull requestem; žádný buildx, žádná emulace, nic, co by na runneru
mohlo chybět. Publikaci multiarch image převzalo .github/workflows/image.yml,
kde jsou běhouni plnohodnotné virtuály a emulace funguje bez zásahu.

Publikovat jde nově jen z tagu. Ruční spuštění nad větví by udělalo `:latest`
ukazující na rozdělanou práci.
2026-08-23 20:31:24 +02:00
gitea-actions 9283a87a1e Ověřit emulaci dřív, než se do stavby pustíme
binárka / linux (pull_request) Successful in 10s
image / image (pull_request) Failing after 2s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
První běh na runneru odhalil dvě věci.

Rouru do `head` shodil pipefail: head zavřel rouru po dvaceti řádcích, docker
dostal EPIPE a krok skončil na 255, přestože se builder založil v pořádku.

Horší je, že binfmt se sice nainstaloval (installing: arm64 OK), ale buildkit
pak arm64 mezi platformami nenabídl. Emulace musí být registrovaná s příznakem
F, aby si na ni buildkit ve svém kontejneru sáhl bez interpretu v souborovém
systému; jinak vidí jen nativní platformy. Doma to tak mám z qemu-user-static
a proto tam multiarch stavba prošla.

Builder se teď staví načisto — ten z minulého běhu by si pamatoval platformy
z doby před registrací — a hned po něm se ověří, že arm64 v seznamu opravdu
je. Bez toho by stavba spadla až na konci s hláškou, ze které se nic nepozná.
2026-08-23 20:14:17 +02:00
gitea-actions 0bb2e0efab Publikovat image do registru balíčků Gitey
binárka / linux (pull_request) Successful in 11s
image / image (pull_request) Failing after 9s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Compose soubor měl v sobě build: context: .., takže si image stavěl z
lokálních zdrojáků a bez naklonovaného repozitáře byl k ničemu. Proti LXC
cestě, kde se stahuje hotová binárka, to byla nesrovnalost.

Nové workflow staví image na domácím runneru a při vydání ho pošle do
registru Gitey, pro amd64 i arm64. Compose se tím smrskl na jeden soubor,
který si člověk stáhne a rovnou spustí.

Automatický token Actions na balíčky nestačí — Gitea to zatím neumí a sama
v dokumentaci odkazuje na osobní token. Je v secrets pod PACKAGE_TOKEN;
jméno nesmí začínat na GITEA_, ten prefix si Gitea vyhrazuje.

Nad pull requesty se image staví a zkouší, ale nepublikuje. Dockerfile do
teď v CI žádné pokrytí neměl a rozbil by se až při releasu.

Image se jmenuje podle repozitáře, dice-counter. Zbytek projektu pořád mluví
o kostkách; sjednocení je na samostatné issue, protože se dotkne vydaných
binárek i běžících instalací.
2026-08-23 20:09:20 +02:00
david-spacil 4faddc8b22 Merge pull request 'Příkaz update a konzole bez hesla' (#14) from agents/claude/update-v-kontejneru into main
testy / pytest (3.11) (push) Successful in 0s
testy / pytest (3.12) (push) Successful in 0s
testy / pytest (3.13) (push) Successful in 1s
testy / pytest (3.14) (push) Successful in 1s
Reviewed-on: #14
2026-08-22 23:51:56 +02:00
gitea-actions 01d85594b2 Příkaz update a konzole bez hesla
binárka / linux (pull_request) Successful in 10s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Dvě věci, které se u community-scripts čekají a chyběly.

V kontejneru je nově /usr/bin/update, takže aktualizace je jedno slovo. Sám
si přitom obnoví i instalátor z main, aby nejel napořád na té verzi skriptu,
se kterou se instalovalo. Cesta z uzlu (MODE=update) zůstává.

A konzole: kontejner schválně nemá root heslo, jenže pak se do konzole ve
webu Proxmoxu nedalo dostat — uživatel žádné přihlašovací údaje nezná a ani
nemá odkud. Autologin přes container-getty@1 to řeší přesně tak, jak to
dělají community-scripts. Nová práva to nikomu nedává, kdo má web Proxmoxu,
má root na uzlu tak jako tak. Vypíná se AUTOLOGIN=0.
2026-08-22 23:19:49 +02:00
david-spacil fc2df8d86e Merge pull request 'Nasazení na server: Proxmox LXC a Docker' (#13) from agents/claude/nasazeni into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 1s
testy / pytest (3.13) (push) Successful in 0s
testy / pytest (3.14) (push) Successful in 0s
binárka / linux (push) Successful in 11s
Reviewed-on: #13
2026-08-22 21:32:13 +02:00
gitea-actions 83654fe6b5 Zapnout kontejneru nesting
binárka / linux (pull_request) Successful in 10s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 0s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Debian 13 veze systemd 257 a ten v neprivilegovaném LXC bez nestingu
nedostane, co potřebuje — Proxmox na to při startu sám upozorňuje hláškou
"Systemd 257 detected. You may need to enable nesting."

Appka i tak nastartovala, ale spoléhat se na to nemá cenu. Stejnou výchozí
hodnotu mají i community-scripts a sami varují, že moderní distribuce se
systemd nesting potřebují.
2026-08-22 21:23:51 +02:00
gitea-actions 861470da55 Šablonu vybírat podle architektury uzlu
binárka / linux (pull_request) Successful in 10s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 2s
pveam nabízí amd64 i arm64 vedle sebe a výběr přes `sort -V | tail -1` padl
na tu abecedně poslední — tedy arm64 i na x86 uzlu. Šablona, která na uzlu
už ležela, se pak nenašla a skript začal stahovat 120 MB nepoužitelného
archivu.

Architektura se teď bere z dpkg --print-architecture a nejdřív se kouká, co
na uzlu už je; stahuje se, až když tam nic nesedí. Neúspěšné stažení navíc
skript zastaví, místo aby se šlo zakládat kontejner ze šablony, která není.

Ke stejné příležitosti Ctrl+C: bez trapu zabil jen rozdělaný podproces
a skript pokračoval dál — v hlášení od uživatele je vidět, jak po přerušeném
stahování vesele oznámil hotovou šablonu a pustil se do pct create.
2026-08-22 21:21:09 +02:00
gitea-actions 746916a755 Jeden příkaz místo dvou stažených souborů
binárka / linux (pull_request) Successful in 10s
testy / pytest (3.11) (pull_request) Successful in 0s
testy / pytest (3.12) (pull_request) Successful in 0s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 1s
Instalace teď vypadá tak, jak se u community-scripts čeká:

    bash -c "$(curl -fsSL .../deploy/pve-kostky.sh)"

Dva soubory to neuměly. Přes rouru z curlu není na disku hostitele žádný
soubor, který by šel poslat do kontejneru, takže se instalátor přesunul
dovnitř skriptu jako text v heredocu. Duplicita tím nevzniká — vypsat se dá
zpátky přes `bash pve-kostky.sh instalator`, když ho chce někdo pustit
v kontejneru, který si založil sám.

Argumenty se rourou předávají mizerně, tak jde všechno i proměnnou:
CTID=123 MODE=update. Ze staženého souboru funguje i pozičně.

Ke stejné příležitosti: úložiště se přestalo hádat. local-lvm na ZFS
instalacích neexistuje a pct create by spadlo až po půlce práce; teď se
vybere první aktivní, a když se nějaké vnutí, ověří se, že vůbec existuje.
2026-08-22 19:22:53 +02:00
gitea-actions eaa9c12ebc Nasazení na server: Proxmox LXC a Docker
binárka / linux (pull_request) Successful in 10s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 0s
Aby se to dalo nechat běžet pořád, ne jen spouštět večer na notebooku.

pve-kostky.sh se pouští na uzlu Proxmoxu, založí kontejner a předá řízení
lxc-install.sh, který uvnitř stáhne vydanou binárku, ověří kontrolní součet
a zapíše službu. Ty dva jsou oddělené schválně: instalátor o Proxmoxu nic
neví, takže se dá pustit i v kontejneru, který sis založil sám. Podoba je
odkoukaná od community-scripts.org, ale nic z jejich frameworku se nestahuje
— jejich build.func si instalační skript hledá natvrdo ve vlastním
repozitáři, takže mimo něj nefunguje.

Docker se staví ze zdrojáků, ne z binárky: image pak jde sestavit pro obě
architektury a nezávisí na tom, jestli release proběhl. Zamčené závislosti
z minula dělají sestavení opakovatelné.

Compose používá síť hostitele, a to je podstatné. V bridge režimu vidí
kontejner uvnitř adresu 172.17.0.2, tabule ji poctivě nabídne a udělá na ni
QR kód — jenže z telefonu je nedosažitelná. Ověřeno, ne odhadnuto; kdo bridge
potřebuje, musí nastavit DICE_HOST.

Aplikace sama nepotřebovala změnit nic. DICE_DB, DICE_PORT a DICE_HOST
pokrývají obojí a v LXC má kontejner vlastní adresu v LAN, takže hledání
adres i QR kód fungují beze změny.
2026-08-22 19:12:57 +02:00
david-spacil 923fc03581 Merge pull request 'Infrastrukturní úklid: zamčené závislosti, verze schématu i binárky' (#12) from agents/claude/infrastruktura into main
testy / pytest (3.11) (push) Successful in 0s
testy / pytest (3.12) (push) Successful in 0s
testy / pytest (3.13) (push) Successful in 1s
testy / pytest (3.14) (push) Successful in 1s
Reviewed-on: #12
2026-08-22 18:42:28 +02:00
gitea-actions 3c3805362f Srovnat počet testů v README
binárka / linux (pull_request) Successful in 11s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 2s
testy / pytest (3.14) (pull_request) Successful in 0s
2026-08-22 18:32:27 +02:00
gitea-actions 023b743c76 Stavět načisto, ať projde i druhá stavba za sebou
uv nad existujícím prostředím skončí chybou a chce --clear. Domácí runner si
pracovní adresář mezi běhy drží a git clean -fd na .build nesáhne, protože je
v .gitignore — takže druhá stavba ve stejném adresáři spadla dřív, než se
stihlo cokoli postavit.
2026-08-22 18:32:16 +02:00
gitea-actions a655bfe6f3 Stažení databáze ze síně slávy
Dokud se hrálo ze zdrojáků, ležel dice.db v pracovním adresáři a záloha byla
otázka zkopírování. Z binárky se schovává do systémového datového adresáře,
který na Windows běžný člověk nenajde — takže se teď dá stáhnout přes
prohlížeč.

VACUUM INTO udělá konzistentní kopii i uprostřed zápisu, na rozdíl od
prostého přečtení souboru. Kopie se přečte do paměti a hned zahodí, ať
nezůstane ležet v dočasném adresáři, když si ji někdo nestáhne celou.
2026-08-22 18:30:49 +02:00
gitea-actions 1a2053ff66 Přidat linuxovou binárku pro ARM64
GitHub dává veřejným repozitářům běhouny ubuntu-24.04-arm zadarmo, takže je
to jeden řádek do matice. Hodí se pro Raspberry Pi nebo jinou malou pořád
zapnutou krabičku, což je rozumnější místo, kde tohle nechat běžet, než
notebook, který se každý večer zapíná.

x86-64 zůstává doma. Tam jde stavět proti staré glibc a když GitHub vypadne,
release má pořád tu platformu, na které to reálně poběží. ARM je výjimka
prostě proto, že doma na něm není na čem postavit.
2026-08-22 18:29:29 +02:00
gitea-actions 33e8582079 Obsluhovat to waitressem, ne vývojovým serverem Flasku
Flask pod pěkně naformátovanou úvodní hlášku vysypal červené varování, že
tohle není produkční server. Waitress je čistě pythonní, takže ho PyInstaller
zabalí bez řečí, a osm vláken pobere telefony u stolu i tabuli, která se
doptává každé dvě vteřiny.

Cestou se ukázalo, že hláška se nikdy nedostala do přesměrovaného výstupu:
mimo konzoli drží Python výstup v bloku a pouští ho, až program skončí —
jenže server neskončí. Zkouška binárky u Gitey si logovala do souboru a pak
ho vypisovala, takže celou dobu tiskla prázdno. Řeší to řádkové bufferování
v console.utf8(), kam to patří vedle té kódové stránky.
2026-08-22 18:29:00 +02:00
gitea-actions e3d56f4c22 Binárka má vědět, která je
Venku jsou čtyři soubory na release, tři systémy a k tomu zdrojáky. Když
někdo napíše, že mu něco nefunguje, není jak zjistit, co vlastně pustil.

Verzi zapisuje build.sh do verze.txt, kostky.spec ji přibalí a version.py ji
z rozbalené binárky přečte. Ze zdrojáků má přednost git describe, který ví
i o commitech nad tagem a o rozdělané práci. V CI se na tagu vnucuje
proměnnou, protože runner klonuje jeden commit bez značek.

Hlásí se přes --version, v úvodní hlášce serveru a v patičce každé stránky.
Zkouška v obou workflow ověří, že se do binárky opravdu dostala.
2026-08-22 18:26:32 +02:00
gitea-actions 4ffd7d3c9d Verzovat schéma databáze
Dokud to běželo jen doma, šlo dice.db v nejhorším smazat. Teď jsou venku
binárky, které si zakládají databázi v %LOCALAPPDATA% a v Library, a ta data
patří cizím lidem — první sloupec, který přibude, na nich prostě nebude.

Verze se drží v PRAGMA user_version. Existující databáze mají nulu a plné
schéma zároveň, což vychází: SCHEMA je samé IF NOT EXISTS, takže se na nich
jen orazítkuje a nic se neztratí.

Databázi z novější verze aplikace radši odmítneme otevřít. Srozumitelná
hláška je lepší než tiše rozbitá data, ke kterým neexistuje záloha.
2026-08-22 18:23:38 +02:00
gitea-actions 3af8fe22ef Zamknout závislosti
Bez zamčených verzí si každá stavba stáhne to, co je zrovna nejnovější:
binárka z dneška a binárka z příštího jara obsahují něco jiného a není jak
zjistit co. Zároveň může vydání kterékoli knihovny rozbít build, aniž by se
sáhlo na řádku kódu — a nejspíš zrovna ve chvíli, kdy se tagne release.

Volné seznamy jsou nově v `.in`, zamčené v `.txt`. Rozděleno na tři: běh,
testy a stavba. Pytest se tak přestane instalovat do prostředí, kde se
staví binárka, a PyInstaller má konečně taky zamčenou verzi — ta na podobu
výsledku sahá ze všech nejvíc.

Zůstává, že se verze píšou i v hlavičce PEP 723, jinak by přestalo fungovat
`uv run web.py`. Aby se ta dvě místa nerozešla, hlídá je test.
2026-08-22 18:22:35 +02:00
david-spacil e0413b6adf Merge pull request 'Čeština na výpisu nesmí shodit server' (#11) from agents/claude/cestina-na-windows into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 1s
testy / pytest (3.13) (push) Successful in 1s
testy / pytest (3.14) (push) Successful in 1s
binárka / linux (push) Successful in 11s
Reviewed-on: #11
2026-08-22 16:17:10 +02:00
gitea-actionsandClaude Opus 5 5821385dc8 Čeština na výpisu nesmí shodit server
binárka / linux (pull_request) Successful in 10s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 0s
testy / pytest (3.13) (pull_request) Successful in 0s
testy / pytest (3.14) (pull_request) Successful in 1s
Windowsová binárka spadla dřív, než stihla vypsat adresu:

  print("Počitadlo je dostupné na:")
  UnicodeEncodeError: 'charmap' codec can't encode character 'č'

Windows sahá po historické kódové stránce všude, kde výstup není konzole —
přesměrovaný do souboru, puštěný ze skriptu, ve frontě CI. První háček nad
takovým proudem shodí celý program.

Oba vstupní body si teď výstup přepnou na UTF-8. Na konzoli i na ostatních
systémech je to prázdná operace, protože tam je UTF-8 dávno výchozí.
Pojistka errors="replace" je pro zbytek případů: rozsypaný háček je pořád
lepší než spadlý server.

Chytila to zkouška po stavbě, ne uživatel. Test to reprodukuje přes
TextIOWrapper s cp1252, takže se to bez opravy neprojde ani na Linuxu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:12:16 +02:00
david-spacil 0fd8d072ec Merge pull request 'Opravit značky akcí a zrušený Intel běhoun' (#10) from agents/claude/oprava-znacek into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 1s
testy / pytest (3.13) (push) Successful in 0s
testy / pytest (3.14) (push) Successful in 1s
Reviewed-on: #10
2026-08-22 16:07:11 +02:00
gitea-actionsandClaude Opus 5 d4b979e627 Opravit značky akcí a zrušený Intel běhoun
binárka / linux (pull_request) Successful in 10s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 0s
První ostrý běh na GitHubu spadl hned v přípravě jobu:

  Unable to resolve action `astral-sh/setup-uv@v10`

Vydání v10.0.1 existuje, plovoucí značka v10 ne — setup-uv je přestal
vydávat po v7. Obě akce se teď píšou přesnou verzí; u třetí strany je to
stejně poctivější, protože je vidět, co se doopravdy pouští.

Druhá věc: macos-13 GitHub zrušil, takže ten job jen visel ve frontě.
Intel se staví na macos-15-intel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 15:57:11 +02:00
david-spacil 68a9f1d19f Merge pull request 'Binárky pro Windows a macOS' (#9) from agents/claude/binarky-win-mac into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 0s
testy / pytest (3.13) (push) Successful in 1s
testy / pytest (3.14) (push) Successful in 1s
Reviewed-on: #9
2026-08-22 13:46:55 +02:00
gitea-actionsandClaude Opus 5 a7621bb01b Binárky pro Windows a macOS přes GitHub Actions
binárka / linux (pull_request) Successful in 11s
testy / pytest (3.11) (pull_request) Successful in 1s
testy / pytest (3.12) (pull_request) Successful in 1s
testy / pytest (3.13) (pull_request) Successful in 1s
testy / pytest (3.14) (pull_request) Successful in 0s
PyInstaller neumí křížovou kompilaci — binárku pro každý systém musí
postavit ten systém. Vlastní runner pro Windows a hlavně pro macOS by byl
nepoměr, tak si tyhle stroje půjčujeme od GitHub Actions.

GitHub je jen půjčená dílna, ne druhý domov projektu: repozitář se tam
z Gitey zrcadlí, hotové soubory se posílají zpátky na zdejší release přes
Gitea API a stránka s releasy zůstává jedna. Linux se tam schválně
nestaví; doma to jde proti glibc 2.17 a je to zároveň pojistka, kdyby
GitHub vypadl.

build.sh je společný pro všechny tři systémy, liší se jen tím, že Windows
dává spustitelné soubory venvu do Scripts/ místo bin/. Ořezání symbolů
zůstává jen Linuxu: na macOS by rozbilo podpis, který si PyInstaller sám
přidává a bez kterého se binárka na Apple Silicon vůbec nespustí.

Zkouška po stavbě je stejná jako u linuxové: nastartovat, stáhnout si
stránku i styly a ověřit, že se založila databáze.

Nepodepsané binárky si Gatekeeper ani SmartScreen nenechají líbit, tak je
v README napsané, co s tím — certifikáty za tisíce ročně by na počitadlo
kostek byly nesmysl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:13:57 +02:00
gitea-actionsandClaude Opus 5 26338bfa29 Adresy a data podle systému, ne podle Linuxu
Než začneme rozdávat binárky pro macOS a Windows, musí se na nich chovat
rozumně to, co dosud počítalo s Linuxem.

Adresy rozhraní se hledaly linuxovým ioctl. Na Windows chybí fcntl, na
macOS to číslo znamená něco jiného, takže obojí propadlo až na poslední
zálohu přes hostname — a ta na macOS vrátí jednu adresu. Tabule by tam
nabídla jedinou volbu, přesně to, co jsme minule opravovali. Přibyla
mezizáloha přes ifconfig; spouští se jen tam, kde ioctl nic nenašel,
takže na Linuxu se neplatí nic. Parsování je zvlášť jako čistá funkce,
ať jde otestovat i bez macOS pod rukama.

Databáze z binárky mířila do XDG adresáře na všech systémech. Na Windows
by to znamenalo C:\Users\...\.local\share\, což tam nikdo nečeká. Teď se
volí podle systému: LOCALAPPDATA, Library/Application Support, XDG.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:13:41 +02:00
35 changed files with 1860 additions and 85 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
# Do image patří jen to, co appka za běhu potřebuje.
.git/
.github/
.gitea/
.venv/
.build/
.pytest_cache/
__pycache__/
dist/
tests/
deploy/
*.db
*.spec
build.sh
dice.py
verze.txt
README.md
requirements*.in
requirements-dev.txt
requirements-build.txt
+21 -9
View File
@@ -53,17 +53,28 @@ jobs:
uv --version
- name: Testy
run: uv run --with-requirements requirements.txt --no-project pytest -q
run: uv run --with-requirements requirements-dev.txt --no-project pytest -q
- name: Binárka
run: ./build.sh
run: |
# Runner klonuje jeden commit bez značek, takže si git describe
# nemá o co opřít — na tagu mu verzi řekneme rovnou.
if [ "$GITHUB_REF_TYPE" = tag ]; then
export VERSION="$GITHUB_REF_NAME"
fi
./build.sh
- name: Zkouška
run: |
XDG_DATA_HOME=/tmp/zkouska DICE_PORT=8321 DICE_HOST=127.0.0.1 \
./dist/kostky > /tmp/server.log 2>&1 &
# Vlastní adresář pro každý běh. Runner si /tmp mezi běhy drží
# a binárka umí sáhnout po databázi ve starém umístění, takže
# zbytek po dřívější zkoušce by tuhle poslal vedle.
XDG=$(mktemp -d)
XDG_DATA_HOME="$XDG" DICE_PORT=8321 DICE_HOST=127.0.0.1 \
./dist/dice-counter > /tmp/server.log 2>&1 &
server=$!
trap 'kill $server 2>/dev/null || true' EXIT
trap 'kill $server 2>/dev/null || true; rm -rf "$XDG"' EXIT
for _ in $(seq 30); do
curl -sf -o /dev/null http://127.0.0.1:8321/ && break
@@ -73,14 +84,15 @@ jobs:
curl -sf -o /dev/null http://127.0.0.1:8321/
curl -sf -o /dev/null http://127.0.0.1:8321/static/style.css
curl -sf -o /dev/null http://127.0.0.1:8321/board
test -f /tmp/zkouska/kostky/dice.db
test -f "$XDG/dice-counter/dice.db"
./dist/dice-counter --version
cat /tmp/server.log
- name: Na release
if: startsWith(github.ref, 'refs/tags/')
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
ASSET: kostky-linux-x86_64
ASSET: dice-counter-linux-x86_64
run: |
api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
auth="Authorization: token ${TOKEN}"
@@ -97,8 +109,8 @@ jobs:
fi
# Součet se počítá nad jménem, pod kterým se soubor stahuje, aby
# šel ověřit rovnou: sha256sum -c kostky-linux-x86_64.sha256
cp dist/kostky "$ASSET"
# šel ověřit rovnou: sha256sum -c dice-counter-linux-x86_64.sha256
cp dist/dice-counter "$ASSET"
sha256sum "$ASSET" | tee "$ASSET.sha256"
for file in "$ASSET" "$ASSET.sha256"; do
+85
View File
@@ -0,0 +1,85 @@
# Postaví kontejnerový image a zkusí ho spustit. Nikam ho neposílá.
#
# Dockerfile by se jinak rozbil až ve chvíli, kdy se tagne release — tohle ho
# ohlídá nad každým pull requestem.
#
# Publikuje se jinde: .github/workflows/image.yml. Zdejší runner běží v LXC,
# kde nejde zaregistrovat binfmt, takže arm64 by se tu postavit nedalo.
# Registrace je vlastnost jádra hostitele a to je produkční stroj, do kterého
# se kvůli počitadlu kostek vrtat nebude.
#
# Staví se nativní amd64 obyčejným `docker build`, žádný buildx ani emulace.
name: image
on:
pull_request:
workflow_dispatch:
env:
ZKOUSKA_PORT: "18000"
jobs:
image:
runs-on: homelab
steps:
- name: Zdrojáky
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git init -q .
git remote remove origin 2>/dev/null || true
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git config http.extraheader "Authorization: token ${TOKEN}"
git fetch -q --depth 1 origin "${GITHUB_REF}"
git config --unset http.extraheader
git checkout -qf FETCH_HEAD
git clean -qfd
- name: Předpoklady
run: |
command -v docker >/dev/null \
|| { echo "Docker na runneru není." >&2; exit 1; }
docker version --format '{{.Server.Version}}'
- name: Stavba
run: |
docker build -f deploy/Dockerfile \
--build-arg VERSION="zkouska-${GITHUB_SHA:0:7}" \
-t dice-counter:zkouska .
- name: Zkouška
run: |
trap 'docker rm -f dice-counter-zkouska >/dev/null 2>&1 || true' EXIT
# Port musí být stejný uvnitř i venku: appka poslouchá na DICE_PORT,
# takže mapovat ho na jiný by znamenalo mířit do prázdna.
# Adresa se vnucuje, ať zkouška nezávisí na síti runneru a dá se
# ověřit, co přesně tabule nabídne.
docker run -d --name dice-counter-zkouska --init \
-p "${ZKOUSKA_PORT}:${ZKOUSKA_PORT}" -e DICE_HOST=127.0.0.1 \
-e DICE_PORT="${ZKOUSKA_PORT}" dice-counter:zkouska
for _ in $(seq 30); do
curl -sf -o /dev/null "http://127.0.0.1:${ZKOUSKA_PORT}/" && break
sleep 1
done
curl -sf -o /dev/null "http://127.0.0.1:${ZKOUSKA_PORT}/" || {
echo "Kontejner se nerozeběhl." >&2
docker logs dice-counter-zkouska >&2
exit 1; }
curl -sf -o /dev/null "http://127.0.0.1:${ZKOUSKA_PORT}/static/style.css"
curl -sf "http://127.0.0.1:${ZKOUSKA_PORT}/board" | grep -q "127.0.0.1:${ZKOUSKA_PORT}"
curl -sf -o /dev/null "http://127.0.0.1:${ZKOUSKA_PORT}/export"
# Verze se musí do image opravdu dostat, ne se hlásit jako neznámá.
docker run --rm dice-counter:zkouska python web.py --version
! docker run --rm dice-counter:zkouska python web.py --version | grep -q "neznámá"
docker logs dice-counter-zkouska
echo "Image jede."
- name: Úklid
if: always()
run: docker rmi dice-counter:zkouska >/dev/null 2>&1 || true
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
- name: Testy
run: |
uv run --managed-python --python "${{ matrix.python }}" \
--with-requirements requirements.txt --no-project \
--with-requirements requirements-dev.txt --no-project \
pytest -q
- name: Terminálová verze
+142
View File
@@ -0,0 +1,142 @@
# Binárky pro Windows, macOS a Linux na ARM.
#
# PyInstaller neumí křížovou kompilaci: binárku pro každý systém musí postavit
# ten systém. Kupovat kvůli tomu Mac nebo krmit Windows VM by bylo přehnané,
# tak si tyhle stroje půjčujeme od GitHub Actions.
#
# GitHub je tu jen půjčená dílna, ne druhý domov projektu: repozitář se sem
# z Gitey zrcadlí a hotové soubory se posílají zpátky na tamní release přes
# Gitea API. Jedna stránka s releasy, ta původní.
#
# Linux na x86-64 se tu schválně nestaví — o ten se stará vlastní runner
# (.gitea/workflows/binarka.yml). Když GitHub vypadne, release má pořád tu
# platformu, na které to reálně poběží. ARM doma není na čem postavit, takže
# ten jediný linuxový sem patří.
name: binárky
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
env:
GITEA: https://gitea.spacilovi.eu
GITEA_REPO: david-spacil/dice-counter
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
asset: dice-counter-windows-x86_64.exe
built: dist/dice-counter.exe
- os: macos-15-intel # Intel; macos-13 GitHub zrušil
asset: dice-counter-macos-x86_64
built: dist/dice-counter
- os: macos-latest # Apple Silicon
asset: dice-counter-macos-arm64
built: dist/dice-counter
- os: ubuntu-24.04-arm # pro Raspberry Pi a spol.
asset: dice-counter-linux-arm64
built: dist/dice-counter
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
# Přesné verze, ne plovoucí `v7`. Ta u setup-uv nevede tam, kam by
# člověk čekal — hlavní značky se přestaly vydávat po v7, takže `v10`
# neexistuje, i když vydání v10.0.1 ano.
- uses: actions/checkout@v7.0.1
- uses: astral-sh/setup-uv@v10.0.1
- name: Testy
run: uv run --with-requirements requirements-dev.txt --no-project pytest -q
- name: Binárka
run: |
# Na tagu je verze jistá. Jinde je repozitář naklonovaný mělce
# a build.sh si vystačí s tím, co z gitu vyčte.
if [ "$GITHUB_REF_TYPE" = tag ]; then
export VERSION="$GITHUB_REF_NAME"
fi
./build.sh
- name: Zkouška
run: |
# Binárka se musí umět rozbalit, najít šablony a začít poslouchat.
# Adresa se vnucuje, ať zkouška nezávisí na síti běhounu.
export DICE_HOST=127.0.0.1 DICE_PORT=8123 DICE_DB=zkouska/dice.db
"${{ matrix.built }}" &
for _ in $(seq 30); do
curl -sf -o /dev/null "http://127.0.0.1:8123/" && break
sleep 1
done
curl -sf -o /dev/null "http://127.0.0.1:8123/" || {
echo "Server se nerozeběhl." >&2; exit 1; }
curl -sf -o /dev/null "http://127.0.0.1:8123/static/style.css"
curl -sf "http://127.0.0.1:8123/board" | grep -q "127.0.0.1:8123"
test -f zkouska/dice.db
# Verze se musí do binárky opravdu dostat, ne se hlásit jako neznámá.
"${{ matrix.built }}" --version
! "${{ matrix.built }}" --version | grep -q "neznámá"
echo "Binárka jede."
- name: Na release
if: startsWith(github.ref, 'refs/tags/')
env:
TOKEN: ${{ secrets.GITEA_TOKEN }}
ASSET: ${{ matrix.asset }}
run: |
api="$GITEA/api/v1/repos/$GITEA_REPO"
auth="Authorization: token $TOKEN"
json() { uv run --no-project python -c "$@"; }
# Release u tagu být může, ale nemusí — dá se založit i ručně a
# sahají na něj čtyři buildy naráz, tak se po neúspěchu ptáme znovu.
for _ in 1 2 3; do
id=$(curl -s -H "$auth" "$api/releases/tags/${GITHUB_REF_NAME}" \
| json 'import json,sys; print(json.load(sys.stdin).get("id",""))')
[ -n "$id" ] && break
curl -s -o /dev/null -X POST -H "$auth" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\"}" \
"$api/releases"
done
[ -n "$id" ] || { echo "Release ${GITHUB_REF_NAME} se nepodařilo najít." >&2; exit 1; }
# macOS zná jen shasum, Git Bash na Windows jen sha256sum. Formát
# výsledku je stejný, takže se dá ověřit kterýmkoli z nich.
if command -v sha256sum >/dev/null; then
soucet() { sha256sum "$@"; }
else
soucet() { shasum -a 256 "$@"; }
fi
# Součet se počítá nad jménem, pod kterým se soubor stahuje, aby
# šel ověřit rovnou: shasum -a 256 -c dice-counter-macos-arm64.sha256
cp "${{ matrix.built }}" "$ASSET"
soucet "$ASSET" | tee "$ASSET.sha256"
for file in "$ASSET" "$ASSET.sha256"; do
# Aby se dal build zopakovat, stará příloha stejného jména jde pryč.
curl -s -H "$auth" "$api/releases/$id/assets" \
| json 'import json,sys; [print(a["id"]) for a in json.load(sys.stdin) if a["name"] == sys.argv[1]]' "$file" \
| while read -r old; do
curl -sf -X DELETE -H "$auth" "$api/releases/$id/assets/$old"
done
curl -sf -o /dev/null -X POST -H "$auth" -F "attachment=@$file" \
"$api/releases/$id/assets?name=$file"
echo "$file pověšen na ${GITHUB_REF_NAME}"
done
+79
View File
@@ -0,0 +1,79 @@
# Publikuje kontejnerový image do registru balíčků Gitey, pro amd64 i arm64.
#
# Proč zrovna odsud: domácí runner běží v LXC, kde nejde zaregistrovat binfmt
# — je to vlastnost jádra hostitele a ten je produkční. Běhouni GitHubu jsou
# plnohodnotné virtuály, kde emulace funguje bez řečí.
#
# Doma se image přesto staví a zkouší nad každým pull requestem
# (.gitea/workflows/image.yml), jen se nikam neposílá. Stejná dělba jako
# u binárek: doma se hlídá, tady se vydává.
name: image
on:
push:
tags: ["v*"]
workflow_dispatch:
env:
REGISTRY: gitea.spacilovi.eu
IMAGE: david-spacil/dice-counter
PLATFORMS: linux/amd64,linux/arm64
jobs:
publikace:
runs-on: ubuntu-latest
steps:
# Přesná verze, ne plovoucí značka — viz binarky.yml.
- uses: actions/checkout@v7.0.1
- name: QEMU pro arm64
run: |
# Bez hotové akce z marketplace, ať není co udržovat. Běhoun je
# virtuál, takže privilegovaný kontejner smí zapisovat do binfmt
# a registrace platí pro celé jádro.
docker run --privileged --rm tonistiigi/binfmt --install arm64
- name: Builder
run: |
# Výchozí ovladač Dockeru multiplatformní stavbu neumí.
docker buildx create --name dice-counter --driver docker-container --use
docker buildx inspect --bootstrap
# Bez tohohle by stavba spadla až na konci a hláška by nic neřekla.
PLATFORMY=$(docker buildx inspect)
echo "$PLATFORMY" | grep -q "linux/arm64" || {
echo "Buildkit nevidí linux/arm64, emulace se nezaregistrovala." >&2
exit 1; }
- name: Publikace
env:
TOKEN: ${{ secrets.PACKAGE_TOKEN }}
# Uživatel v Gitee, ne na GitHubu. Shodou okolností je to totéž.
VLASTNIK: david-spacil
run: |
# Spouští se to i ručně, a to klidně nad větví. Bez tagu není z čeho
# udělat verzi a `:latest` by ukazoval na rozdělanou práci.
[ "$GITHUB_REF_TYPE" = tag ] || {
echo "Publikovat jde jen z tagu, tohle je ${GITHUB_REF_TYPE} ${GITHUB_REF_NAME}." >&2
exit 1; }
# Automatický token na balíčky nestačí ani u Gitey samotné, natož
# odsud — musí to být osobní token s oprávněním write:package.
[ -n "$TOKEN" ] || { echo "Chybí secret PACKAGE_TOKEN." >&2; exit 1; }
echo "$TOKEN" | docker login "$REGISTRY" -u "$VLASTNIK" --password-stdin
trap 'docker logout "$REGISTRY" >/dev/null 2>&1 || true' EXIT
# Značka bez počátečního "v": v repozitáři je v1.2.0, u image se
# čeká 1.2.0. Verze vypálená do image ale zůstává s "v", ať --version
# hlásí totéž co binárky.
VERZE="${GITHUB_REF_NAME#v}"
docker buildx build --platform "$PLATFORMS" --push \
-f deploy/Dockerfile \
--build-arg VERSION="${GITHUB_REF_NAME}" \
-t "$REGISTRY/$IMAGE:$VERZE" \
-t "$REGISTRY/$IMAGE:latest" .
echo "Publikováno: $REGISTRY/$IMAGE:$VERZE a :latest"
docker buildx imagetools inspect "$REGISTRY/$IMAGE:$VERZE"
+1
View File
@@ -5,3 +5,4 @@ __pycache__/
dist/
build/
.build/
verze.txt
+241 -19
View File
@@ -8,16 +8,46 @@ ten samý Adam jako z minulého měsíce. Žádné účty, žádná hesla.
## Spuštění
Stáhni binárku z [releases](https://gitea.spacilovi.eu/david-spacil/dice-counter/releases)
a spusť ji:
Stáhni binárku pro svůj systém z
[releases](https://gitea.spacilovi.eu/david-spacil/dice-counter/releases):
| Systém | Soubor |
|---|---|
| Linux (x86-64) | `dice-counter-linux-x86_64` |
| Linux (ARM64) | `dice-counter-linux-arm64` |
| macOS (Apple Silicon) | `dice-counter-macos-arm64` |
| macOS (Intel) | `dice-counter-macos-x86_64` |
| Windows (x86-64) | `dice-counter-windows-x86_64.exe` |
Nic se neinstaluje — Python, Flask i šablony jsou uvnitř.
Na Linuxu a macOS:
```bash
chmod +x kostky
./kostky
chmod +x dice-counter-*
./dice-counter-linux-x86_64
```
Nic se neinstaluje — Python, Flask i šablony jsou uvnitř. Zatím jen pro Linux
na x86-64; postavené proti glibc 2.17, takže jede prakticky všude.
Na Windows stačí na `.exe` poklepat.
Linuxová binárka je postavená proti glibc 2.17, takže jede prakticky všude.
### Než to poprvé pustíš
Binárky nejsou podepsané — podpisové certifikáty stojí tisíce ročně a na
počitadlo kostek by to byl nesmysl. Systémy si toho všimnou:
- **macOS** stažený soubor označí za karanténní a odmítne ho spustit. Buď
značku sundej (`xattr -dr com.apple.quarantine dice-counter-macos-arm64`), nebo
soubor stáhni rovnou z terminálu přes `curl -LO` — tudy se karanténa
nenastavuje.
- **Windows** ukáže modré okno SmartScreenu. *Další informace* →
*Přesto spustit*. Občas si postěžuje i antivirus; u zabalených Python
programů je to běžný falešný poplach. Při prvním spuštění se ještě zeptá
brána firewall — bez povolení do místní sítě se telefon nepřipojí.
Kdo tomu nechce věřit, ověří si stažený soubor podle přiloženého součtu
(níž) nebo si ho postaví sám — `./build.sh`, zdrojáky jsou tady celé.
Ze zdrojáků to je stejně krátké:
@@ -30,12 +60,17 @@ Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará sám.
Server vypíše všechny adresy, na kterých je dostupný:
```
dice-counter v1.2.2
Počitadlo je dostupné na:
→ http://10.186.234.182:8000 místní síť
http://100.91.0.24:8000 přes Tailscale
http://172.17.0.1:8000 virtuální síť
```
Kterou verzi máš, řekne i `dice-counter --version`; visí taky v patičce každé
stránky. Když něco nefunguje, je to první věc, na kterou se zeptám.
Na notebooku otevři `/board`, naskenuj QR kód telefonem a hraj. Tabule
nabízí i zbylé adresy — když jedna nefunguje, klikni na jinou a QR kód se
přepne na ni. Zastavuje se `Ctrl+C`.
@@ -53,7 +88,9 @@ server vypíše při startu.
| Spuštěno | Databáze |
|---|---|
| binárkou | `~/.local/share/kostky/dice.db` |
| binárkou na Linuxu | `~/.local/share/dice-counter/dice.db` |
| binárkou na macOS | `~/Library/Application Support/dice-counter/dice.db` |
| binárkou na Windows | `%LOCALAPPDATA%\dice-counter\dice.db` |
| ze zdrojáků | `dice.db` v pracovním adresáři |
Binárka se při každém spuštění rozbaluje do dočasného adresáře a pouští se
@@ -62,11 +99,134 @@ napevno domovský adresář. Ze zdrojáků zůstává relativní, ať se dá mí
sad vedle sebe.
Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho
najde kdekoli.
najde kdekoli. Kdo ho hledat nechce — a z binárky na Windows je zalezlý —
si ho stáhne ze síně slávy odkazem **Stáhnout databázi** (`/export`). Kopie
se dělá přes `VACUUM INTO`, takže je použitelná i uprostřed rozehrané hry.
Schéma má verzi uloženou v `PRAGMA user_version` a `storage.migrate()` si
databázi při každém připojení dorovná. Starší soubory z verzí, kdy se verze
schématu ještě nepsala, se poznají podle nuly a orazítkují se. Databázi
založenou novější verzí počitadla appka odmítne otevřít, místo aby ji tiše
rozbila.
Obsluhuje to `waitress` — čistě pythonní WSGI server, který se zabalí do
binárky stejně snadno jako zbytek. Vestavěný server Flasku by pod úvodní
hlášku vysypal varování, že takhle se to nemá, a měl by pravdu.
Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi
může zapisovat.
## Nasazení na server
Kdo nechce každý večer spouštět binárku na notebooku, může to nechat běžet
pořád. Obojí níž počítá **jen s domácí sítí** — appka nemá přihlašování
a ani jedno nasazení ji nijak nezabezpečuje. Do internetu to nepatří.
### Proxmox (LXC)
Jeden příkaz v terminálu uzlu Proxmoxu, jako root. Založí kontejner,
nastaví ho, stáhne poslední vydanou binárku, ověří kontrolní součet a zapne
službu:
```bash
bash -c "$(curl -fsSL https://gitea.spacilovi.eu/david-spacil/dice-counter/raw/branch/main/deploy/pve-dice-counter.sh)"
```
Na konci vypíše adresu, na které počitadlo poslouchá. Výchozí nastavení je
1 jádro, 512 MB, 4 GB disku a Debian 13; úložiště si najde samo. Přebíjí se
proměnnými před příkazem:
```bash
MEMORY=1024 STORAGE=local-zfs PORT=8080 bash -c "$(curl -fsSL ...)"
```
| Proměnná | Výchozí |
|---|---|
| `CTID` | první volné číslo |
| `CT_HOSTNAME` | `dice-counter` |
| `CORES`, `MEMORY`, `DISK` | `1`, `512`, `4` |
| `STORAGE` | první aktivní úložiště pro kontejnery |
| `TEMPLATE_STORAGE`, `BRIDGE` | `local`, `vmbr0` |
| `PORT` | `8000` |
| `UNPRIVILEGED`, `OSVERSION` | `1`, `13` |
| `NESTING` | `1` — systemd v Debianu 13 ho potřebuje |
| `AUTOLOGIN` | `1` — konzole ve webu Proxmoxu bez hesla |
| `VERSION` | poslední vydání |
Aktualizace na novější vydání se dá spustit dvěma způsoby. Buď zevnitř
kontejneru, kde na to stačí jedno slovo:
```bash
update
```
Nebo z uzlu Proxmoxu tím samým příkazem s číslem kontejneru:
```bash
CTID=123 MODE=update bash -c "$(curl -fsSL ...)"
```
Obojí vymění binárku za poslední vydání a službu restartuje; databáze zůstává.
Příkaz `update` si přitom obnoví i sám sebe — instalátor bere z větve `main`,
takže jede na nejnovější verzi skriptu, ne na té, se kterou se instalovalo.
Do konzole kontejneru se dostaneš přímo z webu Proxmoxu, **žádné jméno ani
heslo se nezadává** — kontejner root heslo nemá a konzole se přihlašuje sama.
Nová práva to nikomu nedává: kdo se dostane do webu Proxmoxu, má root na uzlu
tak jako tak. Komu se to nezdá, vypne to přes `AUTOLOGIN=0` a do kontejneru
pak leze přes `pct enter <ctid>`.
Podoba je odkoukaná od [community-scripts.org](https://community-scripts.org),
ale nic z jejich frameworku se nestahuje — jejich `build.func` si instalační
skript hledá natvrdo ve vlastním repozitáři, takže mimo něj nefunguje.
Instalátor, který běží uvnitř kontejneru, je ve skriptu vložený jako text —
přes rouru z curlu na disku hostitele žádný druhý soubor není. O Proxmoxu nic
neví, takže se dá použít i v kontejneru, který sis založil sám:
```bash
bash pve-dice-counter.sh instalator > install.sh
```
Je idempotentní; druhé spuštění jen vymění binárku za nejnovější.
### Docker
Stačí jeden soubor, repozitář k tomu potřeba není:
```bash
curl -fsSLO https://gitea.spacilovi.eu/david-spacil/dice-counter/raw/branch/main/deploy/docker-compose.yaml
docker compose up -d
```
Image se stáhne hotový z registru Gitey, pro amd64 i arm64 — Docker si vybere
svoji variantu sám. Databáze leží v pojmenovaném svazku, appka běží pod
nerootovým uživatelem.
Aktualizace na novější vydání:
```bash
docker compose pull && docker compose up -d
```
Kdo si ho radši postaví ze zdrojáků, přebije ten stažený stejnou značkou:
```bash
docker build -f deploy/Dockerfile -t gitea.spacilovi.eu/david-spacil/dice-counter:latest .
```
**Pozor na síť.** Compose schválně používá `network_mode: host`. V bridge
režimu kontejner uvnitř vidí adresu jako `172.17.0.2`, tabule ji poctivě
nabídne a vygeneruje na ni QR kód — jenže z telefonu je nedosažitelná.
Kdo bridge potřebuje, musí nastavit `DICE_HOST` na adresu hostitele v LAN.
| | LXC | Docker |
|---|---|---|
| Odkud | vydaná binárka z releasu | image z registru |
| Databáze | `/var/lib/dice-counter/dice.db` | svazek `dice-counter-data` |
| Aktualizace | `update` uvnitř kontejneru | `docker compose pull` |
| Adresa a QR | funguje samo | potřeba síť hostitele |
## Když se telefon nepřipojí
Adresy se hledají při každém načtení tabule, takže přechod z domácí WiFi na
@@ -100,7 +260,12 @@ při remíze na prvním místě se hraje dál.
| `stats.py` | síň slávy a kariérní statistiky |
| `web.py` | Flask aplikace |
| `dice.py` | totéž v terminálu, bez ukládání |
| `build.sh`, `kostky.spec` | stavba binárky |
| `console.py` | aby čeština prošla i windowsovou konzolí |
| `version.py` | která verze to je — z gitu, nebo z binárky |
| `build.sh`, `dice-counter.spec` | stavba binárky |
| `.gitea/workflows/` | testy, linuxová binárka a zkouška image doma |
| `.github/workflows/` | binárky pro Windows a macOS |
| `deploy/` | nasazení na server: Proxmox LXC a Docker |
Terminálová verze zůstává funkční jako záloha:
@@ -121,40 +286,97 @@ python -m venv .venv
Na Fedoře stačí i systémové balíčky, pak se nemusí řešit vůbec nic:
```bash
sudo dnf install python3-flask python3-qrcode python3-tabulate
sudo dnf install python3-flask python3-qrcode python3-tabulate python3-waitress
python3 web.py
```
## Závislosti
Verze jsou zamčené, aby binárka postavená dnes a za půl roku obsahovala
totéž. Volné seznamy jsou v `.in`, zamčené v `.txt`:
| Soubor | K čemu |
|---|---|
| `requirements.in` → `.txt` | běh aplikace |
| `requirements-dev.in` → `.txt` | + pytest, pro testy |
| `requirements-build.in` → `.txt` | + PyInstaller, pro stavbu binárky |
Aktualizace je vědomý krok, ne vedlejší efekt toho, že něco vyšlo na PyPI:
```bash
for f in requirements requirements-dev requirements-build; do
uv pip compile "$f.in" -o "$f.txt"
done
```
Verze se píšou ještě jednou v hlavičce PEP 723 na začátku `web.py` a
`dice.py`, aby fungovalo `uv run web.py`. Že se ty dva zápisy shodují, hlídá
`tests/test_zavislosti.py` — po každém přegenerování je potřeba hlavičky
srovnat.
## Vlastní binárka
```bash
./build.sh
```
Verzi si `build.sh` vezme z `git describe`, nebo se dá vnutit přes
`VERSION=v1.2.3 ./build.sh`. Zapíše ji do `verze.txt`, `dice-counter.spec` ji
přibalí a binárka ji pak umí ohlásit i na cizím počítači, kde žádný git není.
Verzi Pythonu, proti kterému se staví, přebíjí `PYTHON_VERSION`.
Staví se proti samostatnému CPythonu od `uv`, ne proti systémovému. Ten je
slinkovaný s glibc 2.17; postavené proti Pythonu z Fedory 44 by to chtělo
glibc 2.43 a nešlo by spustit skoro nikde — glibc drží zpětnou kompatibilitu,
ne dopřednou. Nic z hostitele se do binárky nedostane, takže kontejner k tomu
potřeba není. Výsledek je v `dist/kostky`.
potřeba není. Výsledek je v `dist/`.
Stejný `build.sh` staví i na macOS a na Windows (v Git Bash) — jen tam bez
té starosti o glibc. Křížem to nejde: PyInstaller balí do výsledku interpret
a knihovny toho systému, na kterém běží, takže **binárku pro každý systém
musí postavit ten systém.**
Terminálová verze součástí binárky není; ta se pouští ze zdrojáků.
Releasy si tohle dělají samy. Otagovaný commit spustí
`.gitea/workflows/binarka.yml`, ta binárku postaví, projede testy, zkusí
ji nastartovat a pověsí ji na release jako `kostky-linux-x86_64`:
### Jak vznikají releasy
Otagovaný commit spustí stavbu na obou stranách:
| Kde | Co staví | Workflow |
|---|---|---|
| vlastní runner u Gitey | Linux x86-64 | `.gitea/workflows/binarka.yml` |
| GitHub Actions | Windows, macOS ×2, Linux ARM64 | `.github/workflows/binarky.yml` |
| GitHub Actions | image amd64 + arm64 | `.github/workflows/image.yml` |
```bash
git tag v1.0.0 && git push origin v1.0.0
```
Vedle binárky se věší i `kostky-linux-x86_64.sha256`, takže se stažený
soubor dá ověřit:
GitHub je tu jen půjčená dílna. Repozitář se tam z Gitey zrcadlí
([mirror](https://github.com/david-spacil/dice-counter)), postavené soubory
se posílají zpátky na zdejší release přes Gitea API a projekt má pořád jednu
stránku s releasy — tuhle. Linux na x86-64 se na GitHubu schválně nestaví;
doma to jde proti staré glibc, a když GitHub vypadne, release má aspoň tu
platformu, na které to reálně poběží. ARM64 je výjimka — doma na něm není
na čem stavět, a hodí se pro Raspberry Pi nebo jinou malou pořád zapnutou
krabičku.
Každá stavba nejdřív projede testy a zkusí hotovou binárku nastartovat, než
ji kamkoli pověsí. Vedle každé visí i `.sha256`, takže se stažený soubor dá
ověřit:
```bash
sha256sum -c kostky-linux-x86_64.sha256
sha256sum -c dice-counter-linux-x86_64.sha256 # na macOS: shasum -a 256 -c
```
Stejná stavba běží i nad každým pull requestem, jen se nikam nevěší.
Nad pull requesty se Linux staví taky, jen se nikam nevěší. Totéž platí pro
kontejnerový image — doma se nad každým PR postaví a zkusí nastartovat
(`.gitea/workflows/image.yml`), publikuje se ale až z tagu na GitHubu.
Image se schválně nevydává doma: tamní runner běží v LXC, kde nejde
zaregistrovat emulaci pro arm64 — je to vlastnost jádra hostitele a ten je
produkční stroj. Běhouni GitHubu jsou plnohodnotné virtuály, kde to funguje
bez zásahu do čehokoli.
## Testy
@@ -165,5 +387,5 @@ uv run --with-requirements requirements.txt --no-project pytest
Nebo z připraveného prostředí prostě `pytest`.
Nad každým pull requestem a nad `main` běží `.gitea/workflows/testy.yml` —
84 testů na Pythonu 3.11, 3.12, 3.13 i 3.14, a k tomu skriptovaná partie
118 testů na Pythonu 3.11, 3.12, 3.13 i 3.14, a k tomu skriptovaná partie
v terminálové verzi, na kterou pytest nesahá.
+35 -13
View File
@@ -1,22 +1,44 @@
#!/usr/bin/env bash
# Postaví jednosouborovou binárku pro Linux do dist/kostky.
# Postaví jednosouborovou binárku do dist/ — pro systém, na kterém běží.
#
# Staví se proti samostatnému CPythonu od uv, ne proti systémovému. Ten je
# slinkovaný s glibc 2.17, takže binárka jede i na letitých systémech.
# Postavená proti Pythonu z Fedory 44 by chtěla glibc 2.43 a nešla by
# spustit skoro nikde — glibc drží zpětnou kompatibilitu, ne dopřednou.
# Nic z hostitele se do ní nedostane, takže kontejner k tomu není potřeba.
# PyInstaller neumí křížovou kompilaci: binárku pro každý systém musí postavit
# ten systém. Linux se staví doma na vlastním runneru, Windows a macOS na
# půjčených strojích GitHub Actions (.github/workflows/binarky.yml).
#
# Staví se proti samostatnému CPythonu od uv, ne proti systémovému. Na Linuxu
# je slinkovaný s glibc 2.17, takže binárka jede i na letitých systémech.
# Postavená proti Pythonu z Fedory 44 by chtěla glibc 2.43 a nešla by spustit
# skoro nikde — glibc drží zpětnou kompatibilitu, ne dopřednou. Nic z hostitele
# se do ní nedostane, takže kontejner k tomu není potřeba.
set -euo pipefail
VERSION=${VERSION:-3.11}
PYTHON_VERSION=${PYTHON_VERSION:-3.11}
BUILD=${BUILD:-.build}
uv python install "$VERSION"
uv venv --quiet --managed-python --python "$VERSION" "$BUILD"
uv pip install --quiet --python "$BUILD/bin/python" -r requirements.txt pyinstaller
# Verze, kterou binárka ohlásí přes --version. V CI ji na tagu vnutíme
# proměnnou, protože tam je repozitář naklonovaný na jeden commit a git
# describe nemá o co se opřít.
VERSION=${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo neznámá)}
printf '%s\n' "$VERSION" > verze.txt
echo "Verze: $VERSION"
"$BUILD/bin/pyinstaller" --clean --noconfirm \
--distpath dist --workpath "$BUILD/work" kostky.spec
uv python install "$PYTHON_VERSION"
# --clear, protože runner si pracovní adresář mezi běhy drží a uv nad
# existujícím prostředím jinak skončí chybou. Stavíme načisto.
uv venv --quiet --clear --managed-python --python "$PYTHON_VERSION" "$BUILD"
# Windows dává spustitelné soubory venvu do Scripts/, zbytek světa do bin/.
python="$BUILD/bin/python"
pyinstaller="$BUILD/bin/pyinstaller"
if [ -d "$BUILD/Scripts" ]; then
python="$BUILD/Scripts/python.exe"
pyinstaller="$BUILD/Scripts/pyinstaller.exe"
fi
uv pip install --quiet --python "$python" -r requirements-build.txt
"$pyinstaller" --clean --noconfirm \
--distpath dist --workpath "$BUILD/work" dice-counter.spec
echo
ls -lh dist/kostky
ls -lh dist/
+27
View File
@@ -0,0 +1,27 @@
"""Aby výstup prošel i tam, kde ho nikdo nečeká na konzoli."""
import sys
def utf8() -> None:
"""Přepne standardní výstup na UTF-8 a na řádkové bufferování.
Windows sahá po historické kódové stránce (cp1252, cp852) všude, kde
výstup není konzole — přesměrovaný do souboru, puštěný ze skriptu, ve
frontě CI. První háček nad takovým proudem shodí celý program, takže
binárka umřela dřív, než stihla vypsat adresu, na které poslouchá.
Na konzoli i na ostatních systémech je UTF-8 dávno výchozí a tohle je
tam prázdná operace. `errors="replace"` je pojistka pro zbylé případy:
rozsypaný háček je pořád lepší než spadlý server.
Řádkové bufferování řeší druhou půlku téhož problému. Mimo konzoli
Python drží výstup v bloku a pouští ho, až se naplní nebo až program
skončí — jenže server neskončí, ten běží. Kdo si ho pustí na pozadí
s výpisem do souboru, našel by tam prázdno; přesně to dělá zkouška
binárky v CI. Pár řádek hlášky za to bufferovat nemá cenu.
"""
for stream in (sys.stdout, sys.stderr):
if hasattr(stream, "reconfigure"):
stream.reconfigure(encoding="utf-8", errors="replace",
line_buffering=True)
+56
View File
@@ -0,0 +1,56 @@
# Počitadlo kostek jako kontejner.
#
# Staví se ze zdrojáků, ne z vydané binárky: image jde pak sestavit pro
# amd64 i arm64 a nezávisí na tom, jestli pro danou architekturu release
# proběhl. Závislosti jsou zamčené v requirements.txt, takže je sestavení
# opakovatelné.
#
# docker build -f deploy/Dockerfile -t gitea.spacilovi.eu/david-spacil/dice-counter:latest .
#
# Sestavuje se z kořene repozitáře, ne z deploy/.
FROM python:3.11-slim
# Verze se jinak nemá kde vzít — v image není git ani zabalená binárka.
ARG VERSION=neznámá
# Podle image.source si Gitea připne balíček k repozitáři. Bez toho labelu
# image sice existuje, ale visí jen pod uživatelem a záložka Packages
# u repozitáře zůstane prázdná.
LABEL org.opencontainers.image.source="https://gitea.spacilovi.eu/david-spacil/dice-counter" \
org.opencontainers.image.url="https://gitea.spacilovi.eu/david-spacil/dice-counter" \
org.opencontainers.image.title="dice-counter" \
org.opencontainers.image.description="Počitadlo kostek: telefon zapisuje skóre, notebook ukazuje zápisník" \
org.opencontainers.image.version="$VERSION"
RUN useradd --system --uid 1000 --home-dir /app --shell /usr/sbin/nologin dice-counter
WORKDIR /app
# Závislosti zvlášť a dřív než kód, ať se vrstva s pipem přestavuje jen tehdy,
# když se opravdu změnily.
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY console.py core.py net.py stats.py storage.py version.py web.py ./
COPY templates/ templates/
COPY static/ static/
RUN echo "$VERSION" > verze.txt
# Adresář pro databázi musí vzniknout už tady a patřit správnému uživateli —
# svazek, který Docker založí sám, by jinak patřil rootovi a appka běžící pod
# dice-counter by do něj nezapsala.
RUN install -d -o dice-counter -g dice-counter /data
VOLUME ["/data"]
ENV DICE_DB=/data/dice.db \
DICE_PORT=8000
EXPOSE 8000
USER dice-counter
# Bez curlu, ten ve slim image není. Stačí, že stránka odpoví — sahá i do
# databáze, takže se tím kontroluje i ta.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import os,urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ.get('DICE_PORT','8000') + '/')"
CMD ["python", "web.py"]
+46
View File
@@ -0,0 +1,46 @@
# Počitadlo kostek v Dockeru.
#
# curl -fsSLO https://gitea.spacilovi.eu/david-spacil/dice-counter/raw/branch/main/deploy/docker-compose.yaml
# docker compose up -d
#
# Nic dalšího není potřeba — image se stáhne hotový, pro amd64 i arm64.
# Kdo si ho chce postavit ze zdrojáků, přebije ten stažený stejnou značkou:
#
# docker build -f deploy/Dockerfile -t gitea.spacilovi.eu/david-spacil/dice-counter:latest .
#
# Počítá se s domácí sítí. Appka nemá přihlašování, tohle ji nijak
# nezabezpečuje a do internetu nepatří.
services:
dice-counter:
image: gitea.spacilovi.eu/david-spacil/dice-counter:latest
container_name: dice-counter
restart: unless-stopped
# Python jako PID 1 nesklízí osiřelé procesy a hůř reaguje na signály.
init: true
# Síť hostitele schválně. V bridge režimu kontejner uvnitř vidí adresu
# jako 172.17.0.2, tabule ji poctivě nabídne a vygeneruje na ni QR kód —
# jenže z telefonu je nedosažitelná. Se sítí hostitele appka najde tu
# adresu, na kterou se telefon opravdu dovolá.
#
# Kdo bridge potřebuje, ať zakomentuje řádek níž, odkomentuje ports
# a nastaví DICE_HOST na adresu hostitele v LAN:
#
# ports:
# - "8000:8000"
# environment:
# DICE_HOST: 192.168.1.10
network_mode: host
environment:
DICE_PORT: 8000
DICE_DB: /data/dice.db
volumes:
- dice-counter-data:/data
volumes:
# Zálohu si stáhneš rovnou z appky přes /export, takže se sem nemusí lézt.
dice-counter-data:
+391
View File
@@ -0,0 +1,391 @@
#!/usr/bin/env bash
# Založí na Proxmoxu LXC kontejner s počitadlem kostek.
#
# Pouští se v terminálu uzlu Proxmoxu jako root, jedním příkazem:
#
# bash -c "$(curl -fsSL https://gitea.spacilovi.eu/david-spacil/dice-counter/raw/branch/main/deploy/pve-dice-counter.sh)"
#
# Aktualizace stávajícího kontejneru na novější vydání:
#
# CTID=123 MODE=update bash -c "$(curl -fsSL .../pve-dice-counter.sh)"
#
# Ze staženého souboru jde i obojí postaru: `bash pve-dice-counter.sh` a
# `bash pve-dice-counter.sh update 123`.
#
# Podoba je odkoukaná od community-scripts.org, ale nic z jejich frameworku
# se tu nestahuje — jejich build.func si instalační skript hledá natvrdo ve
# vlastním repozitáři, takže mimo něj nefunguje.
#
# Počítá se s domácí sítí. Appka nemá přihlašování a tenhle skript ji nijak
# nezabezpečuje; do internetu ji nepouštěj.
set -euo pipefail
APP="dice-counter"
GITEA="${GITEA:-https://gitea.spacilovi.eu}"
REPO="${REPO:-david-spacil/dice-counter}"
# Výchozí hodnoty. Všechny se dají přebít proměnnou prostředí, třeba:
# MEMORY=1024 STORAGE=local-zfs bash -c "$(curl -fsSL ...)"
CT_HOSTNAME="${CT_HOSTNAME:-dice-counter}"
CORES="${CORES:-1}"
MEMORY="${MEMORY:-512}"
DISK="${DISK:-4}"
STORAGE="${STORAGE:-}"
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
BRIDGE="${BRIDGE:-vmbr0}"
PORT="${PORT:-8000}"
UNPRIVILEGED="${UNPRIVILEGED:-1}"
OSVERSION="${OSVERSION:-13}"
NESTING="${NESTING:-1}"
AUTOLOGIN="${AUTOLOGIN:-1}" # konzole ve webu Proxmoxu bez hesla
VERSION="${VERSION:-}" # prázdné = poslední vydání
# --- výpisy ------------------------------------------------------------------
# Bez tohohle Ctrl+C zabije jen rozdělaný podproces a skript jede vesele dál
# — třeba rovnou zakládat kontejner, na který se čekat nemá.
trap 'echo; echo -e "\e[1;31m ✗\e[0m Přerušeno." >&2; exit 130' INT TERM
msg() { echo -e "\e[1;34m →\e[0m $*"; }
ok() { echo -e "\e[1;32m ✓\e[0m $*"; }
die() { echo -e "\e[1;31m ✗\e[0m $*" >&2; exit 1; }
header() {
echo
echo -e "\e[1;36m $APP\e[0m — počitadlo kostek do LXC"
echo -e " \e[2m$GITEA/$REPO\e[0m"
echo
}
# --- instalátor, který poběží uvnitř kontejneru ------------------------------
#
# Je tu jako text schválně. Skript se pouští i rourou z curlu, kdy na disku
# hostitele žádný soubor není a nebylo by co poslat dovnitř. Uvozovky kolem
# 'INSTALL' jsou podstatné: nic z toho se tady nesmí rozvinout, všechny
# proměnné patří až tomu, co poběží v kontejneru.
#
# Vypsat se dá i samostatně: `bash pve-dice-counter.sh instalator > install.sh`,
# což se hodí, když chceš appku dostat do kontejneru, který sis založil sám.
installer() {
cat <<'INSTALL'
#!/usr/bin/env bash
# Instalace počitadla uvnitř kontejneru. O Proxmoxu nic neví, takže se dá
# pustit v jakémkoli Debianu. Je idempotentní — druhé spuštění jen vymění
# binárku za nejnovější.
set -euo pipefail
GITEA="${GITEA:-https://gitea.spacilovi.eu}"
REPO="${REPO:-david-spacil/dice-counter}"
PORT="${PORT:-8000}"
VERSION="${VERSION:-}"
USER_NAME=dice-counter
HOME_DIR=/opt/dice-counter
DATA_DIR=/var/lib/dice-counter
SERVICE=dice-counter
msg() { echo -e "\e[1;34m →\e[0m $*"; }
ok() { echo -e "\e[1;32m ✓\e[0m $*"; }
die() { echo -e "\e[1;31m ✗\e[0m $*" >&2; exit 1; }
case "$(dpkg --print-architecture)" in
amd64) ASSET=dice-counter-linux-x86_64 ;;
arm64) ASSET=dice-counter-linux-arm64 ;;
*) die "Pro architekturu $(dpkg --print-architecture) binárka není." ;;
esac
msg "Doinstalovávám, co je potřeba ke stažení"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl ca-certificates >/dev/null
ok "Základ je hotový"
if [ -z "$VERSION" ]; then
msg "Zjišťuji poslední vydanou verzi"
# Jen tag; adresa přílohy se z něj složí, takže tu nepotřebujeme jq.
VERSION=$(curl -fsSL "$GITEA/api/v1/repos/$REPO/releases/latest" \
| grep -o '"tag_name":[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4)
[ -n "$VERSION" ] || die "Nepodařilo se zjistit poslední verzi z $GITEA."
fi
ok "Instaluje se $VERSION ($ASSET)"
BASE="$GITEA/$REPO/releases/download/$VERSION"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
msg "Stahuji binárku"
curl -fsSL -o "$TMP/$ASSET" "$BASE/$ASSET" \
|| die "$ASSET pro $VERSION na releasu není. Vydání pro tuhle architekturu možná ještě neproběhlo."
curl -fsSL -o "$TMP/$ASSET.sha256" "$BASE/$ASSET.sha256" \
|| die "Chybí kontrolní součet — radši nic neinstaluju."
msg "Ověřuji kontrolní součet"
( cd "$TMP" && sha256sum -c "$ASSET.sha256" >/dev/null ) \
|| die "Kontrolní součet nesedí. Stažený soubor zahazuju."
ok "Součet sedí"
id -u "$USER_NAME" >/dev/null 2>&1 \
|| useradd --system --home-dir "$HOME_DIR" --shell /usr/sbin/nologin "$USER_NAME"
install -d -o root -g root -m 755 "$HOME_DIR"
# Zastavit se dá jen to, co běží — při první instalaci služba ještě není.
if systemctl is-active --quiet "$SERVICE"; then
systemctl stop "$SERVICE"
fi
install -o root -g root -m 755 "$TMP/$ASSET" "$HOME_DIR/dice-counter"
echo "$VERSION" > "$HOME_DIR/verze"
ok "Binárka je na místě"
msg "Zapisuji službu"
cat > "/etc/systemd/system/$SERVICE.service" <<EOF
[Unit]
Description=Počitadlo kostek
Documentation=$GITEA/$REPO
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$USER_NAME
Environment=DICE_DB=$DATA_DIR/dice.db
Environment=DICE_PORT=$PORT
ExecStart=$HOME_DIR/dice-counter
Restart=on-failure
RestartSec=5
# Databáze je jediné, co appka potřebuje mít zapisovatelné. StateDirectory ji
# založí a předá správnému uživateli, takže se tu nemusí nic chownovat.
StateDirectory=dice-counter
ReadWritePaths=$DATA_DIR
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
# Binárka se při každém startu rozbaluje do /tmp; tady do vlastního, ne do
# sdíleného.
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --quiet --now "$SERVICE"
ok "Služba běží"
# --- konzole v rozhraní Proxmoxu --------------------------------------------
if [ "${AUTOLOGIN:-1}" = "1" ]; then
msg "Nastavuji konzoli"
# Kontejner nemá root heslo, takže by se do konzole ve webu Proxmoxu
# nedalo dostat. Autologin to řeší stejně, jako to dělají community-scripts.
# Nová práva to nikomu nedává — kdo má web Proxmoxu, má root na uzlu tak
# jako tak.
mkdir -p /etc/systemd/system/container-getty@1.service.d
cat > /etc/systemd/system/container-getty@1.service.d/override.conf <<'GETTY'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 $TERM
GETTY
systemctl daemon-reload
systemctl restart container-getty@1.service 2>/dev/null || true
ok "Konzole se přihlásí sama"
fi
# --- příkaz update ----------------------------------------------------------
# Instalátor si necháme, ať je z čeho aktualizovat. Když se skript pouští
# rourou, soubor neexistuje a zůstane ten z minula.
#
# `-ef` je tu nutnost, ne opatrnost: příkaz `update` si nový instalátor uloží
# rovnou sem a odsud ho pustí, takže `$0` a cíl jsou tentýž soubor. `install`
# na to řekne "are the same file", skončí chybou a se `set -e` by celý update
# spadl dřív, než by stihl cokoli udělat.
if [ -f "$0" ] && ! [ "$0" -ef "$HOME_DIR/install.sh" ]; then
install -m 755 "$0" "$HOME_DIR/install.sh"
fi
cat > /usr/bin/update <<UPDATE
#!/usr/bin/env bash
# Aktualizace počitadla na poslední vydání. Stačí napsat: update
set -euo pipefail
# Nejdřív zkusíme obnovit i samotný instalátor, kdyby se mezitím zlepšil.
# Bez sítě nebo při změněné adrese se použije ten uložený.
NOVY=\$(mktemp)
if curl -fsSL "$GITEA/$REPO/raw/branch/main/deploy/pve-dice-counter.sh" -o "\$NOVY" 2>/dev/null \\
&& bash "\$NOVY" instalator > "\$NOVY.in" 2>/dev/null \\
&& bash -n "\$NOVY.in" 2>/dev/null; then
install -m 755 "\$NOVY.in" "$HOME_DIR/install.sh"
fi
rm -f "\$NOVY" "\$NOVY.in"
exec env PORT="$PORT" GITEA="$GITEA" REPO="$REPO" bash "$HOME_DIR/install.sh"
UPDATE
chmod 755 /usr/bin/update
ok "Aktualizovat půjde příkazem: update"
msg "Čekám, až začne odpovídat"
for _ in $(seq 30); do
curl -fsS -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null && break
sleep 1
done
curl -fsS -o /dev/null "http://127.0.0.1:$PORT/" \
|| die "Služba nastartovala, ale neodpovídá. Mrkni na journalctl -u $SERVICE."
# Verzi bereme ze souboru, ne z binárky. Starší vydání --version neumí
# a místo výpisu by nastartovala server, který by tenhle skript zavěsil.
ok "Počitadlo $(cat "$HOME_DIR/verze") odpovídá na portu $PORT"
INSTALL
}
# --- společné části ----------------------------------------------------------
install_into() {
local ctid="$1" tmp
tmp=$(mktemp)
installer > "$tmp"
msg "Posílám instalátor do kontejneru $ctid"
pct push "$ctid" "$tmp" /root/dice-counter-install.sh --perms 0755
rm -f "$tmp"
pct exec "$ctid" -- env PORT="$PORT" GITEA="$GITEA" REPO="$REPO" \
VERSION="$VERSION" AUTOLOGIN="$AUTOLOGIN" bash /root/dice-counter-install.sh
}
address_of() {
pct exec "$1" -- hostname -I 2>/dev/null | awk '{print $1}'
}
hotovo() {
local ctid="$1" ip jmeno
ip=$(address_of "$ctid")
jmeno=$(pct config "$ctid" | awk -F': ' '/^hostname:/ {print $2}')
echo
ok "Hotovo. Kontejner $ctid ($jmeno) běží."
echo
echo -e " Zápisník na notebook: \e[1;32mhttp://${ip}:${PORT}/board\e[0m"
echo -e " Zadávání z telefonu: \e[1;32mhttp://${ip}:${PORT}/\e[0m"
echo
echo -e " \e[2mDatabáze: /var/lib/dice-counter/dice.db (uvnitř kontejneru)"
echo -e " Aktualizace: v konzoli kontejneru napiš update"
echo -e " Log: pct exec $ctid -- journalctl -u dice-counter -f"
echo -e " Konzole: pct enter $ctid"
echo -e " Zrušit: pct stop $ctid && pct destroy $ctid\e[0m"
echo
}
# --- co se má vlastně dělat --------------------------------------------------
#
# Rourou z curlu se argumenty předávají mizerně, tak jde všechno i proměnnou:
# MODE=update CTID=123. Ze souboru funguje i `bash pve-dice-counter.sh update 123`.
MODE="${MODE:-${1:-create}}"
[ "$MODE" = "update" ] && CTID="${CTID:-${2:-}}"
if [ "$MODE" = "instalator" ]; then
installer
exit 0
fi
command -v pct >/dev/null || die "Tohle patří na uzel Proxmoxu — pct tu není."
[ "$(id -u)" -eq 0 ] || die "Spusť to jako root."
# --- aktualizace -------------------------------------------------------------
if [ "$MODE" = "update" ]; then
[ -n "${CTID:-}" ] || die "Řekni který: CTID=123 MODE=update ..."
pct status "$CTID" >/dev/null 2>&1 || die "Kontejner $CTID neexistuje."
header
if [ "$(pct status "$CTID")" != "status: running" ]; then
msg "Startuji kontejner"
pct start "$CTID"
sleep 5
fi
install_into "$CTID"
hotovo "$CTID"
exit 0
fi
# --- nový kontejner ----------------------------------------------------------
header
# Úložiště se nehádá. local-lvm na ZFS instalacích neexistuje a pct create by
# spadlo až po půlce práce, tak se radši zeptáme systému, co tu opravdu je.
if [ -z "$STORAGE" ]; then
STORAGE=$(pvesm status -content rootdir 2>/dev/null \
| awk 'NR>1 && $3=="active" {print $1; exit}')
[ -n "$STORAGE" ] || die "Nenašel jsem úložiště pro kontejnery. Zadej STORAGE=<jméno>; co máš, ukáže pvesm status."
msg "Úložiště: $STORAGE (nalezeno automaticky)"
else
pvesm status -content rootdir 2>/dev/null | awk 'NR>1 {print $1}' | grep -qx "$STORAGE" \
|| die "Úložiště $STORAGE pro kontejnery nesedí. Co máš, ukáže: pvesm status -content rootdir"
msg "Úložiště: $STORAGE"
fi
CTID="${CTID:-$(pvesh get /cluster/nextid)}"
msg "Kontejner dostane číslo $CTID"
# Architektura uzlu, ne první šablona v seznamu. pveam nabízí amd64 i arm64
# vedle sebe a bez tohohle filtru by výběr padl na tu abecedně poslední.
ARCH=$(dpkg --print-architecture)
SABLONA="debian-${OSVERSION}-standard[^[:space:]]*_${ARCH}\.tar\.[a-z]*"
msg "Hledám šablonu Debianu $OSVERSION pro $ARCH"
# Nejdřív co už na uzlu leží — stahovat 120 MB znovu je zbytečné.
TEMPLATE=$(pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -o "$SABLONA" | sort -V | tail -1)
if [ -n "$TEMPLATE" ]; then
ok "Šablona $TEMPLATE (už je na uzlu)"
else
pveam update >/dev/null 2>&1 || true
TEMPLATE=$(pveam available --section system 2>/dev/null | grep -o "$SABLONA" | sort -V | tail -1)
[ -n "$TEMPLATE" ] || die "Šablona pro Debian $OSVERSION a $ARCH není k dispozici."
msg "Stahuji $TEMPLATE (asi 120 MB, chvíli to trvá)"
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null \
|| die "Stažení šablony selhalo."
ok "Šablona $TEMPLATE"
fi
# Nesting: Debian 13 veze systemd 257 a ten v neprivilegovaném kontejneru bez
# něj nedostane, co potřebuje — Proxmox na to při startu sám upozorňuje.
# Stejnou výchozí hodnotu mají i community-scripts.
msg "Zakládám kontejner"
pct create "$CTID" "$TEMPLATE_STORAGE:vztmpl/$TEMPLATE" \
--hostname "$CT_HOSTNAME" \
--cores "$CORES" \
--memory "$MEMORY" \
--swap 256 \
--rootfs "$STORAGE:$DISK" \
--net0 "name=eth0,bridge=$BRIDGE,ip=dhcp" \
--unprivileged "$UNPRIVILEGED" \
--features "nesting=$NESTING" \
--ostype debian \
--onboot 1 \
--description "$APP — $GITEA/$REPO" >/dev/null
ok "Kontejner $CTID založen"
msg "Startuji"
pct start "$CTID"
msg "Čekám na síť"
for _ in $(seq 60); do
if [ -n "$(address_of "$CTID")" ] \
&& pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then
break
fi
sleep 2
done
[ -n "$(address_of "$CTID")" ] || die "Kontejner nedostal adresu. Sedí bridge $BRIDGE?"
ok "Adresa $(address_of "$CTID")"
install_into "$CTID"
hotovo "$CTID"
+38
View File
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
"""Jedna binárka se vším: Python, Flask, šablony i styly.
Staví se přes `./build.sh`, ne ručně — ta se stará o to, aby výsledek jel
i na starších systémech, než je ten tvůj.
"""
import os
import sys
# Verzi zapisuje build.sh těsně před stavbou. Když se pyinstaller pustí
# ručně bez ní, binárka se postaví taky — jen se pak hlásí jako neznámá.
datas = [("templates", "templates"), ("static", "static")]
if os.path.exists("verze.txt"):
datas.append(("verze.txt", "."))
analysis = Analysis(
["web.py"],
pathex=["."],
datas=datas,
noarchive=False,
)
pyz = PYZ(analysis.pure)
exe = EXE(
pyz,
analysis.scripts,
analysis.binaries,
analysis.datas,
name="dice-counter",
console=True,
# Ořezání symbolů šetří pár megabajtů, ale jen na Linuxu. Na macOS by
# rozbilo podpis, který si PyInstaller sám přidává a bez kterého se
# binárka na Apple Silicon vůbec nespustí; na Windows nedělá nic.
strip=sys.platform == "linux",
upx=False,
)
+10 -1
View File
@@ -1,6 +1,6 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["tabulate"]
# dependencies = ["tabulate==0.10.0"]
# ///
"""Počitadlo skóre v terminálu.
@@ -8,10 +8,13 @@ Pravidla žijí v `core.Game`, tenhle modul je jen vstup a výstup. Hraje se
v paměti, nic se neukládá — na historii je webová aplikace (`web.py`).
"""
import sys
from os import name, system
from tabulate import tabulate
import console
import version
from core import FINAL_SCORE, Game
table_header = ["Hráč", "Skóre"]
@@ -128,4 +131,10 @@ def main() -> None:
if __name__ == "__main__":
console.utf8()
if {"--version", "-V"} & set(sys.argv[1:]):
print(f"dice-counter {version.current()}")
sys.exit(0)
main()
-26
View File
@@ -1,26 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
"""Jedna binárka se vším: Python, Flask, šablony i styly.
Staví se přes `./build.sh`, ne ručně — ta se stará o to, aby výsledek jel
i na starších systémech, než je ten tvůj.
"""
analysis = Analysis(
["web.py"],
pathex=["."],
datas=[("templates", "templates"), ("static", "static")],
noarchive=False,
)
pyz = PYZ(analysis.pure)
exe = EXE(
pyz,
analysis.scripts,
analysis.binaries,
analysis.datas,
name="kostky",
console=True,
strip=True,
upx=False,
)
+41 -2
View File
@@ -11,7 +11,9 @@ adresy a obecný typ rozhraní, ne to, co má kdo zrovna nainstalované.
import ipaddress
import os
import re
import socket
import subprocess
from dataclasses import dataclass
# Rozsah, ze kterého adresy přiděluje Tailscale (RFC 6598, CGNAT).
@@ -123,8 +125,45 @@ def _from_interfaces() -> list[tuple[str, str]]:
return found
def parse_ifconfig(text: str) -> list[tuple[str, str]]:
"""Rozhraní a jejich IPv4 adresy z výstupu `ifconfig`.
Oddělená čistá funkce, ať se dá otestovat i bez macOS pod rukama.
"""
found = []
name = ""
for line in text.splitlines():
head = re.match(r"(\S+):", line)
if head:
name = head.group(1)
continue
addr = re.search(r"\binet (\d+\.\d+\.\d+\.\d+)", line)
if addr:
found.append((name, addr.group(1)))
return found
def _from_ifconfig() -> list[tuple[str, str]]:
"""Adresy přes `ifconfig`. Záloha pro macOS, kde ioctl výše nesedí.
Spouští se jen tam, kde předchozí cesta nic nenašla, takže na Linuxu
se neplatí nic. Trvá jednotky milisekund, cachovat to nemá cenu.
"""
try:
done = subprocess.run(["ifconfig", "-a"], capture_output=True,
text=True, timeout=5, check=True)
except (OSError, subprocess.SubprocessError):
return []
return parse_ifconfig(done.stdout)
def _from_hostname() -> list[tuple[str, str]]:
"""Záloha pro systémy, kde předchozí cesta nefunguje."""
"""Poslední záloha. Na Windows vrátí adresy všech rozhraní, jinde
obvykle jen jednu — jména rozhraní se takhle nedozvíme."""
try:
infos = socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET)
except OSError:
@@ -144,7 +183,7 @@ def addresses() -> list[Address]:
primary = default_route_address()
found = _from_interfaces() or _from_hostname()
found = _from_interfaces() or _from_ifconfig() or _from_hostname()
if primary and primary not in [ip for _name, ip in found]:
found.append(("", primary))
+4
View File
@@ -0,0 +1,4 @@
# Co je potřeba ke stavbě binárky. Zamyká se do requirements-build.txt:
# uv pip compile requirements-build.in -o requirements-build.txt
-r requirements.in
pyinstaller
+37
View File
@@ -0,0 +1,37 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements-build.in -o requirements-build.txt --python-version 3.11
altgraph==0.17.5
# via pyinstaller
blinker==1.9.0
# via flask
click==8.4.2
# via flask
flask==3.1.3
# via -r requirements.in
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via flask
markupsafe==3.0.3
# via
# flask
# jinja2
# werkzeug
packaging==26.3
# via
# pyinstaller
# pyinstaller-hooks-contrib
pyinstaller==6.22.2
# via -r requirements-build.in
pyinstaller-hooks-contrib==2026.6
# via pyinstaller
qrcode==8.2
# via -r requirements.in
setuptools==84.0.0
# via pyinstaller
tabulate==0.10.0
# via -r requirements.in
waitress==3.0.2
# via -r requirements.in
werkzeug==3.1.8
# via flask
+4
View File
@@ -0,0 +1,4 @@
# Co je potřeba k vývoji a testům. Zamyká se do requirements-dev.txt:
# uv pip compile requirements-dev.in -o requirements-dev.txt
-r requirements.in
pytest
+35
View File
@@ -0,0 +1,35 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements-dev.in -o requirements-dev.txt --python-version 3.11
blinker==1.9.0
# via flask
click==8.4.2
# via flask
flask==3.1.3
# via -r requirements.in
iniconfig==2.3.0
# via pytest
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via flask
markupsafe==3.0.3
# via
# flask
# jinja2
# werkzeug
packaging==26.3
# via pytest
pluggy==1.6.0
# via pytest
pygments==2.21.0
# via pytest
pytest==9.1.1
# via -r requirements-dev.in
qrcode==8.2
# via -r requirements.in
tabulate==0.10.0
# via -r requirements.in
waitress==3.0.2
# via -r requirements.in
werkzeug==3.1.8
# via flask
+6
View File
@@ -0,0 +1,6 @@
# Běhové závislosti. Verze se zamykají do requirements.txt přes:
# uv pip compile requirements.in -o requirements.txt
flask
qrcode
tabulate
waitress
+25 -4
View File
@@ -1,4 +1,25 @@
flask
qrcode
tabulate
pytest
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in -o requirements.txt --python-version 3.11
blinker==1.9.0
# via flask
click==8.4.2
# via flask
flask==3.1.3
# via -r requirements.in
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via flask
markupsafe==3.0.3
# via
# flask
# jinja2
# werkzeug
qrcode==8.2
# via -r requirements.in
tabulate==0.10.0
# via -r requirements.in
waitress==3.0.2
# via -r requirements.in
werkzeug==3.1.8
# via flask
+8
View File
@@ -513,3 +513,11 @@ button {
font-size: 0.85em;
color: var(--ink);
}
/* Verze v patičce. Má být k nalezení, když ji někdo hledá, a jinak neplést. */
.verze {
margin: 2rem 0 0.75rem;
text-align: center;
font-size: 0.75rem;
opacity: 0.35;
}
+53 -4
View File
@@ -16,20 +16,30 @@ from pathlib import Path
from core import FINAL_SCORE, Game, Turn
def data_home() -> Path:
"""Adresář, kam podle zvyklostí systému patří data aplikací."""
if sys.platform == "win32":
local = os.environ.get("LOCALAPPDATA")
return Path(local) if local else Path.home() / "AppData" / "Local"
if sys.platform == "darwin":
return Path.home() / "Library" / "Application Support"
return Path(os.environ.get("XDG_DATA_HOME")
or Path.home() / ".local" / "share")
def default_db() -> Path:
"""Kam se ukládá databáze.
Ze zdrojáků do pracovního adresáře — dá se tak mít víc rozehraných
sad vedle sebe a je vidět, s čím se pracuje. Z binárky do datového
adresáře podle XDG: binárka se spouští odkudkoli a rozbaluje se do
adresáře podle systému: binárka se spouští odkudkoli a rozbaluje se do
dočasného adresáře, takže relativní cesta by databázi rozsypala po disku.
"""
chosen = os.environ.get("DICE_DB")
if chosen:
return Path(chosen)
if getattr(sys, "frozen", False):
share = os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share"
return Path(share) / "kostky" / "dice.db"
return data_home() / "dice-counter" / "dice.db"
return Path("dice.db")
@@ -76,6 +86,45 @@ CREATE TABLE IF NOT EXISTS turns (
CREATE INDEX IF NOT EXISTS turns_game ON turns(game_id, id);
"""
# Každý krok posouvá schéma o jedno dál; krok na indexu 0 vede z verze 1 na 2.
# Přidávat jen na konec a nikdy nesahat na ten, který už někomu proběhl —
# databáze leží lidem na discích a zpátky se nedají stáhnout.
MIGRATIONS: list[str] = []
SCHEMA_VERSION = 1 + len(MIGRATIONS)
def migrate(conn: sqlite3.Connection) -> int:
"""Dorovná databázi na aktuální schéma a vrátí její verzi.
Verze se drží v `PRAGMA user_version`, což je celé číslo, které si SQLite
veze v hlavičce souboru a jinak si ho nevšímá.
Nula znamená dvojí: buď je soubor prázdný, nebo pochází z doby, kdy se
verze ještě nezapisovala. Obojí se vyřeší stejně — `SCHEMA` je samé
`IF NOT EXISTS`, takže na hotové databázi neudělá nic a jen se orazítkuje.
"""
version = conn.execute("PRAGMA user_version").fetchone()[0]
if version > SCHEMA_VERSION:
raise RuntimeError(
f"Databáze je ve verzi {version}, tahle aplikace umí "
f"{SCHEMA_VERSION}. Nejspíš ji založila novější verze počitadla; "
f"aktualizuj, ať o data nepřijdeš.")
if version == 0:
conn.executescript(SCHEMA)
version = 1
for step in MIGRATIONS[version - 1:]:
conn.executescript(step)
version += 1
# PRAGMA neumí zástupné otazníky. Číslo sem teče z kódu, ne od uživatele.
conn.execute(f"PRAGMA user_version = {version}")
conn.commit()
return version
def connect(path: Path | str | None = None) -> sqlite3.Connection:
target = Path(path or DB_PATH)
@@ -83,7 +132,7 @@ def connect(path: Path | str | None = None) -> sqlite3.Connection:
conn = sqlite3.connect(target)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
conn.executescript(SCHEMA)
migrate(conn)
return conn
+1
View File
@@ -8,5 +8,6 @@
</head>
<body>
{% block body %}{% endblock %}
<footer class="verze">dice-counter {{ verze }}</footer>
</body>
</html>
+2 -1
View File
@@ -8,7 +8,8 @@
<p class="eyebrow">Počitadlo</p>
<h1>Síň slávy</h1>
</div>
<p class="meta"><a href="{{ url_for('index') }}">Nová hra</a></p>
<p class="meta"><a href="{{ url_for('index') }}">Nová hra</a>
· <a href="{{ url_for('export') }}">Stáhnout databázi</a></p>
</header>
{% if records.best_turn %}
+65
View File
@@ -0,0 +1,65 @@
import io
import console
class Proud:
"""Náhrada za výstup, která si pamatuje, na co ji přepnuli."""
def __init__(self):
self.encoding = "cp1252"
self.errors = "strict"
self.line_buffering = False
def reconfigure(self, encoding, errors, line_buffering=False):
self.encoding = encoding
self.errors = errors
self.line_buffering = line_buffering
def test_vystup_se_prepne_na_utf8(monkeypatch):
out, err = Proud(), Proud()
monkeypatch.setattr(console.sys, "stdout", out)
monkeypatch.setattr(console.sys, "stderr", err)
console.utf8()
assert (out.encoding, out.errors) == ("utf-8", "replace")
assert (err.encoding, err.errors) == ("utf-8", "replace")
assert out.line_buffering and err.line_buffering
def test_cestina_projde_i_pres_cp1252(monkeypatch):
"""Přesně to, na čem umřela první windowsová binárka."""
surovy = io.BytesIO()
proud = io.TextIOWrapper(surovy, encoding="cp1252")
monkeypatch.setattr(console.sys, "stdout", proud)
monkeypatch.setattr(console.sys, "stderr", proud)
console.utf8()
print("Počitadlo je dostupné na:", file=console.sys.stdout)
console.sys.stdout.flush()
assert "Počitadlo je dostupné" in surovy.getvalue().decode("utf-8")
def test_proud_bez_reconfigure_se_prezije(monkeypatch):
"""Zabalený výstup reconfigure mít nemusí; padat kvůli tomu nesmíme."""
monkeypatch.setattr(console.sys, "stdout", object())
monkeypatch.setattr(console.sys, "stderr", object())
console.utf8()
def test_hlaska_je_v_souboru_hned(tmp_path, monkeypatch):
"""Server běží dál, takže na vyprázdnění bufferu při konci se čekat nedá."""
cesta = tmp_path / "server.log"
with cesta.open("w", encoding="utf-8") as soubor:
monkeypatch.setattr(console.sys, "stdout", soubor)
monkeypatch.setattr(console.sys, "stderr", soubor)
console.utf8()
print("Počitadlo je dostupné na:", file=console.sys.stdout)
# Bez zavření souboru — přesně jako u běžícího serveru.
assert "Počitadlo je dostupné" in cesta.read_text(encoding="utf-8")
+47
View File
@@ -90,3 +90,50 @@ def test_v_systemu_se_neco_najde():
assert all(net.classify(a.ip, a.interface) for a in found)
assert len({a.ip for a in found}) == len(found)
IFCONFIG_MACOS = """\
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
\toptions=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
\tinet 127.0.0.1 netmask 0xff000000
\tinet6 ::1 prefixlen 128
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
\tether 3c:22:fb:aa:bb:cc
\tinet6 fe80::14b0:6b1a:9e2f:1a4c%en0 prefixlen 64 secured scopeid 0xb
\tinet 192.168.0.42 netmask 0xffffff00 broadcast 192.168.0.255
\tmedia: autoselect
\tstatus: active
utun4: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1280
\tinet 100.99.98.97 --> 100.99.98.97 netmask 0xff000000
"""
def test_ifconfig_vytahne_rozhrani_a_adresy():
"""macOS na ioctl z Linuxu neslyší, tak se čte výstup ifconfigu."""
assert net.parse_ifconfig(IFCONFIG_MACOS) == [
("lo0", "127.0.0.1"),
("en0", "192.168.0.42"),
("utun4", "100.99.98.97"),
]
def test_ifconfig_si_neplete_inet6_s_inet():
"""`inet6` začíná stejně jako `inet`; IPv6 adresy tudy projít nesmí."""
assert net.parse_ifconfig("en0: flags=1\n\tinet6 fe80::1 prefixlen 64\n") == []
def test_adresy_z_ifconfigu_projdou_celym_retezem(monkeypatch):
"""Bez linuxového ioctl se sáhne po ifconfigu, teprve pak po hostname."""
monkeypatch.delenv("DICE_HOST", raising=False)
monkeypatch.setattr(net, "_from_interfaces", lambda: [])
monkeypatch.setattr(net, "_from_ifconfig",
lambda: net.parse_ifconfig(IFCONFIG_MACOS))
monkeypatch.setattr(net, "_from_hostname", lambda: pytest.fail("moc brzy"))
monkeypatch.setattr(net, "default_route_address", lambda: "192.168.0.42")
found = net.addresses()
assert [a.ip for a in found] == ["192.168.0.42", "100.99.98.97"]
assert found[0].primary and found[0].interface == "en0"
assert found[1].kind == "tailnet"
+85 -1
View File
@@ -1,3 +1,4 @@
import sqlite3
from pathlib import Path
import pytest
@@ -148,9 +149,26 @@ def test_umisteni_databaze_z_binarky(monkeypatch, tmp_path):
"""Binárka se rozbaluje do dočasného adresáře, databáze tam nesmí."""
monkeypatch.delenv("DICE_DB", raising=False)
monkeypatch.setattr(storage.sys, "frozen", True, raising=False)
monkeypatch.setattr(storage.sys, "platform", "linux")
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
assert storage.default_db() == tmp_path / "kostky" / "dice.db"
assert storage.default_db() == tmp_path / "dice-counter" / "dice.db"
def test_data_home_podle_systemu(monkeypatch, tmp_path):
"""Každý systém má svůj adresář na data; XDG je jen ten linuxový."""
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "AppData" / "Local"))
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path))
monkeypatch.setattr(storage.sys, "platform", "win32")
assert storage.data_home() == tmp_path / "AppData" / "Local"
monkeypatch.setattr(storage.sys, "platform", "darwin")
assert storage.data_home() == tmp_path / "Library" / "Application Support"
monkeypatch.setattr(storage.sys, "platform", "linux")
monkeypatch.delenv("XDG_DATA_HOME", raising=False)
assert storage.data_home() == tmp_path / ".local" / "share"
def test_dice_db_prebije_oboje(monkeypatch, tmp_path):
@@ -165,3 +183,69 @@ def test_connect_zalozi_chybejici_adresar(tmp_path):
conn.close()
assert (tmp_path / "novy" / "adresar" / "dice.db").exists()
# --- verze schématu ---------------------------------------------------------
def verze(conn) -> int:
return conn.execute("PRAGMA user_version").fetchone()[0]
def test_nova_databaze_je_orazitkovana(tmp_path):
conn = storage.connect(tmp_path / "nova.db")
try:
assert verze(conn) == storage.SCHEMA_VERSION
finally:
conn.close()
def test_databaze_z_doby_pred_verzovanim_prezije(tmp_path):
"""Přesně to, co lidem leží na disku z verzí 1.0 a 1.1.
Tabulky má, razítko ne. Nesmí se ani přijít o data, ani spadnout.
"""
path = tmp_path / "stara.db"
stara = sqlite3.connect(path)
stara.executescript(storage.SCHEMA) # bez PRAGMA user_version
stara.execute("INSERT INTO players (name, key, created_at) "
"VALUES ('Adam', 'adam', '2026-01-01T00:00:00')")
stara.commit()
stara.close()
conn = storage.connect(path)
try:
assert verze(conn) == storage.SCHEMA_VERSION
assert [p["name"] for p in storage.known_players(conn)] == ["Adam"]
finally:
conn.close()
def test_migrace_dojede_jen_ty_chybejici(tmp_path, monkeypatch):
"""Podruhé už krok proběhnout nesmí — jinak by ALTER TABLE spadl."""
monkeypatch.setattr(storage, "MIGRATIONS",
["ALTER TABLE players ADD COLUMN barva TEXT;"])
monkeypatch.setattr(storage, "SCHEMA_VERSION", 2)
path = tmp_path / "migrovana.db"
conn = storage.connect(path)
conn.close()
conn = storage.connect(path) # druhé spuštění téže verze
try:
assert verze(conn) == 2
sloupce = [r[1] for r in conn.execute("PRAGMA table_info(players)")]
assert "barva" in sloupce
finally:
conn.close()
def test_databaze_z_novejsi_verze_se_odmitne(tmp_path):
"""Radši srozumitelná hláška než tiše rozbitá data."""
path = tmp_path / "budouci.db"
conn = storage.connect(path)
conn.execute(f"PRAGMA user_version = {storage.SCHEMA_VERSION + 1}")
conn.commit()
conn.close()
with pytest.raises(RuntimeError, match="novější verze"):
storage.connect(path)
+48
View File
@@ -0,0 +1,48 @@
"""Verze se musí poznat i z binárky, kde žádný git není."""
import pytest
import version
@pytest.fixture(autouse=True)
def bez_pameti():
"""`current()` si výsledek pamatuje; mezi testy je potřeba čistý stůl."""
version.current.cache_clear()
yield
version.current.cache_clear()
def test_z_binarky_se_cte_pribaleny_soubor(monkeypatch, tmp_path):
"""V binárce leží verze.txt v dočasném adresáři vedle šablon."""
(tmp_path / version.NAME).write_text("v9.9.9\n", encoding="utf-8")
monkeypatch.setattr(version.sys, "frozen", True, raising=False)
monkeypatch.setattr(version.sys, "_MEIPASS", str(tmp_path), raising=False)
assert version.current() == "v9.9.9"
def test_binarka_se_gitu_nepta(monkeypatch, tmp_path):
"""Na cizím počítači žádný repozitář není a ptát se nemá koho."""
monkeypatch.setattr(version.sys, "frozen", True, raising=False)
monkeypatch.setattr(version.sys, "_MEIPASS", str(tmp_path), raising=False)
monkeypatch.setattr(version, "described",
lambda: pytest.fail("binárka se ptala gitu"))
assert version.current() == version.UNKNOWN
def test_ze_zdrojaku_ma_prednost_git(monkeypatch):
monkeypatch.delattr(version.sys, "frozen", raising=False)
monkeypatch.setattr(version, "described", lambda: "v1.2.3-4-gabcdef")
monkeypatch.setattr(version, "bundled", lambda: "v1.0.0")
assert version.current() == "v1.2.3-4-gabcdef"
def test_bez_gitu_a_bez_souboru_zbyde_neznama(monkeypatch):
monkeypatch.delattr(version.sys, "frozen", raising=False)
monkeypatch.setattr(version, "described", lambda: "")
monkeypatch.setattr(version, "bundled", lambda: "")
assert version.current() == version.UNKNOWN
+33
View File
@@ -326,3 +326,36 @@ def test_qr_obdelniky_kryji_stejne_moduly():
assert painted == expected
assert svg.count("<rect") < len(expected)
# --- záloha -----------------------------------------------------------------
def test_export_vrati_pouzitelnou_databazi(client, tmp_path):
"""Ze staženého souboru musí jít data přečíst zpátky, ne jen že se stáhne."""
game_id = start_game(client, "Adam", "Eva")
score(client, game_id, 150)
response = client.get("/export")
assert response.status_code == 200
assert "attachment" in response.headers["Content-Disposition"]
assert response.headers["Content-Disposition"].endswith(".db")
stazeny = tmp_path / "stazeny.db"
stazeny.write_bytes(response.data)
conn = storage.connect(stazeny)
try:
assert [p["name"] for p in storage.seating(conn, game_id)] == ["Adam", "Eva"]
assert storage.load_game(conn, game_id).totals()["Adam"] == 150
finally:
conn.close()
def test_export_zabere_i_rozehranou_hru(client):
"""Kopie se dělá za běhu, uprostřed nedohrané hry."""
game_id = start_game(client, "Adam")
score(client, game_id, 100)
assert client.get("/export").status_code == 200
assert text(client.get("/stats")).count("Stáhnout databázi") == 1
+60
View File
@@ -0,0 +1,60 @@
"""Závislosti jsou zamčené a všechna místa se shodnou.
Verze se píšou na dvou místech: v `requirements.txt` pro stavbu binárky
a v hlavičce PEP 723 pro `uv run web.py`. Ta dvě místa se dřív nebo později
rozejdou — leda že by to někdo hlídal. Proto tenhle soubor.
"""
import re
from pathlib import Path
import pytest
ROOT = Path(__file__).parent.parent
# Skript a to, co si podle hlavičky nese s sebou.
SCRIPTS = {"web.py": {"flask", "qrcode", "waitress"},
"dice.py": {"tabulate"}}
def pins(name: str) -> dict[str, str]:
"""Zamčené verze z compilovaného requirements souboru."""
text = (ROOT / name).read_text(encoding="utf-8")
return {m.group(1).lower(): m.group(2)
for m in re.finditer(r"^([A-Za-z0-9_.\-]+)==(\S+)", text, re.M)}
def header(script: str) -> dict[str, str]:
"""Závislosti z hlavičky PEP 723 na začátku skriptu."""
text = (ROOT / script).read_text(encoding="utf-8")
line = re.search(r"^# dependencies = \[(.*)\]$", text, re.M)
assert line, f"{script} nemá hlavičku PEP 723"
return {m.group(1).lower(): m.group(2)
for m in re.finditer(r'"([A-Za-z0-9_.\-]+)==(\S+?)"', line.group(1))}
@pytest.mark.parametrize("name", ["requirements.txt", "requirements-dev.txt",
"requirements-build.txt"])
def test_zavislosti_jsou_zamcene(name):
"""Bez zamčených verzí se binárka z března liší od binárky z dubna."""
lines = [line.strip() for line in (ROOT / name).read_text().splitlines()]
volne = [line for line in lines
if line and not line.startswith("#") and "==" not in line]
assert not volne, f"{name} má nezamčené závislosti: {volne}"
@pytest.mark.parametrize("script", sorted(SCRIPTS))
def test_hlavicka_sedi_s_requirements(script):
"""`uv run web.py` musí dostat totéž, co se zabalí do binárky."""
zamcene = pins("requirements.txt")
v_hlavicce = header(script)
assert set(v_hlavicce) == SCRIPTS[script]
for balicek, verze in v_hlavicce.items():
assert balicek in zamcene, f"{balicek} chybí v requirements.txt"
assert verze == zamcene[balicek], (
f"{script}: {balicek}=={verze}, ale zamčeno je "
f"{zamcene[balicek]} — přepiš hlavičku")
+49
View File
@@ -0,0 +1,49 @@
"""Která verze počitadla to vlastně je.
Venku jsou čtyři binárky pro tři systémy a k tomu zdrojáky. Když někdo
napíše, že mu něco nefunguje, je první otázka vždycky stejná — a bez
`--version` na ni nemá jak odpovědět.
"""
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
NAME = "verze.txt"
UNKNOWN = "neznámá"
def bundled() -> str:
"""Verze vypálená do binárky při stavbě.
`build.sh` ji zapíše do souboru, `dice-counter.spec` ji přibalí. V binárce
leží vedle šablon v dočasném adresáři, na který ukazuje `sys._MEIPASS`.
"""
root = Path(getattr(sys, "_MEIPASS", Path(__file__).parent))
try:
return (root / NAME).read_text(encoding="utf-8").strip()
except OSError:
return ""
def described() -> str:
"""Verze podle gitu. Ze zdrojáků je to přesnější než cokoli zapsaného."""
try:
done = subprocess.run(
["git", "describe", "--tags", "--always", "--dirty"],
cwd=Path(__file__).parent, capture_output=True, text=True,
timeout=5, check=True)
except (OSError, subprocess.SubprocessError):
return ""
return done.stdout.strip()
@lru_cache(maxsize=1)
def current() -> str:
"""Verze pro výpis. Ptát se gitu má smysl jen mimo binárku."""
if getattr(sys, "frozen", False):
return bundled() or UNKNOWN
return described() or bundled() or UNKNOWN
+64 -4
View File
@@ -1,6 +1,6 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["flask", "qrcode"]
# dependencies = ["flask==3.1.3", "qrcode==8.2", "waitress==3.0.2"]
# ///
"""Webové počitadlo pro domácí síť.
@@ -8,19 +8,26 @@ Telefon zapisuje skóre (`/game/<id>`), notebook ukazuje zápisník (`/board`).
Server-rendered HTML, formuláře přes POST/redirect/GET, žádný build step.
"""
import io
import os
import shutil
import socket
import sys
import tempfile
from datetime import date
from pathlib import Path
import qrcode
from flask import (Flask, abort, g, redirect, render_template, request,
url_for)
send_file, url_for)
from markupsafe import Markup, escape
from waitress import serve
import console
import net
import stats
import storage
import version
from core import FINAL_SCORE, Game, GameOver
def bundled(folder: str) -> str:
@@ -124,6 +131,12 @@ def qr_svg(data: str) -> Markup:
)
@app.context_processor
def verze():
"""Verze do patičky. Když někdo hlásí chybu, tohle je první otázka."""
return {"verze": version.current()}
@app.template_filter("datum")
def datum(value: str) -> str:
"""Z ISO textu udělá 12. 8. 2026."""
@@ -310,6 +323,36 @@ def board_panel(game_id: int):
return render_template("_panel.html", **view(game, game_id, row))
@app.route("/export")
def export():
"""Databáze ke stažení.
Dokud se hrálo ze zdrojáků, ležel `dice.db` v pracovním adresáři a záloha
byla otázka zkopírování. Z binárky se schovává do systémového datového
adresáře, který na Windows běžný člověk nenajde — tak ať se dá stáhnout
přes prohlížeč.
`VACUUM INTO` udělá konzistentní kopii i uprostřed zápisu, na rozdíl od
prostého přečtení souboru. Výsledek si přečteme do paměti a kopii hned
zahodíme: databáze počitadla se počítá v kilobajtech a takhle nezůstane
ležet v dočasném adresáři, když si ji někdo nestáhne celou.
"""
conn = db()
conn.commit() # VACUUM nesmí běžet v transakci
docasny = tempfile.mkdtemp()
try:
kopie = Path(docasny) / "dice.db"
conn.execute("VACUUM INTO ?", (str(kopie),))
data = kopie.read_bytes()
finally:
shutil.rmtree(docasny, ignore_errors=True)
return send_file(io.BytesIO(data), mimetype="application/vnd.sqlite3",
as_attachment=True,
download_name=f"dice-counter-{date.today().isoformat()}.db")
@app.route("/stats")
def hall():
conn = db()
@@ -319,7 +362,10 @@ def hall():
games=storage.recent_games(conn))
if __name__ == "__main__":
def announce() -> None:
"""Vypíše, kde všude je počitadlo k mání, a čím je zrovna spuštěné."""
print(f"dice-counter {version.current()}\n")
found = net.addresses()
if found:
@@ -333,4 +379,18 @@ if __name__ == "__main__":
print(f"\nDatabáze: {storage.DB_PATH.resolve()}")
app.run(host="0.0.0.0", port=PORT, threaded=True)
if __name__ == "__main__":
console.utf8()
if {"--version", "-V"} & set(sys.argv[1:]):
print(f"dice-counter {version.current()}")
sys.exit(0)
announce()
# Ne vestavěný server Flasku: ten pod úvodní hlášku vysype červené
# varování, že takhle se to nemá, a má pravdu. Waitress je čistě
# pythonní, takže se zabalí do binárky bez řečí, a osm vláken pobere
# telefony u stolu i tabuli, která se doptává každé dvě vteřiny.
serve(app, host="0.0.0.0", port=PORT, threads=8)