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.0">
6 <title>Querystats</title>
7 {% tailwind_css %}
8</head>
9<body class="text-stone-300">
10
11 {% if querystats_enabled %}
12 <div class="flex items-center justify-between border-b border-white/5 px-6 h-14 fixed top-0 left-0 right-0 bg-stone-950 z-10">
13 <!-- <h1 class="text-lg font-semibold">Querystats</h1> -->
14 <div></div>
15 <div class="flex items-center space-x-2">
16 <form method="get" action=".">
17 {{ csrf_input }}
18 <button type="submit" class="px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Reload</button>
19 </form>
20 <form method="post" action=".">
21 {{ csrf_input }}
22 <input type="hidden" name="querystats_action" value="clear">
23 <button type="submit" class="px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Clear</button>
24 </form>
25 <form method="post" action=".">
26 {{ csrf_input }}
27 <input type="hidden" name="querystats_action" value="disable">
28 <button type="submit" class="px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Disable</button>
29 </form>
30 </div>
31 </div>
32 {% endif %}
33
34 {% if querystats %}
35 <div class="flex mt-2 h-full">
36 <aside id="sidebar" class="fixed left-0 top-14 bottom-0 w-82 overflow-auto p-4">
37 <ul class="space-y-2">
38 {% for request_id, qs in querystats.items() %}
39 <li>
40 <button data-request-id="{{ request_id }}" class="w-full text-left px-2 py-1 rounded hover:bg-stone-700 cursor-pointer">
41 <span class="text-sm">{{ qs.request.path }}</span>
42 <span class="font-semibold bg-white/5 rounded-sm px-1 py-0.5 text-xs">{{ qs.request.method }}</span>
43 <div class="text-xs text-stone-400">{{ qs.summary }}</div>
44 <div class="text-xs text-stone-500">{{ qs.timestamp|fromisoformat|timesince }} ago</div>
45 </button>
46 </li>
47 {% endfor %}
48 </ul>
49 </aside>
50
51 <main id="content" class="flex-1 p-6 overflow-auto ml-82 mt-14">
52 {% for request_id, qs in querystats.items() %}
53 <div class="request-detail" data-request-id="{{ request_id }}" style="display: none;">
54 <div class="flex justify-between">
55 <div>
56 <h2 class="font-medium text-sm"><span class="font-semibold">{{ qs.request.method }}</span> {{ qs.request.path }}</h2>
57 <p class="text-sm text-white/70">{{ qs.summary }}</p>
58 </div>
59 <div class="text-right">
60 <div class="text-xs text-white/60">Request ID <code>{{ qs.request.unique_id }}</code></div>
61 <div class="text-xs text-white/60"><code>{{ qs.timestamp|fromisoformat }}</code></div>
62 </div>
63 </div>
64
65 <div class="flex w-full mt-3 overflow-auto rounded-sm">
66 {% for query in qs.queries %}
67 <a href="#query-{{ loop.index }}"
68 {{ loop.cycle('class=\"h-2 bg-amber-400\"', 'class=\"h-2 bg-orange-400\"', 'class=\"h-2 bg-yellow-400\"', 'class=\"h-2 bg-amber-600\"')|safe }}
69 title="[{{ query.duration_display }}] {{ query.sql_display }}"
70 style="width: {{ query.duration / qs.total_time * 100 }}%">
71 </a>
72 {% endfor %}
73 </div>
74
75 <div class="mt-4 space-y-3 text-xs">
76 {% for query in qs.queries %}
77 <details id="query-{{ loop.index }}" class="p-2 rounded bg-white/5">
78 <summary class="truncate">
79 <div class="float-right px-2 py-px mb-px ml-2 text-xs rounded-full bg-zinc-700">
80 <span>{{ query.duration_display }}</span>
81 {% if query.duplicate_count is defined %}
82 <span class="text-red-500"> duplicated {{ query.duplicate_count }} times</span>
83 {% endif %}
84 </div>
85 <code class="font-mono">{{ query.sql }}</code>
86 </summary>
87 <div class="space-y-3 mt-3">
88 <div>
89 <pre><code class="font-mono whitespace-pre-wrap text-zinc-100">{{ query.sql_display }}</code></pre>
90 </div>
91 <div class="text-zinc-400">
92 <span class="font-medium">Parameters</span>
93 <pre><code class="font-mono">{{ query.params|pprint }}</code></pre>
94 </div>
95 {% if query.tb|default(false) %}
96 <details>
97 <summary>Traceback</summary>
98 <pre><code class="block overflow-x-auto font-mono text-xs">{{ query.tb }}</code></pre>
99 </details>
100 {% endif %}
101 </div>
102 </details>
103 {% else %}
104 <div>No queries...</div>
105 {% endfor %}
106 </div>
107 </div>
108 {% endfor %}
109 </main>
110 </div>
111 {% elif querystats_enabled %}
112 <div class="text-center text-white/30 py-8">Querystats are enabled but nothing has been recorded yet.</div>
113 {% else %}
114 <div class="text-center py-8">
115 <div class="text-white/30">Querystats are disabled.</div>
116 <form method="post" action=".">
117 {{ csrf_input }}
118 <input type="hidden" name="querystats_action" value="enable">
119 <button type="submit" class="mt-2 px-2 py-px text-sm rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer whitespace-nowrap">Enable</button>
120 </form>
121 </div>
122 {% endif %}
123
124 <script>
125 document.addEventListener('DOMContentLoaded', function() {
126 const buttons = document.querySelectorAll('#sidebar [data-request-id]');
127 const details = document.querySelectorAll('#content .request-detail');
128 buttons.forEach(function(btn) {
129 btn.addEventListener('click', function(e) {
130 e.preventDefault();
131 const id = this.getAttribute('data-request-id');
132 details.forEach(div => div.style.display = 'none');
133 const sel = document.querySelector('#content .request-detail[data-request-id="' + id + '"]');
134 if (sel) sel.style.display = 'block';
135 buttons.forEach(b => b.classList.remove('bg-stone-700', 'text-white'));
136 this.classList.add('bg-stone-700', 'text-white');
137 });
138 });
139 if (buttons.length > 0) buttons[0].click();
140 });
141 </script>
142
143 </body>
144</html>