Commit Graph
26 Commits
Author SHA1 Message Date
timothyandClaude Opus 4.8 7e6861a928 fix(434): accurate endpoint description (DB-sourced) + tests for case-insensitivity, tag exclusion, distinct
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m43s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m50s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:55:32 +02:00
timothyandClaude Opus 4.8 b4ae0bde18 feat(434): distinct-values search endpoint (text fields, Lucene term enumeration)
Adds GET /api/v1/search/fields/{name}/values?q=&limit= — the backend slice of the
visual rule builder's facet-value typeahead (#434). Enumerates distinct Lucene term
values for a text field via MultiFields.GetTerms + TermsEnum, filtered by a
case-insensitive prefix, limit clamped to [1,50]. 404s when the field is absent from
SearchFieldCatalog or is not type "text". ElasticSearchIndex (the optional external
backend) throws NotSupportedException for this method — its text fields are analyzed,
not keyword-mapped, so a terms aggregation isn't safe to guess at without verifying
against a live cluster.

Regenerated OpenAPI trio (v1.json, v1.d.ts, endpoint-index.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:43:27 +02:00
timothy ae22107d98 feat(176): GET /api/v1/search/fields endpoint + regenerated api artifacts 2026-07-18 02:41:50 +02:00
timothyandClaude Opus 4.8 0009607a09 feat(384): auto-tune DetailPanel content-source member read endpoint
GET /api/v1/channels/auto-tune/members lists the distinct content sources a
proposed auto-tune channel's server-generated SmartCollection resolves to —
parent shows for the TV axes (ItemCount = query-matching episodes), movies for
the movie-genre axis — reusing the existing PagedLibraryBrowseItemsResponseModel
(no new schema). The handler runs the server-owned AutoTuneAxisMap.GenerateQuery
through ISearchIndex (client never sends Lucene, per #69 PR1) and rolls matching
leaf items up to their distinct sources, mirroring GetSmartCollectionItems so the
DetailPanel preview matches what the built playout will contain.

Backend child of #383 (Auto-Tune DetailPanel milestone); read-only, cold review
acceptable. Handler + controller tests (9 new). OpenAPI + endpoint-index
regenerated; d.ts unchanged (reuses existing schema). Docs: api-conventions §5,
decisions.md 2026-07-17, domain-model.

fixes #384

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 09:40:44 +02:00
timothyandClaude Opus 4.8 238937fa5b chore(71): regenerate OpenAPI + client for reshuffle endpoint + seed field
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:19:46 +02:00
timothy 938e4e0f50 chore(69): regenerate endpoint-index after rebase on origin/main
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 5m21s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 8m7s
Union of #340 IPTV-settings endpoints + auto-tune endpoints.

Refs #69
2026-07-16 22:20:53 +02:00
timothy 0320735f47 docs(69): auto-tune OpenAPI regen + api/domain/decisions docs
Refs #69
2026-07-16 22:18:52 +02:00
timothyandClaude Opus 4.8 3bc8192d3b feat(iptv): add configurable advertised base URL for M3U/XMLTV (fixes #340)
ErsatzTV built every absolute M3U/XMLTV URL from the incoming request's
Scheme/Host/PathBase, so a client fetching via a host that downstream
consumers can't resolve (e.g. Dispatcharr over Docker DNS → Kodi) baked
that internal host into programme-image/stream URLs.

Add an optional advertised IPTV base URL, backed by the existing
ConfigElement key/value store (key `iptv.base_url`, no EF migration):

- Central pure Core helper `AdvertisedBaseUrl` (TryParse/Resolve):
  validates absolute http(s), no credentials/query/fragment, preserves
  port + path prefix, normalizes trailing slash. Blank/invalid falls
  back to the request-derived values, so unset output is byte-identical.
- Resolved inside `GetChannelPlaylistHandler` (M3U guide/logo/stream) and
  `GetChannelGuideHandler` (both XMLTV {RequestBase} sites) — controllers
  stay thin, golden tests untouched.
- New `iptv` settings group: GET/PUT /api/v1/settings/iptv (blank clears,
  malformed → 422) + a new IPTV section on the SPA Settings screen.
- Scoped to M3U + XMLTV; HDHomeRun deliberately out of scope. Distinct
  from ETV_BASE_URL (which only sets ASP.NET PathBase).

Tests: AdvertisedBaseUrl unit tests (override/fallback/port/path/invalid),
handler override tests for both generators, settings controller + handler
tests, SPA client + screen tests. Docs: m3u-xmltv, decisions, domain-model,
regenerated OpenAPI v1.json + v1.d.ts + endpoint-index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:08:42 +02:00
timothyandClaude Opus 4.8 ef2bd65c27 feat(api): #286 — mount the whole /api surface at /api/v1
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 3m4s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Version every /api route to /api/v1 (251 controller routes + ~24 Location
headers + the scanner callback URL + the Startup request-log literal),
uniform across the machine API, auth, scanner and scripted-build surfaces.

Add ApiVersionRewriteMiddleware: a legacy unversioned /api/* request is
rewritten (NOT redirected) to /api/v1/* in-pipeline — method, body, auth
headers and query survive — carrying RFC 8594 Deprecation/Sunset headers,
so curl / the future MCP server / bookmarks keep working. An already-
versioned path passes through; a future /api/v2 is never forced to v1.

Standardize the route convention (leading-slash absolute route per method,
no class-[Route] — except the two Scanner/Scripted controllers whose ~all
actions share a parametrized {id} prefix), enforced by ApiRouteVersioningTests
(^/api/v\d+/ over the whole Controllers.Api surface; browser-nav
/auth/oidc/login is out of scope).

Regenerate v1.json (160 paths, all /api/v1)/endpoint-index/v1.d.ts; sweep 945
SPA request literals + the test mocks (regex + positional URL parsers). /api/v1
is additive-only after freeze; the legacy-rewrite shim sunsets in ~2 releases
(owner decision) with removal tracked as a Phase-3 follow-up.

Docs: decisions.md 2026-07-13, api-conventions §1/§9, rest-api/spa-conventions/
blazor-route-parity/e2e-local/domain-model.

fixes #286
refs #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:30:20 +02:00
timothyandClaude Opus 4.8 ec26e1be5b fix(api): #316 review — POST-ify graphics-elements refresh, LockedError→409, no-store machine-key
- GET /api/graphics-elements no longer side-effects; refresh moved to
  POST /api/graphics-elements/refresh (204), closing a CSRF vector on a GET.
- PrepareTroubleshootingPlaybackHandler now returns a typed LockedError from
  both atomic lock-acquire failures; ApiResults.ToErrorResult maps it to 409
  instead of falling through to 422, so a lock lost in the race between the
  controller's pre-check and the handler's atomic acquire still reports 409.
- AuthController.MachineKey sets Cache-Control: no-store + Pragma: no-cache
  on the 200 response carrying the master API key.
- Reworded the stale "subtitleId query parameter" endpoint description now
  that playback/start takes a JSON body.
- Regenerated openapi/v1.json + docs/endpoint-index.md; docs/api-conventions.md
  updated with the LockedError pattern (§3a) and the ToErrorResult table row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:50:42 +02:00
timothyandClaude Opus 4.8 88d82266b6 feat(api): #301 POST-ify side-effecting troubleshoot GETs + GET /api/auth/machine-key
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:50:24 +02:00
timothyandClaude Opus 4.8 6d31758cca feat(api): #271 collections scan-status REST surface + authoritative SPA reconcile
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Add GET /api/media-sources/collections-scan-status (MediaSourcesController →
GetCollectionsScanStatus handler) reporting which media-source families
(plex/jellyfin/emby) currently hold their external-collections scan lock,
reading IEntityLocker.Are{X}CollectionsLocked(). The lock is family-global
(no source id) and boolean (no percent), so the DTO carries just {family} and
returns only active families — the counterpart to GET /api/libraries/scan-status.

SPA: useCollectionsScan now polls this endpoint and reconciles optimistic
pending against the active-family set (seeding on mount so an in-progress scan
disables buttons immediately), using the same grace-tick helper as library
scans (now generic over the pending key type). Drops COLLECTIONS_PENDING_TIMEOUT_MS
— a long deep scan no longer re-enables the button early, and a fast scan no
longer wedges it disabled for the full timeout. A row shows Scanning when its
family is active or it has an in-grace optimistic pending key.

Tests: handler (3), controller route+delegation (2), SPA api fn + hook reconcile
(mount-seed / 202-promote / 409-keeps-disabled / 404-error). OpenAPI + TS types
regenerated. Docs: api-conventions §3b, blazor-route-parity §5, decisions.md.

Unblocks #91b (arc item 4): Libraries.razor's collections-scan affordance now
has full authoritative parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:23:58 +02:00
timothyandClaude Opus 4.8 c40e78d840 fix(api): #197 Bundle C review nits — order-independent operationIds + nullable MediaSources fields
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m36s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m21s
Refs #287 #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:08:50 +02:00
timothyandClaude Opus 4.8 70f357f8f9 feat(api): #288 ChannelDetailResponseModel for edit form + SPA repoints + final regen
Mint ChannelDetailResponseModel (faithful detail DTO exposing the raw editable
field set the channel editor reads: raw FFmpegProfileId/WatermarkId/FallbackFillerId
ids, the mode enums, logo, playoutCount, id) and route GetById/Create/Update through
it, replacing the lean list ChannelResponseModel that resolved the profile to a name
and dropped the editable ids (a functional regression for draftFromChannel). The lean
ChannelResponseModel stays unchanged for GET /api/channels. webEncodedName dropped
(SPA never reads it). Logo is mirrored as a Core ChannelLogoResponseModel since the
Application ArtworkContentTypeModel can't be referenced from Core.

Repoint the hand-written SPA client aliases now that the VMs are gone from the schema:
Channel -> ChannelDetailResponseModel, MediaCollection/SmartCollection -> *ResponseModel,
ProgramSchedule -> ProgramScheduleResponseModel. Fix #288 honest-nullability test fallout
in search.test.ts (null -> [] for now-non-null id arrays). Include the already-on-disk
playouts.ts WithDayNames removal and regenerate v1.json + v1.d.ts + endpoint-index.md
(authoritative final regen; the reset endpoint's {channelNumber}->{id} re-key surfaces
in the generated docs and the OpenApi error-contract test).

Refs #288 #197

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 02:33:39 +02:00
timothyandClaude Opus 4.8 37155c866b security(#197): fail-closed API auth, sensitive-read tier, CORS/ForwardedHeaders lockdown (Bundle A)
Backend of #197 Bundle A (auth posture). Owner decisions: single API key;
Api:RequireKeyForReads defaults true (whole /api surface gated; /iptv streaming
+ guide unaffected — outside the filter's /api scope).

- #280 S1: writes are fail-closed. New IApiKeyProvider resolves the key once
  (Api:WriteKey config, else persisted /config/api.key, else a generated 256-bit
  key written 0600). The empty-key open branch is gone; there is no open mode.
- #282 S3/S5: reads under /api require the key when Api:RequireKeyForReads (default
  true) or the endpoint carries the new [RequiresApiKey]. Applied [RequiresApiKey]
  to Troubleshoot/Logs/Settings/Maintenance so the sensitive tier stays gated even
  if reads are opened. OPTIONS preflight is exempt.
- #281 S2: delete SortController (dead Blazor SortableJS residue; SPA uses PUT
  /api/collections/{id}/custom-order) and AccountController (dead OIDC logout) —
  both non-/api persistent surfaces that bypassed the key.
- #284 S6: replace CORS AllowAll with an opt-in exact-origin allowlist
  (Api:CorsAllowedOrigins; permits X-Api-Key/If-Match, exposes ETag). Default is
  no cross-origin (SPA is same-origin).
- #285 S7/S10: gc GET->POST (spec regenerated); ForwardedHeaders trust configurable
  via ForwardedHeaders:KnownProxies/KnownNetworks (warns when unrestricted);
  ScannerController gains [LocalhostOnly] (scanner always calls back over localhost).

Filter unit tests rewritten for fail-closed + read-gating + tier + OPTIONS;
ApiControllerSecurityTests assert the sensitive tier + scanner-loopback reflectively.
search/all-items paging deferred (SPA add-all coupling) — exposure closed by read-gating.

Refs #197 #280 #281 #282 #284 #285

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:03:55 +02:00
timothyandClaude Opus 4.8 a27cfc475e chore(235): regenerate OpenAPI artifacts + endpoint index after slice merge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:04:33 +02:00
timothy c6ca87c06e chore(api): regenerate OpenAPI + TS client for media-source write endpoints (#202 OpenAPI gate) 2026-07-11 15:52:31 +02:00
timothyandClaude Fable 5 223797a6cb docs+openapi: regenerate v1.json/endpoint-index/types + record flat schedule DTO decision (#126 #207 #212)
- Regenerated ErsatzTV/wwwroot/openapi/v1.json, docs/endpoint-index.md, and
  web/src/api/generated/v1.d.ts for the flat schedule-item DTO + discovery endpoints
- docs/decisions.md: flat ScheduleItemResponseModel + NamedIdResponseModel rationale
- docs/api-conventions.md: shared {id,name} embed, polymorphic-VM flattening, and
  optional enum query-filter conventions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:54:12 +02:00
timothy 819e751cab feat(api): playlist add-items + search all-items endpoints (#208 #209)
- POST /api/playlists/{id:int}/items wraps the existing AddItemsToPlaylist
  command (mirrors CollectionController.AddItems); controller pre-checks
  playlist existence for a real 404, and the handler now rejects adds to
  system (generated) playlists, matching the guard already applied to
  rename/delete/replace-items so the Blazor path gets the same protection.
- GET /api/search/all-items wraps the existing QuerySearchIndexAllItems
  query, returning a new SearchResultAllItemsResponseModel (never expose
  the VM directly) so the SPA's shared "add all to collection/playlist"
  component can materialize ids before calling the add endpoints, same
  two-step flow Blazor's Search.razor already uses.
- Show-detail DTO check: ShowDetailResponseModel already exposes
  libraryId, title, and mediaSourceKind (serialized as a string enum via
  the global StringEnumConverter) - no changes needed.

Adds controller tests (route table + per-action) for both endpoints and
regenerates the OpenAPI document, endpoint index, and SPA client types.
2026-07-09 23:56:50 +02:00
timothyandClaude Fable 5 f1b3879d51 feat(api): playout erase/scheduling-context + collection custom-order (#210, #211)
Backend slice for the ChicoryTV playouts and collections screens.

PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
  Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
  Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
  playout item's stored context by row id via a new
  GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)

CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
  UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
  when the collection's UseCustomPlaybackOrder is set

Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:05:51 +02:00
timothyandClaude Fable 5 5815e4b437 feat(api): add troubleshooting stream-selectors, subtitles, and status endpoints
Add three GET endpoints to TroubleshootController for the SPA port of the
playback troubleshooting page:
- /api/troubleshoot/playback/stream-selectors (List<string>)
- /api/troubleshoot/playback/subtitles/{mediaItemId} (404 pre-check via
  GetMediaItemInfo; maps SubtitleViewModel -> TroubleshootingSubtitleResponseModel)
- /api/troubleshoot/playback/status (TroubleshootingPlaybackStatusResponseModel:
  idle/running/completed/failed + exitCode/speed + logs.txt tail)

Regenerate v1.json, endpoint-index.md, and the web API types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:46:35 +02:00
timothyandClaude Opus 4.8 7d5850214e feat(api): playlist CRUD + item management + preview (#153 backend)
Add write endpoints to PlaylistController for the playlist editor: group
create/rename/delete, playlist create/read/update/delete, item list read,
and draft playout preview. Introduces a RenamePlaylistGroup command/handler
(the one missing Application-layer operation) plus request/response DTOs.

Endpoints (verb / route / route-Name):
- POST   /api/playlists/groups        CreatePlaylistGroup
- PUT    /api/playlists/groups/{id}    UpdatePlaylistGroup (rename)
- DELETE /api/playlists/groups/{id}    DeletePlaylistGroup
- GET    /api/playlists/{id}           GetPlaylistById
- GET    /api/playlists/{id}/items     GetPlaylistItems
- POST   /api/playlists                CreatePlaylistInGroup
- PUT    /api/playlists/{id}           UpdatePlaylist (rename + replace items)
- DELETE /api/playlists/{id}           DeletePlaylist
- POST   /api/playlists/preview        PreviewPlaylist

404-vs-422: unknown-id on GET items / PUT / DELETE returns 404 via a
controller-side existence pre-check (mirrors TemplateController.DeleteGroup),
leaving existing shared handlers untouched; validation failures return 422.
RenamePlaylistGroup returns NotFoundError -> 404 for a missing group.

Regenerated wwwroot/openapi/v1.json, docs/endpoint-index.md, and the SPA
client types (web/src/api/generated/v1.d.ts). No SPA screen in this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:52:07 +02:00
timothy 282f59ea60 Merge remote-tracking branch 'origin/main' into feat/multi-rerun-collections-api
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m15s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
# Conflicts:
#	docs/blazor-route-parity.md
#	docs/endpoint-index.md
2026-07-08 18:43:08 +02:00
timothyandClaude Opus 4.8 cf85bf3093 docs: regenerate endpoint-index after merging main (includes GET collection items) (#155)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m27s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m45s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:28:20 +02:00
timothyandClaude Opus 4.8 a496dad9c9 feat(api): REST CRUD for multi-collections and rerun-collections (#151, #152)
Adds MultiCollectionController + RerunCollectionController (list/get/create/update/
delete) over the existing MediatR handlers, with response/request DTOs, controller +
contract tests, regenerated OpenAPI + TS types, and thin SPA api-client modules. SPA
editor screens are a follow-up. Rerun request DTO resolves the chosen entity id into
the minimal VM the handler reads (documented inline).

Refs #151, #152

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:18:32 +02:00
timothyandClaude Opus 4.8 0290594f0b docs: testing map + generated endpoint index (#185)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m45s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adds docs/testing.md as the authoritative testing map (consolidated
from docs/contributing.md §8, now shrunk to a pointer), and a
generated docs/endpoint-index.md via scripts/generate-endpoint-index.py
(hooked into scripts/update-openapi.sh). Updates docs/README.md's
reading order and removes the "still to come" placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 07:38:47 +02:00