@polderlabs/bizar-omp 0.5.0 → 0.6.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.
@@ -1,594 +1,94 @@
1
- /**
2
- * The dashboard ships as one self-contained document: no fonts, scripts, or
3
- * styles are fetched from the network, so the page works on an air-gapped
4
- * machine. Theme tokens exist for light and dark; the page follows
5
- * `prefers-color-scheme` until the operator picks a theme explicitly.
6
- */
1
+ /** Self-contained, authenticated browser dashboard. */
7
2
  export const DASHBOARD_PATH = "/dash";
8
3
  export function dashboardPageHtml() {
9
4
  return `<!doctype html>
10
5
  <html lang="en" data-theme="dark">
11
6
  <head>
12
7
  <meta charset="utf-8" />
13
- <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
14
9
  <meta name="color-scheme" content="dark light" />
15
- <meta name="robots" content="noindex" />
16
- <title>Bizar OMB Dashboard</title>
10
+ <title>Bizar OMP Dashboard</title>
17
11
  <style>
18
- :root {
19
- --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
20
- --radius: 14px;
21
- }
22
- html[data-theme="dark"] {
23
- --bg: #07080a;
24
- --bg-glow: radial-gradient(1200px 600px at 78% -12%, rgba(255, 46, 166, 0.16), transparent 60%),
25
- radial-gradient(900px 500px at 8% 0%, rgba(0, 180, 255, 0.10), transparent 55%);
26
- --panel: #0f1116;
27
- --panel-2: #14171d;
28
- --panel-invert: #f4f5f7;
29
- --text-invert: #0b0c10;
30
- --line: #232833;
31
- --text: #e7e9ee;
32
- --muted: #8b93a3;
33
- --accent: #ff2ea6;
34
- --accent-2: #00b4ff;
35
- --ok: #5fd68a;
36
- --warn: #ffb347;
37
- --err: #ff5f6d;
38
- --shadow: none;
39
- }
40
- html[data-theme="light"] {
41
- --bg: #efedE7;
42
- --bg-glow: linear-gradient(180deg, rgba(255, 255, 255, 0.75), transparent 320px);
43
- --panel: #fbfaf7;
44
- --panel-2: #f3f0ea;
45
- --panel-invert: #111214;
46
- --text-invert: #f6f6f4;
47
- --line: #ddd8ce;
48
- --text: #14161a;
49
- --muted: #6b6f7a;
50
- --accent: #cf0d77;
51
- --accent-2: #0a6ed1;
52
- --ok: #1f9d55;
53
- --warn: #a86400;
54
- --err: #c81e2b;
55
- --shadow: 0 1px 2px rgba(20, 22, 26, 0.05);
56
- }
57
- * { box-sizing: border-box; }
58
- body {
59
- margin: 0;
60
- min-height: 100vh;
61
- background: var(--bg);
62
- background-image: var(--bg-glow);
63
- background-attachment: fixed;
64
- color: var(--text);
65
- font-family: var(--mono);
66
- font-size: 14px;
67
- line-height: 1.45;
68
- -webkit-font-smoothing: antialiased;
69
- }
70
- a { color: var(--accent-2); }
71
- .bar {
72
- display: flex; align-items: center; justify-content: space-between; gap: 16px;
73
- padding: 18px 28px; border-bottom: 1px solid var(--line);
74
- position: sticky; top: 0; z-index: 5;
75
- background: color-mix(in srgb, var(--bg) 88%, transparent);
76
- backdrop-filter: blur(10px);
77
- }
78
- .brand { display: flex; align-items: center; gap: 10px; letter-spacing: 0.14em; font-size: 12px; }
79
- .brand-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 12px var(--accent); }
80
- .brand-name { font-weight: 700; }
81
- .brand-sub { color: var(--muted); }
82
- .bar-right { display: flex; align-items: center; gap: 10px; }
83
- .chip {
84
- display: inline-flex; align-items: center; gap: 7px;
85
- border: 1px solid var(--line); border-radius: 999px;
86
- padding: 5px 12px; font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
87
- color: var(--muted); background: var(--panel);
88
- }
89
- .chip .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
90
- .chip.live { color: var(--text); }
91
- .chip.live .dot { background: var(--ok); box-shadow: 0 0 10px var(--ok); }
92
- .chip.down .dot { background: var(--err); box-shadow: 0 0 10px var(--err); }
93
- .chip.button { cursor: pointer; font-family: var(--mono); color: var(--text); }
94
- .chip.button:hover { border-color: var(--accent); color: var(--accent); }
95
- main { max-width: 1240px; margin: 0 auto; padding: 26px 28px 64px; }
96
- .muted { color: var(--muted); }
97
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 14px; margin-bottom: 22px; }
98
- .card {
99
- background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
100
- padding: 16px 18px; box-shadow: var(--shadow);
101
- }
102
- .card.invert { background: var(--panel-invert); border-color: var(--panel-invert); color: var(--text-invert); }
103
- .card.invert .label { color: color-mix(in srgb, var(--text-invert) 62%, transparent); }
104
- .label { font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); }
105
- .value { font-size: 40px; font-weight: 700; letter-spacing: 0.01em; font-variant-numeric: tabular-nums; line-height: 1.1; margin-top: 8px; }
106
- .sub { font-size: 11px; color: var(--muted); margin-top: 6px; }
107
- .toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 18px; }
108
- .filters { display: flex; gap: 8px; flex-wrap: wrap; }
109
- .filter {
110
- border: 1px solid var(--line); background: var(--panel); color: var(--muted);
111
- border-radius: 999px; padding: 6px 13px; font-family: var(--mono); font-size: 10px;
112
- letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer;
113
- }
114
- .filter[aria-pressed="true"] { color: var(--text); border-color: var(--accent); }
115
- .toolbar-right { display: flex; align-items: center; gap: 14px; }
116
- .btn {
117
- border: 1px solid var(--line); background: var(--panel); color: var(--text);
118
- border-radius: 10px; padding: 7px 12px; font-family: var(--mono); font-size: 10px;
119
- letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer;
120
- }
121
- .btn:hover { border-color: var(--accent); }
122
- .btn.primary { border-color: var(--accent); color: var(--accent); }
123
- .btn.danger { border-color: var(--err); color: var(--err); }
124
- .btn:disabled { opacity: 0.45; cursor: not-allowed; }
125
- .notice {
126
- border: 1px solid var(--err); border-radius: 10px; padding: 11px 14px; margin-bottom: 16px;
127
- color: var(--err); background: color-mix(in srgb, var(--err) 8%, transparent); font-size: 12px;
128
- }
129
- .hidden { display: none !important; }
130
- .workspace { margin-bottom: 22px; }
131
- .workspace-head {
132
- display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
133
- padding: 0 4px 9px; border-bottom: 1px solid var(--line); margin-bottom: 10px;
134
- }
135
- .workspace-path { font-size: 12px; word-break: break-all; }
136
- .workspace-path b { color: var(--accent-2); font-weight: 400; }
137
- .session {
138
- display: grid; grid-template-columns: 12px minmax(0, 1fr) 96px auto; gap: 14px;
139
- align-items: center; padding: 12px 16px; margin-bottom: 8px;
140
- background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
141
- }
142
- .session:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
143
- .state-dot { width: 9px; height: 9px; border-radius: 50%; }
144
- .dot-ok { background: var(--ok); box-shadow: 0 0 10px var(--ok); }
145
- .dot-warn { background: var(--warn); }
146
- .dot-err { background: var(--err); }
147
- .dot-muted { background: var(--muted); }
148
- .session-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
149
- .session-meta { font-size: 11px; color: var(--muted); margin-top: 3px; overflow-wrap: anywhere; }
150
- .session-age { font-size: 11px; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
151
- .session-actions { display: flex; gap: 8px; }
152
- .session-actions .btn { padding: 5px 10px; }
153
- .empty { border: 1px dashed var(--line); border-radius: 12px; padding: 34px; text-align: center; color: var(--muted); }
154
- footer { max-width: 1240px; margin: 0 auto; padding: 0 28px 40px; color: var(--muted); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
155
- dialog {
156
- border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); color: var(--text);
157
- padding: 0; width: min(520px, 92vw); font-family: var(--mono);
158
- }
159
- dialog::backdrop { background: rgba(4, 5, 8, 0.62); }
160
- .dialog-head { padding: 16px 18px 0; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); }
161
- .dialog-body { padding: 14px 18px; display: grid; gap: 12px; }
162
- .field { display: grid; gap: 6px; }
163
- .field span { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
164
- input, textarea, select {
165
- font-family: var(--mono); font-size: 13px; color: var(--text); background: var(--panel-2);
166
- border: 1px solid var(--line); border-radius: 9px; padding: 9px 11px; width: 100%;
167
- }
168
- input:focus, textarea:focus, select:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
169
- textarea { min-height: 62px; resize: vertical; }
170
- .dialog-foot { display: flex; justify-content: flex-end; gap: 10px; padding: 4px 18px 18px; }
171
- .toast {
172
- position: fixed; right: 22px; bottom: 22px; max-width: 460px; z-index: 20;
173
- background: var(--panel); border: 1px solid var(--accent); border-radius: 10px;
174
- padding: 11px 14px; font-size: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
175
- }
176
- @media (max-width: 760px) {
177
- .session { grid-template-columns: 12px minmax(0, 1fr); }
178
- .session-age, .session-actions { grid-column: 2; text-align: left; }
179
- .session-actions { flex-wrap: wrap; }
180
- }
12
+ :root{color-scheme:dark;--canvas:#090b0e;--surface:#0f1216;--raised:#151a20;--active:#172531;--border:#2d3742;--strong:#435262;--text:#ecf2f8;--muted:#91a0af;--faint:#667481;--accent:#00b4ff;--accent-ink:#001a25;--success:#57d38c;--warning:#f2bd62;--danger:#ff6b75;--focus:#8ad9ff;--shadow:0 18px 48px #00000047;--sans:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;--mono:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace}
13
+ html[data-theme="light"]{color-scheme:light;--canvas:#eef3f7;--surface:#fff;--raised:#f7fafc;--active:#e4f5fc;--border:#c9d5df;--strong:#91a5b5;--text:#15212b;--muted:#526472;--faint:#738492;--accent:#007eb5;--accent-ink:#fff;--success:#187b45;--warning:#8b5a00;--danger:#bd2634;--focus:#005d86;--shadow:0 18px 48px #25384829}
14
+ *{box-sizing:border-box}html,body{width:100%;min-height:100%}body{margin:0;overflow:hidden;background:var(--canvas);color:var(--text);font:14px/1.45 var(--sans)}button,input,textarea{font:inherit;color:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.5}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.hidden{display:none!important}.app{height:100dvh;display:grid;grid-template-rows:64px minmax(0,1fr)}
15
+ .masthead{display:flex;align-items:center;gap:16px;min-width:0;padding:0 18px;border-bottom:1px solid var(--border);background:var(--surface);z-index:5}.brand{display:flex;align-items:center;gap:11px;min-width:190px}.brand-mark{width:30px;height:30px;display:grid;place-items:center;border:1px solid var(--strong);border-radius:7px;color:var(--accent);font:700 19px/1 var(--mono)}.brand-name{font-weight:720;letter-spacing:-.02em}.eyebrow{color:var(--muted);font:600 10px/1.2 var(--mono);letter-spacing:.14em;text-transform:uppercase}.daemon-state{display:flex;align-items:center;gap:8px;min-width:0;color:var(--muted);font-size:12px}.daemon-state span:last-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.status-dot{width:8px;height:8px;flex:0 0 auto;border-radius:50%;background:var(--faint);box-shadow:0 0 0 3px color-mix(in srgb,var(--faint) 16%,transparent)}.status-dot.live{background:var(--success);box-shadow:0 0 0 3px color-mix(in srgb,var(--success) 16%,transparent)}.status-dot.warn{background:var(--warning);box-shadow:0 0 0 3px color-mix(in srgb,var(--warning) 16%,transparent)}.masthead-actions{margin-left:auto;display:flex;align-items:center;gap:8px}
16
+ .button{min-height:38px;border:1px solid var(--border);border-radius:7px;background:var(--raised);color:var(--text);padding:0 13px;display:inline-flex;align-items:center;justify-content:center;gap:8px;font-weight:620}.button:hover:not(:disabled){border-color:var(--strong);background:var(--active)}.button.primary{border-color:var(--accent);background:var(--accent);color:var(--accent-ink)}.button.primary:hover:not(:disabled){filter:brightness(1.08)}.button.danger{color:var(--danger)}.button.ghost{background:transparent}.button.icon{width:40px;padding:0;font:700 15px/1 var(--mono)}
17
+ .workspace{min-height:0;display:grid;grid-template-columns:290px minmax(420px,1fr) 280px}.panel{min-width:0;min-height:0;background:var(--surface)}.session-panel{display:flex;flex-direction:column;border-right:1px solid var(--border)}.conversation-panel{display:grid;grid-template-rows:auto minmax(0,1fr) auto;background:var(--canvas)}.inspector-panel{overflow:auto;border-left:1px solid var(--border)}.panel-head{min-height:72px;padding:15px 16px 13px;border-bottom:1px solid var(--border)}.panel-title{display:flex;align-items:center;gap:9px;margin:3px 0 0;font-weight:700}.count{min-width:24px;padding:2px 7px;border-radius:999px;background:var(--raised);color:var(--muted);text-align:center;font:600 11px/1.5 var(--mono)}
18
+ .search-wrap{padding:12px 12px 8px}.search,.field-input,.field-textarea{width:100%;border:1px solid var(--border);border-radius:7px;background:var(--raised);color:var(--text);padding:10px 11px}.search::placeholder,.field-input::placeholder,.field-textarea::placeholder{color:var(--faint)}.filters{display:flex;gap:6px;padding:0 12px 10px;overflow-x:auto;scrollbar-width:none}.filter{min-height:30px;white-space:nowrap;border:1px solid transparent;border-radius:999px;background:transparent;color:var(--muted);padding:0 9px;font-size:12px}.filter:hover,.filter.active{border-color:var(--border);background:var(--raised);color:var(--text)}.session-list{min-height:0;overflow-y:auto;padding:4px 8px 16px}.workspace-label{padding:13px 8px 6px;color:var(--faint);font:650 10px/1.2 var(--mono);letter-spacing:.1em;text-transform:uppercase;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-row{width:100%;min-height:62px;display:grid;grid-template-columns:10px minmax(0,1fr);gap:10px;align-items:start;border:1px solid transparent;border-radius:7px;background:transparent;color:inherit;padding:10px 9px;text-align:left}.session-row:hover{background:var(--raised)}.session-row.selected{border-color:color-mix(in srgb,var(--accent) 55%,var(--border));background:var(--active)}.session-row .status-dot{margin-top:6px;width:7px;height:7px}.session-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:670}.session-meta{display:flex;gap:7px;margin-top:4px;color:var(--muted);font:11px/1.3 var(--mono)}.empty{padding:26px 18px;color:var(--muted);text-align:center}.empty strong{display:block;margin-bottom:6px;color:var(--text)}
19
+ .conversation-head{min-height:72px;display:flex;align-items:center;gap:14px;padding:12px 18px;border-bottom:1px solid var(--border);background:var(--surface)}.conversation-identity{min-width:0}.conversation-title{margin:2px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:17px;letter-spacing:-.015em}.conversation-actions{margin-left:auto;display:flex;gap:7px}.state-label{display:inline-flex;align-items:center;gap:7px;color:var(--muted);font:600 11px/1.2 var(--mono);text-transform:uppercase;letter-spacing:.08em}.console-wrap{min-height:0;position:relative;overflow:hidden;padding:12px}.console{width:100%;height:100%;margin:0;overflow:auto;border:1px solid var(--border);border-radius:8px;background:#080a0d;color:#dce7ef;padding:16px 18px 32px;white-space:pre-wrap;overflow-wrap:anywhere;font:12.5px/1.55 var(--mono);tab-size:2;scrollbar-color:#34404b #080a0d}html[data-theme="light"] .console{background:#17212a;color:#eaf2f7;border-color:#263745}.console-state{height:100%;display:grid;place-items:center;color:#8fa0ad;text-align:center}.console-state strong{display:block;color:#eaf2f7;margin-bottom:5px}.follow-button{position:absolute;right:28px;bottom:26px;min-height:34px;border:1px solid #3d4e5d;border-radius:999px;background:#17212a;color:#eaf2f7;padding:0 12px;box-shadow:var(--shadow)}
20
+ .composer{border-top:1px solid var(--border);background:var(--surface);padding:12px 16px 14px}.composer-box{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:9px;align-items:end;border:1px solid var(--strong);border-radius:9px;background:var(--raised);padding:8px}.composer textarea{min-height:50px;max-height:160px;resize:vertical;border:0;outline:0;background:transparent;padding:7px 8px;line-height:1.45}.composer-meta{display:flex;justify-content:space-between;gap:10px;margin-top:7px;color:var(--faint);font-size:11px}.inspector-section{padding:18px 16px;border-bottom:1px solid var(--border)}.inspector-section h2{margin:0 0 13px;font-size:13px}.detail-list{display:grid;gap:13px;margin:0}.detail-list dt{margin-bottom:4px;color:var(--faint);font:600 10px/1.2 var(--mono);letter-spacing:.1em;text-transform:uppercase}.detail-list dd{margin:0;overflow-wrap:anywhere;color:var(--muted)}.mono{font-family:var(--mono);font-size:12px}.action-stack{display:grid;gap:8px}.action-stack .button{width:100%;justify-content:flex-start}.mobile-nav{display:none}
21
+ dialog{width:min(520px,calc(100vw - 28px));max-height:min(760px,calc(100dvh - 28px));padding:0;overflow:auto;border:1px solid var(--strong);border-radius:10px;background:var(--surface);color:var(--text);box-shadow:var(--shadow)}dialog::backdrop{background:#030609ad;backdrop-filter:blur(2px)}.dialog-head{display:flex;align-items:flex-start;gap:16px;padding:19px 20px 15px;border-bottom:1px solid var(--border)}.dialog-head h2{margin:2px 0 0;font-size:17px}.dialog-head .button{margin-left:auto}.dialog-body{display:grid;gap:17px;padding:18px 20px}.dialog-foot{display:flex;justify-content:flex-end;gap:9px;padding:14px 20px 19px;border-top:1px solid var(--border)}.field{display:grid;gap:7px}.field-label{font-weight:650}.field-help{color:var(--muted);font-size:12px}.field-textarea{min-height:96px;resize:vertical}.setting-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:center;padding:3px 0}.setting-row strong{display:block;margin-bottom:3px}.setting-row span{color:var(--muted);font-size:12px}.switch{width:46px;height:26px;position:relative;display:inline-flex}.switch input{position:absolute;opacity:0}.switch-track{width:46px;height:26px;border:1px solid var(--strong);border-radius:999px;background:var(--raised)}.switch-track::after{content:"";position:absolute;width:18px;height:18px;left:4px;top:4px;border-radius:50%;background:var(--muted);transition:transform .15s,background .15s}.switch input:checked+.switch-track{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 30%,var(--surface))}.switch input:checked+.switch-track::after{transform:translateX(20px);background:var(--accent)}.switch input:focus-visible+.switch-track{outline:2px solid var(--focus);outline-offset:2px}.toast{position:fixed;z-index:30;right:18px;bottom:18px;max-width:min(420px,calc(100vw - 36px));border:1px solid var(--strong);border-left:3px solid var(--success);border-radius:8px;background:var(--raised);padding:12px 14px;box-shadow:var(--shadow)}.toast.error{border-left-color:var(--danger)}
22
+ @media(max-width:1099px) and (min-width:761px){.workspace{grid-template-columns:270px minmax(0,1fr);overflow:auto}.conversation-panel{min-height:calc(100dvh - 64px)}.inspector-panel{grid-column:2;border-left:0;border-top:1px solid var(--border);overflow:visible}}
23
+ @media(max-width:760px){.app{grid-template-rows:58px minmax(0,1fr) 62px}.masthead{padding:0 12px;gap:10px}.brand{min-width:0}.brand-copy .eyebrow,.daemon-state,#theme-toggle{display:none}.masthead-actions .button{min-height:44px}#new-session-top{width:44px;padding:0;font-size:0}#new-session-top::before{content:"+";font:700 20px/1 var(--sans)}.workspace{display:block;min-height:0;overflow:hidden}.panel{height:100%;border:0}body[data-mobile-view="sessions"] .conversation-panel,body[data-mobile-view="sessions"] .inspector-panel,body[data-mobile-view="chat"] .session-panel,body[data-mobile-view="chat"] .inspector-panel,body[data-mobile-view="details"] .session-panel,body[data-mobile-view="details"] .conversation-panel{display:none}.conversation-head{min-height:66px;padding:10px 12px}.conversation-actions .button:not(.icon){display:none}.console-wrap{padding:8px}.console{padding:13px 12px 28px;font-size:12px}.composer{padding:9px 9px calc(10px + env(safe-area-inset-bottom))}.composer-box{grid-template-columns:minmax(0,1fr) 52px}.composer .button{min-height:44px;padding:0 10px}.composer-meta span:last-child{display:none}.mobile-nav{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--border);background:var(--surface);padding-bottom:env(safe-area-inset-bottom)}.mobile-nav button{min-height:54px;border:0;border-top:2px solid transparent;background:transparent;color:var(--muted);font-weight:650}.mobile-nav button.active{border-top-color:var(--accent);color:var(--text);background:var(--active)}dialog{width:100vw;max-width:none;height:100dvh;max-height:none;border:0;border-radius:0}.dialog-head .button,.dialog-foot .button{min-height:44px}.dialog-foot{padding-bottom:calc(19px + env(safe-area-inset-bottom))}.toast{right:10px;bottom:calc(72px + env(safe-area-inset-bottom));max-width:calc(100vw - 20px)}}
24
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition:none!important}}
181
25
  </style>
182
26
  </head>
183
- <body>
184
- <header class="bar">
185
- <div class="brand">
186
- <span class="brand-dot"></span>
187
- <span class="brand-name">BIZAR</span>
188
- <span class="brand-sub">OMB DASHBOARD</span>
189
- </div>
190
- <div class="bar-right">
191
- <span class="chip" id="link-chip"><span class="dot"></span><span id="link-text">CONNECTING</span></span>
192
- <button class="chip button" id="theme-toggle" type="button" title="Switch theme">LIGHT</button>
193
- </div>
194
- </header>
195
- <main>
196
- <section class="stats" id="stats"></section>
197
- <section class="toolbar">
198
- <div class="filters" id="filters"></div>
199
- <div class="toolbar-right">
200
- <span class="muted" id="updated" style="font-size:11px"></span>
201
- <button class="btn primary" id="create-open" type="button">+ New session</button>
202
- </div>
203
- </section>
204
- <div class="notice hidden" id="notice"></div>
205
- <section id="workspaces"></section>
27
+ <body data-mobile-view="sessions">
28
+ <div class="app">
29
+ <header class="masthead"><div class="brand"><div class="brand-mark" aria-hidden="true">π</div><div class="brand-copy"><div class="brand-name">Bizar OMP</div><div class="eyebrow">OMB DASHBOARD</div></div></div><div class="daemon-state" id="daemon-state"><span class="status-dot"></span><span>Connecting to local daemon</span></div><div class="masthead-actions"><button class="button ghost" id="theme-toggle" type="button">Light theme</button><button class="button icon" id="settings-open" type="button" aria-label="Open settings" title="Settings">⚙</button><button class="button primary" id="new-session-top" type="button">New session</button></div></header>
30
+ <main class="workspace">
31
+ <aside class="panel session-panel" aria-label="Sessions"><div class="panel-head"><div class="eyebrow">Workspace</div><div class="panel-title">Sessions <span class="count" id="session-count">0</span></div></div><div class="search-wrap"><input class="search" id="session-search" type="search" placeholder="Find a session" aria-label="Find a session" /></div><div class="filters" id="filters" aria-label="Filter sessions"></div><div class="session-list" id="session-list"><div class="empty"><strong>Loading sessions</strong>Reading the local daemon snapshot.</div></div></aside>
32
+ <section class="panel conversation-panel" aria-label="Session chat"><header class="conversation-head"><div class="conversation-identity"><div class="state-label" id="session-state"><span class="status-dot"></span><span>No session selected</span></div><h1 class="conversation-title" id="conversation-title">Choose a session</h1></div><div class="conversation-actions"><button class="button ghost" id="console-refresh" type="button" disabled>Refresh</button><button class="button" id="attach-top" type="button" disabled>Open terminal</button></div></header><div class="console-wrap"><pre class="console" id="console" tabindex="0" aria-label="Live session console"><span class="console-state"><span><strong>No session open</strong>Select a session to read its live console and send a message.</span></span></pre><button class="follow-button hidden" id="console-follow" type="button">Jump to latest</button></div><form class="composer" id="composer"><div class="composer-box"><textarea id="composer-input" placeholder="Send a message to the selected session" aria-label="Message" disabled></textarea><button class="button primary" id="composer-send" type="submit" disabled>Send</button></div><div class="composer-meta"><span id="composer-status">Select a live session to begin.</span><span>Ctrl Enter to send</span></div></form></section>
33
+ <aside class="panel inspector-panel" aria-label="Session details"><div class="panel-head"><div class="eyebrow">Selected session</div><div class="panel-title">Details</div></div><div id="inspector"><div class="empty"><strong>Nothing selected</strong>Session identity and controls appear here.</div></div></aside>
206
34
  </main>
207
- <footer>
208
- <span>Bizar OMB &middot; localhost only &middot; token gated</span>
209
- <span>PolderLabs</span>
210
- </footer>
211
-
212
- <dialog id="create-dialog">
213
- <form method="dialog">
214
- <div class="dialog-head">New session</div>
215
- <div class="dialog-body">
216
- <label class="field"><span>Project directory</span>
217
- <input id="create-cwd" list="workspace-list" placeholder="/path/to/project" autocomplete="off" />
218
- <datalist id="workspace-list"></datalist>
219
- </label>
220
- <label class="field"><span>Task (optional)</span>
221
- <textarea id="create-task" placeholder="What should the agent work on?"></textarea>
222
- </label>
223
- <label class="field"><span>Session name (optional)</span>
224
- <input id="create-id" placeholder="review" autocomplete="off" />
225
- </label>
226
- </div>
227
- <div class="dialog-foot">
228
- <button class="btn" value="cancel" type="submit">Cancel</button>
229
- <button class="btn primary" id="create-submit" value="create" type="submit">Create</button>
230
- </div>
231
- </form>
232
- </dialog>
233
-
234
- <dialog id="rename-dialog">
235
- <form method="dialog">
236
- <div class="dialog-head">Rename session</div>
237
- <div class="dialog-body">
238
- <label class="field"><span>Session name</span>
239
- <input id="rename-id" autocomplete="off" />
240
- </label>
241
- </div>
242
- <div class="dialog-foot">
243
- <button class="btn" value="cancel" type="submit">Cancel</button>
244
- <button class="btn primary" id="rename-submit" value="rename" type="submit">Rename</button>
245
- </div>
246
- </form>
247
- </dialog>
248
-
249
- <dialog id="stop-dialog">
250
- <form method="dialog">
251
- <div class="dialog-head">Stop session</div>
252
- <div class="dialog-body">
253
- <p id="stop-text" style="margin:0">This kills the Bizar tmux session and ends its agent work.</p>
254
- </div>
255
- <div class="dialog-foot">
256
- <button class="btn" value="cancel" type="submit">Cancel</button>
257
- <button class="btn danger" value="stop" type="submit">Stop session</button>
258
- </div>
259
- </form>
260
- </dialog>
261
-
262
- <div class="toast hidden" id="toast"></div>
263
-
35
+ <nav class="mobile-nav" aria-label="Dashboard views"><button type="button" data-view="sessions" class="active">Sessions</button><button type="button" data-view="chat">Chat</button><button type="button" data-view="details">Details</button></nav>
36
+ </div>
37
+ <dialog id="create-dialog" aria-labelledby="create-title"><form method="dialog"><div class="dialog-head"><div><div class="eyebrow">Launch</div><h2 id="create-title">New session</h2></div><button class="button icon ghost" value="cancel" aria-label="Close">×</button></div><div class="dialog-body"><label class="field"><span class="field-label">Project directory</span><input class="field-input" id="create-cwd" list="workspace-list" placeholder="/path/to/project" autocomplete="off" required /><span class="field-help">Must be an existing directory on this machine.</span></label><datalist id="workspace-list"></datalist><label class="field"><span class="field-label">First message</span><textarea class="field-textarea" id="create-task" placeholder="Describe the work for this session"></textarea></label><label class="field"><span class="field-label">Session label</span><input class="field-input" id="create-id" placeholder="Optional, for example review" autocomplete="off" /></label></div><div class="dialog-foot"><button class="button" value="cancel">Cancel</button><button class="button primary" value="create">Create session</button></div></form></dialog>
38
+ <dialog id="rename-dialog" aria-labelledby="rename-title"><form method="dialog"><div class="dialog-head"><div><div class="eyebrow">Identity</div><h2 id="rename-title">Rename session</h2></div><button class="button icon ghost" value="cancel" aria-label="Close">×</button></div><div class="dialog-body"><label class="field"><span class="field-label">Session label</span><input class="field-input" id="rename-id" autocomplete="off" required /></label></div><div class="dialog-foot"><button class="button" value="cancel">Cancel</button><button class="button primary" value="rename">Rename</button></div></form></dialog>
39
+ <dialog id="stop-dialog" aria-labelledby="stop-title"><form method="dialog"><div class="dialog-head"><div><div class="eyebrow">Destructive action</div><h2 id="stop-title">Stop session</h2></div><button class="button icon ghost" value="cancel" aria-label="Close">×</button></div><div class="dialog-body"><p id="stop-text" style="margin:0">This ends the tmux session and its agent work.</p></div><div class="dialog-foot"><button class="button" value="cancel">Keep running</button><button class="button danger" value="stop">Stop session</button></div></form></dialog>
40
+ <dialog id="settings-dialog" aria-labelledby="settings-title"><form method="dialog"><div class="dialog-head"><div><div class="eyebrow">Configuration</div><h2 id="settings-title">Dashboard settings</h2></div><button class="button icon ghost" value="cancel" aria-label="Close">×</button></div><div class="dialog-body"><label class="field"><span class="field-label">Projects folder</span><input class="field-input" id="setting-projects" placeholder="/path/to/projects" autocomplete="off" /><span class="field-help">Pre-fills the project directory when creating a session.</span></label><label class="setting-row"><span><strong>Attach latest session</strong>Reconnect plain omb launches to the latest live session.</span><span class="switch"><input id="setting-attach" type="checkbox" /><span class="switch-track"></span></span></label><label class="setting-row"><span><strong>Mouse forwarding</strong>Enable scrolling and pointer events in OMP tmux sessions.</span><span class="switch"><input id="setting-mouse" type="checkbox" /><span class="switch-track"></span></span></label><label class="setting-row"><span><strong>Show stopped sessions</strong>Include recent stopped work in the session rail.</span><span class="switch"><input id="setting-stopped" type="checkbox" /><span class="switch-track"></span></span></label><label class="setting-row"><span><strong>System tray</strong>Keep native quick access available when supported.</span><span class="switch"><input id="setting-tray" type="checkbox" /><span class="switch-track"></span></span></label><div class="field-help" id="settings-status" role="status">Changes are stored in your Bizar OMP configuration.</div></div><div class="dialog-foot"><button class="button" value="cancel">Cancel</button><button class="button primary" value="save">Save settings</button></div></form></dialog>
41
+ <div class="toast hidden" id="toast" role="status" aria-live="polite"></div>
264
42
  <script>
265
- (function () {
266
- "use strict";
267
- var TOKEN_KEY = "bizar.dash.token";
268
- var THEME_KEY = "bizar.dash.theme";
269
- var token = sessionStorage.getItem(TOKEN_KEY) || "";
270
- var sessions = [];
271
- var projectsFolder = "";
272
- var filter = "all";
273
- var timer = null;
274
- var pending = null;
275
-
276
- function byId(id) { return document.getElementById(id); }
277
-
278
- function escapeText(value) {
279
- return String(value == null ? "" : value)
280
- .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
281
- .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
282
- }
283
-
284
- function toast(message, isError) {
285
- var node = byId("toast");
286
- node.textContent = message;
287
- node.style.borderColor = isError ? "var(--err)" : "var(--accent)";
288
- node.classList.remove("hidden");
289
- clearTimeout(node._hide);
290
- node._hide = setTimeout(function () { node.classList.add("hidden"); }, 5200);
291
- }
292
-
293
- function notice(message) {
294
- var node = byId("notice");
295
- if (!message) { node.classList.add("hidden"); node.textContent = ""; return; }
296
- node.textContent = message;
297
- node.classList.remove("hidden");
298
- }
299
-
300
- function link(state, text) {
301
- var chip = byId("link-chip");
302
- chip.className = "chip" + (state === "live" ? " live" : state === "down" ? " down" : "");
303
- byId("link-text").textContent = text;
304
- }
305
-
306
- function ageFrom(seconds) {
307
- if (!seconds) return "unknown";
308
- var delta = Math.max(0, Math.floor(Date.now() / 1000) - seconds);
309
- if (delta < 60) return delta + "s ago";
310
- if (delta < 3600) return Math.floor(delta / 60) + "m ago";
311
- if (delta < 86400) return Math.floor(delta / 3600) + "h ago";
312
- return Math.floor(delta / 86400) + "d ago";
313
- }
314
-
315
- function api(path, options) {
316
- var init = options || {};
317
- init.headers = Object.assign({ authorization: "Bearer " + token }, init.headers || {});
318
- if (init.body) { init.headers["content-type"] = "application/json"; }
319
- return fetch(path, init);
320
- }
321
-
322
- // States are words the daemon chooses; the dashboard must not hardcode a
323
- // vocabulary it does not own, so unknown states still render sensibly.
324
- var STATE_TONE = {
325
- working: "ok", waiting: "warn", connected: "ok", attached: "ok",
326
- disconnected: "muted", detached: "warn", stopped: "muted", dead: "muted", error: "err"
327
- };
328
-
329
- function toneOf(state) { return STATE_TONE[state] || "muted"; }
330
-
331
- // Known-busy states sort first; anything unfamiliar keeps a stable last place.
332
- var STATE_RANK = { working: 0, waiting: 1, connected: 2, attached: 2, disconnected: 3, detached: 3, stopped: 4, dead: 4 };
333
- function rankOf(state) { return STATE_RANK[state] === undefined ? 5 : STATE_RANK[state]; }
334
-
335
- function statesSeen() {
336
- var seen = [];
337
- sessions.forEach(function (session) {
338
- if (seen.indexOf(session.state) === -1) seen.push(session.state);
339
- });
340
- return seen;
341
- }
342
-
343
- function titleOf(session) {
344
- return session.nativeName || session.firstMessage || session.name;
345
- }
346
-
347
- /** Secondary line: the tmux identity, minus anything already in the title. */
348
- function metaOf(session) {
349
- var slug = (session.project || "session") + "/" + (session.id || "main");
350
- return session.name === titleOf(session) ? slug : slug + " \\u00b7 " + session.name;
351
- }
352
-
353
- function workspaceOf(session) {
354
- return session.cwd && session.cwd !== "?" ? session.cwd : session.project || "unknown workspace";
355
- }
356
-
357
- function visible() {
358
- if (filter === "all") return sessions;
359
- return sessions.filter(function (session) { return session.state === filter; });
360
- }
361
-
362
- function renderStats() {
363
- var workspaces = {};
364
- sessions.forEach(function (s) { workspaces[workspaceOf(s)] = true; });
365
- var counts = {};
366
- statesSeen().forEach(function (state) { counts[state] = 0; });
367
- sessions.forEach(function (session) { counts[session.state] += 1; });
368
- var cards = [
369
- { label: "Sessions under management", value: String(sessions.length), sub: Object.keys(workspaces).length + " workspace(s)", invert: true }
370
- ];
371
- Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a] || a.localeCompare(b); })
372
- .forEach(function (state) { cards.push({ label: state, value: String(counts[state]), sub: "sessions" }); });
373
- byId("stats").innerHTML = cards.map(function (card) {
374
- return '<article class="card' + (card.invert ? " invert" : "") + '">' +
375
- '<div class="label">' + escapeText(card.label) + "</div>" +
376
- '<div class="value">' + escapeText(card.value) + "</div>" +
377
- '<div class="sub">' + escapeText(card.sub) + "</div></article>";
378
- }).join("");
379
- }
380
-
381
- function renderFilters() {
382
- var keys = ["all"].concat(statesSeen());
383
- if (keys.indexOf(filter) === -1) filter = "all";
384
- var counts = { all: sessions.length };
385
- sessions.forEach(function (session) { counts[session.state] = (counts[session.state] || 0) + 1; });
386
- byId("filters").innerHTML = keys.map(function (key) {
387
- return '<button type="button" class="filter" data-filter="' + escapeText(key) + '" aria-pressed="' +
388
- (filter === key) + '">' + escapeText(key) + " " + counts[key] + "</button>";
389
- }).join("");
390
- }
391
-
392
- function renderSessions() {
393
- var target = byId("workspaces");
394
- var list = visible();
395
- if (!list.length) {
396
- target.innerHTML = '<div class="empty">No sessions match this view. Create one with <b>+ New session</b>.</div>';
397
- return;
398
- }
399
- var groups = {};
400
- var order = [];
401
- list.forEach(function (session) {
402
- var key = workspaceOf(session);
403
- if (!groups[key]) { groups[key] = []; order.push(key); }
404
- groups[key].push(session);
405
- });
406
- order.sort();
407
- target.innerHTML = order.map(function (key) {
408
- var rows = groups[key].slice().sort(function (a, b) {
409
- var left = rankOf(a.state);
410
- var right = rankOf(b.state);
411
- return (left - right) || a.name.localeCompare(b.name);
412
- }).map(function (session) {
413
- var meta = metaOf(session);
414
- return '<article class="session" data-name="' + escapeText(session.name) + '">' +
415
- '<span class="state-dot dot-' + toneOf(session.state) + '" title="' + escapeText(session.state) + '"></span>' +
416
- "<div>" +
417
- '<div class="session-title">' + escapeText(titleOf(session)) + "</div>" +
418
- '<div class="session-meta">' + escapeText(meta) + "</div>" +
419
- "</div>" +
420
- '<div class="session-age">' + escapeText(ageFrom(session.lastAttachedAt || session.createdAt)) + "</div>" +
421
- '<div class="session-actions">' +
422
- '<button type="button" class="btn" data-action="attach">Attach</button>' +
423
- '<button type="button" class="btn" data-action="rename">Rename</button>' +
424
- '<button type="button" class="btn danger" data-action="stop">Stop</button>' +
425
- "</div>" +
426
- "</article>";
427
- }).join("");
428
- return '<section class="workspace">' +
429
- '<div class="workspace-head"><span class="workspace-path"><b>' + escapeText(key) + "</b></span>" +
430
- '<span class="muted" style="font-size:11px">' + groups[key].length + " session(s)</span></div>" +
431
- rows + "</section>";
432
- }).join("");
433
-
434
- var options = order.map(function (key) { return '<option value="' + escapeText(key) + '"></option>'; });
435
- var datalist = byId("workspace-list");
436
- datalist.innerHTML = (projectsFolder ? '<option value="' + escapeText(projectsFolder) + '"></option>' : "") + options.join("");
437
- }
438
-
439
- function render() {
440
- renderStats();
441
- renderFilters();
442
- renderSessions();
443
- }
444
-
445
- function refresh() {
446
- return api("/sessions").then(function (response) {
447
- if (response.status === 401) {
448
- link("down", "LOCKED");
449
- notice("Dashboard access expired. Run \`omb dashboard\` again to get a fresh link.");
450
- return;
451
- }
452
- if (!response.ok) throw new Error("status " + response.status);
453
- return response.json().then(function (body) {
454
- sessions = Array.isArray(body.sessions) ? body.sessions : [];
455
- link("live", "LIVE");
456
- notice("");
457
- byId("updated").textContent = "updated " + new Date().toLocaleTimeString();
458
- render();
459
- });
460
- }).catch(function () {
461
- link("down", "OFFLINE");
462
- notice("Daemon unreachable. Sessions keep running in tmux; retrying automatically.");
463
- });
464
- }
465
-
466
- function start() {
467
- refresh();
468
- if (timer) clearInterval(timer);
469
- timer = setInterval(refresh, 3000);
470
- }
471
-
472
- function post(path, body) {
473
- return api(path, { method: "POST", body: JSON.stringify(body || {}) }).then(function (response) {
474
- return response.json().catch(function () { return {}; }).then(function (payload) {
475
- if (!response.ok) throw new Error(payload.error || ("status " + response.status));
476
- return payload;
477
- });
478
- });
479
- }
480
-
481
- function errorMessage(error) {
482
- return error && error.message ? error.message : String(error);
483
- }
484
-
485
- document.addEventListener("click", function (event) {
486
- var filterButton = event.target.closest("[data-filter]");
487
- if (filterButton) { filter = filterButton.getAttribute("data-filter"); render(); return; }
488
-
489
- var action = event.target.closest("[data-action]");
490
- if (action) {
491
- var row = action.closest("[data-name]");
492
- var name = row.getAttribute("data-name");
493
- var kind = action.getAttribute("data-action");
494
- if (kind === "attach") {
495
- post("/sessions/" + encodeURIComponent(name) + "/attach", {}).then(function (payload) {
496
- if (payload.spawned) toast("Opening " + payload.terminal + " attached to " + name);
497
- else toast("No terminal emulator found. Run: " + payload.command, true);
498
- }).catch(function (error) { toast("Attach failed: " + errorMessage(error), true); });
499
- } else if (kind === "rename") {
500
- pending = name;
501
- byId("rename-id").value = (sessions.find(function (s) { return s.name === name; }) || {}).id || "";
502
- byId("rename-dialog").showModal();
503
- } else if (kind === "stop") {
504
- pending = name;
505
- byId("stop-text").textContent = "Stop " + name + "? This kills the Bizar tmux session and ends its agent work.";
506
- byId("stop-dialog").showModal();
507
- }
508
- return;
509
- }
510
-
511
- if (event.target.id === "theme-toggle") {
512
- var next = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
513
- applyTheme(next, true);
514
- return;
515
- }
516
- if (event.target.id === "create-open") {
517
- byId("create-cwd").value = projectsFolder || "";
518
- byId("create-dialog").showModal();
519
- }
520
- });
521
-
522
- byId("rename-dialog").addEventListener("close", function (event) {
523
- var name = pending;
524
- pending = null;
525
- if (event.target.returnValue !== "rename" || !name) return;
526
- var nextId = byId("rename-id").value.trim();
527
- if (!nextId) return;
528
- post("/sessions/" + encodeURIComponent(name) + "/rename", { sessionId: nextId })
529
- .then(function () { toast("Renamed " + name + " to " + nextId); return refresh(); })
530
- .catch(function (error) { toast("Rename failed: " + errorMessage(error), true); });
531
- });
532
-
533
- byId("stop-dialog").addEventListener("close", function (event) {
534
- var name = pending;
535
- pending = null;
536
- if (event.target.returnValue !== "stop" || !name) return;
537
- post("/sessions/" + encodeURIComponent(name) + "/delete", {})
538
- .then(function () { toast("Stopped " + name); return refresh(); })
539
- .catch(function (error) { toast("Stop failed: " + errorMessage(error), true); });
540
- });
541
-
542
- byId("create-dialog").addEventListener("close", function (event) {
543
- if (event.target.returnValue !== "create") return;
544
- var cwd = byId("create-cwd").value.trim();
545
- if (!cwd) { toast("A project directory is required", true); return; }
546
- var payload = { cwd: cwd, task: byId("create-task").value.trim(), sessionId: byId("create-id").value.trim() };
547
- post("/sessions/create", payload)
548
- .then(function () { toast("Session created in " + cwd); return refresh(); })
549
- .catch(function (error) { toast("Create failed: " + errorMessage(error), true); });
550
- });
551
-
552
- function applyTheme(theme, remember) {
553
- document.documentElement.setAttribute("data-theme", theme);
554
- byId("theme-toggle").textContent = theme === "dark" ? "LIGHT" : "DARK";
555
- if (remember) localStorage.setItem(THEME_KEY, theme);
556
- }
557
-
558
- function bootstrap() {
559
- var stored = localStorage.getItem(THEME_KEY);
560
- var preferred = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
561
- applyTheme(stored || preferred, false);
562
-
563
- var params = new URLSearchParams(window.location.search);
564
- var code = params.get("oc");
565
- var auth = code
566
- ? fetch("/dash/session", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code: code }) })
567
- .then(function (response) {
568
- if (!response.ok) throw new Error("access code rejected");
569
- return response.json();
570
- })
571
- : Promise.resolve({ token: token });
572
-
573
- auth.then(function (payload) {
574
- token = payload.token;
575
- sessionStorage.setItem(TOKEN_KEY, token);
576
- if (code) window.history.replaceState({}, "", "/dash");
577
- return api("/projects").then(function (response) { return response.ok ? response.json() : {}; });
578
- }).then(function (meta) {
579
- projectsFolder = meta && typeof meta.projectsFolder === "string" ? meta.projectsFolder : "";
580
- start();
581
- }).catch(function () {
582
- link("down", "LOCKED");
583
- notice("Open this dashboard from the URL printed by \`omb dashboard\`.");
584
- });
585
- }
586
-
587
- bootstrap();
43
+ (function(){"use strict";
44
+ var token="",sessions=[],selectedName="",filter="all",query="",projectsFolder="",refreshTimer,consoleTimer,consoleSessionName="",consoleBusy=false,consoleText="",followConsole=true,THEME_KEY="bizar.dash.theme",SELECTED_KEY="bizar.dash.selected";
45
+ function byId(id){return document.getElementById(id)}
46
+ function esc(value){return String(value==null?"":value).replace(/[&<>"']/g,function(c){if(c==="&")return"&amp;";if(c==="<")return"&lt;";if(c===">")return"&gt;";if(c.charCodeAt(0)===34)return"&quot;";return"&#39;"})}
47
+ function err(error){return error&&error.message?error.message:"Unexpected error"}
48
+ function toast(message,isError){var n=byId("toast");n.textContent=message;n.classList.toggle("error",Boolean(isError));n.classList.remove("hidden");clearTimeout(n._hide);n._hide=setTimeout(function(){n.classList.add("hidden")},4800)}
49
+ function api(path,options){var next=options||{};next.headers=Object.assign({authorization:"Bearer "+token,"content-type":"application/json"},next.headers||{});return fetch(path,next)}
50
+ function apiJson(path,options){return api(path,options).then(function(response){return response.json().catch(function(){return{}}).then(function(body){if(!response.ok)throw new Error(body.error||("Request failed with "+response.status));return body})})}
51
+ function post(path,body,method){return apiJson(path,{method:method||"POST",body:JSON.stringify(body||{})})}
52
+ function epochMs(value){return!value?0:value<100000000000?value*1000:value}
53
+ function age(value){var d=Math.max(0,Date.now()-epochMs(value));if(!value)return"unknown";if(d<60000)return"now";if(d<3600000)return Math.floor(d/60000)+"m";if(d<86400000)return Math.floor(d/3600000)+"h";return Math.floor(d/86400000)+"d"}
54
+ function date(value){return value?new Date(epochMs(value)).toLocaleString([],{dateStyle:"medium",timeStyle:"short"}):"Unknown"}
55
+ function selected(){return sessions.find(function(s){return s.name===selectedName})}
56
+ function title(s){return s.id||s.project||s.name}
57
+ function available(s){return Boolean(s&&s.state!=="stopped"&&s.hosted!==false)}
58
+ function tone(state){return state==="working"||state==="connected"?"live":state==="waiting"||state==="disconnected"?"warn":""}
59
+ function mobile(view){document.body.setAttribute("data-mobile-view",view);document.querySelectorAll("[data-view]").forEach(function(b){b.classList.toggle("active",b.dataset.view===view)})}
60
+ function visible(){var needle=query.toLowerCase();return sessions.filter(function(s){return(filter==="all"||s.state===filter)&&(!needle||[s.name,s.id,s.project,s.cwd,s.state].join(" ").toLowerCase().includes(needle))})}
61
+ function renderFilters(){var counts={};sessions.forEach(function(s){counts[s.state]=(counts[s.state]||0)+1});var keys=["all"].concat(Object.keys(counts).sort());byId("filters").innerHTML=keys.map(function(key){var count=key==="all"?sessions.length:counts[key];return'<button type="button" class="filter'+(filter===key?' active':'')+'" data-filter="'+esc(key)+'">'+esc(key)+" "+count+"</button>"}).join("")}
62
+ function renderSessions(){var list=visible(),target=byId("session-list");byId("session-count").textContent=String(sessions.length);if(!list.length){target.innerHTML='<div class="empty"><strong>'+(sessions.length?"No matching sessions":"No sessions yet")+'</strong>'+(sessions.length?"Change the search or state filter.":"Create a session to start work from the dashboard.")+"</div>";return}var groups={};list.forEach(function(s){var key=s.cwd||s.project||"Unknown workspace";(groups[key]=groups[key]||[]).push(s)});target.innerHTML=Object.keys(groups).sort().map(function(workspace){return'<div class="workspace-label" title="'+esc(workspace)+'">'+esc(workspace)+"</div>"+groups[workspace].map(function(s){return'<button type="button" class="session-row'+(s.name===selectedName?' selected':'')+'" data-session="'+esc(s.name)+'"><span class="status-dot '+tone(s.state)+'"></span><span><span class="session-name">'+esc(title(s))+'</span><span class="session-meta"><span>'+esc(s.state)+'</span><span>·</span><span>'+esc(age(s.lastAttachedAt||s.createdAt))+"</span></span></span></button>"}).join("")}).join("")}
63
+ function renderInspector(){var s=selected(),target=byId("inspector");if(!s){target.innerHTML='<div class="empty"><strong>Nothing selected</strong>Session identity and controls appear here.</div>';return}var activity=s.activity&&s.activity.label?s.activity.label:"No structured activity";target.innerHTML='<section class="inspector-section"><h2>Session</h2><dl class="detail-list"><div><dt>Status</dt><dd><span class="state-label"><span class="status-dot '+tone(s.state)+'"></span><span>'+esc(s.state)+'</span></span></dd></div><div><dt>Activity</dt><dd>'+esc(activity)+'</dd></div><div><dt>Workspace</dt><dd class="mono">'+esc(s.cwd)+'</dd></div><div><dt>Session id</dt><dd class="mono">'+esc(s.name)+'</dd></div><div><dt>Created</dt><dd>'+esc(date(s.createdAt))+'</dd></div><div><dt>Last attached</dt><dd>'+esc(date(s.lastAttachedAt))+'</dd></div></dl></section><section class="inspector-section"><h2>Actions</h2><div class="action-stack"><button class="button" type="button" data-action="attach"'+(available(s)?'':' disabled')+'>Open in terminal</button><button class="button" type="button" data-action="rename"'+(available(s)?'':' disabled')+'>Rename session</button><button class="button danger" type="button" data-action="stop"'+(available(s)?'':' disabled')+'>Stop session</button></div></section>'}
64
+ function consoleState(head,detail){consoleText="";byId("console").innerHTML='<span class="console-state"><span><strong>'+esc(head)+"</strong>"+esc(detail)+"</span></span>";byId("console-follow").classList.add("hidden")}
65
+ function renderConversation(){var s=selected(),ok=available(s);byId("conversation-title").textContent=s?title(s):"Choose a session";byId("session-state").innerHTML='<span class="status-dot '+(s?tone(s.state):'')+'"></span><span>'+esc(s?s.state:"No session selected")+'</span>';byId("console-refresh").disabled=!ok;byId("attach-top").disabled=!ok;byId("composer-input").disabled=!ok;byId("composer-send").disabled=!ok;byId("composer-status").textContent=ok?"Input is sent directly to the active tmux pane.":s?"This session has no live console.":"Select a live session to begin.";if(!s)consoleState("No session open","Select a session to read its live console and send a message.");else if(!ok)consoleState("Console unavailable","This session is stopped or no longer has a Bizar-owned tmux host.")}
66
+ function render(){renderFilters();renderSessions();renderConversation();renderInspector()}
67
+ function daemonState(body){var n=byId("daemon-state"),dot=n.querySelector(".status-dot");dot.className="status-dot "+(body.stale?"warn":"live");n.querySelector("span:last-child").textContent=body.scanError?"Scan issue: "+body.scanError:body.stale?"Session data is stale":"Local daemon live · updated "+age(body.collectedAt)}
68
+ function refresh(){return apiJson("/sessions").then(function(body){sessions=Array.isArray(body.sessions)?body.sessions:[];if(!selectedName||!sessions.some(function(s){return s.name===selectedName})){var remembered=localStorage.getItem(SELECTED_KEY)||"";selectedName=sessions.some(function(s){return s.name===remembered})?remembered:(sessions[0]?sessions[0].name:"")}daemonState(body);render();if(selectedName)startConsole()}).catch(function(error){var n=byId("daemon-state");n.querySelector(".status-dot").className="status-dot warn";n.querySelector("span:last-child").textContent="Daemon unavailable";toast("Session refresh failed: "+err(error),true)})}
69
+ function loadConsole(force){var s=selected();if(!available(s)||consoleBusy)return Promise.resolve();consoleBusy=true;if(force||!consoleText)consoleState("Loading console","Capturing the active tmux pane.");return apiJson("/sessions/"+encodeURIComponent(s.name)+"/console").then(function(body){var node=byId("console"),wasNear=followConsole||node.scrollHeight-node.scrollTop-node.clientHeight<80,next=typeof body.text==="string"?body.text:"";consoleText=next;if(next)node.textContent=next;else consoleState("Console is quiet","Waiting for output from this session.");if(wasNear&&next){node.scrollTop=node.scrollHeight;followConsole=true;byId("console-follow").classList.add("hidden")}}).catch(function(error){consoleState("Could not read console",err(error))}).finally(function(){consoleBusy=false})}
70
+ function startConsole(){var s=selected();if(!available(s)){clearInterval(consoleTimer);consoleSessionName="";return}if(consoleSessionName===s.name&&consoleTimer)return;clearInterval(consoleTimer);consoleSessionName=s.name;void loadConsole(true);consoleTimer=setInterval(function(){void loadConsole(false)},1800)}
71
+ function choose(name){if(selectedName===name){mobile("chat");return}selectedName=name;localStorage.setItem(SELECTED_KEY,name);consoleText="";render();startConsole();mobile("chat");byId("composer-input").focus()}
72
+ function attach(){var s=selected();if(!s)return;post("/sessions/"+encodeURIComponent(s.name)+"/attach",{}).then(function(body){if(body.spawned)toast("Opened "+body.terminal);else if(body.command){if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(body.command).catch(function(){});toast("No terminal emulator found. Attach command copied when browser access allowed.",true)}}).catch(function(error){toast("Could not open terminal: "+err(error),true)})}
73
+ function openCreate(){byId("create-cwd").value=projectsFolder;byId("create-task").value="";byId("create-id").value="";byId("create-dialog").showModal();requestAnimationFrame(function(){byId("create-cwd").focus()})}
74
+ function openRename(){var s=selected();if(!s)return;byId("rename-id").value=s.id||"";byId("rename-dialog").showModal();requestAnimationFrame(function(){byId("rename-id").select()})}
75
+ function openStop(){var s=selected();if(!s)return;byId("stop-text").textContent="Stop "+title(s)+"? This ends its tmux session and agent work.";byId("stop-dialog").showModal()}
76
+ function loadSettings(){return apiJson("/settings").then(function(body){var s=body.settings;projectsFolder=s.projectsFolder||"";byId("setting-projects").value=projectsFolder;byId("setting-attach").checked=Boolean(s.autoAttachLatest);byId("setting-mouse").checked=Boolean(s.mouseScrolling);byId("setting-stopped").checked=Boolean(s.showStoppedSessions);byId("setting-tray").checked=Boolean(s.trayEnabled);byId("settings-status").textContent="Changes are stored in your Bizar OMP configuration."})}
77
+ function openSettings(){byId("settings-dialog").showModal();byId("settings-status").textContent="Loading settings.";loadSettings().catch(function(error){byId("settings-status").textContent="Could not load settings: "+err(error)})}
78
+ function theme(value,remember){document.documentElement.setAttribute("data-theme",value);byId("theme-toggle").textContent=value==="dark"?"Light theme":"Dark theme";if(remember)localStorage.setItem(THEME_KEY,value)}
79
+ document.addEventListener("click",function(event){var target=event.target.closest("button");if(!target)return;if(target.dataset.session)choose(target.dataset.session);if(target.dataset.filter){filter=target.dataset.filter;renderFilters();renderSessions()}if(target.dataset.view)mobile(target.dataset.view);if(target.dataset.action==="attach")attach();if(target.dataset.action==="rename")openRename();if(target.dataset.action==="stop")openStop()});
80
+ byId("session-search").addEventListener("input",function(event){query=event.target.value.trim();renderSessions()});byId("new-session-top").addEventListener("click",openCreate);byId("settings-open").addEventListener("click",openSettings);byId("attach-top").addEventListener("click",attach);byId("console-refresh").addEventListener("click",function(){void loadConsole(true)});byId("theme-toggle").addEventListener("click",function(){theme(document.documentElement.getAttribute("data-theme")==="dark"?"light":"dark",true)});
81
+ byId("console").addEventListener("scroll",function(event){var n=event.target;followConsole=n.scrollHeight-n.scrollTop-n.clientHeight<80;byId("console-follow").classList.toggle("hidden",followConsole)});byId("console-follow").addEventListener("click",function(){var n=byId("console");n.scrollTop=n.scrollHeight;followConsole=true;byId("console-follow").classList.add("hidden")});byId("composer-input").addEventListener("keydown",function(event){if(event.key==="Enter"&&(event.ctrlKey||event.metaKey)){event.preventDefault();byId("composer").requestSubmit()}});
82
+ byId("composer").addEventListener("submit",function(event){event.preventDefault();var s=selected(),input=byId("composer-input"),message=input.value;if(!s||!message.trim())return;byId("composer-send").disabled=true;byId("composer-status").textContent="Sending to "+title(s)+".";post("/sessions/"+encodeURIComponent(s.name)+"/console",{message:message}).then(function(){input.value="";byId("composer-status").textContent="Sent. Waiting for session output.";followConsole=true;return new Promise(function(resolve){setTimeout(resolve,180)})}).then(function(){return loadConsole(false)}).catch(function(error){byId("composer-status").textContent="Send failed. Your draft is still here.";toast("Message was not sent: "+err(error),true)}).finally(function(){byId("composer-send").disabled=!available(selected())})});
83
+ byId("create-dialog").addEventListener("close",function(event){if(event.target.returnValue!=="create")return;var cwd=byId("create-cwd").value.trim();if(!cwd)return;post("/sessions/create",{cwd:cwd,task:byId("create-task").value.trim(),sessionId:byId("create-id").value.trim()}).then(function(body){selectedName=body.name||"";toast("Session created in "+cwd);return refresh()}).then(function(){if(selectedName)choose(selectedName)}).catch(function(error){toast("Could not create session: "+err(error),true)})});
84
+ byId("rename-dialog").addEventListener("close",function(event){var s=selected();if(event.target.returnValue!=="rename"||!s)return;var next=byId("rename-id").value.trim();if(!next)return;post("/sessions/"+encodeURIComponent(s.name)+"/rename",{sessionId:next}).then(function(){selectedName="";toast("Session renamed to "+next);return refresh()}).catch(function(error){toast("Could not rename session: "+err(error),true)})});
85
+ byId("stop-dialog").addEventListener("close",function(event){var s=selected();if(event.target.returnValue!=="stop"||!s)return;post("/sessions/"+encodeURIComponent(s.name)+"/delete",{}).then(function(){toast("Stopped "+title(s));selectedName="";return refresh()}).catch(function(error){toast("Could not stop session: "+err(error),true)})});
86
+ byId("settings-dialog").addEventListener("close",function(event){if(event.target.returnValue!=="save")return;var payload={projectsFolder:byId("setting-projects").value.trim(),autoAttachLatest:byId("setting-attach").checked,mouseScrolling:byId("setting-mouse").checked,showStoppedSessions:byId("setting-stopped").checked,trayEnabled:byId("setting-tray").checked};post("/settings",payload,"PATCH").then(function(body){projectsFolder=body.settings.projectsFolder||"";toast("Dashboard settings saved");return refresh()}).catch(function(error){toast("Could not save settings: "+err(error),true)})});
87
+ function bootstrap(){var remembered=localStorage.getItem(THEME_KEY);theme(remembered==="light"||remembered==="dark"?remembered:matchMedia("(prefers-color-scheme: light)").matches?"light":"dark",false);var code=new URL(location.href).searchParams.get("code"),auth=code?fetch("/dash/session",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({code:code})}).then(function(response){return response.json().then(function(body){if(!response.ok)throw new Error(body.error||"Access code rejected");return body})}):Promise.reject(new Error("Open the dashboard with omb dashboard --open to obtain a fresh access code."));auth.then(function(body){token=body.token;history.replaceState({},"",location.pathname);return loadSettings()}).then(refresh).then(function(){refreshTimer=setInterval(function(){void refresh()},5000)}).catch(function(error){byId("session-list").innerHTML='<div class="empty"><strong>Dashboard access failed</strong>'+esc(err(error))+"</div>";consoleState("Dashboard locked",err(error));var n=byId("daemon-state");n.querySelector(".status-dot").className="status-dot warn";n.querySelector("span:last-child").textContent="Authentication required"})}
88
+ window.addEventListener("beforeunload",function(){clearInterval(refreshTimer);clearInterval(consoleTimer)});bootstrap();
588
89
  })();
589
90
  </script>
590
91
  </body>
591
- </html>
592
- `;
92
+ </html>`;
593
93
  }
594
94
  //# sourceMappingURL=dashboard-html.js.map