1{# Request pill — observer state + request stats, clickable to open Observer panel #}
2<button
3 type="button"
4 class="inline-flex items-center cursor-pointer text-xs rounded-full px-1 py-px bg-white/8 text-white/80 hover:bg-white/12 overflow-hidden divide-x divide-white/10 [&>span]:px-1.5"
5 data-toolbar-tab="Observer"
6>
7 {% if observer.is_persisting() %}
8 <span class="inline-flex items-center">
9 <span class="relative inline-flex size-1.5 mr-1.5 flex-shrink-0">
10 <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
11 <span class="relative inline-flex size-1.5 rounded-full bg-red-500"></span>
12 </span>
13 <span class="text-[11px]">Recording</span>
14 </span>
15 {% elif not observer.is_enabled() %}
16 <span class="inline-flex items-center gap-1.5">
17 <span class="rounded-full bg-zinc-500 size-1.5 flex-shrink-0"></span>
18 <span class="text-[11px]">Disabled</span>
19 </span>
20 {% endif %}
21
22 {% if trace_stats %}
23 <span
24 class="text-[11px] text-white/50
25 data-[level=warn]:text-amber-400
26 data-[level=danger]:text-red-400"
27 title="SQL queries"
28 {% if trace_stats.query_level != "ok" %}data-level="{{ trace_stats.query_level }}"{% endif %}
29 >{{ trace_stats.query_count }}{% if trace_stats.duplicate_count %}+{{ trace_stats.duplicate_count }}{% endif %} {{ "query" if trace_stats.query_count == 1 else "queries" }}</span>
30 {% if trace_stats.duration_display %}
31 <span
32 class="text-[11px] text-white/50
33 data-[level=warn]:text-amber-400
34 data-[level=danger]:text-red-400"
35 title="Request duration"
36 {% if trace_stats.duration_level != "ok" %}data-level="{{ trace_stats.duration_level }}"{% endif %}
37 >{{ trace_stats.duration_display }}</span>
38 {% endif %}
39 {% endif %}
40
41 <span
42 data-observer-response-size
43 class="hidden text-[11px] text-white/50
44 data-[level=warn]:text-amber-400
45 data-[level=danger]:text-red-400"
46 title="Response body size"
47 data-observer-response-size-value
48 ></span>
49</button>
50
51{% if system_stats %}
52{# System pill — CPU and memory, non-interactive #}
53<span class="inline-flex items-center text-xs rounded-full px-1 py-px bg-white/8 overflow-hidden divide-x divide-white/10 [&>span]:px-1.5">
54 {% if system_stats.cpu_percent is defined %}
55 <span class="items-baseline text-[11px] tabular-nums" title="Server process CPU usage">
56 <span class="text-[9px] text-white/30 mr-0.5">CPU</span>
57 <span
58 class="text-white/50
59 data-[level=warn]:text-amber-400
60 data-[level=danger]:text-red-400"
61 {% if system_stats.cpu_level != "ok" %}data-level="{{ system_stats.cpu_level }}"{% endif %}
62 >{{ system_stats.cpu_percent }}%</span>
63 </span>
64 {% endif %}
65 {% if system_stats.mem_display %}
66 <span class="items-baseline text-[11px] tabular-nums" title="{{ system_stats.mem_title }}">
67 <span class="text-[9px] text-white/30 mr-0.5">MEM</span>
68 <span
69 class="text-white/50
70 data-[level=warn]:text-amber-400
71 data-[level=danger]:text-red-400"
72 {% if system_stats.mem_level != "ok" %}data-level="{{ system_stats.mem_level }}"{% endif %}
73 >{{ system_stats.mem_display }}</span>
74 </span>
75 {% endif %}
76</span>
77{% endif %}
78
79<script nonce="{{ request.csp_nonce }}">
80(function() {
81 function formatBytes(bytes) {
82 if (bytes >= 1000000) return (bytes / 1000000).toFixed(1) + " MB";
83 if (bytes >= 1000) return Math.round(bytes / 1000) + " KB";
84 return bytes + " B";
85 }
86
87 window.addEventListener("load", function() {
88 var nav = performance.getEntriesByType("navigation")[0];
89 var el = document.querySelector("[data-observer-response-size]");
90 if (nav && nav.decodedBodySize > 0 && el) {
91 el.textContent = formatBytes(nav.decodedBodySize);
92 if (nav.decodedBodySize >= 1000000) {
93 el.dataset.level = "danger";
94 } else if (nav.decodedBodySize >= 100000) {
95 el.dataset.level = "warn";
96 }
97 el.classList.remove("hidden");
98 }
99 });
100})();
101</script>