Fix false "Plex fallback" banner during the first scan
history_coverage is only written at the end of an ingest, but has_completion_data was reading from it. During the first scan the coverage table is empty while watch_event already holds tens of thousands of Tautulli rows, so the dashboard announced a fallback that had not happened and claimed the rejection component was disabled when it was not. The flag now comes from the events themselves — does any row carry a percent_complete — which is true the moment Tautulli rows land and false for Plex-only history. history_source falls back to the scan record when coverage is absent, so it reads "tautulli" mid-scan instead of null. Also: the banner named Plex as the source without checking, and now reports whichever source is actually active, and stays quiet while a scan is running since the counts are still moving. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVbG48GAXfCZatcmX123Ra
This commit is contained in:
parent
a842aeb344
commit
71cfa31f09
4 changed files with 55 additions and 10 deletions
|
|
@ -212,7 +212,7 @@ def sources():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"plex": {"configured": c.plex_configured, "base_url": c.plex_base_url},
|
"plex": {"configured": c.plex_configured, "base_url": c.plex_base_url},
|
||||||
"tautulli": {"configured": c.tautulli_configured, "base_url": c.tautulli_base_url},
|
"tautulli": {"configured": c.tautulli_configured, "base_url": c.tautulli_base_url},
|
||||||
"history_source": (cov[0]["source"] if cov else None),
|
"history_source": queries.active_history_source(d),
|
||||||
"has_completion_data": queries.history_has_completion(d),
|
"has_completion_data": queries.history_has_completion(d),
|
||||||
"coverage": cov,
|
"coverage": cov,
|
||||||
"providers": [dict(r) for r in d.query("SELECT * FROM provider")],
|
"providers": [dict(r) for r in d.query("SELECT * FROM provider")],
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,26 @@ def _score_ctx(db, cfg, has_completion_data: bool) -> scoring.ScoreContext:
|
||||||
|
|
||||||
|
|
||||||
def history_has_completion(db) -> bool:
|
def history_has_completion(db) -> bool:
|
||||||
"""True when the active history source records percent_complete (§4.11)."""
|
"""True when the ingested history actually carries percent_complete (§4.11).
|
||||||
row = db.one(
|
|
||||||
"SELECT source FROM history_coverage ORDER BY event_count DESC LIMIT 1"
|
Read from the events themselves, not from history_coverage: that row is only
|
||||||
)
|
written at the END of an ingest, so during the first scan the coverage table
|
||||||
return bool(row and row["source"] == "tautulli")
|
is empty while watch_event is already full of Tautulli rows. Deriving the
|
||||||
|
flag from the table made the UI announce a Plex fallback that had not
|
||||||
|
happened, mid-scan, on every fresh database.
|
||||||
|
"""
|
||||||
|
return bool(db.scalar(
|
||||||
|
"SELECT 1 FROM watch_event WHERE percent_complete IS NOT NULL LIMIT 1"))
|
||||||
|
|
||||||
|
|
||||||
|
def active_history_source(db) -> str | None:
|
||||||
|
"""Which source history came from — coverage if it exists, else the scan."""
|
||||||
|
row = db.one("SELECT source FROM history_coverage ORDER BY event_count DESC LIMIT 1")
|
||||||
|
if row:
|
||||||
|
return row["source"]
|
||||||
|
row = db.one("SELECT history_source FROM scan WHERE history_source IS NOT NULL "
|
||||||
|
"ORDER BY id DESC LIMIT 1")
|
||||||
|
return row["history_source"] if row else None
|
||||||
|
|
||||||
|
|
||||||
BASE_COLUMNS = """
|
BASE_COLUMNS = """
|
||||||
|
|
@ -273,7 +288,7 @@ def overview(db, cfg) -> dict:
|
||||||
"episodes": s("SELECT COUNT(*) FROM episode WHERE status='present'"),
|
"episodes": s("SELECT COUNT(*) FROM episode WHERE status='present'"),
|
||||||
"watch_events": s("SELECT COUNT(*) FROM watch_event"),
|
"watch_events": s("SELECT COUNT(*) FROM watch_event"),
|
||||||
"accounts": s("SELECT COUNT(*) FROM account"),
|
"accounts": s("SELECT COUNT(*) FROM account"),
|
||||||
"history_source": cov["source"] if cov else None,
|
"history_source": active_history_source(db),
|
||||||
"history_since": cov["earliest_event_at"] if cov else None,
|
"history_since": cov["earliest_event_at"] if cov else None,
|
||||||
"history_until": cov["latest_event_at"] if cov else None,
|
"history_until": cov["latest_event_at"] if cov else None,
|
||||||
"has_completion_data": history_has_completion(db),
|
"has_completion_data": history_has_completion(db),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const state = {
|
||||||
views: [],
|
views: [],
|
||||||
lastPage: null,
|
lastPage: null,
|
||||||
hasCompletion: true,
|
hasCompletion: true,
|
||||||
|
scanning: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── helpers ──────────────────────────────────────────────────────── */
|
/* ── helpers ──────────────────────────────────────────────────────── */
|
||||||
|
|
@ -162,9 +163,11 @@ async function loadDashboard() {
|
||||||
? 'Completion data available — the rejection component is active'
|
? 'Completion data available — the rejection component is active'
|
||||||
: 'No completion data — running degraded, rejection component disabled';
|
: 'No completion data — running degraded, rejection component disabled';
|
||||||
}
|
}
|
||||||
if (!ov.has_completion_data && ov.watch_events > 0) {
|
// Only a finished scan can tell us this; mid-scan the counts are still moving.
|
||||||
banner('warn', 'Watch history has no completion data (Plex fallback). ' +
|
if (!ov.has_completion_data && ov.watch_events > 0 && !state.scanning) {
|
||||||
'The score is running degraded: the “rejection” component is disabled.');
|
banner('warn', `Watch history has no completion data (source: ` +
|
||||||
|
`${ov.history_source || 'unknown'}). The score is running degraded: ` +
|
||||||
|
`the “rejection” component is disabled.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// size by library
|
// size by library
|
||||||
|
|
@ -727,6 +730,7 @@ $('#scan-now').addEventListener('click', async () => {
|
||||||
async function pollScan() {
|
async function pollScan() {
|
||||||
const btn = $('#scan-now');
|
const btn = $('#scan-now');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
state.scanning = true;
|
||||||
const tick = async () => {
|
const tick = async () => {
|
||||||
const s = await api('/scans/current');
|
const s = await api('/scans/current');
|
||||||
if (s) {
|
if (s) {
|
||||||
|
|
@ -735,6 +739,7 @@ async function pollScan() {
|
||||||
} else {
|
} else {
|
||||||
btn.textContent = 'Scan now';
|
btn.textContent = 'Scan now';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
state.scanning = false;
|
||||||
$('#banner-area').replaceChildren();
|
$('#banner-area').replaceChildren();
|
||||||
show(state.view);
|
show(state.view);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,3 +182,28 @@ def test_bulk_keep(client, scanned):
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.get_json()["created"] == len(ids)
|
assert r.get_json()["created"] == len(ids)
|
||||||
assert client.get("/api/v1/keeps").get_json()["kept_items"] >= len(ids)
|
assert client.get("/api/v1/keeps").get_json()["kept_items"] >= len(ids)
|
||||||
|
|
||||||
|
|
||||||
|
def test_completion_flag_comes_from_events_not_the_coverage_table(client, scanned):
|
||||||
|
"""history_coverage is written at the END of an ingest. Reading the flag from
|
||||||
|
it made the UI announce a Plex fallback mid-scan on every fresh database."""
|
||||||
|
from mediashelf import queries
|
||||||
|
|
||||||
|
assert queries.history_has_completion(scanned) is True
|
||||||
|
|
||||||
|
# simulate mid-scan: events present, coverage not yet written
|
||||||
|
scanned.execute("DELETE FROM history_coverage")
|
||||||
|
assert queries.history_has_completion(scanned) is True, \
|
||||||
|
"flag went false with events already ingested"
|
||||||
|
assert queries.active_history_source(scanned) == "tautulli", \
|
||||||
|
"source should fall back to the scan record when coverage is absent"
|
||||||
|
|
||||||
|
ov = client.get("/api/v1/stats/overview").get_json()
|
||||||
|
assert ov["has_completion_data"] is True
|
||||||
|
assert ov["history_source"] == "tautulli"
|
||||||
|
|
||||||
|
|
||||||
|
def test_completion_flag_is_false_for_plex_only_history(scanned):
|
||||||
|
from mediashelf import queries
|
||||||
|
scanned.execute("UPDATE watch_event SET percent_complete = NULL")
|
||||||
|
assert queries.history_has_completion(scanned) is False
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue