tools/: Screenshot-Skript (headless Chromium) + Demo-Generator dauerhaft ablegen
Image bauen und in die Registry schieben / build (push) Successful in 6s
Image bauen und in die Registry schieben / build (push) Successful in 6s
- tools/screenshot.sh: konsolidiert (--view today|history, --size, --out, --full); startet eigenen Server mit temporaerem Zustand, befuellt eine Session bzw. importiert Demo-Daten, schießt den Screenshot, raeumt auf - tools/gen_demo.py: Demo-Daten-Generator (Pfade relativ zum Repo) - tools/README.md: Nutzung
This commit is contained in:
1 parent
c7647ba962
commit
314f4f329e
3 files changed
+307
No files matched your search
@@ -0,0 +1,33 @@
|
|||||||
|
# tools
|
||||||
|
|
||||||
|
Helfer für Entwicklung und Doku. Nicht Teil der App.
|
||||||
|
|
||||||
|
## `gen_demo.py`
|
||||||
|
|
||||||
|
Erzeugt `demo-data.json` (12 Wochen realistischer Verlauf: Plateaus,
|
||||||
|
Deload-Woche, ausgelassene Tage, Streuung, Notizen).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/gen_demo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## `screenshot.sh`
|
||||||
|
|
||||||
|
Screenshot der App per headless Chromium (für README/Doku). Startet einen
|
||||||
|
eigenen Server auf einem temporären Zustand, befüllt eine Beispiel-Session
|
||||||
|
oder importiert die Demo-Daten, schießt den Screenshot und räumt auf.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Heute-Ansicht, alle Übungen befüllt (für README)
|
||||||
|
tools/screenshot.sh --full --out assets/screenshot.png
|
||||||
|
|
||||||
|
# Verlauf mit Demo-Daten
|
||||||
|
tools/screenshot.sh --view history --out /tmp/history.png
|
||||||
|
|
||||||
|
# Handy-Ansicht
|
||||||
|
tools/screenshot.sh --size 390x844 --out /tmp/mobile.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionen: `--view today|history`, `--size WxH`, `--out PFAD`, `--full`.
|
||||||
|
|
||||||
|
Voraussetzung: `chromium` (oder `chromium-browser`), `python3`, `curl`.
|
||||||
Executable
+179
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Erzeugt demo-data.json (trainingsplan-export-v2) mit 12 Wochen realistischem
|
||||||
|
Verlauf: Plateau-Phasen, Deload-Woche, ausgelassene Tage, Reps-Streuung,
|
||||||
|
gelegentlich nicht abgehakte Saetze und Notizen - statt glattgebuegelter
|
||||||
|
Steigerung."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CFG = json.load(open(f"{BASE}/config.json", encoding="utf-8"))
|
||||||
|
gym = next(a for a in CFG["activities"] if a["id"] == "gym")
|
||||||
|
|
||||||
|
rng = random.Random(42)
|
||||||
|
|
||||||
|
WEEK_COUNT = 12
|
||||||
|
WEEKS = ["Push", "Pull", "Legs"]
|
||||||
|
|
||||||
|
|
||||||
|
def rep_value(target):
|
||||||
|
if not target:
|
||||||
|
return "10"
|
||||||
|
lo = int(target.split("-")[0])
|
||||||
|
hi = int(target.split("-")[1]) if "-" in target else lo
|
||||||
|
return (lo + hi) // 2
|
||||||
|
|
||||||
|
|
||||||
|
def week_dates():
|
||||||
|
"""Montag/Mittwoch/Freitag; die letzte Woche endet VOR heute (alles Vergangenheit)."""
|
||||||
|
today = date.today()
|
||||||
|
last_mon = today - timedelta(days=(today.weekday() % 7) + 7)
|
||||||
|
out = []
|
||||||
|
for week in range(WEEK_COUNT):
|
||||||
|
base = last_mon - timedelta(weeks=WEEK_COUNT - 1 - week)
|
||||||
|
out.append([base, base + timedelta(days=2), base + timedelta(days=4)])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def set_weight(base, session_idx, week_idx):
|
||||||
|
"""Realistische Gewichtskurve: Plateau-Phasen + Deload in Woche 8."""
|
||||||
|
w = base
|
||||||
|
progress = session_idx // 2 # alle zwei Sessions +1 Stufe
|
||||||
|
# Uebung-spezifische Rate: grosse Lifts 2.5er-Schritte, kleine 1.25er
|
||||||
|
step = 2.5 if base >= 20 else 1.25
|
||||||
|
w += step * progress
|
||||||
|
# Plateau: bei manchen Sessions bleibt das Gewicht trotz Fortschritt
|
||||||
|
if rng.random() < 0.18:
|
||||||
|
w -= step
|
||||||
|
# Deload-Woche (Woche 8, idx 7): -12%
|
||||||
|
if week_idx == 7:
|
||||||
|
w = round(w * 0.88 / step) * step
|
||||||
|
return max(0, round(w / step) * step)
|
||||||
|
|
||||||
|
|
||||||
|
def build_gym_session(sid, d, day_key, day_cfg, session_idx, week_idx, note):
|
||||||
|
entries = []
|
||||||
|
for ex in day_cfg["exercises"]:
|
||||||
|
base = float(ex.get("startKg") or 0)
|
||||||
|
w = set_weight(base, session_idx, week_idx)
|
||||||
|
cfg = ex["sets"]
|
||||||
|
count = len(cfg) if isinstance(cfg, list) else int(cfg)
|
||||||
|
if week_idx == 7:
|
||||||
|
count = max(1, count - 1) # Deload: ein Satz weniger
|
||||||
|
reps_t = rep_value(ex.get("reps") or "")
|
||||||
|
for i in range(count):
|
||||||
|
reps = max(4, reps_t + rng.choice([-2, -1, 0, 0, 0, 1]))
|
||||||
|
done = 1
|
||||||
|
weight = f"{w:g}"
|
||||||
|
# Letzter Satz haengt mal (weniger Reps), selten ein Satz ausgelassen
|
||||||
|
if i == count - 1 and rng.random() < 0.15:
|
||||||
|
reps = max(3, reps - 2)
|
||||||
|
if rng.random() < 0.05:
|
||||||
|
done = 0
|
||||||
|
entries.append({
|
||||||
|
"exercise": f"{day_key}/{ex['name']}", "sort": i,
|
||||||
|
"weight": weight, "reps": str(reps), "done": done,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"id": sid, "activity": "gym", "day": day_key,
|
||||||
|
"date": d.isoformat(),
|
||||||
|
"start_time": rng.choice(["17:45", "18:00", "18:15", "19:00"]),
|
||||||
|
"end_time": rng.choice(["18:50", "19:10", "19:20", "20:05"]),
|
||||||
|
"note": note, "fields": {}, "status": "done", "entries": entries,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_run(sid, d, idx):
|
||||||
|
km = round(rng.uniform(4.0, 7.2), 1)
|
||||||
|
pace = rng.uniform(5.9, 7.1) # min/km
|
||||||
|
pm, ps = int(pace), int(round((pace - int(pace)) * 60))
|
||||||
|
minutes = round(km * pace)
|
||||||
|
return {
|
||||||
|
"id": sid, "activity": "run", "day": "", "date": d.isoformat(),
|
||||||
|
"start_time": rng.choice(["07:00", "07:15", "18:30"]),
|
||||||
|
"end_time": "08:00", "note": rng.choice(["", "", "Lockerer Lauf",
|
||||||
|
"Tempo-Runde", "Regen"]),
|
||||||
|
"fields": {
|
||||||
|
"distanceKm": f"{km:.1f}",
|
||||||
|
"minutes": str(minutes),
|
||||||
|
"pacePerKm": f"{pm}:{ps:02d}",
|
||||||
|
"heartRate": str(rng.randint(140, 166)),
|
||||||
|
"elevation": str(rng.randint(10, 55)),
|
||||||
|
},
|
||||||
|
"status": "done", "entries": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_ride(sid, d, idx):
|
||||||
|
km = rng.randint(14, 30)
|
||||||
|
speed = rng.uniform(20.5, 25.5)
|
||||||
|
minutes = round(km / speed * 60)
|
||||||
|
return {
|
||||||
|
"id": sid, "activity": "ride", "day": "", "date": d.isoformat(),
|
||||||
|
"start_time": "09:00", "end_time": "10:00",
|
||||||
|
"note": rng.choice(["", "Ausfahrt", "Mit Gegenwind"]),
|
||||||
|
"fields": {
|
||||||
|
"distanceKm": str(km), "minutes": str(minutes),
|
||||||
|
"avgSpeed": f"{speed:.1f}",
|
||||||
|
"heartRate": str(rng.randint(132, 158)),
|
||||||
|
"elevation": str(rng.randint(45, 130)),
|
||||||
|
},
|
||||||
|
"status": "done", "entries": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dates = week_dates()
|
||||||
|
days = {d["key"]: d for d in gym["cycle"]["days"]}
|
||||||
|
sessions = []
|
||||||
|
sid = 1
|
||||||
|
notes = {
|
||||||
|
"Push": ["", "", "Brust fiel schwer", "Fokus Form, Kontrolle", ""],
|
||||||
|
"Pull": ["", "Ruecken verkrampft", "", "Zugkraft gut", ""],
|
||||||
|
"Legs": ["", "", "Deload: leichter", "Beine schwer, trotzdem durch", ""],
|
||||||
|
}
|
||||||
|
skipped = {(2, 0), (5, 2), (7, 0), (10, 1)} # Woche, Tag-Index (Mo/Di/Fr=0/1/2)
|
||||||
|
for week in range(WEEK_COUNT):
|
||||||
|
for di, day_key in enumerate(WEEKS):
|
||||||
|
if (week, di) in skipped:
|
||||||
|
continue
|
||||||
|
d = dates[week][di]
|
||||||
|
note = rng.choice(notes[day_key])
|
||||||
|
sid_s = 0
|
||||||
|
for s in sessions:
|
||||||
|
if s["day"] == day_key:
|
||||||
|
sid_s += 1
|
||||||
|
sessions.append(build_gym_session(sid, d, day_key, days[day_key],
|
||||||
|
sid_s, week, note))
|
||||||
|
sid += 1
|
||||||
|
# Cardio: Mo abends laufen, So manchmal Fahrt
|
||||||
|
sessions.append(build_run(sid, dates[week][0] + timedelta(days=0), week))
|
||||||
|
sid += 1
|
||||||
|
if rng.random() < 0.3:
|
||||||
|
sessions.append(build_ride(sid, dates[week][2] + timedelta(days=2), week))
|
||||||
|
sid += 1
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
"format": "trainingsplan-export-v2",
|
||||||
|
"exported_at": date.today().isoformat() + "T00:00:00",
|
||||||
|
"config": CFG,
|
||||||
|
"sessions": sessions,
|
||||||
|
}
|
||||||
|
out = f"{BASE}/demo-data.json"
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(doc, f, ensure_ascii=False, indent=1)
|
||||||
|
acts = {}
|
||||||
|
for s in sessions:
|
||||||
|
acts[s["activity"]] = acts.get(s["activity"], 0) + 1
|
||||||
|
print(f"{out}: {len(sessions)} Sessions, {acts}")
|
||||||
|
print("Zeitraum:", min(s["date"] for s in sessions), "bis",
|
||||||
|
max(s["date"] for s in sessions))
|
||||||
|
notes_n = sum(1 for s in sessions if s["note"])
|
||||||
|
undone = sum(1 for s in sessions for e in s["entries"] if not e["done"])
|
||||||
|
print("mit Notiz:", notes_n, "| nicht abgehakte Saetze:", undone)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Screenshot der App fuer Doku/README (headless Chromium).
|
||||||
|
#
|
||||||
|
# Nutzung:
|
||||||
|
# tools/screenshot.sh [--view today|history] [--size 1280x1080]
|
||||||
|
# [--out PFAD] [--full]
|
||||||
|
#
|
||||||
|
# --view today (Standard, mit Beispiel-Session) oder history (Demo-Verlauf)
|
||||||
|
# --size Fenstergroesse, z.B. 390x844 fuer die Handy-Ansicht
|
||||||
|
# --out Zieldatei (Standard: tools/screenshot.png)
|
||||||
|
# --full heutige Ansicht mit ALLEN Uebungen befuellt (nicht nur ein paar)
|
||||||
|
#
|
||||||
|
# Voraussetzungen: python3, curl, chromium (oder chromium-browser).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
VIEW=today
|
||||||
|
SIZE=1280x1080
|
||||||
|
OUT="$BASE/tools/screenshot.png"
|
||||||
|
FULL=0
|
||||||
|
PORT=8097
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--view) VIEW="$2"; shift 2;;
|
||||||
|
--size) SIZE="$2"; shift 2;;
|
||||||
|
--out) OUT="$2"; shift 2;;
|
||||||
|
--full) FULL=1; shift;;
|
||||||
|
*) echo "unbekannte Option: $1" >&2; exit 1;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
STATE="$(mktemp -d)"
|
||||||
|
TRAINING_STATE_DIR="$STATE" PORT="$PORT" python3 "$BASE/server.py" >"$STATE/log" 2>&1 &
|
||||||
|
SRV=$!
|
||||||
|
trap 'kill "$SRV" 2>/dev/null || true; rm -rf "$STATE"' EXIT
|
||||||
|
sleep 1.2
|
||||||
|
B="http://127.0.0.1:$PORT"
|
||||||
|
|
||||||
|
if [ "$VIEW" = "history" ]; then
|
||||||
|
curl -s -X PUT "$B/api/settings" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"setupDone":true,"startTab":"history"}' >/dev/null
|
||||||
|
python3 - "$B" "$BASE" <<'PY'
|
||||||
|
import json, sys, urllib.request
|
||||||
|
B, base = sys.argv[1], sys.argv[2]
|
||||||
|
demo = json.load(open(base + "/demo-data.json", encoding="utf-8"))
|
||||||
|
data = json.dumps({"format": "trainingsplan-export-v2",
|
||||||
|
"data": json.dumps(demo)}).encode()
|
||||||
|
urllib.request.urlopen(urllib.request.Request(
|
||||||
|
B + "/api/import", data=data,
|
||||||
|
headers={"Content-Type": "application/json"}, method="POST")).read()
|
||||||
|
PY
|
||||||
|
else
|
||||||
|
curl -s -X PUT "$B/api/settings" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"setupDone":true}' >/dev/null
|
||||||
|
python3 - "$B" "$FULL" <<'PY'
|
||||||
|
import json, sys, urllib.request
|
||||||
|
B, full = sys.argv[1], sys.argv[2] == "1"
|
||||||
|
sid = json.load(urllib.request.urlopen(urllib.request.Request(
|
||||||
|
B + "/api/sessions",
|
||||||
|
data=json.dumps({"activity": "gym", "day": "Push"}).encode(),
|
||||||
|
headers={"Content-Type": "application/json"}, method="POST")))["id"]
|
||||||
|
small = {
|
||||||
|
"Push/Bankdrücken KH (je Hand)": [("18,5", "10"), ("18,5", "10"), ("20", "8"), ("20", "6")],
|
||||||
|
"Push/Schrägbank KH (je Hand)": [("14", "12"), ("14", "10")],
|
||||||
|
"Push/Pectoral/Butterfly": [("20", "15")],
|
||||||
|
}
|
||||||
|
big = {
|
||||||
|
"Push/Bankdrücken KH (je Hand)": [("18,5", "10"), ("18,5", "10"), ("20", "8"), ("20", "6")],
|
||||||
|
"Push/Schrägbank KH (je Hand)": [("14", "12"), ("14", "10"), ("14", "10")],
|
||||||
|
"Push/Pectoral/Butterfly": [("20", "15"), ("20", "15"), ("25", "12")],
|
||||||
|
"Push/Seitheben (je Hand)": [("8", "15"), ("8", "14"), ("8", "12")],
|
||||||
|
"Push/Schulterpresse Maschine": [("25", "12"), ("30", "10"), ("30", "9")],
|
||||||
|
"Push/Kabel-Trizeps-Pushdown": [("25", "15"), ("25", "14"), ("27,5", "12")],
|
||||||
|
"Push/Trizeps-Strecken (French Press Maschine)": [("15", "15"), ("15", "12"), ("15", "12")],
|
||||||
|
"Push/Kabel-Curls (Bizeps)": [("15", "15"), ("15", "14"), ("17,5", "12")],
|
||||||
|
}
|
||||||
|
data = big if full else small
|
||||||
|
sets = [{"exercise": ex, "sort": i, "weight": w, "reps": r, "done": 1}
|
||||||
|
for ex, lst in data.items() for i, (w, r) in enumerate(lst)]
|
||||||
|
urllib.request.urlopen(urllib.request.Request(
|
||||||
|
B + "/api/sessions/" + str(sid), data=json.dumps({"sets": sets}).encode(),
|
||||||
|
headers={"Content-Type": "application/json"}, method="PATCH")).read()
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHROME="$(command -v chromium || command -v chromium-browser)"
|
||||||
|
WS="${SIZE/x/,}"
|
||||||
|
"$CHROME" --headless=new --no-sandbox --disable-gpu --hide-scrollbars \
|
||||||
|
--virtual-time-budget=6000 --window-size="$WS" --screenshot="$OUT" "$B/" \
|
||||||
|
>/dev/null 2>&1 || \
|
||||||
|
"$CHROME" --headless --no-sandbox --disable-gpu --hide-scrollbars \
|
||||||
|
--virtual-time-budget=6000 --window-size="$WS" --screenshot="$OUT" "$B/" \
|
||||||
|
>/dev/null 2>&1
|
||||||
|
echo "geschrieben: $OUT"
|
||||||
Reference in new issue
Block a user