repoburg 1.3.254 → 1.3.256

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 (78) hide show
  1. package/agent-pane/frontend/dist/assets/{index-hYkepxdY.js → index-BzDC6DcX.js} +86 -86
  2. package/agent-pane/frontend/dist/index.html +1 -1
  3. package/daemon/dist/index.js +61 -3
  4. package/daemon/dist/staticServing.js +2 -2
  5. package/daemon/dist/tabtape/api/follow.js +174 -0
  6. package/daemon/dist/tabtape/api/http.js +221 -0
  7. package/daemon/dist/tabtape/api/record.js +149 -0
  8. package/daemon/dist/tabtape/api/router.js +519 -0
  9. package/daemon/dist/tabtape/api/router.spec.js +604 -0
  10. package/daemon/dist/tabtape/api/static.js +70 -0
  11. package/daemon/dist/tabtape/api/ws.js +166 -0
  12. package/daemon/dist/tabtape/buffer/buffer.js +347 -0
  13. package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
  14. package/daemon/dist/tabtape/config.js +108 -0
  15. package/daemon/dist/tabtape/e2e-server.js +96 -0
  16. package/daemon/dist/tabtape/errors.js +77 -0
  17. package/daemon/dist/tabtape/export/export.js +331 -0
  18. package/daemon/dist/tabtape/export/export.spec.js +262 -0
  19. package/daemon/dist/tabtape/har/gostd.js +395 -0
  20. package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
  21. package/daemon/dist/tabtape/har/har.js +582 -0
  22. package/daemon/dist/tabtape/har/har.spec.js +325 -0
  23. package/daemon/dist/tabtape/index.js +134 -0
  24. package/daemon/dist/tabtape/index.spec.js +104 -0
  25. package/daemon/dist/tabtape/lock.js +67 -0
  26. package/daemon/dist/tabtape/log.js +32 -0
  27. package/daemon/dist/tabtape/mcp/definitions.js +362 -0
  28. package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
  29. package/daemon/dist/tabtape/mcp/params.js +345 -0
  30. package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
  31. package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
  32. package/daemon/dist/tabtape/mcp/server.js +411 -0
  33. package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
  34. package/daemon/dist/tabtape/mcp/tools.js +1085 -0
  35. package/daemon/dist/tabtape/query/format.js +195 -0
  36. package/daemon/dist/tabtape/query/network.js +133 -0
  37. package/daemon/dist/tabtape/query/perf.js +93 -0
  38. package/daemon/dist/tabtape/query/query.spec.js +233 -0
  39. package/daemon/dist/tabtape/query/reader.js +602 -0
  40. package/daemon/dist/tabtape/query/types.js +274 -0
  41. package/daemon/dist/tabtape/redact.js +37 -0
  42. package/daemon/dist/tabtape/repro/lang.js +95 -0
  43. package/daemon/dist/tabtape/repro/repro.js +422 -0
  44. package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
  45. package/daemon/dist/tabtape/sink.js +24 -0
  46. package/daemon/dist/tabtape/store/record.js +99 -0
  47. package/daemon/dist/tabtape/store/segment.js +267 -0
  48. package/daemon/dist/tabtape/store/store.js +846 -0
  49. package/daemon/dist/tabtape/store/store.spec.js +485 -0
  50. package/daemon/dist/tabtape/testing/querytest.js +119 -0
  51. package/daemon/dist/tabtape/testing/tmp.js +33 -0
  52. package/package.json +3 -2
  53. package/tabtape/extension/dist/background.js +8 -0
  54. package/tabtape/extension/dist/background.js.map +7 -0
  55. package/tabtape/extension/dist/bridge.js +3 -0
  56. package/tabtape/extension/dist/bridge.js.map +7 -0
  57. package/tabtape/extension/dist/icons/128.png +0 -0
  58. package/tabtape/extension/dist/icons/16.png +0 -0
  59. package/tabtape/extension/dist/icons/32.png +0 -0
  60. package/tabtape/extension/dist/icons/48.png +0 -0
  61. package/tabtape/extension/dist/manifest.json +28 -0
  62. package/tabtape/extension/dist/popup.css +62 -0
  63. package/tabtape/extension/dist/popup.html +34 -0
  64. package/tabtape/extension/dist/popup.js +4 -0
  65. package/tabtape/extension/dist/popup.js.map +7 -0
  66. package/tabtape/extension/dist/recorder.js +133 -0
  67. package/tabtape/extension/dist/recorder.js.map +1 -0
  68. package/tabtape/extension/dist/state.js +2 -0
  69. package/tabtape/extension/dist/state.js.map +1 -0
  70. package/tabtape/playwright/mcp.mjs +58 -0
  71. package/tabtape/playwright/tabtape.d.mts +37 -0
  72. package/tabtape/playwright/tabtape.mjs +146 -0
  73. package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
  74. package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
  75. package/tabtape/ui/dist/app/index.html +14 -0
  76. package/tabtape/ui/dist/export/viewer.css +2 -0
  77. package/tabtape/ui/dist/export/viewer.js +143 -0
  78. package/visual-editor-proxy/dist/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,62 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --bg: #ffffff;
