fix(live): expose full refresh busy and phase progress

Ensure full refresh marks the live projection as building before source refresh awaits, and expose phase-specific counters so language detection progress is determinate.

Covers #40 and #41.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-08 16:20:36 +02:00
parent 09bdaa3ec1
commit ddd7a3f295
7 changed files with 139 additions and 25 deletions

View File

@ -26,12 +26,14 @@ test('presents Laya identity progress using AI batch counters', () => {
assert.equal(view.progressText, 'AI batch 3 of 8 · 38%');
});
test('presents local Laya language batch progress after projection', () => {
test('presents local Laya language batch progress from generic phase counters', () => {
const view = present({
status: 'building',
phase: 'laya_language',
languageProcessed: 5,
languageTotal: 10
phaseProcessed: 5,
phaseTotal: 10,
layaProcessed: 2,
layaTotal: 20
});
assert.equal(view.statusLabel, 'Detecting stream languages…');
assert.equal(view.progressIndeterminate, false);
@ -39,12 +41,24 @@ test('presents local Laya language batch progress after projection', () => {
assert.equal(view.progressText, 'Language batch 5 of 10 · 50%');
});
test('falls back to Laya counters for language batch progress', () => {
const view = present({
status: 'building',
phase: 'laya_language',
layaProcessed: 5,
layaTotal: 10
});
assert.equal(view.progressIndeterminate, false);
assert.equal(view.progressPercent, 50);
assert.equal(view.progressText, 'Language batch 5 of 10 · 50%');
});
test('presents indeterminate progress when totals are unavailable', () => {
const view = present({ status: 'queued', processed: null, total: null });
assert.equal(view.busy, true);
assert.equal(view.progressIndeterminate, true);
assert.equal(view.progressPercent, null);
assert.equal(view.progressText, 'In progress…');
assert.equal(view.progressText, 'In progress — this step may take several minutes.');
assert.equal(view.buttonLabel, 'Analyze channels');
assert.equal(present({ status: 'running', processed: null, total: 100 }).progressIndeterminate, true);
});

View File

@ -104,7 +104,7 @@ function waitFor(condition, message) {
});
}
test('Settings renders Laya batch progress and removes misleading values when its total is unknown', () => {
test('Settings renders language phase progress accessibly and removes misleading values when its total is unknown', () => {
const { window } = createDom();
const settingsPage = Object.create(window.SettingsPage.prototype);
settingsPage.app = { player: { settings: { languageDetectionMode: 'hybrid' } } };
@ -112,30 +112,33 @@ test('Settings renders Laya batch progress and removes misleading values when it
settingsPage.renderLiveAnalysisStatus({
status: 'building',
phase: 'laya_identity',
layaProcessed: 3,
layaTotal: 8,
phase: 'laya_language',
phaseProcessed: 5,
phaseTotal: 10,
processed: 0,
total: 39348
});
const track = window.document.getElementById('live-analysis-progress-track');
const bar = window.document.getElementById('live-analysis-progress-bar');
assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Analyzing with local AI…');
assert.equal(window.document.getElementById('live-analysis-progress-text').textContent, 'AI batch 3 of 8 · 38%');
assert.equal(track.getAttribute('aria-valuenow'), '38');
assert.equal(bar.style.width, '38%');
assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Detecting stream languages…');
assert.equal(window.document.getElementById('live-analysis-progress-text').textContent, 'Language batch 5 of 10 · 50%');
assert.equal(track.getAttribute('aria-valuemin'), '0');
assert.equal(track.getAttribute('aria-valuemax'), '100');
assert.equal(track.getAttribute('aria-valuenow'), '50');
assert.equal(track.getAttribute('aria-valuetext'), 'Language batch 5 of 10 · 50%');
assert.equal(bar.style.width, '50%');
settingsPage.renderLiveAnalysisStatus({
status: 'building',
phase: 'laya_identity',
phase: 'laya_language',
layaProcessed: 0,
layaTotal: null,
processed: 0,
total: 39348
});
assert.equal(window.document.getElementById('live-analysis-progress-text').textContent, 'In progress…');
assert.equal(window.document.getElementById('live-analysis-progress-text').textContent, 'In progress — this step may take several minutes.');
assert.equal(track.classList.contains('indeterminate'), true);
assert.equal(track.hasAttribute('aria-valuenow'), false);
assert.equal(bar.style.width, '');

View File

@ -78,4 +78,25 @@ test('projection status exposes phase-specific Laya progress separately from cat
assert.equal(state.total, 12);
assert.equal(state.layaProcessed, 1);
assert.equal(state.layaTotal, 2);
assert.equal(state.phaseProcessed, 1);
assert.equal(state.phaseTotal, 2);
});
test('projection status exposes generic phase progress outside Laya analysis', () => {
const state = serializeState({
status: 'building',
phase: 'projecting',
started_at: 100,
completed_at: null,
input_hash: null,
projected_count: 0,
processed_count: 7,
total_count: 12,
laya_processed_count: 0,
laya_total_count: 0,
error: null
});
assert.equal(state.phaseProcessed, 7);
assert.equal(state.phaseTotal, 12);
});

View File

@ -0,0 +1,71 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
const liveProjection = require('../server/services/liveProjectionService');
const { LIVE_PROJECTION_SCHEMA_SQL } = require('../server/db/liveProjectionSchema');
function createDb() {
const db = new Database(':memory:');
db.exec(`
CREATE TABLE playlist_items (
id TEXT PRIMARY KEY,
source_id INTEGER NOT NULL,
item_id TEXT NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
category_id TEXT,
parent_id TEXT,
stream_icon TEXT,
stream_url TEXT,
container_extension TEXT,
rating REAL,
year TEXT,
added_at TEXT,
is_hidden INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
data JSON,
logical_channel_id TEXT,
grouping_fingerprint TEXT,
grouping_input_hash TEXT,
quality_label TEXT,
quality_rank INTEGER,
variant_rank INTEGER,
language_code TEXT,
language_confidence REAL,
language_scores JSON,
language_evidence JSON,
language_detector TEXT,
language_input_hash TEXT
);
`);
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
return db;
}
test('full refresh marks projection busy before awaiting source refresh work', () => {
const db = createDb();
db.prepare(`
INSERT INTO catalog_projection_state
(projection, status, started_at, completed_at, input_hash, projected_count,
phase, processed_count, total_count, laya_processed_count, laya_total_count, error)
VALUES ('live', 'success', 100, 200, 'old-hash', 12, 'complete', 12, 12, 0, 0, NULL)
`).run();
let liveRefreshStarted = false;
liveProjection.fullRebuildOrchestrator(db, {
syncService: {
refreshEnabledLiveSources: () => {
liveRefreshStarted = true;
return new Promise(() => {});
},
refreshEnabledEpgSources: async () => {}
},
sources: { getByType: async () => [] }
});
const state = liveProjection.projectionState(db);
assert.equal(liveRefreshStarted, true);
assert.equal(state.status, 'building');
assert.equal(state.phase, 'refreshing_live_sources');
assert.equal(state.completed_at, null);
});