@jefuriiij/synthra 0.1.1 → 0.1.2

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.
@@ -343,28 +343,39 @@ var public_default = `<!doctype html>
343
343
 
344
344
  <main>
345
345
  <section>
346
- <h2>Global totals <span class="muted">(all projects)</span></h2>
346
+ <h2>
347
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 14l4-4 4 4 5-5"/></svg>
348
+ Global totals
349
+ <span class="muted">(all projects)</span>
350
+ </h2>
347
351
  <div class="cards" id="cards"></div>
348
352
  </section>
349
353
 
350
354
  <section>
351
- <h2>Projects</h2>
355
+ <h2>
356
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
357
+ Projects
358
+ </h2>
352
359
  <div class="projects" id="projects"></div>
353
360
  <p class="empty hidden" id="projects-empty">No projects registered yet. Run <code>syn .</code> in any project to add it.</p>
354
361
  </section>
355
362
 
356
363
  <section>
357
- <h2>Recent calls <span class="muted">(across all projects)</span></h2>
364
+ <h2>
365
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
366
+ Recent calls
367
+ <span class="muted">(across all projects)</span>
368
+ </h2>
358
369
  <table id="turns">
359
370
  <thead>
360
371
  <tr>
361
372
  <th>Time</th>
362
373
  <th>Project</th>
363
374
  <th>Model</th>
364
- <th class="num">Input</th>
365
- <th class="num">Output</th>
366
- <th class="num">Cache R / W</th>
367
- <th class="num">Cost</th>
375
+ <th class="num"><span class="has-tooltip" data-tooltip="New (uncached) tokens you sent to Claude this turn. Usually small \u2014 most of the conversation comes from cache.">Input <span class="help-icon">i</span></span></th>
376
+ <th class="num"><span class="has-tooltip" data-tooltip="Tokens Claude generated in its response. The most expensive line item \u2014 ~5\xD7 the input rate on Opus.">Output <span class="help-icon">i</span></span></th>
377
+ <th class="num"><span class="has-tooltip" data-tooltip="Cache read / cache write. Reads (~10% of input rate) reuse prior context; writes (~125% of input rate) save new context for future turns.">Cache R / W <span class="help-icon">i</span></span></th>
378
+ <th class="num"><span class="has-tooltip" data-tooltip="Approximate USD cost for this turn \u2014 input \xD7 rate + output \xD7 5\xD7rate + cache_read \xD7 0.1\xD7rate + cache_write \xD7 1.25\xD7rate, using the turn's model.">Cost <span class="help-icon">i</span></span></th>
368
379
  </tr>
369
380
  </thead>
370
381
  <tbody></tbody>
@@ -373,14 +384,17 @@ var public_default = `<!doctype html>
373
384
  </section>
374
385
 
375
386
  <section>
376
- <h2>Recent gate decisions</h2>
387
+ <h2>
388
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
389
+ Recent gate decisions
390
+ </h2>
377
391
  <table id="gates">
378
392
  <thead>
379
393
  <tr>
380
394
  <th>Time</th>
381
395
  <th>Project</th>
382
396
  <th>Tool</th>
383
- <th>Decision</th>
397
+ <th><span class="has-tooltip" data-tooltip="ALLOW = Synthra let the tool call through. BLOCK = Synthra intercepted it because the graph already had high-confidence context \u2014 Claude should use graph_continue instead.">Decision <span class="help-icon">i</span></span></th>
384
398
  <th>Query</th>
385
399
  </tr>
386
400
  </thead>
@@ -391,11 +405,42 @@ var public_default = `<!doctype html>
391
405
  </main>
392
406
 
393
407
  <footer>
394
- <span>Token Counter MCP \xB7 live polling every 2s</span>
395
- <span class="muted">Cost figures are approximate \u2014 see /docs/PROTOCOL.md</span>
408
+ <span>Synthra Token Dashboard \xB7 live polling every 2s</span>
409
+ <span class="muted">Cost figures are approximate \u2014 based on published Anthropic rates.</span>
396
410
  </footer>
397
411
 
398
412
  <script>
413
+ // Inline SVG icons. Each is a stroke-based icon, currentColor for the
414
+ // stroke, designed to inherit color from the parent's CSS.
415
+ const ICONS = {
416
+ dollar: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>',
417
+ chat: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
418
+ arrowDown: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>',
419
+ arrowUp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>',
420
+ refresh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>',
421
+ save: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>',
422
+ folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
423
+ shield: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
424
+ trending: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>',
425
+ ban: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>',
426
+ };
427
+
428
+ // Classify a model name into a family for color-coding.
429
+ function modelFamily(model) {
430
+ if (!model) return 'unknown';
431
+ const m = model.toLowerCase();
432
+ if (m === '<synthetic>') return 'unknown';
433
+ if (m.includes('opus')) return 'opus';
434
+ if (m.includes('sonnet')) return 'sonnet';
435
+ if (m.includes('haiku')) return 'haiku';
436
+ return 'unknown';
437
+ }
438
+
439
+ function modelLabel(model) {
440
+ if (!model || model === '<synthetic>') return model === '<synthetic>' ? 'synthetic' : 'unknown';
441
+ return model;
442
+ }
443
+
399
444
  const $ = (sel) => document.querySelector(sel);
400
445
  const cardsEl = $("#cards");
401
446
  const projectsEl = $("#projects");
@@ -438,23 +483,84 @@ var public_default = `<!doctype html>
438
483
  }
