Infrastrukturní úklid: zamčené závislosti, verze schématu i binárky #12

Merged
david-spacil merged 8 commits from agents/claude/infrastruktura into main 2026-08-22 18:42:29 +02:00
4 changed files with 73 additions and 3 deletions
Showing only changes of commit a655bfe6f3 - Show all commits

No files matched your search

+3 -1
View File
@@ -99,7 +99,9 @@ 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
+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 %}
+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
+35 -1
View File
@@ -8,14 +8,18 @@ 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
@@ -319,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"kostky-{date.today().isoformat()}.db")
@app.route("/stats")
def hall():
conn = db()