react-doctor-cli-dev 1.0.7 → 1.0.12
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/backend/dist/db.js +33 -11
- package/backend/dist/index.js +29 -3
- package/backend/dist/routes/reports.js +106 -55
- package/backend/public/assets/index-BpODc0fS.css +1 -0
- package/backend/public/assets/index-zKyZPsv1.js +118 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/backend/public/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/backend/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/backend/public/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/backend/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/backend/public/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/backend/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/backend/public/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/backend/public/assets/tajawal-arabic-300-normal-Bq0yWa0Z.woff +0 -0
- package/backend/public/assets/tajawal-arabic-300-normal-By07C9pa.woff2 +0 -0
- package/backend/public/assets/tajawal-arabic-400-normal-CyCXRvzh.woff2 +0 -0
- package/backend/public/assets/tajawal-arabic-400-normal-DCQxawbB.woff +0 -0
- package/backend/public/assets/tajawal-arabic-500-normal-BZ8ojJNu.woff2 +0 -0
- package/backend/public/assets/tajawal-arabic-500-normal-CbVEaYEW.woff +0 -0
- package/backend/public/assets/tajawal-arabic-700-normal-9L7Zusdl.woff +0 -0
- package/backend/public/assets/tajawal-arabic-700-normal-D2-eand5.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-300-normal-C0-xR3ms.woff +0 -0
- package/backend/public/assets/tajawal-latin-300-normal-CeEKeOxZ.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-400-normal-BVNSOH3d.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-400-normal-BdYcZznU.woff +0 -0
- package/backend/public/assets/tajawal-latin-500-normal-CoYeBiSI.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-500-normal-DU9v6xgj.woff +0 -0
- package/backend/public/assets/tajawal-latin-700-normal-BypgxfGb.woff2 +0 -0
- package/backend/public/assets/tajawal-latin-700-normal-CV3bxpHe.woff +0 -0
- package/backend/public/favicon.svg +1 -0
- package/backend/public/icons.svg +24 -0
- package/backend/public/index.html +254 -0
- package/backend/src/db.ts +42 -18
- package/backend/src/index.ts +31 -3
- package/backend/src/routes/reports.ts +141 -52
- package/cli/dist/commands/full.js +82 -48
- package/cli/src/commands/full.ts +161 -115
- package/dashboard/index.html +253 -0
- package/dashboard/package-lock.json +913 -0
- package/dashboard/package.json +19 -0
- package/dashboard/public/favicon.svg +1 -0
- package/dashboard/public/icons.svg +24 -0
- package/dashboard/src/api.js +107 -0
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/javascript.svg +1 -0
- package/dashboard/src/assets/vite.svg +1 -0
- package/dashboard/src/css/main.css +441 -0
- package/dashboard/src/data.js +202 -0
- package/dashboard/src/main.js +53 -0
- package/dashboard/src/pages.js +797 -0
- package/dashboard/src/router.js +77 -0
- package/dashboard/src/utils.js +163 -0
- package/dashboard/vite.config.js +19 -0
- package/data/screenshots/5--fcp.png +0 -0
- package/data/screenshots/5--fullLoad.png +0 -0
- package/data/screenshots/5-docs-fullLoad.png +0 -0
- package/data/screenshots/5-white-fullLoad.png +0 -0
- package/data/screenshots/6--fcp.png +0 -0
- package/data/screenshots/6--fullLoad.png +0 -0
- package/data/screenshots/6-docs-fullLoad.png +0 -0
- package/data/screenshots/6-white-fullLoad.png +0 -0
- package/package.json +4 -1
package/cli/src/commands/full.ts
CHANGED
|
@@ -11,18 +11,7 @@
|
|
|
11
11
|
// 4. RuleEngine → combines both reports into suggestions
|
|
12
12
|
// 5. ReportCompiler → merges everything into finalreport.json
|
|
13
13
|
// 6. Upload → sends the report to the backend API
|
|
14
|
-
//
|
|
15
|
-
// HOW IMPORTS WORK:
|
|
16
|
-
// The CLI imports core modules directly as TypeScript classes.
|
|
17
|
-
// No shell commands, no spawning child processes, no ts-node
|
|
18
|
-
// inside ts-node. Everything runs in the same Node.js process,
|
|
19
|
-
// which means objects are passed between steps in memory —
|
|
20
|
-
// fast, clean, and no disk reads between steps.
|
|
21
|
-
//
|
|
22
|
-
// The core folder is 2 levels up from cli/src/commands/:
|
|
23
|
-
// cli/src/commands/full.ts
|
|
24
|
-
// → ../../.. = react-tool root
|
|
25
|
-
// → ../../../core = core folder
|
|
14
|
+
// and opens the dashboard to that report
|
|
26
15
|
// ─────────────────────────────────────────────────────────────
|
|
27
16
|
|
|
28
17
|
import { Command } from "commander";
|
|
@@ -45,14 +34,7 @@ import {
|
|
|
45
34
|
} from "../ui";
|
|
46
35
|
|
|
47
36
|
// ── Core imports ──────────────────────────────────────────────
|
|
48
|
-
// These are the actual classes from the core folder.
|
|
49
|
-
// We use require() with a resolved path so they work whether
|
|
50
|
-
// the CLI is run from its own folder or from the project root.
|
|
51
|
-
|
|
52
37
|
function getCoreModule(relativePath: string) {
|
|
53
|
-
// __dirname = cli/src/commands/
|
|
54
|
-
// 3 levels up = react-tool root
|
|
55
|
-
// then into core/
|
|
56
38
|
return require(
|
|
57
39
|
path.resolve(__dirname, "..", "..", "..", "core", relativePath),
|
|
58
40
|
);
|
|
@@ -104,21 +86,19 @@ export function registerFullCommand(program: Command): void {
|
|
|
104
86
|
"Backend API URL to upload to",
|
|
105
87
|
"http://localhost:3000",
|
|
106
88
|
)
|
|
107
|
-
// ✅ Single --api-key option (defined BEFORE .action)
|
|
108
89
|
.option(
|
|
109
90
|
"--api-key <key>",
|
|
110
91
|
"API key for backend authentication (overrides REACT_DOCTOR_API_KEY env var)",
|
|
111
92
|
process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this",
|
|
112
93
|
)
|
|
113
94
|
.option("--no-banner", "Skip the banner")
|
|
114
|
-
// ✅ .action() comes LAST, with no trailing semicolon/comment
|
|
115
95
|
.action(async (projectPath: string, options) => {
|
|
116
96
|
await runFullCommand(projectPath, options);
|
|
117
97
|
});
|
|
118
98
|
}
|
|
99
|
+
|
|
119
100
|
// ─────────────────────────────────────────────────────────────
|
|
120
101
|
// MAIN RUNNER
|
|
121
|
-
// Exported so other commands (analyze --full) can call it too.
|
|
122
102
|
// ─────────────────────────────────────────────────────────────
|
|
123
103
|
|
|
124
104
|
export async function runFullCommand(
|
|
@@ -149,10 +129,6 @@ export async function runFullCommand(
|
|
|
149
129
|
}
|
|
150
130
|
|
|
151
131
|
// ── Determine device configuration ──────────────────────────
|
|
152
|
-
// --desktop and --mobile are independent flags.
|
|
153
|
-
// If neither is passed, desktop is the default.
|
|
154
|
-
// If only --mobile is passed, only mobile runs.
|
|
155
|
-
// If both are passed, both run in one pass.
|
|
156
132
|
const wantDesktop = options.desktop || (!options.desktop && !options.mobile);
|
|
157
133
|
const wantMobile = options.mobile ?? false;
|
|
158
134
|
|
|
@@ -180,8 +156,6 @@ export async function runFullCommand(
|
|
|
180
156
|
printInfo("Network", throttleLabel);
|
|
181
157
|
|
|
182
158
|
// ── Output directory ───────────────────────────────────────
|
|
183
|
-
// Reports are saved inside the user's project in a hidden
|
|
184
|
-
// .react-doctor/ folder — easy to find, easy to gitignore.
|
|
185
159
|
const outputDir = path.join(resolvedPath, ".react-doctor");
|
|
186
160
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
187
161
|
|
|
@@ -275,7 +249,6 @@ export async function runFullCommand(
|
|
|
275
249
|
),
|
|
276
250
|
);
|
|
277
251
|
|
|
278
|
-
// Print results for each route
|
|
279
252
|
for (const [key, report] of Object.entries(runtimeReports)) {
|
|
280
253
|
const [route, device] = key.includes("::")
|
|
281
254
|
? key.split("::")
|
|
@@ -285,7 +258,6 @@ export async function runFullCommand(
|
|
|
285
258
|
console.log(
|
|
286
259
|
` ${chalk.bold(route)} ${chalk.gray(`[${device}]`)} Score: ${scoreBadge(report.performanceScore)}`,
|
|
287
260
|
);
|
|
288
|
-
// ── Device / CPU / Network line ──────────────────────────
|
|
289
261
|
console.log(
|
|
290
262
|
` ${chalk.gray("Device:")} ${device} ` +
|
|
291
263
|
`${chalk.gray("CPU:")} ${report.cpuThrottling ?? cpuLabel}x ` +
|
|
@@ -346,7 +318,6 @@ export async function runFullCommand(
|
|
|
346
318
|
} catch (err: any) {
|
|
347
319
|
profilingSpin.fail(chalk.red("Runtime profiling failed"));
|
|
348
320
|
console.log(chalk.red(`\n ${err.message}\n`));
|
|
349
|
-
// Profiling failure is not fatal — rule engine can still run on static data
|
|
350
321
|
}
|
|
351
322
|
|
|
352
323
|
// ════════════════════════════════════════════════════════════
|
|
@@ -452,103 +423,178 @@ export async function runFullCommand(
|
|
|
452
423
|
}
|
|
453
424
|
|
|
454
425
|
// ════════════════════════════════════════════════════════════
|
|
455
|
-
// OPTIONAL — UPLOAD TO BACKEND API
|
|
426
|
+
// OPTIONAL — UPLOAD TO BACKEND API (WITH SCREENSHOTS)
|
|
456
427
|
// ════════════════════════════════════════════════════════════
|
|
457
428
|
|
|
458
429
|
if (options.upload && finalReport) {
|
|
459
|
-
|
|
430
|
+
printSection("Uploading to Backend");
|
|
460
431
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
// Ensure apiUrl has a default
|
|
465
|
-
const apiUrl = options.apiUrl ?? "http://localhost:3000";
|
|
466
|
-
|
|
467
|
-
// 1. Check if backend is already running
|
|
468
|
-
try {
|
|
469
|
-
await axios.get(`${apiUrl}/health`, { timeout: 2000 });
|
|
470
|
-
uploadSpin.text = "Backend detected. Preparing upload...";
|
|
471
|
-
} catch (err) {
|
|
472
|
-
// 2. If not running, start it automatically
|
|
473
|
-
|
|
474
|
-
// Determine the project root directory (where cli/ and backend/ are siblings)
|
|
475
|
-
const projectRoot = path.resolve(__dirname, "..", "..", "..");
|
|
476
|
-
|
|
477
|
-
// Backend is a sibling folder to cli at projectRoot
|
|
478
|
-
const backendRoot = path.resolve(projectRoot, "backend");
|
|
479
|
-
|
|
480
|
-
// Check for compiled JS first (for installed packages)
|
|
481
|
-
const backendDist = path.join(backendRoot, "dist", "index.js");
|
|
482
|
-
// Check for TS source (for local dev)
|
|
483
|
-
const backendSrc = path.join(backendRoot, "src", "index.ts");
|
|
484
|
-
|
|
485
|
-
let command: string;
|
|
486
|
-
let args: string[];
|
|
487
|
-
|
|
488
|
-
if (fs.existsSync(backendDist)) {
|
|
489
|
-
command = "node";
|
|
490
|
-
args = [backendDist];
|
|
491
|
-
} else if (fs.existsSync(backendSrc)) {
|
|
492
|
-
command = "npx";
|
|
493
|
-
args = ["ts-node", backendSrc];
|
|
494
|
-
} else {
|
|
495
|
-
throw new Error(`Cannot find backend at: ${backendRoot}. Ensure 'backend' folder exists next to 'cli'.`);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
uploadSpin.text = "Backend not found. Starting local server automatically...";
|
|
432
|
+
const uploadSpin = spinner(`Connecting to ${options.apiUrl}...`);
|
|
499
433
|
|
|
500
|
-
|
|
501
|
-
const
|
|
434
|
+
try {
|
|
435
|
+
const apiUrl = options.apiUrl ?? "http://localhost:3000";
|
|
436
|
+
|
|
437
|
+
// 1. Check if backend is already running
|
|
438
|
+
try {
|
|
439
|
+
await axios.get(`${apiUrl}/health`, { timeout: 2000 });
|
|
440
|
+
uploadSpin.text = "Backend detected. Preparing upload...";
|
|
441
|
+
} catch (err) {
|
|
442
|
+
// 2. If not running, start it automatically
|
|
443
|
+
const projectRoot = path.resolve(__dirname, "..", "..", "..");
|
|
444
|
+
const backendRoot = path.resolve(projectRoot, "backend");
|
|
445
|
+
const backendDist = path.join(backendRoot, "dist", "index.js");
|
|
446
|
+
const backendSrc = path.join(backendRoot, "src", "index.ts");
|
|
447
|
+
|
|
448
|
+
let command: string;
|
|
449
|
+
let args: string[];
|
|
450
|
+
|
|
451
|
+
if (fs.existsSync(backendDist)) {
|
|
452
|
+
command = "node";
|
|
453
|
+
args = [backendDist];
|
|
454
|
+
} else if (fs.existsSync(backendSrc)) {
|
|
455
|
+
command = "npx";
|
|
456
|
+
args = ["ts-node", backendSrc];
|
|
457
|
+
} else {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`Cannot find backend at: ${backendRoot}. Ensure 'backend' folder exists next to 'cli'.`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
502
462
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
463
|
+
uploadSpin.text =
|
|
464
|
+
"Backend not found. Starting local server automatically...";
|
|
465
|
+
|
|
466
|
+
const port = new URL(apiUrl).port || "3000";
|
|
467
|
+
const backendDataDir = path.join(outputDir, "backend-data");
|
|
468
|
+
fs.mkdirSync(backendDataDir, { recursive: true });
|
|
469
|
+
|
|
470
|
+
spawn(command, args, {
|
|
471
|
+
stdio: "inherit",
|
|
472
|
+
env: {
|
|
473
|
+
...process.env,
|
|
474
|
+
API_KEY: options.apiKey || "react-doctor-secret-key-change-this",
|
|
475
|
+
PORT: port,
|
|
476
|
+
DB_PATH: path.join(backendDataDir, "reports.db"),
|
|
477
|
+
},
|
|
478
|
+
cwd: backendRoot,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// 3. Wait for backend to be ready (up to 15s)
|
|
482
|
+
let isReady = false;
|
|
483
|
+
let retries = 0;
|
|
484
|
+
while (!isReady && retries < 15) {
|
|
485
|
+
try {
|
|
486
|
+
await axios.get(`${apiUrl}/health`, { timeout: 1000 });
|
|
487
|
+
isReady = true;
|
|
488
|
+
} catch {
|
|
489
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
490
|
+
retries++;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
507
493
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
API_KEY: options.apiKey || "react-doctor-secret-key-change-this",
|
|
513
|
-
PORT: port,
|
|
514
|
-
DB_PATH: path.join(backendDataDir, "reports.db"),
|
|
515
|
-
},
|
|
516
|
-
cwd: backendRoot
|
|
517
|
-
});
|
|
494
|
+
if (!isReady)
|
|
495
|
+
throw new Error("Backend failed to start after 15 seconds.");
|
|
496
|
+
uploadSpin.text = "Backend started successfully!";
|
|
497
|
+
}
|
|
518
498
|
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
499
|
+
// ── 4. Read and encode screenshots ──────────────────────
|
|
500
|
+
uploadSpin.text = "Processing screenshots...";
|
|
501
|
+
const screenshots: any[] = [];
|
|
502
|
+
const runtime = finalReport.runtime || {};
|
|
503
|
+
|
|
504
|
+
for (const [routeKey, routeData] of Object.entries(runtime)) {
|
|
505
|
+
if ((routeData as any).screenshots && (routeData as any).screenshots.length > 0) {
|
|
506
|
+
for (const screenshot of (routeData as any).screenshots) {
|
|
507
|
+
try {
|
|
508
|
+
// Check if this is a file path or already a data URL
|
|
509
|
+
if (screenshot.dataUrl && !screenshot.dataUrl.startsWith('data:')) {
|
|
510
|
+
// It's a file path - read and encode it
|
|
511
|
+
const screenshotPath = path.join(outputDir, screenshot.dataUrl);
|
|
512
|
+
if (fs.existsSync(screenshotPath)) {
|
|
513
|
+
const imageBuffer = fs.readFileSync(screenshotPath);
|
|
514
|
+
const base64Image = imageBuffer.toString('base64');
|
|
515
|
+
screenshots.push({
|
|
516
|
+
route: routeKey,
|
|
517
|
+
label: screenshot.label || 'screenshot',
|
|
518
|
+
takenAt: screenshot.takenAt || 0,
|
|
519
|
+
dataUrl: `data:image/png;base64,${base64Image}`,
|
|
520
|
+
});
|
|
521
|
+
} else {
|
|
522
|
+
// File doesn't exist - store as placeholder
|
|
523
|
+
console.log(chalk.yellow(` ⚠️ Screenshot not found: ${screenshotPath}`));
|
|
524
|
+
screenshots.push({
|
|
525
|
+
route: routeKey,
|
|
526
|
+
label: screenshot.label || 'screenshot',
|
|
527
|
+
takenAt: screenshot.takenAt || 0,
|
|
528
|
+
dataUrl: null,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
} else if (screenshot.dataUrl && screenshot.dataUrl.startsWith('data:')) {
|
|
532
|
+
// Already a data URL - use it directly
|
|
533
|
+
screenshots.push({
|
|
534
|
+
route: routeKey,
|
|
535
|
+
label: screenshot.label || 'screenshot',
|
|
536
|
+
takenAt: screenshot.takenAt || 0,
|
|
537
|
+
dataUrl: screenshot.dataUrl,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
} catch (err: any) {
|
|
541
|
+
console.log(chalk.yellow(` ⚠️ Failed to process screenshot: ${err.message}`));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
529
544
|
}
|
|
530
545
|
}
|
|
531
546
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
547
|
+
// ── 5. Prepare upload data with screenshots ─────────────
|
|
548
|
+
const uploadData = {
|
|
549
|
+
...finalReport,
|
|
550
|
+
screenshots: screenshots,
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
uploadSpin.text = `Uploading report and ${screenshots.length} screenshots...`;
|
|
554
|
+
|
|
555
|
+
// ── 6. Perform the upload ──────────────────────────────
|
|
556
|
+
const response = await axios.post(
|
|
557
|
+
`${apiUrl}/api/reports/upload`,
|
|
558
|
+
uploadData,
|
|
559
|
+
{
|
|
560
|
+
headers: {
|
|
561
|
+
"Content-Type": "application/json",
|
|
562
|
+
"x-api-key":
|
|
563
|
+
options.apiKey || "react-doctor-secret-key-change-this",
|
|
564
|
+
} as Record<string, string>,
|
|
565
|
+
timeout: 30000, // Longer timeout for images
|
|
566
|
+
},
|
|
567
|
+
);
|
|
535
568
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
"x-api-key": options.apiKey || "react-doctor-secret-key-change-this",
|
|
542
|
-
} as Record<string, string>,
|
|
543
|
-
timeout: 10000,
|
|
544
|
-
});
|
|
569
|
+
const reportId = response.data?.id;
|
|
570
|
+
const uploadedCount = response.data?.screenshots || 0;
|
|
571
|
+
uploadSpin.succeed(
|
|
572
|
+
chalk.green(`Report uploaded successfully (${uploadedCount} screenshots)`)
|
|
573
|
+
);
|
|
545
574
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
575
|
+
// 7. Open the dashboard directly to this report
|
|
576
|
+
if (reportId) {
|
|
577
|
+
const dashboardUrl = `${apiUrl}/report/${reportId}`;
|
|
578
|
+
printInfo("Opening dashboard", dashboardUrl);
|
|
579
|
+
|
|
580
|
+
const openCmd: [string, string[]] =
|
|
581
|
+
process.platform === "win32"
|
|
582
|
+
? ["cmd", ["/c", "start", "", dashboardUrl]]
|
|
583
|
+
: process.platform === "darwin"
|
|
584
|
+
? ["open", [dashboardUrl]]
|
|
585
|
+
: ["xdg-open", [dashboardUrl]];
|
|
586
|
+
|
|
587
|
+
spawn(openCmd[0], openCmd[1], {
|
|
588
|
+
stdio: "ignore",
|
|
589
|
+
detached: true,
|
|
590
|
+
}).unref();
|
|
591
|
+
}
|
|
592
|
+
} catch (err: any) {
|
|
593
|
+
uploadSpin.fail(chalk.yellow("Upload failed — report saved locally"));
|
|
594
|
+
console.log(chalk.gray(` ${err.message}`));
|
|
595
|
+
}
|
|
550
596
|
}
|
|
551
|
-
|
|
597
|
+
|
|
552
598
|
// ════════════════════════════════════════════════════════════
|
|
553
599
|
// FINAL SUMMARY
|
|
554
600
|
// ════════════════════════════════════════════════════════════
|
|
@@ -571,4 +617,4 @@ export async function runFullCommand(
|
|
|
571
617
|
}
|
|
572
618
|
|
|
573
619
|
printDone("Full diagnostic finished.");
|
|
574
|
-
}
|
|
620
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>React Doctor — Dashboard</title>
|
|
7
|
+
<meta name="theme-color" content="#0B0C10" />
|
|
8
|
+
<!-- main.css is imported by src/main.js via Vite — no link tag needed here -->
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<!-- ── Sidebar ─────────────────────────────────────────────── -->
|
|
13
|
+
<aside id="sidebar">
|
|
14
|
+
<div class="sidebar-logo">
|
|
15
|
+
<div>
|
|
16
|
+
<div class="logo-text">React Doctor</div>
|
|
17
|
+
<div class="logo-sub">Performance Dashboard</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="sidebar-section">Analysis</div>
|
|
22
|
+
|
|
23
|
+
<button class="nav-item" data-route="overview">
|
|
24
|
+
Overview
|
|
25
|
+
<span class="nav-badge" id="nav-score"></span>
|
|
26
|
+
</button>
|
|
27
|
+
<button class="nav-item" data-route="vitals">
|
|
28
|
+
Web Vitals
|
|
29
|
+
</button>
|
|
30
|
+
<button class="nav-item" data-route="issues">
|
|
31
|
+
Code Issues
|
|
32
|
+
<span class="nav-badge" id="nav-issues-count"></span>
|
|
33
|
+
</button>
|
|
34
|
+
<button class="nav-item" data-route="suggestions">
|
|
35
|
+
Suggestions
|
|
36
|
+
<span class="nav-badge" id="nav-sug-count"></span>
|
|
37
|
+
</button>
|
|
38
|
+
|
|
39
|
+
<div class="sidebar-section">Reports</div>
|
|
40
|
+
|
|
41
|
+
<button class="nav-item" data-route="history">
|
|
42
|
+
History
|
|
43
|
+
<span class="nav-badge" id="nav-hist-count"></span>
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<div class="sidebar-footer">
|
|
47
|
+
<div class="project-pill">
|
|
48
|
+
<div class="status-dot"></div>
|
|
49
|
+
<div>
|
|
50
|
+
<div class="proj-name" id="sidebar-project">—</div>
|
|
51
|
+
<div class="proj-sub" id="sidebar-date">—</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</aside>
|
|
56
|
+
|
|
57
|
+
<!-- ── Main ───────────────────────────────────────────────── -->
|
|
58
|
+
<div id="main">
|
|
59
|
+
|
|
60
|
+
<!-- Topbar -->
|
|
61
|
+
<div class="topbar">
|
|
62
|
+
<div>
|
|
63
|
+
<div class="topbar-title" id="topbar-title">Overview</div>
|
|
64
|
+
<div class="topbar-sub" id="topbar-sub">Performance summary</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="topbar-right">
|
|
67
|
+
<div class="topbar-time" id="topbar-time"></div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- ══ PAGE: OVERVIEW ═══════════════════════════════════════ -->
|
|
72
|
+
<div class="page-content" id="page-overview">
|
|
73
|
+
|
|
74
|
+
<!-- Score + meta -->
|
|
75
|
+
<div class="card section-gap">
|
|
76
|
+
<div class="score-ring-wrap">
|
|
77
|
+
<div id="ov-score-ring"></div>
|
|
78
|
+
<div class="score-details">
|
|
79
|
+
<h2>Performance Score</h2>
|
|
80
|
+
<p id="ov-summary" style="margin-bottom:12px">Loading…</p>
|
|
81
|
+
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
|
82
|
+
<div id="ov-grade"></div>
|
|
83
|
+
<span style="font-size:.78rem;color:var(--text3)">Project: <strong id="ov-project" style="color:var(--text)">—</strong></span>
|
|
84
|
+
<span style="font-size:.78rem;color:var(--text3)">Analyzed: <span id="ov-analyzed" style="color:var(--text2)">—</span></span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Vital stat tiles -->
|
|
91
|
+
<div class="grid-4 section-gap">
|
|
92
|
+
<div class="stat-tile" id="tile-lcp">
|
|
93
|
+
<div class="stat-label">Avg LCP</div>
|
|
94
|
+
<div class="stat-value">—</div>
|
|
95
|
+
<div class="stat-meta">Good < 2.5s</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="stat-tile" id="tile-fcp">
|
|
98
|
+
<div class="stat-label">Avg FCP</div>
|
|
99
|
+
<div class="stat-value">—</div>
|
|
100
|
+
<div class="stat-meta">Good < 1.8s</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="stat-tile" id="tile-cls">
|
|
103
|
+
<div class="stat-label">Avg CLS</div>
|
|
104
|
+
<div class="stat-value">—</div>
|
|
105
|
+
<div class="stat-meta">Good < 0.1</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="stat-tile" id="tile-issues">
|
|
108
|
+
<div class="stat-label">Total Issues</div>
|
|
109
|
+
<div class="stat-value">—</div>
|
|
110
|
+
<div class="stat-meta">Static analysis</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Routes table -->
|
|
115
|
+
<div class="card section-gap">
|
|
116
|
+
<div class="card-title"> Routes Profiled</div>
|
|
117
|
+
<div id="ov-routes"></div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- Top suggestions -->
|
|
121
|
+
<div class="card">
|
|
122
|
+
<div class="card-title" style="margin-bottom:14px">
|
|
123
|
+
Top Suggestions
|
|
124
|
+
<button onclick="window.location.hash='suggestions'" style="margin-left:auto;background:none;border:1px solid var(--border);color:var(--text2);border-radius:var(--radius-sm);padding:3px 12px;cursor:pointer;font-size:.72rem">View all →</button>
|
|
125
|
+
</div>
|
|
126
|
+
<div id="ov-suggestions"></div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
</div><!-- /page-overview -->
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
<!-- ══ PAGE: VITALS ══════════════════════════════════════════ -->
|
|
133
|
+
<div class="page-content" id="page-vitals">
|
|
134
|
+
|
|
135
|
+
<!-- Route selector -->
|
|
136
|
+
<div class="route-tabs" id="vitals-tabs"></div>
|
|
137
|
+
|
|
138
|
+
<div class="grid-2 section-gap" style="grid-template-columns:1fr 320px">
|
|
139
|
+
|
|
140
|
+
<!-- Bars -->
|
|
141
|
+
<div class="card">
|
|
142
|
+
<div class="card-title">
|
|
143
|
+
Core Web Vitals
|
|
144
|
+
<div id="vitals-meta" style="display:flex;gap:8px;flex-wrap:wrap;margin-left:8px"></div>
|
|
145
|
+
</div>
|
|
146
|
+
<div id="vitals-bars"></div>
|
|
147
|
+
<div style="margin-top:16px;display:flex;align-items:center;gap:10px">
|
|
148
|
+
<span style="font-size:.75rem;color:var(--text3)">Performance score:</span>
|
|
149
|
+
<div id="vitals-score"></div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<!-- Re-renders -->
|
|
154
|
+
<div class="card">
|
|
155
|
+
<div class="card-title">Component Re-renders</div>
|
|
156
|
+
<div id="vitals-rerenders"></div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Chart: all routes comparison -->
|
|
161
|
+
<div class="card section-gap">
|
|
162
|
+
<div class="card-title">All Routes — LCP / FCP / TTFB Comparison</div>
|
|
163
|
+
<div style="height:240px;position:relative">
|
|
164
|
+
<canvas id="vitals-chart"></canvas>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Errors -->
|
|
169
|
+
<div class="card section-gap">
|
|
170
|
+
<div class="card-title">JS Errors & Console Warnings</div>
|
|
171
|
+
<div id="vitals-errors"></div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<!-- Screenshots -->
|
|
175
|
+
<div class="card">
|
|
176
|
+
<div class="card-title">Screenshots</div>
|
|
177
|
+
<div class="filmstrip" id="vitals-filmstrip"></div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
</div><!-- /page-vitals -->
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
<!-- ══ PAGE: ISSUES ══════════════════════════════════════════ -->
|
|
184
|
+
<div class="page-content" id="page-issues">
|
|
185
|
+
|
|
186
|
+
<div class="filter-bar" id="issues-filters">
|
|
187
|
+
<button class="filter-btn active" data-filter="all">All</button>
|
|
188
|
+
<button class="filter-btn fc" data-filter="critical">🔴 Critical</button>
|
|
189
|
+
<button class="filter-btn fw" data-filter="warning">🟠 Warning</button>
|
|
190
|
+
<button class="filter-btn fi" data-filter="info">🔵 Info</button>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="card">
|
|
194
|
+
<div id="issues-table"></div>
|
|
195
|
+
<div id="issues-pagination"></div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
</div><!-- /page-issues -->
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
<!-- ══ PAGE: SUGGESTIONS ═════════════════════════════════════ -->
|
|
202
|
+
<div class="page-content" id="page-suggestions">
|
|
203
|
+
|
|
204
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
|
205
|
+
<div class="filter-bar" id="sug-filters" style="margin-bottom:0">
|
|
206
|
+
<button class="filter-btn active" data-filter="all">All</button>
|
|
207
|
+
<button class="filter-btn fc" data-filter="critical">🔴 Critical</button>
|
|
208
|
+
<button class="filter-btn fw" data-filter="warning">🟠 Warning</button>
|
|
209
|
+
<button class="filter-btn fi" data-filter="info">🔵 Info</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div id="sug-counts" style="display:flex;gap:8px;margin-left:auto;flex-wrap:wrap"></div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div id="sug-list"></div>
|
|
215
|
+
<div id="sug-pagination"></div>
|
|
216
|
+
|
|
217
|
+
</div><!-- /page-suggestions -->
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
<!-- ══ PAGE: HISTORY ══════════════════════════════════════════ -->
|
|
221
|
+
<div class="page-content" id="page-history">
|
|
222
|
+
|
|
223
|
+
<!-- Trend chart -->
|
|
224
|
+
<div class="card section-gap">
|
|
225
|
+
<div class="card-title">Score Trend — my-react-app</div>
|
|
226
|
+
<div style="height:200px;position:relative">
|
|
227
|
+
<canvas id="hist-chart"></canvas>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- Table -->
|
|
232
|
+
<div class="card">
|
|
233
|
+
<div class="card-title"> All Runs</div>
|
|
234
|
+
<div id="hist-table"></div>
|
|
235
|
+
<div id="hist-pagination"></div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
</div><!-- /page-history -->
|
|
239
|
+
|
|
240
|
+
</div><!-- /main -->
|
|
241
|
+
|
|
242
|
+
<!-- Lightbox -->
|
|
243
|
+
<div id="lightbox">
|
|
244
|
+
<button id="lb-close">✕</button>
|
|
245
|
+
<img src="" alt="screenshot" />
|
|
246
|
+
<div id="lb-caption"></div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Scripts -->
|
|
250
|
+
<script type="module" src="/src/main.js"></script>
|
|
251
|
+
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|