|
16 | 16 | from .config import Config |
17 | 17 |
|
18 | 18 | _PAGE = """<!doctype html> |
19 | | -<html><head><meta charset="utf-8"><title>ix-mcp</title> |
| 19 | +<html lang="en"><head><meta charset="utf-8"> |
| 20 | +<meta name="viewport" content="width=device-width,initial-scale=1"> |
| 21 | +<title>ix · executions</title> |
20 | 22 | <style> |
21 | | - body{background:#1a1b26;color:#c0caf5;font:13px/1.5 ui-monospace,Menlo,monospace;margin:0;padding:16px} |
22 | | - h1{font-size:14px;color:#7aa2f7;margin:0 0 12px} |
23 | | - .job{border:1px solid #2a2e42;border-radius:6px;margin:0 0 10px;padding:10px} |
24 | | - .job.running{border-color:#9ece6a} |
25 | | - .hdr{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap} |
26 | | - .id{color:#7dcfff}.name{color:#bb9af7}.dur{color:#565f89;margin-left:auto} |
27 | | - .st{padding:0 6px;border-radius:4px;font-size:11px} |
28 | | - .running .st{background:#9ece6a;color:#1a1b26}.done .st{background:#2a2e42} |
29 | | - .error .st{background:#f7768e;color:#1a1b26}.cancelled .st{background:#565f89;color:#1a1b26} |
30 | | - pre{white-space:pre-wrap;word-break:break-word;margin:6px 0 0;color:#a9b1d6;max-height:320px;overflow:auto} |
31 | | - details.code{margin:6px 0 0} |
32 | | - details.code>summary{cursor:pointer;color:#565f89;font-size:11px;list-style:none;user-select:none;display:inline-block} |
33 | | - details.code>summary::-webkit-details-marker{display:none} |
34 | | - details.code>summary::before{content:"▸ code"} |
35 | | - details.code[open]>summary::before{content:"▾ code"} |
36 | | - details.code>pre{color:#565f89;max-height:320px} |
37 | | - .res{color:#9ece6a} |
38 | | - .empty{color:#565f89} |
39 | | - .rich{background:#fff;color:#111;padding:8px;border-radius:4px;margin:6px 0 0;overflow:auto;max-height:460px} |
40 | | - .rich table{border-collapse:collapse;font:12px/1.4 ui-monospace,Menlo,monospace} |
41 | | - .rich th,.rich td{border:1px solid #d0d7de;padding:2px 7px;text-align:right} |
42 | | - .rich th{background:#f6f8fa} |
43 | | - .img{display:block;max-width:100%;margin:6px 0 0;border-radius:4px;background:#fff} |
44 | | - .layout{display:flex;gap:16px;align-items:flex-start} |
| 23 | + :root{ |
| 24 | + --bg:#0b0b0c; --panel:#141416; --panel-2:#1a1a1d; --inset:#101012; |
| 25 | + --line:#242427; --line-2:#2e2e33; |
| 26 | + --text:#e6e6e6; --dim:#9a9aa0; --muted:#6a6a70; --faint:#45454b; |
| 27 | + --active:#f2f2f2; --err:#cf5a5a; |
| 28 | + --mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,"Cascadia Code",monospace; |
| 29 | + } |
| 30 | + *{box-sizing:border-box} |
| 31 | + html{scrollbar-color:var(--line-2) transparent} |
| 32 | + body{background:var(--bg);color:var(--text);font:13px/1.55 var(--mono);margin:0; |
| 33 | + -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility} |
| 34 | + ::selection{background:#33333a;color:#fff} |
| 35 | +
|
| 36 | + header.top{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:12px; |
| 37 | + padding:11px 18px;background:rgba(11,11,12,.86);backdrop-filter:blur(8px); |
| 38 | + border-bottom:1px solid var(--line)} |
| 39 | + .brand{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);font-weight:600} |
| 40 | + .brand b{color:var(--text);font-weight:600} |
| 41 | + .spacer{flex:1} |
| 42 | + .stat{font-size:11px;letter-spacing:.04em;color:var(--muted)} |
| 43 | + .stat b{color:var(--text);font-weight:600} |
| 44 | + .dot{display:inline-block;width:6px;height:6px;background:var(--active);margin-right:6px;vertical-align:middle} |
| 45 | +
|
| 46 | + .wrap{display:flex;gap:18px;align-items:flex-start;padding:18px;max-width:1600px;margin:0 auto} |
45 | 47 | #main{flex:1 1 auto;min-width:0} |
46 | | - .sidebar{flex:0 0 540px;position:sticky;top:0;max-height:100vh;overflow:auto} |
47 | | - .res-card{border:1px solid #2a2e42;border-radius:6px;margin:0 0 10px;padding:8px} |
48 | | - .res-card.live{border-color:#7dcfff} |
49 | | - .res-card.error{border-color:#f7768e} |
50 | | - .res-hdr{display:flex;gap:8px;align-items:center;margin:0 0 6px} |
51 | | - .res-dot{width:8px;height:8px;border-radius:50%;background:#7dcfff;flex:none} |
52 | | - .res-card.error .res-dot{background:#f7768e} |
53 | | - .res-title{color:#bb9af7;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
54 | | - .res-kind{color:#565f89;font-size:11px;margin-left:auto;flex:none} |
55 | | - .res-body{overflow:auto;max-height:60vh} |
| 48 | + .sidebar{flex:0 0 520px;position:sticky;top:62px;max-height:calc(100vh - 78px);overflow:auto} |
| 49 | + @media(max-width:1100px){.wrap{flex-direction:column}.sidebar{flex:none;width:100%;position:static;max-height:none}} |
| 50 | +
|
| 51 | + .sec{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--muted); |
| 52 | + margin:0 0 12px;padding-bottom:7px;border-bottom:1px solid var(--line);font-weight:600} |
| 53 | +
|
| 54 | + /* execution card */ |
| 55 | + .job{background:var(--panel);border:1px solid var(--line);border-left:2px solid var(--line-2); |
| 56 | + margin:0 0 9px;padding:11px 14px} |
| 57 | + .job.running{border-left-color:var(--active)} |
| 58 | + .job.error{border-left-color:var(--err)} |
| 59 | + .hdr{display:flex;gap:9px;align-items:center;flex-wrap:wrap} |
| 60 | + .st{font-size:9px;letter-spacing:.12em;text-transform:uppercase;font-weight:600; |
| 61 | + padding:2px 6px;border:1px solid var(--line-2);color:var(--dim)} |
| 62 | + .running .st{background:var(--active);color:#0b0b0c;border-color:var(--active)} |
| 63 | + .error .st{color:var(--err);border-color:#43282b} |
| 64 | + .cancelled .st{color:var(--muted)} |
| 65 | + .id{color:var(--muted);font-size:12px} |
| 66 | + .name{color:var(--text);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 67 | + .dur{margin-left:auto;color:var(--faint);font-size:11px;font-variant-numeric:tabular-nums;flex:none} |
| 68 | +
|
| 69 | + details.code{margin:8px 0 0} |
| 70 | + details.code>summary{cursor:pointer;color:var(--faint);font-size:10px;letter-spacing:.14em; |
| 71 | + text-transform:uppercase;list-style:none;user-select:none;display:inline-block} |
| 72 | + details.code>summary::-webkit-details-marker{display:none} |
| 73 | + details.code>summary::before{content:"+ source"} |
| 74 | + details.code[open]>summary::before{content:"− source"} |
| 75 | + details.code>pre{color:var(--dim);background:var(--inset);border:1px solid var(--line); |
| 76 | + padding:9px 11px;max-height:340px} |
| 77 | +
|
| 78 | + pre{white-space:pre-wrap;word-break:break-word;margin:8px 0 0;color:var(--dim); |
| 79 | + max-height:340px;overflow:auto;font-size:12px} |
| 80 | + pre.res{color:var(--text)} |
| 81 | + pre.error{color:var(--err)} |
| 82 | + .empty{color:var(--faint);font-style:italic;font-size:12px;padding:2px 0} |
| 83 | +
|
| 84 | + /* rich notebook output: blends into the surface, no white card */ |
| 85 | + .rich{background:var(--inset);border:1px solid var(--line);padding:10px;margin:8px 0 0; |
| 86 | + overflow:auto;max-height:480px;color:var(--text)} |
| 87 | + .rich table{border-collapse:collapse;font:12px/1.45 var(--mono);color:var(--text)} |
| 88 | + .rich th,.rich td{border-bottom:1px solid var(--line);padding:3px 12px;text-align:right;white-space:nowrap} |
| 89 | + .rich th{color:var(--dim);font-weight:600;border-bottom:1px solid var(--line-2)} |
| 90 | + .rich tr:hover td{background:var(--panel-2)} |
| 91 | + .img{display:block;max-width:100%;margin:8px 0 0;border:1px solid var(--line);background:#fff} |
| 92 | +
|
| 93 | + /* resources */ |
| 94 | + .res-card{background:var(--panel);border:1px solid var(--line);border-left:2px solid var(--line-2); |
| 95 | + margin:0 0 9px;padding:9px 11px} |
| 96 | + .res-card.live{border-left-color:var(--active)} |
| 97 | + .res-card.error{border-left-color:var(--err)} |
| 98 | + .res-hdr{display:flex;gap:8px;align-items:center;margin:0 0 7px} |
| 99 | + .res-dot{width:6px;height:6px;background:var(--active);flex:none} |
| 100 | + .res-card.error .res-dot{background:var(--err)} |
| 101 | + .res-title{color:var(--text);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 102 | + .res-kind{margin-left:auto;color:var(--faint);font-size:10px;letter-spacing:.1em; |
| 103 | + text-transform:uppercase;flex:none} |
| 104 | + .res-body{overflow:auto;max-height:62vh} |
| 105 | + ::-webkit-scrollbar{width:9px;height:9px} |
| 106 | + ::-webkit-scrollbar-thumb{background:var(--line-2)} |
| 107 | + ::-webkit-scrollbar-track{background:transparent} |
56 | 108 | </style></head><body> |
57 | | -<div class="layout"> |
58 | | - <div id="main"><h1>ix-mcp executions</h1><div id="jobs"></div></div> |
59 | | - <aside class="sidebar"><h1>resources</h1><div id="resources"></div></aside> |
| 109 | +<header class="top"> |
| 110 | + <span class="brand"><b>ix</b> · mcp</span> |
| 111 | + <span class="spacer"></span> |
| 112 | + <span class="stat" id="run-stat"></span> |
| 113 | +</header> |
| 114 | +<div class="wrap"> |
| 115 | + <div id="main"><div class="sec">executions</div><div id="jobs"></div></div> |
| 116 | + <aside class="sidebar"><div class="sec">resources</div><div id="resources"></div></aside> |
60 | 117 | </div> |
61 | 118 | <script> |
| 119 | +const esc=s=>(s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])); |
62 | 120 | async function tick(){ |
63 | 121 | try{ |
64 | 122 | const r=await fetch('api/jobs');const js=await r.json(); |
65 | 123 | const el=document.getElementById('jobs'); |
| 124 | + const running=js.filter(j=>j.status==='running').length; |
| 125 | + document.getElementById('run-stat').innerHTML= |
| 126 | + (running?`<span class="dot"></span><b>${running}</b> running `:'')+`<b>${js.length}</b> total`; |
66 | 127 | if(!js.length){el.innerHTML='<div class="empty">no executions yet</div>';return;} |
67 | 128 | js.sort((a,b)=>a.started_at-b.started_at); // oldest at top, newest at bottom |
68 | 129 | const nearBottom=(window.innerHeight+window.scrollY)>=(document.body.scrollHeight-120); |
| 130 | + // Rich outputs render like a notebook cell. The dashboard is read-only over the |
| 131 | + // tailnet (the trust boundary); the HTML is the agent's own code output, so it |
| 132 | + // is injected as-is rather than re-sanitized. |
| 133 | + const rich=o=>{const d=(o&&o.data)||{}; |
| 134 | + if(d['image/png'])return `<img class="img" src="data:image/png;base64,${d['image/png']}">`; |
| 135 | + if(d['image/jpeg'])return `<img class="img" src="data:image/jpeg;base64,${d['image/jpeg']}">`; |
| 136 | + if(d['image/svg+xml'])return `<div class="rich">${d['image/svg+xml']}</div>`; |
| 137 | + if(d['text/html'])return `<div class="rich">${d['text/html']}</div>`; |
| 138 | + if(d['text/markdown'])return `<pre>${esc(d['text/markdown'])}</pre>`; |
| 139 | + if(d['text/plain'])return `<pre class="res">${esc(d['text/plain'])}</pre>`; |
| 140 | + return '';}; |
69 | 141 | el.innerHTML=js.map(j=>{ |
70 | 142 | const dur=((j.ended_at||Date.now()/1000)-j.started_at).toFixed(1); |
71 | | - const esc=s=>(s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])); |
72 | | - // Rich outputs render like a notebook cell. The dashboard is read-only over the |
73 | | - // tailnet (the trust boundary); the HTML is the agent's own code output, so it |
74 | | - // is injected as-is rather than re-sanitized. |
75 | | - const rich=o=>{const d=(o&&o.data)||{}; |
76 | | - if(d['image/png'])return `<img class="img" src="data:image/png;base64,${d['image/png']}">`; |
77 | | - if(d['image/jpeg'])return `<img class="img" src="data:image/jpeg;base64,${d['image/jpeg']}">`; |
78 | | - if(d['image/svg+xml'])return `<div class="rich">${d['image/svg+xml']}</div>`; |
79 | | - if(d['text/html'])return `<div class="rich">${d['text/html']}</div>`; |
80 | | - if(d['text/markdown'])return `<pre>${esc(d['text/markdown'])}</pre>`; |
81 | | - if(d['text/plain'])return `<pre class="res">${esc(d['text/plain'])}</pre>`; |
82 | | - return '';}; |
83 | 143 | const richOut=(j.outputs&&j.outputs.length) |
84 | 144 | ? j.outputs.map(rich).join('') |
85 | 145 | : (j.result?`<pre class="res">${esc(j.result)}</pre>`:''); |
|
90 | 150 | <details class="code"><summary></summary><pre>${esc(j.code)}</pre></details> |
91 | 151 | ${j.output?`<pre>${esc(j.output)}</pre>`:''} |
92 | 152 | ${richOut} |
93 | | - ${j.error&&!j.output.includes(j.error)?`<pre class="error">${esc(j.error)}</pre>`:''} |
| 153 | + ${j.error&&!(j.output||'').includes(j.error)?`<pre class="error">${esc(j.error)}</pre>`:''} |
94 | 154 | </div>`; |
95 | 155 | }).join(''); |
96 | 156 | if(nearBottom) window.scrollTo(0, document.body.scrollHeight); |
|
101 | 161 | const r=await fetch('api/resources');const rs=await r.json(); |
102 | 162 | const el=document.getElementById('resources'); |
103 | 163 | if(!rs.length){el.innerHTML='<div class="empty">no live resources</div>';return;} |
104 | | - const esc=s=>(s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])); |
105 | 164 | // A resource's html is the agent's own live render (a terminal screen, a custom |
106 | 165 | // widget). The dashboard is read-only over the tailnet (the trust boundary), so |
107 | 166 | // it is injected as-is, exactly like job output above. |
|
0 commit comments