react-doctor-cli-dev 1.0.6 → 1.0.9
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/data/screenshots/4--fcp.png +0 -0
- package/backend/data/screenshots/4--fullLoad.png +0 -0
- package/backend/data/screenshots/4-docs-fullLoad.png +0 -0
- package/backend/data/screenshots/4-white-fullLoad.png +0 -0
- package/backend/dist/db.js +38 -9
- package/backend/dist/index.js +29 -3
- package/backend/dist/routes/reports.js +105 -22
- 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 +46 -14
- package/backend/src/index.ts +31 -3
- package/backend/src/routes/reports.ts +140 -52
- package/cli/dist/commands/full.js +82 -48
- package/cli/src/commands/full.ts +161 -115
- package/package.json +25 -4
- package/shared/dist/index.d.ts +0 -2
- package/shared/dist/index.js +0 -19
- package/shared/dist/schemas.d.ts +0 -91
- package/shared/dist/schemas.js +0 -82
- package/shared/dist/types.d.ts +0 -44
- package/shared/dist/types.js +0 -2
- package/shared/package-lock.json +0 -47
- package/shared/package.json +0 -21
- package/shared/src/index.ts +0 -4
- package/shared/src/schemas.ts +0 -136
- package/shared/src/types.ts +0 -137
- package/shared/tsconfig.json +0 -15
- package/tsconfig.json +0 -25
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/backend/dist/db.js
CHANGED
|
@@ -3,24 +3,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.screenshotsDir = void 0;
|
|
6
7
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const dbDir = path_1.default.join(os_1.default.homedir(), ".react-doctor");
|
|
12
|
-
const dbPath = process.env.DB_PATH || path_1.default.join(dbDir, "reports.db");
|
|
13
|
-
fs_1.default.mkdirSync(path_1.default.dirname(dbPath), { recursive: true });
|
|
10
|
+
// ── Initialize database ──────────────────────────────────────
|
|
11
|
+
const dbPath = process.env.DB_PATH || path_1.default.join(__dirname, '../../reports.db');
|
|
14
12
|
const db = new better_sqlite3_1.default(dbPath);
|
|
13
|
+
// ── Enable foreign keys ──────────────────────────────────────
|
|
14
|
+
db.pragma('foreign_keys = ON');
|
|
15
|
+
// ── Create reports table if it doesn't exist ──────────────
|
|
15
16
|
db.exec(`
|
|
16
|
-
CREATE TABLE IF NOT EXISTS reports (
|
|
17
|
+
CREATE TABLE IF NOT EXISTS reports (
|
|
17
18
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
19
|
project TEXT NOT NULL,
|
|
19
20
|
score INTEGER NOT NULL,
|
|
20
21
|
grade TEXT NOT NULL,
|
|
21
22
|
analyzed_at TEXT NOT NULL,
|
|
22
|
-
created_at TEXT
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
static_json TEXT NOT NULL,
|
|
25
|
+
runtime_json TEXT NOT NULL,
|
|
26
|
+
suggestions TEXT NOT NULL
|
|
27
|
+
)
|
|
25
28
|
`);
|
|
29
|
+
// ── Create screenshots table if it doesn't exist ───────────
|
|
30
|
+
db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS screenshots (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
report_id INTEGER NOT NULL,
|
|
34
|
+
route TEXT NOT NULL,
|
|
35
|
+
label TEXT NOT NULL,
|
|
36
|
+
taken_at INTEGER NOT NULL,
|
|
37
|
+
data_url TEXT NOT NULL,
|
|
38
|
+
FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE
|
|
39
|
+
)
|
|
40
|
+
`);
|
|
41
|
+
// ── Create indexes for performance ──────────────────────────
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_reports_project ON reports(project);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_reports_created_at ON reports(created_at DESC);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_screenshots_report_id ON screenshots(report_id);
|
|
46
|
+
`);
|
|
47
|
+
// ── Define screenshots directory ────────────────────────────
|
|
48
|
+
const screenshotsDir = path_1.default.join(__dirname, '../../data/screenshots');
|
|
49
|
+
exports.screenshotsDir = screenshotsDir;
|
|
50
|
+
// ── Ensure screenshots directory exists ──────────────────────
|
|
51
|
+
if (!fs_1.default.existsSync(screenshotsDir)) {
|
|
52
|
+
fs_1.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
// ── Export both the database and the screenshots dir ──────
|
|
26
55
|
exports.default = db;
|
package/backend/dist/index.js
CHANGED
|
@@ -16,20 +16,46 @@ const PORT = process.env.PORT || 3000;
|
|
|
16
16
|
const API_KEY = process.env.API_KEY || "react-doctor-secret-key-change-this";
|
|
17
17
|
// Make API_KEY available globally so auth middleware can use it
|
|
18
18
|
process.env.API_KEY = API_KEY;
|
|
19
|
-
|
|
19
|
+
// ── Security headers ────────────────────────────────────────────
|
|
20
|
+
// CSP disabled so the bundled dashboard JS/CSS can load without
|
|
21
|
+
// Express blocking inline scripts or the Vite-built bundle.
|
|
22
|
+
app.use((0, helmet_1.default)({ contentSecurityPolicy: false }));
|
|
20
23
|
app.use((0, cors_1.default)());
|
|
21
24
|
app.use(express_1.default.json({ limit: '50mb' }));
|
|
25
|
+
// ── API routes ───────────────────────────────────────────────────
|
|
22
26
|
app.use('/api/reports', reports_1.default);
|
|
23
27
|
app.get('/health', (_req, res) => {
|
|
24
28
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
25
29
|
});
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
// ── Serve the built dashboard (frontend/ built via `npm run build`
|
|
31
|
+
// into backend/public/) ───────────────────────────────────────
|
|
32
|
+
const publicDir = path_1.default.join(__dirname, '..', 'public');
|
|
33
|
+
app.use(express_1.default.static(publicDir));
|
|
34
|
+
// ── SPA fallback ─────────────────────────────────────────────────
|
|
35
|
+
// Any route not matched above (e.g. /report/7, /history, /overview)
|
|
36
|
+
// gets index.html so the dashboard's client-side router can take
|
|
37
|
+
// over and render the right page based on the URL.
|
|
38
|
+
//
|
|
39
|
+
// IMPORTANT: this must come AFTER /api/reports and /health so API
|
|
40
|
+
// calls are never accidentally swallowed by this fallback — but
|
|
41
|
+
// BEFORE the 404 handler below, since it's the last real route.
|
|
42
|
+
app.get('/{*path}', (req, res) => {
|
|
43
|
+
const indexPath = path_1.default.join(publicDir, 'index.html');
|
|
44
|
+
res.sendFile(indexPath, (err) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
res.status(404).json({
|
|
47
|
+
message: 'Dashboard not built yet.',
|
|
48
|
+
hint: 'Run "npm run build" inside the frontend/ folder, then restart the backend.',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
28
52
|
});
|
|
53
|
+
// ── Error handler ────────────────────────────────────────────────
|
|
29
54
|
app.use((err, _req, res, _next) => {
|
|
30
55
|
console.error(err.stack);
|
|
31
56
|
res.status(500).json({ message: 'Internal Server Error' });
|
|
32
57
|
});
|
|
58
|
+
// ── Start ─────────────────────────────────────────────────────────
|
|
33
59
|
app.listen(PORT, () => {
|
|
34
60
|
console.log(`🩺 React Doctor backend running on http://localhost:${PORT}`);
|
|
35
61
|
});
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// Returns the full report for one run — static + runtime +
|
|
15
15
|
// suggestions all parsed back to objects.
|
|
16
16
|
//
|
|
17
|
+
// GET /api/reports/:id/screenshots
|
|
18
|
+
// Returns all screenshots for a report as base64 data URLs.
|
|
19
|
+
//
|
|
17
20
|
// GET /api/reports/project/:name
|
|
18
21
|
// All runs for a named project, summary only.
|
|
19
22
|
//
|
|
@@ -39,8 +42,8 @@ const express_1 = require("express");
|
|
|
39
42
|
const path_1 = __importDefault(require("path"));
|
|
40
43
|
const fs_1 = __importDefault(require("fs"));
|
|
41
44
|
const db_1 = __importDefault(require("../db"));
|
|
45
|
+
const db_2 = require("../db");
|
|
42
46
|
const auth_1 = require("../middleware/auth");
|
|
43
|
-
const screenshotsDir = "data/screenshots";
|
|
44
47
|
const router = (0, express_1.Router)();
|
|
45
48
|
// ── GET /api/reports ─────────────────────────────────────────
|
|
46
49
|
// Summary list — no blobs, just the columns the history page needs.
|
|
@@ -76,6 +79,87 @@ router.get("/project/:name", (req, res) => {
|
|
|
76
79
|
res.status(500).json({ error: "Internal server error" });
|
|
77
80
|
}
|
|
78
81
|
});
|
|
82
|
+
// ── GET /api/reports/:id/screenshots ──────────────────────────
|
|
83
|
+
// Returns all screenshots for a report as base64 data URLs.
|
|
84
|
+
// This is used by the dashboard to display screenshots.
|
|
85
|
+
router.get("/:id/screenshots", (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const reportId = req.params.id;
|
|
88
|
+
// First, check if screenshots table exists
|
|
89
|
+
const tableCheck = db_1.default.prepare(`
|
|
90
|
+
SELECT name FROM sqlite_master
|
|
91
|
+
WHERE type='table' AND name='screenshots'
|
|
92
|
+
`).get();
|
|
93
|
+
if (!tableCheck) {
|
|
94
|
+
return res.json({ screenshots: [] });
|
|
95
|
+
}
|
|
96
|
+
// Get screenshots from the database
|
|
97
|
+
const stmt = db_1.default.prepare(`
|
|
98
|
+
SELECT route, label, taken_at, data_url
|
|
99
|
+
FROM screenshots
|
|
100
|
+
WHERE report_id = ?
|
|
101
|
+
ORDER BY taken_at ASC
|
|
102
|
+
`);
|
|
103
|
+
const dbScreenshots = stmt.all(reportId);
|
|
104
|
+
if (dbScreenshots.length === 0) {
|
|
105
|
+
// Fallback: try to extract from runtime_json
|
|
106
|
+
const report = db_1.default.prepare(`
|
|
107
|
+
SELECT runtime_json FROM reports WHERE id = ?
|
|
108
|
+
`).get(reportId);
|
|
109
|
+
if (report) {
|
|
110
|
+
const runtime = JSON.parse(report.runtime_json || '{}');
|
|
111
|
+
const screenshots = [];
|
|
112
|
+
for (const [routeKey, routeData] of Object.entries(runtime)) {
|
|
113
|
+
const route = routeData;
|
|
114
|
+
if (route.screenshots && Array.isArray(route.screenshots)) {
|
|
115
|
+
for (const screenshot of route.screenshots) {
|
|
116
|
+
if (screenshot.dataUrl && screenshot.dataUrl.startsWith('/screenshots/')) {
|
|
117
|
+
const filename = screenshot.dataUrl.replace('/screenshots/', '');
|
|
118
|
+
const filePath = path_1.default.join(db_2.screenshotsDir, filename);
|
|
119
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
120
|
+
try {
|
|
121
|
+
const imageBuffer = fs_1.default.readFileSync(filePath);
|
|
122
|
+
const base64Image = imageBuffer.toString('base64');
|
|
123
|
+
screenshots.push({
|
|
124
|
+
route: routeKey,
|
|
125
|
+
label: screenshot.label || 'screenshot',
|
|
126
|
+
taken_at: screenshot.takenAt || 0,
|
|
127
|
+
data_url: `data:image/png;base64,${base64Image}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
console.warn(`Could not read screenshot ${filename}: ${err}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (screenshot.dataUrl && screenshot.dataUrl.startsWith('data:image')) {
|
|
136
|
+
screenshots.push({
|
|
137
|
+
route: routeKey,
|
|
138
|
+
label: screenshot.label || 'screenshot',
|
|
139
|
+
taken_at: screenshot.takenAt || 0,
|
|
140
|
+
data_url: screenshot.dataUrl,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return res.json({ screenshots });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Return screenshots from database
|
|
150
|
+
const screenshots = dbScreenshots.map((s) => ({
|
|
151
|
+
route: s.route,
|
|
152
|
+
label: s.label,
|
|
153
|
+
taken_at: s.taken_at,
|
|
154
|
+
data_url: s.data_url,
|
|
155
|
+
}));
|
|
156
|
+
res.json({ screenshots });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
console.error("GET /:id/screenshots error:", err.message);
|
|
160
|
+
res.status(500).json({ error: "Internal server error" });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
79
163
|
// ── POST /api/reports/upload ──────────────────────────────────
|
|
80
164
|
// Receives a FinalReport from the CLI, strips screenshots to disk,
|
|
81
165
|
// and stores the three JSON blobs in separate columns.
|
|
@@ -99,9 +183,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
|
|
|
99
183
|
// ── Extract grade from static report ────────────────────
|
|
100
184
|
const grade = body.static?.grade ?? "N/A";
|
|
101
185
|
// ── Strip screenshots from runtime, save as .png files ──
|
|
102
|
-
// We do this BEFORE inserting so the DB never holds base64.
|
|
103
|
-
// The row ID isn't known yet — we'll rename after insert.
|
|
104
|
-
// For now we use a temp prefix and rename below.
|
|
105
186
|
const { cleanedRuntime, pendingScreenshots } = extractScreenshots(body.runtime);
|
|
106
187
|
// ── Insert the row ───────────────────────────────────────
|
|
107
188
|
const stmt = db_1.default.prepare(`
|
|
@@ -115,7 +196,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
|
|
|
115
196
|
// ── Save screenshots with final filenames ────────────────
|
|
116
197
|
const savedScreenshots = saveScreenshots(reportId, pendingScreenshots);
|
|
117
198
|
// ── Patch runtime_json with final screenshot paths ───────
|
|
118
|
-
// Now that we have the reportId we can write the correct paths.
|
|
119
199
|
if (savedScreenshots.length > 0) {
|
|
120
200
|
const patchedRuntime = patchScreenshotPaths(cleanedRuntime, savedScreenshots);
|
|
121
201
|
db_1.default.prepare("UPDATE reports SET runtime_json = ? WHERE id = ?").run(JSON.stringify(patchedRuntime), reportId);
|
|
@@ -148,7 +228,6 @@ router.get("/:id", (req, res) => {
|
|
|
148
228
|
grade: row.grade,
|
|
149
229
|
analyzedAt: row.analyzed_at,
|
|
150
230
|
createdAt: row.created_at,
|
|
151
|
-
// Parse the three JSON blobs back into objects
|
|
152
231
|
static: JSON.parse(row.static_json),
|
|
153
232
|
runtime: JSON.parse(row.runtime_json),
|
|
154
233
|
suggestions: JSON.parse(row.suggestions),
|
|
@@ -170,10 +249,6 @@ function getMissingFields(body) {
|
|
|
170
249
|
/**
|
|
171
250
|
* Walk the runtime map, strip every screenshot.dataUrl,
|
|
172
251
|
* and collect them as Buffers ready to write to disk.
|
|
173
|
-
*
|
|
174
|
-
* Returns:
|
|
175
|
-
* cleanedRuntime — runtime map with dataUrls replaced by tempPath markers
|
|
176
|
-
* pendingScreenshots — list of screenshots to save once we have a reportId
|
|
177
252
|
*/
|
|
178
253
|
function extractScreenshots(runtime) {
|
|
179
254
|
const pending = [];
|
|
@@ -182,17 +257,24 @@ function extractScreenshots(runtime) {
|
|
|
182
257
|
const routeClone = { ...routeData };
|
|
183
258
|
if (Array.isArray(routeClone.screenshots)) {
|
|
184
259
|
routeClone.screenshots = routeClone.screenshots.map((shot) => {
|
|
185
|
-
if
|
|
260
|
+
// Check if it's a base64 data URL
|
|
261
|
+
if (shot.dataUrl && shot.dataUrl.startsWith("data:image/png;base64,")) {
|
|
262
|
+
const base64 = shot.dataUrl.replace("data:image/png;base64,", "");
|
|
263
|
+
const buffer = Buffer.from(base64, "base64");
|
|
264
|
+
const safeRoute = routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
|
|
265
|
+
const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
|
|
266
|
+
const tempPath = `__PENDING__${safeRoute}__${safeLabel}`;
|
|
267
|
+
pending.push({ routeKey, label: shot.label, buffer, tempPath });
|
|
268
|
+
return { ...shot, dataUrl: tempPath };
|
|
269
|
+
}
|
|
270
|
+
else if (shot.dataUrl && shot.dataUrl.startsWith('/screenshots/')) {
|
|
271
|
+
// Already a path - keep it
|
|
186
272
|
return shot;
|
|
187
273
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
|
|
193
|
-
const tempPath = `__PENDING__${safeRoute}__${safeLabel}`;
|
|
194
|
-
pending.push({ routeKey, label: shot.label, buffer, tempPath });
|
|
195
|
-
return { ...shot, dataUrl: tempPath };
|
|
274
|
+
else {
|
|
275
|
+
// No valid dataUrl - keep as is or set to null
|
|
276
|
+
return { ...shot, dataUrl: null };
|
|
277
|
+
}
|
|
196
278
|
});
|
|
197
279
|
}
|
|
198
280
|
cleaned[routeKey] = routeClone;
|
|
@@ -201,15 +283,17 @@ function extractScreenshots(runtime) {
|
|
|
201
283
|
}
|
|
202
284
|
/**
|
|
203
285
|
* Write each screenshot buffer to data/screenshots/<reportId>-<route>-<label>.png
|
|
204
|
-
* Returns the list of saved files with their final URL paths.
|
|
205
286
|
*/
|
|
206
287
|
function saveScreenshots(reportId, pending) {
|
|
207
288
|
const saved = [];
|
|
289
|
+
if (!fs_1.default.existsSync(db_2.screenshotsDir)) {
|
|
290
|
+
fs_1.default.mkdirSync(db_2.screenshotsDir, { recursive: true });
|
|
291
|
+
}
|
|
208
292
|
for (const shot of pending) {
|
|
209
293
|
const safeRoute = shot.routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
|
|
210
294
|
const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
|
|
211
295
|
const filename = `${reportId}-${safeRoute}-${safeLabel}.png`;
|
|
212
|
-
const fullPath = path_1.default.join(screenshotsDir, filename);
|
|
296
|
+
const fullPath = path_1.default.join(db_2.screenshotsDir, filename);
|
|
213
297
|
try {
|
|
214
298
|
fs_1.default.writeFileSync(fullPath, shot.buffer);
|
|
215
299
|
saved.push({
|
|
@@ -230,7 +314,6 @@ function saveScreenshots(reportId, pending) {
|
|
|
230
314
|
* the final /screenshots/<file> URL paths.
|
|
231
315
|
*/
|
|
232
316
|
function patchScreenshotPaths(runtime, saved) {
|
|
233
|
-
// Build a lookup from tempPath → filePath
|
|
234
317
|
const lookup = {};
|
|
235
318
|
for (const s of saved)
|
|
236
319
|
lookup[s.tempPath] = s.filePath;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:300;src:url(/assets/tajawal-arabic-300-normal-By07C9pa.woff2)format("woff2"),url(/assets/tajawal-arabic-300-normal-Bq0yWa0Z.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:300;src:url(/assets/tajawal-latin-300-normal-CeEKeOxZ.woff2)format("woff2"),url(/assets/tajawal-latin-300-normal-C0-xR3ms.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/tajawal-arabic-400-normal-CyCXRvzh.woff2)format("woff2"),url(/assets/tajawal-arabic-400-normal-DCQxawbB.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/tajawal-latin-400-normal-BVNSOH3d.woff2)format("woff2"),url(/assets/tajawal-latin-400-normal-BdYcZznU.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/tajawal-arabic-500-normal-BZ8ojJNu.woff2)format("woff2"),url(/assets/tajawal-arabic-500-normal-CbVEaYEW.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/tajawal-latin-500-normal-CoYeBiSI.woff2)format("woff2"),url(/assets/tajawal-latin-500-normal-DU9v6xgj.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/tajawal-arabic-700-normal-D2-eand5.woff2)format("woff2"),url(/assets/tajawal-arabic-700-normal-9L7Zusdl.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/tajawal-latin-700-normal-BypgxfGb.woff2)format("woff2"),url(/assets/tajawal-latin-700-normal-CV3bxpHe.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAASIABAAAAAACQQAAAQuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVF4AdBEICoRkhAALIAABNgIkAzoEIAWFAAeBFAwHG2oHKJ6DceNW4lJEFHn9tEOE37LNsvkjHr7f739rn3u+OSrSxLQytE91Dw2PYtNVq3Qaw/vJ7fUBMdOj/u2ImB98cT5WUx9F13ZKt06mU7tG1sAYcM26yCRX4f/0904bmwq8hwkUUMTRX61pa9C3xlpAGYw1vOu8C2SBZZFlld7DAW82RJWmwYMI1AJCKBNCIASqdZGFmtPSNQRhMpR0EKjWTwm6z6sJ+4jqhxjovTinVdRpZaQjaQzAYjI8NgAKDJCokgunCn9oUQE8VZd/F//+v6P4F1U9gLr58yNI/dJX9BAshEoJbTDogz7ocKoUsPvES8UK/aQIUQgpGgECBVLkOEV6iASTYgNAPKdNXayhDPL7IJuAZnEYSZ0eOLc9i5Rv5/+lEi3sW/kfnCf/+fTe0sxS7CKLx5erRJkKOixEqQaoqrvx5HN4iz4VhX0gS6DjFIWUzn/fIXJRVFY1NFRVNVeFt+SYmrK6vCU6eJsnKnJj5HTESMTQZOTI3Y/tzr3rUcOsiRy/ciP50s3Mycgxmhz9GMXhjzVH30ut796NHP0UUa31/Zoa6vCH6iPvdIM0IRANaT60FablmZlZd+UKNFy9NrvnFGxb5NC2CRxR0/rIyF/WNn+35sv9r+tHVhkseuWGKkCPPh96+GXt8Lc/Sh24ujfjf+tNw1lZWreiPuHb/PSpyjdv2rf/++ZE5TSKjFdZxYsBvr1sEHl5STuHZqYR7jGYGvGBBdjcaB5bODY1GAxMjsd0wDMIzXAXODRd74DxNMtO4YuYHclgLQRzp9KlUmxptCwv9bYgvWGD2xBW7r9413fdJu+mxzvzAUYeeONQvZP4kgfqZle4jhH/MWh+UbxgtJdrLdXLymNL53PRU0D3Q6FcXTfGqGcul6vrqhgLGAVSv+IVIfZOHpu+mlLqSlnj903j8mazXDsba/wbxhS/4Oubh1c5/uXDgt4j5KtnGEy5BIJHo0ur+jD+r2qCkuP1aRVM8EpUoRYSJyqNRA38uwDvHBNo0AejO2Z+ZYLLal1QiFyFh1EDQOgSCSCpjwwKU8yDbIjroEwne0G5Qca49cYp4AlqtQNZfWTQoFb1tRpFYAmRD+HXTkV4uQhG2bg4OKmyoypAsUGtWgQXyFxYrkpODeUxvGqIZA61erXo0sFG1UBGuPgpuon8RNUG2DhovAjZCBuZwtXrU3mQGki9+pm0eVZAXoWxLWTQoYHBPrJR4WunNxIFbCHfkTyPK+sqxjxDmGArZvf79JGJ3GwEqjwc7a7NIrl/7bJ7Nzu4+n1Ow6shEPnUrtVo7cnd5Wi5qCUbBIQbNKCP95FMCSZylEV5VCiiMqqiOmrKZ/I/C0gI1fyuulVcM2E1r4MhtcV/fbCD+HwEvVzH1dGNAzFE0FTbKzyQz3gootrNbN2a4PuG0j0JOgE=)format("woff2"),url(data:font/woff;base64,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)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2)format("woff2"),url(/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2)format("woff2"),url(/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,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)format("woff2"),url(/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(data:font/woff2;base64,d09GMgABAAAAAASUABAAAAAACRAAAAQ3AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVEwAdBEICoRkg3oLIAABNgIkAzoEIAWFHgeBFAwHG3AHKB4HzukrRjyF5Gb4kQ/B873d17mv8TWkoqUUGaHodJCtoqntw7m8tiPWyGkfWDU/IMpzVk4zAs9Czv3c7Os5NvKFG1Ht9vBVXySRZpJc4tTrbM3sTRFcLa3zVboKWRIoAksKSKgqUQWEwlSoqjp44V5J9/kjd0+Fi5xaFjwJArgBAIJgIAgCCAI4CdNMqGuYXYUKdADLAkAAJwl6iX0+bZqEm1AH0Hu9rvrJHwxQG1Q/gFYwWYbgTRWgwEESVzbmtjQAt3DXO7Tu/+9Q/KNKWhTCgZ8fQSWoL0CUUgTBjgL8iEAcce7YHBeA8ZvogieCnxI0BCVeACCAAhBD/oDcAdUPABSgAX4kBPBTwjCAjwYSCneiqF65LUo/qXdYFrRwTujtMrKm07plTbSqLQFAiHBN7E4DG5wkiGXhCYBNETh3K/AciHN69iFfQjFOr9EXQQAAQXRNszs8PR0OH4fDV/dwGe76CHdj1MQa11DXYGcfZ6+B1X0OfwjY+i7KpQ1w9d+1z3/HydCB1f3U8A0fRFv+0bXxPWrZu3d9Nn4UcbLs/QCXLH/j3PCKblKOiZxLOR78W9brPo7eGTLz/F27ZPmePQdCB1YP4nJU91o4gU9cy64L3c3SPu8GfDn6ZXD2cGmllVqSxtD5vCXHm5xaeHlnpffx7DcPvXog5NnjA//cPz2btsz3yf+0MI8ri33tuv7upNbYmW5STbosW8bYItV/SPzxvXC0KnpWnFdC91oojit5Lyo6WcJQ1YzwBYbi52ohJVpcxfcHBqsv0nFz8U74rzS6M7r0RVlXZ3jJC4G92O//7Lm78VOtnUx+aQxAzsqPnrRr1t9zDrezKePvZ027x+fGte4s4NXu7/jEp3G9cM1bICYgACbuETUUD5wmenGXA/IBpAKVrl0AEJ5cqr6Q89tGq+SpqkDfX2DtG+pZ9teuae0AX98c301CX179InyErnSG8WCiABB4HFbVrw/lpTdBIOk82nXbpHKoQGyBpUovAAD4XQA8ZTUCeBJHIpI0v0FRrgcnckV3ANckrwYJKXEihQfdSKPER6XeExmEeEMmSe6QBwWmkZdmSOSthhibgIkBlUYreBKd/0NeVAE9WAyEaQpH6KikDWlUVg43Jy2FOmaYlsZhDEGZXO3mWEyWHI6zuaJkMlcSQ9k4F+KidIiFyZItGDc7r1pUj6wP6zrWB0/fbeeFeHl4YOvETMNiM8Ny+7OO6Qh2wur20YVxFts5dlORdl5Kwd5utR7SRcxqX8RA5Ehacha76hg7c6yzNm/Xf6vOLNrv544lIEkWg7xWo/ItBAeIJEmt0XgBwgM8gTifoiiLJroYYooNTeziEKe4zOoR/wvIKp89hSr+6jFAqw/LaonjmZHSscMNg4iPlWpFrO1RN3zkMU+/xUP+JQ99TbvTl1Vg0gtirT6lbwYAAAA=)format("woff2"),url(data:font/woff;base64,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)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2)format("woff2"),url(/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2)format("woff2"),url(/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(data:font/woff2;base64,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)format("woff2"),url(/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff)format("woff");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--bg:#0b0c10;--bg2:#0f1117;--bg3:#161b22;--bg4:#1c2333;--border:#21262d;--border2:#30363d;--text:#e6edf3;--text2:#8b949e;--text3:#6e7681;--accent:#c778dd;--accent2:#b05fc9;--accent-rgb:199, 120, 221;--teal:#00ffc2;--teal-rgb:0, 255, 194;--purple:#c778dd;--blue:#58a6ff;--orange:#f0883e;--red:#ff7b72;--green:#3fb950;--yellow:#d29922;--sidebar-w:240px;--radius:10px;--radius-sm:6px;--shadow:0 4px 24px #0006;--transition:.18s ease}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth;font-size:15px}body{background:var(--bg);color:var(--text);min-height:100vh;font-family:Tajawal,system-ui,sans-serif;display:flex;overflow-x:hidden}#sidebar{width:var(--sidebar-w);background:var(--bg2);border-right:1px solid var(--border);z-index:100;flex-direction:column;min-height:100vh;display:flex;position:fixed;top:0;bottom:0;left:0}.sidebar-logo{border-bottom:1px solid var(--border);align-items:center;gap:10px;padding:22px 20px 18px;display:flex}.logo-icon{background:linear-gradient(135deg, var(--accent), var(--teal));border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;width:34px;height:34px;font-size:18px;display:flex}.logo-text{letter-spacing:-.01em;font-size:.95rem;font-weight:700;line-height:1.2}.logo-sub{color:var(--text3);letter-spacing:.05em;text-transform:uppercase;font-size:.65rem;font-weight:400}.sidebar-section{letter-spacing:.12em;text-transform:uppercase;color:var(--text3);padding:18px 16px 6px;font-size:.6rem;font-weight:700}.nav-item{border-radius:var(--radius-sm);cursor:pointer;color:var(--text2);transition:background var(--transition), color var(--transition);text-align:left;background:0 0;border:none;align-items:center;gap:10px;width:calc(100% - 16px);margin:1px 8px;padding:9px 14px;font-size:.85rem;font-weight:500;display:flex}.nav-item:hover{background:var(--bg4);color:var(--text)}.nav-item.active{background:rgba(var(--accent-rgb),.1);color:var(--accent)}.nav-icon{text-align:center;flex-shrink:0;width:20px;font-size:1rem}.nav-badge{background:var(--bg4);color:var(--text3);border-radius:20px;margin-left:auto;padding:2px 7px;font-size:.64rem;font-weight:700}.nav-item.active .nav-badge{background:rgba(var(--accent-rgb),.15);color:var(--accent)}.sidebar-footer{border-top:1px solid var(--border);margin-top:auto;padding:16px}.project-pill{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:9px 12px;display:flex}.status-dot{background:var(--accent);width:7px;height:7px;box-shadow:0 0 6px var(--accent);border-radius:50%;flex-shrink:0}.proj-name{color:var(--text);font-size:.8rem;font-weight:600}.proj-sub{color:var(--text3);margin-top:1px;font-size:.68rem}#main{margin-left:var(--sidebar-w);flex-direction:column;flex:1;min-height:100vh;display:flex}.topbar{border-bottom:1px solid var(--border);background:var(--bg2);z-index:50;align-items:center;gap:12px;height:54px;padding:0 28px;display:flex;position:sticky;top:0}.topbar-title{font-size:.95rem;font-weight:600}.topbar-sub{color:var(--text3);font-size:.75rem}.topbar-right{align-items:center;gap:12px;margin-left:auto;display:flex}.topbar-time{color:var(--text3);font-family:JetBrains Mono,monospace;font-size:.7rem}.page-content{flex:1;padding:26px 28px;display:none}.page-content.active{display:block}.section-gap{margin-bottom:20px}.card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:20px}.card-title{text-transform:uppercase;letter-spacing:.1em;color:var(--text3);align-items:center;gap:8px;margin-bottom:16px;font-size:.72rem;font-weight:700;display:flex}.grid-2{grid-template-columns:1fr 1fr;gap:16px;display:grid}.grid-3{grid-template-columns:repeat(3,1fr);gap:16px;display:grid}.grid-4{grid-template-columns:repeat(4,1fr);gap:16px;display:grid}.grid-5{grid-template-columns:repeat(5,1fr);gap:16px;display:grid}.stat-tile{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;position:relative;overflow:hidden}.stat-tile:before{content:"";height:2px;position:absolute;top:0;left:0;right:0}.stat-tile.good:before{background:var(--green)}.stat-tile.warn:before{background:var(--orange)}.stat-tile.bad:before{background:var(--red)}.stat-tile.blue:before{background:var(--blue)}.stat-tile.purple:before{background:var(--teal)}.stat-label{text-transform:uppercase;letter-spacing:.1em;color:var(--text3);margin-bottom:8px;font-size:.68rem;font-weight:700}.stat-value{letter-spacing:-.02em;font-family:JetBrains Mono,monospace;font-size:1.85rem;font-weight:700;line-height:1}.stat-value.good{color:var(--green)}.stat-value.warn{color:var(--orange)}.stat-value.bad{color:var(--red)}.stat-value.blue{color:var(--blue)}.stat-value.purple{color:var(--teal)}.stat-meta{color:var(--text3);margin-top:6px;font-size:.7rem}.score-ring-wrap{align-items:center;gap:32px;display:flex}.score-ring{flex-shrink:0;width:140px;height:140px;position:relative}.score-ring svg{transform:rotate(-90deg)}.ring-bg{fill:none;stroke:var(--bg4);stroke-width:10px}.ring-fg{fill:none;stroke-width:10px;stroke-linecap:round;transition:stroke-dashoffset 1.4s cubic-bezier(.4,0,.2,1)}.ring-label{text-align:center;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.ring-num{font-family:JetBrains Mono,monospace;font-size:2rem;font-weight:700;line-height:1}.ring-grade{color:var(--text3);margin-top:3px;font-size:.75rem;font-weight:600}.score-details h2{margin-bottom:6px;font-size:1.05rem;font-weight:700}.score-details p{color:var(--text2);font-size:.8rem;line-height:1.6}.badge{letter-spacing:.04em;text-transform:uppercase;border-radius:20px;align-items:center;gap:4px;padding:2px 8px;font-size:.68rem;font-weight:700;display:inline-flex}.badge.critical{color:var(--red);background:#ff7b7226;border:1px solid #ff7b724d}.badge.warning{color:var(--orange);background:#f0883e26;border:1px solid #f0883e4d}.badge.info{color:var(--blue);background:#58a6ff1f;border:1px solid #58a6ff40}.badge.good{color:var(--green);background:#3fb9501f;border:1px solid #3fb95040}.badge.grade-a{background:rgba(var(--accent-rgb),.12);color:var(--accent);border:1px solid rgba(var(--accent-rgb),.25);padding:3px 12px;font-size:.78rem}.badge.grade-b{color:var(--blue);background:#58a6ff1f;border:1px solid #58a6ff40;padding:3px 12px;font-size:.78rem}.badge.grade-c{color:var(--orange);background:#f0883e1f;border:1px solid #f0883e4d;padding:3px 12px;font-size:.78rem}.badge.grade-d{color:var(--red);background:#ff7b721f;border:1px solid #ff7b724d;padding:3px 12px;font-size:.78rem}.rd-table{border-collapse:collapse;width:100%;font-size:.82rem}.rd-table th{text-align:left;text-transform:uppercase;letter-spacing:.1em;color:var(--text3);border-bottom:1px solid var(--border);white-space:nowrap;padding:10px 14px;font-size:.64rem;font-weight:700}.rd-table td{border-bottom:1px solid var(--border);color:var(--text2);vertical-align:middle;padding:11px 14px}.rd-table tr:last-child td{border-bottom:none}.rd-table tbody tr:hover td{background:var(--bg3)}.rd-table .mono{color:var(--blue);font-family:JetBrains Mono,monospace;font-size:.78rem}.rd-table .score-cell{font-family:JetBrains Mono,monospace;font-weight:700}.suggestion-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg2);align-items:flex-start;gap:14px;padding:16px 18px;display:flex}.suggestion-card+.suggestion-card{margin-top:10px}.sug-icon{border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;width:36px;height:36px;font-size:1rem;display:flex}.sug-icon.critical{background:#ff7b721f}.sug-icon.warning{background:#f0883e1f}.sug-icon.info{background:#58a6ff1f}.sug-body{flex:1;min-width:0}.sug-title{align-items:center;gap:8px;margin-bottom:4px;font-size:.88rem;font-weight:600;display:flex}.sug-desc{color:var(--text2);margin-bottom:8px;font-size:.78rem;line-height:1.55}.sug-fix{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--accent);word-break:break-word;padding:8px 12px;font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.5}.sug-component{color:var(--text3);margin-top:6px;font-family:JetBrains Mono,monospace;font-size:.7rem}.filmstrip{gap:14px;padding-bottom:4px;display:flex;overflow-x:auto}.filmstrip::-webkit-scrollbar{height:4px}.filmstrip::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}.film-frame{text-align:center;flex-shrink:0}.film-img{object-fit:cover;border-radius:var(--radius-sm);border:2px solid var(--border);background:var(--bg3);cursor:pointer;width:210px;height:128px;transition:border-color var(--transition), transform var(--transition);display:block}.film-img:hover{border-color:var(--accent);transform:scale(1.02)}.film-label{text-transform:uppercase;letter-spacing:.08em;color:var(--text3);margin-top:6px;font-size:.66rem;font-weight:700}.film-time{color:var(--accent);font-family:JetBrains Mono,monospace;font-size:.7rem}.film-placeholder{border-radius:var(--radius-sm);border:2px dashed var(--border2);background:var(--bg3);width:210px;height:128px;color:var(--text3);flex-direction:column;justify-content:center;align-items:center;gap:6px;font-size:.72rem;display:flex}#lightbox{z-index:9999;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#000000e0;justify-content:center;align-items:center;display:none;position:fixed;inset:0}#lightbox.open{display:flex}#lightbox img{border-radius:var(--radius);border:1px solid var(--border2);max-width:90vw;max-height:86vh;box-shadow:var(--shadow)}#lb-close{background:var(--bg3);border:1px solid var(--border2);color:var(--text);cursor:pointer;border-radius:50%;justify-content:center;align-items:center;width:36px;height:36px;font-size:1.1rem;display:flex;position:fixed;top:20px;right:24px}#lb-caption{color:var(--text2);background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:6px 16px;font-size:.8rem;position:fixed;bottom:24px;left:50%;transform:translate(-50%)}.vital-row{margin-bottom:14px}.vital-row-head{justify-content:space-between;margin-bottom:5px;font-size:.75rem;font-weight:600;display:flex}.vital-row-head .vname{color:var(--text2)}.vital-row-head .vval{font-family:JetBrains Mono,monospace;font-size:.77rem}.vital-track{background:var(--bg4);border-radius:3px;height:6px;overflow:hidden}.vital-fill{border-radius:3px;height:100%;transition:width 1s cubic-bezier(.4,0,.2,1)}.pagination{flex-flow:row;justify-content:center;align-items:center;gap:6px;margin-top:20px;display:flex;overflow-x:auto}.page-btn{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg2);min-width:32px;height:32px;color:var(--text2);cursor:pointer;transition:all var(--transition);justify-content:center;align-items:center;padding:0 8px;font-size:.78rem;display:flex}.page-btn:hover:not(:disabled){background:var(--bg4);color:var(--text);border-color:var(--border2)}.page-btn.active{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:700}.page-btn:disabled{opacity:.35;cursor:not-allowed}.page-info{color:var(--text3);padding:0 6px;font-size:.73rem}.filter-bar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:16px;display:flex}.filter-btn{border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;transition:all var(--transition);border-radius:20px;padding:5px 14px;font-size:.73rem;font-weight:600}.filter-btn:hover{border-color:var(--border2);color:var(--text)}.filter-btn.active{background:rgba(var(--accent-rgb),.1);border-color:var(--accent);color:var(--accent)}.filter-btn.fc.active{border-color:var(--red);color:var(--red);background:#ff7b721a}.filter-btn.fw.active{border-color:var(--orange);color:var(--orange);background:#f0883e1a}.filter-btn.fi.active{border-color:var(--blue);color:var(--blue);background:#58a6ff1a}.route-tabs{flex-wrap:wrap;gap:6px;margin-bottom:20px;display:flex}.route-tab{border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;transition:all var(--transition);padding:6px 14px;font-family:JetBrains Mono,monospace;font-size:.76rem;font-weight:500}.route-tab:hover{border-color:var(--border2);color:var(--text)}.route-tab.active{background:rgba(var(--accent-rgb),.08);border-color:var(--accent);color:var(--accent)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
|