feat: persist incremental live channel analysis

Persist live channel language metadata across background projection rebuilds and target Local AI identity analysis to newly detected streams unless a manual full analysis is requested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-07 23:12:42 +02:00
parent 9967fd7adf
commit 4319e64e1e
10 changed files with 516 additions and 98 deletions

View File

@ -141,12 +141,12 @@ test('Settings renders Laya batch progress and removes misleading values when it
assert.equal(bar.style.width, '');
});
test('analysis remains visibly busy while Laya language batches run after projection completes', async () => {
test('analysis completion reloads the persisted projection', async () => {
const { window } = createDom();
let rebuildClicked = false;
let languageRefreshStarted = false;
let releaseLanguageRefresh;
const languageRefresh = new Promise(resolve => { releaseLanguageRefresh = resolve; });
let projectionReloadStarted = false;
let releaseProjectionReload;
const projectionReload = new Promise(resolve => { releaseProjectionReload = resolve; });
window.API = {
live: {
getProjectionStatus: async () => rebuildClicked
@ -176,11 +176,11 @@ test('analysis remains visibly busy while Laya language batches run after projec
saveSettings() {}
},
channelList: {
loadChannels: async () => {},
refreshLayaLanguages: async () => {
languageRefreshStarted = true;
await languageRefresh;
}
loadChannels: async () => {
projectionReloadStarted = true;
await projectionReload;
},
refreshLayaLanguages: async () => assert.fail('Browser-side Laya refresh must not run')
}
};
window.app = app;
@ -193,28 +193,19 @@ test('analysis remains visibly busy while Laya language batches run after projec
window.document.getElementById('live-analysis-button').click();
try {
await waitFor(() => languageRefreshStarted, 'Expected Laya language refresh to start after projection completion');
assert.notEqual(window.document.getElementById('live-analysis-status').textContent, 'Analysis complete');
assert.equal(window.document.getElementById('live-analysis-progress').hidden, false);
await waitFor(() => projectionReloadStarted, 'Expected persisted projection reload after analysis completion');
assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Analysis complete');
} finally {
releaseLanguageRefresh();
releaseProjectionReload();
window.clearInterval(settingsPage.liveAnalysisPollTimer);
}
});
test('user analysis click keeps visible channels after Local AI language failure', async () => {
test('user analysis click reloads persisted languages without a browser-side Laya batch', async () => {
const { window } = createDom();
let rebuildClicked = 0;
let layaBatchCalls = 0;
let channelLoads = 0;
const consoleWarnings = [];
window.console = {
...console,
warn: (...args) => {
consoleWarnings.push(args.join(' '));
console.warn(...args);
}
};
window.API = {
live: {
getChannels: async () => {
@ -269,26 +260,22 @@ test('user analysis click keeps visible channels after Local AI language failure
window.clearInterval(channelList._epgRefreshTimer);
window.clearInterval(settingsPage.liveAnalysisPollTimer);
const notice = window.document.querySelector('.laya-language-notice');
const afterNames = [...window.document.querySelectorAll('.channel-name')].map(item => item.textContent);
const languageBadges = [...window.document.querySelectorAll('.variant-language')].map(item => item.textContent);
const groupNames = [...window.document.querySelectorAll('.group-name')].map(item => item.textContent);
assert.equal(channelLoads, 2);
assert.equal(layaBatchCalls, 1);
assert.ok(notice);
assert.match(notice.textContent, /Local AI language detection failed/);
assert.equal(layaBatchCalls, 0);
assert.deepEqual(afterNames, ['Sports One', 'Sports One']);
assert.deepEqual([...new Set(languageBadges)].sort(), ['EN 100%', 'FR 100%']);
assert.deepEqual(groupNames, ['Favorites', 'Sports France']);
assert.equal(window.document.querySelector('.empty-state'), null);
assert.equal(window.document.querySelector('#channel-list > .loading'), null);
assert.equal(channelList.languageDetectionFallback, true);
assert.equal(channelList.languageDetectionFallback, false);
assert.equal(channelList.logicalChannels[0].variants.length, 2);
assert.ok(consoleWarnings.some(line => line.includes('Local AI language detection unavailable')));
});
test('successful Standard fallback after Local AI failure keeps channels and reports the AI failure', async () => {
test('hybrid analysis completion relies on persisted projection without Standard browser fallback', async () => {
const { window } = createDom();
let rebuildClicked = 0;
let layaBatchCalls = 0;
@ -345,16 +332,13 @@ test('successful Standard fallback after Local AI failure keeps channels and rep
window.clearInterval(settingsPage.liveAnalysisPollTimer);
const afterNames = [...window.document.querySelectorAll('.channel-name')].map(item => item.textContent);
const notice = window.document.querySelector('.laya-language-notice');
assert.equal(layaBatchCalls, 1);
assert.equal(layaBatchCalls, 0);
assert.deepEqual(afterNames, ['Sports One', 'Sports One']);
assert.equal(window.document.querySelector('.empty-state'), null);
assert.equal(window.document.querySelector('#channel-list > .loading'), null);
assert.ok(notice);
assert.match(notice.textContent, /Local AI language detection failed/);
assert.match(notice.textContent, /Standard matching/);
assert.equal(channelList.languageDetectionFallback, true);
assert.equal(window.document.querySelector('.laya-language-notice'), null);
assert.equal(channelList.languageDetectionFallback, false);
});
test('successful Laya language batch updates visible variant languages without Standard fallback', async () => {