opticore-profiler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/assets/css/debug-message.css +16 -0
  4. package/dist/assets/css/home-page.css +177 -0
  5. package/dist/assets/css/profiler-detail.css +215 -0
  6. package/dist/assets/css/profiler-list.css +131 -0
  7. package/dist/assets/css/toolbar-standalone.css +213 -0
  8. package/dist/assets/js/home-particles.js +65 -0
  9. package/dist/assets/js/profiler-detail.js +77 -0
  10. package/dist/assets/js/profiler-list.js +76 -0
  11. package/dist/assets/js/toolbar.js +230 -0
  12. package/dist/index.cjs +1942 -0
  13. package/dist/index.d.cts +488 -0
  14. package/dist/index.d.ts +488 -0
  15. package/dist/index.js +1893 -0
  16. package/dist/utils/translations/message.translation.en.json +190 -0
  17. package/dist/utils/translations/message.translation.fr.json +190 -0
  18. package/dist/views/templates/home.njk +120 -0
  19. package/dist/views/templates/macros/icons.njk +19 -0
  20. package/dist/views/templates/macros/tables.njk +17 -0
  21. package/dist/views/templates/message.njk +14 -0
  22. package/dist/views/templates/panels/configuration.njk +16 -0
  23. package/dist/views/templates/panels/database.njk +36 -0
  24. package/dist/views/templates/panels/exception.njk +26 -0
  25. package/dist/views/templates/panels/logs.njk +44 -0
  26. package/dist/views/templates/panels/performance.njk +56 -0
  27. package/dist/views/templates/panels/request.njk +141 -0
  28. package/dist/views/templates/panels/routes.njk +51 -0
  29. package/dist/views/templates/panels/routing.njk +22 -0
  30. package/dist/views/templates/profiler-detail.njk +114 -0
  31. package/dist/views/templates/profiler-list.njk +163 -0
  32. package/dist/views/templates/toolbar-wdt.njk +161 -0
  33. package/package.json +73 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guy-serge Kouacou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # OptiCore Profiler
