diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e5eb7bf..7e84da9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,3 +62,4 @@ is at the bottom; the current roadmap lives in the [README](README.md#roadmap). - **v3.3.1 — The whole sentence airs (gh-#424)** — patch: cue-out now comes only from silence that **actually extends to EOF**. `FfmpegCueAnalyzer` took the *last* silence region as cue-out unconditionally, so an interior pause that happened to be last — a 0.6 s injected TTS sentence gap (they clear the 0.5 s silencedetect floor), a quiet mid-track break — became `liq_cue_out`, and **the final sentence of every multi-sentence patter clip was cut on air** while booth logs, the cached wav, and the preview all looked complete (`DurationMs` derives from the same cue-out, so even the progress bar agreed). Trailing now means open-ended at EOF or closed within 0.25 s of the container duration; interior-only files report the full audible extent so durations stay measured for straddle boundary-fit; misclassification degrades safe-only (untrimmed tail → `blank.eat` backstop). Analyzer-level on purpose: music with a quiet interior break stops mis-trimming too. Playout self-heals on deploy (blurb cues re-measure per process); persisted cue rows — music enrichment, authored imaging — keep old values until re-enriched/re-authored. Found in Dean's T229 listening pass (both v2.9.0 and v3.3.0 sightings were this one path); regression spec red-checked against the pre-fix parser. - **v3.3.2 — Nothing grim, nothing clipped (gh-#433, gh-#424 rollout)** — patch, two halves. **The history airability gate (gh-#433)**: `HistoryFactHygiene` sits between the On-This-Day day cache and either delivery lane at **vend** time — somber facts (violent-death / disaster / atrocity vocabulary, word-boundary matched, deliberately fail-closed) never air, and half-stripped `[[wikilink]]` bracket residue is cleaned from what does. Driven by a live sighting: a fatal plane crash aired as chill-morning patter color, stray bracket included. An all-somber day — Wikimedia's curated `selected` feed skews grim; the real captured reply was five-for-five — is an ordinary F107.6 skip, one Information line; pool-widening to the fuller `events` feed is the recorded follow-up if coverage thins too far. **The stale-cue retirement (db/34)**: every cue point persisted before v3.3.1's analyzer fix could carry a mid-file `cue_out` (music with a quiet break clipped at the pause — heard live on the demo). A marker-guarded one-time migration (`library.one_time_fix` ledger, claim-CTE, twice-run-proven against a real postgres:16.4) nulls the stale cue columns at deploy — instantly curative, NULL cue = full-file playback — and the F13 backfill predicate re-measures the whole library with the fixed analyzer at the worker's own pace. Zero operator action, per the standing zero-manual-release-steps rule. - **v3.4.0 — Dayparting: named shows (SPEC F115–F121, gh-#383)** — the hours get names. **Shows are identity packages** (`station.show` widened, db/35: slug/tagline/flavor/provenance + the dormant schedulable-bundle columns, ruled unread until that slice designs their readers): authored on a new **Shows page** (60/120/400 budgets, guarded delete naming referencing blocks), assigned from the schedule grid's side panel with **run-span default** — one click names a whole contiguous same-persona run, and music-only runs span too (ruled: one click names the overnight stretch). **The ear knows**: sign-on ceremony carries the incoming show's name + flavor, a same-DJ show flip airs exactly one transition piece (F92.3 dedupe now keys persona AND show), a **cadence-gated flavor line** shares F107.5's single extra-line slot (context facts win; the prompt ceiling never grows), and top-of-hour idents go **show-scoped-first** — authored rows scoped to the show, then the station's authored pool, then a templated "You're listening to ⟨show⟩ on ⟨station⟩." floor (ladder ruled: authored beats template). **The public face knows**: spectator now-playing carries `show {name, tagline}` and `upNext.show` (name-only by type shape; flavor is prompt-config, structurally absent from every public payload — value-level pinned). **The catalog grows its third community kind**: `show` entries (schemas + budget lint + 12 seed cards, one per demo DJ, shipped as 12 one-entry PRs per the moderation model) import through the F79 shell with an **atomic authored-protection upsert** (`ON CONFLICT … WHERE imported_from IS NOT NULL` — the gh-#394 conditional-write form; an authored show's slug survives any import byte-for-byte) and a **soft "also hire ⟨DJ⟩" offer** that opens the persona's own full-card review (never a silent second import); imported Name/Flavor are sanitized at the parser seam (control chars flattened, fence runs collapsed) with the F90 full-card confirm as the ruled trust gate. **Dated specials land as the ruled-droppable tail, delivered** (db/36, own migration): per-date EXCLUDE-guarded one-offs shadow the weekly grid for their span with honest boundaries both directions (mid-block resume; a midnight special still arms the outgoing sign-off via the tomorrow-boundary-peek), authored on a dated-list form — deliberately not a second paint grid. **Airings are show-countable** (`booth_log.show_id`, air-time stamp, no FK) — the demo-hour gate gains its show halves as one indexed query. Rider: **time announcements join the ident** ("It's two o'clock on ⟨station⟩." — gh-#453, ruled on the first live listen; the forever-cache re-keys by construction). Built via /build-loop's adversarial gate across five stacked PRs: catches included a grid-save wipe of every show assignment (killed at three layers), a published-NuGet binary break, a boundary-fit estimator corruption, and an upNext erasure at exactly the boundary this epic ruled real — plus two display bugs only the live wire smokes caught. +- **v3.5.0 — Every kind in its place (gh-#372, gh-#393, catalog#33 prep)** — the mixed piles get shelves. **The Wardrobe silos by kind** (gh-#393): Personas | Themes | Fonts | Shows, URL-driven tabs with every tab present even when empty (ruled on the issue: an empty kind shows its own empty state, never a hidden tab); fonts keep their founding cards + uninstall, the other kinds list genuinely-imported rows only (`importedFrom`-gated, the two-provenance-class rule) under their own verbs — Hired / Imported / Installed — with actions deliberately staying on each kind's own page. **The community shelf gets the same treatment** (gh-#372): `?kind=` tabs over the catalog grid — a persona card and a show card were visually identical without opening them — while the cross-kind machinery (the soft hire offer including its failure path, the install modals, imported-state maps) keeps reading the full index untouched. The tab strip itself is extracted once (`components/ui/tab-strip.tsx` — CatalogTabs and BoothLogTabs were already byte-identical copies; four consumers now share one). **The validator learns the catalog's next layout** (genwave-catalog#33): `entries///` admitted alongside the flat layout — the kind folder, when present, must name the entry's own kind; slug ownership reads the second-to-last path segment; and a new one-directory invariant pins an entry's manifest/meta/assets to a single directory (what keeps bare filenames unique for the filename-keyed asset route once one slug can legally exist under two layouts). The catalog-side move (genwave-catalog#50) holds until this release is on the box — an older app rejects a migrated index wholesale, fail-closed. diff --git a/compose.demo.yaml b/compose.demo.yaml index e3d8219a..cf34cd26 100644 --- a/compose.demo.yaml +++ b/compose.demo.yaml @@ -47,7 +47,7 @@ services: restart: unless-stopped admin_ui: - image: ghcr.io/genwave-org/genwave-admin-ui:home-v3.4.0 # bump per release + image: ghcr.io/genwave-org/genwave-admin-ui:home-v3.5.0 # bump per release pull_policy: always # `!reset` drops the base `build:` context entirely: a demo box must pull this image or # fail loudly — never silently fall back to building (and then forever running) local @@ -61,7 +61,7 @@ services: - "127.0.0.1:3000:3000" engine: - image: ghcr.io/genwave-org/genwave-engine:home-v3.4.0 # bump per release + image: ghcr.io/genwave-org/genwave-engine:home-v3.5.0 # bump per release pull_policy: always # Same !reset rationale as admin_ui above (gh-#93). Note genwave.liq is a bind mount # from the checkout (see compose.yaml), NOT baked into this image — the image carries @@ -74,7 +74,7 @@ services: # silently source-build and freeze. NOTE the first genwave-piper publish happens at the FIRST # TAG CUT AFTER gh-#240/#241 — until that release's pin bump this tag does not exist and the # pull fails loudly by design. Bump with the other four pins per release. - image: ghcr.io/genwave-org/genwave-piper:home-v3.4.0 # bump per release + image: ghcr.io/genwave-org/genwave-piper:home-v3.5.0 # bump per release pull_policy: always # Same !reset rationale as admin_ui above (gh-#93). build: !reset null @@ -123,7 +123,7 @@ services: restart: "no" icecast: - image: ghcr.io/genwave-org/genwave-icecast:home-v3.4.0 # bump per release + image: ghcr.io/genwave-org/genwave-icecast:home-v3.5.0 # bump per release pull_policy: always # Base compose publishes 8000:8000 on 0.0.0.0 — right for LAN listening in # Operator/Standard, wrong on a public box: Icecast serves status.xsl (listener @@ -134,7 +134,7 @@ services: ports: !override [] api: - image: ghcr.io/genwave-org/genwave:home-v3.4.0 # bump per release + image: ghcr.io/genwave-org/genwave:home-v3.5.0 # bump per release pull_policy: always # compose.yaml's base `ports:` publishes BOTH 8080 and 8081 on 0.0.0.0 — fine for # Operator/Standard (LAN reachability is the point there), wrong for a public box.