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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/assets/css/debug-message.css +16 -0
- package/dist/assets/css/home-page.css +177 -0
- package/dist/assets/css/profiler-detail.css +215 -0
- package/dist/assets/css/profiler-list.css +131 -0
- package/dist/assets/css/toolbar-standalone.css +213 -0
- package/dist/assets/js/home-particles.js +65 -0
- package/dist/assets/js/profiler-detail.js +77 -0
- package/dist/assets/js/profiler-list.js +76 -0
- package/dist/assets/js/toolbar.js +230 -0
- package/dist/index.cjs +1942 -0
- package/dist/index.d.cts +488 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.js +1893 -0
- package/dist/utils/translations/message.translation.en.json +190 -0
- package/dist/utils/translations/message.translation.fr.json +190 -0
- package/dist/views/templates/home.njk +120 -0
- package/dist/views/templates/macros/icons.njk +19 -0
- package/dist/views/templates/macros/tables.njk +17 -0
- package/dist/views/templates/message.njk +14 -0
- package/dist/views/templates/panels/configuration.njk +16 -0
- package/dist/views/templates/panels/database.njk +36 -0
- package/dist/views/templates/panels/exception.njk +26 -0
- package/dist/views/templates/panels/logs.njk +44 -0
- package/dist/views/templates/panels/performance.njk +56 -0
- package/dist/views/templates/panels/request.njk +141 -0
- package/dist/views/templates/panels/routes.njk +51 -0
- package/dist/views/templates/panels/routing.njk +22 -0
- package/dist/views/templates/profiler-detail.njk +114 -0
- package/dist/views/templates/profiler-list.njk +163 -0
- package/dist/views/templates/toolbar-wdt.njk +161 -0
- package/package.json +73 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% if not panelData.hasLogs %}
|
|
2
|
+
<div class="empty-state">
|
|
3
|
+
<div class="empty-icon">📋</div>
|
|
4
|
+
<div class="empty-text">{{ t('logs.no_logs') }}</div>
|
|
5
|
+
<div style="font-size:12px;color:#555;margin-top:8px;">
|
|
6
|
+
{{ t('logs.hint', { code: safeHtml('<code>logCollector.record(token, { level, message })</code>') }) | safe }}
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
{% else %}
|
|
10
|
+
<div class="log-tabs" id="log-tabs">
|
|
11
|
+
<button class="log-tab active" data-ltab="all">{{ t('logs.all_messages') }}</button>
|
|
12
|
+
<button class="log-tab" data-ltab="error">{{ t('logs.errors') }} {% if panelData.errCount > 0 %}<span class="log-tab-badge log-tab-err">{{ panelData.errCount }}</span>{% else %}<span class="log-tab-zero">0</span>{% endif %}</button>
|
|
13
|
+
<button class="log-tab" data-ltab="deprecation">{{ t('logs.deprecations') }} {% if panelData.deprCount > 0 %}<span class="log-tab-badge log-tab-depr">{{ panelData.deprCount }}</span>{% else %}<span class="log-tab-zero">0</span>{% endif %}</button>
|
|
14
|
+
<button class="log-tab" data-ltab="warning">{{ t('logs.warnings') }} {% if panelData.warnCount > 0 %}<span class="log-tab-badge log-tab-warn">{{ panelData.warnCount }}</span>{% else %}<span class="log-tab-zero">0</span>{% endif %}</button>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="log-table-wrap">
|
|
18
|
+
<table class="log-table">
|
|
19
|
+
<thead><tr><th>{{ t('logs.time') }}</th><th>{{ t('logs.message') }}</th></tr></thead>
|
|
20
|
+
<tbody id="log-tbody">
|
|
21
|
+
{% for row in panelData.rows %}
|
|
22
|
+
<tr class="log-row {{ row.rowClass }}" data-ltype="{{ row.logType }}">
|
|
23
|
+
<td class="log-td-time">
|
|
24
|
+
{{ row.timeStr }}<br>
|
|
25
|
+
<span class="log-level {{ row.levelCls }}">{{ row.level }}</span>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="log-td-msg">
|
|
28
|
+
<span class="log-msg-text">{{ row.message }}</span>
|
|
29
|
+
{% if row.source %}<span class="log-source-ref">{{ row.source }}</span>{% endif %}
|
|
30
|
+
{% if row.extra %}
|
|
31
|
+
<div class="log-meta-links">
|
|
32
|
+
{% if row.extra.showContextBtn %}<button class="log-ctx-btn" onclick="toggleLogCtx('{{ row.extra.ctxId }}', this)">{{ t('logs.show_context') }}</button>{% endif %}
|
|
33
|
+
{% if row.extra.showTraceBtn %}<button class="log-ctx-btn" onclick="toggleLogCtx('{{ row.extra.stackId }}', this)">{{ t('logs.show_trace') }}</button>{% endif %}
|
|
34
|
+
</div>
|
|
35
|
+
{% if row.extra.ctxJson %}<pre id="{{ row.extra.ctxId }}" class="log-context-detail">{{ row.extra.ctxJson }}</pre>{% endif %}
|
|
36
|
+
{% if row.extra.stackText %}<pre id="{{ row.extra.stackId }}" class="log-context-detail log-stack-trace">{{ row.extra.stackText }}</pre>{% endif %}
|
|
37
|
+
{% endif %}
|
|
38
|
+
</td>
|
|
39
|
+
</tr>
|
|
40
|
+
{% endfor %}
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
</div>
|
|
44
|
+
{% endif %}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{% from "macros/tables.njk" import kv_table with context %}
|
|
2
|
+
<div class="panel-title">{{ t('performance.title') }}</div>
|
|
3
|
+
|
|
4
|
+
<div class="perf-metrics-row">
|
|
5
|
+
<div class="perf-metric">
|
|
6
|
+
<div class="perf-metric-label">{{ t('performance.total_execution') }}</div>
|
|
7
|
+
<div class="perf-metric-value">{{ panelData.total }}<span class="perf-metric-unit">ms</span></div>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="perf-metric">
|
|
10
|
+
<div class="perf-metric-label">{{ t('performance.app_time') }}</div>
|
|
11
|
+
<div class="perf-metric-value">{{ panelData.appTime }}<span class="perf-metric-unit">ms</span></div>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="perf-metric">
|
|
14
|
+
<div class="perf-metric-label">{{ t('performance.peak_memory') }}</div>
|
|
15
|
+
<div class="perf-metric-value">{{ panelData.memoryMiB }}<span class="perf-metric-unit"> MiB</span></div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="panel-section">
|
|
20
|
+
<div class="panel-section-title">{{ t('performance.execution_timeline') }}</div>
|
|
21
|
+
|
|
22
|
+
<div class="tl-threshold-row">
|
|
23
|
+
{{ t('performance.threshold') }}
|
|
24
|
+
<input type="number" class="tl-threshold-input" id="tl-threshold" value="0" min="0" max="{{ panelData.total }}">
|
|
25
|
+
ms
|
|
26
|
+
<span class="tl-threshold-hint">{{ t('performance.threshold_hint') }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="tl-legend">
|
|
30
|
+
<span class="tl-legend-item"><span class="tl-legend-dot" style="background:#C87A3C;"></span> {{ t('performance.application_legend') }}</span>
|
|
31
|
+
{% if panelData.hasSqlTime %}<span class="tl-legend-item"><span class="tl-legend-dot" style="background:#2D6A4A;"></span> {{ t('performance.database_legend') }}</span>{% endif %}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="tl-chart">
|
|
35
|
+
{% if panelData.hasEvents %}
|
|
36
|
+
{% for e in panelData.events %}
|
|
37
|
+
<div class="tl-row">
|
|
38
|
+
<div class="tl-name">{{ e.label }}</div>
|
|
39
|
+
<div class="tl-track">
|
|
40
|
+
<div class="tl-bar" style="width:{{ e.widthPct }}%;background:{{ e.color }};">
|
|
41
|
+
<span class="tl-bar-label">{{ e.durationFormatted }}</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="tl-info">{{ e.durationFormatted }}</div>
|
|
45
|
+
</div>
|
|
46
|
+
{% endfor %}
|
|
47
|
+
{% else %}
|
|
48
|
+
<div class="tl-empty">{{ t('performance.no_timing_events') }}</div>
|
|
49
|
+
{% endif %}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="panel-section">
|
|
54
|
+
<div class="panel-section-title">{{ t('performance.process_info') }}</div>
|
|
55
|
+
<div class="table-wrap">{{ kv_table(panelData.processInfo) }}</div>
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
{% from "macros/tables.njk" import kv_table with context %}
|
|
2
|
+
{# <div class="panel-title">Request / Response</div> #}
|
|
3
|
+
|
|
4
|
+
<div class="stats-grid">
|
|
5
|
+
<div class="stat-card">
|
|
6
|
+
<div class="stat-value" style="color:{{ '#4caf50' if panelData.statusOk else '#e74c3c' }}">{{ panelData.statusCode }}</div>
|
|
7
|
+
<div class="stat-label">{{ t('panel.http_status') }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="stat-card">
|
|
10
|
+
<div class="stat-value">{{ panelData.durationFormatted }}</div>
|
|
11
|
+
<div class="stat-label">{{ t('panel.duration') }}</div>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="stat-card">
|
|
14
|
+
<div class="stat-value">{{ panelData.memoryFormatted }}</div>
|
|
15
|
+
<div class="stat-label">{{ t('panel.memory') }}</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="stat-card">
|
|
18
|
+
<div class="stat-value">{{ panelData.sqlQueryCount }}</div>
|
|
19
|
+
<div class="stat-label">{{ t('panel.sql_queries') }}</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="rr-tabs" id="rr-tabs" role="tablist">
|
|
24
|
+
<button type="button" class="rr-tab active" data-rtab="request">{{ t('request.tab_request') }}</button>
|
|
25
|
+
<button type="button" class="rr-tab" data-rtab="response">{{ t('request.tab_response') }}</button>
|
|
26
|
+
<button type="button" class="rr-tab" data-rtab="cookies">{{ t('request.tab_cookies') }}</button>
|
|
27
|
+
<button type="button" class="rr-tab{{ ' rr-tab-empty' if not panelData.hasSession }}" data-rtab="session">{{ t('request.tab_session') }}</button>
|
|
28
|
+
<button type="button" class="rr-tab" data-rtab="server">{{ t('request.tab_server') }}</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- ── REQUEST ── -->
|
|
32
|
+
<div class="rr-tabpanel active" data-rpanel="request">
|
|
33
|
+
<div class="card-grid">
|
|
34
|
+
<div class="card">
|
|
35
|
+
<div class="card-title">{{ t('request.get_params') }}</div>
|
|
36
|
+
{% if panelData.getParams.length > 0 %}{{ kv_table(panelData.getParams) }}{% else %}<div class="card-empty">{{ t('common.none') }}</div>{% endif %}
|
|
37
|
+
</div>
|
|
38
|
+
<div class="card">
|
|
39
|
+
<div class="card-title">{{ t('request.post_params') }}</div>
|
|
40
|
+
{% if panelData.postParamsJson %}<pre class="json-pre">{{ panelData.postParamsJson | safe }}</pre>{% else %}<div class="card-empty">{{ t('common.none') }}</div>{% endif %}
|
|
41
|
+
</div>
|
|
42
|
+
<div class="card">
|
|
43
|
+
<div class="card-title">{{ t('request.uploaded_files') }}</div>
|
|
44
|
+
<div class="card-empty">{{ t('common.none') }}</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="card">
|
|
47
|
+
<div class="card-title">{{ t('request.route_params') }}</div>
|
|
48
|
+
{% if panelData.routeParams.length > 0 %}{{ kv_table(panelData.routeParams) }}{% else %}<div class="card-empty">{{ t('common.none') }}</div>{% endif %}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="panel-section">
|
|
53
|
+
<div class="table-wrap">
|
|
54
|
+
<div class="table-wrap-title">{{ t('request.attributes') }} <input type="search" class="table-search" placeholder="{{ t('common.search_ellipsis') }}" oninput="filterTable(this)"></div>
|
|
55
|
+
{{ kv_table(panelData.attributes, headers=[t('common.key'), t('common.value')]) }}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="panel-section">
|
|
60
|
+
<div class="table-wrap">
|
|
61
|
+
<div class="table-wrap-title">{{ t('request.headers') }} <input type="search" class="table-search" placeholder="{{ t('common.search_ellipsis') }}" oninput="filterTable(this)"></div>
|
|
62
|
+
{{ kv_table(panelData.requestHeaders, headers=[t('common.header'), t('common.value')]) }}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="panel-section">
|
|
67
|
+
<div class="panel-section-title">{{ t('request.content') }}</div>
|
|
68
|
+
{% if panelData.requestContentJson %}
|
|
69
|
+
<pre class="json-pre">{{ panelData.requestContentJson | safe }}</pre>
|
|
70
|
+
{% else %}
|
|
71
|
+
<div class="table-wrap"><div class="empty-state"><div class="empty-text">{{ t('request.no_body') }}</div></div></div>
|
|
72
|
+
{% endif %}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="panel-section">
|
|
76
|
+
<div class="panel-section-title">{{ t('request.retry_command') }}</div>
|
|
77
|
+
<div class="curl-block">
|
|
78
|
+
<button type="button" class="curl-copy-btn" onclick="copyCurl(this)">{{ t('request.copy_curl') }}</button>
|
|
79
|
+
<pre class="curl-pre">{{ panelData.retryCommandCurl }}</pre>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- ── RESPONSE ── -->
|
|
85
|
+
<div class="rr-tabpanel" data-rpanel="response" style="display:none;">
|
|
86
|
+
<div class="panel-section">
|
|
87
|
+
<div class="table-wrap">
|
|
88
|
+
<div class="table-wrap-title">{{ t('request.response_headers') }}</div>
|
|
89
|
+
{{ kv_table(panelData.responseHeaders, headers=[t('common.header'), t('common.value')]) }}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="panel-section">
|
|
93
|
+
<div class="panel-section-title">{{ t('request.response_content') }}</div>
|
|
94
|
+
{% if panelData.responseBodyJson %}
|
|
95
|
+
<pre class="json-pre">{{ panelData.responseBodyJson | safe }}</pre>
|
|
96
|
+
{% else %}
|
|
97
|
+
<div class="table-wrap"><div class="empty-state"><div class="empty-text">{{ t('request.no_response_body') }}</div></div></div>
|
|
98
|
+
{% endif %}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- ── COOKIES ── -->
|
|
103
|
+
<div class="rr-tabpanel" data-rpanel="cookies" style="display:none;">
|
|
104
|
+
<div class="panel-section">
|
|
105
|
+
<div class="table-wrap">
|
|
106
|
+
<div class="table-wrap-title">{{ t('request.request_cookies') }}</div>
|
|
107
|
+
{{ kv_table(panelData.cookies, headers=[t('common.key'), t('common.value')]) }}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="panel-section">
|
|
111
|
+
<div class="table-wrap">
|
|
112
|
+
<div class="table-wrap-title">{{ t('request.response_cookies') }}</div>
|
|
113
|
+
{{ kv_table(panelData.responseCookies, headers=[t('common.key'), t('common.value')]) }}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<!-- ── SESSION ── -->
|
|
119
|
+
<div class="rr-tabpanel" data-rpanel="session" style="display:none;">
|
|
120
|
+
{% if panelData.hasSession %}
|
|
121
|
+
<div class="panel-section">
|
|
122
|
+
<div class="table-wrap">
|
|
123
|
+
<div class="table-wrap-title">{{ t('request.session_attributes') }}</div>
|
|
124
|
+
{{ kv_table(panelData.session, headers=[t('common.key'), t('common.value')]) }}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
{% else %}
|
|
128
|
+
<div class="empty-state"><div class="empty-text">{{ t('request.no_session') }}</div></div>
|
|
129
|
+
{% endif %}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
<!-- ── SERVER PARAMETERS ── -->
|
|
134
|
+
<div class="rr-tabpanel" data-rpanel="server" style="display:none;">
|
|
135
|
+
<div class="panel-section">
|
|
136
|
+
<div class="table-wrap">
|
|
137
|
+
<div class="table-wrap-title">{{ t('request.server_params') }}</div>
|
|
138
|
+
{{ kv_table(panelData.serverParams, headers=[t('common.key'), t('common.value')]) }}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{% import "macros/icons.njk" as icons %}
|
|
2
|
+
{% macro route_table(rows) %}
|
|
3
|
+
{%- if rows.length == 0 -%}
|
|
4
|
+
<div style="padding:14px;color:var(--muted);font-size:12px;text-align:center;">{{ t('routes.none') }}</div>
|
|
5
|
+
{%- else -%}
|
|
6
|
+
<table class="data-table">
|
|
7
|
+
<thead><tr><th>{{ t('routes.method') }}</th><th>{{ t('routes.path') }}</th><th style="text-align:center;">{{ t('routes.middlewares') }}</th></tr></thead>
|
|
8
|
+
<tbody>
|
|
9
|
+
{%- for r in rows %}
|
|
10
|
+
<tr>
|
|
11
|
+
<td><span class="route-method {{ r.methodClass }}">{{ r.method }}</span></td>
|
|
12
|
+
<td class="route-path">{% for seg in r.pathSegments %}{% if seg.type == "param" %}<span class="param">:{{ seg.value }}</span>{% else %}{{ seg.value }}{% endif %}{% endfor %}</td>
|
|
13
|
+
<td class="center mono muted">{{ r.middlewareCount }}</td>
|
|
14
|
+
</tr>
|
|
15
|
+
{%- endfor %}
|
|
16
|
+
</tbody>
|
|
17
|
+
</table>
|
|
18
|
+
{%- endif -%}
|
|
19
|
+
{% endmacro %}
|
|
20
|
+
|
|
21
|
+
{% if not panelData.hasRoutes %}
|
|
22
|
+
<div class="panel-title">{{ icons.icon("map") }} {{ t('routes.title') }}</div>
|
|
23
|
+
<div class="empty-state">
|
|
24
|
+
<div class="empty-icon">🗺</div>
|
|
25
|
+
<div class="empty-text">{{ t('routes.no_routes') }}</div>
|
|
26
|
+
</div>
|
|
27
|
+
{% else %}
|
|
28
|
+
<div class="panel-title">{{ icons.icon("map") }} {{ t('routes.title') }}</div>
|
|
29
|
+
|
|
30
|
+
<div class="routes-summary">
|
|
31
|
+
<div class="routes-summary-item"><div class="routes-summary-value">{{ panelData.totalRoutes }}</div><div class="routes-summary-label">{{ t('routes.total_routes') }}</div></div>
|
|
32
|
+
<div class="routes-summary-item"><div class="routes-summary-value">{{ panelData.appRoutesCount }}</div><div class="routes-summary-label">{{ t('routes.app_routes') }}</div></div>
|
|
33
|
+
{% for mc in panelData.methodCounts %}
|
|
34
|
+
<div class="routes-summary-item"><div class="routes-summary-value">{{ mc.count }}</div><div class="routes-summary-label">{{ mc.method }}</div></div>
|
|
35
|
+
{% endfor %}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{% if panelData.hasAppRoutes %}
|
|
39
|
+
<div class="panel-section">
|
|
40
|
+
<div class="panel-section-title">{{ t('routes.application_routes') }} <span class="badge badge-info">{{ panelData.appRoutesCount }}</span></div>
|
|
41
|
+
<div class="table-wrap">{{ route_table(panelData.appRoutesRows) }}</div>
|
|
42
|
+
</div>
|
|
43
|
+
{% endif %}
|
|
44
|
+
|
|
45
|
+
{% if panelData.hasDebugRoutes %}
|
|
46
|
+
<div class="panel-section">
|
|
47
|
+
<div class="panel-section-title">{{ t('routes.framework_debug_routes') }} <span class="badge badge-muted">{{ panelData.debugRoutesRows.length }}</span></div>
|
|
48
|
+
<div class="table-wrap">{{ route_table(panelData.debugRoutesRows) }}</div>
|
|
49
|
+
</div>
|
|
50
|
+
{% endif %}
|
|
51
|
+
{% endif %}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{% import "macros/icons.njk" as icons %}
|
|
2
|
+
{% from "macros/tables.njk" import kv_table with context %}
|
|
3
|
+
<div class="panel-title">{{ icons.icon("route") }} {{ t('panel.routing') }}</div>
|
|
4
|
+
|
|
5
|
+
<div class="panel-section">
|
|
6
|
+
<div class="panel-section-title">{{ t('routing.matched_route') }}</div>
|
|
7
|
+
<div class="table-wrap">{{ kv_table(panelData.routeRows) }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
{% if panelData.hasRouteParams %}
|
|
11
|
+
<div class="panel-section">
|
|
12
|
+
<div class="panel-section-title">{{ t('routing.route_params') }}</div>
|
|
13
|
+
<div class="table-wrap">{{ kv_table(panelData.routeParamsRows, headers=[t('common.name'), t('common.value')]) }}</div>
|
|
14
|
+
</div>
|
|
15
|
+
{% endif %}
|
|
16
|
+
|
|
17
|
+
{% if panelData.hasQueryParams %}
|
|
18
|
+
<div class="panel-section">
|
|
19
|
+
<div class="panel-section-title">{{ t('routing.query_string') }}</div>
|
|
20
|
+
<div class="table-wrap">{{ kv_table(panelData.queryParamsRows, headers=[t('common.key'), t('common.value')]) }}</div>
|
|
21
|
+
</div>
|
|
22
|
+
{% endif %}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{% import "macros/icons.njk" as icons %}
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
|
+
<title>OptiCoreJs Profiler — {{ profile.method }} {{ profile.url }}</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
11
|
+
<link rel="stylesheet" href="/_profiler/assets/css/profiler-detail.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
|
|
15
|
+
<!-- ── HEADER ── -->
|
|
16
|
+
<div style="max-width:2000px;margin:0 auto;padding:34px 200px 0 200px;">
|
|
17
|
+
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
18
|
+
<a href="/_profiler" style="display:flex;align-items:center;gap:14px;text-decoration:none;cursor:pointer;">
|
|
19
|
+
<span style="font-size: 20px; font-weight: 900; color:#222; letter-spacing:-.2px;">OptiCoreJs Profiler</span>
|
|
20
|
+
</a>
|
|
21
|
+
<div style="display:flex;align-items:center;gap:14px;">
|
|
22
|
+
<div style="position:relative;width:340px;">
|
|
23
|
+
<svg style="position:absolute;left:12px;top:50%;transform:translateY(-50%);" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
24
|
+
<input placeholder="{{ t('detail.search_placeholder') }}" disabled style="width:100%;height:42px;padding:0 14px 0 36px;border:1px solid #d8d8d8;border-radius:4px;font-family:'Montserrat',-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;font-size:14px;color:#555;outline:none;background:#fff;cursor:default;opacity:.6;">
|
|
25
|
+
</div>
|
|
26
|
+
<div class="lang-switch">
|
|
27
|
+
{% for l in languages %}
|
|
28
|
+
<a href="#" class="lang-switch-item{{ ' active' if l.active }}" onclick="event.preventDefault();var u=new URL(location.href);u.searchParams.set('lang','{{ l.code }}');location.href=u.toString();">{{ l.code | upper }}</a>
|
|
29
|
+
{% endfor %}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- ── STATUS BANNER ── -->
|
|
36
|
+
<div style="width: auto; margin: 22px auto 0 auto; padding:0 200px;">
|
|
37
|
+
<div style="background:{{ banner.bg }}; border-radius:6px; border-top:4px solid {{ banner.border }}; padding: 16px 16px 10px 16px;">
|
|
38
|
+
<div style="display:flex; align-items:center; gap:14px; margin-bottom: 10px;">
|
|
39
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="{{ banner.border }}" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="13"/><circle cx="12" cy="16.5" r="1.1" fill="{{ banner.border }}" stroke="none"/></svg>
|
|
40
|
+
<span style="font-size: 19px; font-weight: 900; color:{{ banner.code }};">
|
|
41
|
+
{{ 'ERROR ' if banner.isError }}{{ profile.statusCode }}
|
|
42
|
+
</span>
|
|
43
|
+
<span style="font-size: 19px; font-weight:400; color:{{ banner.msg }};">
|
|
44
|
+
{{ profile.statusMessage }}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div style="display:flex; align-items:center; gap:14px; margin-bottom: 10px;">
|
|
48
|
+
<span style="display:inline-block; padding: 2px 10px; border: 1px solid #d7d7d7; border-radius: 4px; font-size: 14px; font-weight:600; color:#555; background:#f5f5f5;">
|
|
49
|
+
{{ profile.method }}
|
|
50
|
+
</span>
|
|
51
|
+
<span style="font-size: 15px; font-weight:400; color:#333;">
|
|
52
|
+
{{ profile.url }}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div style="display:flex; align-items:center; gap:34px; font-size:16px; color:#777;">
|
|
56
|
+
<span><strong style="color:#555; font-size: 14px; font-weight:600;">IP:</strong> {{ profile.ip }}</span>
|
|
57
|
+
<span><strong style="color:#555; font-size: 14px; font-weight:600;">Profiled on:</strong> {{ profile.date }} at {{ profile.time }}</span>
|
|
58
|
+
<span><strong style="color:#555; font-size: 14px; font-weight:600;">Token:</strong> {{ profile.token6 }}</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- ── MAIN GRID ── -->
|
|
64
|
+
<div style="max-width:2000px; margin: 20px auto 0 auto;padding:0 200px 80px 200px;display:grid;grid-template-columns:250px 1fr;gap:40px;">
|
|
65
|
+
|
|
66
|
+
<!-- ── SIDEBAR CARD ── -->
|
|
67
|
+
<div>
|
|
68
|
+
<div style="border:1px solid #e3e3e3;border-radius:4px;overflow:hidden;">
|
|
69
|
+
|
|
70
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:18px 20px;border-bottom:1px solid #ececec;">
|
|
71
|
+
<a href="/_profiler" style="display:flex;align-items:center;gap:8px;text-decoration:none;">
|
|
72
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
73
|
+
<span style="font-size: 13px;color:#888;">{{ t('list.search_profiles') }}</span>
|
|
74
|
+
</a>
|
|
75
|
+
<a href="/_profiler/latest" style="font-size:13px; color:#C87A3C;text-decoration:none;cursor:pointer;">{{ t('list.latest') }}</a>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{% for item in sidebarItems %}
|
|
79
|
+
<a href="?panel={{ item.panel }}" style="display:flex;align-items:center;gap:14px;{{ 'justify-content:space-between;' if item.badge > 0 }}padding:16px 20px;{% if item.active %}border-left:4px solid #C87A3C;background:#FDF3EA;color:#C87A3C;font-weight:600;padding-left:20px;{% else %}padding-left:24px;color:#555;border-left:4px solid transparent;{% endif %}font-size:12.5px;text-decoration:none;" onmouseenter="if(!this.classList.contains('act'))this.style.background='#fafafa'" onmouseleave="if(!this.classList.contains('act'))this.style.background=''">
|
|
80
|
+
{% if item.badge > 0 %}
|
|
81
|
+
<span style="display:flex;align-items:center;gap:14px;">{{ icons.icon(item.icon) }} {{ t(item.label) }}</span>
|
|
82
|
+
<span style="background:{{ '#fbe3e4' if item.badgeIsError else '#e8f4ec' }};color:{{ '#c0392b' if item.badgeIsError else '#1a7a3c' }};border-radius:4px;padding:2px 9px;font-size:13px;font-weight:600;">{{ item.badge }}</span>
|
|
83
|
+
{% else %}
|
|
84
|
+
{{ icons.icon(item.icon) }} {{ t(item.label) }}
|
|
85
|
+
{% endif %}
|
|
86
|
+
</a>
|
|
87
|
+
{% endfor %}
|
|
88
|
+
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<a href="/_profiler" style="display:flex;align-items:center;gap:8px;margin-top:18px;color:#777;text-decoration:none;font-size:13px;">
|
|
92
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
|
93
|
+
{{ t('list.settings') }}
|
|
94
|
+
</a>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- ── CONTENT ── -->
|
|
98
|
+
<div class="pf-content">
|
|
99
|
+
<h1 class="panel-title">{{ t(panelLabel) }}</h1>
|
|
100
|
+
{% include "panels/" + panelName + ".njk" %}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<script>
|
|
106
|
+
window.__opticoreProfilerI18n = {
|
|
107
|
+
showContext: {{ t('logs.show_context') | dump | safe }},
|
|
108
|
+
hideContext: {{ t('logs.hide_context') | dump | safe }},
|
|
109
|
+
copied: {{ t('js.copied') | dump | safe }}
|
|
110
|
+
};
|
|
111
|
+
</script>
|
|
112
|
+
<script src="/_profiler/assets/js/profiler-detail.js"></script>
|
|
113
|
+
</body>
|
|
114
|
+
</html>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>OptiCoreJs Profiler</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/_profiler/assets/css/profiler-list.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body data-autoreload="{{ '1' if autoReload else '0' }}">
|
|
13
|
+
|
|
14
|
+
<div class="pl-header">
|
|
15
|
+
<div class="pl-header-row">
|
|
16
|
+
<a href="/_profiler" class="pl-brand">
|
|
17
|
+
<span class="pl-brand-name">OptiCoreJs Profiler</span>
|
|
18
|
+
</a>
|
|
19
|
+
<form method="get" action="/_profiler" class="pl-search-wrap">
|
|
20
|
+
<svg class="pl-search-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
21
|
+
<input type="search" name="search" value="{{ filters.search }}" class="pl-search-input" placeholder="{{ t('list.search_placeholder') }}">
|
|
22
|
+
</form>
|
|
23
|
+
<div class="lang-switch">
|
|
24
|
+
{% for l in languages %}
|
|
25
|
+
<a href="#" class="lang-switch-item{{ ' active' if l.active }}" onclick="event.preventDefault();var u=new URL(location.href);u.searchParams.set('lang','{{ l.code }}');location.href=u.toString();">{{ l.code | upper }}</a>
|
|
26
|
+
{% endfor %}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="pl-banner">
|
|
32
|
+
<div class="pl-banner-inner">
|
|
33
|
+
<h2 class="pl-banner-title">{{ t('list.profile_search') }}</h2>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="pl-grid">
|
|
38
|
+
|
|
39
|
+
<!-- SIDEBAR -->
|
|
40
|
+
<div>
|
|
41
|
+
<div class="pl-sidebar-card">
|
|
42
|
+
<div class="pl-sidebar-head">
|
|
43
|
+
<a href="/_profiler" class="pl-sidebar-search-link">
|
|
44
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
45
|
+
<span class="pl-sidebar-search-label">{{ t('list.search_profiles') }}</span>
|
|
46
|
+
</a>
|
|
47
|
+
<a href="/_profiler/latest" class="pl-sidebar-latest{{ ' is-disabled' if not filters.hasProfiles }}">{{ t('list.latest') }}</a>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<form method="get" action="/_profiler">
|
|
51
|
+
<label class="pl-label">{{ t('list.ip') }}</label>
|
|
52
|
+
<input name="ip" value="{{ filters.ip }}" class="pl-field pl-field--gap">
|
|
53
|
+
|
|
54
|
+
<div class="pl-field-row">
|
|
55
|
+
<div class="pl-field-col">
|
|
56
|
+
<label class="pl-label">{{ t('list.method') }}</label>
|
|
57
|
+
<div class="pl-select-wrap">
|
|
58
|
+
<select name="method" class="pl-field pl-select">
|
|
59
|
+
{% for opt in filters.methodOptions %}
|
|
60
|
+
<option value="{{ opt.value }}"{{ ' selected' if opt.selected }}>{{ t('list.method_any') if opt.value == '' else opt.label }}</option>
|
|
61
|
+
{% endfor %}
|
|
62
|
+
</select>
|
|
63
|
+
<span class="pl-chevron"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="pl-field-col">
|
|
67
|
+
<label class="pl-label">{{ t('list.status') }}</label>
|
|
68
|
+
<input name="status" value="{{ filters.status }}" class="pl-field">
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<label class="pl-label">{{ t('list.search_label') }}</label>
|
|
73
|
+
<input name="search" value="{{ filters.search }}" class="pl-field pl-field--gap" placeholder="{{ t('list.search_placeholder') }}">
|
|
74
|
+
|
|
75
|
+
<label class="pl-label">{{ t('list.token') }}</label>
|
|
76
|
+
<input name="token" value="{{ filters.token }}" class="pl-field pl-field--token">
|
|
77
|
+
|
|
78
|
+
<label class="pl-label">{{ t('list.from') }}</label>
|
|
79
|
+
<div class="pl-field-date-wrap">
|
|
80
|
+
<input type="date" name="from" value="{{ filters.from }}" class="pl-field pl-date">
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<label class="pl-label">{{ t('list.until') }}</label>
|
|
84
|
+
<div class="pl-field-date-wrap">
|
|
85
|
+
<input type="date" name="until" value="{{ filters.until }}" class="pl-field pl-date">
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<label class="pl-label">{{ t('list.results') }}</label>
|
|
89
|
+
<div class="pl-results-row">
|
|
90
|
+
<div class="pl-select-wrap pl-select-wrap--limit">
|
|
91
|
+
<select name="limit" class="pl-field pl-select" style="width:74px;">
|
|
92
|
+
{% for opt in filters.limitOptions %}
|
|
93
|
+
<option value="{{ opt.value }}"{{ ' selected' if opt.selected }}>{{ opt.value }}</option>
|
|
94
|
+
{% endfor %}
|
|
95
|
+
</select>
|
|
96
|
+
<span class="pl-chevron pl-chevron--limit"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
97
|
+
</div>
|
|
98
|
+
<button type="submit" class="pl-submit-btn">{{ t('list.search_btn') }}</button>
|
|
99
|
+
</div>
|
|
100
|
+
</form>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<a href="/_profiler" class="pl-settings-link">
|
|
104
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
|
105
|
+
{{ t('list.settings') }}
|
|
106
|
+
</a>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- RESULTS -->
|
|
110
|
+
<div class="pl-results">
|
|
111
|
+
<div class="pl-tabs">
|
|
112
|
+
<a href="{{ tabs.requests.href }}" class="pl-tab">
|
|
113
|
+
<span class="pl-tab-label{{ ' is-active' if tabs.requests.active }}">{{ t('list.http_requests_tab') }}</span>
|
|
114
|
+
<span class="pl-tab-underline{{ ' is-active' if tabs.requests.active }}"></span>
|
|
115
|
+
</a>
|
|
116
|
+
<a href="{{ tabs.commands.href }}" class="pl-tab">
|
|
117
|
+
<span class="pl-tab-label{{ ' is-active' if tabs.commands.active }}">{{ t('list.console_commands_tab') }}</span>
|
|
118
|
+
<span class="pl-tab-underline{{ ' is-active' if tabs.commands.active }}"></span>
|
|
119
|
+
</a>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{% if tab == "commands" %}
|
|
123
|
+
<h2 class="pl-section-heading">{{ t('list.console_commands_tab') }}</h2>
|
|
124
|
+
<div class="pl-empty-commands">
|
|
125
|
+
<div class="pl-empty-commands-text">{{ t('list.no_commands') }}</div>
|
|
126
|
+
</div>
|
|
127
|
+
{% else %}
|
|
128
|
+
<h2 class="pl-section-heading">{{ t('list.results_found_one' if resultCount == 1 else 'list.results_found_other', { n: resultCount }) }}</h2>
|
|
129
|
+
<table class="pl-table">
|
|
130
|
+
<thead>
|
|
131
|
+
<tr>
|
|
132
|
+
<th>{{ t('list.col_status') }}</th><th>{{ t('list.col_method') }}</th><th>{{ t('list.col_url') }}</th><th>{{ t('list.col_time') }}</th><th>{{ t('list.col_token') }}</th>
|
|
133
|
+
</tr>
|
|
134
|
+
</thead>
|
|
135
|
+
<tbody>
|
|
136
|
+
{% if rows.length > 0 %}
|
|
137
|
+
{% for row in rows %}
|
|
138
|
+
<tr class="pl-row pl-row-font-size" onclick="location.href='/_profiler/{{ row.token }}'">
|
|
139
|
+
<td class="pl-cell"><span class="pl-status-badge pl-status-badge--{{ row.statusClass }}">{{ row.statusCode }}</span></td>
|
|
140
|
+
<td class="pl-cell">{{ row.method }}</td>
|
|
141
|
+
<td class="pl-cell pl-cell-url" title="{{ row.url }}">{{ row.url }}</td>
|
|
142
|
+
<td class="pl-cell pl-cell-date">{{ row.dateStr }}<br><span class="pl-cell-time">{{ row.timeStr }}</span></td>
|
|
143
|
+
<td class="pl-cell"><a href="/_profiler/{{ row.token }}" onclick="event.stopPropagation()" class="pl-token-link">{{ row.tokenShort }}</a></td>
|
|
144
|
+
</tr>
|
|
145
|
+
{% endfor %}
|
|
146
|
+
{% else %}
|
|
147
|
+
<tr><td colspan="5" class="pl-empty-row">{{ t('list.no_requests_found') }}</td></tr>
|
|
148
|
+
{% endif %}
|
|
149
|
+
</tbody>
|
|
150
|
+
</table>
|
|
151
|
+
{% endif %}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<script>
|
|
156
|
+
window.__opticoreProfilerI18n = {
|
|
157
|
+
resultsFoundOne: {{ t('list.results_found_one') | dump | safe }},
|
|
158
|
+
resultsFoundOther: {{ t('list.results_found_other') | dump | safe }}
|
|
159
|
+
};
|
|
160
|
+
</script>
|
|
161
|
+
<script src="/_profiler/assets/js/profiler-list.js"></script>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|