2
+
3
+ A Symfony WebProfilerBundle-style profiler for the OptiCore Express stack:
4
+ **collect → persist by token → display deferred**.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install opticore-profiler
10
+ ```
11
+
12
+ Requires `opticore-express` and `opticore-router` in the host app (both are
13
+ declared dependencies and installed automatically) and Node.js >= 18.
14
+
15
+ ### The 4 principles, and where they live
16
+
17
+ 1. **Collectors observe, they never intrude.** `DataCollector` (`types.ts`)
18
+ defines `collect(ctx, error?) / getName() / getData() / reset()`.
19
+ `RequestCollector`/`TimeCollector`/`MemoryCollector` read only from the
20
+ `req`/`res` objects the middleware already has. `DatabaseCollector` and
21
+ `LoggerCollector` are fed by **decorating** `OptiCoreMySQLDriver.prototype.makeQuery`
22
+ and `LoggerCore.prototype.{info,warn,error,debug,success}` **once, at
23
+ bootstrap** (`instrumentMySQL`/`instrumentLogger`) — business code keeps
24
+ calling `db.query(...)` / `logger.info(...)` completely unmodified, and
25
+ unaware profiling exists. The link between "a query just ran" and "which
26
+ request is that for" is `AsyncLocalStorage` (`context.ts`), the Node
27
+ analogue of what a DI container gives Symfony's collectors implicitly.
28
+ Register custom collectors on the registry the middleware exposes:
29
+ `profiler.registry.register(() => new MyCollector())`.
30
+
31
+ 2. **Persistence is decoupled from collection, keyed by token.** Every
32
+ profiled request gets a short token (`token.ts`, 6–8 alphanumeric chars).
33
+ On `res.on('finish')` — i.e. **after** the response has already been sent,
34
+ so this adds zero perceived latency — every collector's `getData()` is
35
+ assembled into a `Profile` and handed to a `ProfilerStorage`
36
+ (`MemoryStorage` or `FileStorage`, under `.opticore-profiler-cache/` by
37
+ default). `X-Debug-Token` and `X-Debug-Token-Link` headers are set on
38
+ every profiled response. Automatic purge runs after each write
39
+ (`retentionMs`, default 24h).
40
+
41
+ 3. **Display is deferred and AJAX-loaded, not embedded.** The middleware
42
+ never renders a toolbar into the page it's decorating. It buffers HTML
43
+ responses (`middleware/htmlInjector.ts`) and, only when the response is
44
+ `text/html`, splices a handful of lines before `</body>`
45
+ (`middleware/snippet.ts`): a stylesheet `<link>` and a `<script>` that
46
+ fetches `GET ${routePrefix}/wdt/:token` and swaps it in. That fragment,
47
+ plus `GET ${routePrefix}` (list) and `GET ${routePrefix}/:token` (detail,
48
+ one panel per collector), are the only 3 profiler routes — all excluded
49
+ from profiling themselves. `assets/js/toolbar.js` also wraps `fetch` and
50
+ `XMLHttpRequest` client-side so AJAX calls the page itself makes (which
51
+ carry the same `X-Debug-Token` header, since they're profiled requests
52
+ too) show up live in the toolbar's HTTP list.
53
+
54
+ 4. **Ephemeral by default, safe by construction.** `enabled` defaults to
55
+ `false` — must be explicitly turned on (`NODE_ENV === 'development'`).
56
+ `RequestCollector` masks configurable sensitive keys (`security/masker.ts`,
57
+ default list in `DEFAULT_SENSITIVE_KEYS`: `authorization`, `cookie`,
58
+ `password`, `token`, `session`, ...) in headers/cookies/query/body
59
+ **before** the data is ever persisted. Nunjucks' `autoescape: true` covers
60
+ template output; `security/escape.ts` and `toolbar.js`'s own
61
+ `escapeHtml()` cover the two spots that build HTML by hand. `profiler.clear()`
62
+ purges everything on demand.
63
+
64
+
65
+ ## Integration example
66
+
67
+ ```ts
68
+ import { express } from "opticore-express";
69
+ import { OptiCoreMySQLDriver } from "opticore-mysqldb";
70
+ import { LoggerCore } from "opticore-logger";
71
+ import {
72
+ opticoreProfiler,
73
+ profilerErrorHandler,
74
+ registerProfilerViews,
75
+ createProfilerRouter,
76
+ instrumentMySQL,
77
+ instrumentLogger,
78
+ FileStorage,
79
+ } from "opticore-profiler";
80
+
81
+ const app = express();
82
+
83
+ // Decorate infrastructure once, at bootstrap — never touches business code.
84
+ instrumentMySQL(OptiCoreMySQLDriver);
85
+ instrumentLogger(LoggerCore);
86
+
87
+ const profiler = opticoreProfiler({
88
+ enabled: process.env.NODE_ENV === "development",
89
+ storage: new FileStorage(".profiler-cache"), // any writable directory of your choosing
90
+ });
91
+
92
+ app.use(profiler); // 1. collection + token + deferred display
93
+ registerProfilerViews(app, profiler); // 2. view engine, static assets, "/" page
94
+
95
+ app.use(/* ...your feature routers, including createProfilerRouter(profiler)... */);
96
+
97
+ app.use(profilerErrorHandler()); // 3. after routes: feeds ExceptionCollector
98
+ app.use(yourOwnErrorHandler); // your app's error handling is untouched
99
+ ```
@@ -0,0 +1,16 @@
1
+ body {
2
+ background: #1a1a1a;
3
+ color: #666;
4
+ font-family: monospace;
5
+ font-size: 13px;
6
+ display: flex;
7
+ flex-direction: column;
8
+ align-items: center;
9
+ justify-content: center;
10
+ height: 100vh;
11
+ margin: 0;
12
+ gap: 12px;
13
+ }
14
+ a { color: #7c98d3; }
15
+ .msg-title { font-size: 18px; color: #e74c3c; }
16
+ .msg-code code { color: #aaa; }
@@ -0,0 +1,177 @@
1
+ :root {
2
+ --bg: #f5f5f5;
3
+ --card: #FFFCF7;
4
+ --border: #E0D8CA;
5
+ --text: #1A1A14;
6
+ --muted: #7A7268;
7
+ --accent: #FAC68E;
8
+ --ok: #31af05;
9
+ --warn: #FAC68E;
10
+ }
11
+ * { box-sizing: border-box; margin: 0; padding: 0; }
12
+ html, body {
13
+ height: 100%; background: var(--bg); color: var(--text);
14
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
15
+ font-size: 14px; line-height: 1.6;
16
+ }
17
+ body.has-toolbar { padding-bottom: 44px; }
18
+ a { color: var(--accent); text-decoration: none; }
19
+ a:hover { text-decoration: none; }
20
+
21
+ /* ===== CANVAS PARTICLES ===== */
22
+ #op-canvas {
23
+ position: fixed; inset: 0;
24
+ width: 100%; height: 100%;
25
+ z-index: 0; pointer-events: none;
26
+ }
27
+
28
+ /* ===== WAVES ===== */
29
+ .op-waves {
30
+ position: fixed; top: 0; left: 0; right: 0;
31
+ height: 500px; z-index: 1;
32
+ pointer-events: none; overflow: hidden;
33
+ }
34
+ .op-wave {
35
+ position: absolute; top: 0; left: 0;
36
+ width: 200%; height: 100%;
37
+ transform-origin: top left;
38
+ will-change: transform;
39
+ }
40
+
41
+ /*
42
+ * 3 animations distinctes :
43
+ * - vitesses très différentes → effet parallaxe 3D
44
+ * - animation-delay décalé → phases jamais alignées
45
+ * - wave-3 tourne en sens inverse → accentue la profondeur
46
+ */
47
+ .op-wave-1 { animation: op-wave-slow 16s ease-in-out infinite; }
48
+ .op-wave-2 { animation: op-wave-mid 11s ease-in-out -4s infinite; }
49
+ .op-wave-3 { animation: op-wave-fast 7s ease-in-out -2s infinite reverse; }
50
+
51
+ @keyframes op-wave-slow {
52
+ 0%,100% { transform: translateX(0) scaleY(1); }
53
+ 50% { transform: translateX(-25%) scaleY(.96); }
54
+ }
55
+ @keyframes op-wave-mid {
56
+ 0%,100% { transform: translateX(0) scaleY(1); }
57
+ 50% { transform: translateX(-25%) scaleY(1.04); }
58
+ }
59
+ @keyframes op-wave-fast {
60
+ 0%,100% { transform: translateX(0) scaleY(1); }
61
+ 50% { transform: translateX(-25%) scaleY(.98); }
62
+ }
63
+
64
+ /* ===== LAYOUT ===== */
65
+ .page {
66
+ position: relative; z-index: 2;
67
+ min-height: 100%;
68
+ display: flex; flex-direction: column;
69
+ align-items: center;
70
+ padding: 48px 24px 48px;
71
+ }
72
+ .container { width: 100%; max-width: 860px; }
73
+
74
+ /* ===== HEADER ===== */
75
+ .hero { text-align: center; margin-bottom: 48px; }
76
+ .hero-logo {
77
+ display: inline-flex; align-items: center; gap: 14px;
78
+ margin-bottom: 20px;
79
+ }
80
+ .hero-icon {
81
+ width: 52px; height: 52px; background: var(--accent);
82
+ border-radius: 14px; display: flex; align-items: center;
83
+ justify-content: center; font-size: 22px; font-weight: 900; color: #fff;
84
+ box-shadow: 0 4px 18px rgba(250,198,142,.30);
85
+ }
86
+ .hero-name { font-size: 28px; font-weight: 700; color: var(--text); }
87
+ .hero-sub { font-size: 13px; color: var(--muted); letter-spacing: .5px; text-transform: uppercase; }
88
+ .hero-desc { color: var(--muted); font-size: 14px; margin-top: 8px; }
89
+
90
+ /* ===== STATUS PILL ===== */
91
+ .status-row {
92
+ display: flex; align-items: center; justify-content: center;
93
+ gap: 8px; margin-bottom: 36px;
94
+ }
95
+ .status-pill {
96
+ display: inline-flex;
97
+ align-items: center;
98
+ gap: 6px;
99
+ background: #ffffff;
100
+ border: 1px solid #31af05;
101
+ color: var(--ok);
102
+ padding: 5px 14px;
103
+ border-radius: 20px;
104
+ font-size: 12px;
105
+ font-weight: 600;
106
+ }
107
+ .status-dot {
108
+ width: 7px; height: 7px; border-radius: 50%;
109
+ background: var(--ok); animation: pulse 2s infinite;
110
+ }
111
+ @keyframes pulse { 0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(45,106,74,.35)} 50%{opacity:.7;box-shadow:0 0 0 5px rgba(45,106,74,0)} }
112
+ .env-pill {
113
+ display: inline-flex; align-items: center;
114
+ padding: 5px 14px; border-radius: 20px;
115
+ font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .4px;
116
+ }
117
+ .env-pill--dev { background: #FDF3EA; border: 1px solid #FAC68E; color: #C07030; }
118
+ .env-pill--prod { background: #FDECEA; border: 1px solid #E57373; color: #C62828; }
119
+
120
+ /* ===== CARDS ===== */
121
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-bottom: 28px; }
122
+ .card {
123
+ background: var(--card); border: 1px solid var(--border);
124
+ border-radius: 10px; padding: 18px 20px;
125
+ transition: border-color .2s, box-shadow .2s;
126
+ }
127
+ .card:hover { border-color: #C8B8A4; box-shadow: 0 2px 12px rgba(250,198,142,.10); }
128
+ .card-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }
129
+ .card-value { font-size: 17px; font-weight: 600; color: var(--text); font-family: monospace; }
130
+ .card-value--dev { color: #C07030; }
131
+ .card-value--prod { color: #C62828; }
132
+ .card-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
133
+
134
+ /* ===== SECTION ===== */
135
+ .section { margin-bottom: 28px; }
136
+ .section-title {
137
+ font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px;
138
+ color: var(--muted); margin-bottom: 12px;
139
+ display: flex; align-items: center; gap: 8px;
140
+ }
141
+ .section-title::after { content:""; flex:1; height:1px; background:var(--border); }
142
+
143
+ /* ===== QUICK LINKS ===== */
144
+ .quick-links { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }
145
+ .quick-link {
146
+ display: flex; align-items: center; gap: 10px;
147
+ background: var(--card); border: 1px solid var(--border);
148
+ border-radius: 8px; padding: 12px 14px;
149
+ color: var(--text); text-decoration: none;
150
+ transition: border-color .2s, background .2s, box-shadow .2s;
151
+ }
152
+ .quick-link:hover { border-color: var(--accent); background: #FDF3EA; color: var(--text); box-shadow: 0 2px 10px rgba(250,198,142,.12); text-decoration: none; }
153
+ .quick-link-icon {
154
+ width: 32px; height: 32px; border-radius: 7px;
155
+ display: flex; align-items: center; justify-content: center;
156
+ font-size: 15px; flex-shrink: 0;
157
+ }
158
+ .quick-link-label { font-size: 13px; font-weight: 500; }
159
+ .quick-link-desc { font-size: 11px; color: var(--muted); }
160
+
161
+ /* ===== API NOTICE ===== */
162
+ .api-notice {
163
+ background: var(--card); border: 1px solid var(--border);
164
+ border-radius: 10px; padding: 20px 24px;
165
+ display: flex; align-items: flex-start; gap: 14px;
166
+ }
167
+ .api-notice-icon { font-size: 22px; flex-shrink: 0; margin-top: 1px; }
168
+ .api-notice-title { font-weight: 600; color: var(--text); margin-bottom: 4px; }
169
+ .api-notice-body { font-size: 13px; color: var(--muted); line-height: 1.6; }
170
+ .api-notice-body code {
171
+ background: #F0EBE1; border: 1px solid #D8D0C4; padding: 1px 6px;
172
+ border-radius: 3px; font-size: 12px; color: #6A5A48; font-family: monospace;
173
+ }
174
+
175
+ /* ===== FOOTER ===== */
176
+ .footer { margin-top: 40px; text-align: center; color: var(--muted); font-size: 12px; padding-bottom: 8px; }
177
+ .footer a { color: var(--accent); }
@@ -0,0 +1,215 @@
1
+ * { box-sizing:border-box; margin:0; padding:0; }
2
+ html, body {
3
+ background-attachment: fixed;
4
+ background-color: #ffffff;
5
+ background-image: radial-gradient(#ffeede 1.35px, transparent 0);
6
+ background-size: 15px 15px;
7
+ color: #404040;
8
+ font-size: 14px;
9
+ line-height: 1.4;
10
+ font-family:'Montserrat',-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;
11
+ -webkit-font-smoothing: antialiased;
12
+ }
13
+
14
+ a { color:#C87A3C; text-decoration:none; }
15
+ a:hover { text-decoration:underline; }
16
+ code, pre { font-family:"SF Mono",Consolas,"Liberation Mono",monospace; }
17
+
18
+ :root {
19
+ --bg:#fff; --sidebar-bg:#fff; --content-bg:#fff;
20
+ --border:#e3e3e3; --text:#222; --muted:#888; --accent:#C87A3C;
21
+ --hover:#fafafa; --active-bg:#FDF3EA; --active-border:#C87A3C;
22
+ --ok-bg:#e8f4ec; --ok-text:#1a7a3c;
23
+ --warn-bg:#fbe3e4; --warn-text:#c0392b;
24
+ --info-bg:#FDF3EA; --info-text:#9A5020;
25
+ }
26
+
27
+ /* CONTENT area */
28
+ .pf-content {
29
+ flex:1;
30
+ overflow-y:auto;
31
+ }
32
+
33
+ /* Panel inner styles */
34
+ .panel-title { font-size:18px; font-weight:600; color:#333; margin-bottom:18px; }
35
+ .panel-section { margin-bottom:22px; }
36
+ .panel-section-title { font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:#888; margin-bottom:10px; display:flex; align-items:center; gap:6px; }
37
+ .panel-section-title::after { content:""; flex:1; height:1px; background:#e6e6e6; }
38
+ .card-grid { display: flow; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:12px; margin-bottom:16px; }
39
+ .card { background:#f8f8f8; border:1px solid var(--border); border-radius:4px; padding: 12px 16px; margin-bottom: 7px; }
40
+ .card-title { font-size:13px; color:var(--muted); margin-bottom:4px; font-weight:600; text-transform:uppercase; letter-spacing:.4px; }
41
+ .card-value { font-size:14px; color:#333; }
42
+ .card-empty { color:var(--muted); font-style:italic; font-size:14px; }
43
+ .data-table { width:100%; border-collapse:collapse; font-size:14px; }
44
+ .data-table th { background:#f5f5f5; padding:9px 13px; text-align:left; font-size:11px; font-weight:700; color:#666; border-bottom:1px solid #e6e6e6; text-transform:uppercase; letter-spacing:.4px; }
45
+ .data-table td { padding:9px 13px; border-bottom:1px solid #ececec; vertical-align:top; }
46
+ .data-table tr:hover td { background:#fafafa; }
47
+ .data-table tr:last-child td { border-bottom:none; }
48
+ .data-table .key-col { color:#555; font-family:monospace; font-size:13px; white-space:nowrap; width:240px; }
49
+ .data-table .val-col { color:#333; font-family:monospace; font-size:13px; word-break:break-all; }
50
+ .table-wrap { background:#fff; border:1px solid #e6e6e6; border-radius:4px; overflow:hidden; margin-bottom:18px; }
51
+ .table-wrap-title { background:#f5f5f5; padding:10px 14px; font-size:13px; font-weight:600; color:#444; border-bottom:1px solid #e6e6e6; display:flex; align-items:center; gap:10px; }
52
+ .table-search { margin-left:auto; background:#fff; border:1px solid #d0d0d0; color:#333; padding:3px 8px; border-radius:3px; font-size:12px; outline:none; font-family:'Montserrat',-apple-system,'Segoe UI',Helvetica,Arial,sans-serif; }
53
+ .table-search:focus { border-color:#C87A3C; }
54
+ .sql-block { padding:11px 14px; border-bottom:1px solid #ececec; }
55
+ .sql-block:last-child { border-bottom:none; }
56
+ .sql-meta { display:flex; gap:8px; margin-bottom:5px; align-items:center; }
57
+ .sql-type { font-size:10px; font-weight:700; padding:2px 6px; border-radius:3px; text-transform:uppercase; }
58
+ .sql-type-SELECT{background:#FDF3EA;color:#9A5020} .sql-type-INSERT{background:#e8f4ec;color:#1a7a3c}
59
+ .sql-type-UPDATE{background:#FEF9E7;color:#7D6608} .sql-type-DELETE{background:#fbe3e4;color:#c0392b} .sql-type-OTHER{background:#f0f0f0;color:#666}
60
+ .sql-duration { color:var(--muted); font-size:11px; margin-left:auto; }
61
+ .sql-code { background:#f8f8f8; border:1px solid #e6e6e6; border-radius:4px; padding:10px 12px; font-family:monospace; font-size:12px; color:#333; white-space:pre-wrap; word-break:break-all; line-height:1.6; }
62
+ .log-entry { padding:9px 13px; border-bottom:1px solid #ececec; display:flex; gap:10px; align-items:flex-start; }
63
+ .log-entry:last-child { border-bottom:none; }
64
+ .log-level { font-size:10px; font-weight:700; padding:2px 6px; border-radius:3px; text-transform:uppercase; white-space:nowrap; flex-shrink:0; }
65
+ .log-debug{background:#f0f0f0;color:#666} .log-info{background:#FDF3EA;color:#9A5020}
66
+ .log-warning{background:#FEF9E7;color:#7D6608} .log-error{background:#fbe3e4;color:#c0392b} .log-critical{background:#fbe3e4;color:#7F1010}
67
+ .log-msg { color:#333; font-size:13px; flex:1; word-break:break-word; }
68
+ .log-time { color:var(--muted); font-size:11px; white-space:nowrap; font-family:monospace; }
69
+ .log-context { margin-top:5px; font-family:monospace; font-size:11px; color:#555; background:#f5f5f5; padding:6px 8px; border-radius:3px; }
70
+ .perf-metrics-row { display:flex; border:1px solid #e6e6e6; border-radius:6px; overflow:hidden; margin-bottom:18px; }
71
+ .perf-metric { flex:1; padding:16px 20px; border-right:1px solid #e6e6e6; text-align:center; }
72
+ .perf-metric:last-child { border-right:none; }
73
+ .perf-metric-label { font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.5px; color:var(--muted); margin-bottom:6px; }
74
+ .perf-metric-value { font-size:22px; font-weight:700; color:#222; font-family:monospace; }
75
+ .perf-metric-unit { font-size:13px; font-weight:400; color:var(--muted); }
76
+ .tl-threshold-row { display:flex; align-items:center; gap:8px; font-size:12px; color:#333; margin-bottom:10px; flex-wrap:wrap; }
77
+ .tl-threshold-input { width:52px; padding:3px 7px; border:1px solid #d0d0d0; border-radius:3px; background:#fff; color:#333; font-size:12px; outline:none; text-align:center; font-family:'Montserrat',-apple-system,'Segoe UI',Helvetica,Arial,sans-serif; }
78
+ .tl-threshold-hint { color:var(--muted); font-size:12px; }
79
+ .tl-legend { display:flex; align-items:center; gap:12px; font-size:12px; color:var(--muted); margin-bottom:10px; }
80
+ .tl-legend-item { display:flex; align-items:center; gap:5px; }
81
+ .tl-legend-dot { width:10px; height:10px; border-radius:2px; display:inline-block; }
82
+ .tl-chart { background:#fff; border:1px solid #e6e6e6; border-radius:4px; overflow:hidden; }
83
+ .tl-row { display:flex; align-items:center; gap:12px; padding:8px 13px; border-bottom:1px solid #ececec; }
84
+ .tl-row:last-child { border-bottom:none; }
85
+ .tl-name { width:200px; flex-shrink:0; font-size:12px; color:#333; font-family:monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
86
+ .tl-track { flex:1; height:18px; background:#f0f0f0; border-radius:3px; overflow:hidden; position:relative; }
87
+ .tl-bar { height:100%; min-width:4px; border-radius:3px; display:flex; align-items:center; justify-content:flex-end; padding-right:6px; }
88
+ .tl-bar-label { color:#fff; font-size:10px; font-weight:600; white-space:nowrap; }
89
+ .tl-info { width:75px; font-size:11px; color:var(--muted); font-family:monospace; flex-shrink:0; text-align:right; }
90
+ .tl-empty { color:var(--muted); font-size:13px; padding:24px; text-align:center; }
91
+ .routes-summary { display:flex; border:1px solid #e6e6e6; border-radius:6px; overflow:hidden; margin-bottom:18px; }
92
+ .routes-summary-item { flex:1; padding:12px 16px; border-right:1px solid #e6e6e6; text-align:center; }
93
+ .routes-summary-item:last-child { border-right:none; }
94
+ .routes-summary-value { font-size:22px; font-weight:700; color:#222; font-family:monospace; }
95
+ .routes-summary-label { font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.5px; color:var(--muted); margin-top:4px; }
96
+ .route-method { display:inline-block; padding:2px 7px; border-radius:3px; font-size:11px; font-weight:700; color:#fff; font-family:monospace; }
97
+ .rm-GET{background:#2e7d32} .rm-POST{background:#1565c0} .rm-PUT{background:#e65100} .rm-DELETE{background:#c62828} .rm-PATCH{background:#6a1b9a} .rm-ALL{background:#424242} .rm-OPTIONS{background:#37474f}
98
+ .route-path { font-family:monospace; font-size:13px; color:#333; }
99
+ .route-path .param { color:#C87A3C; }
100
+ .stats-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:16px; }
101
+ .stat-card { background:#f8f8f8; border:1px solid #e6e6e6; border-radius:4px; padding:12px 16px; text-align:center; }
102
+ .stat-value { font-size:20px; font-weight:700; color:#222; margin-bottom:3px; }
103
+ .stat-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; }
104
+ .badge { display:inline-block; padding:2px 7px; border-radius:4px; font-size:11px; font-weight:600; }
105
+ .badge-ok { background:#e8f4ec; color:#1a7a3c; }
106
+ .badge-warn { background:#fbe3e4; color:#c0392b; }
107
+ .badge-info { background:#FDF3EA; color:#9A5020; }
108
+ .badge-muted{ background:#f0f0f0; color:#666; }
109
+ .empty-state { text-align:center; padding:40px 24px; color:var(--muted); }
110
+ .empty-icon { font-size:28px; margin-bottom:8px; }
111
+ .empty-text { font-size:13px; }
112
+ .json-pre { background:#f8f8f8; padding:12px; font-size:12px; color:#333; font-family:monospace; white-space:pre-wrap; word-break:break-all; max-height:400px; overflow:auto; line-height:1.6; }
113
+ .json-pre .json-key { color:#e36209; }
114
+ .json-pre .json-val { color:#447dbd; }
115
+ hr.divider { border:none; border-top:1px solid #e6e6e6; margin:18px 0; }
116
+ .perf-bar-wrap { margin-bottom:8px; }
117
+ .perf-bar-label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:3px; }
118
+ .perf-bar-outer { height:7px; background:#f0f0f0; border-radius:4px; overflow:hidden; }
119
+ .perf-bar-inner { height:100%; border-radius:4px; }
120
+ .center { text-align:center; }
121
+ .mono { font-family:monospace; }
122
+ .muted { color:var(--muted); }
123
+
124
+ .lang-switch { display:flex; align-items:center; gap:2px; background:#f5f3f0; border:1px solid #e6e6e6; border-radius:5px; padding:3px; flex-shrink:0; }
125
+ .lang-switch-item { padding:5px 10px; font-size:11px; font-weight:700; letter-spacing:.3px; color:#888; border-radius:3px; text-decoration:none; }
126
+ .lang-switch-item.active { background:#fff; color:#C87A3C; box-shadow:0 1px 3px rgba(0,0,0,.1); }
127
+ .lang-switch-item:hover:not(.active) { color:#555; }
128
+
129
+ /* ── LOG FILTER TABS ── */
130
+ .log-tabs { display:flex; gap:6px; margin-bottom:16px; flex-wrap:wrap; }
131
+ .log-tab {
132
+ padding:5px 13px; border:1px solid #e0e0e0; border-radius:4px;
133
+ font-size:13px; cursor:pointer; background:#f8f8f8; color:#555;
134
+ font-family:inherit; transition:all .12s;
135
+ display:inline-flex; align-items:center; gap:5px;
136
+ }
137
+ .log-tab.active { background:#fff; border-color:#C87A3C; color:#C87A3C; font-weight:600; }
138
+ .log-tab:hover:not(.active) { background:#fff; border-color:#bbb; color:#333; }
139
+ .log-tab-badge {
140
+ display:inline-flex; align-items:center; justify-content:center;
141
+ border-radius:10px; font-size:10px; font-weight:700;
142
+ min-width:18px; height:18px; padding:0 5px; color:#fff;
143
+ }
144
+ .log-tab-err { background:#c0392b; }
145
+ .log-tab-warn { background:#e67e22; }
146
+ .log-tab-depr { background:#8e44ad; }
147
+ .log-tab-zero { color:#aaa; font-size:12px; }
148
+
149
+ /* ── LOG MESSAGES TABLE ── */
150
+ .log-table-wrap { border:1px solid #e6e6e6; border-radius:4px; overflow:hidden; }
151
+ .log-table { width:100%; border-collapse:collapse; font-size:12px; }
152
+ .log-table th { background:#f5f5f5; padding:9px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.4px; color:#888; border-bottom:1px solid #e6e6e6; }
153
+ .log-row { border-bottom:1px solid #ececec; }
154
+ .log-row:last-child { border-bottom:none; }
155
+ .log-row td { padding:10px 14px; vertical-align:top; }
156
+ .log-td-time { width:150px; white-space:nowrap; color:#888; font-family:monospace; font-size:11px; }
157
+ .log-row-error { border-left:3px solid #e74c3c; }
158
+ .log-row-warning { border-left:3px solid #e67e22; }
159
+ .log-row-deprecation { border-left:3px solid #8e44ad; }
160
+ .log-msg-text { font-size:12.5px; color:#333; word-break:break-word; line-height:1.5; }
161
+ .log-meta-links { margin-top:5px; }
162
+ .log-ctx-btn {
163
+ background:none; border:none; padding:0; cursor:pointer;
164
+ color:#C87A3C; font-size:11.5px; text-decoration:underline; font-family:inherit;
165
+ }
166
+ .log-ctx-btn:hover { color:#9A5020; }
167
+ .log-ctx-btn + .log-ctx-btn { margin-left:10px; }
168
+ .log-context-detail {
169
+ display:none; margin-top:6px; background:#f5f5f5; border:1px solid #ebebeb;
170
+ border-radius:3px; padding:8px 10px; font-family:monospace; font-size:11px;
171
+ color:#555; white-space:pre-wrap; word-break:break-all;
172
+ }
173
+ .log-stack-trace {
174
+ max-height:280px; overflow-y:auto; line-height:1.7; color:#444;
175
+ }
176
+ .log-source-ref {
177
+ display:block; margin-top:4px; font-family:monospace; font-size:11px;
178
+ color:#888; word-break:break-all;
179
+ }
180
+ .log-deprecation { background:#f3e5f5; color:#6a1b9a; }
181
+
182
+ /* ── REQUEST / RESPONSE TABS ── */
183
+ .rr-tabs { display:flex; background:#f5f3f0; border:1px solid #e6e6e6; border-radius:5px; padding:4px; gap:2px; margin-bottom:24px; width:fit-content; }
184
+ .rr-tab {
185
+ padding:7px 16px; font-size:13px; font-weight:600; color:#555;
186
+ border-radius:4px; cursor:pointer; background:transparent; border:none;
187
+ font-family:inherit; transition:background .12s, color .12s;
188
+ }
189
+ .rr-tab.active { background:#fff; color:#222; box-shadow:0 1px 3px rgba(0,0,0,.1); }
190
+ .rr-tab:hover:not(.active) { background:rgba(255,255,255,.6); color:#333; }
191
+ .rr-tab-empty { color:#bbb; }
192
+ .rr-tab-empty:hover:not(.active) { color:#999; background:rgba(255,255,255,.4); }
193
+ .rr-tabpanel { animation:rr-fade .1s ease-in; }
194
+ @keyframes rr-fade { from { opacity:0; } to { opacity:1; } }
195
+
196
+ .curl-block { position:relative; background:#f8f8f8; border:1px solid #e6e6e6; border-radius:4px; padding:16px; }
197
+ .curl-copy-btn {
198
+ position:absolute; top:12px; right:12px;
199
+ padding:6px 14px; border:1px solid #d0d0d0; border-radius:4px;
200
+ background:#fff; color:#444; font-size:12px; font-weight:600;
201
+ cursor:pointer; font-family:inherit; transition:border-color .12s, color .12s;
202
+ }
203
+ .curl-copy-btn:hover { border-color:#C87A3C; color:#C87A3C; }
204
+ .curl-pre { font-family:monospace; font-size:12px; color:#333; white-space:pre-wrap; word-break:break-all; line-height:1.7; margin-top:6px; }
205
+
206
+ /* ── ROUTES PANEL FILTER (defined once, never overridden by page-level styles) ── */
207
+ .route-filter { display:flex; gap:8px; margin-bottom:14px; flex-wrap:wrap; }
208
+ .route-filter-btn {
209
+ padding:4px 12px; border-radius:3px; border:1px solid var(--border);
210
+ background:#F0EBE1; color:var(--muted); font-size:11.5px; cursor:pointer;
211
+ transition:all .1s;
212
+ }
213
+ .route-filter-btn:hover, .route-filter-btn.active {
214
+ background:var(--hover); border-color:var(--accent); color:var(--text);
215
+ }
@@ -0,0 +1,131 @@
1
+ * { box-sizing:border-box; }
2
+ body {
3
+ margin:0;
4
+ background-color: #ffffff;
5
+ background-image: radial-gradient(#ffeede 1.35px, transparent 0);
6
+ background-size: 15px 15px;
7
+ font-family: 'Montserrat',-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;
8
+ color:#222;
9
+ -webkit-font-smoothing:antialiased;
10
+ }
11
+
12
+ a { color:#C87A3C; text-decoration:none; }
13
+ a:hover { text-decoration:none; }
14
+ input:focus, select:focus { border-color:#C87A3C !important; outline:none; }
15
+
16
+ .pl-header { max-width:2000px; margin:0 auto; padding:34px 200px 0 200px; }
17
+ .pl-header-row { display:flex; align-items:center; justify-content:space-between; gap:14px; }
18
+ .lang-switch { display:flex; align-items:center; gap:2px; background:#f5f3f0; border:1px solid #e6e6e6; border-radius:5px; padding:3px; flex-shrink:0; }
19
+ .lang-switch-item { padding:5px 10px; font-size:11px; font-weight:700; letter-spacing:.3px; color:#888; border-radius:3px; }
20
+ .lang-switch-item.active { background:#fff; color:#C87A3C; box-shadow:0 1px 3px rgba(0,0,0,.1); }
21
+ .lang-switch-item:hover:not(.active) { color:#555; }
22
+ .pl-brand { display:flex; align-items:center; gap:14px; cursor:pointer; text-decoration:none; }
23
+ .pl-brand-name { font-weight:900; font-size:20px; color:#222; letter-spacing:-.2px; }
24
+ .pl-search-wrap { position:relative; width:340px; }
25
+ .pl-search-icon { position:absolute; left:12px; top:50%; transform:translateY(-50%); }
26
+ .pl-search-input {
27
+ width:100%; height:42px; padding:0 14px 0 36px; border:1px solid #d8d8d8;
28
+ border-radius:4px; font-size:12.5px; color:#555; outline:none;
29
+ background:#fff; cursor:default; opacity:.6;
30
+ }
31
+
32
+ .pl-banner { max-width:2000px; margin:15px auto 0 auto; padding:0 200px; }
33
+ .pl-banner-inner { background:#f5f5f5; border-radius:5px 5px 0 0; padding:16px 36px; border-top:4px solid #737373; }
34
+ .pl-banner-title { margin:0; font-size: 14.5px; font-weight:900; color:#3c3c3c; }
35
+
36
+ .pl-grid {
37
+ max-width:2000px; margin:20px auto 0 auto; padding:0 200px 80px 200px;
38
+ display:grid; grid-template-columns:250px 1fr; gap:20px;
39
+ }
40
+
41
+ /* ── SIDEBAR ── */
42
+ .pl-sidebar-card { background:#ffffff; box-shadow:#e4e4e4 0 0 0 1px; border-radius:5px; padding:22px 22px 26px 22px; }
43
+ .pl-sidebar-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:24px; }
44
+ .pl-sidebar-search-link { display:flex; align-items:center; gap:8px; text-decoration:none; }
45
+ .pl-sidebar-search-label { font-size:13px; color:#888; }
46
+ .pl-sidebar-latest { font-size:13px; color:#C87A3C; cursor:pointer; }
47
+ .pl-sidebar-latest.is-disabled { opacity:.4; pointer-events:none; }
48
+
49
+ .pl-label {
50
+ display:block;
51
+ font-size: 11px;
52
+ font-weight:600;
53
+ color:#333;
54
+ margin-bottom:8px;
55
+ }
56
+ .pl-field {
57
+ width: 100%;
58
+ height: 30px;
59
+ border: 1px solid #d0d0d0;
60
+ border-radius: 3px;
61
+ padding:0 10px;
62
+ font-size: 14px;
63
+ outline: none;
64
+ box-sizing: border-box;
65
+ }
66
+
67
+ .pl-field--gap { margin-bottom:20px; }
68
+ .pl-field--token { width:128px; margin-bottom:20px; }
69
+ .pl-field-row { display:flex; gap:16px; margin-bottom:20px; }
70
+ .pl-field-col { flex:1; }
71
+ .pl-field-date-wrap { margin-bottom:20px; }
72
+ .pl-date { color:#777; cursor:pointer; }
73
+
74
+ .pl-select-wrap { position:relative; }
75
+ .pl-select-wrap--limit { width:74px; }
76
+ .pl-select {
77
+ appearance:none; -webkit-appearance:none; color:#333; background:#fff;
78
+ cursor:pointer; padding-right:28px;
79
+ }
80
+ .pl-chevron { position:absolute; right:9px; top:50%; transform:translateY(-50%); pointer-events:none; }
81
+ .pl-chevron--limit { right:8px; }
82
+
83
+ .pl-results-row { display:flex; align-items:center; gap:14px; }
84
+ .pl-submit-btn {
85
+ height: 30px; padding:0 18px; border:1px solid #cfcfcf; border-radius:3px;
86
+ background:#f0f0f0; font-size:14px; color:#333; cursor:pointer;
87
+ }
88
+ .pl-settings-link { display:flex; align-items:center; gap:8px; margin-top:18px; color:#777; text-decoration:none; font-size:13px; }
89
+
90
+ /* ── RESULTS PANEL ── */
91
+ .pl-results { width:100%; }
92
+ .pl-tabs {
93
+ background:#fff; border:1px solid #e3e3e3; border-radius:6px;
94
+ padding:18px 28px 0 28px; margin-bottom:26px;
95
+ display:flex; align-items:center; gap:36px;
96
+ }
97
+ .pl-tab { text-decoration:none; display:flex; flex-direction:column; align-items:center; }
98
+ .pl-tab-label { font-size: 12px; font-weight:500; color:#888; cursor:pointer; }
99
+ .pl-tab-label.is-active { font-weight:700; color:#222; }
100
+ .pl-tab-underline { display:block; width:100%; height:3px; background:transparent; margin-top:14px; border-radius:2px 2px 0 0; }
101
+ .pl-tab-underline.is-active { background:#C87A3C; }
102
+
103
+ .pl-section-heading { margin:0 0 22px 0; font-size: 13px; font-weight:700; color:#222; }
104
+ .pl-empty-commands { border:1px solid #e6e6e6; border-radius:4px; background:#f5f5f5; padding:70px 20px; text-align:center; }
105
+ .pl-empty-commands-text { font-size:13.5px; color:#999; }
106
+
107
+ .pl-table { width:100%; border-collapse:collapse; }
108
+ .pl-table th {
109
+ text-align:left; padding:10px 14px; font-size:13px; font-weight:600; color:#444;
110
+ border-top:1px solid #e6e6e6; border-bottom:1px solid #e6e6e6; background:#f5f5f5;
111
+ }
112
+ .pl-table th:first-child { border-radius:4px 0 0 0; }
113
+ .pl-table th:last-child { border-radius:0 4px 0 0; }
114
+ .pl-row-font-size {
115
+ font-size: 13px
116
+ }
117
+ .pl-row { border-bottom:1px solid #ececec; cursor:pointer; }
118
+ .pl-row:hover { background:#fafafa; }
119
+ .pl-cell { padding:8px 12px; font-size:12px; color:#333; }
120
+ .pl-cell-url { max-width:380px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
121
+ .pl-cell-date { line-height:1.4; white-space:nowrap; }
122
+ .pl-cell-time { color:#888; font-size:11px; }
123
+ .pl-token-link { font-size:13px; color:#C87A3C; text-decoration:none; }
124
+ .pl-empty-row { padding:30px 20px; text-align:center; color:#999; font-size:14px; }
125
+
126
+ .pl-status-badge {
127
+ display:inline-block; padding:2px 7px; border-radius:4px; font-size:12px; font-weight:600;
128
+ }
129
+ .pl-status-badge--ok { background:#e8f4ec; color:#1a7a3c; }
130
+ .pl-status-badge--redirect { background:#e3edf8; color:#1565c0; }
131
+ .pl-status-badge--error { background:#fbe3e4; color:#c0392b; }