Correct configuration E2E and isolate native playback dialog rejection

This commit is contained in:
2026-10-12 22:04:10 +02:00
parent f5cdfa285e
commit a5a1df21a4
6 changed files with 243 additions and 27 deletions
+10 -1
View File
@@ -89,4 +89,13 @@ Wyniki w osobnych JSON: `e2e-ui-20261012.json`, `e2e-config-20261012.json`, `e2e
Całego E2E nie uznano za zielony: TVP1, TVP2 i TVN nie uzyskały klatek; źródło dekodera było puste lub miało niewłaściwy program/transponder według równoległej diagnostyki natywnej. Przy błędach źródła Jellyfin Web nadal zgłosił nieobsłużone odrzucenia obietnic `undefined`; wyjście do kanałów przeszło. Administracyjna nawigacja konfiguracji dla tymczasowego konta jest raportowana osobno i nie wolno uznać jej za PASS na podstawie samego API HTML. Nie zmieniano konfiguracji tunerów, nie restartowano Vu ani nie omijano ochrony logowania administratora. Końcowe aktywne transmisje wstrzymano na prośbę zadania dekodera prowadzącego ponownie izolowane strojenie. Całego E2E nie uznano za zielony: TVP1, TVP2 i TVN nie uzyskały klatek; źródło dekodera było puste lub miało niewłaściwy program/transponder według równoległej diagnostyki natywnej. Przy błędach źródła Jellyfin Web nadal zgłosił nieobsłużone odrzucenia obietnic `undefined`; wyjście do kanałów przeszło. Administracyjna nawigacja konfiguracji dla tymczasowego konta jest raportowana osobno i nie wolno uznać jej za PASS na podstawie samego API HTML. Nie zmieniano konfiguracji tunerów, nie restartowano Vu ani nie omijano ochrony logowania administratora. Końcowe aktywne transmisje wstrzymano na prośbę zadania dekodera prowadzącego ponownie izolowane strojenie.
Zakres nie obejmuje Android TV, pełnego logowania SSO w Keycloak, tworzenia/wykonywania nowych nagrań ani wielogodzinnej stabilności. Wynik short E2E nie jest gwarancją tych funkcji. Zakres nie obejmuje Android TV, pełnego logowania SSO w Keycloak, tworzenia/wykonywania nowych nagrań ani wielogodzinnej stabilności. Wynik short E2E nie jest gwarancją tych funkcji.
## Analiza niezaliczonych punktów — 12.10.2026
Korekta raportu panelu administratora: wcześniejszy FAIL był fałszywym wynikiem scenariusza testowego. Test zmieniał uprawnienia po logowaniu oraz nawigował przed zakończeniem inicjalizacji sesji. Wydzielone konto administratora utworzone przed pierwszym logowaniem, oczekiwanie na zapisane credentials i zniknięcie formularza logowania oraz dopiero potem nawigacja pozwoliły otworzyć konfigurację. Z modułem web i z modułem wyłączonym wyłącznie w izolowanej przeglądarce formularz i API konfiguracji działały (HTTP 200, host192.168.10.20, port8001, nagraniaVu=false). Nie zmieniano konfiguracji dodatku ani serwera. Poprawiony `test-e2e.cjs --ui-only` zakończył się **17/17 PASS**, a `--config-only` **3/3 PASS**. Podane poprzednio przypisanie tego FAIL do konfiguracji wtyczki jest wycofane.
`diagnose-web.cjs` nie otwiera TV na odbiorniku: podstawia odpowiedzi PlaybackInfo i blokuje ewentualny endpoint otwarcia live. Porównuje moduł web z jego brakiem, oraz zapisuje typ/treść unhandledrejection i przebieg stop/powrotu. Wynik `web-analysis-20261012.json` potwierdza: sam błąd HTTP i anulowanie oczekującego żądania nie wywołały unhandledrejection; wyjście po błędzie, podczas zamykania komunikatu odtwarzacza, wywołało `undefined` także bez modułu wtyczki. Przy aktywnym module zarówno `playbackManager.stop()`, jak i `appRouter.show()` spełniły obietnice; samo dodanie catch do tych dwóch wywołań nie usuwa źródłowego odrzucenia.
Mechanizm pasujący do wyników: natywny [playbackmanager](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/playback/playbackmanager.js) wywołuje `alert()` w showPlaybackInfoErrorMessage bez obsługi zwróconej obietnicy. [Dialog](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/dialog/dialog.js) kończy się `Promise.reject()` bez argumentu, gdy zamknięto go bez wyniku przycisku. [dialogHelper](https://github.com/jellyfin/jellyfin-web/blob/master/src/components/dialogHelper/dialogHelper.js) zamyka dialogi przy zmianie historii. To tłumaczy undefined po opuszczeniu komunikatu; nie jest dowodem błędu zatrzymania strumienia ani diagnozą OSCam. W tym zadaniu analizy nie modyfikowano natywnego klienta lub produkcyjnego modułu web. Ewentualne obejście w module wtyczki powinno skoordynować domknięcie komunikatu i nawigację; nie należy globalnie tłumić unhandledrejection.
Brak obrazu na TVP1/TVP2/TVN nadal ma odrębne dowody: produkcyjne logi końcowego E2E zawierają POST PlaybackInfo z InvalidOperationException po HTTP502 z relay, a wcześniejsze odczyty bezpośrednie i diagnostyka odbiornika wykazały brak lub niewłaściwy program/transponder. Nie jest to błąd logowania do konfiguracji ani sam komunikat undefined. Odtwarzanie 4Fun, native OSD, współdzielenie i zwolnienie źródła pozostają potwierdzone wynikami E2E. Aktualne próby sterowania SCR są prowadzone wyłącznie przez zadanie odbiornika; ich wynik nie jest zakładany z góry.
+89
View File
@@ -0,0 +1,89 @@
// No receiver streams: playback failures are injected into the browser request only.
const fs=require('node:fs');
const {execFileSync}=require('node:child_process');
const {chromium}=require('C:/Users/michal/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const ssh=code=>JSON.parse(execFileSync('ssh',['-o','BatchMode=yes','root@192.168.100.25','pct exec 100191 -- python3 -'],{input:code,encoding:'utf8',timeout:30000}).trim().split('\n').at(-1));
const admin=`import sqlite3,json,urllib.request,secrets,pathlib
c=sqlite3.connect('file:/var/lib/jellyfin/data/jellyfin.db?mode=ro',uri=True);k=c.execute('SELECT AccessToken FROM ApiKeys LIMIT 1').fetchone()[0];c.close()
def api(p,d=None,method=None):
r=urllib.request.Request('http://127.0.0.1:8096'+p,headers={'Authorization':'MediaBrowser Token="'+k+'", Client="Analysis", Device="Maintenance", DeviceId="decoder-analysis", Version="1"','Content-Type':'application/json'},data=json.dumps(d).encode() if d is not None else None,method=method)
b=urllib.request.urlopen(r,timeout=15).read();return json.loads(b) if b else None
`;
const origin='http://192.168.100.191:8096',web=origin+'/web/index.html';
const report={checked_utc:new Date().toISOString(),receiver_connections_opened_by_test:0,configuration:[],playback:[]};
const users=[];let browser;
const safe=s=>String(s).replace(/([?&](?:api_key|AccessToken)=)[^&\s]+/gi,'$1<redacted>').slice(0,1800);
async function login(page,u){await page.goto(web);await page.locator('#txtManualName').fill(u.name);await page.locator('#txtManualPassword').fill(u.password);await page.getByRole('checkbox').check();await page.getByRole('button',{name:'Sign In',exact:true}).click();await page.waitForURL(/home/,{timeout:20000});await page.waitForFunction(()=>JSON.parse(localStorage.getItem('jellyfin_credentials')||'{}').Servers?.some(s=>s.AccessToken&&s.UserId));await page.waitForTimeout(1000);}
async function traceBridge(page){
const source=fs.readFileSync(__dirname+'/web/decoder-channel-playback.js','utf8')
.replace('await playbackManager.stop();',`await Promise.resolve(playbackManager.stop()).then(()=>{(window.__leaveTrace??=[]).push('stop resolved');},e=>{(window.__leaveTrace??=[]).push('stop rejected: '+String(e));throw e;});`)
.replace("await appRouter.show('livetv?tab=2');",`await Promise.resolve(appRouter.show('livetv?tab=2')).then(()=>{(window.__leaveTrace??=[]).push('route resolved');},e=>{(window.__leaveTrace??=[]).push('route rejected: '+String(e));throw e;});`);
await page.route('**/decoder-channel-playback.js',r=>r.fulfill({status:200,contentType:'application/javascript',body:source}));
}
function create(isAdmin){
const u=ssh(admin+`n='decoder-analysis-'+secrets.token_hex(4);p=secrets.token_urlsafe(24);u=api('/Users/New',{'Name':n});print(json.dumps({'id':u['Id'],'name':n,'password':p}))`);users.push(u);
ssh(admin+`api('/Users/${u.id}/Password',{'CurrentPw':'','NewPw':${JSON.stringify(u.password)}})
u=api('/Users/${u.id}');p=u['Policy'];p['IsAdministrator']=${isAdmin?'True':'False'};api('/Users/${u.id}/Policy',p)
print('{}')`);return u;
}
(async()=>{
try{
browser=await chromium.launch({headless:true,channel:'msedge'});
const elevated=create(true),ordinary=create(false);
for(const bridge of [true,false]){
const context=await browser.newContext(),page=await context.newPage();const responses=[],errors=[];
if(!bridge)await page.route('**/decoder-channel-playback.js',r=>r.fulfill({status:200,contentType:'application/javascript',body:'window.DecoderChannelPlayback = async () => class { constructor({playbackManager,appRouter}) { this.id="decoder-channel-playback";this.type="integration";window.__diagnosticPlayer=playbackManager;window.__diagnosticRouter=appRouter; } };'}));
page.on('pageerror',e=>errors.push(safe(e.stack||e.message)));
page.on('response',r=>{const path=new URL(r.url()).pathname;if(/ConfigurationPage|\/Plugins\//i.test(path))responses.push({path,status:r.status()});});
await login(page,elevated);await page.evaluate(()=>location.hash='#/configurationpage?name=enigma2');
let opened=false;try{await page.locator('#txtHostName').waitFor({timeout:12000});await page.waitForFunction(()=>document.querySelector('#txtHostName')?.value==='192.168.10.20',{},{timeout:12000});opened=true;}catch{}
const state=await page.evaluate(()=>({route:location.hash,login_fields:document.querySelectorAll('#txtManualPassword').length,configuration_fields:document.querySelectorAll('#txtHostName').length}));
let values;if(opened){values={host:await page.locator('#txtHostName').inputValue(),port:await page.locator('#txtStreamingPort').inputValue(),vu_recordings:await page.locator('#chkEnableVuRecordings').isChecked()};}
report.configuration.push({bridge,administrator_before_initial_login:true,opened,state,values,responses,errors});console.log('configuration '+bridge+': '+opened);await context.close();
}
for(const bridge of [true,false]){
const context=await browser.newContext(),page=await context.newPage();const errors=[];let injected=0;
if(bridge)await traceBridge(page);
if(!bridge)await page.route('**/decoder-channel-playback.js',r=>r.fulfill({status:200,contentType:'application/javascript',body:'window.DecoderChannelPlayback = async () => class { constructor({playbackManager,appRouter}) { this.id="decoder-channel-playback";this.type="integration";window.__diagnosticPlayer=playbackManager;window.__diagnosticRouter=appRouter; } };'}));
await page.addInitScript(()=>{window.__rejections=[];window.addEventListener('unhandledrejection',e=>window.__rejections.push({type:typeof e.reason,message:String(e.reason?.message||e.reason),stack:e.reason?.stack||null}));});
page.on('pageerror',e=>errors.push(safe(e.stack||e.message)));
await page.route('**/*PlaybackInfo*',r=>{++injected;return r.fulfill({status:500,contentType:'application/json',body:JSON.stringify({Message:'Diagnostic unavailable source',StatusCode:500})});});
// Defense in depth: an unexpected live-open request is also blocked locally.
await page.route('**/LiveTv/LiveStreams/Open*',r=>r.fulfill({status:503,body:'Diagnostic: blocked live open'}));
await login(page,ordinary);await page.goto(web+'#/livetv?tab=2');await page.locator('.card[data-type="TvChannel"]').first().waitFor({timeout:15000});
const card=page.locator('.card[data-type="TvChannel"]').filter({has:page.locator('a.textActionButton',{hasText:/^\d+ TVP 1 HD$/})});
if(bridge)await card.locator('a.cardImageContainer').click({position:{x:20,y:20}});
else {
// Start the same native player directly; the bridge is the only excluded layer.
await page.evaluate(async()=>{const Plugin=await window.DecoderChannelPlayback();window.__nativeStart=Plugin;});
const play=card.locator('[data-action="play"]');
if(await play.count())await play.first().click({force:true});else await card.locator('a.cardImageContainer').click();
if(await page.getByRole('button',{name:'Play',exact:true}).count())await page.getByRole('button',{name:'Play',exact:true}).first().click();
}
await page.waitForTimeout(5000);
if(bridge&&await page.locator('#decoderPlaybackExit').count()) {await page.locator('#decoderPlaybackExit').click();await page.waitForTimeout(1000);}
if(!bridge) {await page.evaluate(async()=>{try{await window.__diagnosticPlayer.stop();}catch{}try{await window.__diagnosticRouter.show('livetv?tab=2');}catch{}});await page.waitForTimeout(1000);}
const rejected=await page.evaluate(()=>window.__rejections);report.playback.push({bridge,injected_failures:injected,errors,rejected:rejected.map(r=>({...r,stack:r.stack?safe(r.stack):null})),leave_trace:await page.evaluate(()=>window.__leaveTrace||[])});console.log('failure '+bridge+': '+errors.length);await context.close();
}
const context=await browser.newContext(),page=await context.newPage();let injected=0;const errors=[];
await page.addInitScript(()=>{window.__rejections=[];window.addEventListener('unhandledrejection',e=>window.__rejections.push({type:typeof e.reason,message:String(e.reason?.message||e.reason),stack:e.reason?.stack||null}));});
page.on('pageerror',e=>errors.push(safe(e.stack||e.message)));
await page.route('**/*PlaybackInfo*',async r=>{++injected;await new Promise(resolve=>setTimeout(resolve,10000));try{await r.fulfill({status:500,body:'Diagnostic unavailable source'});}catch{}});
await page.route('**/LiveTv/LiveStreams/Open*',r=>r.fulfill({status:503,body:'Diagnostic: blocked live open'}));
await login(page,ordinary);await page.goto(web+'#/livetv?tab=2');await page.locator('.card[data-type="TvChannel"]').first().waitFor();
await page.locator('.card[data-type="TvChannel"]').first().locator('a.cardImageContainer').click({position:{x:20,y:20}});await page.locator('#decoderPlaybackExit').waitFor();
await page.waitForTimeout(1000);await page.locator('#decoderPlaybackExit').click();await page.waitForTimeout(12000);
report.playback.push({bridge:true,scenario:'cancel pending playback request',injected_failures:injected,errors,rejected:await page.evaluate(()=>window.__rejections),final_route:new URL(page.url()).hash});await context.close();
const emptyContext=await browser.newContext(),empty=await emptyContext.newPage();const emptyErrors=[];let emptyResponses=0;
empty.on('pageerror',e=>emptyErrors.push(safe(e.stack||e.message)));
await empty.route('**/*PlaybackInfo*',r=>{++emptyResponses;return r.fulfill({status:200,contentType:'application/json',body:JSON.stringify({MediaSources:[],PlaySessionId:'00000000000000000000000000000000',ErrorCode:'NoCompatibleStream'})});});
await empty.route('**/LiveTv/LiveStreams/Open*',r=>r.fulfill({status:503,body:'Diagnostic: blocked live open'}));
await login(empty,ordinary);await empty.goto(web+'#/livetv?tab=2');await empty.locator('.card[data-type="TvChannel"]').first().waitFor();await empty.locator('.card[data-type="TvChannel"]').first().locator('a.cardImageContainer').click({position:{x:20,y:20}});await empty.waitForTimeout(5000);
if(await empty.locator('#decoderPlaybackExit').count())await empty.locator('#decoderPlaybackExit').click();await empty.waitForTimeout(1000);
report.playback.push({bridge:true,scenario:'empty media sources and exit',injected_failures:emptyResponses,errors:emptyErrors,final_route:new URL(empty.url()).hash});await emptyContext.close();
}catch(e){report.error=safe(e.stack||e.message);}
finally{
if(browser)await browser.close();for(const u of users)ssh(admin+`api('/Users/${u.id}',method='DELETE');pathlib.Path('/var/lib/jellyfin/decoder-integration/users/${u.id.replace(/-/g,'')}.json').unlink(missing_ok=True);print('{}')`);
report.finished_utc=new Date().toISOString();fs.writeFileSync(__dirname+'/web-analysis-20261012.json',JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report));
}
})();
+5 -6
View File
@@ -1,5 +1,5 @@
{ {
"checked_utc": "2026-10-12T19:38:18.545Z", "checked_utc": "2026-10-12T19:54:03.596Z",
"mode": "ui", "mode": "ui",
"checks": [ "checks": [
{ {
@@ -12,8 +12,7 @@
}, },
{ {
"name": "administrator plugin configuration loads current receiver settings", "name": "administrator plugin configuration loads current receiver settings",
"status": "FAIL", "status": "PASS"
"error": "{\"route\":\"#/home\",\"admin\":true,\"headings\":[\"Please sign in\",\"Please sign in\",\"My Media\",\"Continue Watching\",\"Continue Listening\"],\"input_ids\":[\"txtManualName\",\"txtManualPassword\",\"\"]}"
}, },
{ {
"name": "no uncaught browser errors", "name": "no uncaught browser errors",
@@ -30,8 +29,8 @@
"HasImage": false, "HasImage": false,
"Status": "Active" "Status": "Active"
}, },
"finished_utc": "2026-10-12T19:39:29.601Z", "finished_utc": "2026-10-12T19:54:27.907Z",
"pass": 2, "pass": 3,
"fail": 1, "fail": 0,
"blocked": 0 "blocked": 0
} }
+6 -7
View File
@@ -1,5 +1,5 @@
{ {
"checked_utc": "2026-10-12T19:31:08.236Z", "checked_utc": "2026-10-12T19:58:18.326Z",
"mode": "ui", "mode": "ui",
"checks": [ "checks": [
{ {
@@ -19,7 +19,7 @@
"status": "PASS", "status": "PASS",
"evidence": { "evidence": {
"channels": 216, "channels": 216,
"loaded_logos": 34 "loaded_logos": 35
} }
}, },
{ {
@@ -85,8 +85,7 @@
}, },
{ {
"name": "administrator plugin configuration loads current receiver settings", "name": "administrator plugin configuration loads current receiver settings",
"status": "FAIL", "status": "PASS"
"error": "locator.waitFor: Timeout 20000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('#txtHostName') to be visible\u001b[22m\n"
}, },
{ {
"name": "no uncaught browser errors", "name": "no uncaught browser errors",
@@ -103,8 +102,8 @@
"HasImage": false, "HasImage": false,
"Status": "Active" "Status": "Active"
}, },
"finished_utc": "2026-10-12T19:32:03.314Z", "finished_utc": "2026-10-12T19:58:57.786Z",
"pass": 16, "pass": 17,
"fail": 1, "fail": 0,
"blocked": 0 "blocked": 0
} }
+7 -13
View File
@@ -121,20 +121,14 @@ async function frames(page) {
}); });
} }
await check('administrator plugin configuration loads current receiver settings',async()=>{ await check('administrator plugin configuration loads current receiver settings',async()=>{
const policy=ssh(admin+`u=api('/Users/${accounts[1].id}');p=u['Policy'];p['IsAdministrator']=True;api('/Users/${accounts[1].id}/Policy',p);print(json.dumps({'administrator':api('/Users/${accounts[1].id}')['Policy']['IsAdministrator']}))`); const user=ssh(admin+`n='decoder-e2e-'+secrets.token_hex(4);p=secrets.token_urlsafe(24);u=api('/Users/New',{'Name':n});print(json.dumps({'id':u['Id'],'name':n,'password':p}))`);accounts.push(user);
assert(policy.administrator,'Temporary user did not gain admin rights'); ssh(admin+`api('/Users/${user.id}/Password',{'CurrentPw':'','NewPw':${JSON.stringify(user.password)}});u=api('/Users/${user.id}');p=u['Policy'];p['IsAdministrator']=True;api('/Users/${user.id}/Policy',p);print('{}')`);
// The old client session caches its original non-admin policy; sign in afresh. // Set administrator permissions before this account's first authentication.
const config=await (await browser.newContext()).newPage();await config.goto(web);await config.locator('#txtManualName').fill(accounts[1].name);await config.locator('#txtManualPassword').fill(accounts[1].password);await config.getByRole('checkbox').check();await config.getByRole('button',{name:'Sign In',exact:true}).click();await config.waitForURL(/home/,{timeout:20000}); const config=await (await browser.newContext()).newPage();await config.goto(web);await config.locator('#txtManualName').fill(user.name);await config.locator('#txtManualPassword').fill(user.password);await config.getByRole('checkbox').check();await config.getByRole('button',{name:'Sign In',exact:true}).click();await config.waitForURL(/home/,{timeout:20000});
await config.waitForFunction(()=>window.ApiClient?.getCurrentUserId()); await config.waitForFunction(()=>JSON.parse(localStorage.getItem('jellyfin_credentials')||'{}').Servers?.some(s=>s.AccessToken&&s.UserId));
await config.locator('#txtManualPassword:visible').waitFor({state:'hidden',timeout:10000});
await config.waitForTimeout(1000);
await config.evaluate(()=>window.location.hash='#/configurationpage?name=enigma2'); await config.evaluate(()=>window.location.hash='#/configurationpage?name=enigma2');
// Jellyfin 12 can request a separate elevated administrator sign-in.
const elevated=config.locator('#txtManualPassword:visible');
try {
await elevated.waitFor({timeout:5000});
const name=config.locator('#txtManualName:visible');if(await name.count()&&await name.isEditable())await name.fill(accounts[1].name);
await elevated.fill(accounts[1].password);await config.getByRole('button',{name:'Sign In',exact:true}).last().click();
await config.evaluate(()=>window.location.hash='#/configurationpage?name=enigma2');
} catch {}
try {await config.locator('#txtHostName').waitFor({timeout:20000});} try {await config.locator('#txtHostName').waitFor({timeout:20000});}
catch {throw Error(JSON.stringify(await config.evaluate(async()=>({route:location.hash,admin:(await window.ApiClient.getCurrentUser()).Policy.IsAdministrator,headings:[...document.querySelectorAll('h1,h2')].map(e=>e.textContent).slice(0,5),input_ids:[...document.querySelectorAll('input')].map(i=>i.id).slice(0,20)}))));} catch {throw Error(JSON.stringify(await config.evaluate(async()=>({route:location.hash,admin:(await window.ApiClient.getCurrentUser()).Policy.IsAdministrator,headings:[...document.querySelectorAll('h1,h2')].map(e=>e.textContent).slice(0,5),input_ids:[...document.querySelectorAll('input')].map(i=>i.id).slice(0,20)}))));}
await config.waitForFunction(()=>document.querySelector('#txtHostName')?.value==='192.168.10.20');assert(await config.locator('#txtStreamingPort').inputValue()==='8001','Unexpected configured source port'); await config.waitForFunction(()=>document.querySelector('#txtHostName')?.value==='192.168.10.20');assert(await config.locator('#txtStreamingPort').inputValue()==='8001','Unexpected configured source port');
+126
View File
@@ -0,0 +1,126 @@
{
"checked_utc": "2026-10-12T20:02:11.340Z",
"receiver_connections_opened_by_test": 0,
"configuration": [
{
"bridge": true,
"administrator_before_initial_login": true,
"opened": true,
"state": {
"route": "#/configurationpage?name=enigma2",
"login_fields": 0,
"configuration_fields": 1
},
"values": {
"host": "192.168.10.20",
"port": "8001",
"vu_recordings": false
},
"responses": [
{
"path": "/web/configurationpage",
"status": 200
},
{
"path": "/web/ConfigurationPages",
"status": 200
},
{
"path": "/Plugins/193f29f9-ea6c-4595-a6f6-55e79d7c590a/Configuration",
"status": 200
}
],
"errors": []
},
{
"bridge": false,
"administrator_before_initial_login": true,
"opened": true,
"state": {
"route": "#/configurationpage?name=enigma2",
"login_fields": 0,
"configuration_fields": 1
},
"values": {
"host": "192.168.10.20",
"port": "8001",
"vu_recordings": false
},
"responses": [
{
"path": "/web/configurationpage",
"status": 200
},
{
"path": "/web/ConfigurationPages",
"status": 200
},
{
"path": "/Plugins/193f29f9-ea6c-4595-a6f6-55e79d7c590a/Configuration",
"status": 200
}
],
"errors": []
}
],
"playback": [
{
"bridge": true,
"injected_failures": 1,
"errors": [
"undefined"
],
"rejected": [
{
"type": "undefined",
"message": "undefined",
"stack": null
}
],
"leave_trace": [
"stop resolved",
"route resolved"
]
},
{
"bridge": false,
"injected_failures": 2,
"errors": [
"undefined",
"undefined"
],
"rejected": [
{
"type": "undefined",
"message": "undefined",
"stack": null
},
{
"type": "undefined",
"message": "undefined",
"stack": null
}
],
"leave_trace": []
},
{
"bridge": true,
"scenario": "cancel pending playback request",
"injected_failures": 1,
"errors": [],
"rejected": [],
"final_route": "#/livetv?tab=2"
},
{
"bridge": true,
"scenario": "empty media sources and exit",
"injected_failures": 1,
"errors": [
"undefined",
"undefined"
],
"final_route": "#/livetv?tab=2"
}
],
"finished_utc": "2026-10-12T20:03:22.678Z"
}