tokenrace 0.1.20 → 0.1.22
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/assets/index-BD6TWWBL.css +1 -0
- package/dist/assets/{index-C8zpckDk.js → index-Dt3hqx3Z.js} +58 -58
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api-routes.js +6 -1
- package/src/server.js +1 -4
- package/src/store.js +82 -47
- package/dist/assets/index-BuT9l_n-.css +0 -1
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>tokenrace</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-Dt3hqx3Z.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BD6TWWBL.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/api-routes.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { Router } from 'express'
|
|
13
13
|
import {
|
|
14
|
-
getStatus, getSummary, getTimeseries, getProjects,
|
|
14
|
+
getStatus, getSummary, getTimeseries, getTimeseriesByProject, getProjects,
|
|
15
15
|
getSessions, getUnlabeledSessions, getSessionEvents,
|
|
16
16
|
getEvents, getTools, getAgents, getModels,
|
|
17
17
|
labelSession, ignoreSession, reset, resetProject
|
|
@@ -114,6 +114,11 @@ export function createRouter({ port = 1337 } = {}) {
|
|
|
114
114
|
res.json(getTimeseries(req.query.metric, req.query.from, req.query.bucket))
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
+
/** GET /api/timeseries/by-project — serie temporal desglosada por proyecto, acepta ?metric, ?from, ?bucket */
|
|
118
|
+
router.get('/api/timeseries/by-project', (req, res) => {
|
|
119
|
+
res.json(getTimeseriesByProject(req.query.metric, req.query.from, req.query.bucket))
|
|
120
|
+
})
|
|
121
|
+
|
|
117
122
|
/** GET /api/projects — proyectos con métricas, acepta ?from */
|
|
118
123
|
router.get('/api/projects', (req, res) => {
|
|
119
124
|
res.json(getProjects(req.query.from))
|
package/src/server.js
CHANGED
|
@@ -15,7 +15,7 @@ import express from 'express'
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { fileURLToPath } from 'node:url'
|
|
17
17
|
import { parseMetrics, parseEvents, parseTraces } from './otlp-parser.js'
|
|
18
|
-
import { processMetric, processEvent,
|
|
18
|
+
import { processMetric, processEvent, loadFromDisk, startAutoSave, saveSync, drainAutoDetectQueue, labelSession } from './store.js'
|
|
19
19
|
import { detectProjectBySessionId } from './git-detector.js'
|
|
20
20
|
import { createRouter, broadcast } from './api-routes.js'
|
|
21
21
|
|
|
@@ -92,9 +92,6 @@ export async function startServer({ port = 1337 } = {}) {
|
|
|
92
92
|
*/
|
|
93
93
|
app.post('/v1/traces', (req, res) => {
|
|
94
94
|
const spans = parseTraces(req.body)
|
|
95
|
-
for (const span of spans) {
|
|
96
|
-
processTrace(span)
|
|
97
|
-
}
|
|
98
95
|
broadcast('trace', { count: spans.length })
|
|
99
96
|
res.json({ partialSuccess: {} })
|
|
100
97
|
})
|
package/src/store.js
CHANGED
|
@@ -65,7 +65,6 @@ const state = {
|
|
|
65
65
|
eventIndex: 0,
|
|
66
66
|
projects: new Map(), // projectName → ProjectAggregates
|
|
67
67
|
tools: new Map(), // toolName → ToolStats
|
|
68
|
-
agents: new Map(), // agentId → AgentData
|
|
69
68
|
models: new Map(), // modelName → ModelStats
|
|
70
69
|
cumulativeValues: new Map(), // clave → último valor acumulativo (no persiste)
|
|
71
70
|
lastSeen: null,
|
|
@@ -97,7 +96,7 @@ function normalizeIncoming(name, value, labels) {
|
|
|
97
96
|
switch (name) {
|
|
98
97
|
case 'claude_code.token.usage': {
|
|
99
98
|
const type = labels.type
|
|
100
|
-
const key = `token:${sid}:${type}:${labels.model ?? ''}:${labels.query_source ?? ''}`
|
|
99
|
+
const key = `token:${sid}:${type}:${labels.model ?? ''}:${labels.query_source ?? ''}:${labels['agent.name'] ?? ''}`
|
|
101
100
|
const delta = cumulativeDelta(key, value)
|
|
102
101
|
const nameMap = {
|
|
103
102
|
'input': 'claude_code.tokens.input',
|
|
@@ -110,7 +109,7 @@ function normalizeIncoming(name, value, labels) {
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
case 'claude_code.cost.usage': {
|
|
113
|
-
const key = `cost:${sid}:${labels.model ?? ''}:${labels.query_source ?? ''}:${labels.effort ?? ''}`
|
|
112
|
+
const key = `cost:${sid}:${labels.model ?? ''}:${labels.query_source ?? ''}:${labels.effort ?? ''}:${labels['agent.name'] ?? ''}`
|
|
114
113
|
const delta = cumulativeDelta(key, value)
|
|
115
114
|
return { name: 'claude_code.cost', value: delta }
|
|
116
115
|
}
|
|
@@ -409,15 +408,15 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
|
|
|
409
408
|
session.apiRequests++
|
|
410
409
|
}
|
|
411
410
|
|
|
412
|
-
// ── Tool stats ──
|
|
413
|
-
if (eventName === '
|
|
411
|
+
// ── Tool stats de sesión ──
|
|
412
|
+
if (eventName === 'tool_result') {
|
|
414
413
|
session.toolCalls++
|
|
415
414
|
}
|
|
416
415
|
}
|
|
417
416
|
|
|
418
|
-
// ── Tool stats globales ──
|
|
419
|
-
if (eventName === '
|
|
420
|
-
const toolName = attributes['
|
|
417
|
+
// ── Tool stats globales — Claude Code envía tool_result (no tool_use) ──
|
|
418
|
+
if (eventName === 'tool_result') {
|
|
419
|
+
const toolName = attributes['tool_name'] ?? attributes['tool.name'] ?? 'unknown'
|
|
421
420
|
|
|
422
421
|
if (!state.tools.has(toolName)) {
|
|
423
422
|
state.tools.set(toolName, { count: 0, successes: 0, totalDurationMs: 0 })
|
|
@@ -425,12 +424,11 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
|
|
|
425
424
|
const tool = state.tools.get(toolName)
|
|
426
425
|
tool.count++
|
|
427
426
|
|
|
428
|
-
if (attributes.success === true || attributes
|
|
427
|
+
if (attributes.success === true || attributes.success === 'true') {
|
|
429
428
|
tool.successes++
|
|
430
429
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
430
|
+
const durMs = Number(attributes['duration_ms'] ?? attributes['tool.duration_ms'] ?? 0)
|
|
431
|
+
if (durMs > 0) tool.totalDurationMs += durMs
|
|
434
432
|
}
|
|
435
433
|
|
|
436
434
|
state.lastSeen = timestamp
|
|
@@ -439,33 +437,6 @@ export function processEvent({ eventName, timestamp, severity, attributes }) {
|
|
|
439
437
|
return eventObj
|
|
440
438
|
}
|
|
441
439
|
|
|
442
|
-
/**
|
|
443
|
-
* Procesa un span normalizado.
|
|
444
|
-
* Solo actúa si el span tiene agentId.
|
|
445
|
-
* @param {Object} span
|
|
446
|
-
*/
|
|
447
|
-
export function processTrace(span) {
|
|
448
|
-
const attrs = span.attributes
|
|
449
|
-
const agentId = attrs['agent.id'] ?? attrs['gen_ai.agent.id'] ?? null
|
|
450
|
-
if (!agentId) return
|
|
451
|
-
|
|
452
|
-
if (!state.agents.has(agentId)) {
|
|
453
|
-
state.agents.set(agentId, {
|
|
454
|
-
agentId,
|
|
455
|
-
parentAgentId: attrs['agent.parent_id'] ?? null,
|
|
456
|
-
tokensInput: 0,
|
|
457
|
-
tokensOutput: 0,
|
|
458
|
-
cost: 0,
|
|
459
|
-
toolCalls: 0,
|
|
460
|
-
durationMs: 0
|
|
461
|
-
})
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const agent = state.agents.get(agentId)
|
|
465
|
-
const duration = span.endTime - span.startTime
|
|
466
|
-
if (duration > 0) agent.durationMs += duration
|
|
467
|
-
}
|
|
468
|
-
|
|
469
440
|
/**
|
|
470
441
|
* Asigna un proyecto a una sesión, con aplicación retroactiva.
|
|
471
442
|
* @param {string} sessionId
|
|
@@ -507,7 +478,6 @@ export function reset() {
|
|
|
507
478
|
state.eventIndex = 0
|
|
508
479
|
state.projects.clear()
|
|
509
480
|
state.tools.clear()
|
|
510
|
-
state.agents.clear()
|
|
511
481
|
state.models.clear()
|
|
512
482
|
state.cumulativeValues.clear()
|
|
513
483
|
state.lastSeen = null
|
|
@@ -694,6 +664,36 @@ export function getTimeseries(metric, from, bucket) {
|
|
|
694
664
|
* Proyectos con sus métricas agregadas filtradas por rango temporal.
|
|
695
665
|
* @param {string} from
|
|
696
666
|
*/
|
|
667
|
+
/**
|
|
668
|
+
* Serie temporal de una métrica desglosada por proyecto.
|
|
669
|
+
* Devuelve [{ timestamp, projects: { [projectName]: value } }]
|
|
670
|
+
*/
|
|
671
|
+
export function getTimeseriesByProject(metric, from, bucket) {
|
|
672
|
+
const minTs = parseTimeRange(from)
|
|
673
|
+
const bucketMs = parseBucket(bucket)
|
|
674
|
+
const points = state.timeseries.get(metric) ?? []
|
|
675
|
+
|
|
676
|
+
// Map: bucketKey → Map<projectName, value>
|
|
677
|
+
const buckets = new Map()
|
|
678
|
+
for (const point of points) {
|
|
679
|
+
if (point.ts < minTs) continue
|
|
680
|
+
const sid = point.labels['session.id']
|
|
681
|
+
if (sid && state.ignoredSessions.has(sid)) continue
|
|
682
|
+
const proj = resolveProject(sid, point.labels.project) ?? '(sin proyecto)'
|
|
683
|
+
const key = Math.floor(point.ts / bucketMs) * bucketMs
|
|
684
|
+
if (!buckets.has(key)) buckets.set(key, new Map())
|
|
685
|
+
const byProj = buckets.get(key)
|
|
686
|
+
byProj.set(proj, (byProj.get(proj) ?? 0) + point.value)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return Array.from(buckets.entries())
|
|
690
|
+
.map(([timestamp, byProj]) => ({
|
|
691
|
+
timestamp,
|
|
692
|
+
projects: Object.fromEntries(byProj),
|
|
693
|
+
}))
|
|
694
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
697
|
export function getProjects(from) {
|
|
698
698
|
const minTs = parseTimeRange(from)
|
|
699
699
|
const result = []
|
|
@@ -856,24 +856,51 @@ export function getTools(from) {
|
|
|
856
856
|
avgDurationMs: stats.count > 0 ? stats.totalDurationMs / stats.count : 0
|
|
857
857
|
})).sort((a, b) => b.count - a.count)
|
|
858
858
|
|
|
859
|
-
// Tasa de aprobación/rechazo desde
|
|
859
|
+
// Tasa de aprobación/rechazo desde tool_result (Claude Code usa decision_type)
|
|
860
860
|
let approved = 0
|
|
861
861
|
let rejected = 0
|
|
862
862
|
for (const event of state.events) {
|
|
863
863
|
if (event.timestamp < minTs) continue
|
|
864
|
-
if (event.eventName !== '
|
|
865
|
-
if (event.attributes
|
|
866
|
-
if (event.attributes
|
|
864
|
+
if (event.eventName !== 'tool_result') continue
|
|
865
|
+
if (event.attributes['decision_type'] === 'accept') approved++
|
|
866
|
+
else if (event.attributes['decision_type'] === 'reject') rejected++
|
|
867
867
|
}
|
|
868
868
|
|
|
869
869
|
return { usage, decisionRate: { approved, rejected } }
|
|
870
870
|
}
|
|
871
871
|
|
|
872
872
|
/**
|
|
873
|
-
*
|
|
873
|
+
* Estadísticas de subagentes (Task tool), derivadas de las métricas de tokens
|
|
874
|
+
* y coste cuyas labels traen query_source: "subagent" y agent.name.
|
|
875
|
+
* Claude Code no emite eventos/spans con identidad de agente individual, así
|
|
876
|
+
* que se agrega por nombre de agente (p. ej. "Explore", "general-purpose").
|
|
874
877
|
*/
|
|
875
878
|
export function getAgents() {
|
|
876
|
-
|
|
879
|
+
const byAgent = new Map() // agentName → { tokensInput, tokensOutput, cost }
|
|
880
|
+
|
|
881
|
+
const metricField = {
|
|
882
|
+
'claude_code.tokens.input': 'tokensInput',
|
|
883
|
+
'claude_code.tokens.output': 'tokensOutput',
|
|
884
|
+
'claude_code.cost': 'cost'
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
for (const [metricName, field] of Object.entries(metricField)) {
|
|
888
|
+
for (const point of state.timeseries.get(metricName) ?? []) {
|
|
889
|
+
const labels = point.labels
|
|
890
|
+
if (labels.query_source !== 'subagent') continue
|
|
891
|
+
const name = labels['agent.name']
|
|
892
|
+
if (!name) continue
|
|
893
|
+
|
|
894
|
+
if (!byAgent.has(name)) {
|
|
895
|
+
byAgent.set(name, { tokensInput: 0, tokensOutput: 0, cost: 0 })
|
|
896
|
+
}
|
|
897
|
+
byAgent.get(name)[field] += point.value
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return Array.from(byAgent.entries())
|
|
902
|
+
.map(([name, stats]) => ({ name, ...stats }))
|
|
903
|
+
.sort((a, b) => b.cost - a.cost)
|
|
877
904
|
}
|
|
878
905
|
|
|
879
906
|
/**
|
|
@@ -918,7 +945,8 @@ export function saveSync() {
|
|
|
918
945
|
events: state.events,
|
|
919
946
|
eventIndex: state.eventIndex,
|
|
920
947
|
totalEvents: state.totalEvents,
|
|
921
|
-
startTime: state.startTime
|
|
948
|
+
startTime: state.startTime,
|
|
949
|
+
tools: Array.from(state.tools.entries())
|
|
922
950
|
}
|
|
923
951
|
|
|
924
952
|
fs.writeFileSync(dataFile, JSON.stringify(data), { mode: 0o600 })
|
|
@@ -989,6 +1017,13 @@ export function loadFromDisk() {
|
|
|
989
1017
|
state.startTime = data.startTime
|
|
990
1018
|
}
|
|
991
1019
|
|
|
1020
|
+
// Restaurar stats de herramientas
|
|
1021
|
+
if (Array.isArray(data.tools)) {
|
|
1022
|
+
for (const [toolName, stats] of data.tools) {
|
|
1023
|
+
state.tools.set(toolName, stats)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
992
1027
|
// Re-aplicar mappings a sesiones (timeseries no necesitan propagación:
|
|
993
1028
|
// resolveProject() consulta sessionMappings primero en todos los lectores)
|
|
994
1029
|
for (const [sessionId, project] of state.sessionMappings.entries()) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Fira Code,Menlo,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-2{right:.5rem}.top-0{top:0}.top-10{top:2.5rem}.top-2{top:.5rem}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-48{height:12rem}.h-\[600px\]{height:600px}.h-full{height:100%}.max-h-40{max-height:10rem}.min-h-\[60vh\]{min-height:60vh}.min-h-screen{min-height:100vh}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-32{width:8rem}.w-44{width:11rem}.w-6{width:1.5rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[640px\]{min-width:640px}.min-w-\[720px\]{min-width:720px}.max-w-screen-2xl{max-width:1536px}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-accent-green{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.border-bg-border{--tw-border-opacity: 1;border-color:rgb(26 26 26 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.bg-accent-green{--tw-bg-opacity: 1;background-color:rgb(255 107 53 / var(--tw-bg-opacity, 1))}.bg-accent-purple{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-accent-teal{--tw-bg-opacity: 1;background-color:rgb(0 212 170 / var(--tw-bg-opacity, 1))}.bg-accent-yellow{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-bg-base{--tw-bg-opacity: 1;background-color:rgb(20 25 31 / var(--tw-bg-opacity, 1))}.bg-bg-card,.bg-bg-subtle{--tw-bg-opacity: 1;background-color:rgb(33 38 43 / var(--tw-bg-opacity, 1))}.bg-text-muted{--tw-bg-opacity: 1;background-color:rgb(68 68 68 / var(--tw-bg-opacity, 1))}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,Menlo,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-accent-blue{--tw-text-opacity: 1;color:rgb(77 166 255 / var(--tw-text-opacity, 1))}.text-accent-green,.text-accent-orange{--tw-text-opacity: 1;color:rgb(255 107 53 / var(--tw-text-opacity, 1))}.text-accent-purple{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-accent-teal{--tw-text-opacity: 1;color:rgb(0 212 170 / var(--tw-text-opacity, 1))}.text-accent-yellow{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-text-muted{--tw-text-opacity: 1;color:rgb(68 68 68 / var(--tw-text-opacity, 1))}.text-text-primary{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-text-secondary{--tw-text-opacity: 1;color:rgb(136 136 136 / var(--tw-text-opacity, 1))}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{--bg-base: #14191F;--bg-card: #21262B;--bg-card-hover: #272d33;--bg-border: #1a1a1a;--bg-subtle: #21262B;--text-primary: #ffffff;--text-secondary: #888888;--text-muted: #444444;--accent-green: #ff6b35;--accent-blue: #4da6ff;--accent-purple: #a855f7;--accent-orange: #ff6b35;--accent-teal: #00d4aa;--accent-yellow: #fbbf24}*{box-sizing:border-box}html,body,#root{height:100%;margin:0;padding:0;background-color:#14191f;color:#fff}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:#0d0d0d}::-webkit-scrollbar-thumb{background:#333;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#444}.hover\:border-accent-green:hover{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.hover\:bg-bg-base:hover{--tw-bg-opacity: 1;background-color:rgb(20 25 31 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-card:hover{--tw-bg-opacity: 1;background-color:rgb(33 38 43 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-card-hover:hover{--tw-bg-opacity: 1;background-color:rgb(39 45 51 / var(--tw-bg-opacity, 1))}.hover\:text-accent-orange:hover{--tw-text-opacity: 1;color:rgb(255 107 53 / var(--tw-text-opacity, 1))}.hover\:text-text-primary:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:text-text-secondary:hover{--tw-text-opacity: 1;color:rgb(136 136 136 / var(--tw-text-opacity, 1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-accent-green:focus{--tw-border-opacity: 1;border-color:rgb(255 107 53 / var(--tw-border-opacity, 1))}.disabled\:opacity-40:disabled{opacity:.4}@media(min-width:768px){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}
|