4
+ --fg: #18181b;
5
+ --muted: #71717a;
6
+ --border: #e4e4e7;
7
+ --panel: #f4f4f5;
8
+ --red: #e5484d;
9
+ --green: #30a46c;
10
+ --accent: #3e63dd;
11
+ }
12
+ @media (prefers-color-scheme: dark) {
13
+ :root { --bg: #18181b; --fg: #fafafa; --muted: #a1a1aa; --border: #3f3f46; --panel: #27272a; }
14
+ }
15
+ * { box-sizing: border-box; }
16
+ [hidden] { display: none !important; }
17
+ body { margin: 0; width: 320px; font: 13px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif; background: var(--bg); color: var(--fg); }
18
+ header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--border); }
19
+ .brand { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 15px; letter-spacing: 0.3px; }
20
+ .logo { width: 12px; height: 12px; border-radius: 50%; background: var(--red); box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 25%, transparent); }
21
+ .server { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; }
22
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
23
+ .server.ok .dot { background: var(--green); }
24
+ .server.down .dot { background: var(--red); }
25
+ main { padding: 14px; display: flex; flex-direction: column; gap: 10px; }
26
+ .tab { background: var(--panel); border-radius: 8px; padding: 8px 10px; }
27
+ .tab .title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
28
+ .tab .url { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
29
+ .live { display: flex; align-items: center; gap: 8px; font-weight: 600; }
30
+ .live .pulse { width: 10px; height: 10px; border-radius: 50%; background: var(--red); animation: pulse 1.2s ease-in-out infinite; }
31
+ @keyframes pulse { 50% { opacity: 0.35; } }
32
+ .live .time { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--muted); }
33
+ .stats { color: var(--muted); font-size: 12px; }
34
+ .warn { color: #b45309; font-size: 12px; }
35
+ button { font: inherit; cursor: pointer; }
36
+ button.primary, button.danger, form button { border: 0; border-radius: 8px; padding: 9px 12px; font-weight: 600; color: #fff; background: var(--accent); }
37
+ button.primary { background: var(--red); }
38
+ button.danger { background: var(--fg); color: var(--bg); }
39
+ button:disabled { opacity: 0.45; cursor: not-allowed; }
40
+ button.secondary { border: 1px solid var(--border); background: transparent; color: var(--fg); border-radius: 8px; padding: 8px 12px; }
41
+ button.watch { display: flex; align-items: center; justify-content: center; gap: 7px; font-weight: 600; }
42
+ .live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--red); box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 25%, transparent); }
43
+ .link { border: 0; background: none; color: var(--accent); padding: 0; }
44
+ .muted { color: var(--muted); font-size: 12px; }
45
+ .error { margin: 0 14px 10px; padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--red) 12%, transparent); color: var(--red); font-size: 12px; }
46
+ footer { display: flex; justify-content: space-between; padding: 10px 14px; border-top: 1px solid var(--border); }
47
+ form { padding: 0 14px 14px; display: flex; flex-direction: column; gap: 8px; }
48
+ label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
49
+ label.check { flex-direction: row; align-items: center; gap: 6px; color: var(--fg); }
50
+ input[type="url"] { font: inherit; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--fg); }
51
+ .row { display: flex; align-items: center; gap: 10px; }
52
+ code { font-size: 11px; background: var(--panel); padding: 1px 4px; border-radius: 4px; }
53
+ fieldset { border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px 10px; margin: 0; display: flex; flex-direction: column; gap: 8px; }
54
+ legend { font-size: 12px; font-weight: 600; padding: 0 4px; }
55
+ textarea, input[type="number"] { font: 12px ui-monospace, Menlo, monospace; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--fg); resize: vertical; }
56
+ input[type="number"] { width: 64px; }
57
+ label.inline { flex-direction: row; align-items: center; justify-content: space-between; }
58
+ .buffer { display: flex; flex-direction: column; gap: 8px; padding: 8px 10px; border-radius: 8px; border: 1px dashed var(--border); }
59
+ .buffer-head { display: flex; align-items: center; gap: 8px; font-weight: 600; }
60
+ .buf-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
61
+ .deep-option { display: flex; flex-direction: column; gap: 2px; }
62
+ .deep-option .muted { padding-left: 26px; } /* under the label text, past the checkbox */
@@ -0,0 +1,34 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="stylesheet" href="popup.css" />
6
+ </head>
7
+ <body>
8
+ <header>
9
+ <div class="brand"><span class="logo"></span>tabtape</div>
10
+ <div id="server" class="server"><span class="dot"></span><span id="server-text">checking…</span></div>
11
+ </header>
12
+ <main id="main"></main>
13
+ <p id="error" class="error" hidden></p>
14
+ <footer>
15
+ <button id="open-ui" class="link" type="button">Open replay UI</button>
16
+ <button id="toggle-settings" class="link" type="button">Settings</button>
17
+ </footer>
18
+ <form id="settings" hidden>
19
+ <label>Server URL <input id="server-url" type="url" required placeholder="http://127.0.0.1:9998/tabtape" /></label>
20
+ <div class="muted">tabtape inside the Repoburg daemon: <code>http://127.0.0.1:9998/tabtape</code></div>
21
+ <label class="check"><input id="capture-bodies" type="checkbox" /> Capture request/response bodies</label>
22
+ <label class="check" title="Defines the Redux DevTools hook on recorded pages, so Redux Toolkit and Zustand devtools stores are recorded without code changes"><input id="record-state" type="checkbox" /> Record Redux / Zustand state</label>
23
+ <fieldset>
24
+ <legend>Buffer mode</legend>
25
+ <label class="check"><input id="buffer-enabled" type="checkbox" /> Keep the last minutes of these sites</label>
26
+ <label>Sites, one Chrome match pattern per line <textarea id="buffer-sites" rows="3" spellcheck="false" placeholder="http://localhost/*"></textarea></label>
27
+ <label class="inline">Minutes to keep <input id="buffer-minutes" type="number" min="1" max="10" step="1" /></label>
28
+ <label class="check"><input id="start-with-buffer" type="checkbox" /> Start recordings with the buffered minutes</label>
29
+ </fieldset>
30
+ <div class="row"><button type="submit">Save</button><span id="saved" class="muted"></span></div>
31
+ </form>
32
+ <script src="popup.js"></script>
33
+ </body>
34
+ </html>
@@ -0,0 +1,4 @@
1
+ "use strict";(()=>{var N=/^(\*|https?|file):\/\/(\*|\*\.[^/*:]+|[^/*:]+)?(?::(\d+|\*))?(\/.*)$/;function x(e){return e==="<all_urls>"||N.test(e.trim())}var A=new Set(["localhost","127.0.0.1","[::1]"]);function R(e,t){if(!e||!t)return!1;let n,o;try{n=new URL(e),o=new URL(t)}catch{return!1}if(n.protocol!==o.protocol)return!1;let a=u=>u.port||(u.protocol==="https:"?"443":"80"),i=u=>A.has(u.hostname)?"loopback":u.hostname;if(a(n)!==a(o)||i(n)!==i(o))return!1;let f=T(o).toLowerCase(),g=n.pathname.toLowerCase();return!f||g===f||g.startsWith(`${f}/`)}function T(e){return e.pathname.replace(/\/+$/,"")}function I(e){let t;try{t=new URL(e.trim())}catch{return null}return t.protocol!=="http:"&&t.protocol!=="https:"||t.search||t.hash||t.username||t.password?null:`${t.origin}${T(t)}`}var U={serverUrl:"http://127.0.0.1:9998/tabtape",captureBodies:!0,checkoutMinutes:5,bufferEnabled:!1,bufferSites:["http://localhost/*","http://127.0.0.1/*"],bufferMinutes:2,startWithBuffer:!0,recordState:!0},O="http://127.0.0.1:7777";function $(e){let t={...U,...e},n=typeof t.serverUrl=="string"?t.serverUrl.trim().replace(/\/+$/,""):"";return t.serverUrl=!n||n===O?U.serverUrl:n,t}var y={min:1,max:10};var s=e=>document.getElementById(e),c,l,b=null,d=!1,k=null,v=!1;function r(e,t={},...n){let o=document.createElement(e);for(let[a,i]of Object.entries(t))a==="onclick"?o.addEventListener("click",i):a==="class"?o.className=String(i):i!==void 0&&i!==!1&&o.setAttribute(a,i===!0?"":String(i));for(let a of n)a&&o.append(a);return o}function S(e,...t){for(let n of t)n&&e.append(n)}function h(e){return chrome.runtime.sendMessage(e)}var D=e=>!!e&&/^(https?|file):/.test(e)&&!/^https:\/\/(chromewebstore\.google\.com|chrome\.google\.com\/webstore)/.test(e);function p(e){let t=s("error");t.textContent=e,t.hidden=!e}async function m(){try{[c]=await chrome.tabs.query({active:!0,currentWindow:!0}),l=await h({type:"status",tabId:c?.id}),b=l.recording&&l.recording.tabId===c?.id?await z(l.recording.tabId):null}catch(e){p(`Extension background not reachable: ${e instanceof Error?e.message:String(e)}`);return}w()}function w(){if(!l)return;let e=s("server");e.className=`server ${l.server.ok?"ok":"down"}`,s("server-text").textContent=l.server.ok?"server connected":"server offline";let t=s("main");t.replaceChildren();let n=l.recording;if(n&&n.tabId===c?.id){let{lines:g,bytes:u,queuedBytes:C,lastError:L}=l.stats;S(t,r("div",{class:"live"},r("span",{class:"pulse"}),"Recording this tab",r("span",{class:"time"},_(n.startedAt))),r("div",{class:"stats"},`${g.toLocaleString()} events \xB7 ${P(u)} saved`),L?r("div",{class:"warn"},`Upload failing (${L}); ${P(C)} buffered.`):null,j(n),K(),H(n),r("button",{class:"danger",onclick:B,disabled:d},"\u25A0 Stop recording"));return}if(n){S(t,r("div",{class:"live"},r("span",{class:"pulse"}),"Recording another tab",r("span",{class:"time"},_(n.startedAt))),r("div",{class:"tab"},r("div",{class:"title"},n.title||"Untitled"),r("div",{class:"url"},n.url??"")),H(n),r("div",{class:"row"},r("button",{class:"secondary",onclick:()=>chrome.tabs.update(n.tabId,{active:!0})},"Go to tab"),r("button",{class:"danger",onclick:B,disabled:d},"\u25A0 Stop")));return}let o=R(c?.url,l.server.url),a=D(c?.url)&&!o,i=l.buffer,f=i.enabled&&i.tab?.active;S(t,r("div",{class:"tab"},r("div",{class:"title"},c?.title||"Untitled"),r("div",{class:"url"},c?.url??"")),f?r("div",{class:"buffer"},r("div",{class:"buffer-head"},r("span",{class:"buf-dot"}),`Buffering the last ${i.minutes} min`),r("button",{class:"secondary",onclick:q,disabled:d||!l.server.ok,title:"Shortcut: \u2325\u21E7S"},`Save last ${i.minutes} minute${i.minutes===1?"":"s"}`),k?r("button",{class:"link",onclick:()=>M(k.sessionId)},"Saved: open the recording \u2192"):null,i.error?r("div",{class:"warn"},`Buffer data is not reaching the server (${i.error}); it is dropped until the server is back.`):null):null,a?F():null,r("button",{class:"primary",onclick:W,disabled:d||!a||!l.server.ok},"\u25CF Start recording"),f?r("div",{class:"muted"},"The recording starts with the buffered minutes (see Settings)."):null,a?null:r("div",{class:"muted"},o?"tabtape\u2019s own pages are never recorded.":"Browser pages cannot be recorded."),l.server.ok?null:r("div",{class:"muted"},"tabtape runs inside the Repoburg daemon. Start it first: ",r("code",{},"repoburg-daemon")),l.lastSession?r("button",{class:"link",onclick:()=>M(l.lastSession.sessionId)},"Open last recording \u2192"):null)}async function W(){if(!c?.id)return;d=!0,p(""),w();let e=await h({type:"start",tabId:c.id,deep:v});d=!1,e.ok?(v=!1,e.warning&&p(e.warning)):p(e.error),await m()}function F(){let e=r("input",{type:"checkbox",id:"deep-mode",checked:v,disabled:d});return e.addEventListener("change",()=>v=e.checked),r("div",{class:"deep-option"},r("label",{class:"check"},e,"Deep network mode"),r("div",{class:"muted"},"WebSockets, real cookies, CORS errors and worker requests. Chrome shows a \u201Cstarted debugging this browser\u201D bar until the recording stops."))}function j(e){return e.deep?.state==="on"?r("div",{class:"muted"},"Deep network mode on"):e.deep?.state==="ended"?r("div",{class:"warn"},`Deep network mode stopped (${e.deep.reason??"the debugger detached"}); requests are captured in the page again.`):null}async function q(){if(!c?.id)return;d=!0,p(""),w();let e=await h({type:"save-buffer",tabId:c.id});d=!1,e.ok?k={sessionId:e.sessionId,serverUrl:e.serverUrl}:p(e.error),await m()}async function B(){d=!0,w();let e=await h({type:"stop"});d=!1,e.ok||p(e.error),await m()}function K(){return!b||b.stateHook==="off"?null:b.stores.length?r("div",{class:"muted state-note"},`State: ${b.stores.join(", ")}`):b.stateHook!=="late"?null:r("div",{class:"muted state-note"},"Using Redux or Zustand? Reload the page to record its state. ",r("button",{class:"link",onclick:()=>c?.id!==void 0&&void chrome.tabs.reload(c.id)},"Reload"))}async function z(e){try{return await chrome.tabs.sendMessage(e,{type:"page-info"},{frameId:0})??null}catch{return null}}function H(e){return r("button",{class:"secondary watch",onclick:()=>{G(e)},title:"Open the replay in a new window and follow the recording live"},r("span",{class:"live-dot"}),"Watch live")}async function G(e){let t=`${e.serverUrl}/#/s/${e.sessionId}?live=1`;try{let n=await chrome.windows.getCurrent(),o=n.width??1280,a=Math.min(Math.max(Math.round(o/2),960),o);await chrome.windows.create({url:t,type:"normal",focused:!0,width:a,height:n.height,left:(n.left??0)+o-a,top:n.top})}catch{try{await chrome.windows.create({url:t,type:"normal",focused:!0})}catch(n){p(`Could not open the live view: ${n instanceof Error?n.message:String(n)}`)}}}async function M(e){let t=l?.lastSession?.serverUrl??(await E()).serverUrl;await chrome.tabs.create({url:e?`${t}/#/s/${e}`:`${t}/`})}async function E(){let{settings:e}=await chrome.storage.local.get("settings");return $(e)}function _(e){let t=Math.max(0,Math.floor((Date.now()-e)/1e3)),n=Math.floor(t/60);return`${String(n).padStart(2,"0")}:${String(t%60).padStart(2,"0")}`}function P(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/1024/1024).toFixed(1)} MB`}s("open-ui").addEventListener("click",()=>{M()});s("toggle-settings").addEventListener("click",async()=>{let e=s("settings");if(e.hidden=!e.hidden,!e.hidden){let t=await E();s("server-url").value=t.serverUrl,s("capture-bodies").checked=t.captureBodies,s("record-state").checked=t.recordState,s("buffer-enabled").checked=t.bufferEnabled,s("buffer-sites").value=t.bufferSites.join(`
2
+ `),s("buffer-minutes").value=String(t.bufferMinutes),s("start-with-buffer").checked=t.startWithBuffer}});s("settings").addEventListener("submit",async e=>{e.preventDefault();let t=await E(),n=I(s("server-url").value);if(!n){s("saved").textContent="Server URL: an http or https URL, like http://127.0.0.1:9998/tabtape";return}let o=s("buffer-sites").value.split(`
3
+ `).map(u=>u.trim()).filter(Boolean),a=o.filter(u=>!x(u));if(a.length){s("saved").textContent=`Not a match pattern: ${a[0]}`;return}let i=Math.round(Number(s("buffer-minutes").value)),f={...t,serverUrl:n,captureBodies:s("capture-bodies").checked,recordState:s("record-state").checked,bufferEnabled:s("buffer-enabled").checked,bufferSites:o,bufferMinutes:Math.min(Math.max(i||2,y.min),y.max),startWithBuffer:s("start-with-buffer").checked};await chrome.storage.local.set({settings:f});let g=await h({type:"settings-changed"});s("saved").textContent=g.ok?"Saved":`Saved, but: ${g.error}`,setTimeout(()=>s("saved").textContent="",2500),await m()});m();setInterval(()=>{m()},1e3);})();
4
+ //# sourceMappingURL=popup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/match.ts", "../src/protocol.ts", "../src/popup.ts"],
4
+ "sourcesContent": ["// Chrome match patterns (https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns),\n// enough of them to decide which tabs buffer mode records:\n// <all_urls>, *://*.example.com/*, http://localhost/*, http://localhost:3000/app/*\n// A pattern without a port matches every port.\n\nconst PATTERN = /^(\\*|https?|file):\\/\\/(\\*|\\*\\.[^/*:]+|[^/*:]+)?(?::(\\d+|\\*))?(\\/.*)$/;\n\nexport function validPattern(p: string): boolean {\n return p === '<all_urls>' || PATTERN.test(p.trim());\n}\n\nexport function matches(pattern: string, url: string): boolean {\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n return false;\n }\n const scheme = u.protocol.slice(0, -1);\n if (pattern === '<all_urls>') return scheme === 'http' || scheme === 'https' || scheme === 'file';\n const m = PATTERN.exec(pattern.trim());\n if (!m) return false;\n const [, ps, host = '', port, path] = m;\n if (ps === '*' ? scheme !== 'http' && scheme !== 'https' : ps !== scheme) return false;\n if (host && host !== '*') {\n if (host.startsWith('*.')) {\n const base = host.slice(2).toLowerCase();\n if (u.hostname !== base && !u.hostname.endsWith(`.${base}`)) return false;\n } else if (u.hostname !== host.toLowerCase()) {\n return false;\n }\n }\n if (port && port !== '*' && (u.port || (scheme === 'https' ? '443' : '80')) !== port) return false;\n return glob(path, u.pathname + u.search);\n}\n\nexport function matchesAny(patterns: string[], url: string): boolean {\n return patterns.some((p) => matches(p, url));\n}\n\n/** Whether a tab showing `url` can be recorded: web and file pages, but not the Web Store. */\nexport function isRecordable(url: string | undefined): boolean {\n if (!url || !/^(https?|file):/.test(url)) return false;\n return !/^https:\\/\\/(chromewebstore\\.google\\.com|chrome\\.google\\.com\\/webstore)/.test(url);\n}\n\nconst LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);\n\n/**\n * Whether `url` is one of tabtape's own pages (the replay UI, the live view,\n * record pages) on the server at `serverUrl`: same origin (localhost,\n * 127.0.0.1 and [::1] count as the same host) and at or below the server's\n * path. The path matters: the Repoburg daemon serves tabtape under /tabtape\n * and other pages on the same origin (the agent pane), which stay\n * recordable. A server URL without a path covers its whole origin. Needed\n * because match patterns ignore ports: http://127.0.0.1/* also matches the\n * server.\n */\nexport function isServerPage(url: string | undefined, serverUrl: string | undefined): boolean {\n if (!url || !serverUrl) return false;\n let u: URL;\n let s: URL;\n try {\n u = new URL(url);\n s = new URL(serverUrl);\n } catch {\n return false;\n }\n if (u.protocol !== s.protocol) return false;\n const port = (x: URL) => x.port || (x.protocol === 'https:' ? '443' : '80');\n const host = (x: URL) => (LOOPBACK.has(x.hostname) ? 'loopback' : x.hostname);\n if (port(u) !== port(s) || host(u) !== host(s)) return false;\n // Express routes ignore case: /TabTape/ is tabtape's too.\n const base = serverPath(s).toLowerCase();\n const path = u.pathname.toLowerCase();\n return !base || path === base || path.startsWith(`${base}/`);\n}\n\n/** The path the server is mounted at, without a trailing slash: \"/tabtape\", or \"\" at the root of its origin. */\nexport function serverPath(serverUrl: URL): string {\n return serverUrl.pathname.replace(/\\/+$/, '');\n}\n\n/**\n * A server URL as settings keep it: http or https, an optional path (the\n * mount, /tabtape in the Repoburg daemon), no trailing slash, no query or\n * fragment. Null when `input` is not one.\n */\nexport function normalizeServerUrl(input: string): string | null {\n let u: URL;\n try {\n u = new URL(input.trim());\n } catch {\n return null;\n }\n if ((u.protocol !== 'http:' && u.protocol !== 'https:') || u.search || u.hash || u.username || u.password) return null;\n return `${u.origin}${serverPath(u)}`;\n}\n\nfunction glob(pattern: string, text: string): boolean {\n const re = new RegExp(`^${pattern.split('*').map(escape).join('.*')}$`);\n return re.test(text);\n}\n\nfunction escape(s: string): string {\n return s.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n", "// Messages between the extension's parts.\n//\n// recorder.js (page MAIN world) --DOM CustomEvents--> bridge.js (isolated world)\n// bridge.js --chrome.runtime messages--> background.js --HTTP--> tabtape (Repoburg daemon, /tabtape)\n\n/** DOM event names used between the recorder (MAIN world) and the bridge. */\nexport const DOM_EVENT = {\n /** recorder -> bridge, detail: one NDJSON line */\n data: '__tabtape:data',\n /** recorder -> bridge: send what you have now (page is going away) */\n flush: '__tabtape:flush',\n /** recorder -> bridge: I am loaded, tell me what to do */\n hello: '__tabtape:hello',\n /** bridge -> recorder, detail: JSON ControlMessage */\n control: '__tabtape:control',\n /** bridge -> recorder: report PageInfo now (answered synchronously with infoReply) */\n info: '__tabtape:info',\n /** recorder -> bridge, detail: JSON PageInfo */\n infoReply: '__tabtape:info-reply',\n} as const;\n\n/** The state hook bundle (state.js) publishes its API on window under this symbol. */\nexport const STATE_HOOK_KEY = 'tabtape.state';\n\nexport interface RecorderOptions {\n captureBodies: boolean;\n maxBodyChars: number;\n /** Periodic full snapshots keep replays usable after ring buffer eviction. */\n checkoutEveryNms: number;\n /**\n * In-page network capture (fetch, XHR, beacons, Resource and Navigation\n * Timing). Off while deep mode records the tab's network through the\n * debugger, so no request is written twice.\n */\n inPageNetwork: boolean;\n /** Redux / Zustand state recording; needs the state hook (state.js) on the page. */\n recordState: boolean;\n}\n\n/** What the recorder knows about its page; the popup asks for it (ST-R13). */\nexport interface PageInfo {\n /**\n * early: the Redux DevTools hook was defined before the page's scripts ran;\n * late: injected into a page that was already open (stores created at load\n * missed it); off: state recording is off (no state.js on the page).\n */\n stateHook: 'early' | 'late' | 'off';\n /** Names of the stores connected to the hook. */\n stores: string[];\n}\n\n/** Where a tab's lines go: a recording, or the tab's buffer (buffer mode). */\nexport type Target = { kind: 'session'; id: string } | { kind: 'buffer'; key: string; windowMs: number };\n\nexport type ControlMessage =\n | { cmd: 'start'; sessionId: string; options: RecorderOptions }\n | { cmd: 'stop' }\n | { cmd: 'disable' };\n\n/** How often the bridge sends its lines; faster while someone watches the recording live. */\nexport const FLUSH_MS = { normal: 1000, live: 250 };\n\nexport type Decision =\n | {\n record: true;\n target: Target;\n options: RecorderOptions;\n /** Bridge flush interval, when not FLUSH_MS.normal. */\n flushMs?: number;\n }\n | { record: false };\n\nexport type RuntimeMessage =\n | { type: 'hello' }\n | { type: 'batch'; target: Target; lines: string }\n | { type: 'status'; tabId?: number }\n /** deep: record this recording's network through the debugger (deep mode, NET-R1) */\n | { type: 'start'; tabId: number; deep?: boolean }\n | { type: 'stop' }\n | { type: 'save-buffer'; tabId: number }\n | { type: 'settings-changed' };\n\n/** background -> bridge */\nexport type BridgeMessage =\n /** stop recording; the response carries the lines not sent yet */\n | { type: 'stop' }\n /** hand over the lines not sent yet without stopping */\n | { type: 'take' }\n /** live viewers came or went: send batches every `ms` */\n | { type: 'flush-interval'; ms: number }\n /** the recording's options changed (deep mode ended): ask for a new decision */\n | { type: 'redecide' }\n /** popup -> bridge: the response is the recorder's PageInfo, or null without a recorder */\n | { type: 'page-info' };\n\nexport interface Settings {\n /** tabtape's server with its path, no trailing slash: URLs are built as `${serverUrl}/api/\u2026`. */\n serverUrl: string;\n captureBodies: boolean;\n checkoutMinutes: number;\n /** Buffer mode: keep the last minutes of allow-listed tabs on the server. */\n bufferEnabled: boolean;\n /** Chrome match patterns, e.g. http://localhost/* */\n bufferSites: string[];\n bufferMinutes: number;\n /** Recordings started on a buffered tab begin with the buffered minutes. */\n startWithBuffer: boolean;\n /** Record Redux / Zustand stores through the Redux DevTools hook (ST-R1). */\n recordState: boolean;\n}\n\nexport const DEFAULT_SETTINGS: Settings = {\n // tabtape runs inside the Repoburg daemon, mounted under /tabtape.\n serverUrl: 'http://127.0.0.1:9998/tabtape',\n captureBodies: true,\n checkoutMinutes: 5,\n bufferEnabled: false,\n bufferSites: ['http://localhost/*', 'http://127.0.0.1/*'],\n bufferMinutes: 2,\n startWithBuffer: true,\n recordState: true,\n};\n\n/** The default server URL of tabtape's former standalone server. */\nconst STANDALONE_SERVER_URL = 'http://127.0.0.1:7777';\n\n/**\n * Stored settings over the defaults. The server URL loses trailing slashes;\n * one saved with the standalone server's default moves to the daemon.\n */\nexport function withDefaults(stored: Partial<Settings> | undefined): Settings {\n const s: Settings = { ...DEFAULT_SETTINGS, ...stored };\n const url = typeof s.serverUrl === 'string' ? s.serverUrl.trim().replace(/\\/+$/, '') : '';\n s.serverUrl = !url || url === STANDALONE_SERVER_URL ? DEFAULT_SETTINGS.serverUrl : url;\n return s;\n}\n\nexport const BUFFER_MINUTES = { min: 1, max: 10 };\n\n/** A checkout every quarter window (15 s \u2013 1 min) keeps every save replayable. */\nexport function bufferCheckoutMs(windowMs: number): number {\n return Math.min(Math.max(windowMs / 4, 15_000), 60_000);\n}\n\nexport const MAX_BODY_CHARS = 1_000_000;\n\nexport interface Recording {\n sessionId: string;\n tabId: number;\n startedAt: number;\n serverUrl: string;\n title?: string;\n url?: string;\n /** Deep mode (NET-R1): set when the recording was started with it. */\n deep?: DeepStatus;\n}\n\n/**\n * on: the debugger is attached and records the tab's network;\n * ended: it detached early (the user canceled Chrome's debugging bar, the tab\n * crashed, the extension restarted) and the page captures requests again.\n */\nexport interface DeepStatus {\n state: 'on' | 'ended';\n /** Why it ended, in words for the popup. */\n reason?: string;\n}\n\nexport interface BufferTab {\n key: string;\n /** The tab currently shows an allow-listed page and is being buffered. */\n active: boolean;\n url?: string;\n}\n\nexport interface Status {\n recording: Recording | null;\n lastSession: { sessionId: string; serverUrl: string } | null;\n server: { ok: boolean; url: string; version?: string; error?: string };\n stats: { lines: number; bytes: number; queuedBytes: number; dropped: number; lastError?: string };\n buffer: { enabled: boolean; minutes: number; tab: BufferTab | null; error?: string };\n}\n\nexport type Result<T = object> = ({ ok: true } & T) | { ok: false; error: string };\n", "import { isServerPage, normalizeServerUrl, validPattern } from './match';\nimport { BUFFER_MINUTES, withDefaults, type BridgeMessage, type PageInfo, type Recording, type Result, type RuntimeMessage, type Settings, type Status } from './protocol';\n\nconst $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;\n\nlet tab: chrome.tabs.Tab | undefined;\nlet status: Status | undefined;\n/** The recorded page's recorder, when this tab is the recorded one. */\nlet pageInfo: PageInfo | null = null;\nlet busy = false;\nlet saved: { sessionId: string; serverUrl: string } | null = null;\n/** The \"Deep network mode\" option: for the next recording only, kept across the 1-second re-renders (NET-R1). */\nlet deepMode = false;\n\ntype Child = Node | string | null | undefined | false;\nfunction h(tag: string, props: Record<string, unknown> = {}, ...children: Child[]): HTMLElement {\n const el = document.createElement(tag);\n for (const [k, v] of Object.entries(props)) {\n if (k === 'onclick') el.addEventListener('click', v as EventListener);\n else if (k === 'class') el.className = String(v);\n else if (v !== undefined && v !== false) el.setAttribute(k, v === true ? '' : String(v));\n }\n for (const c of children) if (c) el.append(c);\n return el;\n}\n\nfunction append(parent: HTMLElement, ...children: Child[]): void {\n for (const c of children) if (c) parent.append(c);\n}\n\nfunction send<T>(msg: RuntimeMessage): Promise<T> {\n return chrome.runtime.sendMessage(msg);\n}\n\nconst recordable = (url?: string) => !!url && /^(https?|file):/.test(url) && !/^https:\\/\\/(chromewebstore\\.google\\.com|chrome\\.google\\.com\\/webstore)/.test(url);\n\nfunction showError(message: string): void {\n const el = $('error');\n el.textContent = message;\n el.hidden = !message;\n}\n\nasync function refresh(): Promise<void> {\n try {\n [tab] = await chrome.tabs.query({ active: true, currentWindow: true });\n status = await send<Status>({ type: 'status', tabId: tab?.id });\n pageInfo = status.recording && status.recording.tabId === tab?.id ? await askPage(status.recording.tabId) : null;\n } catch (err) {\n showError(`Extension background not reachable: ${err instanceof Error ? err.message : String(err)}`);\n return;\n }\n render();\n}\n\nfunction render(): void {\n if (!status) return;\n const server = $('server');\n server.className = `server ${status.server.ok ? 'ok' : 'down'}`;\n $('server-text').textContent = status.server.ok ? 'server connected' : 'server offline';\n\n const main = $('main');\n main.replaceChildren();\n const rec = status.recording;\n\n if (rec && rec.tabId === tab?.id) {\n const { lines, bytes, queuedBytes, lastError } = status.stats;\n append(\n main,\n h('div', { class: 'live' }, h('span', { class: 'pulse' }), 'Recording this tab', h('span', { class: 'time' }, elapsed(rec.startedAt))),\n h('div', { class: 'stats' }, `${lines.toLocaleString()} events \u00B7 ${formatBytes(bytes)} saved`),\n lastError ? h('div', { class: 'warn' }, `Upload failing (${lastError}); ${formatBytes(queuedBytes)} buffered.`) : null,\n deepNote(rec),\n stateNote(),\n watchButton(rec),\n h('button', { class: 'danger', onclick: stop, disabled: busy }, '\u25A0 Stop recording'),\n );\n return;\n }\n\n if (rec) {\n append(\n main,\n h('div', { class: 'live' }, h('span', { class: 'pulse' }), 'Recording another tab', h('span', { class: 'time' }, elapsed(rec.startedAt))),\n h('div', { class: 'tab' }, h('div', { class: 'title' }, rec.title || 'Untitled'), h('div', { class: 'url' }, rec.url ?? '')),\n watchButton(rec),\n h('div', { class: 'row' },\n h('button', { class: 'secondary', onclick: () => chrome.tabs.update(rec.tabId, { active: true }) }, 'Go to tab'),\n h('button', { class: 'danger', onclick: stop, disabled: busy }, '\u25A0 Stop'),\n ),\n );\n return;\n }\n\n const ownPage = isServerPage(tab?.url, status.server.url);\n const canRecord = recordable(tab?.url) && !ownPage;\n const buf = status.buffer;\n const buffering = buf.enabled && buf.tab?.active;\n append(\n main,\n h('div', { class: 'tab' }, h('div', { class: 'title' }, tab?.title || 'Untitled'), h('div', { class: 'url' }, tab?.url ?? '')),\n buffering\n ? h(\n 'div',\n { class: 'buffer' },\n h('div', { class: 'buffer-head' }, h('span', { class: 'buf-dot' }), `Buffering the last ${buf.minutes} min`),\n h(\n 'button',\n { class: 'secondary', onclick: saveBuffer, disabled: busy || !status.server.ok, title: 'Shortcut: \u2325\u21E7S' },\n `Save last ${buf.minutes} minute${buf.minutes === 1 ? '' : 's'}`,\n ),\n saved ? h('button', { class: 'link', onclick: () => openUI(saved!.sessionId) }, 'Saved: open the recording \u2192') : null,\n buf.error ? h('div', { class: 'warn' }, `Buffer data is not reaching the server (${buf.error}); it is dropped until the server is back.`) : null,\n )\n : null,\n canRecord ? deepOption() : null,\n h('button', { class: 'primary', onclick: start, disabled: busy || !canRecord || !status.server.ok }, '\u25CF Start recording'),\n buffering ? h('div', { class: 'muted' }, 'The recording starts with the buffered minutes (see Settings).') : null,\n !canRecord ? h('div', { class: 'muted' }, ownPage ? 'tabtape\u2019s own pages are never recorded.' : 'Browser pages cannot be recorded.') : null,\n !status.server.ok ? h('div', { class: 'muted' }, 'tabtape runs inside the Repoburg daemon. Start it first: ', h('code', {}, 'repoburg-daemon')) : null,\n status.lastSession\n ? h('button', { class: 'link', onclick: () => openUI(status!.lastSession!.sessionId) }, 'Open last recording \u2192')\n : null,\n );\n}\n\nasync function start(): Promise<void> {\n if (!tab?.id) return;\n busy = true;\n showError('');\n render();\n const res = await send<Result<{ warning?: string }>>({ type: 'start', tabId: tab.id, deep: deepMode });\n busy = false;\n if (!res.ok) {\n showError(res.error);\n } else {\n // Deep mode is chosen per recording.\n deepMode = false;\n if (res.warning) showError(res.warning);\n }\n await refresh();\n}\n\n/** The checkbox for the next recording, and what Chrome will show while it records. */\nfunction deepOption(): HTMLElement {\n const box = h('input', { type: 'checkbox', id: 'deep-mode', checked: deepMode, disabled: busy });\n box.addEventListener('change', () => (deepMode = (box as HTMLInputElement).checked));\n return h(\n 'div',\n { class: 'deep-option' },\n h('label', { class: 'check' }, box, 'Deep network mode'),\n h(\n 'div',\n { class: 'muted' },\n 'WebSockets, real cookies, CORS errors and worker requests. Chrome shows a \u201Cstarted debugging this browser\u201D bar until the recording stops.',\n ),\n );\n}\n\n/** Deep mode of the running recording: on, or why it stopped early (NET-R14). */\nfunction deepNote(rec: Recording): HTMLElement | null {\n if (rec.deep?.state === 'on') return h('div', { class: 'muted' }, 'Deep network mode on');\n if (rec.deep?.state === 'ended') {\n return h('div', { class: 'warn' }, `Deep network mode stopped (${rec.deep.reason ?? 'the debugger detached'}); requests are captured in the page again.`);\n }\n return null;\n}\n\nasync function saveBuffer(): Promise<void> {\n if (!tab?.id) return;\n busy = true;\n showError('');\n render();\n const res = await send<Result<{ sessionId: string; serverUrl: string }>>({ type: 'save-buffer', tabId: tab.id });\n busy = false;\n if (res.ok) saved = { sessionId: res.sessionId, serverUrl: res.serverUrl };\n else showError(res.error);\n await refresh();\n}\n\nasync function stop(): Promise<void> {\n busy = true;\n render();\n const res = await send<Result<{ sessionId?: string }>>({ type: 'stop' });\n busy = false;\n if (!res.ok) showError(res.error);\n await refresh();\n}\n\n/** The recorded page's state hook: which stores it records, or a reload hint (ST-R13). */\nfunction stateNote(): HTMLElement | null {\n if (!pageInfo || pageInfo.stateHook === 'off') return null;\n if (pageInfo.stores.length) return h('div', { class: 'muted state-note' }, `State: ${pageInfo.stores.join(', ')}`);\n if (pageInfo.stateHook !== 'late') return null;\n return h(\n 'div',\n { class: 'muted state-note' },\n 'Using Redux or Zustand? Reload the page to record its state. ',\n h('button', { class: 'link', onclick: () => tab?.id !== undefined && void chrome.tabs.reload(tab.id) }, 'Reload'),\n );\n}\n\nasync function askPage(tabId: number): Promise<PageInfo | null> {\n try {\n const info = (await chrome.tabs.sendMessage(tabId, { type: 'page-info' } satisfies BridgeMessage, { frameId: 0 })) as PageInfo | null | undefined;\n return info ?? null;\n } catch {\n return null; // no bridge on this page (yet)\n }\n}\n\nfunction watchButton(rec: Recording): HTMLElement {\n return h(\n 'button',\n { class: 'secondary watch', onclick: () => void watchLive(rec), title: 'Open the replay in a new window and follow the recording live' },\n h('span', { class: 'live-dot' }),\n 'Watch live',\n );\n}\n\n/**\n * Opens the live replay in its own window over the right half of this\n * browser window, so the recorded page stays in view. tabtape's own pages are\n * never recorded, so the watch window is not either.\n */\nasync function watchLive(rec: Recording): Promise<void> {\n const url = `${rec.serverUrl}/#/s/${rec.sessionId}?live=1`;\n try {\n const current = await chrome.windows.getCurrent();\n const w = current.width ?? 1280;\n const width = Math.min(Math.max(Math.round(w / 2), 960), w);\n await chrome.windows.create({ url, type: 'normal', focused: true, width, height: current.height, left: (current.left ?? 0) + w - width, top: current.top });\n } catch {\n // Chrome refuses bounds it considers off-screen; let it place the window.\n try {\n await chrome.windows.create({ url, type: 'normal', focused: true });\n } catch (err) {\n showError(`Could not open the live view: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n}\n\nasync function openUI(sessionId?: string): Promise<void> {\n const base = status?.lastSession?.serverUrl ?? (await loadSettings()).serverUrl;\n await chrome.tabs.create({ url: sessionId ? `${base}/#/s/${sessionId}` : `${base}/` });\n}\n\nasync function loadSettings(): Promise<Settings> {\n const { settings } = await chrome.storage.local.get('settings');\n return withDefaults(settings as Partial<Settings> | undefined);\n}\n\nfunction elapsed(since: number): string {\n const s = Math.max(0, Math.floor((Date.now() - since) / 1000));\n const m = Math.floor(s / 60);\n return `${String(m).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;\n}\n\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n return `${(n / 1024 / 1024).toFixed(1)} MB`;\n}\n\n$('open-ui').addEventListener('click', () => void openUI());\n$('toggle-settings').addEventListener('click', async () => {\n const form = $('settings');\n form.hidden = !form.hidden;\n if (!form.hidden) {\n const s = await loadSettings();\n $<HTMLInputElement>('server-url').value = s.serverUrl;\n $<HTMLInputElement>('capture-bodies').checked = s.captureBodies;\n $<HTMLInputElement>('record-state').checked = s.recordState;\n $<HTMLInputElement>('buffer-enabled').checked = s.bufferEnabled;\n $<HTMLTextAreaElement>('buffer-sites').value = s.bufferSites.join('\\n');\n $<HTMLInputElement>('buffer-minutes').value = String(s.bufferMinutes);\n $<HTMLInputElement>('start-with-buffer').checked = s.startWithBuffer;\n }\n});\n$<HTMLFormElement>('settings').addEventListener('submit', async (e) => {\n e.preventDefault();\n const current = await loadSettings();\n // A path is fine (tabtape is under /tabtape in the daemon); trailing slashes go.\n const serverUrl = normalizeServerUrl($<HTMLInputElement>('server-url').value);\n if (!serverUrl) {\n $('saved').textContent = 'Server URL: an http or https URL, like http://127.0.0.1:9998/tabtape';\n return;\n }\n const sites = $<HTMLTextAreaElement>('buffer-sites')\n .value.split('\\n')\n .map((l) => l.trim())\n .filter(Boolean);\n const bad = sites.filter((p) => !validPattern(p));\n if (bad.length) {\n $('saved').textContent = `Not a match pattern: ${bad[0]}`;\n return;\n }\n const minutes = Math.round(Number($<HTMLInputElement>('buffer-minutes').value));\n const settings: Settings = {\n ...current,\n serverUrl,\n captureBodies: $<HTMLInputElement>('capture-bodies').checked,\n recordState: $<HTMLInputElement>('record-state').checked,\n bufferEnabled: $<HTMLInputElement>('buffer-enabled').checked,\n bufferSites: sites,\n bufferMinutes: Math.min(Math.max(minutes || 2, BUFFER_MINUTES.min), BUFFER_MINUTES.max),\n startWithBuffer: $<HTMLInputElement>('start-with-buffer').checked,\n };\n await chrome.storage.local.set({ settings });\n const res = await send<Result>({ type: 'settings-changed' });\n $('saved').textContent = res.ok ? 'Saved' : `Saved, but: ${res.error}`;\n setTimeout(() => ($('saved').textContent = ''), 2500);\n await refresh();\n});\n\nvoid refresh();\nsetInterval(() => void refresh(), 1000);\n"],
5
+ "mappings": "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",
6
+ "names": ["PATTERN", "validPattern", "p", "LOOPBACK", "isServerPage", "url", "serverUrl", "u", "s", "port", "x", "host", "base", "serverPath", "path", "normalizeServerUrl", "input", "DEFAULT_SETTINGS", "STANDALONE_SERVER_URL", "withDefaults", "stored", "s", "url", "BUFFER_MINUTES", "$", "id", "tab", "status", "pageInfo", "busy", "saved", "deepMode", "h", "tag", "props", "children", "el", "k", "v", "c", "append", "parent", "send", "msg", "recordable", "url", "showError", "message", "refresh", "askPage", "err", "render", "server", "main", "rec", "lines", "bytes", "queuedBytes", "lastError", "elapsed", "formatBytes", "deepNote", "stateNote", "watchButton", "stop", "ownPage", "isServerPage", "canRecord", "buf", "buffering", "saveBuffer", "openUI", "deepOption", "start", "res", "box", "tabId", "watchLive", "current", "w", "width", "sessionId", "base", "loadSettings", "settings", "withDefaults", "since", "s", "m", "n", "form", "serverUrl", "normalizeServerUrl", "sites", "l", "bad", "p", "validPattern", "minutes", "BUFFER_MINUTES"]
7
+ }