@testrelic/maestro-analytics 1.3.1 → 1.4.0-next.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -778,6 +778,9 @@ var import_node_fs18 = require("fs");
778
778
  var import_node_path16 = require("path");
779
779
  var import_node_crypto4 = require("crypto");
780
780
 
781
+ // src/schema.ts
782
+ var SCHEMA_VERSION = "1.1.0";
783
+
781
784
  // src/parsers/junit-parser.ts
782
785
  var import_fast_xml_parser = require("fast-xml-parser");
783
786
  var import_node_fs5 = require("fs");
@@ -1877,6 +1880,30 @@ function collectArtifacts(testOutputDir, debugOutputDir) {
1877
1880
  }
1878
1881
 
1879
1882
  // src/timeline-builder.ts
1883
+ function mapStepKind(category) {
1884
+ switch (category) {
1885
+ case "interaction":
1886
+ return "interaction";
1887
+ case "assertion":
1888
+ return "assertion";
1889
+ case "ai":
1890
+ return "ai";
1891
+ case "navigation":
1892
+ return "navigation";
1893
+ case "device":
1894
+ return "device";
1895
+ case "media":
1896
+ return "media";
1897
+ case "script":
1898
+ return "script";
1899
+ case "flow_control":
1900
+ return "flow_control";
1901
+ case "recording":
1902
+ return "media";
1903
+ default:
1904
+ return "other";
1905
+ }
1906
+ }
1880
1907
  function mapCommandCategory(category) {
1881
1908
  switch (category) {
1882
1909
  case "interaction":
@@ -1965,6 +1992,8 @@ function commandToAction(cmd, flowStartedMs) {
1965
1992
  return {
1966
1993
  title: buildStepTitle(cmd),
1967
1994
  category: mapCommandCategory(cmd.category),
1995
+ kind: mapStepKind(cmd.category),
1996
+ selector: cmd.selector ?? null,
1968
1997
  status: cmd.status === "failed" ? "failed" : "passed",
1969
1998
  duration: cmd.duration,
1970
1999
  timestamp: cmd.timestamp,
@@ -1979,6 +2008,40 @@ function sortActions(a, b) {
1979
2008
  if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
1980
2009
  return 0;
1981
2010
  }
2011
+ function flowDevice(flow) {
2012
+ return {
2013
+ platform: flow.platform,
2014
+ // Maestro never reports an OS version or a UDID distinct from the device
2015
+ // id; saying so with null beats inventing a value.
2016
+ platformVersion: null,
2017
+ deviceName: flow.deviceId ?? null,
2018
+ automationName: "maestro",
2019
+ appId: flow.appId,
2020
+ appTestMode: flow.platform === "web" ? "mobile-web" : "native",
2021
+ udid: flow.deviceId ?? null
2022
+ };
2023
+ }
2024
+ function flowLogs(flow) {
2025
+ return flow.logEntries.map((e) => ({
2026
+ level: e.level === "ERROR" ? "error" : e.level === "WARN" ? "warn" : e.level === "DEBUG" ? "debug" : "info",
2027
+ text: e.message,
2028
+ timestamp: e.timestamp,
2029
+ location: null,
2030
+ source: "driver",
2031
+ tag: e.source ?? null,
2032
+ pid: null,
2033
+ stackTrace: null
2034
+ }));
2035
+ }
2036
+ function flowDefects(flow) {
2037
+ return flow.aiDefects.map((d) => ({
2038
+ type: d.type,
2039
+ severity: d.severity,
2040
+ description: d.description,
2041
+ location: d.location,
2042
+ screenshot: d.screenshot
2043
+ }));
2044
+ }
1982
2045
  function buildTestResult(flow, apiCalls) {
1983
2046
  const flowStartedMs = Date.parse(flow.startedAt);
1984
2047
  const orderedCommands = [...flow.commands].sort((a, b) => {
@@ -2004,12 +2067,12 @@ function buildTestResult(flow, apiCalls) {
2004
2067
  actions.sort(sortActions);
2005
2068
  const artifacts = flow.screenshotPaths.length > 0 || flow.videoPath ? {
2006
2069
  screenshot: flow.screenshotPaths[0] ?? void 0,
2070
+ // The flow's whole strip, not just its first frame. The Screenshots
2071
+ // tab used to ignore the flow entirely and render the run's shots.
2072
+ ...flow.screenshotPaths.length > 0 ? { screenshots: [...flow.screenshotPaths] } : {},
2007
2073
  video: flow.videoPath ?? void 0
2008
2074
  } : null;
2009
2075
  const tags = [...flow.tags];
2010
- if (flow.platform !== "unknown" && !tags.includes(flow.platform)) {
2011
- tags.push(flow.platform);
2012
- }
2013
2076
  return {
2014
2077
  title: flow.flowName,
2015
2078
  status: flow.status,
@@ -2032,7 +2095,11 @@ function buildTestResult(flow, apiCalls) {
2032
2095
  apiCalls: apiCalls.length > 0 ? [...apiCalls] : null,
2033
2096
  apiAssertions: null,
2034
2097
  actions: actions.length > 0 ? actions : null,
2035
- consoleLogs: null
2098
+ // The driver log reached the cloud payload and nothing else; the local
2099
+ // report showed no log lines at all.
2100
+ consoleLogs: flow.logEntries.length > 0 ? flowLogs(flow) : null,
2101
+ device: flowDevice(flow),
2102
+ aiDefects: flow.aiDefects.length > 0 ? flowDefects(flow) : null
2036
2103
  };
2037
2104
  }
2038
2105
  function buildTimelineEntry(flow, apiCallsByFlow) {
@@ -2598,13 +2665,11 @@ var currentFilter='all';
2598
2665
  var searchQuery='';
2599
2666
  var openFlowIdx=-1;
2600
2667
 
2668
+ /* The platform is a device fact now, not a tag the builder appended. */
2601
2669
  function detectPlatform(){
2602
2670
  for(var i=0;i<flows.length;i++){
2603
- var tags=flows[i].test.tags||[];
2604
- for(var j=0;j<tags.length;j++){
2605
- var t=tags[j].toLowerCase();
2606
- if(t==='android'||t==='ios'||t==='web')return t;
2607
- }
2671
+ var d=flows[i].test.device;
2672
+ if(d&&d.platform&&d.platform!=='unknown')return d.platform;
2608
2673
  }
2609
2674
  return '';
2610
2675
  }
@@ -2880,18 +2945,28 @@ function closeDrawer(){
2880
2945
  }
2881
2946
 
2882
2947
  function renderDrawerContent(flow){
2883
- var tabs=['Overview','Steps','Assertions','Screenshots','AI Defects'];
2948
+ var t=flow.test;
2949
+ /* Every tab is now about THIS flow. Screenshots and AI Defects used to take
2950
+ no flow argument at all and showed the whole run on every flow. */
2951
+ var tabs=[
2952
+ {name:'Overview',html:function(){return renderOverviewTab(flow);}},
2953
+ {name:'Steps',html:function(){return renderStepsTab(flow);}},
2954
+ {name:'Replay',html:function(){return renderReplay(t);}},
2955
+ {name:'Assertions',html:function(){return renderAssertionsTab(flow);}},
2956
+ {name:'Driver log',n:(t.consoleLogs||[]).length,html:function(){return renderDriverLogTab(t);}},
2957
+ {name:'Screenshots',n:testScreenshots(t).length,html:function(){return renderShotStrip(t);}},
2958
+ {name:'AI Defects',n:(t.aiDefects||[]).length,html:function(){return renderFlowAiDefects(t);}}
2959
+ ];
2960
+
2884
2961
  var h='<div class="seg-ctrl">';
2885
- tabs.forEach(function(name,i){
2886
- h+='<button class="seg-btn'+(i===0?' active':'')+'" data-seg="tab'+i+'">'+name+'</button>';
2962
+ tabs.forEach(function(tab,i){
2963
+ var label=tab.name+(tab.n?' ('+tab.n+')':'');
2964
+ h+='<button class="seg-btn'+(i===0?' active':'')+'" data-seg="tab'+i+'">'+label+'</button>';
2887
2965
  });
2888
2966
  h+='</div>';
2889
-
2890
- h+='<div class="seg-pane active" data-seg-pane="tab0">'+renderOverviewTab(flow)+'</div>';
2891
- h+='<div class="seg-pane" data-seg-pane="tab1">'+renderStepsTab(flow)+'</div>';
2892
- h+='<div class="seg-pane" data-seg-pane="tab2">'+renderAssertionsTab(flow)+'</div>';
2893
- h+='<div class="seg-pane" data-seg-pane="tab3">'+renderScreenshotsTab()+'</div>';
2894
- h+='<div class="seg-pane" data-seg-pane="tab4">'+renderAiDefectsTab()+'</div>';
2967
+ tabs.forEach(function(tab,i){
2968
+ h+='<div class="seg-pane'+(i===0?' active':'')+'" data-seg-pane="tab'+i+'">'+tab.html()+'</div>';
2969
+ });
2895
2970
 
2896
2971
  $('drawer-body').innerHTML=h;
2897
2972
  }
@@ -3044,7 +3119,7 @@ function renderStepsTab(flow){
3044
3119
  var dotCls='cmd-dot '+(isFailed?'cmd-dot-fail':'cmd-dot-pass');
3045
3120
  h+='<div class="'+rowCls+'">';
3046
3121
  h+='<div class="'+dotCls+'"></div>';
3047
- h+=getCmdBadge(a.category);
3122
+ h+=a.kind?'<span class="cmd-kind k-'+esc(a.kind)+'">'+esc(a.kind)+'</span>':getCmdBadge(a.category);
3048
3123
  h+='<div class="cmd-title" title="'+esc(a.title)+'">'+esc(a.title)+'</div>';
3049
3124
  if(isSlow||isFailed&&a.duration>2000)h+='<span class="cmd-badge cmd-badge-slow">SLOW</span>';
3050
3125
  h+='<div class="cmd-dur">'+fmtDur(a.duration)+'</div>';
@@ -3212,7 +3287,10 @@ function renderScreenshotsTab(){
3212
3287
  var name=p.split(/[\\/\\\\]/).pop()||'screenshot';
3213
3288
  var isFail=name.toLowerCase().indexOf('fail')>=0||name.toLowerCase().indexOf('error')>=0;
3214
3289
  h+='<div class="screenshot-card'+(isFail?' failure-shot':'')+'" data-lb-idx="'+i+'">'
3215
- +'<img src="'+esc(p)+'" alt="'+esc(name)+'" loading="lazy" onerror="this.parentElement.style.display=\\'none\\'"/>'
3290
+ /* No lazy loading: this grid lives in a seg-pane that starts hidden, and
3291
+ Chrome never fetches a lazy image inside display:none \u2014 the cards came
3292
+ up blank until something else forced a reflow. */
3293
+ +'<img src="'+esc(p)+'" alt="'+esc(name)+'" onerror="this.parentElement.style.display=\\'none\\'"/>'
3216
3294
  +'<div class="screenshot-card-label">'+(isFail?'Failure Screenshot':esc(name))+'</div></div>';
3217
3295
  });
3218
3296
  h+='</div>';
@@ -3359,6 +3437,578 @@ document.addEventListener('keydown',function(e){
3359
3437
  });
3360
3438
  `;
3361
3439
 
3440
+ // src/html-js-mobile.ts
3441
+ var JS_MOBILE = `
3442
+ function vNum(v){return typeof v==='number'?v:0;}
3443
+
3444
+ /* Every flow's TestResult, in the order the list shows them. */
3445
+ function flowTests(){
3446
+ var out=[];
3447
+ for(var i=0;i<flows.length;i++)out.push(flows[i].test);
3448
+ return out;
3449
+ }
3450
+
3451
+ /* \u2500\u2500 Device identity \u2500\u2500 */
3452
+
3453
+ var DEVICE_GLYPH={
3454
+ android:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="5" y="2" width="14" height="20" rx="2.5"/><path d="M10 18.5h4"/></svg>',
3455
+ ios:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 19h4"/></svg>',
3456
+ web:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 010 18a15 15 0 010-18"/></svg>'
3457
+ };
3458
+
3459
+ function deviceGlyph(p){
3460
+ return DEVICE_GLYPH[p]||'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="5" y="2" width="14" height="20" rx="2.5"/></svg>';
3461
+ }
3462
+
3463
+ function deviceHeadline(d){
3464
+ if(!d)return '';
3465
+ var os=d.platform==='ios'?'iOS':d.platform==='android'?'Android':d.platform==='web'?'Web':'Device';
3466
+ return d.platformVersion?os+' '+d.platformVersion:os;
3467
+ }
3468
+
3469
+ function shortAppId(v){
3470
+ var s=String(v);
3471
+ if(s.indexOf('/')<0&&s.indexOf('\\\\')<0)return s;
3472
+ var parts=s.split(/[\\/\\\\]/);
3473
+ return parts[parts.length-1]||s;
3474
+ }
3475
+
3476
+ function renderDeviceStrip(){
3477
+ var el=$('device-strip');
3478
+ if(!el)return;
3479
+ var ts=flowTests(),d=null,seen={},count=0;
3480
+ for(var i=0;i<ts.length;i++){
3481
+ var dv=ts[i].device;
3482
+ if(!dv||dv.platform==='unknown')continue;
3483
+ if(!d)d=dv;
3484
+ var key=(dv.platform||'')+'|'+(dv.deviceName||'');
3485
+ if(!seen[key]){seen[key]=1;count++;}
3486
+ }
3487
+ if(!d){el.innerHTML='';return;}
3488
+
3489
+ var h='<div class="devstrip"><div class="dev-id">';
3490
+ h+='<span class="dev-icon dev-icon-'+esc(d.platform||'unknown')+'">'+deviceGlyph(d.platform)+'</span>';
3491
+ h+='<span><span class="dev-name">'+esc(deviceHeadline(d))+'</span><br>';
3492
+ h+='<span class="dev-sub">'+esc(d.deviceName||'device not named')+'</span></span>';
3493
+ h+='</div><span class="dev-rule"></span><div class="dev-facts">';
3494
+ var facts=[{k:'Driver',v:d.automationName},{k:'App',v:d.appId},{k:'Mode',v:d.appTestMode}];
3495
+ for(var f=0;f<facts.length;f++){
3496
+ if(!facts[f].v)continue;
3497
+ h+='<span class="dev-fact"><span class="dev-fact-k">'+esc(facts[f].k)+'</span>';
3498
+ h+='<span class="dev-fact-v">'+esc(shortAppId(facts[f].v))+'</span></span>';
3499
+ }
3500
+ h+='</div>';
3501
+ if(count>1)h+='<span class="dev-more">'+count+' devices in this run</span>';
3502
+ el.innerHTML=h+'</div>';
3503
+ }
3504
+
3505
+ function deviceBadge(d){
3506
+ if(!d||!d.platform||d.platform==='unknown')return '';
3507
+ var cls=d.platform==='ios'?'trb trb-device-ios':d.platform==='web'?'trb trb-device-web':'trb trb-device';
3508
+ var label=deviceHeadline(d);
3509
+ if(d.deviceName)label+=' \\u00b7 '+d.deviceName;
3510
+ return '<span class="'+cls+'" title="'+esc(label)+'">'+esc(label)+'</span>';
3511
+ }
3512
+
3513
+ /* \u2500\u2500 Mobile cards \u2500\u2500 */
3514
+
3515
+ var KIND_ORDER=['interaction','assertion','gesture','lifecycle','device','keyboard','navigation','find','alert','context','screenshot','media','script','flow_control','ai','other'];
3516
+ var KIND_COLOR={interaction:'#60a5fa',assertion:'#34d399',gesture:'#e879f9',lifecycle:'#fbbf24',device:'#fb923c',keyboard:'#f59e0b',navigation:'#a78bfa',find:'#8b949e',alert:'#ef4444',context:'#484f58',screenshot:'#60a5fa',media:'#60a5fa',script:'#484f58',flow_control:'#484f58',ai:'#2dd4a8',other:'#484f58'};
3517
+
3518
+ function countKinds(steps,into){
3519
+ for(var i=0;i<steps.length;i++){
3520
+ var k=steps[i].kind||'other';
3521
+ into[k]=(into[k]||0)+1;
3522
+ if(steps[i].children&&steps[i].children.length)countKinds(steps[i].children,into);
3523
+ }
3524
+ }
3525
+
3526
+ function renderStepKindCard(){
3527
+ var ts=flowTests(),counts={},total=0,k;
3528
+ for(var i=0;i<ts.length;i++)countKinds(ts[i].actions||[],counts);
3529
+ for(k in counts){if(counts.hasOwnProperty(k))total+=counts[k];}
3530
+ if(total===0)return '';
3531
+ var present=[];
3532
+ for(var o=0;o<KIND_ORDER.length;o++){
3533
+ if(counts[KIND_ORDER[o]])present.push({k:KIND_ORDER[o],n:counts[KIND_ORDER[o]]});
3534
+ }
3535
+ present.sort(function(a,b){return b.n-a.n;});
3536
+ var max=present[0].n||1;
3537
+ var h='<div class="tcard"><div class="tcard-h">By step category<span class="tcard-sub">'+total+' steps</span></div>';
3538
+ for(var p=0;p<Math.min(present.length,7);p++){
3539
+ var c=KIND_COLOR[present[p].k]||'#8b949e';
3540
+ var pct=Math.max(2,Math.round((present[p].n/max)*100));
3541
+ h+='<div class="krow"><span class="krow-k" style="color:'+c+'">'+esc(present[p].k)+'</span>';
3542
+ h+='<span class="krow-track"><span class="krow-fill" style="width:'+pct+'%;background:'+c+'"></span></span>';
3543
+ h+='<span class="krow-n">'+present[p].n+'</span></div>';
3544
+ }
3545
+ return h+'</div>';
3546
+ }
3547
+
3548
+ function renderDriverLogCard(){
3549
+ var ts=flowTests(),total=0,errors=0,warns=0,sample=null;
3550
+ for(var i=0;i<ts.length;i++){
3551
+ var logs=ts[i].consoleLogs||[];
3552
+ total+=logs.length;
3553
+ for(var j=0;j<logs.length;j++){
3554
+ if(logs[j].level==='error'){errors++;if(!sample)sample=logs[j];}
3555
+ else if(logs[j].level==='warn')warns++;
3556
+ }
3557
+ }
3558
+ if(total===0)return '';
3559
+ var other=Math.max(0,total-errors-warns);
3560
+ var h='<div class="tcard"><div class="tcard-h">Driver log<span class="tcard-sub">'+total+' lines</span></div>';
3561
+ h+='<div class="verdict-bar">';
3562
+ if(errors)h+='<span style="flex:'+errors+';background:#ef4444;border-radius:999px"></span>';
3563
+ if(warns)h+='<span style="flex:'+warns+';background:#f59e0b;border-radius:999px"></span>';
3564
+ if(other)h+='<span style="flex:'+other+';background:rgba(255,255,255,.08);border-radius:999px"></span>';
3565
+ h+='</div><div class="lgrid">';
3566
+ h+='<div class="lrow"><span class="lrow-src" style="background:rgba(239,68,68,.12);color:#ef4444">error</span><span class="lrow-l">lines</span><span class="lrow-n" style="color:#ef4444">'+errors+'</span></div>';
3567
+ h+='<div class="lrow"><span class="lrow-src" style="background:rgba(245,158,11,.12);color:#f59e0b">warn</span><span class="lrow-l">lines</span><span class="lrow-n" style="color:#f59e0b">'+warns+'</span></div>';
3568
+ h+='</div>';
3569
+ if(sample)h+='<div class="lsample">'+esc(String(sample.text||''))+'</div>';
3570
+ return h+'</div>';
3571
+ }
3572
+
3573
+ function renderAiDefectCard(){
3574
+ var ts=flowTests(),bySev={critical:0,warning:0,info:0},total=0;
3575
+ for(var i=0;i<ts.length;i++){
3576
+ var ds=ts[i].aiDefects||[];
3577
+ for(var j=0;j<ds.length;j++){total++;bySev[ds[j].severity]=(bySev[ds[j].severity]||0)+1;}
3578
+ }
3579
+ if(total===0)return '';
3580
+ var h='<div class="tcard"><div class="tcard-h">AI defects</div>';
3581
+ h+='<div class="tbig"><span class="tbig-n">'+total+'</span><span class="tbig-l">found</span></div><div class="lgrid">';
3582
+ var rows=[{k:'critical',c:'#ef4444'},{k:'warning',c:'#f59e0b'},{k:'info',c:'#60a5fa'}];
3583
+ for(var r=0;r<rows.length;r++){
3584
+ h+='<div class="lrow"><span class="vc-dot" style="background:'+rows[r].c+'"></span>';
3585
+ h+='<span class="lrow-l">'+rows[r].k+'</span><span class="lrow-n">'+(bySev[rows[r].k]||0)+'</span></div>';
3586
+ }
3587
+ return h+'</div></div>';
3588
+ }
3589
+
3590
+ function renderTypeBand(){
3591
+ var el=$('type-band');
3592
+ if(!el)return;
3593
+ var cards=[];
3594
+ var a=renderStepKindCard();if(a)cards.push(a);
3595
+ var b=renderDriverLogCard();if(b)cards.push(b);
3596
+ var c=renderAiDefectCard();if(c)cards.push(c);
3597
+ el.innerHTML=cards.length?'<div class="tband">'+cards.join('')+'</div>':'';
3598
+ }
3599
+
3600
+ /* \u2500\u2500 Needs attention \u2500\u2500 */
3601
+
3602
+ function failingStep(t){
3603
+ var acts=t.actions||[];
3604
+ for(var i=0;i<acts.length;i++)if(acts[i].status==='failed')return acts[i];
3605
+ return null;
3606
+ }
3607
+
3608
+ function correlatedLog(t,atMs){
3609
+ var logs=t.consoleLogs||[],best=null,bestGap=Infinity;
3610
+ for(var i=0;i<logs.length;i++){
3611
+ var e=logs[i];
3612
+ if(e.level!=='error')continue;
3613
+ var ms=Date.parse(e.timestamp);
3614
+ if(!isFinite(ms))continue;
3615
+ var gap=Math.abs(ms-atMs);
3616
+ if(gap<bestGap&&gap<=5000){best=e;bestGap=gap;}
3617
+ }
3618
+ return best;
3619
+ }
3620
+
3621
+ function mobileEvidenceChips(t){
3622
+ var h='',acts=t.actions||[],gestures=0,failedSteps=0,i;
3623
+ for(i=0;i<acts.length;i++){
3624
+ if(acts[i].kind==='gesture')gestures++;
3625
+ if(acts[i].status==='failed')failedSteps++;
3626
+ }
3627
+ if(acts.length>0)h+='<span class="nchip'+(failedSteps>0?' nchip-bad':'')+'">'+acts.length+' step'+(acts.length===1?'':'s')+(failedSteps>0?' \\u00b7 '+failedSteps+' failed':'')+'</span>';
3628
+ if(gestures>0)h+='<span class="nchip nchip-gesture">'+gestures+' gesture'+(gestures===1?'':'s')+'</span>';
3629
+ var errs=0,logs=t.consoleLogs||[];
3630
+ for(i=0;i<logs.length;i++)if(logs[i].level==='error')errs++;
3631
+ if(errs>0)h+='<span class="nchip nchip-bad">'+errs+' log error'+(errs===1?'':'s')+'</span>';
3632
+ var shots=testScreenshots(t).length;
3633
+ if(shots>0)h+='<span class="nchip">'+shots+' screenshot'+(shots===1?'':'s')+'</span>';
3634
+ if(t.artifacts&&t.artifacts.video)h+='<span class="nchip">recording</span>';
3635
+ var defects=(t.aiDefects||[]).length;
3636
+ if(defects>0)h+='<span class="nchip nchip-bad">'+defects+' AI defect'+(defects===1?'':'s')+'</span>';
3637
+ return h;
3638
+ }
3639
+
3640
+ function renderNeedsAttention(){
3641
+ var el=$('needs-attention');
3642
+ if(!el)return;
3643
+ var bad=[],i;
3644
+ for(i=0;i<flows.length;i++){
3645
+ var st=flows[i].test.status;
3646
+ if(st==='failed'||st==='timedout'||st==='flaky')bad.push(flows[i]);
3647
+ }
3648
+ if(bad.length===0){el.innerHTML='';return;}
3649
+
3650
+ var h='<div class="needs"><div class="needs-head"><span class="needs-title">Needs attention</span>';
3651
+ h+='<span class="needs-count">'+bad.length+'</span></div><div class="needs-list">';
3652
+ for(var j=0;j<Math.min(bad.length,8);j++){
3653
+ var t=bad[j].test;
3654
+ h+='<div class="needs-card" data-flow-idx="'+bad[j].idx+'">';
3655
+ h+='<div class="needs-body"><div class="needs-row1"><span class="needs-name">'+esc(t.title)+'</span>';
3656
+ h+='<span class="trb trb-type">mobile</span>'+deviceBadge(t.device);
3657
+ h+='<span class="needs-file">'+esc(t.filePath)+'</span></div>';
3658
+
3659
+ var step=failingStep(t);
3660
+ if(step){
3661
+ h+='<div class="needs-evidence"><div class="needs-step">';
3662
+ h+='<span class="cmd-kind k-'+esc(step.kind||'other')+'">'+esc(step.kind||'step')+'</span>';
3663
+ h+='<span class="needs-step-t">'+esc(step.title||'')+'</span>';
3664
+ h+='<span class="needs-step-d">'+fmtDur(step.duration||0)+'</span></div>';
3665
+ if(step.error)h+='<div class="needs-cause">'+esc(String(step.error))+'</div>';
3666
+ var log=correlatedLog(t,Date.parse(step.timestamp));
3667
+ if(log){
3668
+ var ts='';
3669
+ try{ts=new Date(log.timestamp).toISOString().substr(11,12);}catch(e){ts='';}
3670
+ h+='<div class="needs-log"><span class="needs-log-t">driver log \\u00b7 '+esc(ts)+'</span>&nbsp;&nbsp;'+esc(String(log.text||''))+'</div>';
3671
+ }
3672
+ h+='</div>';
3673
+ }else if(t.failure&&t.failure.message){
3674
+ h+='<div class="needs-cause">'+esc(String(t.failure.message).split('\\n')[0])+'</div>';
3675
+ }
3676
+
3677
+ h+='<div class="needs-chips">'+mobileEvidenceChips(t)+'</div></div>';
3678
+ h+='<div class="needs-right"><div class="needs-dur">'+fmtDur(t.duration)+'</div>';
3679
+ h+='<div class="needs-open">Open<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M9 18l6-6-6-6"/></svg></div></div></div>';
3680
+ }
3681
+ el.innerHTML=h+'</div></div>';
3682
+ }
3683
+
3684
+ /* \u2500\u2500 Per-flow evidence \u2500\u2500 */
3685
+
3686
+ /* Screenshots belong to the flow that produced them. The Screenshots tab
3687
+ took no flow argument and showed the whole run's shots on every flow. */
3688
+ function testScreenshots(t){
3689
+ var a=t.artifacts;
3690
+ if(a&&a.screenshots&&a.screenshots.length)return a.screenshots;
3691
+ if(a&&a.screenshot)return [a.screenshot];
3692
+ return [];
3693
+ }
3694
+
3695
+ function renderShotStrip(t){
3696
+ var shots=testScreenshots(t);
3697
+ if(shots.length===0)return '<div class="artifact-empty">No screenshots for this flow</div>';
3698
+ var h='<div class="shot-strip">';
3699
+ for(var i=0;i<shots.length;i++){
3700
+ var src=String(shots[i]);
3701
+ var name=src.split(/[\\/\\\\]/).pop()||('shot '+(i+1));
3702
+ var isFail=/fail|error/i.test(name);
3703
+ h+='<div class="shot'+(isFail?' is-failure':'')+'" data-lightbox="'+esc(src)+'">';
3704
+ h+='<img src="'+esc(src)+'" alt="'+esc(name)+'">';
3705
+ h+='<div class="shot-cap" title="'+esc(name)+'">'+esc(name)+'</div></div>';
3706
+ }
3707
+ return h+'</div>';
3708
+ }
3709
+
3710
+ /* Steps against the recording, using the videoOffset the timeline builder
3711
+ has always computed and the local report never read. */
3712
+ function renderReplay(t){
3713
+ var video=t.artifacts&&t.artifacts.video;
3714
+ var acts=t.actions||[];
3715
+ if(!video&&acts.length===0)return '<div class="artifact-empty">No recording for this flow</div>';
3716
+
3717
+ var withOffsets=[],i,maxOff=0;
3718
+ for(i=0;i<acts.length;i++){
3719
+ if(typeof acts[i].videoOffset==='number'){
3720
+ withOffsets.push(acts[i]);
3721
+ if(acts[i].videoOffset>maxOff)maxOff=acts[i].videoOffset;
3722
+ }
3723
+ }
3724
+ var span=maxOff>0?maxOff:(t.duration||0)/1000;
3725
+ if(span<=0)span=1;
3726
+
3727
+ var h='<div class="replay">';
3728
+ if(video)h+='<div class="replay-video"><video id="replay-video" src="'+esc(video)+'" controls preload="metadata"></video></div>';
3729
+ h+='<div class="replay-side">';
3730
+ if(withOffsets.length>0){
3731
+ h+='<div class="scrub">';
3732
+ for(i=0;i<withOffsets.length;i++){
3733
+ var w=withOffsets[i],pct=Math.min(99,Math.max(0,(w.videoOffset/span)*100));
3734
+ var failed=w.status==='failed';
3735
+ var col=failed?'#ef4444':(KIND_COLOR[w.kind||'other']||'#8b949e');
3736
+ h+='<span class="scrub-mark'+(failed?' is-failed':'')+'" style="left:'+pct.toFixed(2)+'%;background:'+col+'" data-seek="'+w.videoOffset.toFixed(2)+'" title="'+esc(w.title||'')+'"></span>';
3737
+ }
3738
+ h+='</div><div class="scrub-legend"><span>0.0s</span><span>'+span.toFixed(1)+'s</span></div>';
3739
+ }
3740
+ h+='<div class="replay-steps">';
3741
+ for(i=0;i<acts.length;i++){
3742
+ var a=acts[i],failed2=a.status==='failed';
3743
+ h+='<div class="rstep'+(failed2?' is-failed':'')+'"'+(typeof a.videoOffset==='number'?' data-seek="'+a.videoOffset.toFixed(2)+'"':'')+'>';
3744
+ h+='<span class="rstep-n">'+(i+1<10?'0':'')+(i+1)+'</span>';
3745
+ h+='<span class="cmd-kind k-'+esc(a.kind||'other')+'">'+esc(a.kind||'step')+'</span>';
3746
+ h+='<span class="rstep-t" title="'+esc(a.title||'')+'">'+esc(a.title||'')+'</span>';
3747
+ if(typeof a.videoOffset==='number')h+='<span class="rstep-off">'+a.videoOffset.toFixed(1)+'s</span>';
3748
+ h+='<span class="rstep-d">'+fmtDur(a.duration||0)+'</span></div>';
3749
+ }
3750
+ return h+'</div></div></div>';
3751
+ }
3752
+
3753
+ /* The driver log, per flow. Maestro's report had no log surface at all. */
3754
+ function renderDriverLogTab(t){
3755
+ var logs=t.consoleLogs||[];
3756
+ if(logs.length===0)return '<div class="artifact-empty">No driver log lines for this flow</div>';
3757
+ var errs=0,warns=0,i;
3758
+ for(i=0;i<logs.length;i++){
3759
+ if(logs[i].level==='error')errs++;else if(logs[i].level==='warn')warns++;
3760
+ }
3761
+ var h='<div class="console-panel"><div class="console-header">';
3762
+ h+='<span class="console-title">Driver log ('+logs.length+')</span>';
3763
+ if(errs>0)h+='<span class="apisec-badge ab-bad" style="background:rgba(239,68,68,.12);color:#ef4444;font-size:10px;font-weight:700;padding:1px 7px;border-radius:4px">'+errs+' errors</span>';
3764
+ if(warns>0)h+='<span style="background:rgba(245,158,11,.12);color:#f59e0b;font-size:10px;font-weight:700;padding:1px 7px;border-radius:4px">'+warns+' warnings</span>';
3765
+ h+='</div><div class="console-list">';
3766
+ var SHOW=200;
3767
+ for(i=0;i<Math.min(logs.length,SHOW);i++)h+=renderLogRow(logs[i]);
3768
+ h+='</div>';
3769
+ if(logs.length>SHOW)h+='<div class="console-more">'+(logs.length-SHOW)+' more lines not shown</div>';
3770
+ return h+'</div>';
3771
+ }
3772
+
3773
+ function renderLogRow(e){
3774
+ var lv=(e.level||'info').toLowerCase();
3775
+ var color=lv==='error'?'#f87171':lv==='warn'?'#fbbf24':lv==='debug'?'#a78bfa':'#60a5fa';
3776
+ var badge=lv==='error'?'E':lv==='warn'?'W':lv==='debug'?'D':'I';
3777
+ var h='<div class="console-entry console-level-'+esc(lv)+'" data-console-level="'+esc(lv)+'">';
3778
+ h+='<span class="console-level-badge" style="color:'+color+'">'+badge+'</span>';
3779
+ var ts='';
3780
+ try{ts=new Date(e.timestamp).toISOString().substr(11,12);}catch(err){ts=String(e.timestamp||'');}
3781
+ h+='<span class="console-time">'+esc(ts)+'</span>';
3782
+ if(e.tag)h+='<span class="console-tag" title="'+esc(String(e.tag))+'">'+esc(String(e.tag))+'</span>';
3783
+ h+='<span class="console-text">'+esc(String(e.text||''))+'</span></div>';
3784
+ return h;
3785
+ }
3786
+
3787
+ function renderFlowAiDefects(t){
3788
+ var defects=t.aiDefects||[];
3789
+ if(defects.length===0)return '<div class="artifact-empty">No AI defects for this flow</div>';
3790
+ var h='';
3791
+ for(var i=0;i<defects.length;i++){
3792
+ var d=defects[i];
3793
+ h+='<div class="aidef aidef-'+esc(d.severity||'info')+'">';
3794
+ if(d.screenshot)h+='<div class="aidef-shot"><img src="'+esc(d.screenshot)+'" alt=""></div>';
3795
+ h+='<div class="aidef-body"><div class="aidef-pills">';
3796
+ h+='<span class="aidef-sev aidef-sev-'+esc(d.severity||'info')+'">'+esc(d.severity||'info')+'</span>';
3797
+ h+='<span class="aidef-type">'+esc(d.type||'ui')+'</span></div>';
3798
+ h+='<div class="aidef-desc">'+esc(d.description||'')+'</div>';
3799
+ if(d.location)h+='<div class="aidef-loc">'+esc(d.location)+'</div>';
3800
+ h+='</div></div>';
3801
+ }
3802
+ return h;
3803
+ }
3804
+
3805
+ /* Clicking a step or a scrub mark seeks the recording. */
3806
+ document.addEventListener('click',function(e){
3807
+ var el=e.target.closest?e.target.closest('[data-seek]'):null;
3808
+ if(el){
3809
+ var v=document.getElementById('replay-video');
3810
+ if(v){
3811
+ var at=parseFloat(el.getAttribute('data-seek'));
3812
+ if(isFinite(at)){try{v.currentTime=at;}catch(err){}}
3813
+ }
3814
+ return;
3815
+ }
3816
+ var card=e.target.closest?e.target.closest('.needs-card'):null;
3817
+ if(card){
3818
+ var idx=parseInt(card.getAttribute('data-flow-idx'),10);
3819
+ if(!isNaN(idx)&&typeof openDrawer==='function')openDrawer(idx);
3820
+ }
3821
+ });
3822
+ `;
3823
+
3824
+ // src/html-css-mobile.ts
3825
+ var CSS_MOBILE = `
3826
+ /* \u2500\u2500 Device strip \u2500\u2500 */
3827
+ .devstrip{background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:14px 20px;display:flex;align-items:center;gap:26px;flex-wrap:wrap;margin-bottom:20px}
3828
+ .dev-id{display:flex;align-items:center;gap:11px}
3829
+ .dev-icon{width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
3830
+ .dev-icon-android{background:rgba(52,211,153,.1);border:1px solid rgba(52,211,153,.22);color:#34d399}
3831
+ .dev-icon-ios{background:rgba(96,165,250,.1);border:1px solid rgba(96,165,250,.22);color:#60a5fa}
3832
+ .dev-icon-web{background:rgba(167,139,250,.1);border:1px solid rgba(167,139,250,.22);color:#a78bfa}
3833
+ .dev-icon-unknown{background:rgba(255,255,255,.04);border:1px solid var(--bd);color:var(--fg-1)}
3834
+ .dev-name{font-size:14px;font-weight:700;line-height:1.25}
3835
+ .dev-sub{font-size:10.5px;color:var(--fg-1)}
3836
+ .dev-rule{width:1px;height:30px;background:var(--bd)}
3837
+ .dev-facts{display:flex;gap:28px;flex-wrap:wrap}
3838
+ .dev-fact{display:flex;flex-direction:column;gap:2px}
3839
+ .dev-fact-k{font-size:9.5px;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase}
3840
+ .dev-fact-v{font-size:11.5px;color:var(--fg);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3841
+ .dev-more{margin-left:auto;font-size:11px;color:var(--fg-1);border:1px solid var(--bd);border-radius:8px;padding:5px 11px;display:flex;align-items:center;gap:7px}
3842
+
3843
+ /* Row + drawer device badge */
3844
+ .trb-device{background:rgba(52,211,153,.1);color:#34d399}
3845
+ .trb-device-ios{background:rgba(96,165,250,.1);color:#60a5fa}
3846
+ .trb-device-web{background:rgba(167,139,250,.1);color:#a78bfa}
3847
+
3848
+ /* \u2500\u2500 Verdict band \u2500\u2500 */
3849
+ .verdict{display:flex;gap:16px;align-items:stretch;margin-bottom:20px;flex-wrap:wrap}
3850
+ .verdict-main{flex:1 1 480px;background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:18px 22px;display:flex;flex-direction:column;gap:14px;min-width:0}
3851
+ .verdict-main.v-failed{border-color:rgba(239,68,68,.22)}
3852
+ .verdict-main.v-passed{border-color:rgba(34,197,94,.18)}
3853
+ .verdict-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
3854
+ .verdict-title{font-size:21px;font-weight:700;letter-spacing:-.01em}
3855
+ .verdict-where{font-size:12px;color:var(--fg-1)}
3856
+ .verdict-where code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--fg)}
3857
+ .verdict-dur{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:12px;color:var(--fg-1)}
3858
+ .verdict-bar{display:flex;height:8px;gap:2px}
3859
+ .verdict-bar>span{border-radius:999px}
3860
+ .verdict-bar-empty{flex:1;background:rgba(255,255,255,.06)}
3861
+ .vb-passed{background:#22c55e}.vb-failed{background:#ef4444}.vb-flaky{background:#f59e0b}
3862
+ .vb-timedout{background:#f97316}.vb-skipped{background:#6b7280}
3863
+ .verdict-counts{display:flex;align-items:center;gap:20px;flex-wrap:wrap}
3864
+ .vc{display:flex;align-items:center;gap:7px}
3865
+ .vc-dot{width:9px;height:9px;border-radius:50%}
3866
+ .vc-n{font-size:14px;font-weight:700}
3867
+ .vc-l{font-size:12px;color:var(--fg-1)}
3868
+ .verdict-zeros{margin-left:auto;font-size:11px;color:var(--fg-2)}
3869
+ .verdict-side{width:300px;flex:0 0 auto;background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:12px}
3870
+ .verdict-side-label{font-size:10px;font-weight:600;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase}
3871
+ .verdict-side-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 10px}
3872
+ .vs-n{font-size:17px;font-weight:700}
3873
+ .vs-n small{font-size:11px;font-weight:500;color:var(--fg-1)}
3874
+ .vs-l{font-size:10px;color:var(--fg-1)}
3875
+ .vs-bad{color:#ef4444}
3876
+
3877
+ /* \u2500\u2500 Type band cards \u2500\u2500 */
3878
+ .tband{display:grid;gap:16px;margin-bottom:20px;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
3879
+ .tcard{background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:16px 20px;display:flex;flex-direction:column;gap:12px;min-width:0}
3880
+ .tcard-h{font-size:10px;font-weight:600;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase;display:flex;align-items:center;gap:8px}
3881
+ .tcard-sub{margin-left:auto;font-weight:500;letter-spacing:0;text-transform:none;font-size:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3882
+ .krow{display:flex;align-items:center;gap:10px}
3883
+ .krow-k{width:78px;font-size:11px;font-weight:700}
3884
+ .krow-track{display:block;flex:1;height:6px;border-radius:999px;background:rgba(255,255,255,.06);overflow:hidden}
3885
+ .krow-fill{display:block;height:100%;border-radius:999px;min-width:3px}
3886
+ .krow-n{font-size:11px;color:var(--fg-1);width:34px;text-align:right}
3887
+ .lgrid{display:flex;flex-direction:column;gap:7px}
3888
+ .lrow{display:flex;align-items:center;gap:9px}
3889
+ .lrow-src{font-size:9px;font-weight:700;padding:1px 6px;border-radius:4px}
3890
+ .lrow-l{font-size:11px;color:var(--fg-1);flex:1}
3891
+ .lrow-n{font-size:12px;font-weight:700}
3892
+ .lsample{font-size:10px;color:var(--fg-2);border-top:1px solid var(--bd);padding-top:8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3893
+ .tbig{display:flex;align-items:baseline;gap:8px}
3894
+ .tbig-n{font-size:28px;font-weight:800;line-height:1}
3895
+ .tbig-l{font-size:11px;color:var(--fg-1)}
3896
+
3897
+ /* \u2500\u2500 Needs attention \u2500\u2500 */
3898
+ .needs{margin-bottom:20px}
3899
+ .needs-head{display:flex;align-items:center;gap:8px;padding:0 2px 10px}
3900
+ .needs-title{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
3901
+ .needs-count{font-size:10px;font-weight:700;color:#ef4444;background:rgba(239,68,68,.12);padding:1px 7px;border-radius:4px}
3902
+ .needs-list{display:flex;flex-direction:column;gap:10px}
3903
+ .needs-card{background:var(--bg-1);border:1px solid rgba(239,68,68,.2);border-left:3px solid #ef4444;border-radius:10px;padding:14px 18px;display:flex;gap:20px;align-items:flex-start;cursor:pointer}
3904
+ .needs-card:hover{border-color:rgba(239,68,68,.4)}
3905
+ .needs-card.nc-flaky{border-color:rgba(245,158,11,.2);border-left-color:#f59e0b}
3906
+ .needs-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:9px}
3907
+ .needs-row1{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
3908
+ .needs-name{font-size:14px;font-weight:600}
3909
+ .needs-file{font-size:11px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3910
+ .needs-cause{font-size:12.5px;color:var(--fg-err)}
3911
+ .needs-cause em{font-style:normal}
3912
+ .needs-evidence{background:var(--bg-code);border:1px solid var(--bd);border-radius:8px;padding:10px 13px;display:flex;flex-direction:column;gap:7px}
3913
+ .needs-step{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
3914
+ .needs-step-t{font-size:11px;color:#d4d4d4;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3915
+ .needs-step-d{margin-left:auto;font-size:10px;color:var(--fg-2)}
3916
+ .needs-log{font-size:10.5px;color:#fb923c;border-top:1px solid var(--bd);padding-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3917
+ .needs-log-t{color:var(--fg-2)}
3918
+ .needs-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
3919
+ .nchip{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--bd);padding:2px 8px;border-radius:999px}
3920
+ .nchip-bad{color:#ef4444;background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.22)}
3921
+ .nchip-gesture{color:#e879f9;background:rgba(232,121,249,.1);border-color:rgba(232,121,249,.22)}
3922
+ .needs-right{display:flex;flex-direction:column;align-items:flex-end;gap:8px;flex-shrink:0}
3923
+ .needs-dur{font-size:13px;font-weight:700;color:var(--fg-1)}
3924
+ .needs-open{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#2dd4a8}
3925
+
3926
+ /* \u2500\u2500 Step kind badges \u2500\u2500 */
3927
+ .cmd-kind{font-size:9px;font-weight:700;padding:1px 6px;border-radius:999px;letter-spacing:.04em;text-transform:uppercase;flex-shrink:0;min-width:60px;text-align:center}
3928
+ .k-gesture{background:rgba(232,121,249,.12);color:#e879f9}
3929
+ .k-interaction{background:rgba(96,165,250,.12);color:#60a5fa}
3930
+ .k-assertion{background:rgba(52,211,153,.12);color:#34d399}
3931
+ .k-lifecycle{background:rgba(251,191,36,.12);color:#fbbf24}
3932
+ .k-device{background:rgba(251,146,60,.12);color:#fb923c}
3933
+ .k-keyboard{background:rgba(245,158,11,.12);color:#f59e0b}
3934
+ .k-navigation{background:rgba(167,139,250,.12);color:#a78bfa}
3935
+ .k-find{background:rgba(255,255,255,.05);color:var(--fg-1)}
3936
+ .k-alert{background:rgba(239,68,68,.12);color:#ef4444}
3937
+ .k-screenshot,.k-media{background:rgba(96,165,250,.1);color:#60a5fa}
3938
+ .k-context,.k-script,.k-flow_control,.k-other{background:rgba(255,255,255,.04);color:var(--fg-2)}
3939
+ .k-ai{background:rgba(3,183,156,.14);color:#2dd4a8}
3940
+ .cmd-selector{font-size:10px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px}
3941
+
3942
+ /* \u2500\u2500 Device log panel \u2500\u2500
3943
+ Maestro had no log surface at all, so it has none of these base classes;
3944
+ the new Driver log tab rendered as unstyled text without them. */
3945
+ .console-panel{border:1px solid var(--bd);border-radius:8px;overflow:hidden;background:var(--bg-1)}
3946
+ .console-header{font-size:11px;font-weight:700;color:var(--fg-1);text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--bd);background:var(--bg-2)}
3947
+ .console-title{font-size:11px;font-weight:700}
3948
+ .console-list{max-height:400px;overflow-y:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;background:var(--bg)}
3949
+ .console-entry{display:flex;align-items:baseline;gap:8px;padding:3px 10px;border-bottom:1px solid rgba(255,255,255,.03);line-height:20px}
3950
+ .console-entry:hover{background:rgba(255,255,255,.03)}
3951
+ .console-level-badge{font-weight:700;font-size:9px;min-width:14px;text-align:center;flex-shrink:0}
3952
+ .console-time{color:var(--fg-2);font-size:10px;white-space:nowrap;min-width:76px;flex-shrink:0}
3953
+ .console-text{flex:1;min-width:0;word-break:break-word;white-space:pre-wrap;color:var(--fg)}
3954
+ .console-loc{color:var(--fg-2);font-size:10px;white-space:nowrap;margin-left:auto;max-width:180px;overflow:hidden;text-overflow:ellipsis}
3955
+ .console-more{font-size:10.5px;color:var(--fg-2);padding:8px 12px;border-top:1px solid var(--bd)}
3956
+
3957
+ /* \u2500\u2500 Device log provenance \u2500\u2500 */
3958
+ .console-source{font-size:9px;font-weight:700;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.04);color:var(--fg-1);flex-shrink:0}
3959
+ .console-source-crashlog{background:rgba(239,68,68,.12);color:#ef4444}
3960
+ .console-tag{font-size:10px;color:#fb923c;flex-shrink:0;width:104px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3961
+ .console-pid{font-size:10px;color:var(--fg-2);flex-shrink:0;width:52px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3962
+ .console-stack{grid-column:1/-1;font-size:10px;color:var(--fg-2);white-space:pre-wrap;padding:4px 0 6px 24px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3963
+
3964
+ /* \u2500\u2500 Replay: steps against the recording \u2500\u2500 */
3965
+ .replay{display:flex;gap:22px;flex-wrap:wrap}
3966
+ .replay-video{width:232px;flex-shrink:0}
3967
+ .replay-video video{width:100%;border-radius:12px;border:1px solid var(--bd);background:#000;display:block}
3968
+ .replay-side{flex:1;min-width:280px;display:flex;flex-direction:column;gap:12px}
3969
+ .scrub{position:relative;height:10px;border-radius:999px;background:rgba(255,255,255,.06)}
3970
+ .scrub-mark{position:absolute;top:-3px;width:3px;height:16px;border-radius:2px;cursor:pointer}
3971
+ .scrub-mark.is-failed{top:-5px;width:4px;height:20px;background:#ef4444}
3972
+ .scrub-legend{display:flex;justify-content:space-between;font-size:10px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3973
+ .replay-steps{display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto}
3974
+ .rstep{display:flex;align-items:center;gap:10px;padding:6px 10px;border-radius:7px;cursor:pointer}
3975
+ .rstep:hover{background:var(--bg-2)}
3976
+ .rstep.is-failed{background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.18)}
3977
+ .rstep-n{font-size:10px;color:var(--fg-2);width:22px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3978
+ .rstep-t{font-size:11.5px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3979
+ .rstep-off{font-size:10px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3980
+ .rstep-d{font-size:11px;color:var(--fg-1);width:52px;text-align:right}
3981
+ .rstep.is-failed .rstep-d{color:#ef4444;font-weight:700}
3982
+
3983
+ /* \u2500\u2500 Screenshot strip: every shot, not only the first \u2500\u2500 */
3984
+ .shot-strip{display:flex;gap:10px;flex-wrap:wrap}
3985
+ .shot{width:118px;border-radius:8px;border:1px solid var(--bd);overflow:hidden;background:var(--bg-2);cursor:pointer}
3986
+ .shot img{width:100%;display:block}
3987
+ .shot-cap{font-size:9.5px;color:var(--fg-2);padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3988
+ .shot.is-failure{border-color:rgba(239,68,68,.4)}
3989
+
3990
+ /* \u2500\u2500 AI defects \u2500\u2500 */
3991
+ .aidef{border-left:3px solid #6b7280;background:var(--bg-1);border:1px solid var(--bd);border-radius:10px;padding:14px 18px;display:flex;gap:16px;align-items:flex-start;margin-bottom:10px}
3992
+ .aidef-critical{border-left-color:#ef4444}
3993
+ .aidef-warning{border-left-color:#f59e0b}
3994
+ .aidef-info{border-left-color:#60a5fa}
3995
+ .aidef-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:9px}
3996
+ .aidef-pills{display:flex;align-items:center;gap:8px}
3997
+ .aidef-sev{font-size:9px;font-weight:700;padding:1px 7px;border-radius:999px;letter-spacing:.04em;text-transform:uppercase}
3998
+ .aidef-sev-critical{background:rgba(239,68,68,.14);color:#ef4444}
3999
+ .aidef-sev-warning{background:rgba(245,158,11,.14);color:#f59e0b}
4000
+ .aidef-sev-info{background:rgba(96,165,250,.14);color:#60a5fa}
4001
+ .aidef-type{font-size:9px;font-weight:700;padding:1px 7px;border-radius:999px;background:rgba(255,255,255,.04);color:var(--fg-1);letter-spacing:.04em;text-transform:uppercase}
4002
+ .aidef-desc{font-size:13.5px;line-height:1.55}
4003
+ .aidef-loc{font-size:10.5px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
4004
+ .aidef-shot{width:92px;flex-shrink:0;border-radius:8px;border:1px solid var(--bd);overflow:hidden}
4005
+ .aidef-shot img{width:100%;display:block}
4006
+
4007
+ @media (max-width:900px){
4008
+ .verdict-side{width:100%}
4009
+ }
4010
+ `;
4011
+
3362
4012
  // src/html-template.ts
3363
4013
  var JS = `
3364
4014
  (function(){
@@ -3373,10 +4023,14 @@ var JS = `
3373
4023
  ${JS_ASSERTIONS}
3374
4024
  ${JS_AI_DEFECTS}
3375
4025
  ${JS_INTERACTIONS}
4026
+ ${JS_MOBILE}
3376
4027
 
3377
4028
  applyTheme();
3378
4029
  renderRunMeta();
4030
+ renderDeviceStrip();
3379
4031
  renderHeroStrip();
4032
+ renderTypeBand();
4033
+ renderNeedsAttention();
3380
4034
  renderWaterfall();
3381
4035
  renderFilterBar();
3382
4036
  renderFlowList();
@@ -3391,10 +4045,10 @@ function renderHtmlDocument(reportJson) {
3391
4045
  <head>
3392
4046
  <meta charset="UTF-8">
3393
4047
  <meta name="viewport" content="width=device-width,initial-scale=1">
3394
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob: file: 'self'; media-src blob: 'self'; connect-src 'self';">
4048
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob: file: 'self'; media-src blob: data: file: 'self'; connect-src 'self';">
3395
4049
  <title>TestRelic Maestro Test Report</title>
3396
4050
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(LOGO_SVG_RAW).toString("base64")}">
3397
- <style>${CSS}</style>
4051
+ <style>${CSS}${CSS_MOBILE}</style>
3398
4052
  <script>(function(){var p=localStorage.getItem('tr-theme')||'system';var t=p==='system'?(window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):p;document.documentElement.setAttribute('data-theme',t);})()</script>
3399
4053
  </head>
3400
4054
  <body>
@@ -3418,7 +4072,10 @@ function renderHtmlDocument(reportJson) {
3418
4072
  </div>
3419
4073
  </div>
3420
4074
 
4075
+ <div id="device-strip"></div>
3421
4076
  <div id="hero-strip" class="hero-strip"></div>
4077
+ <div id="type-band"></div>
4078
+ <div id="needs-attention"></div>
3422
4079
  <div id="waterfall" class="waterfall-card"></div>
3423
4080
  <div id="filter-bar" class="filter-bar"></div>
3424
4081
  <div id="flow-list"></div>
@@ -4893,7 +5550,7 @@ async function orchestrateReport(input) {
4893
5550
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
4894
5551
  const totalDuration = Date.now() - new Date(startedAt).getTime();
4895
5552
  const report = {
4896
- schemaVersion: "1.0.0",
5553
+ schemaVersion: SCHEMA_VERSION,
4897
5554
  testRunId,
4898
5555
  startedAt,
4899
5556
  completedAt,