proxy2model 2.0.0__py3-none-any.whl

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.
Files changed (66) hide show
  1. proxy2model/__init__.py +19 -0
  2. proxy2model/__main__.py +6 -0
  3. proxy2model/atomic.py +68 -0
  4. proxy2model/cli/__init__.py +3 -0
  5. proxy2model/cli/analyze.py +501 -0
  6. proxy2model/cli/app.py +408 -0
  7. proxy2model/cli/browse.py +279 -0
  8. proxy2model/cli/build.py +399 -0
  9. proxy2model/cli/common.py +190 -0
  10. proxy2model/cli/history.py +224 -0
  11. proxy2model/cli/mcp.py +184 -0
  12. proxy2model/cli/ui.py +62 -0
  13. proxy2model/core/__init__.py +1 -0
  14. proxy2model/core/changes.py +124 -0
  15. proxy2model/core/context.py +571 -0
  16. proxy2model/core/diff.py +330 -0
  17. proxy2model/core/domains.py +98 -0
  18. proxy2model/core/graph.py +562 -0
  19. proxy2model/core/methodology.py +343 -0
  20. proxy2model/core/model.py +998 -0
  21. proxy2model/core/stack.py +218 -0
  22. proxy2model/data/fonts/LICENSES.md +16 -0
  23. proxy2model/data/fonts/inter-latin-wght-normal.woff2 +0 -0
  24. proxy2model/data/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  25. proxy2model/data/fonts/nippo-200.woff2 +0 -0
  26. proxy2model/data/fonts/nippo-300.woff2 +0 -0
  27. proxy2model/data/fonts/nippo-400.woff2 +0 -0
  28. proxy2model/data/fonts/nippo-500.woff2 +0 -0
  29. proxy2model/data/fonts/nippo-700.woff2 +0 -0
  30. proxy2model/data/public_suffix_list.dat +16497 -0
  31. proxy2model/db/__init__.py +1 -0
  32. proxy2model/db/bql.py +710 -0
  33. proxy2model/db/bql_js.py +278 -0
  34. proxy2model/db/store.py +547 -0
  35. proxy2model/export/__init__.py +1 -0
  36. proxy2model/export/openapi.py +248 -0
  37. proxy2model/ingest/__init__.py +1 -0
  38. proxy2model/ingest/browse.py +181 -0
  39. proxy2model/ingest/browser.py +1058 -0
  40. proxy2model/ingest/crawl.py +673 -0
  41. proxy2model/ingest/extract.py +208 -0
  42. proxy2model/ingest/infer.py +75 -0
  43. proxy2model/ingest/journey.py +148 -0
  44. proxy2model/ingest/live.py +96 -0
  45. proxy2model/ingest/parse.py +1275 -0
  46. proxy2model/ingest/redact.py +535 -0
  47. proxy2model/py.typed +0 -0
  48. proxy2model/recon/__init__.py +1 -0
  49. proxy2model/recon/osint.py +607 -0
  50. proxy2model/report/__init__.py +1 -0
  51. proxy2model/report/animate.py +163 -0
  52. proxy2model/report/dash.py +469 -0
  53. proxy2model/report/fonts.py +51 -0
  54. proxy2model/report/html.py +988 -0
  55. proxy2model/report/inventory.py +258 -0
  56. proxy2model/report/map.py +515 -0
  57. proxy2model/report/pages.py +537 -0
  58. proxy2model/report/query.py +306 -0
  59. proxy2model/report/shot.py +55 -0
  60. proxy2model/report/view.py +213 -0
  61. proxy2model-2.0.0.dist-info/METADATA +148 -0
  62. proxy2model-2.0.0.dist-info/RECORD +66 -0
  63. proxy2model-2.0.0.dist-info/WHEEL +5 -0
  64. proxy2model-2.0.0.dist-info/entry_points.txt +2 -0
  65. proxy2model-2.0.0.dist-info/licenses/LICENSE +201 -0
  66. proxy2model-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,258 @@
