Also: record prod cutover as done in CLAUDE.md (fork :prod live since 2026-06-27) and add standing v26.4.0 release-checkpoint note to the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ Custom IPTV channel server for Jellyfin. Forked from [ErsatzTV/ErsatzTV](https:/
|
||||
- **Docker host**: jazz (192.168.1.99), container `ersatztv`, port 8409
|
||||
- **Config volume**: `~/downloadswarm/ersatztv/` on jazz → `/config` in container
|
||||
- **SQLite DB**: `/config/ersatztv.sqlite3` (WAL mode, root-owned)
|
||||
- **Images** (our fork, built by `.gitea/workflows/docker-build.yml` → `192.168.1.95:3000/timothy/ersatztv`): push to `main` → `:latest` + `:<sha>` (test image); push `v*` tag → `:prod` + `:<version>` + `:<sha>`. Prod container still runs upstream `ghcr.io/ersatztv/ersatztv:latest` pending cutover (server-management#481). Pipeline details: `docs/ci-cd.md`.
|
||||
- **Images** (our fork, built by `.gitea/workflows/docker-build.yml` → `192.168.1.95:3000/timothy/ersatztv`): push to `main` → `:latest` + `:<sha>` (test image); push `v*` tag → `:prod` + `:<version>` + `:<sha>`. Cutover done (2026-06-27, server-management#481/#482): prod container runs fork `:prod` (currently v26.3.1), test container tracks `:latest`; prod advances only when a new `v*` tag is pushed (next: `v26.4.0`, first app-change release). Pipeline details: `docs/ci-cd.md`.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ filed backend gap issues #100–#111; a 7–9-way parallel workflow build once e
|
||||
builds are limited to 2–3 concurrent, never wide fan-outs. Backend gaps all landed by
|
||||
2026-07-04 (PRs #113–#119); merge pass PR #120; live-data screens: #109 Dashboard (PR #123),
|
||||
#84 Channels (PR #124), #86 Schedule editor (PR #125), #87 Playouts (PR #127), #88 Libraries
|
||||
(PR #128), #85 Guide/EPG (PR #129); #62 prerequisites: #65 library browse (PR #130).
|
||||
(PR #128), #85 Guide/EPG (PR #129); #62 prerequisites: #65 library browse (PR #130), #64
|
||||
channel templates (PR #133).
|
||||
|
||||
**PROCESS (2026-07-05, binding)**: Fable writes each Codex prompt AND performs the PR review
|
||||
(via review subagents — parallel lenses: correctness fork + cheaper contract/tests +
|
||||
@@ -18,23 +19,44 @@ subagents too (NITS: Fable fixes on the branch; SUBSTANTIAL: back to Codex verba
|
||||
in-session — user decides). Merges need explicit user consent (standing consent for this
|
||||
stretch: merge when reviewers are happy and tests pass).
|
||||
|
||||
**Session state (2026-07-06, post-#65)**: main = c4007293 (PR #130): `GET /api/library/browse`
|
||||
(`BrowseLibrary`) — unified browse/search over movies/shows/seasons/artists via the existing
|
||||
Lucene ISearchIndex plus manual/smart/multi/rerun collections + playlists via narrow EF
|
||||
projections (they are not Lucene docs), merged paging with a skip cascade, typed picker ids
|
||||
per CollectionType, per-item duration + counts, poster artwork via the existing
|
||||
jellyfin://-emby:// proxy conventions, manual-collection total duration + representative
|
||||
poster, pageNum/pageSize clamped. Fable's 3-lens review (correctness / API-contract /
|
||||
tests+scope) found 2 SUBSTANTIAL (manual-collection artwork+duration silently swept into a
|
||||
deferral that only covered smart/multi/rerun; tests missed exactly the risky logic —
|
||||
cross-source paging, season/artist hydration, libraryId exclusion rule) + 6 nits; Codex fixed
|
||||
ALL in 630119f3, delta re-verified by Fable. #65 CLOSED. Deferred on #65: exact aggregate
|
||||
duration/artwork for smart/multi/rerun (kind badge + item counts only until #89 needs more).
|
||||
Baselines: ErsatzTV.Tests **379**, Core.Tests **493** (+1 skip); web tests **80**;
|
||||
lint/typecheck/build clean. Main checkout sits on docs/59-ui-redesign-brief (fully merged,
|
||||
safe to switch).
|
||||
**RELEASE CHECKPOINT (standing, added 2026-07-06)**: prod cutover to the fork is DONE —
|
||||
prod container `ersatztv` on bumblebee runs `192.168.1.95:3000/timothy/ersatztv:prod`
|
||||
(= v26.3.1, app-identical to upstream 26.3.0); `ersatztv-test` tracks `:latest` (main).
|
||||
Prod only advances on `v*` tags. At every milestone merge, FLAG THE USER: is this slice
|
||||
worth tagging **v26.4.0** (reserved for the first app-change release)? Latest sensible tag
|
||||
point is #91 (cutover); earlier if a stable API slice should reach prod sooner. Tagging
|
||||
needs explicit user consent; NEVER `[skip ci]` a commit you'll tag.
|
||||
|
||||
**Session state (2026-07-06, post-#64)**: main = 4854c45a (PR #133 merged): `ChannelTemplate`
|
||||
domain entity — composition-by-reference (FFmpegProfileId + optional watermark/fallback/
|
||||
pre/mid/post-roll filler refs) plus stream/audio/playout/subtitle/music-video/transcode/idle/
|
||||
schedule defaults; `IsSystem` built-ins ("Standard", "Music videos") seeded idempotently in
|
||||
DbInitializer (user edits survive restarts, verified by test); default-template selection via
|
||||
ConfigElement `channel_templates.default_template_id`; 7 endpoints under /api/channel-templates
|
||||
(list/get/get-default/set-default/create/update/delete; delete rejects system + active
|
||||
default). FIRST FORK SCHEMA CHANGE: dual migrations `AddChannelTemplates` (SQLite
|
||||
20260706155532 / MySQL 20260706155538) + first `IDesignTimeDbContextFactory`
|
||||
(ErsatzTV/TvContextDesignTimeFactory.cs — lets `dotnet ef` run without a live MySQL).
|
||||
Fable review found 3 CONFIRMED (untrimmed-name uniqueness check → DbUpdateException/500;
|
||||
dead `.OrderBy(Name)` before SelectOneAsync in the default fallback; flat folder layout vs
|
||||
contributing §2) + nits; Codex fixed all in 1a61b89f (+ shared ChannelTemplateDefault helper,
|
||||
ConfigElementKey regrouped); deferred: Validation.Apply conversion (would erase NotFoundError
|
||||
→ breaks 404 mapping, stays early-return Option<BaseError>). #64 CLOSED. Templates are
|
||||
applied at channel-create time by #63 — NOT live-linked to channels (so #68 unblocked).
|
||||
Baselines: ErsatzTV.Tests **419**, Core.Tests **493** (+1 skip); web tests **80**. Main
|
||||
checkout sits on docs/59-ui-redesign-brief; .worktrees/issue-64-channel-templates is merged
|
||||
(remove it).
|
||||
|
||||
**Lessons for all remaining prompts** (accumulated):
|
||||
- `SelectOneAsync` re-applies `.OrderBy(keySelector)` INTERNALLY, which REPLACES any ordering
|
||||
the caller composed before it (#133) — never pre-`OrderBy` into SelectOneAsync; write the
|
||||
explicit `.Where(...).OrderBy(...).FirstOrDefaultAsync(...)` when ordering matters.
|
||||
- Normalize user input ONCE (#133): validate uniqueness/lengths against the SAME normalized
|
||||
(e.g. trimmed) value you persist, or a whitespace variant slips past validation and dies on
|
||||
the unique index as an unhandled 500.
|
||||
- Application command/query records + handlers live in `<Domain>/Commands/` and
|
||||
`<Domain>/Queries/` subfolders (contributing §2); namespace stays
|
||||
`ErsatzTV.Application.<Domain>` regardless of subfolder (#133).
|
||||
- Deferral wording must ENUMERATE what is deferred (#130): "aggregate collection metadata is
|
||||
deferred" quietly swallowed manual collections, which are a cheap direct join — the review
|
||||
had to split the deferral. Cheap-vs-expensive is per collection kind, not per feature.
|
||||
@@ -81,6 +103,8 @@ safe to switch).
|
||||
enable`; Application has NO nullable context. NSubstitute+ConfigElementKey:
|
||||
`Arg.Any<ConfigElementKey>()` + `<T>`. `Option<T>.ToNullable()` → `MatchUnsafe`.
|
||||
update-openapi.sh needs a prior normal build. Child GETs 404 unknown parents via pre-check.
|
||||
Validation.Apply ERASES NotFoundError subtypes (#44 gotcha) — multi-check validation that
|
||||
must 404 stays early-return.
|
||||
- Backlog nits (unfiled): unclamped pageSize (browse is clamped; older endpoints aren't);
|
||||
PlayoutController Create/Delete lack Name=; heavy GetItems pre-check; >30 MB uploads →
|
||||
bare 413; artwork content-type trusted (#66); schedule estimator materializes collections
|
||||
@@ -89,114 +113,119 @@ safe to switch).
|
||||
usage in shell.css. From #129: shared programme/playout-item id on /api/guide +
|
||||
/api/channels/state; extract the duplicated channel-state poll loop into a shared helper;
|
||||
EPG grid re-renders unmemoized on every tick; /api/guide 21-include eager-load untrimmed.
|
||||
NEW (#130): the two manual-collection metadata helpers each fetch CollectionItems (share
|
||||
one fetch); very large manual collections make the browse duration sum heavy (bounded per
|
||||
page but per-item MediaVersions loads). Filed: #126 (OpenAPI polymorphism gap).
|
||||
From #130: the two manual-collection metadata helpers each fetch CollectionItems (share
|
||||
one fetch); very large manual collections make the browse duration sum heavy. Filed: #126
|
||||
(OpenAPI polymorphism gap).
|
||||
|
||||
---
|
||||
|
||||
# PROMPT FOR CODEX — #64: Channel Templates (built-in + custom) — first schema change
|
||||
# PROMPT FOR CODEX — #63: composite "create channel from lineup" endpoint
|
||||
|
||||
You are Codex, the IMPLEMENTER, in /Users/timothy/ersatztv (ErsatzTV fork; .NET 10,
|
||||
CQRS/MediatR, LanguageExt, EF Core dual-provider). Fable (Claude) reviews your PR read-only
|
||||
afterwards — do NOT merge. Read CLAUDE.md and docs/contributing.md first; follow the REST
|
||||
API conventions from the #2a foundation (ApiResults, NotFoundError, request DTOs, paged
|
||||
responses) — copy the patterns of existing controllers/handlers and their tests.
|
||||
API conventions from the #2a foundation (ApiResults, NotFoundError, request DTOs) — copy the
|
||||
patterns of existing controllers/handlers and their tests.
|
||||
|
||||
HARD CONSTRAINTS:
|
||||
- USE SUBAGENTS where appropriate, at fitting effort and model levels: cheap/fast agents for
|
||||
mechanical work (DTO/test boilerplate, OpenAPI regen churn, migration scaffolding checks);
|
||||
higher-effort agents for judgment work (template composition model, seeding strategy).
|
||||
Keep orchestration and final assembly in your main session.
|
||||
- Worktree: `git worktree add .worktrees/issue-64-channel-templates -b feat/64-channel-templates origin/main`.
|
||||
mechanical work (DTO/test boilerplate, OpenAPI regen churn); higher-effort agents for
|
||||
judgment work (transaction orchestration, template-defaults stamping).
|
||||
- Worktree: `git worktree add .worktrees/issue-63-composite-create -b feat/63-composite-create origin/main`.
|
||||
Never touch the main checkout or other .worktrees/*. Run `npm ci` in web/ in the worktree
|
||||
if you touch anything web-side (typegen check).
|
||||
if you regen typegen.
|
||||
- Max 2–3 concurrent builds machine-wide; ONE dotnet build at a time here.
|
||||
- NEVER set ETV_UPDATE_GOLDENS. A golden-file diff means your code is wrong.
|
||||
- BACKEND-only: no web/ feature code (regenerating web/src/api/generated/v1.d.ts to prove
|
||||
typegen still works is fine and encouraged).
|
||||
- THIS ISSUE ADDS A DOMAIN ENTITY → migrations in BOTH providers via
|
||||
`scripts/add-migration.sh <Name>` (does SQLite + MySql together; NEVER hand-write or
|
||||
single-provider). CI's `migrations` job enforces model-drift + apply-to-fresh-DB per
|
||||
provider — a red migrations job means your model and migration disagree.
|
||||
- BACKEND-only; regenerating web/src/api/generated/v1.d.ts to prove typegen still works is
|
||||
fine and encouraged.
|
||||
- NO schema change expected — this composes EXISTING entities. If you believe you need a
|
||||
migration, STOP and justify on the issue first.
|
||||
- DTO records in ErsatzTV.Core/Api get file-scoped `#nullable enable`; Application has NO
|
||||
nullable context (`string?` there trips CS8632).
|
||||
nullable context. Command/query files go in `<Domain>/Commands|Queries/` subfolders.
|
||||
|
||||
## Task
|
||||
Issue #64 (part of epic #62; directly serves the #59 goal "better defaults and templates"):
|
||||
a **ChannelTemplate** entity bundling the clunky technical/behavioral channel settings so
|
||||
most channels need zero manual config. Read the FULL issue body first. Deliver:
|
||||
- Domain entity + dual migrations. Design decision to state on the issue BEFORE coding:
|
||||
compose existing entities by REFERENCE (FFmpegProfileId, FillerPresetId for pre/mid/
|
||||
post-roll, watermark?) vs duplicating values — strongly prefer composition-by-reference;
|
||||
survey what a channel + playout actually needs configured (Channel entity, ProgramSchedule
|
||||
defaults, subtitle/shuffle/playout knobs) and pick the bundle deliberately. Fields the
|
||||
issue names: transcode/FFmpeg profile, fillers, interleaving, subtitle behavior, shuffle
|
||||
default, playout defaults.
|
||||
- **Built-in templates** shipped out of the box (e.g. "Standard", "Music videos") AND
|
||||
user-saved custom templates. Investigate how the codebase seeds defaults today (e.g. the
|
||||
default FFmpegProfile) and follow that precedent; built-ins should be non-deletable (or
|
||||
clearly flagged `IsSystem`) and survive upgrades without clobbering user edits.
|
||||
- A way to mark/get the DEFAULT template (pre-selected in the create flow).
|
||||
- REST CRUD: list/get/create/update/delete under /api/channel-templates (+ default
|
||||
selection). Deleting a template must not break channels created from it (templates are
|
||||
applied at create time by #63, not live-linked — state this explicitly in your design
|
||||
comment if you agree, or argue otherwise). 404s via pre-check + ApiResults with
|
||||
OpenApiErrorResponseContractTests entries; route Name= on every endpoint; clamp paging if
|
||||
you page.
|
||||
- DESIGN FOR (don't implement): #63's composite create-channel endpoint consumes a template
|
||||
+ "Advanced" overrides; the SPA create flow (#89) lists templates with the default
|
||||
pre-selected. Keep the DTO shape friendly to both.
|
||||
- If the bundle turns out to have a large tail of fields, land the entity + CRUD + a solid
|
||||
core bundle first and propose the tail as a follow-up on the issue rather than gold-plating.
|
||||
Issue #63 (part of epic #62; the backend contract for the #89 Channel Builder): ONE API
|
||||
operation that atomically creates everything a working channel needs. Read the FULL issue
|
||||
body first. Deliver:
|
||||
- Accept: channel basics (name, number, group, image?, always-on?), `templateId` (from #64's
|
||||
/api/channel-templates; default template pre-selected client-side) + optional "Advanced"
|
||||
overrides for the template's knobs, shuffle/order mode, and an ORDERED lineup of library
|
||||
item references (ids as produced by #65's /api/library/browse picker ids — movies, shows,
|
||||
seasons, artists, collections, playlists; decide + document which kinds v1 accepts).
|
||||
- Create, in ONE transaction: Channel + Collection (from the lineup) + ProgramSchedule +
|
||||
schedule items + Playout. Roll back cleanly on ANY failure — no orphan entities. State
|
||||
your transaction strategy in the design comment (existing handlers SaveChanges eagerly —
|
||||
you will likely need a single-handler orchestration inside one TvContext transaction
|
||||
rather than chaining existing MediatR commands; check how PlayoutBuilder/rebuild is
|
||||
normally triggered post-create and whether it belongs inside or after the transaction).
|
||||
- Apply the selected ChannelTemplate's defaults at create time (stamp values — NOT a live
|
||||
link), with Advanced overrides winning over template values. Missing/deleted templateId →
|
||||
404 via pre-check; invalid lineup ids → 404/422 with the item spelled out.
|
||||
- Validation per the #133 lessons: normalize name ONCE and check uniqueness against what you
|
||||
persist; channel-number collision → clean 422.
|
||||
- Response: the created channel (existing ChannelResponseModel or a small composite response
|
||||
with created ids — decide, justify, keep it #89-friendly).
|
||||
- Route Name= on the endpoint; OpenApiErrorResponseContractTests + ApiErrorResponseMetadataTests
|
||||
entries for every documented 404/422; regenerate v1.json + v1.d.ts.
|
||||
- DESIGN FOR (don't implement): #89 wizard flow (template select → lineup pick → advanced
|
||||
overrides → create); #71 (persistent shuffle) and #77 (clock-boundary padding) may add
|
||||
knobs later — keep the request shape extensible.
|
||||
|
||||
## Context (main = c4007293; baselines: ErsatzTV.Tests 379, Core.Tests 493+1skip, web 80)
|
||||
## Context (main = 4854c45a; baselines: ErsatzTV.Tests 419, Core.Tests 493+1skip, web 80)
|
||||
- Gitea: http://192.168.1.95:3000/timothy/ersatztv (basic auth timothy:ded89Lm4).
|
||||
- After code changes: normal `dotnet build ErsatzTV/ErsatzTV.csproj` FIRST, then
|
||||
./scripts/update-openapi.sh (regen is authoritative); commit the regenerated v1.json.
|
||||
- Related open issue #68 (resume/bookmark channels) may eventually want per-channel behavior
|
||||
flags — do NOT implement, but note in your design comment if the template shape would
|
||||
block or ease it.
|
||||
- Test with the ErsatzTV.Tests harness precedents (NUnit + Shouldly + NSubstitute;
|
||||
InMemoryTvContext for handler tests).
|
||||
- Study the Blazor create flows for what "a working channel" minimally needs (Channel editor,
|
||||
Schedule editor, Playout add) — the composite must produce a channel that actually plays.
|
||||
- Templates: GET /api/channel-templates/{id} + ChannelTemplate entity (see PR #133) — fields
|
||||
cover FFmpeg profile, watermark, fillers, stream/audio/subtitle/music-video/transcode/idle
|
||||
+ schedule defaults (shuffle etc.).
|
||||
- Test with the ErsatzTV.Tests harness precedents (NUnit + Shouldly + NSubstitute; SQLite
|
||||
in-memory harness). Add a rollback test (fail mid-transaction → NOTHING persisted).
|
||||
|
||||
## Process
|
||||
1. Comment on issue #64 with findings + approach (entity shape, composition-by-reference
|
||||
decision, seeding strategy for built-ins, endpoint list with routes/DTOs) BEFORE coding.
|
||||
2. Implement; comment progress on #64 as you go.
|
||||
1. Comment on issue #63 with findings + approach (request/response DTO shape, accepted
|
||||
lineup-id kinds, transaction strategy, playout-build timing, template stamping matrix)
|
||||
BEFORE coding.
|
||||
2. Implement; comment progress on #63 as you go.
|
||||
3. Verify: TZ=UTC dotnet build ErsatzTV.sln; TZ=UTC dotnet test ErsatzTV.Tests then
|
||||
ErsatzTV.Core.Tests sequentially (expect 379+new / 493+1skip); migrations: apply both
|
||||
providers' migrations to a fresh DB if feasible locally (SQLite at minimum) and ensure no
|
||||
model drift; if v1.json changed, cd web && npm ci && npm run generate:api && npm run
|
||||
typecheck (commit the regenerated v1.d.ts).
|
||||
4. Push, open PR → main: "feat(api): channel templates entity + CRUD (#64)", body lists
|
||||
entity design + endpoints + seeding decisions; `closes #64`. Poll CI by head SHA until
|
||||
green — the `migrations` job especially. Do NOT merge.
|
||||
ErsatzTV.Core.Tests sequentially (expect 419+new / 493+1skip); if v1.json changed,
|
||||
cd web && npm ci && npm run generate:api && npm run typecheck (commit v1.d.ts).
|
||||
4. Push, open PR → main: "feat(api): composite create-channel endpoint (#63)", body lists
|
||||
DTO shape + transaction strategy + template-stamping decisions; `closes #63`. Poll CI by
|
||||
head SHA until green. Do NOT merge.
|
||||
|
||||
## On completion — REQUIRED final output
|
||||
Print a fenced handoff prompt addressed to Claude (Fable) asking it to review the PR
|
||||
READ-ONLY (Fable runs its own review subagents). Include: PR number, branch, head SHA, base,
|
||||
files changed, entity/DTO/endpoint table, migration names, verification commands + results,
|
||||
deferred/uncertain list, and the finding classification (NITS = Fable fixes on the branch;
|
||||
SUBSTANTIAL = back to Codex verbatim or fixed in-session with subagents — user decides).
|
||||
After approval + user merge consent, Fable merges, verifies main's post-merge run (image job
|
||||
included), updates THIS handoff (pop #64, next prompt = #63 composite create-channel
|
||||
endpoint, record PR + main SHA + baselines), and pushes it to main.
|
||||
files changed, DTO/endpoint table, verification commands + results, deferred/uncertain list,
|
||||
and the finding classification (NITS = Fable fixes on the branch; SUBSTANTIAL = back to
|
||||
Codex verbatim or fixed in-session with subagents — user decides). After approval + user
|
||||
merge consent, Fable merges, verifies main's post-merge run (image job included), updates
|
||||
THIS handoff (pop #63, next prompt = #89 Channel Builder, record PR + main SHA + baselines),
|
||||
pushes it to main, AND raises the release checkpoint (see standing note above): with #62
|
||||
complete the API surface for #89 is done — ask the user whether to tag v26.4.0 now or wait
|
||||
for the SPA cutover (#91).
|
||||
|
||||
---
|
||||
|
||||
## Issue queue (work top-down)
|
||||
0. HOUSEKEEPING: #99 stays open for the final /api/channels/state onAir wiring; #126
|
||||
(OpenAPI polymorphism gap) is a good backend slot-filler between screens.
|
||||
1. #64 Channel Templates ← CODEX PROMPT above (first schema change of the fork; #63 depends
|
||||
on it; Fable reviews via subagents, merges on consent, updates this file).
|
||||
2. #62 epic continues: #63 composite create-channel endpoint (consumes #64 templates + #65
|
||||
browse) → then #89 Channel Builder (needs #104 artwork upload ✓, #65 browse ✓, first
|
||||
(OpenAPI polymorphism gap) is a good backend slot-filler between screens. Six Renovate/
|
||||
dependency PRs are open (#21, #48, #49, #61, #131 security, #132) — cheap batch-merge
|
||||
pass when convenient. MCP PR #76 (#58 read-only server foundation) predates most of the
|
||||
API surface — needs a rebase/refresh pass; good parallel track once the API stops moving
|
||||
(post-#63).
|
||||
1. #63 composite create-channel endpoint ← CODEX PROMPT above (last piece of epic #62;
|
||||
unblocks #89; Fable reviews via subagents, merges on consent, updates this file, raises
|
||||
the v26.4.0 release checkpoint).
|
||||
2. #89 Channel Builder (needs #63 + #64 ✓ + #65 ✓ + #104 artwork upload ✓; first
|
||||
Dialog/Modal component; languages endpoint follow-up from #105 when needed). #66/#67 as
|
||||
#89 demands.
|
||||
3. #93 Settings screen is dependency-free — usable as a frontend interleave if a backend
|
||||
session needs review turnaround.
|
||||
4. Then: #90 rebrand → #91 cutover.
|
||||
Cross-refs: #99 seam landed (PR #121), final wiring open; #68 reframed (resume/bookmark) —
|
||||
noted in the #64 prompt as design-awareness only. Done recently: PR #129 (#85 Guide/EPG),
|
||||
PR #130 (#65 library browse — closed 2026-07-06, main c4007293). Filed: #126.
|
||||
4. Then: #90 rebrand → #91 cutover (+ tag v26.4.0 at the latest here — see RELEASE
|
||||
CHECKPOINT note).
|
||||
Cross-refs: #99 seam landed (PR #121), final wiring open; #68 unblocked (templates stamp at
|
||||
create time, no live link). Done recently: PR #130 (#65 library browse), PR #133 (#64
|
||||
channel templates — closed 2026-07-06, main 4854c45a, fix delta 1a61b89f).
|
||||
|
||||
Reference in New Issue
Block a user