chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)

Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -0,0 +1,174 @@
import base64
import pathlib
import textwrap
root = pathlib.Path("/home/anthony/Documents/Projects/GameTime")
anton = base64.b64encode((root / "assets/fonts/Anton-Regular.ttf").read_bytes()).decode()
archivo = base64.b64encode((root / "assets/fonts/Archivo-Variable.ttf").read_bytes()).decode()
# Palette Court Blazer (theme sombre)
GOLD = "#C9A24A"
CRIMSON = "#D72638"
BG = "#080A12"
SURFACE = "#141824"
TEXT = "#F5F1E8"
TEXT_SECONDARY = "#A7ADBA"
BORDER = "#242A3A"
TILE_W = 260
TILE_H = 330
FACE_D = 216
GAP = 24
N = 5
WIDTH = N * TILE_W + (N + 1) * GAP
HEIGHT = TILE_H + 190
def watch_face(cx, cy, r, label_top, big_value, big_color, status_line, button_label, button_style, disconnected=False):
parts = []
# bezel
parts.append(f'<circle cx="{cx}" cy="{cy}" r="{r+6}" fill="{BORDER}"/>')
parts.append(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{SURFACE}"/>')
# crimson top accent arc (clin d\'oeil liseré 2px des cartes cles, adapte en arc sur le bezel interne)
if not disconnected:
parts.append(f'<path d="M {cx - r*0.55} {cy - r*0.82} A {r} {r} 0 0 1 {cx + r*0.55} {cy - r*0.82}" '
f'fill="none" stroke="{CRIMSON}" stroke-width="3" stroke-linecap="round"/>')
else:
parts.append(f'<path d="M {cx - r*0.55} {cy - r*0.82} A {r} {r} 0 0 1 {cx + r*0.55} {cy - r*0.82}" '
f'fill="none" stroke="{BORDER}" stroke-width="3" stroke-linecap="round"/>')
top_y = cy - r * 0.44
value_y = cy - r * 0.02
status_y = cy + r * 0.30
btn_cy = cy + r * 0.62
btn_w = r * 1.5
btn_h = 40
label_color = TEXT_SECONDARY if not disconnected else "#5A6072"
parts.append(
f'<text x="{cx}" y="{top_y}" text-anchor="middle" font-family="Archivo" font-weight="600" '
f'font-size="13" letter-spacing="1" fill="{label_color}">{label_top}</text>'
)
parts.append(
f'<text x="{cx}" y="{value_y}" text-anchor="middle" font-family="Anton" font-size="56" '
f'fill="{big_color}">{big_value}</text>'
)
parts.append(
f'<text x="{cx}" y="{status_y}" text-anchor="middle" font-family="Archivo" font-weight="400" '
f'font-size="13.5" fill="{TEXT_SECONDARY if not disconnected else "#5A6072"}">{status_line}</text>'
)
# button pill
bx = cx - btn_w / 2
by = btn_cy - btn_h / 2
if button_style == "gold_fill":
fill, stroke, textcol = GOLD, GOLD, "#1A1206"
elif button_style == "crimson_outline":
fill, stroke, textcol = "none", CRIMSON, CRIMSON
elif button_style == "crimson_fill":
fill, stroke, textcol = CRIMSON, CRIMSON, TEXT
else: # muted outline (disconnected)
fill, stroke, textcol = "none", "#3A4152", TEXT_SECONDARY
parts.append(
f'<rect x="{bx:.1f}" y="{by:.1f}" width="{btn_w:.1f}" height="{btn_h}" rx="{btn_h/2:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="1.6"/>'
)
parts.append(
f'<text x="{cx}" y="{btn_cy + 5}" text-anchor="middle" font-family="Archivo" font-weight="700" '
f'font-size="15" fill="{textcol}">{button_label}</text>'
)
return "\n".join(parts)
states = [
dict(
caption="1 — Pret a demarrer",
sub="Etape / exercice sous chrono, pas encore lance",
label_top="DRIBBLE COMBO",
big_value="00:00",
big_color=TEXT_SECONDARY,
status_line="Serie 2 / 4",
button_label="Demarrer",
button_style="gold_fill",
),
dict(
caption="2 — Chrono en cours",
sub="Le chrono actif tourne (exercice ou etape)",
label_top="DRIBBLE COMBO",
big_value="00:23",
big_color=GOLD,
status_line="Serie 2 / 4 - en cours",
button_label="Arreter",
button_style="crimson_outline",
),
dict(
caption="3 — Chrono en pause",
sub="Chrono arrete avec valeur, reprise possible",
label_top="DRIBBLE COMBO",
big_value="00:23",
big_color=GOLD,
status_line="Serie 2 / 4 - en pause",
button_label="Reprendre",
button_style="gold_fill",
),
dict(
caption="4 — Pas de chrono actif",
sub="Serie reps/score libre : l'action utile est de conclure",
label_top="TIRS EN SUSPENSION",
big_value="2 / 4",
big_color=GOLD,
status_line="Serie en cours",
button_label="Terminer la serie",
button_style="crimson_fill",
),
dict(
caption="5 — Montre non synchronisee",
sub="Perte de liaison : dernier etat connu, sans alarmer",
label_top="DERNIERE DONNEE RECUE",
big_value="00:23",
big_color="#5A6072",
status_line="Non synchronisee",
button_label="Reessayer",
button_style="muted",
disconnected=True,
),
]
svg_parts = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}">',
'<defs><style>',
f'@font-face {{ font-family: "Anton"; src: url(data:font/ttf;base64,{anton}) format("truetype"); }}',
f'@font-face {{ font-family: "Archivo"; src: url(data:font/ttf;base64,{archivo}) format("truetype"); }}',
'</style></defs>',
f'<rect x="0" y="0" width="{WIDTH}" height="{HEIGHT}" fill="{BG}"/>',
f'<text x="{WIDTH/2}" y="46" text-anchor="middle" font-family="Anton" font-size="30" fill="{TEXT}">GAMETIME — MONTRE SYNCHRONISEE</text>',
f'<text x="{WIDTH/2}" y="72" text-anchor="middle" font-family="Archivo" font-weight="400" font-size="15" fill="{TEXT_SECONDARY}">Ecran unique, bouton contextuel unique — style Court Blazer</text>',
]
for i, st in enumerate(states):
cx = GAP + TILE_W * i + TILE_W / 2
cy = 96 + FACE_D / 2 + 10
r = FACE_D / 2
svg_parts.append(watch_face(cx, cy, r, st["label_top"], st["big_value"], st["big_color"],
st["status_line"], st["button_label"], st["button_style"],
disconnected=st.get("disconnected", False)))
cap_y = cy + r + 34
sub_y = cap_y + 22
svg_parts.append(
f'<text x="{cx}" y="{cap_y}" text-anchor="middle" font-family="Archivo" font-weight="700" '
f'font-size="14" fill="{TEXT}">{st["caption"]}</text>'
)
lines = textwrap.wrap(st["sub"], width=30)
for li, line in enumerate(lines):
svg_parts.append(
f'<text x="{cx}" y="{sub_y + li * 15}" text-anchor="middle" font-family="Archivo" font-weight="400" '
f'font-size="11.5" fill="{TEXT_SECONDARY}">{line}</text>'
)
svg_parts.append('</svg>')
out = root / ".ideai/tickets/91/assets/watch_screen_mockup.svg"
out.write_text("\n".join(svg_parts), encoding="utf-8")
print("written", out, out.stat().st_size)

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,6 +1,6 @@
---
issueRef: "#91"
version: 3
version: 4
updatedBy: {"kind":"user"}
updatedAt: 1784656427938
updatedAt: 1785230763779
---

