A person who does not know the app sets up the TV while somebody watches and says nothing. The sheet says what to write down, so the result is a list of issues instead of an impression.
#20 Each settings column is a focus group now. The middle column is shorter
than the left, so "down" past its last row used to search wider and land
back in the left column — "Erweitert" was only reachable by going right,
down, left.
#22 The whole backend path had no coverage, which is why an afternoon
against the real instance found three bugs. Parsing moved out of
DispatcharrRepository into DispatcharrJson and is covered by twelve tests
built from the shapes that instance actually returns: pagination, channels
without a uuid or hidden from output, effective vs plain names and logos,
favourites without the key, inactive output profiles, and all three EPG
envelopes the fork has shipped. Android's stub org.json throws in unit
tests, so the real implementation is on the test classpath.
What no test can reach is written down in tests/dispatcharr-checkliste.md.
#24 The remote's channel tab was only ever read in source. Driven through
the DevTools protocol against the real backend it lists 503 channels with
logos and EPG, the search filters, the club chip switches view, and a star
survives the round trip to the backend and back — set and unset. It also
showed the phone still spelling out "BL: Bayer Leverkusen - VfB Stuttgart,
tipico Topspiel der Woche, 16. Spieltag" where the TV shortens to the
fixture; the payload shortens now too.
#23 The TV says "Der alte Code war abgelaufen — neuer Code 566071643"
instead of swapping the digits without a word.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The favourite count sat at zero while the backend already had two: the rail
read the favourites off the StateFlow inside the composable instead of
collecting it, so it never recomposed — a regression from making favourites
work for both source types. The list collects again.
Real EPG titles broke the fixture shortener in both directions. "BL: VfB
Stuttgart - Saison 25-26" became a fixture against a season, and
"… FC Bayern München - VfB Stuttgart - 1. Halbzeit" grew a third team; a
season, a matchday, a half or a year is now rejected, and a second dash ends
the away side. The kick-off chip also gave way in the row: the line below
already carries the time, and "Bayer Leverkusen - VfB Stuttgart" needed the
width to fit at all.
Merged provider EPGs list the same programme several times, minutes apart —
the day plan showed The Big Bang Theory three times and two entries marked
as running at once. XmltvParser.collapseDuplicates folds a repeat into the
entry whose slot it falls inside.
The login code expired before anyone could confirm it: Authentik hands out a
minute, and the TV dropped back to step one without a word. It fetches a
fresh code instead, up to eight times.
Tested against the household's own Dispatcharr through api.tv.beckm4nn.net.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Defects. "Kopplung zurücksetzen" now asks first, naming how many phones
would have to scan again — one stray OK used to wipe a working pairing, and
the settings put the focus right on it: entering the screen landed on the
gear, one press down jumped into the right column onto that very action, and
every further press down did nothing. Focus now starts on the first row of
the left column. 1. FC Köln was labelled "EFC". An unreachable source used
to look like an evening without programmes — cached list, empty EPG, club
menus counting down to zero — so both repositories now record when they last
came through, and the top bar says "Server nicht erreichbar · Stand 14:46".
The field labels on "Erweitert" sat on the outline with the border running
through them; they sit above their field now.
Polish. The app opens on the channel list — on the channel last watched,
which survives a restart — instead of the settings gear. Rows without EPG
fill their empty half with the channel's group. The club chip says "ab
15:06" like the row next to it, the club menu carries the club's name
instead of "FCSP", and a fixture title is shortened to the fixture, so
"Pokal: Nordstadt - FC St. P…" became "Nordstadt - FC St. Pauli".
The settings are three columns and no longer scroll (CONTEXT.md's ten-foot
rule).
Features. Favourites work for a plain M3U source too, stored on the device
and mirrored to the phone; they used to exist only with a Dispatcharr login.
The channel keys jump the list by initial letter, left and right jump a
division in the club picker, and right on a channel opens its day plan —
the EPG beyond now/next, which was parsed all along and never shown. When a
club match starts elsewhere the player offers to switch. The remote asked
for a code "unten auf dem TV-Bildschirm" where none was; the welcome screen
shows it now and the wording points at where it is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SettingsScreen and LiveScreen had grown to 760 and 689 lines and were where
most of the last few days' bugs lived. Each is now the screen itself plus
the pieces it composes — club chooser, pickers and rows next to settings,
channel row and cells next to the live list. No behaviour changes: the same
composables, moved, with the visibility widened from private to internal
where a caller now sits in another file.
formatClock existed twice, once per screen, and the move turned that into a
compile error rather than a quiet duplicate. It is one internal function now.
The smoke test seeds the demo playlist into the app's preferences before
launching, so it walks the channel list and the player instead of stopping
at the onboarding screen — the screens this commit moves are now actually
exercised. It also refuses to start when its port is already taken: a
leftover server from an earlier session answered every request with a 404
and the wait loop span forever.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rate limit, the version compare and the download check were all wrapped
in Android — a Context, a socket, a file — so none of them had a test, and
all three shipped broken: the remote thrown out after five reconnects
(0.11.1), a truncated APK handed to the installer (0.11.5), and a version
compare that would read 0.11.10 as older than 0.11.9 the moment we get
there.
They are now plain Kotlin in AttemptBudget and UpdateRules, with the clock
injected, and 20 tests covering the failures themselves. AttemptBudget also
prunes expired entries before evicting: dropping only empty queues let the
map grow without bound while every tracked address held one fresh failure.
tests/smoke.sh installs the debug APK on the headless emulator, walks the
first screen with the D-pad and fails on a crash or ANR — the class of bug
unit tests cannot see, and the reason 0.11.0 crashed on startup. Forty
seconds end to end.
tools/release.sh does the bump, tests, signed build, tag, apk branch and
Gitea release in one command. It refuses an unsigned build and never writes
the release notes itself; commit subjects are offered as a draft.
tests/helpers/jdk.sh picks a JDK Gradle can run on: Ubuntu moved
default-java to 25, which fails the build with a bare
"IllegalArgumentException: 25.0.4".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Google TV emulator was launched by hand with -no-snapshot, which forces
a cold boot every time. tests/helpers/emulator.sh boots from the AVD's
default_boot snapshot (~7s) and refreshes that snapshot on stop, so the next
run stays fast. It also wraps the two things a test run needs afterwards:
installing an APK and pulling a screenshot into tests/runs/.
CONTEXT.md's example dialogue still described Xtream credentials entered
through the Remote, which ADR-0005 and ADR-0007 replaced with device flow and
phone-side onboarding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The club chooser was a flat list of 56 names in a generic picker
dialog, which is unreadable from a couch. It now has its own dialog
with a crest per row, the short label on the right, and section
headers per division. Enabled clubs in the settings card show their
crest too.
A search field would have been the obvious alternative, but text
entry on a remote is exactly what the Ten-Foot rule rules out, so
grouping plus crests carries the recognition instead.
Also adds tests/demo, a generator for neutral playlist and EPG data
so screenshots never carry real channel names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A three-hour window was empty most of the day, which is exactly when
someone wants to know whether their club plays tonight. The scan now runs
until midnight, never less than three hours, so a late-evening question
still covers the rest of the night.
A channel can carry several of the club's broadcasts in one evening — a
preview at 18:20 and the match at 21:45 on the same station. The row names
the next one and appends "+n weitere" instead of silently hiding the rest,
so the list stays a channel list where one click means one station.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The club shortcut now offers every club of the first three German
divisions instead of two hard-coded ones. Up to three can be active at
once; benjamin and tobiasb still start with Hansa and the VfB switched on.
Also fixes a crash on the left jump out of the channel list: the jump
targeted a FocusRequester bound to the selected rail row, and a row that
is scrolled out of a LazyColumn is not composed, so requesting focus on it
threw. Jumps are now attempted and fall back to normal focus movement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an optional rail entry per club that lists every channel whose EPG
mentions the club within the next three hours, with the matching kick-off
instead of the usual Now/Next. Only sport and free-to-air groups are
scanned (NFL excluded), so the scan stays cheap and quiet.
Which clubs appear is a per-TV setting: the signed-in viewer's own club is
on by default, and every club can be switched on or off under Einstellungen
-> Vereinsmenüs, so a household can follow more than one.
Crests are trademarks, so the icon is a plain shield in the club colours.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Control server security and robustness:
- Socket read timeout, handshake deadline and a client cap so an idle or
hostile connection can no longer pin threads forever (#1)
- Per-address rate limiting that counts every failed hello, Origin
checking on the upgrade, and a separate revocable session token for
code-authenticated clients so the guessable path no longer yields the
QR credential (#2)
- Credentials excluded from cloud backup and device transfer, constant
time comparisons, and a pairing reset in the settings (#3)
- Playlist fetches restricted to http(s), capped at 24 MB and bounded by
an overall transfer deadline (#4)
- Port conflicts and MediaSession id collisions no longer crash the app;
the remote degrades to unavailable with a plain-language note (#11)
Player:
- Seeking no longer collapses to position 0 when the duration is unknown
(#5)
- Pause acts on playWhenReady, so pausing during a stall works and
playback cannot resume in the background after leaving the app (#6)
- Playback failures stay on screen with a retry action instead of
silently dropping back to the list (#7)
- Reconnects are spaced 1s/3s/8s and re-entering the channel just closed
waits out a short grace period, which is what the provider needs to
release the previous session (#12)
Channel list and remote:
- Leaving playback returns to the channel the viewer came from (#13)
- The remote only rebuilds its list when the data changed, never
overwrites a focused input and carries indices instead of scanning (#8)
- Pairing retry reconnects properly, resets its backoff and validates the
code before spending an attempt (#9)
- M3U parsing keeps commas in names, strips a BOM and rejects payloads
that are not playlists, covered by unit tests under tests/ (#10)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>