1
+ """The Inventory view of the offline report: a Burp-style site map, request table and viewer.
2
+
3
+ Left, a site-map tree (host, then path folders; endpoints the code names but nothing requested
4
+ show greyed out). Top right, the request table with sortable, resizable columns and filters.
5
+ Bottom right, the redacted request and response side by side. Every value is already masked in
6
+ the payload; this only lays it out.
7
+ """
8
+
9
+ INV_CSS = r"""
10
+ /* inventory (Burp-style) */
11
+ .m-inventory{padding:0!important;overflow:hidden!important;display:flex;flex-direction:column}
12
+ .m-inventory .view{max-width:none;flex:1;min-height:0;display:flex;flex-direction:column}
13
+ .m-inventory .foot{display:none}
14
+ .inv{display:flex;flex-direction:column;flex:1;min-height:0}
15
+ .inv-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);background:var(--panel)}
16
+ .inv-bar h1{font:700 15px var(--sans);margin:0 6px 0 0;letter-spacing:-.01em}
17
+ .inv-q{position:relative;flex:1 1 280px;min-width:200px;max-width:560px}
18
+ .inv-q input{width:100%;height:32px;padding:0 10px 0 30px;border:1px solid var(--line2);border-radius:7px;background:var(--panel2);color:var(--text);font:12.5px var(--mono)}
19
+ .inv-q input:focus{border-color:var(--signal);outline:none}
20
+ .inv-q svg{position:absolute;left:9px;top:8px;width:15px;height:15px;color:var(--faint)}
21
+ .inv-chips{display:flex;gap:5px;flex-wrap:wrap;align-items:center}
22
+ .ichip{font:600 11px var(--mono);padding:4px 9px;border-radius:6px;border:1px solid var(--line2);background:var(--panel);color:var(--muted)}
23
+ .ichip:hover{color:var(--text);border-color:var(--faint)}
24
+ .ichip.on{background:var(--text);color:var(--ink);border-color:var(--text)}
25
+ .ichip.s2.on{background:var(--ok);border-color:var(--ok);color:#fff}.ichip.s3.on{background:var(--observed);border-color:var(--observed);color:#fff}
26
+ .ichip.s4.on{background:var(--warn);border-color:var(--warn);color:#fff}.ichip.s5.on{background:var(--bad);border-color:var(--bad);color:#fff}
27
+ .inv-bar select{height:30px;border:1px solid var(--line2);border-radius:7px;background:var(--panel);color:var(--text);font:600 11px var(--mono);padding:0 6px}
28
+ .inv-stat{margin-left:auto;color:var(--muted);font:12px var(--mono);white-space:nowrap}
29
+ .inv-err{padding:6px 14px;background:var(--panel);color:var(--bad);font:12px var(--mono);border-bottom:1px solid var(--line)}
30
+ .inv-main{display:flex;flex:1;min-height:0;background:var(--ink)}
31
+ .inv-tree{width:var(--tw,250px);min-width:150px;max-width:60%;overflow:auto;background:var(--panel);border-right:1px solid var(--line);padding:6px 0;font:12px var(--mono);flex:none}
32
+ .tn{display:flex;align-items:center;gap:5px;padding:3px 8px 3px calc(8px + var(--d,0)*14px);cursor:pointer;white-space:nowrap;border-left:2px solid transparent}
33
+ .tn:hover{background:var(--panel2)}
34
+ .tn.on{background:var(--signal-soft);border-left-color:var(--signal);color:var(--text)}
35
+ .tn .tg{width:12px;color:var(--faint);font-size:9px;flex:none;text-align:center}
36
+ .tn .nm{overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
37
+ .tn .cnt{color:var(--faint);font-size:10.5px;flex:none}
38
+ .tn .sd{width:7px;height:7px;border-radius:50%;flex:none;background:var(--ok)}
39
+ .tn.host .nm{font-weight:700}
40
+ .tn.ghost .nm{color:var(--faint);font-style:italic}
41
+ .tn.ghost .sd{background:transparent;border:1.5px dashed var(--signal)}
42
+ .tn .gt{font:600 9px var(--mono);color:var(--signal);border:1px solid var(--signal);border-radius:4px;padding:0 4px;flex:none}
43
+ .inv-gut{flex:none;background:var(--line);position:relative;z-index:2}
44
+ .inv-gut.v{width:3px;cursor:col-resize}.inv-gut.h{height:3px;cursor:row-resize}
45
+ .inv-gut:hover,.inv-gut.drag{background:var(--signal)}
46
+ .inv-right{flex:1;min-width:0;display:flex;flex-direction:column}
47
+ .inv-tbl{display:flex;flex-direction:column;min-height:90px;background:var(--panel)}
48
+ .th{display:grid;grid-template-columns:var(--cols);align-items:stretch;background:var(--panel2);border-bottom:1px solid var(--line);flex:none;font:600 10.5px var(--mono);letter-spacing:.05em;text-transform:uppercase;color:var(--muted);user-select:none}
49
+ .th>div{position:relative;padding:6px 8px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-right:1px solid var(--line)}
50
+ .th>div:hover{color:var(--text)}
51
+ .th .ar{color:var(--signal);margin-left:3px}
52
+ .th .rz{position:absolute;right:-3px;top:0;bottom:0;width:7px;cursor:col-resize;z-index:3}
53
+ .tbody{flex:1;overflow:auto;position:relative;outline:none}
54
+ .tr{display:grid;grid-template-columns:var(--cols);height:24px;align-items:center;font:12px var(--mono);cursor:default;border-bottom:1px solid transparent}
55
+ .tr.z{background:var(--panel3)}
56
+ .tr:hover{background:var(--panel2)}
57
+ .tr.sel,.tr.sel:hover{background:var(--signal-soft);box-shadow:inset 2px 0 var(--signal)}
58
+ .tr>div{padding:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
59
+ .tr .c-id{color:var(--faint)}.tr .c-host{color:var(--muted)}
60
+ .tr .c-url .q{color:var(--faint)}
61
+ .tr .c-method{font-weight:700}.tr .c-method.w{color:var(--signal)}
62
+ .tr .c-status{font-weight:700}.tr .c-status.g{color:var(--ok)}.tr .c-status.y{color:var(--warn)}.tr .c-status.r{color:var(--bad)}.tr .c-status.b{color:var(--observed)}
63
+ .tr .c-params{color:var(--ok)}.tr .c-len,.tr .c-mime,.tr .c-role{color:var(--muted)}
64
+ .tr .c-len{text-align:right}
65
+ .inv-empty{padding:34px 20px;color:var(--faint);text-align:center;font:13px var(--sans)}
66
+ .inv-view{flex:1;min-height:120px;display:flex;background:var(--panel);overflow:hidden}
67
+ .inv-view>.rr{flex:1}
68
+ .seenchip{font:600 11px var(--mono);padding:3px 8px;border-radius:6px;background:var(--panel2);color:var(--muted);margin-left:8px;white-space:nowrap}
69
+ .ghostcard{padding:22px 24px;color:var(--muted);font-size:13px;max-width:640px}
70
+ .ghostcard h3{margin:0 0 6px;font:700 15px var(--sans);color:var(--text)}
71
+ .ghostcard .tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
72
+ .ghostcard .tags span{font:600 11.5px var(--mono);padding:3px 8px;border-radius:6px;background:var(--panel2)}
73
+ @media(max-width:820px){.inv-tree{display:none}.inv-gut.v{display:none}}
74
+ """
75
+
76
+ INV_JS = r"""
77
+ /* ---------- INVENTORY (Burp-style: site map + request table + viewer) ---------- */
78
+ const INV_COLS=[
79
+ {k:"id",t:"#",w:54,dir:1},{k:"host",t:"Host",w:150,dir:1},{k:"method",t:"Method",w:74,dir:1},{k:"url",t:"URL",w:0,dir:1},
80
+ {k:"params",t:"Params",w:64,dir:-1},{k:"status",t:"Status",w:66,dir:1},{k:"len",t:"Length",w:76,dir:-1},{k:"mime",t:"MIME type",w:92,dir:1},{k:"role",t:"Role",w:100,dir:1}];
81
+ const INV=Object.assign({q:"",methods:new Set(),sc:new Set(),role:"",paramsOnly:false,sel:null,node:"",sortKey:"id",sortDir:1,
82
+ tw:250,th:300,open:new Set(),tab:{req:"pretty",res:"pretty"},widths:{},bql:null,bqlErr:null},
83
+ (function(){try{const s=JSON.parse(localStorage.getItem("b2m-inv")||"{}")||{};const o={};["tw","th","widths"].forEach(k=>{if(s[k]!=null)o[k]=s[k];});return o;}catch(e){return {};}})());
84
+ function invSave(){try{localStorage.setItem("b2m-inv",JSON.stringify({tw:INV.tw,th:INV.th,widths:INV.widths}));}catch(e){}}
85
+ const INV_ROW=EVL.map(e=>{
86
+ const path=e.path||"",qi=path.indexOf("?"),clean=qi<0?path:path.slice(0,qi);
87
+ const body=e.response&&e.response.body?e.response.body.length:0;
88
+ return {e,id:e.id,host:e.host||"",method:e.method||"?",path,clean,
89
+ params:(qi>=0||(e.request&&e.request.body))?1:0,status:e.status==null?0:e.status,
90
+ len:body,lenTr:!!(e.response&&e.response.truncated),mime:(e.mime||"").toLowerCase(),role:e.role||"",
91
+ hay:`${e.method} ${e.host}${path} ${e.status} ${e.mime||""} ${e.role||""}`.toLowerCase()};
92
+ });
93
+ function invTree(){
94
+ const root={key:"",kids:new Map(),n:0,worst:0,ghost:0};
95
+ const add=(host,clean,ghost,status)=>{
96
+ const segs=clean.split("/").filter(Boolean);let nd=root,key="";
97
+ [host].concat(segs).forEach((sg,i)=>{key+=(i?"/":"")+sg;
98
+ let k=nd.kids.get(sg);if(!k){k={key,name:sg,kids:new Map(),n:0,worst:0,ghost:ghost?1:0,host:i===0};nd.kids.set(sg,k);}
99
+ if(!ghost){k.n++;k.ghost=0;k.worst=Math.max(k.worst,status>=500?4:status>=400?3:status>=300?2:status>=200?1:0);}
100
+ nd=k;});
101
+ };
102
+ INV_ROW.forEach(r=>add(r.host,r.clean,false,r.status));
103
+ EP.filter(x=>x.state==="STATIC_ONLY").forEach(x=>add(x.host||"",x.path||"",true,0));
104
+ return root;
105
+ }
106
+ const INV_TREE=invTree();
107
+ function invMatchNode(r){if(!INV.node)return true;const k=r.host+r.clean.replace(/\/$/,"");return k===INV.node||k.startsWith(INV.node+"/");}
108
+ function invFiltered(){
109
+ const q=(INV.q||"").trim().toLowerCase(),g=(query||"").toLowerCase();
110
+ let rows=INV_ROW.filter(r=>{
111
+ if(INV.bql&&!INV.bql.has(r.id))return false;
112
+ if(q&&!INV.bql&&!r.hay.includes(q))return false;
113
+ if(g&&!r.hay.includes(g))return false;
114
+ if(INV.methods.size&&!INV.methods.has(r.method))return false;
115
+ if(INV.sc.size&&!INV.sc.has(String(statusClass(r.status||null)).replace("xx","")))return false;
116
+ if(INV.role&&r.role!==INV.role)return false;
117
+ if(INV.paramsOnly&&!r.params)return false;
118
+ return invMatchNode(r);});
119
+ const k=INV.sortKey,d=INV.sortDir;
120
+ const key=r=>k==="url"?r.clean:k==="params"?r.params:r[k];
121
+ rows=rows.slice().sort((a,b)=>{const x=key(a),y=key(b);return (typeof x==="number"?x-y:String(x).localeCompare(String(y)))*d||a.id-b.id;});
122
+ return rows;
123
+ }
124
+ const BQLISH=/(^|\s)(req|resp|node|edge)\.[a-z.]+:|(^|\s)(role|id):/i;
125
+ function invApplyQuery(v){
126
+ INV.q=v;INV.bql=null;INV.bqlErr=null;
127
+ if(BQLDB&&BQLISH.test(v)){
128
+ try{const r=BQL.run(BQLDB,v+" limit 5000");
129
+ if(r.kind!=="requests")INV.bqlErr="This filters requests: use req.* / resp.* fields.";
130
+ else INV.bql=new Set(r.rows.map(x=>+x.ev.slice(3)));}
131
+ catch(e){INV.bqlErr=e.message;}}
132
+ }
133
+ function vEvidence(){
134
+ const meths=tally(EVL,e=>e.method||"?").map(x=>x[0]).slice(0,8);
135
+ const roles=[...new Set(EVL.map(e=>e.role).filter(Boolean))].sort();
136
+ return `<div class="inv" id="inv"><div class="inv-bar"><h1>Requests</h1>
137
+ <div class="inv-q"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
138
+ <input id="invq" spellcheck="false" autocomplete="off" placeholder="${BQLDB?"filter: text, or BQL e.g. req.method:POST AND resp.code.gte:400":"filter by method, URL, status, role"}" value="${esc(INV.q)}"></div>
139
+ <div class="inv-chips" id="invchips">${meths.map(m=>`<button class="ichip${INV.methods.has(m)?" on":""}" data-m="${esc(m)}">${esc(m)}</button>`).join("")}
140
+ <span style="width:6px"></span>${["2","3","4","5"].map(x=>`<button class="ichip s${x}${INV.sc.has(x)?" on":""}" data-sc="${x}">${x}xx</button>`).join("")}
141
+ <button class="ichip${INV.paramsOnly?" on":""}" id="invparams">params</button>
142
+ ${roles.length?`<select id="invrole"><option value="">all roles</option>${roles.map(r=>`<option${INV.role===r?" selected":""}>${esc(r)}</option>`).join("")}</select>`:""}
143
+ <button class="ichip" id="invclear" title="Clear every filter">clear</button></div>
144
+ <span class="inv-stat" id="invstat"></span></div>
145
+ <div id="inverr"></div>
146
+ <div class="inv-main"><aside class="inv-tree" id="invtree" style="--tw:${INV.tw}px"></aside><div class="inv-gut v" id="gv"></div>
147
+ <section class="inv-right"><div class="inv-tbl" id="invtbl" style="height:${INV.th}px;flex:none"><div class="th" id="invth"></div><div class="tbody" id="invbody" tabindex="0"></div></div>
148
+ <div class="inv-gut h" id="gh"></div><div class="inv-view" id="invview"></div></section></div></div>`;
149
+ }
150
+ function invColsCss(){
151
+ const w=INV_COLS.map(c=>c.w?(INV.widths[c.k]||c.w)+"px":"minmax(180px,1fr)").join(" ");
152
+ return w;
153
+ }
154
+ function initEvq(){
155
+ const root=$("#inv");if(!root)return;
156
+ const ROWH=24;let rows=[],cols=invColsCss();
157
+ const body=$("#invbody"),th=$("#invth"),tree=$("#invtree");
158
+ root.style.setProperty("--cols",cols);
159
+
160
+ /* header */
161
+ function drawHead(){
162
+ th.innerHTML=INV_COLS.map(c=>`<div data-k="${c.k}">${esc(c.t)}${INV.sortKey===c.k?`<span class="ar">${INV.sortDir>0?"▲":"▼"}</span>`:""}${c.w?`<span class="rz" data-rz="${c.k}"></span>`:""}</div>`).join("");
163
+ th.style.setProperty("--cols",invColsCss());
164
+ $$("[data-k]",th).forEach(d=>d.onclick=ev=>{if(ev.target.closest(".rz"))return;const k=d.dataset.k;if(INV.sortKey===k)INV.sortDir*=-1;else{INV.sortKey=k;INV.sortDir=INV_COLS.find(c=>c.k===k).dir;}refresh();});
165
+ $$("[data-rz]",th).forEach(h=>h.onpointerdown=ev=>{ev.preventDefault();ev.stopPropagation();const k=h.dataset.rz,c=INV_COLS.find(x=>x.k===k),sx=ev.clientX,w0=INV.widths[k]||c.w;h.setPointerCapture(ev.pointerId);
166
+ h.onpointermove=e2=>{INV.widths[k]=Math.max(40,w0+e2.clientX-sx);root.style.setProperty("--cols",invColsCss());th.style.setProperty("--cols",invColsCss());drawBody();};
167
+ h.onpointerup=()=>{h.onpointermove=h.onpointerup=null;invSave();};});
168
+ }
169
+ /* table body, windowed so a 20,000-request capture stays instant */
170
+ function rowHtml(r,i){
171
+ const e=r.e,ssc=r.status>=500?"r":r.status>=400?"y":r.status>=300?"b":r.status>=200?"g":"";
172
+ const u=r.path,qi=u.indexOf("?");
173
+ return `<div class="tr${i%2?" z":""}${INV.sel===r.id?" sel":""}" data-id="${r.id}"><div class="c-id">${r.id}</div><div class="c-host" title="${esc(r.host)}">${esc(r.host)}</div>
174
+ <div class="c-method${r.method!=="GET"&&r.method!=="HEAD"?" w":""}">${esc(r.method)}</div>
175
+ <div class="c-url" title="${esc(u)}">${qi<0?esc(u):esc(u.slice(0,qi))+'<span class="q">'+esc(u.slice(qi))+"</span>"}</div>
176
+ <div class="c-params">${r.params?"✓":""}</div><div class="c-status ${ssc}">${r.status||""}</div>
177
+ <div class="c-len">${r.len?(r.lenTr?"≥":"")+r.len:""}</div><div class="c-mime">${esc(r.mime)}</div><div class="c-role">${esc(r.role)}</div></div>`;
178
+ }
179
+ function drawBody(){
180
+ if(!rows.length){body.innerHTML=`<div class="inv-empty">${INV.node&&!INV_ROW.some(invMatchNode)?"Named in the client code, but no request to it was captured.":"No request matches these filters."}</div>`;return;}
181
+ const vh=body.clientHeight||400,st=body.scrollTop,first=Math.max(0,Math.floor(st/ROWH)-10),last=Math.min(rows.length,Math.ceil((st+vh)/ROWH)+10);
182
+ body.innerHTML=`<div style="height:${rows.length*ROWH}px;position:relative"><div style="position:absolute;left:0;right:0;top:${first*ROWH}px">${rows.slice(first,last).map((r,i)=>rowHtml(r,first+i)).join("")}</div></div>`;
183
+ }
184
+ body.addEventListener("scroll",drawBody);
185
+ body.addEventListener("click",e=>{const t=e.target.closest(".tr");if(t)select(+t.dataset.id);});
186
+ body.addEventListener("keydown",e=>{
187
+ if(!["ArrowDown","ArrowUp","j","k","Home","End","PageDown","PageUp"].includes(e.key))return;e.preventDefault();
188
+ let i=rows.findIndex(r=>r.id===INV.sel);const d=e.key==="ArrowDown"||e.key==="j"?1:e.key==="ArrowUp"||e.key==="k"?-1:e.key==="PageDown"?15:e.key==="PageUp"?-15:0;
189
+ i=e.key==="Home"?0:e.key==="End"?rows.length-1:Math.max(0,Math.min(rows.length-1,(i<0?0:i)+d));
190
+ if(rows[i]){select(rows[i].id);const y=i*ROWH;if(y<body.scrollTop)body.scrollTop=y;else if(y+ROWH>body.scrollTop+body.clientHeight)body.scrollTop=y+ROWH-body.clientHeight;}});
191
+ function select(id){INV.sel=id;drawBody();drawView();}
192
+
193
+ /* viewer: the shared request/response component */
194
+ function drawView(){
195
+ const vw=$("#invview");
196
+ const r=INV_ROW.find(x=>x.id===INV.sel);
197
+ if(!r){
198
+ const ghost=INV.node?EP.filter(x=>x.state==="STATIC_ONLY"&&(x.host||"")+(x.path||"")===INV.node):[];
199
+ vw.innerHTML=ghost.length?`<div class="ghostcard"><h3>${methodm(ghost[0].method)} ${esc(ghost[0].path)}</h3>Named in the client code, never requested in this capture, so there is no request or response to show.
200
+ <div class="tags">${ghost[0].referenced_by.map(x=>`<span>${esc(x)}</span>`).join("")}</div></div>`
201
+ :`<div class="inv-empty" style="flex:1">Select a request to see it here. Arrow keys move through the table.</div>`;return;}
202
+ const e=r.e;
203
+ const ep=EP.find(x=>(x.evidence||[]).includes(e.id)),sn=ep&&D.seen&&D.seen[ep.id];
204
+ const bl=sn&&D.builds.find(b=>b[0]===sn[0]);
205
+ rrMount(vw,[e.id],{actions:[["Copy URL",(i,b)=>fallbackCopy(`${e.host}${e.path}`,()=>{b.textContent="Copied";setTimeout(()=>b.textContent="Copy URL",1200);})],["Details",()=>openEv(e.id)]]});
206
+ if(sn){const last=D.builds[D.builds.length-1][0],bar=$(".rrbar .sp",vw);
207
+ if(bar)bar.insertAdjacentHTML("beforebegin",`<span class="seenchip" title="${bl?esc(bl[1]+" "+bl[2]):""}">${sn[0]===last&&last>1?"new in build "+last:"since build "+sn[0]}${sn[1]<last?" · gone after build "+sn[1]:""}</span>`);}
208
+ }
209
+
210
+ /* site map */
211
+ function drawTree(){
212
+ const out=[];
213
+ const walk=(nd,d)=>{
214
+ [...nd.kids.values()].sort((a,b)=>(b.kids.size>0)-(a.kids.size>0)||a.name.localeCompare(b.name)).forEach(k=>{
215
+ const isOpen=INV.open.has(k.key)||(d===0&&!INV.open.has("-"+k.key));
216
+ const kids=k.kids.size>0,gh=k.ghost&&!k.n;
217
+ out.push(`<div class="tn${k.host?" host":""}${INV.node===k.key?" on":""}${gh?" ghost":""}" style="--d:${d}" data-key="${esc(k.key)}">
218
+ <span class="tg" data-tg="${esc(k.key)}">${kids?(isOpen?"▼":"▶"):""}</span>${gh?"":`<span class="sd" style="background:${["var(--faint)","var(--ok)","var(--observed)","var(--warn)","var(--bad)"][k.worst]}"></span>`}${gh?'<span class="sd"></span>':""}
219
+ <span class="nm" title="${esc(k.key)}">${esc(k.name||"/")}</span>${gh?'<span class="gt">code</span>':`<span class="cnt">${k.n}</span>`}</div>`);
220
+ if(kids&&isOpen)walk(k,d+1);});
221
+ };
222
+ walk(INV_TREE,0);
223
+ tree.innerHTML=`<div class="tn${INV.node===""?" on":""}" style="--d:0" data-key=""><span class="tg"></span><span class="nm" style="font-weight:700">All requests</span><span class="cnt">${INV_ROW.length}</span></div>`+out.join("");
224
+ $$(".tn",tree).forEach(t=>t.onclick=ev=>{
225
+ const k=t.dataset.key;
226
+ if(ev.target.closest("[data-tg]")){const was=t.querySelector("[data-tg]").textContent==="▼";
227
+ if(was){INV.open.delete(k);INV.open.add("-"+k);}else{INV.open.add(k);INV.open.delete("-"+k);}drawTree();return;}
228
+ INV.node=k;INV.sel=null;refresh();});
229
+ }
230
+ function refresh(){
231
+ rows=invFiltered();
232
+ if(INV.sel!=null&&!rows.some(r=>r.id===INV.sel))INV.sel=null;
233
+ drawHead();drawBody();drawTree();drawView();
234
+ $("#invstat").textContent=`${rows.length} of ${INV_ROW.length} requests`;
235
+ $("#inverr").innerHTML=INV.bqlErr?`<div class="inv-err">${esc(INV.bqlErr)}</div>`:"";
236
+ $$("#invchips [data-m]").forEach(b=>b.classList.toggle("on",INV.methods.has(b.dataset.m)));
237
+ $$("#invchips [data-sc]").forEach(b=>b.classList.toggle("on",INV.sc.has(b.dataset.sc)));
238
+ $("#invparams").classList.toggle("on",INV.paramsOnly);
239
+ }
240
+ /* filters */
241
+ const qi=$("#invq");
242
+ qi.addEventListener("input",()=>{invApplyQuery(qi.value);refresh();});
243
+ $$("#invchips [data-m]").forEach(b=>b.onclick=()=>{const m=b.dataset.m;INV.methods.has(m)?INV.methods.delete(m):INV.methods.add(m);refresh();});
244
+ $$("#invchips [data-sc]").forEach(b=>b.onclick=()=>{const m=b.dataset.sc;INV.sc.has(m)?INV.sc.delete(m):INV.sc.add(m);refresh();});
245
+ $("#invparams").onclick=()=>{INV.paramsOnly=!INV.paramsOnly;refresh();};
246
+ const rs=$("#invrole");if(rs)rs.onchange=()=>{INV.role=rs.value;refresh();};
247
+ $("#invclear").onclick=()=>{INV.q="";INV.bql=null;INV.bqlErr=null;INV.methods.clear();INV.sc.clear();INV.role="";INV.paramsOnly=false;INV.node="";qi.value="";if(rs)rs.value="";refresh();};
248
+ /* splitters */
249
+ const drag=(el,move,end)=>{el.onpointerdown=ev=>{ev.preventDefault();el.classList.add("drag");el.setPointerCapture(ev.pointerId);
250
+ el.onpointermove=e2=>move(e2);el.onpointerup=()=>{el.classList.remove("drag");el.onpointermove=el.onpointerup=null;end();};};};
251
+ drag($("#gv"),e=>{const b=$(".inv-main").getBoundingClientRect();INV.tw=Math.max(150,Math.min(b.width*.55,e.clientX-b.left));tree.style.setProperty("--tw",INV.tw+"px");},()=>invSave());
252
+ drag($("#gh"),e=>{const b=$(".inv-right").getBoundingClientRect();INV.th=Math.max(90,Math.min(b.height-140,e.clientY-b.top));$("#invtbl").style.height=INV.th+"px";drawBody();},()=>invSave());
253
+ qi.addEventListener("keydown",e=>{if(e.key==="ArrowDown"){e.preventDefault();body.focus();if(INV.sel==null&&rows[0])select(rows[0].id);}});
254
+ if(!INV.thSet){INV.thSet=true;try{if(!localStorage.getItem("b2m-inv")){const h=$(".inv-right").getBoundingClientRect().height;if(h>300){INV.th=Math.round(h*.42);$("#invtbl").style.height=INV.th+"px";}}}catch(e){}}
255
+ refresh();
256
+ if(INV.sel==null&&rows[0])select(rows[0].id);
257
+ }
258
+ """