@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.
package/dist/cli/index.js CHANGED
@@ -18,7 +18,7 @@ var init_package = __esm({
18
18
  "package.json"() {
19
19
  package_default = {
20
20
  name: "@jefuriiij/synthra",
21
- version: "0.1.1",
21
+ version: "0.1.2",
22
22
  publishConfig: {
23
23
  access: "public"
24
24
  },
@@ -456,28 +456,39 @@ var public_default = `<!doctype html>
456
456
 
457
457
  <main>
458
458
  <section>
459
- <h2>Global totals <span class="muted">(all projects)</span></h2>
459
+ <h2>
460
+ <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>
461
+ Global totals
462
+ <span class="muted">(all projects)</span>
463
+ </h2>
460
464
  <div class="cards" id="cards"></div>
461
465
  </section>
462
466
 
463
467
  <section>
464
- <h2>Projects</h2>
468
+ <h2>
469
+ <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>
470
+ Projects
471
+ </h2>
465
472
  <div class="projects" id="projects"></div>
466
473
  <p class="empty hidden" id="projects-empty">No projects registered yet. Run <code>syn .</code> in any project to add it.</p>
467
474
  </section>
468
475
 
469
476
  <section>
470
- <h2>Recent calls <span class="muted">(across all projects)</span></h2>
477
+ <h2>
478
+ <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>
479
+ Recent calls
480
+ <span class="muted">(across all projects)</span>
481
+ </h2>
471
482
  <table id="turns">
472
483
  <thead>
473
484
  <tr>
474
485
  <th>Time</th>
475
486
  <th>Project</th>
476
487
  <th>Model</th>
477
- <th class="num">Input</th>
478
- <th class="num">Output</th>
479
- <th class="num">Cache R / W</th>
480
- <th class="num">Cost</th>
488
+ <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>
489
+ <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>
490
+ <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>
491
+ <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>
481
492
  </tr>
482
493
  </thead>
483
494
  <tbody></tbody>
@@ -486,14 +497,17 @@ var public_default = `<!doctype html>
486
497
  </section>
487
498
 
488
499
  <section>
489
- <h2>Recent gate decisions</h2>
500
+ <h2>
501
+ <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>
502
+ Recent gate decisions
503
+ </h2>
490
504
  <table id="gates">
491
505
  <thead>
492
506
  <tr>
493
507
  <th>Time</th>
494
508
  <th>Project</th>
495
509
  <th>Tool</th>
496
- <th>Decision</th>
510
+ <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>
497
511
  <th>Query</th>
498
512
  </tr>
499
513
  </thead>
@@ -504,11 +518,42 @@ var public_default = `<!doctype html>
504
518
  </main>
505
519
 
506
520
  <footer>
507
- <span>Token Counter MCP \xB7 live polling every 2s</span>
508
- <span class="muted">Cost figures are approximate \u2014 see /docs/PROTOCOL.md</span>
521
+ <span>Synthra Token Dashboard \xB7 live polling every 2s</span>
522
+ <span class="muted">Cost figures are approximate \u2014 based on published Anthropic rates.</span>
509
523
  </footer>
510
524
 
511
525
  <script>
526
+ // Inline SVG icons. Each is a stroke-based icon, currentColor for the
527
+ // stroke, designed to inherit color from the parent's CSS.
528
+ const ICONS = {
529
+ 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>',
530
+ 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>',
531
+ 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>',
532
+ 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>',
533
+ 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>',
534
+ 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>',
535
+ 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>',
536
+ 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>',
537
+ 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>',
538
+ 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>',
539
+ };
540
+
541
+ // Classify a model name into a family for color-coding.
542
+ function modelFamily(model) {
543
+ if (!model) return 'unknown';
544
+ const m = model.toLowerCase();
545
+ if (m === '<synthetic>') return 'unknown';
546
+ if (m.includes('opus')) return 'opus';
547
+ if (m.includes('sonnet')) return 'sonnet';
548
+ if (m.includes('haiku')) return 'haiku';
549
+ return 'unknown';
550
+ }
551
+
552
+ function modelLabel(model) {
553
+ if (!model || model === '<synthetic>') return model === '<synthetic>' ? 'synthetic' : 'unknown';
554
+ return model;
555
+ }
556
+
512
557
  const $ = (sel) => document.querySelector(sel);
513
558
  const cardsEl = $("#cards");
514
559
  const projectsEl = $("#projects");
@@ -551,23 +596,84 @@ var public_default = `<!doctype html>
551
596
  }
552
597
  }
553
598
 
599
+ // Definitions for the global-totals cards. Each: label, value-source key,
600
+ // icon, optional class (accent | money), tooltip text.
601
+ function cardConfigs(g) {
602
+ return [
603
+ {
604
+ label: "Total cost",
605
+ value: fmtCost(g.estimated_cost_usd),
606
+ icon: ICONS.dollar,
607
+ cls: "money",
608
+ 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.",
609
+ },
610
+ {
611
+ label: "Turns",
612
+ value: fmt(g.total_turns),
613
+ icon: ICONS.chat,
614
+ tooltip: "Total number of back-and-forth exchanges with Claude across all projects. One turn = you send a message, Claude responds.",
615
+ },
616
+ {
617
+ label: "Input",
618
+ value: fmt(g.total_input_tokens),
619
+ icon: ICONS.arrowDown,
620
+ tooltip: "New (uncached) tokens sent to Claude across all turns. Usually small \u2014 most of the conversation comes from cache.",
621
+ },
622
+ {
623
+ label: "Output",
624
+ value: fmt(g.total_output_tokens),
625
+ icon: ICONS.arrowUp,
626
+ tooltip: "Tokens Claude generated in responses. The most expensive line item per turn (~5\xD7 input rate on Opus).",
627
+ },
628
+ {
629
+ label: "Cache read",
630
+ value: fmt(g.total_cache_read),
631
+ icon: ICONS.refresh,
632
+ 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.",
633
+ },
634
+ {
635
+ label: "Cache write",
636
+ value: fmt(g.total_cache_create),
637
+ icon: ICONS.save,
638
+ 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.",
639
+ },
640
+ {
641
+ label: "Projects",
642
+ value: fmt(g.project_count),
643
+ icon: ICONS.folder,
644
+ tooltip: "Projects that have ever run \`syn .\` on this machine. Tracked in ~/.synthra/projects.json.",
645
+ },
646
+ {
647
+ label: "Blocked Grep / Glob",
648
+ value: fmt(g.blocked_count),
649
+ icon: ICONS.shield,
650
+ cls: "accent",
651
+ 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.",
652
+ },
653
+ {
654
+ label: "Tokens saved",
655
+ value: fmt(g.estimated_tokens_saved),
656
+ icon: ICONS.trending,
657
+ cls: "accent",
658
+ 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.",
659
+ },
660
+ ];
661
+ }
662
+
554
663
  function renderCards(g) {
555
664
  cardsEl.innerHTML = "";
556
- const cards = [
557
- { label: "Total cost", value: fmtCost(g.estimated_cost_usd), accent: true },
558
- { label: "Turns", value: fmt(g.total_turns) },
559
- { label: "Input", value: fmt(g.total_input_tokens) },
560
- { label: "Output", value: fmt(g.total_output_tokens) },
561
- { label: "Cache read", value: fmt(g.total_cache_read) },
562
- { label: "Cache write", value: fmt(g.total_cache_create) },
563
- { label: "Projects", value: fmt(g.project_count) },
564
- { label: "Blocked Grep / Glob", value: fmt(g.blocked_count), accent: true },
565
- { label: "Tokens saved", value: fmt(g.estimated_tokens_saved), accent: true },
566
- ];
567
- for (const c of cards) {
665
+ for (const c of cardConfigs(g)) {
568
666
  const el = document.createElement("div");
569
- el.className = "card" + (c.accent ? " accent" : "");
570
- el.innerHTML = '<div class="card-label">' + c.label + '</div><div class="card-value">' + c.value + '</div>';
667
+ el.className = "card" + (c.cls ? " " + c.cls : "");
668
+ el.innerHTML =
669
+ '<div class="card-head">' +
670
+ '<div class="card-label has-tooltip" data-tooltip="' + c.tooltip.replace(/"/g, '&quot;') + '">' +
671
+ c.label +
672
+ ' <span class="help-icon">i</span>' +
673
+ '</div>' +
674
+ '<span class="card-icon">' + c.icon + '</span>' +
675
+ '</div>' +
676
+ '<div class="card-value">' + c.value + '</div>';
571
677
  cardsEl.appendChild(el);
572
678
  }
573
679
  }
@@ -588,7 +694,7 @@ var public_default = `<!doctype html>
588
694
  row.className = "project-row";
589
695
  row.innerHTML =
590
696
  '<div class="project-name">' +
591
- '<strong>' + p.name + '</strong>' +
697
+ '<strong>' + ICONS.folder + p.name + '</strong>' +
592
698
  '<code class="project-path">' + p.path + '</code>' +
593
699
  '</div>' +
594
700
  '<div class="project-stats">' +
@@ -610,15 +716,12 @@ var public_default = `<!doctype html>
610
716
  }
611
717
  turnsEmpty.classList.add("hidden");
612
718
  for (const t of turns) {
719
+ const family = modelFamily(t.model);
613
720
  const tr = document.createElement("tr");
614
- const modelCell =
615
- t.model && t.model !== "<synthetic>"
616
- ? "<code>" + t.model + "</code>"
617
- : '<span class="muted">' + (t.model === "<synthetic>" ? "synthetic" : "unknown") + "</span>";
618
721
  tr.innerHTML =
619
722
  "<td>" + fmtTs(t.ts) + "</td>" +
620
723
  "<td><code>" + t.project_name + "</code></td>" +
621
- "<td>" + modelCell + "</td>" +
724
+ '<td><span class="model-pill ' + family + '">' + modelLabel(t.model) + "</span></td>" +
622
725
  '<td class="num">' + fmtFull(t.input) + "</td>" +
623
726
  '<td class="num">' + fmtFull(t.output) + "</td>" +
624
727
  '<td class="num">' + fmt(t.cache_read) + " / " + fmt(t.cache_create) + "</td>" +
@@ -636,12 +739,16 @@ var public_default = `<!doctype html>
636
739
  gatesEmpty.classList.add("hidden");
637
740
  for (const g of gates) {
638
741
  const tr = document.createElement("tr");
639
- const cls = g.decision === "block" ? "decision-block" : "decision-allow";
742
+ const isBlock = g.decision === "block";
743
+ const cls = isBlock ? "decision-block" : "decision-allow";
744
+ const label = isBlock
745
+ ? '<span class="' + cls + '">' + ICONS.ban + ' BLOCK</span>'
746
+ : '<span class="' + cls + '">ALLOW</span>';
640
747
  tr.innerHTML =
641
748
  "<td>" + fmtTs(g.ts) + "</td>" +
642
749
  "<td><code>" + g.project_name + "</code></td>" +
643
750
  "<td><code>" + g.tool + "</code></td>" +
644
- '<td class="' + cls + '">' + g.decision + "</td>" +
751
+ "<td>" + label + "</td>" +
645
752
  "<td><code>" + (g.query || "") + "</code></td>";
646
753
  gatesBody.appendChild(tr);
647
754
  }
@@ -675,7 +782,628 @@ var public_default = `<!doctype html>
675
782
  `;
676
783
 
677
784
  // src/dashboard/public/style.css
678
- 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';
785
+ var style_default = `/* Synthra token dashboard \u2014 palette per project brief.
786
+ Base: cream-on-near-black, dark-red borders, hot-pink highlights.
787
+ Plus: money green for dollar amounts, per-family model colors,
788
+ subtle dot grid + top glow background, tooltip system, icons. */
789
+
790
+ :root {
791
+ /* Brand */
792
+ --color-heading: #ECEBD8;
793
+ --color-body: #EDECD9;
794
+ --color-bg: #000000;
795
+ --color-surface: #140009;
796
+ --color-surface-raised: #1E000D;
797
+ --color-border: #4D0020;
798
+ --color-accent: #FF0073;
799
+ --color-accent-darker: #EB006A;
800
+ --color-form-bg: #2E0014;
801
+ --color-muted: rgba(237, 236, 217, 0.55);
802
+ --color-very-muted: rgba(237, 236, 217, 0.35);
803
+ --color-block: #FF0073;
804
+ --color-allow: #ECEBD8;
805
+
806
+ /* Money green \u2014 used everywhere a $ amount appears */
807
+ --color-money: #36E596;
808
+ --color-money-darker: #00B85F;
809
+ --color-money-bg: rgba(54, 229, 150, 0.08);
810
+
811
+ /* Per-family model colors */
812
+ --color-model-opus: #C9A2FF;
813
+ --color-model-sonnet: #6BD0FF;
814
+ --color-model-haiku: #7BFFC7;
815
+
816
+ /* Background layering */
817
+ --bg-dot-color: rgba(237, 236, 217, 0.045);
818
+ --bg-glow-color: rgba(255, 0, 115, 0.07);
819
+ }
820
+
821
+ * {
822
+ box-sizing: border-box;
823
+ margin: 0;
824
+ padding: 0;
825
+ }
826
+
827
+ html, body {
828
+ background-color: var(--color-bg);
829
+ color: var(--color-body);
830
+ font-family:
831
+ ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
832
+ system-ui, sans-serif;
833
+ font-size: 14px;
834
+ line-height: 1.5;
835
+ min-height: 100vh;
836
+ }
837
+
838
+ /* Body becomes a flex column so main can grow + header/footer stay sticky
839
+ at the natural top/bottom of the viewport. */
840
+ body {
841
+ display: flex;
842
+ flex-direction: column;
843
+ min-height: 100vh;
844
+ }
845
+
846
+ main {
847
+ flex: 1;
848
+ }
849
+
850
+ /* Layered backdrop: dot grid + soft pink glow at the top.
851
+ Both are fixed so they don't repeat as you scroll. */
852
+ body {
853
+ background-image:
854
+ radial-gradient(ellipse 70% 40% at 50% 0%, var(--bg-glow-color), transparent 70%),
855
+ radial-gradient(circle at 1px 1px, var(--bg-dot-color) 1px, transparent 0);
856
+ background-size: 100% 100%, 22px 22px;
857
+ background-attachment: fixed;
858
+ }
859
+
860
+ code, .num, table, .project-path {
861
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
862
+ }
863
+
864
+ header {
865
+ display: flex;
866
+ align-items: center;
867
+ gap: 1.5rem;
868
+ padding: 1.1rem 2rem;
869
+ background: linear-gradient(180deg, rgba(20, 0, 9, 0.7), rgba(20, 0, 9, 0.4));
870
+ backdrop-filter: blur(8px);
871
+ border-bottom: 1px solid var(--color-border);
872
+ position: sticky;
873
+ top: 0;
874
+ z-index: 5;
875
+ }
876
+
877
+ header .brand {
878
+ display: flex;
879
+ align-items: baseline;
880
+ gap: 0.75rem;
881
+ }
882
+
883
+ header h1 {
884
+ color: var(--color-heading);
885
+ font-size: 1.35rem;
886
+ font-weight: 700;
887
+ letter-spacing: 0.02em;
888
+ }
889
+
890
+ header .tag {
891
+ color: var(--color-muted);
892
+ font-size: 0.78rem;
893
+ text-transform: uppercase;
894
+ letter-spacing: 0.1em;
895
+ }
896
+
897
+ header .meta {
898
+ margin-left: auto;
899
+ display: flex;
900
+ align-items: center;
901
+ gap: 0.75rem;
902
+ font-size: 0.78rem;
903
+ font-family: ui-monospace, monospace;
904
+ color: var(--color-muted);
905
+ }
906
+
907
+ header .active-project {
908
+ max-width: 480px;
909
+ white-space: nowrap;
910
+ overflow: hidden;
911
+ text-overflow: ellipsis;
912
+ }
913
+
914
+ main {
915
+ padding: 2rem;
916
+ display: flex;
917
+ flex-direction: column;
918
+ gap: 2rem;
919
+ max-width: 1400px;
920
+ margin: 0 auto;
921
+ width: 100%;
922
+ }
923
+
924
+ section {
925
+ display: flex;
926
+ flex-direction: column;
927
+ gap: 0.85rem;
928
+ }
929
+
930
+ h2 {
931
+ color: var(--color-heading);
932
+ font-size: 0.85rem;
933
+ font-weight: 600;
934
+ letter-spacing: 0.08em;
935
+ text-transform: uppercase;
936
+ display: flex;
937
+ align-items: center;
938
+ gap: 0.5rem;
939
+ }
940
+
941
+ h2 svg {
942
+ width: 14px;
943
+ height: 14px;
944
+ opacity: 0.5;
945
+ }
946
+
947
+ h2 .muted {
948
+ color: var(--color-very-muted);
949
+ font-size: 0.78rem;
950
+ font-weight: 400;
951
+ letter-spacing: 0.06em;
952
+ text-transform: none;
953
+ margin-left: 0.5rem;
954
+ }
955
+
956
+ /* ============================================================
957
+ Cards row (Global totals)
958
+ ============================================================ */
959
+
960
+ .cards {
961
+ display: grid;
962
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
963
+ gap: 0.7rem;
964
+ }
965
+
966
+ .card {
967
+ position: relative;
968
+ background: var(--color-surface);
969
+ border: 1px solid var(--color-border);
970
+ border-radius: 8px;
971
+ padding: 0.95rem 1rem 0.85rem;
972
+ transition: transform 120ms ease, border-color 120ms ease;
973
+ }
974
+
975
+ .card:hover {
976
+ transform: translateY(-1px);
977
+ border-color: rgba(77, 0, 32, 0.85);
978
+ }
979
+
980
+ .card.accent {
981
+ background: var(--color-surface-raised);
982
+ border-color: var(--color-accent);
983
+ }
984
+
985
+ .card.money {
986
+ background: linear-gradient(180deg, var(--color-money-bg), transparent 80%), var(--color-surface);
987
+ border-color: var(--color-money-darker);
988
+ }
989
+
990
+ .card.money:hover {
991
+ border-color: var(--color-money);
992
+ }
993
+
994
+ .card-head {
995
+ display: flex;
996
+ align-items: center;
997
+ justify-content: space-between;
998
+ gap: 0.5rem;
999
+ margin-bottom: 0.4rem;
1000
+ }
1001
+
1002
+ .card-label {
1003
+ color: var(--color-muted);
1004
+ font-size: 0.66rem;
1005
+ text-transform: uppercase;
1006
+ letter-spacing: 0.09em;
1007
+ display: flex;
1008
+ align-items: center;
1009
+ gap: 0.35rem;
1010
+ }
1011
+
1012
+ .card-icon {
1013
+ width: 14px;
1014
+ height: 14px;
1015
+ color: var(--color-muted);
1016
+ opacity: 0.65;
1017
+ flex-shrink: 0;
1018
+ }
1019
+
1020
+ .card.accent .card-icon { color: var(--color-accent); opacity: 0.85; }
1021
+ .card.money .card-icon { color: var(--color-money); opacity: 0.85; }
1022
+
1023
+ .card-value {
1024
+ color: var(--color-heading);
1025
+ font-family: ui-monospace, monospace;
1026
+ font-size: 1.5rem;
1027
+ font-weight: 600;
1028
+ letter-spacing: -0.01em;
1029
+ }
1030
+
1031
+ .card.accent .card-value { color: var(--color-accent); }
1032
+ .card.money .card-value { color: var(--color-money); }
1033
+
1034
+ /* ============================================================
1035
+ Tooltip (data-tooltip on .has-tooltip)
1036
+ ============================================================ */
1037
+
1038
+ .has-tooltip {
1039
+ position: relative;
1040
+ cursor: help;
1041
+ }
1042
+
1043
+ .has-tooltip::before,
1044
+ .has-tooltip::after {
1045
+ position: absolute;
1046
+ pointer-events: none;
1047
+ opacity: 0;
1048
+ transition: opacity 150ms ease, transform 150ms ease;
1049
+ z-index: 100;
1050
+ }
1051
+
1052
+ .has-tooltip::after {
1053
+ content: attr(data-tooltip);
1054
+ bottom: calc(100% + 10px);
1055
+ left: 50%;
1056
+ transform: translate(-50%, 4px);
1057
+ background: var(--color-surface-raised);
1058
+ color: var(--color-body);
1059
+ border: 1px solid var(--color-border);
1060
+ border-radius: 6px;
1061
+ padding: 0.6rem 0.75rem;
1062
+ font-size: 0.74rem;
1063
+ font-weight: 400;
1064
+ text-transform: none;
1065
+ letter-spacing: 0;
1066
+ white-space: normal;
1067
+ width: 260px;
1068
+ text-align: left;
1069
+ line-height: 1.45;
1070
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7);
1071
+ }
1072
+
1073
+ .has-tooltip::before {
1074
+ content: "";
1075
+ bottom: calc(100% + 4px);
1076
+ left: 50%;
1077
+ transform: translate(-50%, 4px);
1078
+ border: 6px solid transparent;
1079
+ border-top-color: var(--color-border);
1080
+ }
1081
+
1082
+ .has-tooltip:hover::after,
1083
+ .has-tooltip:hover::before {
1084
+ opacity: 1;
1085
+ transform: translate(-50%, 0);
1086
+ }
1087
+
1088
+ /* Small \u24D8 icon used to indicate tooltips */
1089
+ .help-icon {
1090
+ display: inline-flex;
1091
+ align-items: center;
1092
+ justify-content: center;
1093
+ width: 13px;
1094
+ height: 13px;
1095
+ border-radius: 50%;
1096
+ border: 1px solid var(--color-muted);
1097
+ color: var(--color-muted);
1098
+ font-size: 9px;
1099
+ font-weight: 600;
1100
+ font-family: ui-sans-serif, sans-serif;
1101
+ line-height: 1;
1102
+ cursor: help;
1103
+ user-select: none;
1104
+ transition: color 120ms, border-color 120ms;
1105
+ }
1106
+
1107
+ .has-tooltip:hover .help-icon {
1108
+ border-color: var(--color-accent);
1109
+ color: var(--color-accent);
1110
+ }
1111
+
1112
+ .card.money .has-tooltip:hover .help-icon { border-color: var(--color-money); color: var(--color-money); }
1113
+
1114
+ /* ============================================================
1115
+ Projects list
1116
+ ============================================================ */
1117
+
1118
+ .projects {
1119
+ display: flex;
1120
+ flex-direction: column;
1121
+ gap: 0.6rem;
1122
+ }
1123
+
1124
+ .project-row {
1125
+ display: grid;
1126
+ grid-template-columns: minmax(220px, 1fr) auto;
1127
+ grid-template-rows: auto auto;
1128
+ gap: 0.6rem 1.25rem;
1129
+ align-items: center;
1130
+ background: var(--color-surface);
1131
+ border: 1px solid var(--color-border);
1132
+ border-radius: 8px;
1133
+ padding: 0.9rem 1.2rem;
1134
+ transition: background 120ms ease;
1135
+ }
1136
+
1137
+ .project-row:hover {
1138
+ background: var(--color-surface-raised);
1139
+ }
1140
+
1141
+ .project-name {
1142
+ display: flex;
1143
+ flex-direction: column;
1144
+ gap: 0.15rem;
1145
+ overflow: hidden;
1146
+ }
1147
+
1148
+ .project-name strong {
1149
+ color: var(--color-heading);
1150
+ font-size: 0.95rem;
1151
+ font-weight: 600;
1152
+ display: flex;
1153
+ align-items: center;
1154
+ gap: 0.4rem;
1155
+ }
1156
+
1157
+ .project-name strong svg {
1158
+ width: 13px;
1159
+ height: 13px;
1160
+ opacity: 0.65;
1161
+ color: var(--color-muted);
1162
+ flex-shrink: 0;
1163
+ }
1164
+
1165
+ .project-name .project-path {
1166
+ color: var(--color-very-muted);
1167
+ font-size: 0.72rem;
1168
+ white-space: nowrap;
1169
+ overflow: hidden;
1170
+ text-overflow: ellipsis;
1171
+ }
1172
+
1173
+ .project-stats {
1174
+ display: flex;
1175
+ gap: 1.6rem;
1176
+ justify-self: end;
1177
+ text-align: right;
1178
+ }
1179
+
1180
+ .stat {
1181
+ display: flex;
1182
+ flex-direction: column;
1183
+ gap: 0.15rem;
1184
+ min-width: 70px;
1185
+ }
1186
+
1187
+ .stat-value {
1188
+ color: var(--color-heading);
1189
+ font-family: ui-monospace, monospace;
1190
+ font-size: 0.95rem;
1191
+ font-weight: 600;
1192
+ }
1193
+
1194
+ .stat-value.cost {
1195
+ color: var(--color-money);
1196
+ }
1197
+
1198
+ .stat-label {
1199
+ color: var(--color-muted);
1200
+ font-size: 0.66rem;
1201
+ text-transform: uppercase;
1202
+ letter-spacing: 0.08em;
1203
+ }
1204
+
1205
+ .bar {
1206
+ grid-column: 1 / -1;
1207
+ height: 3px;
1208
+ background: var(--color-form-bg);
1209
+ border-radius: 2px;
1210
+ overflow: hidden;
1211
+ }
1212
+
1213
+ .bar-fill {
1214
+ height: 100%;
1215
+ background: linear-gradient(90deg, var(--color-accent-darker), var(--color-accent));
1216
+ border-radius: 2px;
1217
+ transition: width 400ms ease;
1218
+ }
1219
+
1220
+ /* ============================================================
1221
+ Tables
1222
+ ============================================================ */
1223
+
1224
+ table {
1225
+ width: 100%;
1226
+ border-collapse: collapse;
1227
+ font-size: 0.83rem;
1228
+ background: var(--color-surface);
1229
+ border: 1px solid var(--color-border);
1230
+ border-radius: 8px;
1231
+ overflow: hidden;
1232
+ }
1233
+
1234
+ table thead th {
1235
+ text-align: left;
1236
+ color: var(--color-muted);
1237
+ text-transform: uppercase;
1238
+ font-size: 0.66rem;
1239
+ letter-spacing: 0.08em;
1240
+ font-weight: 600;
1241
+ padding: 0.65rem 0.85rem;
1242
+ border-bottom: 1px solid var(--color-border);
1243
+ background: var(--color-surface);
1244
+ }
1245
+
1246
+ table thead th.num {
1247
+ text-align: right;
1248
+ }
1249
+
1250
+ table thead th .has-tooltip {
1251
+ display: inline-flex;
1252
+ align-items: center;
1253
+ gap: 0.3rem;
1254
+ }
1255
+
1256
+ table tbody td {
1257
+ padding: 0.55rem 0.85rem;
1258
+ border-bottom: 1px solid rgba(77, 0, 32, 0.4);
1259
+ color: var(--color-body);
1260
+ }
1261
+
1262
+ table tbody td.num {
1263
+ text-align: right;
1264
+ }
1265
+
1266
+ table tbody td.cost {
1267
+ color: var(--color-money);
1268
+ font-weight: 600;
1269
+ }
1270
+
1271
+ table tbody tr:last-child td {
1272
+ border-bottom: none;
1273
+ }
1274
+
1275
+ table tbody tr:hover {
1276
+ background: var(--color-surface-raised);
1277
+ }
1278
+
1279
+ /* ============================================================
1280
+ Model pills \u2014 color-coded by family
1281
+ ============================================================ */
1282
+
1283
+ .model-pill {
1284
+ display: inline-block;
1285
+ padding: 0.1rem 0.5rem;
1286
+ border-radius: 3px;
1287
+ background: var(--color-form-bg);
1288
+ font-family: ui-monospace, monospace;
1289
+ font-size: 0.82em;
1290
+ border: 1px solid transparent;
1291
+ color: var(--color-body);
1292
+ white-space: nowrap;
1293
+ }
1294
+
1295
+ .model-pill.opus {
1296
+ color: var(--color-model-opus);
1297
+ border-color: rgba(201, 162, 255, 0.3);
1298
+ background: rgba(201, 162, 255, 0.08);
1299
+ }
1300
+
1301
+ .model-pill.sonnet {
1302
+ color: var(--color-model-sonnet);
1303
+ border-color: rgba(107, 208, 255, 0.3);
1304
+ background: rgba(107, 208, 255, 0.08);
1305
+ }
1306
+
1307
+ .model-pill.haiku {
1308
+ color: var(--color-model-haiku);
1309
+ border-color: rgba(123, 255, 199, 0.3);
1310
+ background: rgba(123, 255, 199, 0.08);
1311
+ }
1312
+
1313
+ .model-pill.unknown {
1314
+ color: var(--color-muted);
1315
+ font-style: italic;
1316
+ border-color: rgba(237, 236, 217, 0.15);
1317
+ }
1318
+
1319
+ /* Existing inline code in tables (project name, etc.) */
1320
+ code {
1321
+ color: var(--color-heading);
1322
+ background: var(--color-form-bg);
1323
+ padding: 0.1rem 0.4rem;
1324
+ border-radius: 3px;
1325
+ font-size: 0.85em;
1326
+ }
1327
+
1328
+ /* ============================================================
1329
+ Decisions (allow / block)
1330
+ ============================================================ */
1331
+
1332
+ .decision-block {
1333
+ color: var(--color-block);
1334
+ font-weight: 700;
1335
+ text-transform: uppercase;
1336
+ font-size: 0.72rem;
1337
+ letter-spacing: 0.06em;
1338
+ display: inline-flex;
1339
+ align-items: center;
1340
+ gap: 0.3rem;
1341
+ }
1342
+
1343
+ .decision-block svg {
1344
+ width: 11px;
1345
+ height: 11px;
1346
+ }
1347
+
1348
+ .decision-allow {
1349
+ color: var(--color-allow);
1350
+ opacity: 0.7;
1351
+ text-transform: uppercase;
1352
+ font-size: 0.72rem;
1353
+ letter-spacing: 0.06em;
1354
+ }
1355
+
1356
+ .empty {
1357
+ color: var(--color-muted);
1358
+ font-style: italic;
1359
+ padding: 1rem;
1360
+ background: var(--color-surface);
1361
+ border: 1px dashed var(--color-border);
1362
+ border-radius: 8px;
1363
+ }
1364
+
1365
+ .hidden {
1366
+ display: none;
1367
+ }
1368
+
1369
+ footer {
1370
+ display: flex;
1371
+ justify-content: space-between;
1372
+ align-items: center;
1373
+ padding: 0.85rem 2rem;
1374
+ background: linear-gradient(0deg, rgba(20, 0, 9, 0.7), rgba(20, 0, 9, 0.4));
1375
+ backdrop-filter: blur(8px);
1376
+ border-top: 1px solid var(--color-border);
1377
+ color: var(--color-muted);
1378
+ font-size: 0.72rem;
1379
+ font-family: ui-monospace, monospace;
1380
+ position: sticky;
1381
+ bottom: 0;
1382
+ z-index: 5;
1383
+ }
1384
+
1385
+ footer .muted {
1386
+ color: var(--color-very-muted);
1387
+ }
1388
+
1389
+ .dot {
1390
+ width: 8px;
1391
+ height: 8px;
1392
+ border-radius: 50%;
1393
+ background: var(--color-muted);
1394
+ transition: background 200ms ease, box-shadow 200ms ease;
1395
+ }
1396
+
1397
+ .dot.live {
1398
+ background: var(--color-money);
1399
+ box-shadow: 0 0 8px var(--color-money-darker);
1400
+ }
1401
+
1402
+ .dot.dead {
1403
+ background: var(--color-border);
1404
+ box-shadow: none;
1405
+ }
1406
+ `;
679
1407
 
680
1408
  // src/dashboard/server.ts
681
1409
  var FALLBACK_RANGE = 9;