1{# Observer pill + record dot, visually semi-attached #}
2<div class="inline-flex items-center gap-px">
3
4{# Record dot โ separate clickable circle, tight against the pill #}
5{% if observer.is_summarizing() or observer.is_persist_once() %}
6<button
7 type="button"
8 class="inline-flex items-center justify-center cursor-pointer rounded-full px-1.5 py-px bg-white/8 text-white/30 hover:text-red-400 hover:bg-white/12 transition-colors self-stretch"
9 data-observer-record-page
10 title="Record this page"
11>
12 <svg class="size-1.5" viewBox="0 0 16 16" fill="currentColor">
13 <circle cx="8" cy="8" r="8"/>
14 </svg>
15</button>
16{% endif %}
17
18{# Request pill โ observer state + request stats, clickable to open Observer panel #}
19<button
20 type="button"
21 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"
22 data-toolbar-tab="Observer"
23>
24 {% if observer.is_recording_session() %}
25 <span class="inline-flex items-center">
26 <span class="relative inline-flex size-1.5 mr-1.5 flex-shrink-0">
27 <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
28 <span class="relative inline-flex size-1.5 rounded-full bg-red-500"></span>
29 </span>
30 <span class="text-[11px]">Recording</span>
31 </span>
32 {% elif not observer.is_enabled() %}
33 <span class="inline-flex items-center gap-1.5">
34 <span class="rounded-full bg-zinc-500 size-1.5 flex-shrink-0"></span>
35 <span class="text-[11px]">Disabled</span>
36 </span>
37 {% endif %}
38
39 {% if trace_stats %}
40 <span
41 class="text-[11px] text-white/50
42 data-[level=warn]:text-amber-400
43 data-[level=danger]:text-red-400"
44 title="SQL queries"
45 {% if trace_stats.query_level != "ok" %}data-level="{{ trace_stats.query_level }}"{% endif %}
46 >{{ trace_stats.query_count }}{% if trace_stats.duplicate_count %}+{{ trace_stats.duplicate_count }}{% endif %} {{ "query" if trace_stats.query_count == 1 else "queries" }}</span>
47 {% if trace_stats.duration_display %}
48 <span
49 class="text-[11px] text-white/50
50 data-[level=warn]:text-amber-400
51 data-[level=danger]:text-red-400"
52 title="Request duration"
53 {% if trace_stats.duration_level != "ok" %}data-level="{{ trace_stats.duration_level }}"{% endif %}
54 >{{ trace_stats.duration_display }}</span>
55 {% endif %}
56 {% endif %}
57
58 <span
59 data-observer-response-size
60 class="hidden text-[11px] text-white/50
61 data-[level=warn]:text-amber-400
62 data-[level=danger]:text-red-400"
63 title="Response body size"
64 data-observer-response-size-value
65 ></span>
66</button>
67
68{% if system_stats %}
69{# System pill โ CPU and memory, non-interactive #}
70<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">
71 {% if system_stats.cpu_percent is defined %}
72 <span class="items-baseline text-[11px] tabular-nums" title="Server process CPU usage">
73 <span class="text-[9px] text-white/30 mr-0.5">CPU</span>
74 <span
75 class="text-white/50
76 data-[level=warn]:text-amber-400
77 data-[level=danger]:text-red-400"
78 {% if system_stats.cpu_level != "ok" %}data-level="{{ system_stats.cpu_level }}"{% endif %}
79 >{{ system_stats.cpu_percent }}%</span>
80 </span>
81 {% endif %}
82 {% if system_stats.mem_display %}
83 <span class="items-baseline text-[11px] tabular-nums" title="{{ system_stats.mem_title }}">
84 <span class="text-[9px] text-white/30 mr-0.5">MEM</span>
85 <span
86 class="text-white/50
87 data-[level=warn]:text-amber-400
88 data-[level=danger]:text-red-400"
89 {% if system_stats.mem_level != "ok" %}data-level="{{ system_stats.mem_level }}"{% endif %}
90 >{{ system_stats.mem_display }}</span>
91 </span>
92 {% endif %}
93</span>
94{% endif %}
95
96</div>
97
98<script nonce="{{ request.csp_nonce }}">
99(function() {
100 function formatBytes(bytes) {
101 if (bytes >= 1000000) return (bytes / 1000000).toFixed(1) + " MB";
102 if (bytes >= 1000) return Math.round(bytes / 1000) + " KB";
103 return bytes + " B";
104 }
105
106 window.addEventListener("load", function() {
107 var nav = performance.getEntriesByType("navigation")[0];
108 var el = document.querySelector("[data-observer-response-size]");
109 if (nav && nav.decodedBodySize > 0 && el) {
110 el.textContent = formatBytes(nav.decodedBodySize);
111 if (nav.decodedBodySize >= 1000000) {
112 el.dataset.level = "danger";
113 } else if (nav.decodedBodySize >= 100000) {
114 el.dataset.level = "warn";
115 }
116 el.classList.remove("hidden");
117 }
118 });
119
120 const recordBtn = document.querySelector("[data-observer-record-page]");
121
122 {% if observer.is_persist_once() %}
123 // Revert persist_once cookie back to summary, then re-enable record button
124 const revertForm = new FormData();
125 revertForm.append("observe_action", "summary");
126 if (recordBtn) recordBtn.disabled = true;
127 fetch("{{ url('observer:traces') }}", {
128 method: "POST",
129 body: revertForm,
130 credentials: "same-origin"
131 }).then(function() {
132 if (recordBtn) recordBtn.disabled = false;
133 });
134
135 window.addEventListener("load", function() {
136 if (window.plainToolbar) {
137 window.plainToolbar.showTab("Observer");
138 }
139 });
140 {% endif %}
141
142 if (recordBtn) {
143 recordBtn.addEventListener("click", function(e) {
144 e.stopPropagation();
145 recordBtn.style.color = "#f87171";
146 recordBtn.disabled = true;
147 const form = new FormData();
148 form.append("observe_action", "persist_once");
149 fetch("{{ url('observer:traces') }}", {
150 method: "POST",
151 body: form,
152 credentials: "same-origin"
153 }).then(function() {
154 window.location.reload();
155 });
156 });
157 }
158})();
159</script>