28 Commits

Author SHA1 Message Date
L'électron rare 2fffcfe9a3 feat(box): celebratory spinning star on bravo
Make the end-of-routine "Bravo !" lively: the star now spins
(continuous full turn via lv_image_set_rotation) and bounces
(translate_y ping-pong). Both use layer-free paths — image
rotation transforms against the flush band, translate is not a
layer-forcing style — so the render task stays light (no repeat
of the full-screen-layer task_wdt). Auto-return stretched to
3.6 s so the spin shows and the festive cue finishes.

Tower: seed a more festive bravo.mp3 line ("Youpi, tu as
réussi ! Tu es super !").
2026-06-21 12:50:59 +02:00
L'électron rare 7d2a5f9d1c feat(tower): seed routine ok and bravo sounds
Add _seed_fixed_sound() helper and call it from build_routine_manifest
on the real TTS path (guarded by _real_audio = gen_audio is None).
Writes ok.mp3 (Et hop !) and bravo.mp3 (Bravo ! Tu as fini.) into
STAGING_ROUTINES if absent/empty; existence guard avoids re-generation.
Tests stay offline: injected gen_audio != None skips the block entirely.
2026-06-21 11:29:47 +02:00
L'électron rare 65d23cb25d fix(tower): key transfers journal by dir and name
transfers_mark gains an explicit dir kwarg; push_batch (sole caller)
passes dir=subdir so marking is precise. failed_names keys status_by
by (dir, name) instead of name alone, so a podcast and a routine
asset with the same basename are tracked as two distinct records.
Adds test_failed_names_disambiguation_by_dir covering this case.
2026-06-21 10:01:18 +02:00
L'électron rare ad9ee4f057 fix(tower): explicit 400 on bad routines body
Replace bare assert (stripped under python -O) with an explicit
isinstance check that returns HTTP 400 when routines is missing
or not a list. JSON-decode errors still caught by the outer try.
2026-06-21 10:01:13 +02:00
L'électron rare 5a23f3e47b feat(ui): transfers tab with autorefresh
Add panel-transfers + tab-transfers DOM, loadTransfers() with
setInterval(2s) while active != null, stopTransfersTimer() on
leave-tab and on idle, retryTransfers() for POST /api/transfers/retry.
All record fields escaped; null duration/rate guarded.
2026-06-21 09:51:49 +02:00
L'électron rare 281f0ddbfe feat(ui): routines tab editor 2026-06-21 09:46:35 +02:00
L'électron rare 831fd3c45e feat(tower): transfers api and retry endpoint 2026-06-21 09:42:36 +02:00
L'électron rare 483b1ec853 feat(tower): routines api save build push 2026-06-21 09:39:18 +02:00
L'électron rare 89f1ccd3d1 feat(tower): instrument push and retry failed 2026-06-21 09:34:50 +02:00
L'électron rare dfc8d4762b fix(tower): lock transfers history writes
Add _history_lock to serialize history load→extend→write in
transfers_end(), preventing lost-update races on transfers.json if a
second writer is added later. Fix file handle leak in
_transfers_history_load() with context manager.
2026-06-21 09:31:42 +02:00
L'électron rare 58da6b63e4 feat(tower): transfers journal begin mark end 2026-06-21 09:28:45 +02:00
L'électron rare 916d4d123f feat(tower): routines staging push and delta
Add compute_delta_named, routines_staging_files, compute_routines_delta
so routines.json goes last in the push delta. Generalize push_file with
subdir param (default podcasts, compat preserved). Fix test_delta.py:
compute_delta lives in lisael_content, not lisael_server.
2026-06-21 09:24:45 +02:00
L'électron rare e493f85203 test(tower): cover routine cache regen paths
Add two test cases to verify that the routine manifest cache correctly
regenerates assets when their dependencies change:

1. test_build_routine_manifest_regen_on_text_change: Mutate ONE step's
   text while keeping emoji unchanged; verify that audio is regenerated
   (audio count >= 1) AND bin is NOT regenerated (bin count == 0).

2. test_build_routine_manifest_regen_on_emoji_change: Mutate ONE step's
   emoji while keeping text unchanged; verify that bin is regenerated
   (bin count >= 1) AND audio is NOT regenerated (audio count == 0).

Both tests follow the existing cache test pattern: fake generators that
count calls and write files to STAGING_ROUTINES, warm cache via initial
build, mutate content, rebuild with fresh counters, assert expectations.
Cleanup removes ROUTINES_JSON to avoid test state leakage.

All 6 tests pass; cache regen branches are now fully covered.
2026-06-21 09:20:43 +02:00
L'électron rare 0a114e8638 feat(tower): build routine manifest with cache
Adds asset_key(), build_routine_manifest(), _gen_step_audio(),
_routine_cache_load() and ROUTINES_CACHE to lisael_content.py.
Cache (STAGING_ROUTINES/.cache.json) skips regeneration when
emoji/text unchanged and file exists on disk. SD manifest uses
icon/audio keys (firmware schema). Injectable gen_bin/gen_audio
for unit-testable TDD without network.
2026-06-21 09:17:45 +02:00
L'électron rare 39bee27a2d feat(tower): routines truth model load and save
TDD: test_routines_seed_and_roundtrip tests routines_load()
seeding default (matin/soir), round-trip save/load, and
verifies json structure with all required fields (id, title,
emoji, steps). All tests GREEN.
2026-06-21 09:13:19 +02:00
L'électron rare 38141bff69 test(tower): strengthen emoji_cp zwj assertion 2026-06-21 09:11:24 +02:00
L'électron rare f8532ed3f3 feat(tower): twemoji to RGB565A8 bin helper
Add emoji_cp() (pure: emoji → Twemoji codepoint, strips VS16) and
emoji_bin() (download Twemoji, resize 160×160 RGBA, LVGLImage.py →
RGB565A8 .bin in STAGING_ROUTINES). TDD test passes (test_routines.py).
2026-06-21 09:08:36 +02:00
L'électron rare f39bbdebf9 feat(tower): add gateway TTS helper
Add tts_speak() function to call the tailnet gateway TTS endpoint
(model tts-1, voice fr, response mp3). Configurable via LISAEL_TTS_URL
and LISAEL_TTS_VOICE env vars. Returns raw mp3 bytes, raises on failure.
2026-06-21 09:04:44 +02:00
Clément Saillant ec2dd3f24f feat(tower): cap shows at latest N per feed
The additive refresh accumulated episodes without bound (Oli reached 13).
Tag each manifest episode with a source (auto/pick/upload); the refresh now
prunes "auto" episodes that aged out of the feed's latest N while keeping
hand-picked and uploaded ones. Feeds default to N=5.
2026-06-15 08:40:43 +02:00
Clément Saillant 2ffec1cc57 feat(tower): Merlin Studio web UI 2026-06-15 08:06:12 +02:00
Clément Saillant 0eb3d1eb05 feat(tower): Merlin admin API + basic auth 2026-06-15 08:02:50 +02:00
Clément Saillant d6176f295f refactor(tower): additive refresh on shared module 2026-06-15 08:01:49 +02:00
Clément Saillant 0ca9df5496 feat(tower): shared content module 2026-06-15 07:59:14 +02:00
Clément Saillant 1aab35bc3d feat(tower): transcode episodes + async push
Episodes are 18-24 MB and the box's link tops out near 100 KB/s, so
full-size pushes time out. Transcode to mono 48 kbps MP3 (a few MB,
plenty for spoken-word kids podcasts); the _a48 tag gives a fresh name
so the box re-pulls the light version. Ack /register immediately and
push the delta on a background thread (one batch per box) so the box's
short register timeout never trips.
2026-06-15 07:45:10 +02:00
Clément Saillant aa4707502e fix(tower): unique per-episode filenames for delta 2026-06-15 00:57:35 +02:00
Clément Saillant 47df147e7b chore(tower): systemd service + weekly timer 2026-06-15 00:52:59 +02:00
Clément Saillant 647bc57818 feat(tower): register server + delta push to box 2026-06-15 00:50:11 +02:00
Clément Saillant 48e2b6c830 feat(tower): weekly podcast refresh to staging 2026-06-15 00:48:11 +02:00