localpov 0.1.0

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/bin/localpov-mcp.js +15 -0
  4. package/bin/localpov.js +599 -0
  5. package/dashboard/index.html +909 -0
  6. package/dist/collectors/browser-capture.d.ts +124 -0
  7. package/dist/collectors/browser-capture.js +327 -0
  8. package/dist/collectors/browser-capture.js.map +1 -0
  9. package/dist/collectors/build-parser.d.ts +18 -0
  10. package/dist/collectors/build-parser.js +192 -0
  11. package/dist/collectors/build-parser.js.map +1 -0
  12. package/dist/collectors/docker-watcher.d.ts +42 -0
  13. package/dist/collectors/docker-watcher.js +101 -0
  14. package/dist/collectors/docker-watcher.js.map +1 -0
  15. package/dist/collectors/terminal.d.ts +42 -0
  16. package/dist/collectors/terminal.js +128 -0
  17. package/dist/collectors/terminal.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +6 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/mcp-server.d.ts +1 -0
  22. package/dist/mcp-server.js +466 -0
  23. package/dist/mcp-server.js.map +1 -0
  24. package/dist/utils/inject.d.ts +11 -0
  25. package/dist/utils/inject.js +241 -0
  26. package/dist/utils/inject.js.map +1 -0
  27. package/dist/utils/network.d.ts +7 -0
  28. package/dist/utils/network.js +42 -0
  29. package/dist/utils/network.js.map +1 -0
  30. package/dist/utils/proxy.d.ts +24 -0
  31. package/dist/utils/proxy.js +363 -0
  32. package/dist/utils/proxy.js.map +1 -0
  33. package/dist/utils/scanner.d.ts +9 -0
  34. package/dist/utils/scanner.js +96 -0
  35. package/dist/utils/scanner.js.map +1 -0
  36. package/dist/utils/session-manager.d.ts +109 -0
  37. package/dist/utils/session-manager.js +488 -0
  38. package/dist/utils/session-manager.js.map +1 -0
  39. package/dist/utils/shell-init.d.ts +26 -0
  40. package/dist/utils/shell-init.js +422 -0
  41. package/dist/utils/shell-init.js.map +1 -0
  42. package/dist/utils/system-info.d.ts +51 -0
  43. package/dist/utils/system-info.js +170 -0
  44. package/dist/utils/system-info.js.map +1 -0
  45. package/package.json +64 -0