439
484
  }
440
485
 
486
+ // Definitions for the global-totals cards. Each: label, value-source key,
487
+ // icon, optional class (accent | money), tooltip text.
488
+ function cardConfigs(g) {
489
+ return [
490
+ {
491
+ label: "Total cost",
492
+ value: fmtCost(g.estimated_cost_usd),
493
+ icon: ICONS.dollar,
494
+ cls: "money",
495
+ tooltip: "Approximate USD cost across all projects. Computed from per-model pricing (Opus, Sonnet, Haiku) applied to each turn's input/output/cache. Tilde everywhere \u2014 real cost depends on Anthropic's current rates.",
496
+ },
497
+ {
498
+ label: "Turns",
499
+ value: fmt(g.total_turns),
500
+ icon: ICONS.chat,
501
+ tooltip: "Total number of back-and-forth exchanges with Claude across all projects. One turn = you send a message, Claude responds.",
502
+ },
503
+ {
504
+ label: "Input",
505
+ value: fmt(g.total_input_tokens),
506
+ icon: ICONS.arrowDown,
507
+ tooltip: "New (uncached) tokens sent to Claude across all turns. Usually small \u2014 most of the conversation comes from cache.",
508
+ },
509
+ {
510
+ label: "Output",
511
+ value: fmt(g.total_output_tokens),
512
+ icon: ICONS.arrowUp,
513
+ tooltip: "Tokens Claude generated in responses. The most expensive line item per turn (~5\xD7 input rate on Opus).",
514
+ },
515
+ {
516
+ label: "Cache read",
517
+ value: fmt(g.total_cache_read),
518
+ icon: ICONS.refresh,
519
+ tooltip: "Tokens read from Claude's prompt cache \u2014 conversation history, system prompt, Synthra's pack. Cheap: ~10% of the input rate. The bulk of every long session.",
520
+ },
521
+ {
522
+ label: "Cache write",
523
+ value: fmt(g.total_cache_create),
524
+ icon: ICONS.save,
525
+ tooltip: "Tokens newly added to the prompt cache so future turns can read them cheaply. Premium-priced (~125% of input) but pays back over the rest of the session.",
526
+ },
527
+ {
528
+ label: "Projects",
529
+ value: fmt(g.project_count),
530
+ icon: ICONS.folder,
531
+ tooltip: "Projects that have ever run \`syn .\` on this machine. Tracked in ~/.synthra/projects.json.",
532
+ },
533
+ {
534
+ label: "Blocked Grep / Glob",
535
+ value: fmt(g.blocked_count),
536
+ icon: ICONS.shield,
537
+ cls: "accent",
538
+ tooltip: "PreToolUse hook intercepts: Synthra blocked these Grep/Glob calls because the graph already had high-confidence context for the query. Claude pivots to graph_continue or graph_read instead.",
539
+ },
540
+ {
541
+ label: "Tokens saved",
542
+ value: fmt(g.estimated_tokens_saved),
543
+ icon: ICONS.trending,
544
+ cls: "accent",
545
+ tooltip: "Estimated tokens avoided by blocking exploratory Grep/Glob calls. Calculated as blocks \xD7 500 \u2014 conservative; under-counts the cache thrash you also avoid.",
546
+ },
547
+ ];
548
+ }
549
+
441
550
  function renderCards(g) {
442
551
  cardsEl.innerHTML = "";
443
- const cards = [
444
- { label: "Total cost", value: fmtCost(g.estimated_cost_usd), accent: true },
445
- { label: "Turns", value: fmt(g.total_turns) },
446
- { label: "Input", value: fmt(g.total_input_tokens) },
447
- { label: "Output", value: fmt(g.total_output_tokens) },
448
- { label: "Cache read", value: fmt(g.total_cache_read) },
449
- { label: "Cache write", value: fmt(g.total_cache_create) },
450
- { label: "Projects", value: fmt(g.project_count) },
451
- { label: "Blocked Grep / Glob", value: fmt(g.blocked_count), accent: true },
452
- { label: "Tokens saved", value: fmt(g.estimated_tokens_saved), accent: true },
453
- ];
454
- for (const c of cards) {
552
+ for (const c of cardConfigs(g)) {
455
553
  const el = document.createElement("div");
456
- el.className = "card" + (c.accent ? " accent" : "");
457
- el.innerHTML = '<div class="card-label">' + c.label + '</div><div class="card-value">' + c.value + '</div>';
554
+ el.className = "card" + (c.cls ? " " + c.cls : "");
555
+ el.innerHTML =
556
+ '<div class="card-head">' +
557
+ '<div class="card-label has-tooltip" data-tooltip="' + c.tooltip.replace(/"/g, '&quot;') + '">' +
558
+ c.label +
559
+ ' <span class="help-icon">i</span>' +
560
+ '</div>' +
561
+ '<span class="card-icon">' + c.icon + '</span>' +
562
+ '</div>' +
563
+ '<div class="card-value">' + c.value + '</div>';
458
564
  cardsEl.appendChild(el);
459
565
  }
460
566
  }
@@ -475,7 +581,7 @@ var public_default = `<!doctype html>
475
581
  row.className = "project-row";
476
582
  row.innerHTML =
477
583
  '<div class="project-name">' +
478
- '<strong>' + p.name + '</strong>' +
584
+ '<strong>' + ICONS.folder + p.name + '</strong>' +
479
585
  '<code class="project-path">' + p.path + '</code>' +
480
586
  '</div>' +
481
587
  '<div class="project-stats">' +
@@ -497,15 +603,12 @@ var public_default = `<!doctype html>
497
603
  }
498
604
  turnsEmpty.classList.add("hidden");
499
605
  for (const t of turns) {
606
+ const family = modelFamily(t.model);
500
607
  const tr = document.createElement("tr");
501
- const modelCell =
502
- t.model && t.model !== "<synthetic>"
503
- ? "<code>" + t.model + "</code>"
504
- : '<span class="muted">' + (t.model === "<synthetic>" ? "synthetic" : "unknown") + "</span>";
505
608
  tr.innerHTML =
506
609
  "<td>" + fmtTs(t.ts) + "</td>" +
507
610
  "<td><code>" + t.project_name + "</code></td>" +
508
- "<td>" + modelCell + "</td>" +
611
+ '<td><span class="model-pill ' + family + '">' + modelLabel(t.model) + "</span></td>" +
509
612
  '<td class="num">' + fmtFull(t.input) + "</td>" +
510
613
  '<td class="num">' + fmtFull(t.output) + "</td>" +
511
614
  '<td class="num">' + fmt(t.cache_read) + " / " + fmt(t.cache_create) + "</td>" +
@@ -523,12 +626,16 @@ var public_default = `<!doctype html>
523
626
  gatesEmpty.classList.add("hidden");
524
627
  for (const g of gates) {
525
628
  const tr = document.createElement("tr");
526
- const cls = g.decision === "block" ? "decision-block" : "decision-allow";
629
+ const isBlock = g.decision === "block";
630
+ const cls = isBlock ? "decision-block" : "decision-allow";
631
+ const label = isBlock
632
+ ? '<span class="' + cls + '">' + ICONS.ban + ' BLOCK</span>'
633
+ : '<span class="' + cls + '">ALLOW</span>';
527
634
  tr.innerHTML =
528
635
  "<td>" + fmtTs(g.ts) + "</td>" +
529
636
  "<td><code>" + g.project_name + "</code></td>" +
530
637
  "<td><code>" + g.tool + "</code></td>" +
531
- '<td class="' + cls + '">' + g.decision + "</td>" +
638
+ "<td>" + label + "</td>" +
532
639
  "<td><code>" + (g.query || "") + "</code></td>";
533
640
  gatesBody.appendChild(tr);
534
641
  }
@@ -562,7 +669,628 @@ var public_default = `<!doctype html>
562
669
  `;
563
670
 
564
671
  // src/dashboard/public/style.css
565
- var style_default = '/* Synthra token dashboard \u2014 palette per project brief. */\n\n:root {\n --color-heading: #ECEBD8;\n --color-body: #EDECD9;\n --color-bg: #000000;\n --color-surface: #140009;\n --color-surface-raised: #1E000D;\n --color-border: #4D0020;\n --color-accent: #FF0073;\n --color-accent-darker: #EB006A;\n --color-form-bg: #2E0014;\n --color-muted: rgba(237, 236, 217, 0.55);\n --color-very-muted: rgba(237, 236, 217, 0.35);\n --color-block: #FF0073;\n --color-allow: #ECEBD8;\n}\n\n* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\nhtml, body {\n background: var(--color-bg);\n color: var(--color-body);\n font-family:\n ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",\n system-ui, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n min-height: 100vh;\n}\n\ncode, .num, table, .project-path {\n font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;\n}\n\nheader {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n padding: 1.1rem 2rem;\n background: var(--color-surface);\n border-bottom: 1px solid var(--color-border);\n}\n\nheader .brand {\n display: flex;\n align-items: baseline;\n gap: 0.75rem;\n}\n\nheader h1 {\n color: var(--color-heading);\n font-size: 1.35rem;\n font-weight: 700;\n letter-spacing: 0.02em;\n}\n\nheader .tag {\n color: var(--color-muted);\n font-size: 0.78rem;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n}\n\nheader .meta {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 0.78rem;\n font-family: ui-monospace, monospace;\n color: var(--color-muted);\n}\n\nheader .active-project {\n max-width: 480px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\nmain {\n padding: 2rem;\n display: flex;\n flex-direction: column;\n gap: 2rem;\n max-width: 1400px;\n margin: 0 auto;\n}\n\nsection {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\nh2 {\n color: var(--color-heading);\n font-size: 0.85rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\nh2 .muted {\n color: var(--color-very-muted);\n font-size: 0.78rem;\n font-weight: 400;\n letter-spacing: 0.06em;\n text-transform: none;\n margin-left: 0.5rem;\n}\n\n/* Cards row */\n.cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\n gap: 0.7rem;\n}\n\n.card {\n background: var(--color-surface);\n border: 1px solid var(--color-border);\n border-radius: 6px;\n padding: 0.85rem 1rem;\n}\n\n.card.accent {\n background: var(--color-surface-raised);\n border-color: var(--color-accent);\n}\n\n.card-label {\n color: var(--color-muted);\n font-size: 0.66rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n margin-bottom: 0.35rem;\n}\n\n.card-value {\n color: var(--color-heading);\n font-family: ui-monospace, monospace;\n font-size: 1.5rem;\n font-weight: 600;\n}\n\n.card.accent .card-value {\n color: var(--color-accent);\n}\n\n/* Projects list */\n.projects {\n display: flex;\n flex-direction: column;\n gap: 0.6rem;\n}\n\n.project-row {\n display: grid;\n grid-template-columns: minmax(220px, 1fr) auto;\n grid-template-rows: auto auto;\n gap: 0.6rem 1.25rem;\n align-items: center;\n background: var(--color-surface);\n border: 1px solid var(--color-border);\n border-radius: 6px;\n padding: 0.9rem 1.2rem;\n}\n\n.project-row:hover {\n background: var(--color-surface-raised);\n}\n\n.project-name {\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n overflow: hidden;\n}\n\n.project-name strong {\n color: var(--color-heading);\n font-size: 0.95rem;\n font-weight: 600;\n}\n\n.project-name .project-path {\n color: var(--color-very-muted);\n font-size: 0.72rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.project-stats {\n display: flex;\n gap: 1.6rem;\n justify-self: end;\n text-align: right;\n}\n\n.stat {\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n min-width: 70px;\n}\n\n.stat-value {\n color: var(--color-heading);\n font-family: ui-monospace, monospace;\n font-size: 0.95rem;\n font-weight: 600;\n}\n\n.stat-value.cost {\n color: var(--color-accent);\n}\n\n.stat-label {\n color: var(--color-muted);\n font-size: 0.66rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n\n.bar {\n grid-column: 1 / -1;\n height: 3px;\n background: var(--color-form-bg);\n border-radius: 2px;\n overflow: hidden;\n}\n\n.bar-fill {\n height: 100%;\n background: linear-gradient(90deg, var(--color-accent-darker), var(--color-accent));\n border-radius: 2px;\n transition: width 400ms ease;\n}\n\n/* Tables */\ntable {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.83rem;\n background: var(--color-surface);\n border: 1px solid var(--color-border);\n border-radius: 6px;\n overflow: hidden;\n}\n\ntable thead th {\n text-align: left;\n color: var(--color-muted);\n text-transform: uppercase;\n font-size: 0.66rem;\n letter-spacing: 0.08em;\n font-weight: 600;\n padding: 0.65rem 0.85rem;\n border-bottom: 1px solid var(--color-border);\n background: var(--color-surface);\n}\n\ntable thead th.num {\n text-align: right;\n}\n\ntable tbody td {\n padding: 0.55rem 0.85rem;\n border-bottom: 1px solid rgba(77, 0, 32, 0.4);\n color: var(--color-body);\n}\n\ntable tbody td.num {\n text-align: right;\n}\n\ntable tbody td.cost {\n color: var(--color-accent);\n font-weight: 600;\n}\n\ntable tbody tr:last-child td {\n border-bottom: none;\n}\n\ntable tbody tr:hover {\n background: var(--color-surface-raised);\n}\n\ncode {\n color: var(--color-heading);\n background: var(--color-form-bg);\n padding: 0.1rem 0.4rem;\n border-radius: 3px;\n font-size: 0.85em;\n}\n\n.decision-block {\n color: var(--color-block);\n font-weight: 700;\n text-transform: uppercase;\n font-size: 0.72rem;\n letter-spacing: 0.06em;\n}\n\n.decision-allow {\n color: var(--color-allow);\n opacity: 0.7;\n text-transform: uppercase;\n font-size: 0.72rem;\n letter-spacing: 0.06em;\n}\n\n.empty {\n color: var(--color-muted);\n font-style: italic;\n padding: 1rem;\n background: var(--color-surface);\n border: 1px dashed var(--color-border);\n border-radius: 6px;\n}\n\n.hidden {\n display: none;\n}\n\nfooter {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 0.85rem 2rem;\n background: var(--color-surface);\n border-top: 1px solid var(--color-border);\n color: var(--color-muted);\n font-size: 0.72rem;\n font-family: ui-monospace, monospace;\n}\n\nfooter .muted {\n color: var(--color-very-muted);\n}\n\n.dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--color-muted);\n transition: background 200ms ease, box-shadow 200ms ease;\n}\n\n.dot.live {\n background: var(--color-accent);\n box-shadow: 0 0 8px var(--color-accent-darker);\n}\n\n.dot.dead {\n background: var(--color-border);\n box-shadow: none;\n}\n';
672
+ var style_default = `/* Synthra token dashboard \u2014 palette per project brief.
673
+ Base: cream-on-near-black, dark-red borders, hot-pink highlights.
674
+ Plus: money green for dollar amounts, per-family model colors,
675
+ subtle dot grid + top glow background, tooltip system, icons. */
676
+
677
+ :root {
678
+ /* Brand */
679
+ --color-heading: #ECEBD8;
680
+ --color-body: #EDECD9;
681
+ --color-bg: #000000;
682
+ --color-surface: #140009;
683
+ --color-surface-raised: #1E000D;
684
+ --color-border: #4D0020;
685
+ --color-accent: #FF0073;
686
+ --color-accent-darker: #EB006A;
687
+ --color-form-bg: #2E0014;
688
+ --color-muted: rgba(237, 236, 217, 0.55);
689
+ --color-very-muted: rgba(237, 236, 217, 0.35);
690
+ --color-block: #FF0073;
691
+ --color-allow: #ECEBD8;
692
+
693
+ /* Money green \u2014 used everywhere a $ amount appears */
694
+ --color-money: #36E596;
695
+ --color-money-darker: #00B85F;
696
+ --color-money-bg: rgba(54, 229, 150, 0.08);
697
+
698
+ /* Per-family model colors */
699
+ --color-model-opus: #C9A2FF;
700
+ --color-model-sonnet: #6BD0FF;
701
+ --color-model-haiku: #7BFFC7;
702
+
703
+ /* Background layering */
704
+ --bg-dot-color: rgba(237, 236, 217, 0.045);
705
+ --bg-glow-color: rgba(255, 0, 115, 0.07);
706
+ }
707
+
708
+ * {
709
+ box-sizing: border-box;
710
+ margin: 0;
711
+ padding: 0;
712
+ }
713
+
714
+ html, body {
715
+ background-color: var(--color-bg);
716
+ color: var(--color-body);
717
+ font-family:
718
+ ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
719
+ system-ui, sans-serif;
720
+ font-size: 14px;
721
+ line-height: 1.5;
722
+ min-height: 100vh;
723
+ }
724
+
725
+ /* Body becomes a flex column so main can grow + header/footer stay sticky
726
+ at the natural top/bottom of the viewport. */
727
+ body {
728
+ display: flex;
729
+ flex-direction: column;
730
+ min-height: 100vh;
731
+ }
732
+
733
+ main {
734
+ flex: 1;
735
+ }
736
+
737
+ /* Layered backdrop: dot grid + soft pink glow at the top.
738
+ Both are fixed so they don't repeat as you scroll. */
739
+ body {
740
+ background-image:
741
+ radial-gradient(ellipse 70% 40% at 50% 0%, var(--bg-glow-color), transparent 70%),
742
+ radial-gradient(circle at 1px 1px, var(--bg-dot-color) 1px, transparent 0);
743
+ background-size: 100% 100%, 22px 22px;
744
+ background-attachment: fixed;
745
+ }
746
+
747
+ code, .num, table, .project-path {
748
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
749
+ }
750
+
751
+ header {
752
+ display: flex;
753
+ align-items: center;
754
+ gap: 1.5rem;
755
+ padding: 1.1rem 2rem;
756
+ background: linear-gradient(180deg, rgba(20, 0, 9, 0.7), rgba(20, 0, 9, 0.4));
757
+ backdrop-filter: blur(8px);
758
+ border-bottom: 1px solid var(--color-border);
759
+ position: sticky;
760
+ top: 0;
761
+ z-index: 5;
762
+ }
763
+
764
+ header .brand {
765
+ display: flex;
766
+ align-items: baseline;
767
+ gap: 0.75rem;
768
+ }
769
+
770
+ header h1 {
771
+ color: var(--color-heading);
772
+ font-size: 1.35rem;
773
+ font-weight: 700;
774
+ letter-spacing: 0.02em;
775
+ }
776
+
777
+ header .tag {
778
+ color: var(--color-muted);
779
+ font-size: 0.78rem;
780
+ text-transform: uppercase;
781
+ letter-spacing: 0.1em;
782
+ }
783
+
784
+ header .meta {
785
+ margin-left: auto;
786
+ display: flex;
787
+ align-items: center;
788
+ gap: 0.75rem;
789
+ font-size: 0.78rem;
790
+ font-family: ui-monospace, monospace;
791
+ color: var(--color-muted);
792
+ }
793
+
794
+ header .active-project {
795
+ max-width: 480px;
796
+ white-space: nowrap;
797
+ overflow: hidden;
798
+ text-overflow: ellipsis;
799
+ }
800
+
801
+ main {
802
+ padding: 2rem;
803
+ display: flex;
804
+ flex-direction: column;
805
+ gap: 2rem;
806
+ max-width: 1400px;
807
+ margin: 0 auto;
808
+ width: 100%;
809
+ }
810
+
811
+ section {
812
+ display: flex;
813
+ flex-direction: column;
814
+ gap: 0.85rem;
815
+ }
816
+
817
+ h2 {
818
+ color: var(--color-heading);
819
+ font-size: 0.85rem;
820
+ font-weight: 600;
821
+ letter-spacing: 0.08em;
822
+ text-transform: uppercase;
823
+ display: flex;
824
+ align-items: center;
825
+ gap: 0.5rem;
826
+ }
827
+
828
+ h2 svg {
829
+ width: 14px;
830
+ height: 14px;
831
+ opacity: 0.5;
832
+ }
833
+
834
+ h2 .muted {
835
+ color: var(--color-very-muted);
836
+ font-size: 0.78rem;
837
+ font-weight: 400;
838
+ letter-spacing: 0.06em;
839
+ text-transform: none;
840
+ margin-left: 0.5rem;
841
+ }
842
+
843
+ /* ============================================================
844
+ Cards row (Global totals)
845
+ ============================================================ */
846
+
847
+ .cards {
848
+ display: grid;
849
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
850
+ gap: 0.7rem;
851
+ }
852
+
853
+ .card {
854
+ position: relative;
855
+ background: var(--color-surface);
856
+ border: 1px solid var(--color-border);
857
+ border-radius: 8px;
858
+ padding: 0.95rem 1rem 0.85rem;
859
+ transition: transform 120ms ease, border-color 120ms ease;
860
+ }
861
+
862
+ .card:hover {
863
+ transform: translateY(-1px);
864
+ border-color: rgba(77, 0, 32, 0.85);
865
+ }
866
+
867
+ .card.accent {
868
+ background: var(--color-surface-raised);
869
+ border-color: var(--color-accent);
870
+ }
871
+
872
+ .card.money {
873
+ background: linear-gradient(180deg, var(--color-money-bg), transparent 80%), var(--color-surface);
874
+ border-color: var(--color-money-darker);
875
+ }
876
+
877
+ .card.money:hover {
878
+ border-color: var(--color-money);
879
+ }
880
+
881
+ .card-head {
882
+ display: flex;
883
+ align-items: center;
884
+ justify-content: space-between;
885
+ gap: 0.5rem;
886
+ margin-bottom: 0.4rem;
887
+ }
888
+
889
+ .card-label {
890
+ color: var(--color-muted);
891
+ font-size: 0.66rem;
892
+ text-transform: uppercase;
893
+ letter-spacing: 0.09em;
894
+ display: flex;
895
+ align-items: center;
896
+ gap: 0.35rem;
897
+ }
898
+
899
+ .card-icon {
900
+ width: 14px;
901
+ height: 14px;
902
+ color: var(--color-muted);
903
+ opacity: 0.65;
904
+ flex-shrink: 0;
905
+ }
906
+
907
+ .card.accent .card-icon { color: var(--color-accent); opacity: 0.85; }
908
+ .card.money .card-icon { color: var(--color-money); opacity: 0.85; }
909
+
910
+ .card-value {
911
+ color: var(--color-heading);
912
+ font-family: ui-monospace, monospace;
913
+ font-size: 1.5rem;
914
+ font-weight: 600;
915
+ letter-spacing: -0.01em;
916
+ }
917
+
918
+ .card.accent .card-value { color: var(--color-accent); }
919
+ .card.money .card-value { color: var(--color-money); }
920
+
921
+ /* ============================================================
922
+ Tooltip (data-tooltip on .has-tooltip)
923
+ ============================================================ */
924
+
925
+ .has-tooltip {
926
+ position: relative;
927
+ cursor: help;
928
+ }
929
+
930
+ .has-tooltip::before,
931
+ .has-tooltip::after {
932
+ position: absolute;
933
+ pointer-events: none;
934
+ opacity: 0;
935
+ transition: opacity 150ms ease, transform 150ms ease;
936
+ z-index: 100;
937
+ }
938
+
939
+ .has-tooltip::after {
940
+ content: attr(data-tooltip);
941
+ bottom: calc(100% + 10px);
942
+ left: 50%;
943
+ transform: translate(-50%, 4px);
944
+ background: var(--color-surface-raised);
945
+ color: var(--color-body);
946
+ border: 1px solid var(--color-border);
947
+ border-radius: 6px;
948
+ padding: 0.6rem 0.75rem;
949
+ font-size: 0.74rem;
950
+ font-weight: 400;
951
+ text-transform: none;
952
+ letter-spacing: 0;
953
+ white-space: normal;
954
+ width: 260px;
955
+ text-align: left;
956
+ line-height: 1.45;
957
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7);
958
+ }
959
+
960
+ .has-tooltip::before {
961
+ content: "";
962
+ bottom: calc(100% + 4px);
963
+ left: 50%;
964
+ transform: translate(-50%, 4px);
965
+ border: 6px solid transparent;
966
+ border-top-color: var(--color-border);
967
+ }
968
+
969
+ .has-tooltip:hover::after,
970
+ .has-tooltip:hover::before {
971
+ opacity: 1;
972
+ transform: translate(-50%, 0);
973
+ }
974
+
975
+ /* Small \u24D8 icon used to indicate tooltips */
976
+ .help-icon {
977
+ display: inline-flex;
978
+ align-items: center;
979
+ justify-content: center;
980
+ width: 13px;
981
+ height: 13px;
982
+ border-radius: 50%;
983
+ border: 1px solid var(--color-muted);
984
+ color: var(--color-muted);
985
+ font-size: 9px;
986
+ font-weight: 600;
987
+ font-family: ui-sans-serif, sans-serif;
988
+ line-height: 1;
989
+ cursor: help;
990
+ user-select: none;
991
+ transition: color 120ms, border-color 120ms;
992
+ }
993
+
994
+ .has-tooltip:hover .help-icon {
995
+ border-color: var(--color-accent);
996
+ color: var(--color-accent);
997
+ }
998
+
999
+ .card.money .has-tooltip:hover .help-icon { border-color: var(--color-money); color: var(--color-money); }
1000
+
1001
+ /* ============================================================
1002
+ Projects list
1003
+ ============================================================ */
1004
+
1005
+ .projects {
1006
+ display: flex;
1007
+ flex-direction: column;
1008
+ gap: 0.6rem;
1009
+ }
1010
+
1011
+ .project-row {
1012
+ display: grid;
1013
+ grid-template-columns: minmax(220px, 1fr) auto;
1014
+ grid-template-rows: auto auto;
1015
+ gap: 0.6rem 1.25rem;
1016
+ align-items: center;
1017
+ background: var(--color-surface);
1018
+ border: 1px solid var(--color-border);
1019
+ border-radius: 8px;
1020
+ padding: 0.9rem 1.2rem;
1021
+ transition: background 120ms ease;
1022
+ }
1023
+
1024
+ .project-row:hover {
1025
+ background: var(--color-surface-raised);
1026
+ }
1027
+
1028
+ .project-name {
1029
+ display: flex;
1030
+ flex-direction: column;
1031
+ gap: 0.15rem;
1032
+ overflow: hidden;
1033
+ }
1034
+
1035
+ .project-name strong {
1036
+ color: var(--color-heading);
1037
+ font-size: 0.95rem;
1038
+ font-weight: 600;
1039
+ display: flex;
1040
+ align-items: center;
1041
+ gap: 0.4rem;
1042
+ }
1043
+
1044
+ .project-name strong svg {
1045
+ width: 13px;
1046
+ height: 13px;
1047
+ opacity: 0.65;
1048
+ color: var(--color-muted);
1049
+ flex-shrink: 0;
1050
+ }
1051
+
1052
+ .project-name .project-path {
1053
+ color: var(--color-very-muted);
1054
+ font-size: 0.72rem;
1055
+ white-space: nowrap;
1056
+ overflow: hidden;
1057
+ text-overflow: ellipsis;
1058
+ }
1059
+
1060
+ .project-stats {
1061
+ display: flex;
1062
+ gap: 1.6rem;
1063
+ justify-self: end;
1064
+ text-align: right;
1065
+ }
1066
+
1067
+ .stat {
1068
+ display: flex;
1069
+ flex-direction: column;
1070
+ gap: 0.15rem;
1071
+ min-width: 70px;
1072
+ }
1073
+
1074
+ .stat-value {
1075
+ color: var(--color-heading);
1076
+ font-family: ui-monospace, monospace;
1077
+ font-size: 0.95rem;
1078
+ font-weight: 600;
1079
+ }
1080
+
1081
+ .stat-value.cost {
1082
+ color: var(--color-money);
1083
+ }
1084
+
1085
+ .stat-label {
1086
+ color: var(--color-muted);
1087
+ font-size: 0.66rem;
1088
+ text-transform: uppercase;
1089
+ letter-spacing: 0.08em;
1090
+ }
1091
+
1092
+ .bar {
1093
+ grid-column: 1 / -1;
1094
+ height: 3px;
1095
+ background: var(--color-form-bg);
1096
+ border-radius: 2px;
1097
+ overflow: hidden;
1098
+ }
1099
+
1100
+ .bar-fill {
1101
+ height: 100%;
1102
+ background: linear-gradient(90deg, var(--color-accent-darker), var(--color-accent));
1103
+ border-radius: 2px;
1104
+ transition: width 400ms ease;
1105
+ }
1106
+
1107
+ /* ============================================================
1108
+ Tables
1109
+ ============================================================ */
1110
+
1111
+ table {
1112
+ width: 100%;
1113
+ border-collapse: collapse;
1114
+ font-size: 0.83rem;
1115
+ background: var(--color-surface);
1116
+ border: 1px solid var(--color-border);
1117
+ border-radius: 8px;
1118
+ overflow: hidden;
1119
+ }
1120
+
1121
+ table thead th {
1122
+ text-align: left;
1123
+ color: var(--color-muted);
1124
+ text-transform: uppercase;
1125
+ font-size: 0.66rem;
1126
+ letter-spacing: 0.08em;
1127
+ font-weight: 600;
1128
+ padding: 0.65rem 0.85rem;
1129
+ border-bottom: 1px solid var(--color-border);
1130
+ background: var(--color-surface);
1131
+ }
1132
+
1133
+ table thead th.num {
1134
+ text-align: right;
1135
+ }
1136
+
1137
+ table thead th .has-tooltip {
1138
+ display: inline-flex;
1139
+ align-items: center;
1140
+ gap: 0.3rem;
1141
+ }
1142
+
1143
+ table tbody td {
1144
+ padding: 0.55rem 0.85rem;
1145
+ border-bottom: 1px solid rgba(77, 0, 32, 0.4);
1146
+ color: var(--color-body);
1147
+ }
1148
+
1149
+ table tbody td.num {
1150
+ text-align: right;
1151
+ }
1152
+
1153
+ table tbody td.cost {
1154
+ color: var(--color-money);
1155
+ font-weight: 600;
1156
+ }
1157
+
1158
+ table tbody tr:last-child td {
1159
+ border-bottom: none;
1160
+ }
1161
+
1162
+ table tbody tr:hover {
1163
+ background: var(--color-surface-raised);
1164
+ }
1165
+
1166
+ /* ============================================================
1167
+ Model pills \u2014 color-coded by family
1168
+ ============================================================ */
1169
+
1170
+ .model-pill {
1171
+ display: inline-block;
1172
+ padding: 0.1rem 0.5rem;
1173
+ border-radius: 3px;
1174
+ background: var(--color-form-bg);
1175
+ font-family: ui-monospace, monospace;
1176
+ font-size: 0.82em;
1177
+ border: 1px solid transparent;
1178
+ color: var(--color-body);
1179
+ white-space: nowrap;
1180
+ }
1181
+
1182
+ .model-pill.opus {
1183
+ color: var(--color-model-opus);
1184
+ border-color: rgba(201, 162, 255, 0.3);
1185
+ background: rgba(201, 162, 255, 0.08);
1186
+ }
1187
+
1188
+ .model-pill.sonnet {
1189
+ color: var(--color-model-sonnet);
1190
+ border-color: rgba(107, 208, 255, 0.3);
1191
+ background: rgba(107, 208, 255, 0.08);
1192
+ }
1193
+
1194
+ .model-pill.haiku {
1195
+ color: var(--color-model-haiku);
1196
+ border-color: rgba(123, 255, 199, 0.3);
1197
+ background: rgba(123, 255, 199, 0.08);
1198
+ }
1199
+
1200
+ .model-pill.unknown {
1201
+ color: var(--color-muted);
1202
+ font-style: italic;
1203
+ border-color: rgba(237, 236, 217, 0.15);
1204
+ }
1205
+
1206
+ /* Existing inline code in tables (project name, etc.) */
1207
+ code {
1208
+ color: var(--color-heading);
1209
+ background: var(--color-form-bg);
1210
+ padding: 0.1rem 0.4rem;
1211
+ border-radius: 3px;
1212
+ font-size: 0.85em;
1213
+ }
1214
+
1215
+ /* ============================================================
1216
+ Decisions (allow / block)
1217
+ ============================================================ */
1218
+
1219
+ .decision-block {
1220
+ color: var(--color-block);
1221
+ font-weight: 700;
1222
+ text-transform: uppercase;
1223
+ font-size: 0.72rem;
1224
+ letter-spacing: 0.06em;
1225
+ display: inline-flex;
1226
+ align-items: center;
1227
+ gap: 0.3rem;
1228
+ }
1229
+
1230
+ .decision-block svg {
1231
+ width: 11px;
1232
+ height: 11px;
1233
+ }
1234
+
1235
+ .decision-allow {
1236
+ color: var(--color-allow);
1237
+ opacity: 0.7;
1238
+ text-transform: uppercase;
1239
+ font-size: 0.72rem;
1240
+ letter-spacing: 0.06em;
1241
+ }
1242
+
1243
+ .empty {
1244
+ color: var(--color-muted);
1245
+ font-style: italic;
1246
+ padding: 1rem;
1247
+ background: var(--color-surface);
1248
+ border: 1px dashed var(--color-border);
1249
+ border-radius: 8px;
1250
+ }
1251
+
1252
+ .hidden {
1253
+ display: none;
1254
+ }
1255
+
1256
+ footer {
1257
+ display: flex;
1258
+ justify-content: space-between;
1259
+ align-items: center;
1260
+ padding: 0.85rem 2rem;
1261
+ background: linear-gradient(0deg, rgba(20, 0, 9, 0.7), rgba(20, 0, 9, 0.4));
1262
+ backdrop-filter: blur(8px);
1263
+ border-top: 1px solid var(--color-border);
1264
+ color: var(--color-muted);
1265
+ font-size: 0.72rem;
1266
+ font-family: ui-monospace, monospace;
1267
+ position: sticky;
1268
+ bottom: 0;
1269
+ z-index: 5;
1270
+ }
1271
+
1272
+ footer .muted {
1273
+ color: var(--color-very-muted);
1274
+ }
1275
+
1276
+ .dot {
1277
+ width: 8px;
1278
+ height: 8px;
1279
+ border-radius: 50%;
1280
+ background: var(--color-muted);
1281
+ transition: background 200ms ease, box-shadow 200ms ease;
1282
+ }
1283
+
1284
+ .dot.live {
1285
+ background: var(--color-money);
1286
+ box-shadow: 0 0 8px var(--color-money-darker);
1287
+ }
1288
+
1289
+ .dot.dead {
1290
+ background: var(--color-border);
1291
+ box-shadow: none;
1292
+ }
1293
+ `;
566
1294
 
567
1295
  // src/dashboard/server.ts
568
1296
  var FALLBACK_RANGE = 9;