react-doctor-cli-dev 1.0.7 → 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 +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/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
|
@@ -7,15 +7,12 @@ exports.screenshotsDir = void 0;
|
|
|
7
7
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const PACKAGE_ROOT = path_1.default.resolve(__dirname, "..", "..");
|
|
13
|
-
const DEFAULT_DB = path_1.default.join(PACKAGE_ROOT, "backend", "data", "reports.db");
|
|
14
|
-
const dbPath = process.env.DB_PATH || DEFAULT_DB;
|
|
15
|
-
fs_1.default.mkdirSync(path_1.default.dirname(dbPath), { recursive: true });
|
|
16
|
-
fs_1.default.mkdirSync(path_1.default.join(path_1.default.dirname(dbPath), "screenshots"), { recursive: true });
|
|
10
|
+
// ── Initialize database ──────────────────────────────────────
|
|
11
|
+
const dbPath = process.env.DB_PATH || path_1.default.join(__dirname, '../../reports.db');
|
|
17
12
|
const db = new better_sqlite3_1.default(dbPath);
|
|
18
|
-
//
|
|
13
|
+
// ── Enable foreign keys ──────────────────────────────────────
|
|
14
|
+
db.pragma('foreign_keys = ON');
|
|
15
|
+
// ── Create reports table if it doesn't exist ──────────────
|
|
19
16
|
db.exec(`
|
|
20
17
|
CREATE TABLE IF NOT EXISTS reports (
|
|
21
18
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -23,11 +20,36 @@ db.exec(`
|
|
|
23
20
|
score INTEGER NOT NULL,
|
|
24
21
|
grade TEXT NOT NULL,
|
|
25
22
|
analyzed_at TEXT NOT NULL,
|
|
26
|
-
created_at TEXT
|
|
23
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
27
24
|
static_json TEXT NOT NULL,
|
|
28
25
|
runtime_json TEXT NOT NULL,
|
|
29
26
|
suggestions TEXT NOT NULL
|
|
30
|
-
)
|
|
27
|
+
)
|
|
31
28
|
`);
|
|
32
|
-
|
|
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 ──────
|
|
33
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
|
//
|
|
@@ -31,39 +34,6 @@
|
|
|
31
34
|
// /screenshots/<filename>
|
|
32
35
|
// so the dashboard can load them as normal <img> tags.
|
|
33
36
|
// ─────────────────────────────────────────────────────────────
|
|
34
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
35
|
-
if (k2 === undefined) k2 = k;
|
|
36
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
37
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
38
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
39
|
-
}
|
|
40
|
-
Object.defineProperty(o, k2, desc);
|
|
41
|
-
}) : (function(o, m, k, k2) {
|
|
42
|
-
if (k2 === undefined) k2 = k;
|
|
43
|
-
o[k2] = m[k];
|
|
44
|
-
}));
|
|
45
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
46
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
47
|
-
}) : function(o, v) {
|
|
48
|
-
o["default"] = v;
|
|
49
|
-
});
|
|
50
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
51
|
-
var ownKeys = function(o) {
|
|
52
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
53
|
-
var ar = [];
|
|
54
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
55
|
-
return ar;
|
|
56
|
-
};
|
|
57
|
-
return ownKeys(o);
|
|
58
|
-
};
|
|
59
|
-
return function (mod) {
|
|
60
|
-
if (mod && mod.__esModule) return mod;
|
|
61
|
-
var result = {};
|
|
62
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
63
|
-
__setModuleDefault(result, mod);
|
|
64
|
-
return result;
|
|
65
|
-
};
|
|
66
|
-
})();
|
|
67
37
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
68
38
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
69
39
|
};
|
|
@@ -71,7 +41,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
71
41
|
const express_1 = require("express");
|
|
72
42
|
const path_1 = __importDefault(require("path"));
|
|
73
43
|
const fs_1 = __importDefault(require("fs"));
|
|
74
|
-
const db_1 =
|
|
44
|
+
const db_1 = __importDefault(require("../db"));
|
|
45
|
+
const db_2 = require("../db");
|
|
75
46
|
const auth_1 = require("../middleware/auth");
|
|
76
47
|
const router = (0, express_1.Router)();
|
|
77
48
|
// ── GET /api/reports ─────────────────────────────────────────
|
|
@@ -108,6 +79,87 @@ router.get("/project/:name", (req, res) => {
|
|
|
108
79
|
res.status(500).json({ error: "Internal server error" });
|
|
109
80
|
}
|
|
110
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
|
+
});
|
|
111
163
|
// ── POST /api/reports/upload ──────────────────────────────────
|
|
112
164
|
// Receives a FinalReport from the CLI, strips screenshots to disk,
|
|
113
165
|
// and stores the three JSON blobs in separate columns.
|
|
@@ -131,9 +183,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
|
|
|
131
183
|
// ── Extract grade from static report ────────────────────
|
|
132
184
|
const grade = body.static?.grade ?? "N/A";
|
|
133
185
|
// ── Strip screenshots from runtime, save as .png files ──
|
|
134
|
-
// We do this BEFORE inserting so the DB never holds base64.
|
|
135
|
-
// The row ID isn't known yet — we'll rename after insert.
|
|
136
|
-
// For now we use a temp prefix and rename below.
|
|
137
186
|
const { cleanedRuntime, pendingScreenshots } = extractScreenshots(body.runtime);
|
|
138
187
|
// ── Insert the row ───────────────────────────────────────
|
|
139
188
|
const stmt = db_1.default.prepare(`
|
|
@@ -147,7 +196,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
|
|
|
147
196
|
// ── Save screenshots with final filenames ────────────────
|
|
148
197
|
const savedScreenshots = saveScreenshots(reportId, pendingScreenshots);
|
|
149
198
|
// ── Patch runtime_json with final screenshot paths ───────
|
|
150
|
-
// Now that we have the reportId we can write the correct paths.
|
|
151
199
|
if (savedScreenshots.length > 0) {
|
|
152
200
|
const patchedRuntime = patchScreenshotPaths(cleanedRuntime, savedScreenshots);
|
|
153
201
|
db_1.default.prepare("UPDATE reports SET runtime_json = ? WHERE id = ?").run(JSON.stringify(patchedRuntime), reportId);
|
|
@@ -180,7 +228,6 @@ router.get("/:id", (req, res) => {
|
|
|
180
228
|
grade: row.grade,
|
|
181
229
|
analyzedAt: row.analyzed_at,
|
|
182
230
|
createdAt: row.created_at,
|
|
183
|
-
// Parse the three JSON blobs back into objects
|
|
184
231
|
static: JSON.parse(row.static_json),
|
|
185
232
|
runtime: JSON.parse(row.runtime_json),
|
|
186
233
|
suggestions: JSON.parse(row.suggestions),
|
|
@@ -202,10 +249,6 @@ function getMissingFields(body) {
|
|
|
202
249
|
/**
|
|
203
250
|
* Walk the runtime map, strip every screenshot.dataUrl,
|
|
204
251
|
* and collect them as Buffers ready to write to disk.
|
|
205
|
-
*
|
|
206
|
-
* Returns:
|
|
207
|
-
* cleanedRuntime — runtime map with dataUrls replaced by tempPath markers
|
|
208
|
-
* pendingScreenshots — list of screenshots to save once we have a reportId
|
|
209
252
|
*/
|
|
210
253
|
function extractScreenshots(runtime) {
|
|
211
254
|
const pending = [];
|
|
@@ -214,17 +257,24 @@ function extractScreenshots(runtime) {
|
|
|
214
257
|
const routeClone = { ...routeData };
|
|
215
258
|
if (Array.isArray(routeClone.screenshots)) {
|
|
216
259
|
routeClone.screenshots = routeClone.screenshots.map((shot) => {
|
|
217
|
-
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
|
|
218
272
|
return shot;
|
|
219
273
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
|
|
225
|
-
const tempPath = `__PENDING__${safeRoute}__${safeLabel}`;
|
|
226
|
-
pending.push({ routeKey, label: shot.label, buffer, tempPath });
|
|
227
|
-
return { ...shot, dataUrl: tempPath };
|
|
274
|
+
else {
|
|
275
|
+
// No valid dataUrl - keep as is or set to null
|
|
276
|
+
return { ...shot, dataUrl: null };
|
|
277
|
+
}
|
|
228
278
|
});
|
|
229
279
|
}
|
|
230
280
|
cleaned[routeKey] = routeClone;
|
|
@@ -233,15 +283,17 @@ function extractScreenshots(runtime) {
|
|
|
233
283
|
}
|
|
234
284
|
/**
|
|
235
285
|
* Write each screenshot buffer to data/screenshots/<reportId>-<route>-<label>.png
|
|
236
|
-
* Returns the list of saved files with their final URL paths.
|
|
237
286
|
*/
|
|
238
287
|
function saveScreenshots(reportId, pending) {
|
|
239
288
|
const saved = [];
|
|
289
|
+
if (!fs_1.default.existsSync(db_2.screenshotsDir)) {
|
|
290
|
+
fs_1.default.mkdirSync(db_2.screenshotsDir, { recursive: true });
|
|
291
|
+
}
|
|
240
292
|
for (const shot of pending) {
|
|
241
293
|
const safeRoute = shot.routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
|
|
242
294
|
const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
|
|
243
295
|
const filename = `${reportId}-${safeRoute}-${safeLabel}.png`;
|
|
244
|
-
const fullPath = path_1.default.join(
|
|
296
|
+
const fullPath = path_1.default.join(db_2.screenshotsDir, filename);
|
|
245
297
|
try {
|
|
246
298
|
fs_1.default.writeFileSync(fullPath, shot.buffer);
|
|
247
299
|
saved.push({
|
|
@@ -262,7 +314,6 @@ function saveScreenshots(reportId, pending) {
|
|
|
262
314
|
* the final /screenshots/<file> URL paths.
|
|
263
315
|
*/
|
|
264
316
|
function patchScreenshotPaths(runtime, saved) {
|
|
265
|
-
// Build a lookup from tempPath → filePath
|
|
266
317
|
const lookup = {};
|
|
267
318
|
for (const s of saved)
|
|
268
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,d09GMgABAAAAAA9UABAAAAAALMAAAA7zAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGnAbhWocghYGYD9TVEFUXgCDBBEICrBAp3ILgjgAATYCJAOEKAQgBYUAB4xODAcbIiYzo8LGAQCKfn4o/usELQ4rdj22ABHwitFYjZPqB3XSqAbV3C5dFkHYtTehxpeGbU+3+QexdERCkxARw9oNxAyetvXzdhdjl/7aiBiHgcqBjZywaGPSh4mNFccpF3DHlfyO8Jc/OiSe/kb7/iQYRAE23iScpHngiRRBFETNiZzQwc2HOllJ0fqXIjTY8RTYbDsklKc81hGPQcOJojhD/w80ll1aG/qiQErbzqOXjmvOVKd14tuv+Hxrlbarq/5xH7CiMPjaZXlrfNSp7uqaroZXvYAzc8A0kH2zs3MvHQBUORcJ2GG2cdFRgArh+Sgb7yOsjImpmgM0kq1FwhQCQzQ1Mt+j7at5GSAAIQAAKFLk6uMul4pIl4HIpEEUK0dUMiCMmhFWNoSDC+Exghgzi5i3hNhtN9qaINo+EbQj1tEIgJVLj5oebQ6I1/fyLMQYALYgALKS0yEOQCJG89uIiEPKyeRDVCmC0HgZ203jIg7kk6cHxfEKeBSDXtxd6n5yJzqpfSgXs9xMri1fPezlKMAVcCGEIhmwHiDRtJOyKfXkGsAVqI7ohIZs0rsDM/IU4fuQgkppCk2n+VRHy6mFttJ26t16mBjQPyIeYhvZnyXaiDSeaAYYXLiKPIm4uKh1zIq6gSRVU7WNPIV5v1PwBOy07RrrT6skwoEAtYyZeYgpBUABaDEmtPWRGP/UGABFgUUjchFXAghAgAYQpyPkCIBBoRwQgU/g3kQcyqTY9KSDItmjjmmkP+qJHG3C66hjXhtlhzwglI+1+4Be+T77hYQxxeNkCIDaKU+ANXhkMqePOVAQr7uwCjoBlCdtHQE8TNRd17//A/evkhSpkOMDA9VTmwAVAbLNlwMkoAG5qG2bWCYA/h3uEYgEhFqSmJJPpCIFbJHg1Aw5GaAujjqMzIHc8NrIHBEJJjGZP3cuFQZD9PvzLvZIaCIjBcTvEnIzTWiGjqdzlD+VAO2TUERK8smEi8lNNOguiAM9P6Gfo6bX8bD0v7V/cPwA/fDBi0BaGgUllWy5AATy6X9gAZqRVIsMLuVmqQ3QmrBbiFgTiWbxLDj1hBqINGLVYewQwySWWRyeQK1kXVJ0S2UjZ5WgXaIOSTodp42CUzqHXH226ZfJI4tXth45eqnslGdQgSGFhmmMyOdTbEyJcdv56U0pNa3MDJ1JVRZUmldtkcESglQkAOA8AMgAoBeYLQi+wDxAtwAAGlXJUdVLPLmo/TLRystE3SsqO29LUY0hsrtvBoXUw1Fy+XjIhqbj4rEZISsUysW1L68PlGRwyWImjWXjS9LFyUzjC7v7Z5hYsYKRhIH49EyxTJxutwOy2PRxp1R63SVViB+eNFYRywlEzJBIMDIl5Aa4PtbFOno416WTTkfVQwSPgIve66BCbLAuLmpCh59ln5uFECODSuUIJ2CJCCzDwbf+DV2/3yTtan5J1gEVuilcVASHULO9Bt+nWkIEHMMit5gOxaO4JpWjvdzOcySeFek9q4dy3jxi2BxHwsuuo2CzbQ+KJWxyqPUJljVCU3WT8JwlhZyLkKODRhZkQByHu8ERJIcGBexSmdwGPfMPbjnC0SwRUTyCAIwY4Mdr9zxJTKE5yp0mcEe9s3Umm/TZp2s4X/27cQUmhrd4Ow4ibPOTOxxT6w+S8ENc9AGE7meFOlNbuzjngJtzjFKcrMsxtf4G4UIPcNGXbvwNaeDea6+1L7kaYG+M13a455pruOh5Ww0HlhllPy/PsqS9Zr7ZV9cSLODAoItzBt2cI5i1brCxQRIcH6yr9wV5+XjhtlxuziH7LiqGTXVnkfCZntd1doJ5liyIno6X3GqH56fcnGNwE2IXvYhw5a/MJGsMns/TOawd+2HX5kOXxQZFsYkwmMetzGtqKDKSe+h9h7jX77z2Snb98vyL7rjmil1N3B/cCBfsC5rymA/ExpPlni/DL9i7eflmn66oxrSlMRRB/8Dbq/e/3aN95L3BG+dFd1d+uCVzKhQy5/+I77CmWJ/o2Hygaay8yleXU1c1NH2IpqHOYUtriyWHf3iZwxjZvNxm/Ti/O5+22uC3nVFxhu1wvQc08ZZ+nb5/9oBF38ehJXaKPt7K6/v34Ltqai21rbXDXI3BRDa+9XDSGp8gzZZarV2ag9v3RBzSNfFjU/M+uVduNDTsMNY0GCOHDU3O4dazeb3zFXjg0rlPuiV75+m+kzV3v5qc/PHdJ2j6pI+wDP+U7mFPW/uw23ltZemTVTo+3+KNa1yyuwdmfI41j1/yvbG3oaLR3G3Sd1xdrNOUfAou3jYw0z860D9jr+9sqU1IIHve9W56/fIEU2fztvaqHQ213trhKauqjXVmr/kFxP1m7bdCE88Plpb1m840l/VNQz72QvvMPlc5z9fvKPt1xBtXv9yVZ9nGmxscXiDfwGcI4LQ9duBaG7Nxjw2aWMuArp7In2qxObTwKfr40/iy/j34zhqeN59qHuZqDBaysQWiL/PZsk4cyV13mvbwWw7phfEDC/O+fm9/ram+sspcXxs5hm9pHjufSujGRbKXaTPmBAPPfMm45bau15+jR/bbBx80mR7sCth94rfB6Q6sngJcuO1v4a7Lu157ad9GKIlypuEea9GDnXs0IytDmZ2Rka2cVv4jrIHmaFZ3O9OWlWX7gLuRH1mMjsxE8VrEH/XNRrErsh61R6PD3e2d7dDsb3Z38k15W/kFkrwmHk+PhWNKx3nLMhHkSl5ZLMvHWPGPfwRB9AKr2QoSUb4qSlB18X715zkqwRI0G3lvUOptugtcID+tsKQForrC+pqxS4X1dcof21bf4OQcbZ2Le6y53kKao3rxNWE9f1a+S3t2tiIu5yej2d/W12RozBwrLh7LbDT0NkUDi+HBQPj0xIlAoKHFFxAMPB8Vce+KKo28Rq0SHV3LUk6MNLv35DMrCzVNmbxBeCGCKzMtu20y8f+SVD4h31Sosky03Upl9YJxzh+L0ZFANLR4LOpYj452tne145E/wok2e6JX/xBX/vH8WaFQojdWlrhzuOXjs1Zn3cCkT4qWSmZeqX/B4tciE4HBmUBZwGxtMDaqnCpdllPVaIzdNw9ZqfDU30ppYDgw2B+A5JP5L8bj4tgWoy/t6WRRcO2P8uFDakiP2fMWaviyaOYFr94gE0nEsXysWligOuHRC2TiH0XSJ/4WqQcjE4GxmUAocKlG+jECgZHFwGX8vabVzf8f4KQSi8Gp6laIdgV0q8OzbmjyaWdnW1MQXZoJDb7ZaEdzVzMoNQiAGESUwszDAIzTDIqSxGFoxoRZ/vTKM8CfyZ9V/ZAPfzZ/Ds4tMHm0dSxCkOPaNpKlK8/KwGQVCdzqbPDn1HKuiT8WHUnStEQYZB2TC6WJ6spymNIjBV7JK5lSs1C5qDoRSYBUSlgH31LlM7X5O4o8eBxrX3MDwtOG1iZdZ/NjXsu0gBJohRVmlQNfwVfQmsReAdZEAahljqkMaxLHg/gT+BONkwxcgT0CH/5k/pQrcCpMUYiGtPqbDGvVgALfitYyTHyI7iP25WqsSqAlstCbcmZ3k0PcRZlgbs0KCrwb7lImBTqQCCawVEpMWHur0DRm3+9sVAOK6UQiqESr8NaEtY8Vu86AIjK3KRZhXNwqD34Oc38umLZM7/33+38/uOj7///Z9yMAAHqAUtN3Bsa/l9rSiKVxlONqqbKozYWfYMRMRkicEE2FhIs4TuWQJL0RDNwCPy0hP1CbGAVY9Mb6W+DZ2p14CIQoIBjqGKpe6lOONhDJTUFBXMgZYOPct8C7Ffmf2jR2ksQ72U7f6SBu5gywoW8x2Qhy1APl1CZGAQ4ksGGD8loGsjY0UQKXmhdvpBboCNglmiU2XUCieL2dF1bDaTacoykaTjgFAhquyoFkiSk0LZyooeM7gQDVIlmNOwCPlICNKQpatkhBhUQJpdLCVwFqWj3FQ8sWeajTWbJiLeRJQlRmsyh1YLRZtAzVNr0dAoCqYN6wXLhzQGL8LY6mvwSAd1698ly88s2Xfwa7FbVA/wsgBhQAgABfG3dQHs2o/066DhB0wWo68NIlpR8Rtqkj9flbwtMc3oZyWoIn3n+L86o5vA1V8ng6L4C9+Ax3YgMBHN8Er4rV97ZRXgdo2vF244hH35wAzlOM0Y1BABGNnNCZ4UFNPID7U9KeSCgiP5EiDnMizejpExlFzj9RIM2BE2MU8pwgViH7TAQQSqpgnDuJJAk2kHIaYDNnhs+sFsvBuaf5DXMbXT9uwvLIbk49b4mBNm3JsEV+85YtKUHWb5rT5ywap9WpUZtWo6q5Fot8W89a0m4OaXOK2Ywat2KaD3NdRi1S/+eloldCT0fn1K5q61Mpa6eaNrKMB6WMQqn83IfXmTNv1eIRiaKj7aOsikMmoxej+EfqsmjOpFHDLnneylGLQxT66vnHnh/n12VXDN1Hw6/bcNpctWo3vrwApZvfrdEDiP8IzgPkeiYUoQlzDvYWB0cnZ6uLq5t7+aRk5I6TIFGS5IqkSJWWXjqFDEqZVLJky5FrG7W8quQrUEijSLESWtsz0tErVVa9chUqValmUMNoh1omZjxSM4s69Ro0atKspTe1smrLpl2HTl262dg5OLm4eezMw6tHrz79BgzyGarXsBGjxqJtJvhNmjJtxqw58xYsWrJsxS4Bu61aE7THXvvsFxJ2wEERhxx2xFHHrIs6fuE/r+1vm6/8jhhtDCwnASDGR1h4SGhYwEGU1Y/Xffk5G4u9XgIAYtxyGpZ5y9sDdNbyP142W5DgAhoE7i3q0c5x2vm9v24aZuNohdcxqSEAz8up00My03HSwmqPa8bMecG3Tjzb+f9EelEtg2vpzPeiIEEBwSA8IEDBwAE9F5Z6vQIQjAnLGTgWLO+uZTL/qwGEAYOAQpg3TefgwBCQgBDzqukKDBAOoU3KVtqIbBXK9XpEAAujuudkZN724kIGmrtVaS38ZZLrS9/4fsKMQhN3yXK5gvSBvJ2Y9/Nydbcvb0bm58+e3FhmGfGLJ9u7axl1F1LQTbaCeyrbu7wFCsorFgV4qnh+GUrWtaczqkSBB96AEeKRB94k2SyLuyw3qwrxeg3P5FNR1Av1Gsq2/ds6dPPkBAAA)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}
|