opencastle 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +47 -11
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +104 -1
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/export.d.ts +3 -0
- package/dist/cli/convoy/export.d.ts.map +1 -0
- package/dist/cli/convoy/export.js +46 -0
- package/dist/cli/convoy/export.js.map +1 -0
- package/dist/cli/convoy/export.test.d.ts +2 -0
- package/dist/cli/convoy/export.test.d.ts.map +1 -0
- package/dist/cli/convoy/export.test.js +157 -0
- package/dist/cli/convoy/export.test.js.map +1 -0
- package/dist/cli/convoy/health.test.js +1 -0
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +8 -3
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +83 -3
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +1 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/dashboard.d.ts +14 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +73 -36
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/run/adapters/index.d.ts.map +1 -1
- package/dist/cli/run/adapters/index.js +2 -1
- package/dist/cli/run/adapters/index.js.map +1 -1
- package/dist/cli/run/adapters/opencode.d.ts +16 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.js +75 -0
- package/dist/cli/run/adapters/opencode.js.map +1 -0
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +11 -0
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +44 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +18 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +3 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/engine.test.ts +126 -1
- package/src/cli/convoy/engine.ts +39 -9
- package/src/cli/convoy/export.test.ts +190 -0
- package/src/cli/convoy/export.ts +52 -0
- package/src/cli/convoy/health.test.ts +1 -0
- package/src/cli/convoy/store.test.ts +89 -3
- package/src/cli/convoy/store.ts +8 -3
- package/src/cli/convoy/types.ts +1 -0
- package/src/cli/dashboard.ts +94 -42
- package/src/cli/run/adapters/index.ts +2 -1
- package/src/cli/run/adapters/opencode.ts +88 -0
- package/src/cli/run/schema.test.ts +50 -0
- package/src/cli/run/schema.ts +13 -0
- package/src/cli/run.ts +19 -1
- package/src/cli/types.ts +3 -0
- package/src/dashboard/dist/_astro/{index.Bnq19_1M.css → index.DyyaCW8L.css} +1 -1
- package/src/dashboard/dist/index.html +145 -6
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +160 -4
- package/src/dashboard/src/styles/dashboard.css +60 -0
package/src/cli/dashboard.ts
CHANGED
|
@@ -20,6 +20,7 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
20
20
|
|
|
21
21
|
const DATA_FILES = [
|
|
22
22
|
'events.ndjson',
|
|
23
|
+
'convoys.ndjson',
|
|
23
24
|
// Legacy individual files — kept for backwards compatibility
|
|
24
25
|
'sessions.ndjson',
|
|
25
26
|
'delegations.ndjson',
|
|
@@ -32,12 +33,28 @@ interface DashboardArgs {
|
|
|
32
33
|
port: number
|
|
33
34
|
openBrowser: boolean
|
|
34
35
|
seed: boolean
|
|
36
|
+
convoyId?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DashboardServerOptions {
|
|
40
|
+
port?: number
|
|
41
|
+
openBrowser?: boolean
|
|
42
|
+
seed?: boolean
|
|
43
|
+
pkgRoot: string
|
|
44
|
+
convoyId?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DashboardServerResult {
|
|
48
|
+
server: Server
|
|
49
|
+
port: number
|
|
50
|
+
url: string
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
function parseArgs(args: string[]): DashboardArgs {
|
|
38
54
|
let port = 4300
|
|
39
55
|
let openBrowser = true
|
|
40
56
|
let seed = false
|
|
57
|
+
let convoyId: string | undefined
|
|
41
58
|
|
|
42
59
|
for (let i = 0; i < args.length; i++) {
|
|
43
60
|
if (args[i] === '--port' && args[i + 1]) {
|
|
@@ -47,10 +64,13 @@ function parseArgs(args: string[]): DashboardArgs {
|
|
|
47
64
|
openBrowser = false
|
|
48
65
|
} else if (args[i] === '--seed') {
|
|
49
66
|
seed = true
|
|
67
|
+
} else if (args[i] === '--convoy' && args[i + 1]) {
|
|
68
|
+
convoyId = args[i + 1]
|
|
69
|
+
i++
|
|
50
70
|
}
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
return { port, openBrowser, seed }
|
|
73
|
+
return { port, openBrowser, seed, convoyId }
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
function openUrl(url: string): void {
|
|
@@ -104,15 +124,17 @@ function tryListen(
|
|
|
104
124
|
})
|
|
105
125
|
}
|
|
106
126
|
|
|
107
|
-
export
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
127
|
+
export async function startDashboardServer(
|
|
128
|
+
options: DashboardServerOptions,
|
|
129
|
+
): Promise<DashboardServerResult> {
|
|
130
|
+
const port = options.port ?? 4300
|
|
131
|
+
const seed = options.seed ?? false
|
|
132
|
+
const { pkgRoot } = options
|
|
112
133
|
|
|
113
134
|
const distDir = resolve(pkgRoot, 'src', 'dashboard', 'dist')
|
|
114
135
|
const seedDir = resolve(pkgRoot, 'src', 'dashboard', 'seed-data')
|
|
115
136
|
const projectRoot = process.cwd()
|
|
137
|
+
const convoyLogsDir = resolve(projectRoot, '.opencastle', 'logs')
|
|
116
138
|
const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
|
|
117
139
|
|
|
118
140
|
// Check if dist exists
|
|
@@ -122,18 +144,6 @@ export default async function dashboard({
|
|
|
122
144
|
)
|
|
123
145
|
}
|
|
124
146
|
|
|
125
|
-
// Check if any log files exist (for messaging)
|
|
126
|
-
let hasLogs = false
|
|
127
|
-
if (!seed) {
|
|
128
|
-
const checkFiles = ['events.ndjson', ...DATA_FILES]
|
|
129
|
-
for (const f of checkFiles) {
|
|
130
|
-
if (await fileExists(join(logsDir, f))) {
|
|
131
|
-
hasLogs = true
|
|
132
|
-
break
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
147
|
const server = createServer(
|
|
138
148
|
async (req: IncomingMessage, res: ServerResponse) => {
|
|
139
149
|
try {
|
|
@@ -149,22 +159,34 @@ export default async function dashboard({
|
|
|
149
159
|
const dataMatch = pathname.match(/^\/data\/(.+\.ndjson)$/)
|
|
150
160
|
if (dataMatch && DATA_FILES.includes(dataMatch[1])) {
|
|
151
161
|
const filename = dataMatch[1]
|
|
152
|
-
let filePath: string
|
|
153
162
|
|
|
154
163
|
if (seed) {
|
|
155
|
-
filePath = join(seedDir, filename)
|
|
164
|
+
const filePath = join(seedDir, filename)
|
|
165
|
+
if (await fileExists(filePath)) {
|
|
166
|
+
const content = await readFile(filePath)
|
|
167
|
+
res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
|
|
168
|
+
res.end(content)
|
|
169
|
+
} else {
|
|
170
|
+
res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
|
|
171
|
+
res.end('')
|
|
172
|
+
}
|
|
156
173
|
} else {
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
const convoyPath = join(convoyLogsDir, filename)
|
|
175
|
+
const logsPath = join(logsDir, filename)
|
|
176
|
+
const inConvoy = await fileExists(convoyPath)
|
|
177
|
+
const inLogs = await fileExists(logsPath)
|
|
159
178
|
|
|
160
|
-
if (await fileExists(filePath)) {
|
|
161
|
-
const content = await readFile(filePath)
|
|
162
|
-
res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
|
|
163
|
-
res.end(content)
|
|
164
|
-
} else {
|
|
165
|
-
// Graceful fallback — empty body
|
|
166
179
|
res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
|
|
167
|
-
|
|
180
|
+
if (inConvoy && inLogs) {
|
|
181
|
+
const [c1, c2] = await Promise.all([readFile(convoyPath), readFile(logsPath)])
|
|
182
|
+
res.end(Buffer.concat([c1, c2]))
|
|
183
|
+
} else if (inConvoy) {
|
|
184
|
+
res.end(await readFile(convoyPath))
|
|
185
|
+
} else if (inLogs) {
|
|
186
|
+
res.end(await readFile(logsPath))
|
|
187
|
+
} else {
|
|
188
|
+
res.end('')
|
|
189
|
+
}
|
|
168
190
|
}
|
|
169
191
|
return
|
|
170
192
|
}
|
|
@@ -197,23 +219,53 @@ export default async function dashboard({
|
|
|
197
219
|
)
|
|
198
220
|
|
|
199
221
|
const actualPort = await tryListen(server, port)
|
|
200
|
-
const
|
|
222
|
+
const resolvedUrl = `http://localhost:${actualPort}`
|
|
223
|
+
|
|
224
|
+
return { server, port: actualPort, url: resolvedUrl }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default async function dashboard({
|
|
228
|
+
pkgRoot,
|
|
229
|
+
args,
|
|
230
|
+
}: CliContext): Promise<void> {
|
|
231
|
+
const { port, openBrowser, seed, convoyId } = parseArgs(args)
|
|
232
|
+
|
|
233
|
+
// Check if any log files exist (for messaging)
|
|
234
|
+
let hasLogs = false
|
|
235
|
+
if (!seed) {
|
|
236
|
+
const projectRoot = process.cwd()
|
|
237
|
+
const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
|
|
238
|
+
const checkFiles = ['events.ndjson', ...DATA_FILES]
|
|
239
|
+
for (const f of checkFiles) {
|
|
240
|
+
if (await fileExists(join(logsDir, f))) {
|
|
241
|
+
hasLogs = true
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const dashResult = await startDashboardServer({ port, seed, pkgRoot, convoyId })
|
|
201
248
|
|
|
202
249
|
console.log('')
|
|
203
250
|
console.log(' \u{1F3F0} OpenCastle Dashboard')
|
|
204
251
|
console.log('')
|
|
205
|
-
console.log(` \u2192 ${url}`)
|
|
206
252
|
|
|
207
|
-
if (
|
|
208
|
-
console.log(
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
} else if (hasLogs) {
|
|
212
|
-
console.log(' \u{1F4C2} Reading logs from .github/customizations/logs/')
|
|
253
|
+
if (convoyId) {
|
|
254
|
+
console.log(` \u2192 ${dashResult.url}/?convoy=${convoyId}`)
|
|
255
|
+
console.log(` \u{1F4C2} Watching convoy: ${convoyId}`)
|
|
213
256
|
} else {
|
|
214
|
-
console.log(
|
|
215
|
-
|
|
216
|
-
|
|
257
|
+
console.log(` \u2192 ${dashResult.url}`)
|
|
258
|
+
if (seed) {
|
|
259
|
+
console.log(
|
|
260
|
+
' \u{1F4C2} Showing demo data (use without --seed to read project logs)'
|
|
261
|
+
)
|
|
262
|
+
} else if (hasLogs) {
|
|
263
|
+
console.log(' \u{1F4C2} Reading logs from .github/customizations/logs/')
|
|
264
|
+
} else {
|
|
265
|
+
console.log(
|
|
266
|
+
' \u{1F4A1} No agent logs found. Run agents with OpenCastle to generate data, or use --seed for demo data.'
|
|
267
|
+
)
|
|
268
|
+
}
|
|
217
269
|
}
|
|
218
270
|
|
|
219
271
|
console.log('')
|
|
@@ -221,13 +273,13 @@ export default async function dashboard({
|
|
|
221
273
|
console.log('')
|
|
222
274
|
|
|
223
275
|
if (openBrowser) {
|
|
224
|
-
openUrl(url)
|
|
276
|
+
openUrl(convoyId ? `${dashResult.url}/?convoy=${convoyId}` : dashResult.url)
|
|
225
277
|
}
|
|
226
278
|
|
|
227
279
|
// Graceful shutdown
|
|
228
280
|
process.on('SIGINT', () => {
|
|
229
281
|
console.log('\n Dashboard stopped.\n')
|
|
230
|
-
server.close()
|
|
282
|
+
dashResult.server.close()
|
|
231
283
|
process.exit(0)
|
|
232
284
|
})
|
|
233
285
|
|
|
@@ -7,6 +7,7 @@ const ADAPTERS: Record<string, () => Promise<AgentAdapter>> = {
|
|
|
7
7
|
'claude-code': () => import('./claude-code.js') as Promise<AgentAdapter>,
|
|
8
8
|
copilot: () => import('./copilot.js') as Promise<AgentAdapter>,
|
|
9
9
|
cursor: () => import('./cursor.js') as Promise<AgentAdapter>,
|
|
10
|
+
opencode: () => import('./opencode.js') as Promise<AgentAdapter>,
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -28,7 +29,7 @@ export async function getAdapter(name: string): Promise<AgentAdapter> {
|
|
|
28
29
|
* Detection priority order — checked first-to-last.
|
|
29
30
|
* The first available adapter wins.
|
|
30
31
|
*/
|
|
31
|
-
const DETECTION_ORDER = ['copilot', 'claude-code', 'cursor'] as const
|
|
32
|
+
const DETECTION_ORDER = ['copilot', 'claude-code', 'cursor', 'opencode'] as const
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Auto-detect which adapter CLI is available on the system.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
|
|
3
|
+
|
|
4
|
+
/** Adapter name */
|
|
5
|
+
export const name = 'opencode'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if the `opencode` CLI is available on the system PATH.
|
|
9
|
+
*/
|
|
10
|
+
export async function isAvailable(): Promise<boolean> {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const proc = spawn('which', ['opencode'], { stdio: 'pipe' })
|
|
13
|
+
proc.on('close', (code) => resolve(code === 0))
|
|
14
|
+
proc.on('error', () => resolve(false))
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute a task by invoking the OpenCode CLI in headless mode.
|
|
20
|
+
*/
|
|
21
|
+
export async function execute(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
22
|
+
let prompt = `You are a ${task.agent}. ${task.prompt}`
|
|
23
|
+
|
|
24
|
+
if (task.files && task.files.length > 0) {
|
|
25
|
+
prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const args = ['--headless', '-p', prompt]
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const proc = spawn('opencode', args, {
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
env: { ...process.env },
|
|
34
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
let stdout = ''
|
|
38
|
+
let stderr = ''
|
|
39
|
+
|
|
40
|
+
proc.stdout.on('data', (chunk: Buffer) => {
|
|
41
|
+
stdout += chunk.toString()
|
|
42
|
+
if (options.verbose) {
|
|
43
|
+
process.stdout.write(chunk)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
proc.stderr.on('data', (chunk: Buffer) => {
|
|
48
|
+
stderr += chunk.toString()
|
|
49
|
+
if (options.verbose) {
|
|
50
|
+
process.stderr.write(chunk)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
proc.on('close', (code) => {
|
|
55
|
+
const output = [stdout, stderr].filter(Boolean).join('\n')
|
|
56
|
+
resolve({
|
|
57
|
+
success: code === 0,
|
|
58
|
+
output: output.slice(0, 10000), // Cap output size
|
|
59
|
+
exitCode: code ?? -1,
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
proc.on('error', (err) => {
|
|
64
|
+
resolve({
|
|
65
|
+
success: false,
|
|
66
|
+
output: `Failed to spawn opencode: ${err.message}`,
|
|
67
|
+
exitCode: -1,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Store process ref for potential timeout kill
|
|
72
|
+
task._process = proc
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Kill the process associated with a task (used by timeout enforcement).
|
|
78
|
+
*/
|
|
79
|
+
export function kill(task: Task): void {
|
|
80
|
+
if (task._process && !task._process.killed) {
|
|
81
|
+
task._process.kill('SIGTERM')
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
if (task._process && !task._process.killed) {
|
|
84
|
+
task._process.kill('SIGKILL')
|
|
85
|
+
}
|
|
86
|
+
}, 5000)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -529,6 +529,27 @@ describe('validateSpec — branch field', () => {
|
|
|
529
529
|
})
|
|
530
530
|
})
|
|
531
531
|
|
|
532
|
+
// ── validateSpec — per-task adapter ──────────────────────────
|
|
533
|
+
|
|
534
|
+
describe('validateSpec — per-task adapter', () => {
|
|
535
|
+
it('task.adapter must be a string', () => {
|
|
536
|
+
const result = validateSpec({
|
|
537
|
+
name: 'test',
|
|
538
|
+
tasks: [{ id: 'a', prompt: 'x', adapter: 123 }],
|
|
539
|
+
})
|
|
540
|
+
expect(result.valid).toBe(false)
|
|
541
|
+
expect(result.errors).toContainEqual(expect.stringContaining('adapter'))
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('task.adapter accepts valid string', () => {
|
|
545
|
+
const result = validateSpec({
|
|
546
|
+
name: 'test',
|
|
547
|
+
tasks: [{ id: 'a', prompt: 'x', adapter: 'opencode' }],
|
|
548
|
+
})
|
|
549
|
+
expect(result.valid).toBe(true)
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
532
553
|
// ── validateSpec — per-task model and max_retries ──────────────
|
|
533
554
|
|
|
534
555
|
describe('validateSpec — per-task model and max_retries', () => {
|
|
@@ -721,6 +742,35 @@ describe('applyDefaults — convoy spec (version: 1)', () => {
|
|
|
721
742
|
expect(spec.gates).toEqual(['npm test'])
|
|
722
743
|
expect(spec.branch).toBe('feat/convoy')
|
|
723
744
|
})
|
|
745
|
+
|
|
746
|
+
it('applies defaults.adapter to tasks without explicit adapter', () => {
|
|
747
|
+
const spec = applyDefaults({
|
|
748
|
+
name: 'test',
|
|
749
|
+
version: 1,
|
|
750
|
+
defaults: { adapter: 'opencode' },
|
|
751
|
+
tasks: [{ id: 'a', prompt: 'x' }],
|
|
752
|
+
})
|
|
753
|
+
expect(spec.tasks![0].adapter).toBe('opencode')
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
it('task-level adapter overrides defaults.adapter', () => {
|
|
757
|
+
const spec = applyDefaults({
|
|
758
|
+
name: 'test',
|
|
759
|
+
version: 1,
|
|
760
|
+
defaults: { adapter: 'opencode' },
|
|
761
|
+
tasks: [{ id: 'a', prompt: 'x', adapter: 'claude-code' }],
|
|
762
|
+
})
|
|
763
|
+
expect(spec.tasks![0].adapter).toBe('claude-code')
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
it('tasks without adapter remain undefined when no defaults', () => {
|
|
767
|
+
const spec = applyDefaults({
|
|
768
|
+
name: 'test',
|
|
769
|
+
version: 1,
|
|
770
|
+
tasks: [{ id: 'a', prompt: 'x' }],
|
|
771
|
+
})
|
|
772
|
+
expect(spec.tasks![0].adapter).toBeUndefined()
|
|
773
|
+
})
|
|
724
774
|
})
|
|
725
775
|
|
|
726
776
|
// ── applyDefaults — max_retries default always applied ─────────
|
package/src/cli/run/schema.ts
CHANGED
|
@@ -55,6 +55,7 @@ interface RawTask {
|
|
|
55
55
|
description?: unknown
|
|
56
56
|
model?: unknown
|
|
57
57
|
max_retries?: unknown
|
|
58
|
+
adapter?: unknown
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -126,6 +127,9 @@ export function validateSpec(spec: unknown): ValidationResult {
|
|
|
126
127
|
if (d.agent !== undefined && typeof d.agent !== 'string') {
|
|
127
128
|
errors.push('`defaults.agent` must be a string')
|
|
128
129
|
}
|
|
130
|
+
if (d.adapter !== undefined && typeof d.adapter !== 'string') {
|
|
131
|
+
errors.push('`defaults.adapter` must be a string')
|
|
132
|
+
}
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -219,6 +223,11 @@ export function validateSpec(spec: unknown): ValidationResult {
|
|
|
219
223
|
)
|
|
220
224
|
}
|
|
221
225
|
}
|
|
226
|
+
|
|
227
|
+
// adapter
|
|
228
|
+
if (task.adapter !== undefined && typeof task.adapter !== 'string') {
|
|
229
|
+
errors.push(`${prefix}: \`adapter\` must be a string`)
|
|
230
|
+
}
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
// DAG cycle detection
|
|
@@ -308,6 +317,10 @@ export function applyDefaults(spec: Record<string, unknown>): TaskSpec {
|
|
|
308
317
|
task.max_retries =
|
|
309
318
|
d.max_retries !== undefined ? Number(d.max_retries) : 1
|
|
310
319
|
}
|
|
320
|
+
// adapter: task-level overrides defaults, no hardcoded fallback (convoy-level is used at runtime)
|
|
321
|
+
if (task.adapter === undefined && d.adapter !== undefined) {
|
|
322
|
+
task.adapter = d.adapter
|
|
323
|
+
}
|
|
311
324
|
}
|
|
312
325
|
|
|
313
326
|
return s as unknown as TaskSpec
|
package/src/cli/run.ts
CHANGED
|
@@ -124,6 +124,9 @@ function printAdapterError(detectionFailed: boolean, adapterName: string): void
|
|
|
124
124
|
' The Cursor agent CLI ships with the Cursor editor.\n' +
|
|
125
125
|
' Install Cursor from https://cursor.com and ensure the\n' +
|
|
126
126
|
' "agent" command is on your PATH (Cursor > Install CLI).',
|
|
127
|
+
opencode:
|
|
128
|
+
' Install OpenCode from https://opencode.ai\n' +
|
|
129
|
+
' Ensure the "opencode" command is on your PATH.',
|
|
127
130
|
}
|
|
128
131
|
const cliName = adapterName === 'claude-code' ? 'claude' : adapterName
|
|
129
132
|
const hint = hints[adapterName] ?? ''
|
|
@@ -155,7 +158,7 @@ function printConvoyResult(result: ConvoyResult): void {
|
|
|
155
158
|
/**
|
|
156
159
|
* CLI entry point for the `run` command.
|
|
157
160
|
*/
|
|
158
|
-
export default async function run({ args }: CliContext): Promise<void> {
|
|
161
|
+
export default async function run({ args, pkgRoot }: CliContext): Promise<void> {
|
|
159
162
|
const opts = parseArgs(args)
|
|
160
163
|
|
|
161
164
|
if (opts.help) {
|
|
@@ -337,6 +340,18 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
337
340
|
if (spec.branch) console.log(` Branch: ${spec.branch}`)
|
|
338
341
|
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
339
342
|
|
|
343
|
+
const { startDashboardServer } = await import('./dashboard.js')
|
|
344
|
+
let dashboardResult: { server: import('node:http').Server } | null = null
|
|
345
|
+
try {
|
|
346
|
+
dashboardResult = await startDashboardServer({
|
|
347
|
+
pkgRoot,
|
|
348
|
+
openBrowser: true,
|
|
349
|
+
convoyId: 'active',
|
|
350
|
+
})
|
|
351
|
+
} catch {
|
|
352
|
+
// Dashboard failure must not block convoy
|
|
353
|
+
}
|
|
354
|
+
|
|
340
355
|
const engine = createConvoyEngine({
|
|
341
356
|
spec,
|
|
342
357
|
specYaml: specText,
|
|
@@ -346,6 +361,9 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
346
361
|
|
|
347
362
|
const result = await engine.run()
|
|
348
363
|
printConvoyResult(result)
|
|
364
|
+
if (dashboardResult) {
|
|
365
|
+
dashboardResult.server.close()
|
|
366
|
+
}
|
|
349
367
|
process.exit(result.status !== 'done' ? 1 : 0)
|
|
350
368
|
}
|
|
351
369
|
|
package/src/cli/types.ts
CHANGED
|
@@ -132,6 +132,7 @@ export interface TaskDefaults {
|
|
|
132
132
|
model?: string;
|
|
133
133
|
max_retries?: number;
|
|
134
134
|
agent?: string;
|
|
135
|
+
adapter?: string;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/** Validated task spec from YAML. */
|
|
@@ -166,6 +167,8 @@ export interface Task {
|
|
|
166
167
|
model?: string;
|
|
167
168
|
/** Max retry attempts (default: 1). */
|
|
168
169
|
max_retries: number;
|
|
170
|
+
/** Per-task adapter override. */
|
|
171
|
+
adapter?: string;
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
/** Task execution status. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg-primary: #0a0a0f;--bg-secondary: #111118;--bg-tertiary: #1a1a24;--bg-card: rgba(255, 255, 255, .03);--bg-card-hover: rgba(255, 255, 255, .06);--text-primary: #f0f0f5;--text-secondary: #8a8a9a;--text-tertiary: #5a5a6e;--text-accent: #a78bfa;--gradient-accent: linear-gradient(135deg, #a78bfa 0%, #6366f1 50%, #3b82f6 100%);--gradient-glow: radial-gradient(ellipse 800px 400px at 50% 0%, rgba(99, 102, 241, .12) 0%, transparent 70%);--border-color: rgba(255, 255, 255, .06);--border-accent: rgba(167, 139, 250, .3);--color-success: #22c55e;--color-partial: #f59e0b;--color-failed: #ef4444;--color-redirected: #64748b;--color-premium: #f59e0b;--color-standard: #a78bfa;--color-utility: #3b82f6;--color-economy: #64748b;--accent-blue: #3b82f6;--accent-purple: #a78bfa;--accent-indigo: #6366f1;--max-width: 1280px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .3s cubic-bezier(.4, 0, .2, 1)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Inter,Roboto,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;overflow-x:hidden;min-height:100vh}.dash-header{position:sticky;top:0;z-index:50;background:#0a0a0fd9;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border-color)}.dash-header__inner{max-width:var(--max-width);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}.dash-header__brand{display:flex;align-items:center;gap:10px}.dash-header__icon{width:32px;height:32px;border-radius:8px;object-fit:contain}.dash-header__title{font-size:1rem;font-weight:600;color:var(--text-primary)}.dash-layout{display:flex;max-width:var(--max-width);margin:0 auto;position:relative}.dash-sidebar{position:sticky;top:56px;height:calc(100vh - 56px);width:180px;flex-shrink:0;padding:24px 0 24px 24px;overflow-y:auto;display:none}@media(min-width:1024px){.dash-sidebar{display:block}}.dash-sidebar__list{list-style:none;display:flex;flex-direction:column;gap:2px}.dash-sidebar__link{display:block;padding:8px 16px;font-size:.8125rem;font-weight:500;color:var(--text-tertiary);text-decoration:none;border-radius:8px;transition:color var(--transition-fast),background var(--transition-fast)}.dash-sidebar__link:hover{color:var(--text-secondary);background:#ffffff0a}.dash-sidebar__link--active{color:var(--text-accent);background:#a78bfa14;font-weight:600}.dash-main{flex:1;min-width:0;max-width:var(--max-width);margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;position:relative}.dash-main:before{content:"";position:fixed;top:0;left:50%;transform:translate(-50%);width:100%;height:600px;background:var(--gradient-glow);pointer-events:none;z-index:0}.dash-main>*{position:relative;z-index:1}[data-nav-section]{scroll-margin-top:72px}.kpi-row{display:grid;grid-template-columns:1fr;gap:12px}@media(min-width:480px){.kpi-row{grid-template-columns:repeat(2,1fr)}}@media(min-width:960px){.kpi-row{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}}.kpi-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;padding:20px 24px;display:flex;flex-direction:column;gap:4px;transition:border-color var(--transition-fast)}.kpi-card:hover{border-color:#ffffff1a}.kpi-card__label{font-size:.75rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.kpi-card__value{font-size:2rem;font-weight:700;color:var(--text-primary);line-height:1.2;letter-spacing:-.02em}.kpi-card__sub{font-size:.75rem;color:var(--text-secondary);display:flex;align-items:center;gap:4px}.kpi-trend{font-weight:600}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-failed)}.kpi-trend--neutral{color:var(--text-tertiary)}.chart-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;transition:border-color var(--transition-fast)}.chart-card:hover{border-color:#ffffff1a}.chart-card__header{padding:20px 24px 8px}.chart-card__title{font-size:.9375rem;font-weight:600;color:var(--text-primary)}.chart-card__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.chart-card__body{padding:16px 24px 24px;min-height:120px}.chart-card__body--table{padding:0}.charts-row{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:768px){.charts-row{grid-template-columns:repeat(2,1fr)}}.bar-row{display:flex;align-items:center;gap:12px;padding:6px 0}.bar-row+.bar-row{border-top:1px solid rgba(255,255,255,.03)}.bar-label{font-size:.8125rem;color:var(--text-secondary);width:130px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-track{flex:1;height:24px;background:var(--bg-tertiary);border-radius:6px;display:flex;overflow:hidden}.bar-segment{height:100%;transition:width .8s cubic-bezier(.4,0,.2,1);min-width:0}.bar--success{background:var(--color-success)}.bar--partial{background:var(--color-partial)}.bar--failed{background:var(--color-failed)}.bar--premium{background:var(--color-premium)}.bar--standard{background:var(--color-standard)}.bar--utility{background:var(--color-utility)}.bar--economy{background:var(--color-economy)}.bar--accent{background:var(--accent-blue)}.bar-value{font-size:.8125rem;font-weight:600;color:var(--text-primary);width:36px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums}.donut-container{display:flex;align-items:center;justify-content:center;gap:32px;flex-wrap:wrap}.donut-wrap{position:relative;width:180px;height:180px;flex-shrink:0}.donut-svg{width:100%;height:100%}.donut-svg circle{transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1),stroke-dashoffset .8s cubic-bezier(.4,0,.2,1)}.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.donut-total{display:block;font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.donut-total-label{display:block;font-size:.6875rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}.donut-legend{display:flex;flex-direction:column;gap:10px}.legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem}.legend-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}.legend-name{color:var(--text-secondary);text-transform:capitalize}.legend-count{color:var(--text-tertiary);font-variant-numeric:tabular-nums;margin-left:auto}.timeline-svg{width:100%;height:auto;display:block}.timeline-svg text{font-family:inherit}.timeline-legend{display:flex;gap:16px;justify-content:center;margin-top:12px}.timeline-legend__item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--text-tertiary)}.timeline-legend__dot{width:8px;height:8px;border-radius:2px}.pipeline{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 0}.pipeline-stage{flex:1;min-width:140px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;position:relative}.pipeline-stage:not(:last-child):after{content:"";position:absolute;right:-1px;top:50%;transform:translateY(-50%);width:2px;height:40%;background:var(--border-color)}.pipeline-stage__icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1rem}.pipeline-stage__icon--pending{background:#64748b26;color:#94a3b8;border:1px solid rgba(100,116,139,.2)}.pipeline-stage__icon--active{background:#3b82f626;color:#60a5fa;border:1px solid rgba(59,130,246,.3);animation:pulse-glow 2s ease-in-out infinite}.pipeline-stage__icon--review{background:#f59e0b26;color:#fbbf24;border:1px solid rgba(245,158,11,.3)}.pipeline-stage__icon--done{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}@keyframes pulse-glow{0%,to{box-shadow:0 0 #3b82f633}50%{box-shadow:0 0 12px 4px #3b82f626}}.pipeline-stage__count{font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.pipeline-stage__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.pipeline-arrow{display:flex;align-items:center;color:var(--text-tertiary);font-size:1.25rem;padding:0 4px;flex-shrink:0}.exec-log{display:flex;flex-direction:column}.exec-step{display:flex;gap:16px;padding:14px 0;position:relative}.exec-step+.exec-step{border-top:1px solid rgba(255,255,255,.03)}.exec-step__indicator{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:32px}.exec-step__dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6875rem;font-weight:700;flex-shrink:0}.exec-step__dot--success{background:#22c55e26;color:var(--color-success);border:1.5px solid rgba(34,197,94,.3)}.exec-step__dot--partial{background:#f59e0b26;color:var(--color-partial);border:1.5px solid rgba(245,158,11,.3)}.exec-step__dot--failed{background:#ef444426;color:var(--color-failed);border:1.5px solid rgba(239,68,68,.3)}.exec-step__line{flex:1;width:1.5px;background:var(--border-color);margin-top:4px}.exec-step__content{flex:1;min-width:0}.exec-step__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.exec-step__agent{font-size:.875rem;font-weight:600;color:var(--text-primary)}.exec-step__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.exec-step__badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.exec-step__badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.exec-step__badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.exec-step__task{font-size:.8125rem;color:var(--text-secondary);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.exec-step__meta{display:flex;gap:16px;margin-top:6px;font-size:.6875rem;color:var(--text-tertiary)}.exec-step__meta-item{display:flex;align-items:center;gap:4px}.panel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.panel-item{background:var(--bg-tertiary);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px;border:1px solid transparent;transition:border-color var(--transition-fast)}.panel-item:hover{border-color:var(--border-color)}.panel-item__header{display:flex;align-items:center;justify-content:space-between}.panel-item__key{font-size:.8125rem;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-item__verdict{font-size:.6875rem;font-weight:700;padding:2px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em}.panel-item__verdict--pass{background:#22c55e26;color:var(--color-success)}.panel-item__verdict--block{background:#ef444426;color:var(--color-failed)}.panel-item__votes{display:flex;gap:4px}.panel-item__vote{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.625rem;font-weight:700}.panel-item__vote--pass{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.panel-item__vote--block{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.panel-item__fixes{font-size:.6875rem;color:var(--text-tertiary)}.panel-item__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}.panel-item__meta-item{font-size:.625rem;color:var(--text-tertiary);white-space:nowrap}.sessions-table{width:100%;border-collapse:collapse;font-size:.8125rem}.sessions-table thead{position:sticky;top:0}.sessions-table th{padding:12px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.sessions-table th:last-child,.sessions-table td:last-child{text-align:right}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5){text-align:right}.sessions-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.sessions-table tr:hover td{background:#ffffff05}.sessions-table .td-agent{font-weight:500;color:var(--text-primary)}.sessions-table .td-task{max-width:260px;overflow:hidden;text-overflow:ellipsis}.outcome-badge{display:inline-flex;align-items:center;padding:3px 10px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.outcome-badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.outcome-badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.outcome-badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.td-num{font-variant-numeric:tabular-nums;text-align:right}.td-issue{font-size:.75rem;color:var(--text-accent);font-weight:500;font-variant-numeric:tabular-nums}.loading-skeleton{display:flex;align-items:center;justify-content:center;min-height:200px;color:var(--text-tertiary);font-size:.8125rem}.loading-skeleton:after{content:"Loading data…";animation:fade-pulse 1.5s ease-in-out infinite}@keyframes fade-pulse{0%,to{opacity:.4}50%{opacity:1}}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;gap:12px}.empty-state__icon{font-size:2rem;opacity:.4}.empty-state__text{font-size:.875rem;color:var(--text-tertiary);max-width:320px}.empty-state--enhanced{padding:56px 32px;gap:16px;border:1px dashed rgba(167,139,250,.15);border-radius:12px;background:radial-gradient(ellipse 300px 200px at 50% 30%,rgba(99,102,241,.04) 0%,transparent 70%),var(--bg-tertiary);position:relative;overflow:hidden}.empty-state--enhanced:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 23px,rgba(255,255,255,.015) 23px,rgba(255,255,255,.015) 24px);pointer-events:none}.empty-state__icon-wrap{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#a78bfa0f;border:1px solid rgba(167,139,250,.12);color:var(--text-accent);animation:empty-breathe 4s ease-in-out infinite}@keyframes empty-breathe{0%,to{box-shadow:0 0 #a78bfa14;transform:scale(1)}50%{box-shadow:0 0 20px 4px #a78bfa0f;transform:scale(1.03)}}.empty-state__title{font-size:.9375rem;font-weight:600;color:var(--text-secondary);letter-spacing:-.01em}.empty-state__desc{font-size:.8125rem;color:var(--text-tertiary);max-width:380px;line-height:1.55}.kpi-card__hint{color:var(--text-tertiary);font-style:italic;font-size:.6875rem}.kpi-row--empty .kpi-card{border-style:dashed;border-color:#ffffff0a}.kpi-row--empty .kpi-card__value{color:var(--text-tertiary);opacity:.5}.welcome-banner{position:relative;background:var(--bg-secondary);border:1px solid transparent;border-radius:16px;padding:48px 40px;overflow:hidden;z-index:1}.welcome-banner:before{content:"";position:absolute;inset:-1px;border-radius:16px;padding:1px;background:linear-gradient(135deg,#a78bfa4d,#6366f126,#3b82f61a 60%,#a78bfa33);-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:0}.welcome-banner__glow{position:absolute;top:-60px;left:50%;transform:translate(-50%);width:500px;height:300px;background:radial-gradient(ellipse at center,rgba(167,139,250,.08) 0%,rgba(99,102,241,.04) 40%,transparent 70%);pointer-events:none;z-index:0}.welcome-banner__content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:20px}.welcome-banner__icon{width:72px;height:72px;display:flex;align-items:center;justify-content:center;border-radius:20px;background:#a78bfa14;border:1px solid rgba(167,139,250,.15);color:var(--text-accent);animation:welcome-float 6s ease-in-out infinite}@keyframes welcome-float{0%,to{transform:translateY(0);box-shadow:0 8px 32px #a78bfa14}50%{transform:translateY(-6px);box-shadow:0 16px 48px #a78bfa1f}}.welcome-banner__title{font-size:1.375rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.3}.welcome-banner__subtitle{font-size:.9375rem;color:var(--text-secondary);max-width:480px;line-height:1.6}.welcome-banner__steps{display:flex;gap:20px;margin-top:12px;flex-wrap:wrap;justify-content:center}.welcome-step{display:flex;align-items:flex-start;gap:12px;text-align:left;padding:16px 20px;background:#ffffff05;border:1px solid rgba(255,255,255,.05);border-radius:12px;min-width:200px;max-width:220px;transition:border-color var(--transition-fast),background var(--transition-fast)}.welcome-step:hover{border-color:#a78bfa26;background:#ffffff08}.welcome-step__num{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;color:var(--text-accent);background:#a78bfa1a;border:1px solid rgba(167,139,250,.2);flex-shrink:0}.welcome-step__text{display:flex;flex-direction:column;gap:3px}.welcome-step__text strong{font-size:.8125rem;font-weight:600;color:var(--text-primary)}.welcome-step__text span{font-size:.75rem;color:var(--text-tertiary);line-height:1.4}@media(max-width:640px){.welcome-banner{padding:32px 24px}.welcome-banner__steps{flex-direction:column;align-items:center}.welcome-step{max-width:100%;width:100%}}@keyframes slide-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.dash-main>*{animation:slide-up .5s ease-out backwards}.dash-main>*:nth-child(1){animation-delay:0ms}.dash-main>*:nth-child(2){animation-delay:60ms}.dash-main>*:nth-child(3){animation-delay:.12s}.dash-main>*:nth-child(4){animation-delay:.18s}.dash-main>*:nth-child(5){animation-delay:.24s}.dash-main>*:nth-child(6){animation-delay:.3s}.dash-main>*:nth-child(7){animation-delay:.36s}.dash-main>*:nth-child(8){animation-delay:.42s}.dash-main>*:nth-child(9){animation-delay:.48s}.dash-main>*:nth-child(10){animation-delay:.54s}.dash-main>*:nth-child(11){animation-delay:.6s}@media(max-width:640px){.bar-label{width:90px;font-size:.75rem}.donut-container{flex-direction:column;align-items:center}.donut-wrap{width:150px;height:150px}.pipeline{gap:0}.pipeline-stage{min-width:100px;padding:12px 8px}.panel-grid{grid-template-columns:1fr}.sessions-table th:nth-child(3),.sessions-table td:nth-child(3){display:none}}.filter-bar{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;padding:16px 20px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px}.filter-group{display:flex;flex-direction:column;gap:4px;min-width:0}.filter-label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.filter-input,.filter-select{height:34px;padding:0 10px;font-size:.8125rem;color:var(--text-primary);background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;outline:none;transition:border-color var(--transition-fast);font-family:inherit}.filter-input:focus,.filter-select:focus{border-color:var(--border-accent)}.filter-input{width:140px;color-scheme:dark}.filter-select{min-width:140px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235a5a6e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}.filter-reset{height:34px;font-size:.75rem}.dash-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;font-size:.8125rem;font-weight:500;font-family:inherit;border:none;border-radius:8px;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast)}.dash-btn--ghost{color:var(--text-secondary);background:#ffffff0f}.dash-btn--ghost:hover{color:var(--text-primary);background:#ffffff1a}.dash-header__actions{display:flex;align-items:center;gap:8px}@media(max-width:480px){.dash-header__inner{padding:0 12px}.dash-main{padding:12px;gap:12px}.kpi-card,.chart-card__header{padding:14px 16px}.chart-card__body{padding:12px 16px 16px}.filter-bar{padding:12px;gap:8px}.filter-input,.filter-select{width:100%;min-width:unset}.filter-group{flex:1 1 calc(50% - 4px)}.filter-reset{width:100%}.dash-header__title{font-size:.875rem}.exec-step__meta{flex-direction:column;gap:2px}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5),.sessions-table th:nth-child(6),.sessions-table td:nth-child(6),.sessions-table th:nth-child(7),.sessions-table td:nth-child(7),.sessions-table th:nth-child(8),.sessions-table td:nth-child(8){display:none}}@media(max-width:768px){.charts-row{grid-template-columns:1fr}.pipeline{flex-wrap:wrap;gap:8px}.pipeline-arrow{display:none}.pipeline-stage{flex:1 1 calc(50% - 4px);min-width:100px}.tier-chart .donut-container,.donut-container{flex-direction:column;align-items:center}.sessions-table{font-size:.75rem}.sessions-table th,.sessions-table td{padding:8px 6px}}
|
|
1
|
+
:root{--bg-primary: #0a0a0f;--bg-secondary: #111118;--bg-tertiary: #1a1a24;--bg-card: rgba(255, 255, 255, .03);--bg-card-hover: rgba(255, 255, 255, .06);--text-primary: #f0f0f5;--text-secondary: #8a8a9a;--text-tertiary: #5a5a6e;--text-accent: #a78bfa;--gradient-accent: linear-gradient(135deg, #a78bfa 0%, #6366f1 50%, #3b82f6 100%);--gradient-glow: radial-gradient(ellipse 800px 400px at 50% 0%, rgba(99, 102, 241, .12) 0%, transparent 70%);--border-color: rgba(255, 255, 255, .06);--border-accent: rgba(167, 139, 250, .3);--color-success: #22c55e;--color-partial: #f59e0b;--color-failed: #ef4444;--color-redirected: #64748b;--color-premium: #f59e0b;--color-standard: #a78bfa;--color-utility: #3b82f6;--color-economy: #64748b;--accent-blue: #3b82f6;--accent-purple: #a78bfa;--accent-indigo: #6366f1;--max-width: 1280px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .3s cubic-bezier(.4, 0, .2, 1)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Inter,Roboto,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;overflow-x:hidden;min-height:100vh}.dash-header{position:sticky;top:0;z-index:50;background:#0a0a0fd9;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border-color)}.dash-header__inner{max-width:var(--max-width);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}.dash-header__brand{display:flex;align-items:center;gap:10px}.dash-header__icon{width:32px;height:32px;border-radius:8px;object-fit:contain}.dash-header__title{font-size:1rem;font-weight:600;color:var(--text-primary)}.dash-layout{display:flex;max-width:var(--max-width);margin:0 auto;position:relative}.dash-sidebar{position:sticky;top:56px;height:calc(100vh - 56px);width:180px;flex-shrink:0;padding:24px 0 24px 24px;overflow-y:auto;display:none}@media(min-width:1024px){.dash-sidebar{display:block}}.dash-sidebar__list{list-style:none;display:flex;flex-direction:column;gap:2px}.dash-sidebar__link{display:block;padding:8px 16px;font-size:.8125rem;font-weight:500;color:var(--text-tertiary);text-decoration:none;border-radius:8px;transition:color var(--transition-fast),background var(--transition-fast)}.dash-sidebar__link:hover{color:var(--text-secondary);background:#ffffff0a}.dash-sidebar__link--active{color:var(--text-accent);background:#a78bfa14;font-weight:600}.dash-main{flex:1;min-width:0;max-width:var(--max-width);margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;position:relative}.dash-main:before{content:"";position:fixed;top:0;left:50%;transform:translate(-50%);width:100%;height:600px;background:var(--gradient-glow);pointer-events:none;z-index:0}.dash-main>*{position:relative;z-index:1}[data-nav-section]{scroll-margin-top:72px}.kpi-row{display:grid;grid-template-columns:1fr;gap:12px}@media(min-width:480px){.kpi-row{grid-template-columns:repeat(2,1fr)}}@media(min-width:960px){.kpi-row{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}}.kpi-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;padding:20px 24px;display:flex;flex-direction:column;gap:4px;transition:border-color var(--transition-fast)}.kpi-card:hover{border-color:#ffffff1a}.kpi-card__label{font-size:.75rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.kpi-card__value{font-size:2rem;font-weight:700;color:var(--text-primary);line-height:1.2;letter-spacing:-.02em}.kpi-card__sub{font-size:.75rem;color:var(--text-secondary);display:flex;align-items:center;gap:4px}.kpi-trend{font-weight:600}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-failed)}.kpi-trend--neutral{color:var(--text-tertiary)}.chart-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;transition:border-color var(--transition-fast)}.chart-card:hover{border-color:#ffffff1a}.chart-card__header{padding:20px 24px 8px}.chart-card__title{font-size:.9375rem;font-weight:600;color:var(--text-primary)}.chart-card__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.chart-card__body{padding:16px 24px 24px;min-height:120px}.chart-card__body--table{padding:0}.charts-row{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:768px){.charts-row{grid-template-columns:repeat(2,1fr)}}.bar-row{display:flex;align-items:center;gap:12px;padding:6px 0}.bar-row+.bar-row{border-top:1px solid rgba(255,255,255,.03)}.bar-label{font-size:.8125rem;color:var(--text-secondary);width:130px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-track{flex:1;height:24px;background:var(--bg-tertiary);border-radius:6px;display:flex;overflow:hidden}.bar-segment{height:100%;transition:width .8s cubic-bezier(.4,0,.2,1);min-width:0}.bar--success{background:var(--color-success)}.bar--partial{background:var(--color-partial)}.bar--failed{background:var(--color-failed)}.bar--premium{background:var(--color-premium)}.bar--standard{background:var(--color-standard)}.bar--utility{background:var(--color-utility)}.bar--economy{background:var(--color-economy)}.bar--accent{background:var(--accent-blue)}.bar-value{font-size:.8125rem;font-weight:600;color:var(--text-primary);width:36px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums}.donut-container{display:flex;align-items:center;justify-content:center;gap:32px;flex-wrap:wrap}.donut-wrap{position:relative;width:180px;height:180px;flex-shrink:0}.donut-svg{width:100%;height:100%}.donut-svg circle{transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1),stroke-dashoffset .8s cubic-bezier(.4,0,.2,1)}.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.donut-total{display:block;font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.donut-total-label{display:block;font-size:.6875rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}.donut-legend{display:flex;flex-direction:column;gap:10px}.legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem}.legend-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}.legend-name{color:var(--text-secondary);text-transform:capitalize}.legend-count{color:var(--text-tertiary);font-variant-numeric:tabular-nums;margin-left:auto}.timeline-svg{width:100%;height:auto;display:block}.timeline-svg text{font-family:inherit}.timeline-legend{display:flex;gap:16px;justify-content:center;margin-top:12px}.timeline-legend__item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--text-tertiary)}.timeline-legend__dot{width:8px;height:8px;border-radius:2px}.pipeline{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 0}.pipeline-stage{flex:1;min-width:140px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;position:relative}.pipeline-stage:not(:last-child):after{content:"";position:absolute;right:-1px;top:50%;transform:translateY(-50%);width:2px;height:40%;background:var(--border-color)}.pipeline-stage__icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1rem}.pipeline-stage__icon--pending{background:#64748b26;color:#94a3b8;border:1px solid rgba(100,116,139,.2)}.pipeline-stage__icon--active{background:#3b82f626;color:#60a5fa;border:1px solid rgba(59,130,246,.3);animation:pulse-glow 2s ease-in-out infinite}.pipeline-stage__icon--review{background:#f59e0b26;color:#fbbf24;border:1px solid rgba(245,158,11,.3)}.pipeline-stage__icon--done{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}@keyframes pulse-glow{0%,to{box-shadow:0 0 #3b82f633}50%{box-shadow:0 0 12px 4px #3b82f626}}.pipeline-stage__count{font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.pipeline-stage__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.pipeline-arrow{display:flex;align-items:center;color:var(--text-tertiary);font-size:1.25rem;padding:0 4px;flex-shrink:0}.exec-log{display:flex;flex-direction:column}.exec-step{display:flex;gap:16px;padding:14px 0;position:relative}.exec-step+.exec-step{border-top:1px solid rgba(255,255,255,.03)}.exec-step__indicator{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:32px}.exec-step__dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6875rem;font-weight:700;flex-shrink:0}.exec-step__dot--success{background:#22c55e26;color:var(--color-success);border:1.5px solid rgba(34,197,94,.3)}.exec-step__dot--partial{background:#f59e0b26;color:var(--color-partial);border:1.5px solid rgba(245,158,11,.3)}.exec-step__dot--failed{background:#ef444426;color:var(--color-failed);border:1.5px solid rgba(239,68,68,.3)}.exec-step__line{flex:1;width:1.5px;background:var(--border-color);margin-top:4px}.exec-step__content{flex:1;min-width:0}.exec-step__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.exec-step__agent{font-size:.875rem;font-weight:600;color:var(--text-primary)}.exec-step__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.exec-step__badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.exec-step__badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.exec-step__badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.exec-step__task{font-size:.8125rem;color:var(--text-secondary);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.exec-step__meta{display:flex;gap:16px;margin-top:6px;font-size:.6875rem;color:var(--text-tertiary)}.exec-step__meta-item{display:flex;align-items:center;gap:4px}.panel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.panel-item{background:var(--bg-tertiary);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px;border:1px solid transparent;transition:border-color var(--transition-fast)}.panel-item:hover{border-color:var(--border-color)}.panel-item__header{display:flex;align-items:center;justify-content:space-between}.panel-item__key{font-size:.8125rem;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-item__verdict{font-size:.6875rem;font-weight:700;padding:2px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em}.panel-item__verdict--pass{background:#22c55e26;color:var(--color-success)}.panel-item__verdict--block{background:#ef444426;color:var(--color-failed)}.panel-item__votes{display:flex;gap:4px}.panel-item__vote{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.625rem;font-weight:700}.panel-item__vote--pass{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.panel-item__vote--block{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.panel-item__fixes{font-size:.6875rem;color:var(--text-tertiary)}.panel-item__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}.panel-item__meta-item{font-size:.625rem;color:var(--text-tertiary);white-space:nowrap}.sessions-table{width:100%;border-collapse:collapse;font-size:.8125rem}.sessions-table thead{position:sticky;top:0}.sessions-table th{padding:12px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.sessions-table th:last-child,.sessions-table td:last-child{text-align:right}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5){text-align:right}.sessions-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.sessions-table tr:hover td{background:#ffffff05}.sessions-table .td-agent{font-weight:500;color:var(--text-primary)}.sessions-table .td-task{max-width:260px;overflow:hidden;text-overflow:ellipsis}.outcome-badge{display:inline-flex;align-items:center;padding:3px 10px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.outcome-badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.outcome-badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.outcome-badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.td-num{font-variant-numeric:tabular-nums;text-align:right}.td-issue{font-size:.75rem;color:var(--text-accent);font-weight:500;font-variant-numeric:tabular-nums}.loading-skeleton{display:flex;align-items:center;justify-content:center;min-height:200px;color:var(--text-tertiary);font-size:.8125rem}.loading-skeleton:after{content:"Loading data…";animation:fade-pulse 1.5s ease-in-out infinite}@keyframes fade-pulse{0%,to{opacity:.4}50%{opacity:1}}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;gap:12px}.empty-state__icon{font-size:2rem;opacity:.4}.empty-state__text{font-size:.875rem;color:var(--text-tertiary);max-width:320px}.empty-state--enhanced{padding:56px 32px;gap:16px;border:1px dashed rgba(167,139,250,.15);border-radius:12px;background:radial-gradient(ellipse 300px 200px at 50% 30%,rgba(99,102,241,.04) 0%,transparent 70%),var(--bg-tertiary);position:relative;overflow:hidden}.empty-state--enhanced:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 23px,rgba(255,255,255,.015) 23px,rgba(255,255,255,.015) 24px);pointer-events:none}.empty-state__icon-wrap{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#a78bfa0f;border:1px solid rgba(167,139,250,.12);color:var(--text-accent);animation:empty-breathe 4s ease-in-out infinite}@keyframes empty-breathe{0%,to{box-shadow:0 0 #a78bfa14;transform:scale(1)}50%{box-shadow:0 0 20px 4px #a78bfa0f;transform:scale(1.03)}}.empty-state__title{font-size:.9375rem;font-weight:600;color:var(--text-secondary);letter-spacing:-.01em}.empty-state__desc{font-size:.8125rem;color:var(--text-tertiary);max-width:380px;line-height:1.55}.kpi-card__hint{color:var(--text-tertiary);font-style:italic;font-size:.6875rem}.kpi-row--empty .kpi-card{border-style:dashed;border-color:#ffffff0a}.kpi-row--empty .kpi-card__value{color:var(--text-tertiary);opacity:.5}.welcome-banner{position:relative;background:var(--bg-secondary);border:1px solid transparent;border-radius:16px;padding:48px 40px;overflow:hidden;z-index:1}.welcome-banner:before{content:"";position:absolute;inset:-1px;border-radius:16px;padding:1px;background:linear-gradient(135deg,#a78bfa4d,#6366f126,#3b82f61a 60%,#a78bfa33);-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:0}.welcome-banner__glow{position:absolute;top:-60px;left:50%;transform:translate(-50%);width:500px;height:300px;background:radial-gradient(ellipse at center,rgba(167,139,250,.08) 0%,rgba(99,102,241,.04) 40%,transparent 70%);pointer-events:none;z-index:0}.welcome-banner__content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:20px}.welcome-banner__icon{width:72px;height:72px;display:flex;align-items:center;justify-content:center;border-radius:20px;background:#a78bfa14;border:1px solid rgba(167,139,250,.15);color:var(--text-accent);animation:welcome-float 6s ease-in-out infinite}@keyframes welcome-float{0%,to{transform:translateY(0);box-shadow:0 8px 32px #a78bfa14}50%{transform:translateY(-6px);box-shadow:0 16px 48px #a78bfa1f}}.welcome-banner__title{font-size:1.375rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.3}.welcome-banner__subtitle{font-size:.9375rem;color:var(--text-secondary);max-width:480px;line-height:1.6}.welcome-banner__steps{display:flex;gap:20px;margin-top:12px;flex-wrap:wrap;justify-content:center}.welcome-step{display:flex;align-items:flex-start;gap:12px;text-align:left;padding:16px 20px;background:#ffffff05;border:1px solid rgba(255,255,255,.05);border-radius:12px;min-width:200px;max-width:220px;transition:border-color var(--transition-fast),background var(--transition-fast)}.welcome-step:hover{border-color:#a78bfa26;background:#ffffff08}.welcome-step__num{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;color:var(--text-accent);background:#a78bfa1a;border:1px solid rgba(167,139,250,.2);flex-shrink:0}.welcome-step__text{display:flex;flex-direction:column;gap:3px}.welcome-step__text strong{font-size:.8125rem;font-weight:600;color:var(--text-primary)}.welcome-step__text span{font-size:.75rem;color:var(--text-tertiary);line-height:1.4}@media(max-width:640px){.welcome-banner{padding:32px 24px}.welcome-banner__steps{flex-direction:column;align-items:center}.welcome-step{max-width:100%;width:100%}}@keyframes slide-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.dash-main>*{animation:slide-up .5s ease-out backwards}.dash-main>*:nth-child(1){animation-delay:0ms}.dash-main>*:nth-child(2){animation-delay:60ms}.dash-main>*:nth-child(3){animation-delay:.12s}.dash-main>*:nth-child(4){animation-delay:.18s}.dash-main>*:nth-child(5){animation-delay:.24s}.dash-main>*:nth-child(6){animation-delay:.3s}.dash-main>*:nth-child(7){animation-delay:.36s}.dash-main>*:nth-child(8){animation-delay:.42s}.dash-main>*:nth-child(9){animation-delay:.48s}.dash-main>*:nth-child(10){animation-delay:.54s}.dash-main>*:nth-child(11){animation-delay:.6s}@media(max-width:640px){.bar-label{width:90px;font-size:.75rem}.donut-container{flex-direction:column;align-items:center}.donut-wrap{width:150px;height:150px}.pipeline{gap:0}.pipeline-stage{min-width:100px;padding:12px 8px}.panel-grid{grid-template-columns:1fr}.sessions-table th:nth-child(3),.sessions-table td:nth-child(3){display:none}}.filter-bar{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;padding:16px 20px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px}.filter-group{display:flex;flex-direction:column;gap:4px;min-width:0}.filter-label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.filter-input,.filter-select{height:34px;padding:0 10px;font-size:.8125rem;color:var(--text-primary);background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;outline:none;transition:border-color var(--transition-fast);font-family:inherit}.filter-input:focus,.filter-select:focus{border-color:var(--border-accent)}.filter-input{width:140px;color-scheme:dark}.filter-select{min-width:140px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235a5a6e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}.filter-reset{height:34px;font-size:.75rem}.dash-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;font-size:.8125rem;font-weight:500;font-family:inherit;border:none;border-radius:8px;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast)}.dash-btn--ghost{color:var(--text-secondary);background:#ffffff0f}.dash-btn--ghost:hover{color:var(--text-primary);background:#ffffff1a}.dash-header__actions{display:flex;align-items:center;gap:8px}@media(max-width:480px){.dash-header__inner{padding:0 12px}.dash-main{padding:12px;gap:12px}.kpi-card,.chart-card__header{padding:14px 16px}.chart-card__body{padding:12px 16px 16px}.filter-bar{padding:12px;gap:8px}.filter-input,.filter-select{width:100%;min-width:unset}.filter-group{flex:1 1 calc(50% - 4px)}.filter-reset{width:100%}.dash-header__title{font-size:.875rem}.exec-step__meta{flex-direction:column;gap:2px}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5),.sessions-table th:nth-child(6),.sessions-table td:nth-child(6),.sessions-table th:nth-child(7),.sessions-table td:nth-child(7),.sessions-table th:nth-child(8),.sessions-table td:nth-child(8){display:none}}@media(max-width:768px){.charts-row{grid-template-columns:1fr}.pipeline{flex-wrap:wrap;gap:8px}.pipeline-arrow{display:none}.pipeline-stage{flex:1 1 calc(50% - 4px);min-width:100px}.tier-chart .donut-container,.donut-container{flex-direction:column;align-items:center}.sessions-table{font-size:.75rem}.sessions-table th,.sessions-table td{padding:8px 6px}}.convoy-overview{display:flex;flex-wrap:wrap;gap:24px;margin-bottom:20px}.convoy-stat{display:flex;flex-direction:column;gap:4px}.convoy-stat__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.convoy-stat__value{font-size:.95rem;color:var(--text-primary)}.convoy-progress{display:flex;align-items:center;gap:12px;margin-bottom:20px}.convoy-progress__bar{flex:1;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden}.convoy-progress__fill{height:100%;background:var(--gradient-accent);border-radius:4px;transition:width var(--transition-base)}.convoy-progress__label{font-size:.8rem;color:var(--text-secondary);white-space:nowrap}.convoy-tasks{margin-top:8px}
|