coding-tool-x 3.3.9 → 3.4.1
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/web/assets/{Analytics-D6LzK9hk.js → Analytics-CbGxotgz.js} +4 -4
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-BUDYuxRi.js → ConfigTemplates-oP6nrFEb.js} +1 -1
- package/dist/web/assets/{Home-D7KX7iF8.js → Home-DMntmEvh.js} +1 -1
- package/dist/web/assets/{PluginManager-DTgQ--vB.js → PluginManager-BUC_c7nH.js} +1 -1
- package/dist/web/assets/{ProjectList-DMCiGmCT.js → ProjectList-CW8J49n7.js} +1 -1
- package/dist/web/assets/{SessionList-CRBsdVRe.js → SessionList-7lYnF92v.js} +1 -1
- package/dist/web/assets/{SkillManager-DMwx2Q4k.js → SkillManager-Cs08216i.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DapB4ljL.js → WorkspaceManager-CY-oGtyB.js} +1 -1
- package/dist/web/assets/{index-D_5dRFOL.css → index-5qy5NMIP.css} +1 -1
- package/dist/web/assets/index-ClCqKpvX.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/src/server/api/statistics.js +4 -4
- package/src/server/api/workspaces.js +1 -3
- package/src/server/codex-proxy-server.js +4 -92
- package/src/server/gemini-proxy-server.js +5 -28
- package/src/server/opencode-proxy-server.js +3 -93
- package/src/server/proxy-server.js +2 -57
- package/src/server/services/base/base-channel-service.js +247 -0
- package/src/server/services/base/proxy-utils.js +152 -0
- package/src/server/services/channel-health.js +30 -19
- package/src/server/services/channels.js +125 -293
- package/src/server/services/codex-channels.js +149 -517
- package/src/server/services/codex-env-manager.js +100 -67
- package/src/server/services/gemini-channels.js +2 -7
- package/src/server/services/oauth-credentials-service.js +12 -2
- package/src/server/services/opencode-channels.js +7 -9
- package/src/server/services/repo-scanner-base.js +1 -0
- package/src/server/services/statistics-service.js +5 -1
- package/src/server/services/workspace-service.js +100 -155
- package/dist/web/assets/Analytics-DuYvId7u.css +0 -1
- package/dist/web/assets/index-CL-qpoJ_.js +0 -2
|
@@ -8,6 +8,24 @@ const configTemplatesService = require('./config-templates-service');
|
|
|
8
8
|
// 工作区配置文件路径
|
|
9
9
|
const WORKSPACES_CONFIG = PATHS.workspaces;
|
|
10
10
|
|
|
11
|
+
function createSymlink(target, linkPath) {
|
|
12
|
+
try {
|
|
13
|
+
fs.symlinkSync(target, linkPath, 'dir');
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err.code === 'EPERM' && process.platform === 'win32') {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`创建软链接失败:Windows 需要管理员权限或开启开发者模式。\n` +
|
|
18
|
+
`解决方案:\n` +
|
|
19
|
+
`1. 以管理员身份运行终端后重试\n` +
|
|
20
|
+
`2. 或开启开发者模式:设置 → 系统 → 开发者选项 → 开发者模式\n` +
|
|
21
|
+
`3. 对于 Git 项目,建议保持默认的 worktree 模式(无需软链接)\n` +
|
|
22
|
+
`原始错误: ${err.message}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
function runGitCommand(args, options = {}) {
|
|
12
30
|
const execOptions = {
|
|
13
31
|
encoding: 'utf8',
|
|
@@ -287,7 +305,7 @@ function createWorkspace(options) {
|
|
|
287
305
|
let targetPath = sourcePath;
|
|
288
306
|
let worktrees = [];
|
|
289
307
|
|
|
290
|
-
// Git 仓库且需要创建 worktree
|
|
308
|
+
// Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
|
|
291
309
|
if (isGit && useWorktree) {
|
|
292
310
|
// 获取当前分支作为默认分支
|
|
293
311
|
let targetBranch = branch;
|
|
@@ -299,72 +317,46 @@ function createWorkspace(options) {
|
|
|
299
317
|
}
|
|
300
318
|
}
|
|
301
319
|
|
|
302
|
-
// worktree
|
|
303
|
-
const worktreePath =
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (fs.existsSync(worktreePath)) {
|
|
310
|
-
// 已存在则直接使用
|
|
311
|
-
targetPath = worktreePath;
|
|
312
|
-
worktrees.push({
|
|
313
|
-
branch: targetBranch,
|
|
314
|
-
path: worktreePath
|
|
320
|
+
// worktree 直接创建到工作区目录内的项目路径
|
|
321
|
+
const worktreePath = symlinkPath;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
// 尝试检出已有分支
|
|
325
|
+
runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
|
|
326
|
+
cwd: sourcePath
|
|
315
327
|
});
|
|
316
|
-
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// 如果分支不存在,尝试创建新分支
|
|
317
330
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
331
|
+
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
332
|
+
if (baseBranch && baseBranch.trim()) {
|
|
333
|
+
worktreeArgs.push(baseBranch.trim());
|
|
334
|
+
}
|
|
335
|
+
runGitCommand(worktreeArgs, {
|
|
320
336
|
cwd: sourcePath
|
|
321
337
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// 如果分支不存在,尝试创建新分支
|
|
330
|
-
try {
|
|
331
|
-
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
332
|
-
if (baseBranch && baseBranch.trim()) {
|
|
333
|
-
worktreeArgs.push(baseBranch.trim());
|
|
334
|
-
}
|
|
335
|
-
runGitCommand(worktreeArgs, {
|
|
336
|
-
cwd: sourcePath
|
|
337
|
-
});
|
|
338
|
-
targetPath = worktreePath;
|
|
339
|
-
worktrees.push({
|
|
340
|
-
branch: targetBranch,
|
|
341
|
-
path: worktreePath
|
|
342
|
-
});
|
|
343
|
-
} catch (err) {
|
|
344
|
-
// Check if it's a "branch already checked out" error
|
|
345
|
-
if (err.message.includes('already checked out')) {
|
|
346
|
-
throw new Error(
|
|
347
|
-
`无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
348
|
-
`错误详情: ${err.message}\n\n` +
|
|
349
|
-
`提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// For other errors, provide clear message but allow fallback
|
|
354
|
-
console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
|
|
355
|
-
targetPath = sourcePath;
|
|
356
|
-
worktrees = getGitWorktrees(sourcePath);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (err.message.includes('already checked out')) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
342
|
+
`错误详情: ${err.message}\n\n` +
|
|
343
|
+
`提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
|
|
344
|
+
);
|
|
357
345
|
}
|
|
346
|
+
throw new Error(`创建 worktree 失败: ${err.message}`);
|
|
358
347
|
}
|
|
359
348
|
}
|
|
349
|
+
|
|
350
|
+
targetPath = worktreePath;
|
|
351
|
+
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
360
352
|
} else if (isGit) {
|
|
361
|
-
// Git 仓库但不创建 worktree
|
|
353
|
+
// Git 仓库但不创建 worktree:symlink 指向源路径,记录已有 worktrees
|
|
362
354
|
worktrees = getGitWorktrees(sourcePath);
|
|
355
|
+
createSymlink(targetPath, symlinkPath);
|
|
356
|
+
} else {
|
|
357
|
+
// 非 Git 仓库:symlink 指向源路径
|
|
358
|
+
createSymlink(targetPath, symlinkPath);
|
|
363
359
|
}
|
|
364
|
-
// 非 Git 仓库:targetPath 保持为 sourcePath,直接软链接
|
|
365
|
-
|
|
366
|
-
// 创建软链接
|
|
367
|
-
fs.symlinkSync(targetPath, symlinkPath, 'dir');
|
|
368
360
|
|
|
369
361
|
workspaceProjects.push({
|
|
370
362
|
name: symlinkName,
|
|
@@ -438,36 +430,17 @@ function deleteWorkspace(id, removeFiles = false) {
|
|
|
438
430
|
|
|
439
431
|
const workspace = data.workspaces[index];
|
|
440
432
|
|
|
441
|
-
//
|
|
433
|
+
// 注销 worktrees(worktree 目录在工作区内,git 引用需先注销)
|
|
442
434
|
for (const proj of workspace.projects) {
|
|
443
|
-
if (proj.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
console.log(`清理 worktree: ${wt.path}`);
|
|
452
|
-
runGitCommand(['worktree', 'remove', wt.path, '--force'], {
|
|
453
|
-
cwd: proj.sourcePath
|
|
454
|
-
});
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.error(`删除 worktree 失败: ${wt.path}`, error.message);
|
|
457
|
-
// 如果 git worktree remove 失败,尝试手动删除目录
|
|
458
|
-
if (fs.existsSync(wt.path)) {
|
|
459
|
-
try {
|
|
460
|
-
fs.rmSync(wt.path, { recursive: true, force: true });
|
|
461
|
-
console.log(`手动删除 worktree 目录: ${wt.path}`);
|
|
462
|
-
} catch (rmError) {
|
|
463
|
-
console.error(`手动删除 worktree 目录失败: ${wt.path}`, rmError.message);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
435
|
+
if (proj.useWorktree && proj.sourcePath && proj.targetPath) {
|
|
436
|
+
if (fs.existsSync(proj.sourcePath)) {
|
|
437
|
+
try {
|
|
438
|
+
runGitCommand(['worktree', 'remove', proj.targetPath, '--force'], {
|
|
439
|
+
cwd: proj.sourcePath
|
|
440
|
+
});
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error(`注销 worktree 失败: ${proj.targetPath}`, error.message);
|
|
468
443
|
}
|
|
469
|
-
} catch (error) {
|
|
470
|
-
console.error(`扫描 worktree 失败: ${proj.sourcePath}`, error.message);
|
|
471
444
|
}
|
|
472
445
|
}
|
|
473
446
|
}
|
|
@@ -533,9 +506,8 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
533
506
|
let targetPath = sourcePath;
|
|
534
507
|
let worktrees = [];
|
|
535
508
|
|
|
536
|
-
// Git 仓库且需要创建 worktree
|
|
509
|
+
// Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
|
|
537
510
|
if (isGit && useWorktree) {
|
|
538
|
-
// 获取当前分支作为默认分支
|
|
539
511
|
let targetBranch = branch;
|
|
540
512
|
if (!targetBranch) {
|
|
541
513
|
try {
|
|
@@ -545,73 +517,44 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
545
517
|
}
|
|
546
518
|
}
|
|
547
519
|
|
|
548
|
-
const worktreePath =
|
|
549
|
-
path.dirname(sourcePath),
|
|
550
|
-
`${path.basename(sourcePath)}-ws-${targetBranch.replace(/\//g, '-')}`
|
|
551
|
-
);
|
|
520
|
+
const worktreePath = symlinkPath;
|
|
552
521
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
522
|
+
try {
|
|
523
|
+
runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
|
|
524
|
+
cwd: sourcePath
|
|
525
|
+
});
|
|
526
|
+
} catch (error) {
|
|
558
527
|
try {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
} catch (
|
|
565
|
-
|
|
566
|
-
if (error.message && error.message.includes('already checked out')) {
|
|
528
|
+
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
529
|
+
if (baseBranch && baseBranch.trim()) {
|
|
530
|
+
worktreeArgs.push(baseBranch.trim());
|
|
531
|
+
}
|
|
532
|
+
runGitCommand(worktreeArgs, { cwd: sourcePath });
|
|
533
|
+
} catch (err) {
|
|
534
|
+
if (err.message && err.message.includes('already checked out')) {
|
|
567
535
|
throw new Error(
|
|
568
536
|
`无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
569
537
|
`仓库路径: ${sourcePath}\n\n` +
|
|
570
|
-
`Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
|
|
571
538
|
`解决方案:\n` +
|
|
572
539
|
`1. 指定不同的分支名\n` +
|
|
573
540
|
`2. 或者禁用 worktree 模式(设置 createWorktree: false)`
|
|
574
541
|
);
|
|
575
542
|
}
|
|
576
|
-
|
|
577
|
-
// Branch doesn't exist, try creating it
|
|
578
|
-
try {
|
|
579
|
-
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
580
|
-
if (baseBranch && baseBranch.trim()) {
|
|
581
|
-
worktreeArgs.push(baseBranch.trim());
|
|
582
|
-
}
|
|
583
|
-
runGitCommand(worktreeArgs, {
|
|
584
|
-
cwd: sourcePath
|
|
585
|
-
});
|
|
586
|
-
targetPath = worktreePath;
|
|
587
|
-
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
588
|
-
} catch (err) {
|
|
589
|
-
// Check for "already checked out" error in create branch attempt
|
|
590
|
-
if (err.message && err.message.includes('already checked out')) {
|
|
591
|
-
throw new Error(
|
|
592
|
-
`无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
|
|
593
|
-
`仓库路径: ${sourcePath}\n\n` +
|
|
594
|
-
`Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
|
|
595
|
-
`解决方案:\n` +
|
|
596
|
-
`1. 指定不同的分支名\n` +
|
|
597
|
-
`2. 或者禁用 worktree 模式(设置 createWorktree: false)`
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Other errors: fall back to symlink mode
|
|
602
|
-
console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
|
|
603
|
-
targetPath = sourcePath;
|
|
604
|
-
worktrees = getGitWorktrees(sourcePath);
|
|
605
|
-
}
|
|
543
|
+
throw new Error(`创建 worktree 失败: ${err.message}`);
|
|
606
544
|
}
|
|
607
545
|
}
|
|
546
|
+
|
|
547
|
+
targetPath = worktreePath;
|
|
548
|
+
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
608
549
|
} else if (isGit) {
|
|
550
|
+
// Git 仓库但不创建 worktree:symlink 指向源路径
|
|
609
551
|
worktrees = getGitWorktrees(sourcePath);
|
|
552
|
+
createSymlink(targetPath, symlinkPath);
|
|
553
|
+
} else {
|
|
554
|
+
// 非 Git 仓库:symlink 指向源路径
|
|
555
|
+
createSymlink(targetPath, symlinkPath);
|
|
610
556
|
}
|
|
611
557
|
|
|
612
|
-
// 创建软链接
|
|
613
|
-
fs.symlinkSync(targetPath, symlinkPath, 'dir');
|
|
614
|
-
|
|
615
558
|
// 更新配置
|
|
616
559
|
workspace.projects.push({
|
|
617
560
|
name: symlinkName,
|
|
@@ -629,7 +572,7 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
629
572
|
/**
|
|
630
573
|
* 从工作区移除项目
|
|
631
574
|
*/
|
|
632
|
-
function removeProjectFromWorkspace(workspaceId, projectName
|
|
575
|
+
function removeProjectFromWorkspace(workspaceId, projectName) {
|
|
633
576
|
const data = loadWorkspaces();
|
|
634
577
|
const workspace = data.workspaces.find(ws => ws.id === workspaceId);
|
|
635
578
|
|
|
@@ -644,26 +587,28 @@ function removeProjectFromWorkspace(workspaceId, projectName, removeWorktrees =
|
|
|
644
587
|
}
|
|
645
588
|
|
|
646
589
|
const project = workspace.projects[projectIndex];
|
|
647
|
-
const
|
|
590
|
+
const projectPath = path.join(workspace.path, projectName);
|
|
648
591
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
fs.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
});
|
|
662
|
-
} catch (error) {
|
|
663
|
-
console.error(`删除 worktree 失败: ${wt.path}`, error.message);
|
|
592
|
+
if (project.useWorktree) {
|
|
593
|
+
// worktree 项目:用 git worktree remove 注销引用(目录在工作区内)
|
|
594
|
+
if (fs.existsSync(projectPath) && fs.existsSync(project.sourcePath)) {
|
|
595
|
+
try {
|
|
596
|
+
runGitCommand(['worktree', 'remove', projectPath, '--force'], {
|
|
597
|
+
cwd: project.sourcePath
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error(`注销 worktree 失败: ${projectPath}`, error.message);
|
|
601
|
+
// git 注销失败时手动删除目录
|
|
602
|
+
if (fs.existsSync(projectPath)) {
|
|
603
|
+
fs.rmSync(projectPath, { recursive: true, force: true });
|
|
664
604
|
}
|
|
665
605
|
}
|
|
666
606
|
}
|
|
607
|
+
} else {
|
|
608
|
+
// symlink 项目:直接删除软链接
|
|
609
|
+
if (fs.existsSync(projectPath)) {
|
|
610
|
+
fs.unlinkSync(projectPath);
|
|
611
|
+
}
|
|
667
612
|
}
|
|
668
613
|
|
|
669
614
|
// 从配置中移除
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.analytics-page[data-v-7683ab57]{height:100%;background:var(--bg-primary);overflow-y:auto;padding:16px 20px;box-sizing:border-box;display:flex;flex-direction:column;gap:14px}.analytics-header[data-v-7683ab57]{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;flex-wrap:wrap;gap:10px}.page-title[data-v-7683ab57]{margin:0;font-size:18px;font-weight:700;color:var(--text-primary)}.toolbar[data-v-7683ab57]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.range-buttons[data-v-7683ab57]{display:flex;align-items:center;gap:4px}.range-btn[data-v-7683ab57]{padding:4px 10px;font-size:12px;border:1px solid var(--border-color, #e0e0e6);background:transparent;color:var(--text-secondary);border-radius:4px;cursor:pointer;transition:all .15s}.range-btn[data-v-7683ab57]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.range-btn.active[data-v-7683ab57]{background:var(--primary-color, #18a058);border-color:var(--primary-color, #18a058);color:#fff}.custom-range-wrapper[data-v-7683ab57]{margin-left:4px}.loading-overlay[data-v-7683ab57]{display:flex;justify-content:center;padding:20px 0}.summary-cards[data-v-7683ab57]{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;flex-shrink:0}.summary-card[data-v-7683ab57]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px}.summary-label[data-v-7683ab57]{font-size:11px;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em}.summary-value[data-v-7683ab57]{font-size:22px;font-weight:700;color:var(--text-primary);line-height:1.2}.chart-section[data-v-7683ab57]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px;flex-shrink:0}.chart-section.fullscreen[data-v-7683ab57]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;border-radius:0;padding:20px;background:var(--bg-secondary);overflow:auto}.cumulative-section[data-v-7683ab57]{margin-bottom:8px}.chart-section-header[data-v-7683ab57]{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.chart-title[data-v-7683ab57]{font-size:13px;font-weight:600;color:var(--text-primary)}.chart-controls[data-v-7683ab57]{display:flex;align-items:center;gap:8px}.chart-type-toggle[data-v-7683ab57]{display:flex;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;overflow:hidden}.toggle-btn[data-v-7683ab57]{padding:3px 10px;font-size:12px;border:none;background:transparent;color:var(--text-secondary);cursor:pointer;transition:all .15s}.toggle-btn.active[data-v-7683ab57]{background:var(--primary-color, #18a058);color:#fff}.icon-btn[data-v-7683ab57]{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;background:transparent;cursor:pointer;color:var(--text-secondary);transition:all .15s}.icon-btn[data-v-7683ab57]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.main-chart[data-v-7683ab57]{height:560px;width:100%}.chart-section.fullscreen .main-chart[data-v-7683ab57]{height:calc(100vh - 100px)}.cumulative-chart[data-v-7683ab57]{height:200px;width:100%}.empty-state[data-v-7683ab57]{height:120px;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);font-size:13px}@media (max-width: 768px){.analytics-page[data-v-7683ab57]{padding:10px 12px}.summary-cards[data-v-7683ab57]{grid-template-columns:repeat(2,1fr)}.analytics-header[data-v-7683ab57]{flex-direction:column;align-items:flex-start}.toolbar[data-v-7683ab57]{width:100%}}@media (max-width: 480px){.summary-cards[data-v-7683ab57]{grid-template-columns:1fr 1fr}.summary-value[data-v-7683ab57]{font-size:18px}}
|