From 043afde553c964357987e1f14fbd0e9acf70450b Mon Sep 17 00:00:00 2001 From: blomios Date: Thu, 8 Oct 2026 19:30:31 +0200 Subject: [PATCH] fix(settings): keep full refresh busy across phases Centralize persisted full-refresh phase detection so the Settings rebuild button stays in its refreshing state during Laya, projection, writing, and EPG phases. Refs #44 Co-Authored-By: Claude Opus 4.8 --- public/js/pages/Settings.js | 7 +-- .../js/services/liveAnalysisPresentation.js | 10 +++- test/liveAnalysisPresentation.test.js | 12 +++- test/t44FullRefreshLiveRegression.test.js | 57 +++++++++++++++++++ 4 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 test/t44FullRefreshLiveRegression.test.js diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js index 57dc6d1..4b10395 100644 --- a/public/js/pages/Settings.js +++ b/public/js/pages/Settings.js @@ -154,12 +154,9 @@ class SettingsPage { const presentation = LiveAnalysisPresentation.present(status); buttons.forEach(button => { button.disabled = presentation.busy; }); const rebuildButton = document.getElementById('live-rebuild-catalog-button'); - const fullRefreshPhases = new Set([ - 'refreshing_live_sources', 'refreshing_epg_sources', 'matching_epg', - 'laya_epg_matching', 'writing_epg_matches' - ]); const fullRefreshBusy = presentation.busy && - (this.currentLiveAnalysisAction === 'rebuild' || fullRefreshPhases.has(presentation.phase)); + (this.currentLiveAnalysisAction === 'rebuild' || + LiveAnalysisPresentation.isFullRefreshPhase(presentation.phase)); if (rebuildButton) rebuildButton.textContent = fullRefreshBusy ? 'Refreshing…' : 'Run full refresh'; statusElement.textContent = presentation.statusLabel; progress.hidden = !presentation.busy; diff --git a/public/js/services/liveAnalysisPresentation.js b/public/js/services/liveAnalysisPresentation.js index 9e218ac..22a6814 100644 --- a/public/js/services/liveAnalysisPresentation.js +++ b/public/js/services/liveAnalysisPresentation.js @@ -8,6 +8,14 @@ const INDETERMINATE_PHASES = new Set([ 'writing', 'refreshing_live_sources', 'refreshing_epg_sources', 'writing_epg_matches' ]); + const FULL_REFRESH_PHASES = new Set([ + 'refreshing_live_sources', 'laya_identity', 'laya_language', 'projecting', 'writing', + 'refreshing_epg_sources', 'matching_epg', 'laya_epg_matching', 'writing_epg_matches' + ]); + + function isFullRefreshPhase(phase) { + return FULL_REFRESH_PHASES.has(String(phase || '').toLowerCase()); + } function progressValues(status, phase) { if (phase === 'laya_identity') { @@ -93,5 +101,5 @@ : ''; } - return { layaFallbackMessage, present }; + return { isFullRefreshPhase, layaFallbackMessage, present }; }); diff --git a/test/liveAnalysisPresentation.test.js b/test/liveAnalysisPresentation.test.js index 71e00b6..11fd493 100644 --- a/test/liveAnalysisPresentation.test.js +++ b/test/liveAnalysisPresentation.test.js @@ -1,6 +1,16 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const { layaFallbackMessage, present } = require('../public/js/services/liveAnalysisPresentation.js'); +const { isFullRefreshPhase, layaFallbackMessage, present } = require('../public/js/services/liveAnalysisPresentation.js'); + +test('classifies every persisted full refresh phase centrally', () => { + const phases = [ + 'refreshing_live_sources', 'laya_identity', 'laya_language', 'projecting', 'writing', + 'refreshing_epg_sources', 'matching_epg', 'laya_epg_matching', 'writing_epg_matches' + ]; + assert.ok(phases.every(isFullRefreshPhase)); + assert.equal(isFullRefreshPhase('complete'), false); + assert.equal(isFullRefreshPhase(''), false); +}); test('presents determinate channel analysis progress', () => { const view = present({ status: 'building', phase: 'projecting', processed: 25, total: 100 }); diff --git a/test/t44FullRefreshLiveRegression.test.js b/test/t44FullRefreshLiveRegression.test.js new file mode 100644 index 0000000..8874d2c --- /dev/null +++ b/test/t44FullRefreshLiveRegression.test.js @@ -0,0 +1,57 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { JSDOM } = require('jsdom'); + +const root = path.resolve(__dirname, '..'); +const readPublicScript = relativePath => fs.readFileSync(path.join(root, 'public', relativePath), 'utf8'); + +function createSettingsDom() { + const dom = new JSDOM(` + + + +
+ +
+ + + `, { url: 'http://nodecast.test/', runScripts: 'outside-only' }); + const { window } = dom; + window.eval(readPublicScript('js/services/liveAnalysisPresentation.js')); + window.eval(`${readPublicScript('js/pages/Settings.js')}\nwindow.SettingsPage = SettingsPage;`); + return { dom, window }; +} + +test('full refresh keeps the rebuild action visibly busy during Laya language detection from persisted status', () => { + const { dom, window } = createSettingsDom(); + const settingsPage = Object.create(window.SettingsPage.prototype); + settingsPage.app = { player: { settings: { languageDetectionMode: 'laya' } } }; + settingsPage.layaStatus = { modelPresent: true, runtimeAvailable: true }; + settingsPage.currentLiveAnalysisAction = ''; + + try { + settingsPage.renderLiveAnalysisStatus({ + status: 'building', + phase: 'laya_language', + phaseProcessed: 2, + phaseTotal: 4, + processed: 0, + total: 128 + }); + + assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Detecting stream languages…'); + assert.equal(window.document.getElementById('live-analysis-progress-text').textContent, 'Language batch 2 of 4 · 50%'); + assert.equal( + window.document.getElementById('live-rebuild-catalog-button').textContent, + 'Refreshing…', + 'full refresh should remain visibly active while the backend publishes laya_language' + ); + } finally { + dom.window.close(); + } +});