View File

@ -2,7 +2,7 @@
id: "990a5e5a-8490-43ab-b7f6-7cae61be25fd"
number: 91
title: "Interface montre synchronisée"
status: "open"
status: "closed"
priority: "low"
sprint: null
links: []
@ -10,7 +10,7 @@ agentRefs: []
createdBy: {"kind":"user"}
updatedBy: {"kind":"user"}
createdAt: 1784656109713
updatedAt: 1784656427938
version: 3
updatedAt: 1785230763779
version: 4
---
J'aimerais avoir une version pour ma montre android qui soit synchronisée avec l'application téléphone. Elle me permettrait entre autre de lancer/arrêter les chronos (donc dans le cas d'un exercice sous chrono, de lancer le chrono ou de le mettre en pause) ou dans le cas d'une étape de lancer le chrono de l'étape, en gardant la décision prise comme quoi si la première étape est un chrono et que l'exercice est sous chrono, le lancement de chrono est commun a l'exercice et à l'étape. Ca permettrait en plus de ça de pouvoir passer une étape, une séquence, une série ou terminer une série. Bref, pouvoir gérer sa séance depuis sa montre. l'UX/UI est bien sur a voir avec l'agent UX pour savoir comment organiser ça. Heavy propose une fonctionnalité similaire si il y a besoin d'une inspiration