App v0.2.26 and gateway 0.1.20

Client: seek controls, Bazarr subtitle download and cast panel in the
player; MDBList ratings strip; episode and schedule detail pages; series
pace estimate; what's new panel; install-permission onboarding step;
synced per-profile preferences; Emby outage banner.

Gateway: rebuilt admin console (one fragment per page), preference
history and restore, merged Continue Watching, Emby health probe,
subtitle selection and Bazarr download, structured request logging with
per-request identity, and embedded build version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ponzischeme89
2026-08-06 22:33:56 +12:00
co-authored by Claude Opus 5
parent 2675e6d82b
commit 4a4df7a73c
257 changed files with 24868 additions and 3108 deletions
+81
View File
@@ -99,6 +99,87 @@ func TestTracearrSessionTerminalRequiresCompletedState(t *testing.T) {
}
}
func TestImportDueMeasuresElapsedTimeRatherThanProcessUptime(t *testing.T) {
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
at := func(d time.Duration) *time.Time {
value := now.Add(-d)
return &value
}
const incremental = 5 * time.Minute
const full = 24 * time.Hour
for _, testCase := range []struct {
name string
state store.TracearrImportState
wantFull bool
wantDue bool
wantSkip bool
}{
{
name: "never imported runs a full pass",
state: store.TracearrImportState{}, wantFull: true, wantDue: true,
},
{
// The restart case: without persisted stamps every boot re-imported.
name: "recent import is not repeated after a restart",
state: store.TracearrImportState{
LastIncrementalAt: at(30 * time.Second), LastFullAt: at(2 * time.Hour),
},
wantSkip: true,
},
{
name: "elapsed incremental interval is due",
state: store.TracearrImportState{
LastIncrementalAt: at(6 * time.Minute), LastFullAt: at(2 * time.Hour),
},
wantDue: true,
},
{
// A gateway restarted daily never reconciled: the full ticker restarted too.
name: "elapsed full interval wins over the incremental one",
state: store.TracearrImportState{
LastIncrementalAt: at(6 * time.Minute), LastFullAt: at(25 * time.Hour),
},
wantFull: true, wantDue: true,
},
{
name: "a stamp in the future does not strand the importer",
state: store.TracearrImportState{
LastIncrementalAt: at(-time.Hour), LastFullAt: at(-time.Hour),
},
wantFull: true, wantDue: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
gotFull, gotDue := importDue(testCase.state, now, incremental, full)
if testCase.wantSkip {
if gotDue {
t.Fatal("import ran when none was owed")
}
return
}
if gotDue != testCase.wantDue || gotFull != testCase.wantFull {
t.Fatalf("full=%v due=%v, want full=%v due=%v",
gotFull, gotDue, testCase.wantFull, testCase.wantDue)
}
})
}
}
func TestImportDueRespectsDisabledIntervals(t *testing.T) {
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
if _, due := importDue(store.TracearrImportState{}, now, 0, 0); due {
t.Fatal("import was due with scheduling turned off")
}
stale := now.Add(-48 * time.Hour)
state := store.TracearrImportState{LastIncrementalAt: &stale, LastFullAt: &stale}
full, due := importDue(state, now, 5*time.Minute, 0)
if !due || full {
t.Fatalf("full=%v due=%v, want an incremental pass with full reconciliation off",
full, due)
}
}
func TestNextDailyRebuildUsesConfiguredLocalHour(t *testing.T) {
location := time.FixedZone("NZST", 12*60*60)
now := time.Date(2026, 7, 31, 5, 30, 0, 0, location)