@omg-dev/admin 0.4.26 → 0.4.28

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.
@@ -0,0 +1,11 @@
1
+ /** URLs in the admin console can originate from reports or user-controlled
2
+ * Computers. Only ordinary web links may become clickable operator actions. */
3
+ export function safeHref(value: unknown): string | undefined {
4
+ if (typeof value !== "string") return undefined
5
+ try {
6
+ const url = new URL(value)
7
+ return url.protocol === "http:" || url.protocol === "https:" ? value : undefined
8
+ } catch {
9
+ return undefined
10
+ }
11
+ }
package/src/server.ts CHANGED
@@ -30,7 +30,7 @@ export interface OmgAdminProxyOptions {
30
30
  // The proxy only forwards the admin surface — never arbitrary control-plane
31
31
  // paths — so a leaked mount can't wield the powerful admin token against other
32
32
  // endpoints. Keep in sync with the console's AdminClient modules.
33
- const ALLOWED = ["/api/flags/", "/api/adminBilling/", "/api/bugReports/", "/api/users/"];
33
+ const ALLOWED = ["/api/flags/", "/api/adminBilling/", "/api/bugReports/", "/api/users/", "/api/imessageDebug/"];
34
34
 
35
35
  function json(body: unknown, status: number): Response {
36
36
  return new Response(JSON.stringify(body), {
package/src/styles.ts CHANGED
@@ -29,6 +29,7 @@ const CSS = `
29
29
  --vadm-accent-fg: var(--primary-foreground, #ffffff);
30
30
  --vadm-card: var(--card, #fafafa);
31
31
  --vadm-danger: #dc2626;
32
+ --vadm-warn: color-mix(in srgb, #f59e0b 72%, var(--vadm-fg));
32
33
  color: var(--vadm-fg);
33
34
  font-family: inherit;
34
35
  font-size: 14px;
@@ -169,6 +170,240 @@ button.vadm-user-row { cursor: pointer; }
169
170
  animation: vadmPulse 1.4s ease-in-out infinite;
170
171
  }
171
172
  .vadm-skel-avatar { width: 38px; height: 38px; border-radius: 50%; flex: none; }
173
+ /* ── routing flow graph ───────────────────────────────────────────────────
174
+ Port of the docs site's <Flow> (apps/docs/app/components/flow.tsx) onto
175
+ --vadm-* tokens: hairline connector, 12px SVG chevron, pill edge label,
176
+ rounded card node with muted sub + accent ring. Horizontal by default so a
177
+ 100-trace log stays scannable; the 760px breakpoint below collapses it to
178
+ the docs' vertical trunk. No per-trace canvas — the row border is the
179
+ canvas — and rows are left-aligned so decision nodes column-align. */
180
+ .vadm-brain-trace { position: relative; background: var(--vadm-bg); padding: 13px 16px 12px; }
181
+ .vadm-brain-trace[data-failed="true"] { border-color: color-mix(in srgb, var(--vadm-danger) 55%, var(--vadm-border)); }
182
+ .vadm-flow-time {
183
+ position: absolute; top: 12px; right: 14px;
184
+ color: var(--vadm-muted); font-size: 11px; white-space: nowrap;
185
+ }
186
+ .vadm-flow { display: flex; align-items: center; justify-content: flex-start; min-width: 0; padding-right: 76px; }
187
+
188
+ .vadm-flow-node {
189
+ flex: 0 1 auto; min-width: 118px; max-width: 260px; min-height: 0;
190
+ display: flex; flex-direction: column; align-items: center; text-align: center;
191
+ border: 1px solid var(--vadm-border); border-radius: 12px;
192
+ background: var(--vadm-card); padding: 7px 12px;
193
+ text-decoration: none; color: inherit;
194
+ }
195
+ .vadm-flow-node--bare {
196
+ flex: 0 1 260px; min-width: 0; align-items: flex-start; text-align: left;
197
+ border-color: transparent; background: none; padding: 7px 10px 7px 0;
198
+ }
199
+ .vadm-flow-node--accent {
200
+ border-color: var(--vadm-accent);
201
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--vadm-accent) 25%, transparent);
202
+ }
203
+ .vadm-flow-node--accent .vadm-flow-label { color: var(--vadm-accent); }
204
+ /* Amber is mixed toward the host's foreground so it stays legible whether the
205
+ host theme is light or dark (the panel can be dark while the OS is light). */
206
+ .vadm-flow-node--changed {
207
+ border-color: color-mix(in srgb, var(--vadm-warn) 60%, var(--vadm-border));
208
+ background: color-mix(in srgb, #f59e0b 9%, var(--vadm-card));
209
+ }
210
+ .vadm-flow-node--changed .vadm-flow-sub { color: var(--vadm-warn); }
211
+ .vadm-flow-node--danger {
212
+ border-color: var(--vadm-danger);
213
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--vadm-danger) 22%, transparent);
214
+ }
215
+ .vadm-flow-node--danger .vadm-flow-label { color: var(--vadm-danger); }
216
+ a.vadm-flow-node:hover { border-color: var(--vadm-accent); }
217
+
218
+ .vadm-flow-label {
219
+ display: block; max-width: 100%; font-size: 13px; font-weight: 500; line-height: 1.35;
220
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
221
+ }
222
+ .vadm-flow-label[data-mono="true"] { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
223
+ .vadm-flow-pre { color: var(--vadm-muted); font-weight: 400; }
224
+ .vadm-flow-out { color: var(--vadm-muted); font-size: 10px; margin-left: 4px; }
225
+ .vadm-flow-quote::before { content: "\\201C"; }
226
+ .vadm-flow-quote::after { content: "\\201D"; }
227
+ .vadm-flow-sub {
228
+ display: block; max-width: 100%; margin-top: 2px;
229
+ font-size: 11px; line-height: 1.35; color: var(--vadm-muted);
230
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
231
+ }
232
+
233
+ .vadm-flow-edge {
234
+ position: relative; flex: none; align-self: center;
235
+ display: flex; align-items: center; justify-content: flex-end;
236
+ width: 68px; height: 12px; color: var(--vadm-muted);
237
+ }
238
+ .vadm-flow-edge::before {
239
+ content: ""; position: absolute; left: 0; right: 0; top: 50%;
240
+ border-top: 1px solid var(--vadm-border);
241
+ }
242
+ .vadm-flow-edge[data-danger="true"] { color: var(--vadm-danger); }
243
+ .vadm-flow-edge[data-danger="true"]::before { border-top-style: dashed; border-top-color: var(--vadm-danger); }
244
+ .vadm-flow-edge[data-running="true"]::before { animation: vadmPulse 1.4s ease-in-out infinite; }
245
+ .vadm-flow-arrow { position: relative; flex: none; margin-right: -2px; transform: rotate(-90deg); }
246
+ .vadm-flow-pill {
247
+ position: absolute; left: 50%; bottom: calc(50% + 5px); transform: translateX(-50%);
248
+ white-space: nowrap; font-size: 10px; font-weight: 500; line-height: 16px;
249
+ padding: 0 6px; border-radius: 999px;
250
+ border: 1px solid var(--vadm-border); background: var(--vadm-card); color: var(--vadm-muted);
251
+ }
252
+
253
+ /* ── message-list-first view ───────────────────────────────────────────────
254
+ The Chat Brain panel leads with a scannable list of inbound iMessages;
255
+ tapping a row unveils the routing flow graph (TraceRouting) inline. */
256
+ .vadm-filter-pill {
257
+ display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
258
+ padding: 6px 6px 6px 12px; border: 1px solid var(--vadm-border); border-radius: 999px;
259
+ background: color-mix(in srgb, var(--vadm-accent) 7%, var(--vadm-bg)); font-size: 13px;
260
+ width: fit-content; max-width: 100%;
261
+ }
262
+ .vadm-filter-pill strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
263
+ .vadm-userchips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 12px; }
264
+ .vadm-userchip {
265
+ appearance: none; cursor: pointer; font: inherit; font-size: 12px;
266
+ display: inline-flex; align-items: center; gap: 6px;
267
+ border: 1px solid var(--vadm-border); border-radius: 999px;
268
+ background: var(--vadm-bg); color: var(--vadm-fg); padding: 3px 6px 3px 10px;
269
+ }
270
+ .vadm-userchip:hover { border-color: var(--vadm-accent); }
271
+ .vadm-userchip-count {
272
+ min-width: 18px; padding: 0 5px; border-radius: 999px; text-align: center;
273
+ font-size: 11px; font-weight: 600; color: var(--vadm-muted);
274
+ background: color-mix(in srgb, var(--vadm-muted) 15%, transparent);
275
+ }
276
+
277
+ /* Conversation (user view): a self-contained, scrollable chat pane. Oldest at
278
+ the top, newest at the bottom; "Load older" prepends above. iMessage-style
279
+ bubbles — their inbound messages (left) + our replies (right). A bubble with
280
+ a routing trace is tappable and reveals TraceRouting full-width below it.
281
+ Every color reads from the shared --vadm-* design tokens so the pane stays
282
+ consistent with the rest of the admin console in both light and dark. */
283
+ .vadm-conv {
284
+ display: flex; flex-direction: column; gap: 4px; min-width: 0;
285
+ max-height: min(64vh, 640px); overflow-y: auto; overscroll-behavior: contain;
286
+ padding: 12px; border: 1px solid var(--vadm-border); border-radius: 12px;
287
+ background: color-mix(in srgb, var(--vadm-muted) 5%, var(--vadm-bg));
288
+ }
289
+ .vadm-conv-more {
290
+ position: sticky; top: -12px; z-index: 1;
291
+ display: flex; justify-content: center; padding: 4px 0 8px;
292
+ margin: -12px -12px 4px; /* bleed to pane edges so the blur covers full width */
293
+ background: linear-gradient(
294
+ to bottom,
295
+ color-mix(in srgb, var(--vadm-muted) 5%, var(--vadm-bg)) 60%,
296
+ transparent
297
+ );
298
+ }
299
+ .vadm-bubble-row { display: flex; flex-direction: column; min-width: 0; }
300
+ .vadm-bubble-row--in { align-items: flex-start; }
301
+ .vadm-bubble-row--out { align-items: flex-end; }
302
+ .vadm-bubble {
303
+ appearance: none; font: inherit; text-align: left; cursor: default;
304
+ max-width: 82%; min-width: 0; border-radius: 16px; padding: 8px 12px;
305
+ border: 1px solid transparent; display: flex; flex-direction: column; gap: 3px;
306
+ }
307
+ .vadm-bubble[data-dir="in"] {
308
+ background: var(--vadm-card); border-color: var(--vadm-border);
309
+ color: var(--vadm-fg); border-bottom-left-radius: 5px;
310
+ }
311
+ .vadm-bubble[data-dir="out"] {
312
+ background: var(--vadm-accent); color: var(--vadm-accent-fg);
313
+ border-bottom-right-radius: 5px;
314
+ }
315
+ .vadm-bubble[data-has-trace] { cursor: pointer; }
316
+ .vadm-bubble[data-has-trace]:hover { filter: brightness(0.97); }
317
+ .vadm-bubble[data-open] {
318
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--vadm-accent) 60%, transparent);
319
+ }
320
+ .vadm-bubble-text { font-size: 14px; line-height: 1.35; white-space: pre-wrap; overflow-wrap: anywhere; }
321
+ .vadm-bubble-media { display: flex; flex-wrap: wrap; gap: 6px; }
322
+ .vadm-bubble-att {
323
+ display: block; max-width: 220px; max-height: 260px; width: auto; height: auto;
324
+ border-radius: 10px; object-fit: cover; background: rgba(0,0,0,0.2);
325
+ }
326
+ .vadm-bubble-meta {
327
+ display: flex; align-items: center; gap: 5px; font-size: 11px;
328
+ color: var(--vadm-muted);
329
+ }
330
+ .vadm-bubble[data-dir="out"] .vadm-bubble-meta {
331
+ color: color-mix(in srgb, var(--vadm-accent-fg) 80%, var(--vadm-accent));
332
+ }
333
+ .vadm-bubble-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--vadm-muted); }
334
+ .vadm-bubble-dot[data-outcome="completed"] { background: color-mix(in srgb, #16a34a 78%, var(--vadm-fg)); }
335
+ .vadm-bubble-dot[data-outcome="failed"] { background: var(--vadm-danger); }
336
+ .vadm-bubble-dot[data-outcome="running"] {
337
+ background: var(--vadm-accent-fg); animation: vadmPulse 1.4s ease-in-out infinite;
338
+ }
339
+ .vadm-bubble-detail {
340
+ align-self: stretch; margin: 4px 0 8px;
341
+ border: 1px solid color-mix(in srgb, var(--vadm-accent) 40%, var(--vadm-border));
342
+ border-radius: 10px; background: var(--vadm-bg); padding: 12px 14px;
343
+ }
344
+
345
+ .vadm-msg-list { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
346
+ .vadm-msg {
347
+ border: 1px solid var(--vadm-border); border-radius: 10px;
348
+ background: var(--vadm-bg); overflow: hidden;
349
+ }
350
+ .vadm-msg[data-open="true"] {
351
+ border-color: color-mix(in srgb, var(--vadm-accent) 45%, var(--vadm-border));
352
+ background: var(--vadm-card);
353
+ }
354
+ .vadm-msg[data-failed="true"] { border-color: color-mix(in srgb, var(--vadm-danger) 45%, var(--vadm-border)); }
355
+ .vadm-msg-row {
356
+ appearance: none; width: 100%; border: none; background: none; cursor: pointer;
357
+ color: inherit; font: inherit; text-align: left;
358
+ display: flex; align-items: center; gap: 11px; padding: 11px 13px; min-width: 0;
359
+ }
360
+ .vadm-msg-row:hover { background: color-mix(in srgb, var(--vadm-muted) 6%, transparent); }
361
+ .vadm-msg-dot {
362
+ flex: none; width: 9px; height: 9px; border-radius: 50%;
363
+ background: var(--vadm-muted);
364
+ }
365
+ .vadm-msg-dot[data-outcome="completed"] { background: color-mix(in srgb, #16a34a 78%, var(--vadm-fg)); }
366
+ .vadm-msg-dot[data-outcome="failed"] { background: var(--vadm-danger); }
367
+ .vadm-msg-dot[data-outcome="running"] {
368
+ background: var(--vadm-accent); animation: vadmPulse 1.4s ease-in-out infinite;
369
+ }
370
+ .vadm-msg-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
371
+ .vadm-msg-text {
372
+ font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
373
+ }
374
+ .vadm-msg-meta {
375
+ display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
376
+ color: var(--vadm-muted); font-size: 12px; min-width: 0;
377
+ }
378
+ .vadm-msg-decision {
379
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;
380
+ color: var(--vadm-fg);
381
+ }
382
+ .vadm-msg-decision[data-variant="accent"] { color: var(--vadm-accent); }
383
+ .vadm-msg-decision[data-variant="danger"] { color: var(--vadm-danger); }
384
+ .vadm-msg-sender { cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px; }
385
+ .vadm-msg-sender:hover { color: var(--vadm-fg); }
386
+ .vadm-msg-chevron {
387
+ flex: none; width: 8px; height: 8px; margin-right: 3px;
388
+ border-right: 1.5px solid var(--vadm-muted); border-bottom: 1.5px solid var(--vadm-muted);
389
+ transform: rotate(-45deg); transition: transform 0.15s;
390
+ }
391
+ .vadm-msg[data-open="true"] .vadm-msg-chevron { transform: rotate(45deg); }
392
+ .vadm-msg-detail { padding: 4px 14px 14px; border-top: 1px dashed var(--vadm-border); }
393
+
394
+ .vadm-pager { display: flex; align-items: center; justify-content: center; gap: 14px; margin-top: 14px; }
395
+
396
+ .vadm-flow-error { margin: 10px 0 0; font-size: 12px; }
397
+ .vadm-flow-more { margin-top: 2px; }
398
+ .vadm-flow-more > summary {
399
+ list-style: none; cursor: pointer; text-align: right;
400
+ color: var(--vadm-muted); font-size: 12px; line-height: 1.2;
401
+ }
402
+ .vadm-flow-more > summary::-webkit-details-marker { display: none; }
403
+ .vadm-flow-more > summary::after { content: "\\2026"; letter-spacing: 1px; }
404
+ .vadm-flow-more[open] > summary::after { content: "\\2212"; }
405
+ .vadm-flow-more > summary:hover { color: var(--vadm-fg); }
406
+ .vadm-flow-more .vadm-kv { margin-top: 8px; padding-top: 10px; border-top: 1px dashed var(--vadm-border); }
172
407
  @keyframes vadmPulse {
173
408
  0%, 100% { opacity: 0.55; }
174
409
  50% { opacity: 1; }
@@ -187,6 +422,23 @@ button.vadm-user-row { cursor: pointer; }
187
422
  .vadm-user-stats { flex-direction: row; align-items: center; gap: 8px; width: 100%; padding-left: 48px; margin-top: -4px; }
188
423
  .vadm-table { display: block; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
189
424
  .vadm-detail.vadm-card { width: 100% !important; max-width: 100%; }
425
+ /* Below the fold the graph becomes the docs' vertical trunk verbatim:
426
+ full-width nodes stacked, hairline + chevron between, pill beside it. */
427
+ .vadm-flow { flex-direction: column; align-items: stretch; padding-right: 0; }
428
+ .vadm-flow-node { flex: none; width: 100%; max-width: none; align-items: center; text-align: center; }
429
+ .vadm-flow-node--bare { align-items: center; text-align: center; padding: 4px 0 6px; }
430
+ .vadm-flow-edge {
431
+ flex-direction: column; justify-content: flex-end; align-self: stretch;
432
+ width: 100%; height: 26px;
433
+ }
434
+ .vadm-flow-edge::before {
435
+ left: 50%; right: auto; top: 0; bottom: 0;
436
+ border-top: 0; border-left: 1px solid var(--vadm-border);
437
+ }
438
+ .vadm-flow-edge[data-danger="true"]::before { border-left-style: dashed; border-left-color: var(--vadm-danger); }
439
+ .vadm-flow-arrow { transform: none; margin: 0 0 -2px; }
440
+ .vadm-flow-pill { left: calc(50% + 8px); bottom: auto; top: 50%; transform: translateY(-50%); }
441
+ .vadm-flow-time { position: static; display: block; text-align: right; margin-bottom: 4px; }
190
442
  }
191
443
 
192
444
  @media (max-width: 480px) {