@@ -0,0 +1,909 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
6
+ <meta name="mobile-web-app-capable" content="yes">
7
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
8
+ <title>LocalPOV</title>
9
+ <style>
10
+ *{margin:0;padding:0;box-sizing:border-box}
11
+ :root{
12
+ --bg:#fff;--bg2:#f5f5f4;--fg:#171717;--fg2:#737373;--fg3:#a3a3a3;
13
+ --bdr:#e5e5e5;--accent:#2563eb;--accent-bg:#eff6ff;
14
+ --green:#16a34a;--red:#ef4444;
15
+ --safe-t:env(safe-area-inset-top,0px);--safe-b:env(safe-area-inset-bottom,0px);
16
+ }
17
+ @media(prefers-color-scheme:dark){:root{
18
+ --bg:#0a0a0a;--bg2:#171717;--fg:#fafafa;--fg2:#a3a3a3;--fg3:#525252;
19
+ --bdr:#262626;--accent:#60a5fa;--accent-bg:#172554;
20
+ --green:#4ade80;--red:#f87171;
21
+ }}
22
+ html,body{height:100%;overflow:hidden}
23
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--fg);-webkit-tap-highlight-color:transparent}
24
+
25
+ .hdr{padding:calc(16px + var(--safe-t)) 20px 12px}
26
+ .hdr h1{font-size:20px;font-weight:700;letter-spacing:-0.5px}
27
+ .hdr p{font-size:13px;color:var(--fg2);margin-top:2px}
28
+
29
+ .apps{padding:8px 16px 16px;overflow-y:auto;-webkit-overflow-scrolling:touch}
30
+ .card{
31
+ background:var(--bg2);border:1px solid var(--bdr);border-radius:14px;
32
+ padding:16px;margin-bottom:8px;display:flex;align-items:center;gap:14px;
33
+ cursor:pointer;transition:transform .1s;-webkit-user-select:none;user-select:none;
34
+ }
35
+ .card:active{transform:scale(0.97)}
36
+ .dot{width:10px;height:10px;border-radius:50%;background:var(--green);flex-shrink:0}
37
+ .dot-term{background:var(--accent)}
38
+ .card-info{flex:1;min-width:0}
39
+ .card-port{font-size:16px;font-weight:600;font-variant-numeric:tabular-nums}
40
+ .card-fw{font-size:13px;color:var(--fg2);margin-top:1px}
41
+ .card-arr{color:var(--fg3);font-size:20px;font-weight:300}
42
+
43
+ .empty{text-align:center;padding:60px 24px;color:var(--fg2)}
44
+ .empty h2{font-size:16px;font-weight:600;color:var(--fg);margin-bottom:4px}
45
+ .empty p{font-size:13px;line-height:1.5}
46
+ .empty code{background:var(--bg2);padding:2px 8px;border-radius:4px;font-size:12px}
47
+
48
+ .scan{display:flex;align-items:center;gap:8px;padding:6px 20px;font-size:12px;color:var(--fg3)}
49
+ .scan-dot{width:6px;height:6px;border-radius:50%;background:var(--green);animation:pulse 1.5s infinite}
50
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
51
+
52
+ /* Tab bar */
53
+ .tab-bar{
54
+ position:fixed;bottom:0;left:0;right:0;z-index:10;
55
+ display:flex;
56
+ border-top:1px solid var(--bdr);background:var(--bg);
57
+ padding-bottom:var(--safe-b);
58
+ }
59
+ .tab{
60
+ flex:1;border:none;background:none;font-family:inherit;font-size:11px;font-weight:600;
61
+ color:var(--fg3);padding:10px 4px 8px;cursor:pointer;text-align:center;
62
+ display:flex;flex-direction:column;align-items:center;gap:3px;
63
+ }
64
+ .tab.active{color:var(--accent)}
65
+ .tab-icon{font-size:20px;line-height:1}
66
+ .tab .tab-badge{
67
+ display:inline-block;min-width:16px;height:16px;border-radius:8px;
68
+ background:var(--red);color:#fff;font-size:10px;font-weight:700;
69
+ line-height:16px;text-align:center;padding:0 4px;margin-left:2px;
70
+ }
71
+
72
+ .reconnect{
73
+ position:fixed;top:0;left:0;right:0;z-index:999;
74
+ background:#ef4444;color:#fff;font-size:13px;font-weight:500;
75
+ text-align:center;padding:8px 16px;padding-top:calc(8px + var(--safe-t));
76
+ transform:translateY(-100%);transition:transform .3s ease;
77
+ }
78
+ .reconnect.show{transform:translateY(0)}
79
+
80
+ .prev-bar{
81
+ display:flex;align-items:center;gap:4px;
82
+ padding:calc(6px + var(--safe-t)) 8px 6px;
83
+ background:var(--bg2);border-bottom:1px solid var(--bdr);
84
+ }
85
+ .btn{
86
+ background:none;border:none;font-size:14px;cursor:pointer;padding:8px 10px;
87
+ border-radius:8px;font-family:inherit;color:var(--accent);font-weight:500;
88
+ }
89
+ .btn:active{background:var(--accent-bg)}
90
+ .btn-icon{font-size:18px;color:var(--fg2);font-weight:400}
91
+ .prev-info{flex:1;text-align:center;font-size:13px;color:var(--fg2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
92
+ .prev-port{color:var(--fg);font-weight:600}
93
+
94
+ #frame{border:none;width:100%;background:var(--bg)}
95
+
96
+ #view-list,#view-preview,#view-terminal,#view-debug{display:none}
97
+ #view-list.active,#view-preview.active,#view-terminal.active,#view-debug.active{display:flex;flex-direction:column}
98
+ #view-list.active{display:block}
99
+
100
+ /* Terminal view */
101
+ .term-bar{
102
+ display:flex;align-items:center;gap:4px;
103
+ padding:calc(6px + var(--safe-t)) 8px 6px;
104
+ background:var(--bg2);border-bottom:1px solid var(--bdr);
105
+ }
106
+ .term-info{flex:1;text-align:center;font-size:13px;color:var(--fg2);font-weight:500}
107
+ .term-cmd{font-family:monospace;color:var(--fg);font-weight:600;font-size:12px}
108
+
109
+ .term-output{
110
+ flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;
111
+ background:#0c0c0c;color:#ccc;
112
+ font-family:'SF Mono','Cascadia Code','Fira Code','Consolas','Monaco',monospace;
113
+ font-size:12px;line-height:1.5;
114
+ padding:12px;white-space:pre-wrap;word-wrap:break-word;
115
+ }
116
+ .term-output .t-err{color:#f87171}
117
+ .term-output .t-sys{color:#60a5fa;font-style:italic}
118
+ /* ANSI colors */
119
+ .term-output .a-bold{font-weight:bold}
120
+ .term-output .a-dim{opacity:0.7}
121
+ .term-output .a-red{color:#f87171}.term-output .a-green{color:#4ade80}
122
+ .term-output .a-yellow{color:#facc15}.term-output .a-blue{color:#60a5fa}
123
+ .term-output .a-magenta{color:#c084fc}.term-output .a-cyan{color:#22d3ee}
124
+ .term-output .a-white{color:#fafafa}.term-output .a-gray{color:#6b7280}
125
+ .term-output .a-bright-red{color:#fca5a5}.term-output .a-bright-green{color:#86efac}
126
+ .term-output .a-bright-yellow{color:#fde68a}.term-output .a-bright-blue{color:#93c5fd}
127
+ .term-output .a-bright-magenta{color:#d8b4fe}.term-output .a-bright-cyan{color:#67e8f9}
128
+
129
+ .term-empty{
130
+ display:flex;align-items:center;justify-content:center;flex:1;
131
+ color:var(--fg3);font-size:14px;text-align:center;padding:40px;
132
+ }
133
+ .term-empty code{background:var(--bg2);padding:2px 8px;border-radius:4px;font-size:12px}
134
+
135
+ .term-status{
136
+ display:flex;align-items:center;gap:6px;padding:6px 12px;
137
+ background:#111;border-top:1px solid #222;font-size:11px;color:#666;
138
+ }
139
+ .term-status-dot{width:6px;height:6px;border-radius:50%}
140
+ .term-status-dot.running{background:var(--green);animation:pulse 1.5s infinite}
141
+ .term-status-dot.stopped{background:var(--red)}
142
+
143
+ /* Debug view */
144
+ .dbg-bar{
145
+ display:flex;align-items:center;gap:4px;
146
+ padding:calc(6px + var(--safe-t)) 12px 6px;
147
+ background:var(--bg2);border-bottom:1px solid var(--bdr);
148
+ }
149
+ .dbg-bar-title{flex:1;text-align:center;font-size:14px;font-weight:600}
150
+ .dbg-scroll{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:12px 16px 80px}
151
+
152
+ .dbg-summary{
153
+ display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;
154
+ }
155
+ .dbg-stat{
156
+ background:var(--bg2);border:1px solid var(--bdr);border-radius:10px;
157
+ padding:12px;text-align:center;
158
+ }
159
+ .dbg-stat-num{font-size:24px;font-weight:700;font-variant-numeric:tabular-nums}
160
+ .dbg-stat-num.has-errors{color:var(--red)}
161
+ .dbg-stat-num.all-good{color:var(--green)}
162
+ .dbg-stat-label{font-size:11px;color:var(--fg2);margin-top:2px;text-transform:uppercase;letter-spacing:0.5px}
163
+
164
+ .dbg-section{margin-bottom:16px}
165
+ .dbg-section-hdr{
166
+ font-size:12px;font-weight:700;color:var(--fg2);text-transform:uppercase;
167
+ letter-spacing:0.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px;
168
+ }
169
+ .dbg-section-hdr .badge{
170
+ display:inline-block;min-width:18px;height:18px;border-radius:9px;
171
+ background:var(--red);color:#fff;font-size:10px;font-weight:700;
172
+ line-height:18px;text-align:center;padding:0 5px;
173
+ }
174
+ .dbg-section-hdr .badge-warn{background:#f59e0b}
175
+ .dbg-section-hdr .badge-ok{background:var(--green)}
176
+
177
+ .dbg-entry{
178
+ background:var(--bg2);border:1px solid var(--bdr);border-radius:8px;
179
+ padding:10px 12px;margin-bottom:6px;font-size:12px;line-height:1.5;
180
+ }
181
+ .dbg-entry-hdr{display:flex;align-items:center;gap:6px;margin-bottom:4px}
182
+ .dbg-level{
183
+ font-size:10px;font-weight:700;text-transform:uppercase;padding:1px 5px;
184
+ border-radius:4px;flex-shrink:0;
185
+ }
186
+ .dbg-level-error{background:#fecaca;color:#991b1b}
187
+ .dbg-level-warn{background:#fef3c7;color:#92400e}
188
+ .dbg-level-info{background:#dbeafe;color:#1e40af}
189
+ .dbg-level-log{background:var(--bg);color:var(--fg2)}
190
+ @media(prefers-color-scheme:dark){
191
+ .dbg-level-error{background:#450a0a;color:#fca5a5}
192
+ .dbg-level-warn{background:#451a03;color:#fde68a}
193
+ .dbg-level-info{background:#172554;color:#93c5fd}
194
+ .dbg-level-log{background:var(--bg);color:var(--fg3)}
195
+ }
196
+ .dbg-time{font-size:10px;color:var(--fg3);margin-left:auto;flex-shrink:0}
197
+ .dbg-msg{
198
+ font-family:'SF Mono','Cascadia Code','Fira Code','Consolas',monospace;
199
+ font-size:11px;color:var(--fg);word-break:break-all;
200
+ }
201
+ .dbg-url{font-size:10px;color:var(--fg3);margin-top:2px;word-break:break-all}
202
+ .dbg-method{font-weight:700;margin-right:4px}
203
+ .dbg-status{font-weight:700;margin-left:4px}
204
+ .dbg-status-fail{color:var(--red)}
205
+ .dbg-status-ok{color:var(--green)}
206
+ .dbg-empty{text-align:center;padding:40px 24px;color:var(--fg3);font-size:13px}
207
+
208
+ .dbg-health{
209
+ display:grid;grid-template-columns:1fr 1fr;gap:6px;
210
+ }
211
+ .dbg-health-item{
212
+ background:var(--bg2);border:1px solid var(--bdr);border-radius:8px;
213
+ padding:8px 10px;font-size:11px;
214
+ }
215
+ .dbg-health-label{color:var(--fg2);margin-bottom:2px}
216
+ .dbg-health-val{font-weight:600;font-variant-numeric:tabular-nums}
217
+ </style>
218
+ </head>
219
+ <body>
220
+
221
+ <!-- App list view -->
222
+ <div id="view-list" class="active">
223
+ <div class="hdr">
224
+ <h1>LocalPOV</h1>
225
+ <p>Development context bridge</p>
226
+ </div>
227
+ <div class="scan" id="scan-bar">
228
+ <span class="scan-dot"></span>
229
+ <span>Scanning for dev servers...</span>
230
+ </div>
231
+ <div class="apps" id="apps"></div>
232
+ </div>
233
+
234
+ <!-- Preview view -->
235
+ <div id="view-preview">
236
+ <div class="prev-bar">
237
+ <button class="btn" id="btn-back">&#8249; Apps</button>
238
+ <div class="prev-info">:<span class="prev-port" id="p-port"></span> <span id="p-fw"></span></div>
239
+ <button class="btn btn-icon" id="btn-reload">&#8635;</button>
240
+ </div>
241
+ <iframe id="frame" sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-modals allow-popups-to-escape-sandbox"></iframe>
242
+ </div>
243
+
244
+ <!-- Terminal view -->
245
+ <div id="view-terminal">
246
+ <div class="term-bar">
247
+ <button class="btn" id="btn-term-back">&#8249; Apps</button>
248
+ <div class="term-info"><span id="term-title">Terminal</span></div>
249
+ <button class="btn btn-icon" id="btn-term-clear" title="Clear">&#8416;</button>
250
+ </div>
251
+ <div id="term-output" class="term-output"></div>
252
+ <div id="term-empty" class="term-empty" style="display:none">
253
+ <div>
254
+ <p style="font-size:16px;margin-bottom:8px">No process running</p>
255
+ <p>Use <code>localpov run -- npm run dev</code></p>
256
+ </div>
257
+ </div>
258
+ <div id="term-status" class="term-status">
259
+ <span id="term-status-dot" class="term-status-dot stopped"></span>
260
+ <span id="term-status-text">Connecting...</span>
261
+ </div>
262
+ </div>
263
+
264
+ <!-- Debug view -->
265
+ <div id="view-debug">
266
+ <div class="dbg-bar">
267
+ <div class="dbg-bar-title">Debug</div>
268
+ <button class="btn btn-icon" id="btn-dbg-refresh" title="Refresh">&#8635;</button>
269
+ </div>
270
+ <div class="dbg-scroll" id="dbg-scroll">
271
+ <!-- Summary cards -->
272
+ <div class="dbg-summary" id="dbg-summary">
273
+ <div class="dbg-stat"><div class="dbg-stat-num" id="dbg-errors">-</div><div class="dbg-stat-label">Console Errors</div></div>
274
+ <div class="dbg-stat"><div class="dbg-stat-num" id="dbg-net-fail">-</div><div class="dbg-stat-label">Network Fails</div></div>
275
+ <div class="dbg-stat"><div class="dbg-stat-num" id="dbg-warnings">-</div><div class="dbg-stat-label">Warnings</div></div>
276
+ <div class="dbg-stat"><div class="dbg-stat-num" id="dbg-net-slow">-</div><div class="dbg-stat-label">Slow Requests</div></div>
277
+ </div>
278
+
279
+ <!-- Console errors -->
280
+ <div class="dbg-section" id="dbg-console-section">
281
+ <div class="dbg-section-hdr">Console <span class="badge" id="dbg-console-badge" style="display:none">0</span></div>
282
+ <div id="dbg-console-list"></div>
283
+ </div>
284
+
285
+ <!-- Network errors -->
286
+ <div class="dbg-section" id="dbg-network-section">
287
+ <div class="dbg-section-hdr">Network <span class="badge" id="dbg-network-badge" style="display:none">0</span></div>
288
+ <div id="dbg-network-list"></div>
289
+ </div>
290
+
291
+ <!-- System health -->
292
+ <div class="dbg-section">
293
+ <div class="dbg-section-hdr">System</div>
294
+ <div class="dbg-health" id="dbg-health">
295
+ <div class="dbg-health-item"><div class="dbg-health-label">Memory</div><div class="dbg-health-val" id="dbg-mem">-</div></div>
296
+ <div class="dbg-health-item"><div class="dbg-health-label">Uptime</div><div class="dbg-health-val" id="dbg-uptime">-</div></div>
297
+ <div class="dbg-health-item"><div class="dbg-health-label">WS Clients</div><div class="dbg-health-val" id="dbg-ws">-</div></div>
298
+ <div class="dbg-health-item"><div class="dbg-health-label">Node</div><div class="dbg-health-val" id="dbg-node">-</div></div>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- Reconnect banner -->
305
+ <div id="reconnect" class="reconnect">Connection lost — reconnecting...</div>
306
+
307
+ <!-- Tab bar -->
308
+ <div class="tab-bar">
309
+ <button class="tab active" id="tab-apps" data-view="list">
310
+ <span class="tab-icon">&#9881;</span>
311
+ Apps
312
+ </button>
313
+ <button class="tab" id="tab-terminal" data-view="terminal">
314
+ <span class="tab-icon">&#9002;</span>
315
+ Terminal
316
+ </button>
317
+ <button class="tab" id="tab-debug" data-view="debug">
318
+ <span class="tab-icon">&#9888;</span>
319
+ <span>Debug <span class="tab-badge" id="dbg-tab-badge" style="display:none">0</span></span>
320
+ </button>
321
+ </div>
322
+
323
+ <script>
324
+ (function() {
325
+ var apps = [];
326
+ var activePort = null;
327
+ var currentView = 'list'; // 'list', 'preview', 'terminal', 'debug'
328
+
329
+ // DOM refs
330
+ var $apps = document.getElementById('apps');
331
+ var $scan = document.getElementById('scan-bar');
332
+ var $conn = document.getElementById('term-status-text');
333
+ var $list = document.getElementById('view-list');
334
+ var $prev = document.getElementById('view-preview');
335
+ var $term = document.getElementById('view-terminal');
336
+ var $debug = document.getElementById('view-debug');
337
+ var $frame = document.getElementById('frame');
338
+ var $port = document.getElementById('p-port');
339
+ var $fw = document.getElementById('p-fw');
340
+ var $termOutput = document.getElementById('term-output');
341
+ var $termEmpty = document.getElementById('term-empty');
342
+ var $termTitle = document.getElementById('term-title');
343
+ var $termStatusDot = document.getElementById('term-status-dot');
344
+ var $termStatusText = document.getElementById('term-status-text');
345
+ var $tabApps = document.getElementById('tab-apps');
346
+ var $tabTerminal = document.getElementById('tab-terminal');
347
+ var $tabDebug = document.getElementById('tab-debug');
348
+
349
+ // ─── Hash routing ───
350
+ // #apps → app list
351
+ // #terminal → terminal view
352
+ // #preview/3000 → preview of port 3000
353
+ function navigate(hash, replace) {
354
+ if (replace) {
355
+ history.replaceState(null, '', '#' + hash);
356
+ } else {
357
+ location.hash = hash;
358
+ }
359
+ }
360
+
361
+ function handleRoute() {
362
+ var hash = (location.hash || '').replace(/^#/, '');
363
+ if (hash === 'terminal') {
364
+ showView('terminal');
365
+ } else if (hash === 'debug') {
366
+ showView('debug');
367
+ } else if (hash.indexOf('preview/') === 0) {
368
+ var port = parseInt(hash.split('/')[1], 10);
369
+ if (port > 0 && port < 65536) {
370
+ // Find framework name from current apps list
371
+ var fw = '';
372
+ for (var i = 0; i < apps.length; i++) {
373
+ if (apps[i].port === port) { fw = apps[i].framework; break; }
374
+ }
375
+ openPreview(port, fw, true); // true = from hash, don't push again
376
+ } else {
377
+ showView('list');
378
+ }
379
+ } else {
380
+ showView('list');
381
+ }
382
+ }
383
+
384
+ window.addEventListener('hashchange', handleRoute);
385
+
386
+ // ─── View management ───
387
+ function showView(name) {
388
+ currentView = name;
389
+ $list.classList.toggle('active', name === 'list');
390
+ $prev.classList.toggle('active', name === 'preview');
391
+ $term.classList.toggle('active', name === 'terminal');
392
+ $debug.classList.toggle('active', name === 'debug');
393
+ $tabApps.classList.toggle('active', name === 'list' || name === 'preview');
394
+ $tabTerminal.classList.toggle('active', name === 'terminal');
395
+ $tabDebug.classList.toggle('active', name === 'debug');
396
+ if (name === 'preview') {
397
+ requestAnimationFrame(function() { requestAnimationFrame(sizeFrame); });
398
+ }
399
+ if (name === 'terminal') {
400
+ scrollTerminal();
401
+ }
402
+ if (name === 'debug') {
403
+ fetchDebugData();
404
+ }
405
+ }
406
+
407
+ // Tab bar
408
+ $tabApps.addEventListener('click', function() {
409
+ if (currentView !== 'list' && currentView !== 'preview') {
410
+ if (activePort) {
411
+ navigate('preview/' + activePort);
412
+ } else {
413
+ navigate('apps');
414
+ }
415
+ }
416
+ });
417
+ $tabTerminal.addEventListener('click', function() {
418
+ navigate('terminal');
419
+ });
420
+ $tabDebug.addEventListener('click', function() {
421
+ navigate('debug');
422
+ });
423
+
424
+ // Set iframe height
425
+ function sizeFrame() {
426
+ var bar = document.querySelector('.prev-bar');
427
+ var tabBar = document.querySelector('.tab-bar');
428
+ if (bar && $frame) {
429
+ $frame.style.height = (window.innerHeight - bar.offsetHeight - tabBar.offsetHeight) + 'px';
430
+ }
431
+ }
432
+ window.addEventListener('resize', sizeFrame);
433
+
434
+ // Navigation
435
+ document.getElementById('btn-back').addEventListener('click', goBack);
436
+ document.getElementById('btn-reload').addEventListener('click', doReload);
437
+ document.getElementById('btn-term-back').addEventListener('click', function() { navigate('apps'); });
438
+ document.getElementById('btn-term-clear').addEventListener('click', function() {
439
+ $termOutput.innerHTML = '';
440
+ });
441
+
442
+ // ─── App list ───
443
+ function fetchApps() {
444
+ var xhr = new XMLHttpRequest();
445
+ xhr.open('GET', 'api/apps', true);
446
+ xhr.timeout = 3000;
447
+ xhr.onload = function() {
448
+ if (xhr.status === 200) {
449
+ try {
450
+ var data = JSON.parse(xhr.responseText);
451
+ renderApps(data.apps || []);
452
+ } catch(e) {}
453
+ }
454
+ };
455
+ xhr.send();
456
+ }
457
+
458
+ var $reconnect = document.getElementById('reconnect');
459
+
460
+ function renderApps(list) {
461
+ apps = list;
462
+ $scan.style.display = list.length > 0 ? 'none' : 'flex';
463
+
464
+ if (list.length === 0) {
465
+ $apps.innerHTML = '<div class="empty"><h2>No dev servers found</h2><p>Run <code>npm run dev</code> or start any<br>local dev server</p></div>';
466
+ return;
467
+ }
468
+
469
+ var html = '';
470
+ for (var i = 0; i < list.length; i++) {
471
+ var a = list[i];
472
+ html += '<div class="card" data-port="' + a.port + '" data-fw="' + esc(a.framework) + '">'
473
+ + '<span class="dot"></span>'
474
+ + '<div class="card-info">'
475
+ + '<div class="card-port">localhost:' + a.port + '</div>'
476
+ + '<div class="card-fw">' + esc(a.framework) + '</div>'
477
+ + '</div>'
478
+ + '<span class="card-arr">\u203A</span>'
479
+ + '</div>';
480
+ }
481
+ $apps.innerHTML = html;
482
+
483
+ var cards = $apps.querySelectorAll('.card');
484
+ for (var j = 0; j < cards.length; j++) {
485
+ cards[j].addEventListener('click', onCardClick);
486
+ }
487
+ }
488
+
489
+ function esc(s) {
490
+ var d = document.createElement('div');
491
+ d.textContent = s;
492
+ return d.innerHTML;
493
+ }
494
+
495
+ function onCardClick() {
496
+ var port = parseInt(this.getAttribute('data-port'), 10);
497
+ var fw = this.getAttribute('data-fw') || '';
498
+ navigate('preview/' + port);
499
+ }
500
+
501
+ function openPreview(port, fw, fromHash) {
502
+ activePort = port;
503
+ $port.textContent = port;
504
+ $fw.textContent = fw ? ' \u00B7 ' + fw : '';
505
+
506
+ // Set per-session port cookie via switch API
507
+ var xhr = new XMLHttpRequest();
508
+ xhr.open('GET', 'api/switch?port=' + port, true);
509
+ xhr.timeout = 5000;
510
+ xhr.onload = function() {
511
+ $frame.src = '../?_lpov=' + Date.now();
512
+ showView('preview');
513
+ };
514
+ xhr.onerror = xhr.ontimeout = function() {
515
+ $frame.src = '../?_lpov=' + Date.now();
516
+ showView('preview');
517
+ };
518
+ xhr.send();
519
+
520
+ if (!fromHash) navigate('preview/' + port);
521
+ }
522
+
523
+ function goBack() {
524
+ activePort = null;
525
+ $frame.src = 'about:blank';
526
+ navigate('apps');
527
+ fetchApps();
528
+ }
529
+
530
+ function doReload() {
531
+ $frame.src = '../?_lpov=' + Date.now();
532
+ }
533
+
534
+ fetchApps();
535
+ setInterval(function() {
536
+ if (currentView === 'list') fetchApps();
537
+ }, 3000);
538
+
539
+ // Handle initial route after first fetch
540
+ setTimeout(handleRoute, 300);
541
+
542
+ // ─── Terminal WebSocket ───
543
+ var termWs = null;
544
+ var termConnected = false;
545
+ var autoScroll = true;
546
+
547
+ function connectTerminal() {
548
+ var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
549
+ var wsUrl = proto + '//' + location.host + '/__localpov__/ws/terminal';
550
+
551
+ try { termWs = new WebSocket(wsUrl); } catch(e) {
552
+ setTimeout(connectTerminal, 3000);
553
+ return;
554
+ }
555
+
556
+ termWs.onopen = function() {
557
+ termConnected = true;
558
+ setTermStatus(true, 'Connected');
559
+ };
560
+
561
+ termWs.onmessage = function(ev) {
562
+ try {
563
+ var msg = JSON.parse(ev.data);
564
+ if (msg.type === 'history') {
565
+ $termOutput.innerHTML = '';
566
+ if (msg.lines && msg.lines.length > 0) {
567
+ $termEmpty.style.display = 'none';
568
+ $termOutput.style.display = 'block';
569
+ for (var i = 0; i < msg.lines.length; i++) {
570
+ appendTermLine(msg.lines[i]);
571
+ }
572
+ scrollTerminal();
573
+ } else {
574
+ $termEmpty.style.display = 'flex';
575
+ $termOutput.style.display = 'none';
576
+ }
577
+ } else if (msg.type === 'data') {
578
+ $termEmpty.style.display = 'none';
579
+ $termOutput.style.display = 'block';
580
+ appendTermLine(msg);
581
+ scrollTerminal();
582
+ } else if (msg.type === 'status') {
583
+ if (!msg.running) {
584
+ $termEmpty.style.display = 'flex';
585
+ $termOutput.style.display = 'none';
586
+ setTermStatus(false, 'No process');
587
+ }
588
+ }
589
+ } catch(e) {}
590
+ };
591
+
592
+ termWs.onclose = function() {
593
+ termConnected = false;
594
+ setTermStatus(false, 'Disconnected');
595
+ setTimeout(connectTerminal, 3000);
596
+ };
597
+
598
+ termWs.onerror = function() {};
599
+ }
600
+
601
+ function setTermStatus(running, text) {
602
+ $termStatusDot.className = 'term-status-dot ' + (running ? 'running' : 'stopped');
603
+ $termStatusText.textContent = text;
604
+ }
605
+
606
+ function scrollTerminal() {
607
+ if (autoScroll) {
608
+ $termOutput.scrollTop = $termOutput.scrollHeight;
609
+ }
610
+ }
611
+
612
+ // Track if user scrolled up manually
613
+ $termOutput.addEventListener('scroll', function() {
614
+ var atBottom = $termOutput.scrollHeight - $termOutput.scrollTop - $termOutput.clientHeight < 40;
615
+ autoScroll = atBottom;
616
+ });
617
+
618
+ // ─── ANSI parser ───
619
+ var ANSI_COLORS = {
620
+ 30:'a-gray',31:'a-red',32:'a-green',33:'a-yellow',
621
+ 34:'a-blue',35:'a-magenta',36:'a-cyan',37:'a-white',
622
+ 90:'a-gray',91:'a-bright-red',92:'a-bright-green',93:'a-bright-yellow',
623
+ 94:'a-bright-blue',95:'a-bright-magenta',96:'a-bright-cyan',97:'a-white'
624
+ };
625
+
626
+ function ansiToHtml(text) {
627
+ var result = '';
628
+ var i = 0;
629
+ var openSpans = 0;
630
+
631
+ while (i < text.length) {
632
+ // ANSI escape sequence
633
+ if (text.charCodeAt(i) === 27 && text[i+1] === '[') {
634
+ var j = i + 2;
635
+ while (j < text.length && text[j] !== 'm' && j - i < 20) j++;
636
+ if (j < text.length && text[j] === 'm') {
637
+ var codes = text.slice(i+2, j).split(';');
638
+ // Close previous spans on reset
639
+ for (var c = 0; c < codes.length; c++) {
640
+ var code = parseInt(codes[c], 10);
641
+ if (code === 0) {
642
+ while (openSpans > 0) { result += '</span>'; openSpans--; }
643
+ } else if (code === 1) {
644
+ result += '<span class="a-bold">'; openSpans++;
645
+ } else if (code === 2) {
646
+ result += '<span class="a-dim">'; openSpans++;
647
+ } else if (ANSI_COLORS[code]) {
648
+ result += '<span class="' + ANSI_COLORS[code] + '">'; openSpans++;
649
+ }
650
+ }
651
+ i = j + 1;
652
+ continue;
653
+ }
654
+ }
655
+
656
+ // Strip other escape sequences (cursor movement, etc.)
657
+ if (text.charCodeAt(i) === 27 && text[i+1] === '[') {
658
+ var k = i + 2;
659
+ while (k < text.length && !(/[A-Za-z]/).test(text[k]) && k - i < 20) k++;
660
+ if (k < text.length) { i = k + 1; continue; }
661
+ }
662
+
663
+ // Carriage return (handle \r for progress bars)
664
+ if (text[i] === '\r') { i++; continue; }
665
+
666
+ // HTML escape
667
+ if (text[i] === '<') result += '&lt;';
668
+ else if (text[i] === '>') result += '&gt;';
669
+ else if (text[i] === '&') result += '&amp;';
670
+ else result += text[i];
671
+ i++;
672
+ }
673
+
674
+ while (openSpans > 0) { result += '</span>'; openSpans--; }
675
+ return result;
676
+ }
677
+
678
+ function appendTermLine(line) {
679
+ var span = document.createElement('span');
680
+ if (line.type === 'stderr' || line.stream === 'stderr') {
681
+ span.className = 't-err';
682
+ } else if (line.type === 'system' || line.stream === 'system') {
683
+ span.className = 't-sys';
684
+ }
685
+ span.innerHTML = ansiToHtml(line.text || '');
686
+ $termOutput.appendChild(span);
687
+
688
+ // Limit DOM nodes (keep last 2000 elements)
689
+ while ($termOutput.childNodes.length > 2000) {
690
+ $termOutput.removeChild($termOutput.firstChild);
691
+ }
692
+ }
693
+
694
+ // Size terminal output
695
+ function sizeTerminal() {
696
+ var bar = document.querySelector('.term-bar');
697
+ var status = document.querySelector('.term-status');
698
+ var tabBar = document.querySelector('.tab-bar');
699
+ if (bar && $termOutput) {
700
+ var h = window.innerHeight - bar.offsetHeight - status.offsetHeight - tabBar.offsetHeight;
701
+ $termOutput.style.height = h + 'px';
702
+ $termEmpty.style.height = h + 'px';
703
+ }
704
+ }
705
+ window.addEventListener('resize', sizeTerminal);
706
+ requestAnimationFrame(sizeTerminal);
707
+
708
+ // Start terminal connection
709
+ connectTerminal();
710
+
711
+ // Check terminal status periodically
712
+ setInterval(function() {
713
+ var xhr = new XMLHttpRequest();
714
+ xhr.open('GET', 'api/terminal', true);
715
+ xhr.timeout = 3000;
716
+ xhr.onload = function() {
717
+ try {
718
+ var data = JSON.parse(xhr.responseText);
719
+ if (data.command) {
720
+ $termTitle.innerHTML = '<span class="term-cmd">' + esc(data.command) + '</span>';
721
+ setTermStatus(data.running, data.running ? 'Running' : 'Exited (' + data.exitCode + ')');
722
+ }
723
+ } catch(e) {}
724
+ };
725
+ xhr.send();
726
+ }, 5000);
727
+
728
+ // ─── Debug panel ───
729
+ var dbgTotalErrors = 0;
730
+
731
+ document.getElementById('btn-dbg-refresh').addEventListener('click', fetchDebugData);
732
+
733
+ function fetchDebugData() {
734
+ fetchJSON('api/browser', renderBrowserSummary);
735
+ fetchJSON('api/browser?source=console', function(data) { renderConsoleEntries(data.entries || []); });
736
+ fetchJSON('api/browser?source=network', function(data) { renderNetworkEntries(data.entries || []); });
737
+ fetchJSON('api/health', renderHealth);
738
+ }
739
+
740
+ function fetchJSON(url, cb) {
741
+ var xhr = new XMLHttpRequest();
742
+ xhr.open('GET', url, true);
743
+ xhr.timeout = 3000;
744
+ xhr.onload = function() {
745
+ if (xhr.status === 200) {
746
+ try { cb(JSON.parse(xhr.responseText)); } catch(e) {}
747
+ }
748
+ };
749
+ xhr.send();
750
+ }
751
+
752
+ function renderBrowserSummary(data) {
753
+ var ce = data.console || {};
754
+ var ne = data.network || {};
755
+
756
+ var errCount = ce.errors || 0;
757
+ var warnCount = ce.warnings || 0;
758
+ var netFail = ne.failed || 0;
759
+ var netSlow = ne.slow || 0;
760
+
761
+ setStatNum('dbg-errors', errCount, errCount > 0);
762
+ setStatNum('dbg-warnings', warnCount, warnCount > 0);
763
+ setStatNum('dbg-net-fail', netFail, netFail > 0);
764
+ setStatNum('dbg-net-slow', netSlow, netSlow > 0);
765
+
766
+ // Tab badge
767
+ dbgTotalErrors = errCount + netFail;
768
+ var $badge = document.getElementById('dbg-tab-badge');
769
+ if (dbgTotalErrors > 0) {
770
+ $badge.textContent = dbgTotalErrors > 99 ? '99+' : dbgTotalErrors;
771
+ $badge.style.display = 'inline-block';
772
+ } else {
773
+ $badge.style.display = 'none';
774
+ }
775
+ }
776
+
777
+ function setStatNum(id, num, isError) {
778
+ var el = document.getElementById(id);
779
+ el.textContent = num;
780
+ el.className = 'dbg-stat-num' + (isError ? ' has-errors' : (num === 0 ? ' all-good' : ''));
781
+ }
782
+
783
+ function renderConsoleEntries(entries) {
784
+ var $list = document.getElementById('dbg-console-list');
785
+ var $badge = document.getElementById('dbg-console-badge');
786
+
787
+ var errors = entries.filter(function(e) { return e.level === 'error' || e.level === 'warn'; });
788
+
789
+ if (errors.length > 0) {
790
+ $badge.textContent = errors.length;
791
+ $badge.style.display = 'inline-block';
792
+ $badge.className = 'badge' + (errors.some(function(e) { return e.level === 'error'; }) ? '' : ' badge-warn');
793
+ } else {
794
+ $badge.style.display = 'none';
795
+ }
796
+
797
+ // Show last 30 entries (most recent first)
798
+ var show = entries.slice(-30).reverse();
799
+ if (show.length === 0) {
800
+ $list.innerHTML = '<div class="dbg-empty">No console output captured yet.<br>Open your app through the proxy to start capturing.</div>';
801
+ return;
802
+ }
803
+
804
+ var html = '';
805
+ for (var i = 0; i < show.length; i++) {
806
+ var e = show[i];
807
+ var lvl = e.level || 'log';
808
+ var cls = 'dbg-level dbg-level-' + (lvl === 'debug' ? 'log' : lvl);
809
+ html += '<div class="dbg-entry">'
810
+ + '<div class="dbg-entry-hdr">'
811
+ + '<span class="' + cls + '">' + lvl + '</span>'
812
+ + '<span class="dbg-time">' + formatAge(e.ts) + '</span>'
813
+ + '</div>'
814
+ + '<div class="dbg-msg">' + esc(String(e.message || '').slice(0, 300)) + '</div>'
815
+ + (e.source ? '<div class="dbg-url">' + esc(e.source) + '</div>' : '')
816
+ + (e.url ? '<div class="dbg-url">' + esc(e.url) + '</div>' : '')
817
+ + '</div>';
818
+ }
819
+ $list.innerHTML = html;
820
+ }
821
+
822
+ function renderNetworkEntries(entries) {
823
+ var $list = document.getElementById('dbg-network-list');
824
+ var $badge = document.getElementById('dbg-network-badge');
825
+
826
+ var failures = entries.filter(function(e) { return e.status >= 400 || e.error; });
827
+
828
+ if (failures.length > 0) {
829
+ $badge.textContent = failures.length;
830
+ $badge.style.display = 'inline-block';
831
+ } else {
832
+ $badge.style.display = 'none';
833
+ }
834
+
835
+ var show = entries.slice(-30).reverse();
836
+ if (show.length === 0) {
837
+ $list.innerHTML = '<div class="dbg-empty">No network requests captured yet.</div>';
838
+ return;
839
+ }
840
+
841
+ var html = '';
842
+ for (var i = 0; i < show.length; i++) {
843
+ var e = show[i];
844
+ var isFail = e.status >= 400 || e.error;
845
+ var statusCls = isFail ? 'dbg-status dbg-status-fail' : 'dbg-status dbg-status-ok';
846
+ html += '<div class="dbg-entry">'
847
+ + '<div class="dbg-entry-hdr">'
848
+ + '<span class="dbg-method">' + esc(e.method || 'GET') + '</span>'
849
+ + '<span class="' + statusCls + '">' + (e.status || 0) + '</span>'
850
+ + '<span class="dbg-time">' + (e.duration || 0) + 'ms &middot; ' + formatAge(e.ts) + '</span>'
851
+ + '</div>'
852
+ + '<div class="dbg-msg">' + esc(String(e.url || '').slice(0, 200)) + '</div>'
853
+ + (e.error ? '<div class="dbg-url" style="color:var(--red)">' + esc(e.error) + '</div>' : '')
854
+ + (e.responseBody ? '<div class="dbg-url">' + esc(String(e.responseBody).slice(0, 200)) + '</div>' : '')
855
+ + '</div>';
856
+ }
857
+ $list.innerHTML = html;
858
+ }
859
+
860
+ function renderHealth(data) {
861
+ document.getElementById('dbg-mem').textContent = (data.memory || 0) + '% (' + (data.heapMB || 0) + 'MB heap)';
862
+ document.getElementById('dbg-uptime').textContent = formatDuration(data.uptime || 0);
863
+ var ws = data.wsClients || {};
864
+ document.getElementById('dbg-ws').textContent = (ws.browser || 0) + ' browser, ' + (ws.terminal || 0) + ' terminal';
865
+ document.getElementById('dbg-node').textContent = (data.node || '-') + ' / ' + (data.platform || '-');
866
+ }
867
+
868
+ function formatAge(ts) {
869
+ if (!ts) return '';
870
+ var sec = Math.floor((Date.now() - ts) / 1000);
871
+ if (sec < 5) return 'just now';
872
+ if (sec < 60) return sec + 's ago';
873
+ if (sec < 3600) return Math.floor(sec / 60) + 'm ago';
874
+ return Math.floor(sec / 3600) + 'h ago';
875
+ }
876
+
877
+ function formatDuration(sec) {
878
+ if (sec < 60) return sec + 's';
879
+ if (sec < 3600) return Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
880
+ return Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm';
881
+ }
882
+
883
+ // Auto-refresh debug view every 2s
884
+ setInterval(function() {
885
+ if (currentView === 'debug') fetchDebugData();
886
+ }, 2000);
887
+
888
+ // Also poll browser summary in background for tab badge
889
+ setInterval(function() {
890
+ if (currentView !== 'debug') {
891
+ fetchJSON('api/browser', function(data) {
892
+ var ce = data.console || {};
893
+ var ne = data.network || {};
894
+ var total = (ce.errors || 0) + (ne.failed || 0);
895
+ var $badge = document.getElementById('dbg-tab-badge');
896
+ if (total > 0) {
897
+ $badge.textContent = total > 99 ? '99+' : total;
898
+ $badge.style.display = 'inline-block';
899
+ } else {
900
+ $badge.style.display = 'none';
901
+ }
902
+ });
903
+ }
904
+ }, 5000);
905
+
906
+ })();
907
+ </script>
908
+ </body>
909
+ </html>