Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/TEMPLATES/tf_admin_metrics.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/TEMPLATES/tf_admin_metrics.html

added on local at 2026-07-11 18:33:06

Added
+446
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to eba99f51bb13
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<div class="page-header"><div><h1>System metrics</h1><p class="subtitle">Platform-wide health. Hover any tile for a preview, click to drill in.</p></div></div>
2
3<style>
4 /* Make the whole KPI tile behave like a link */
5 a.kpi { display: block; text-decoration: none; color: inherit; cursor: pointer; }
6 a.kpi:hover { text-decoration: none; }
7
8 /* Mouse-following preview overlay */
9 .kpi-pv {
10 position: fixed;
11 z-index: 9999;
12 pointer-events: none;
13 min-width: 280px;
14 max-width: 380px;
15 padding: 12px 14px 10px;
16 background: linear-gradient(135deg, rgba(20,27,45,.97) 0%, rgba(28,22,52,.97) 50%, rgba(18,25,42,.97) 100%);
17 backdrop-filter: blur(12px) saturate(140%);
18 -webkit-backdrop-filter: blur(12px) saturate(140%);
19 color: #f1f6ff;
20 font-family: var(--font-display, inherit);
21 font-size: 12.5px;
22 line-height: 1.45;
23 border-radius: 12px;
24 border: 1px solid rgba(90,169,255,.4);
25 box-shadow:
26 0 18px 44px rgba(0,0,0,.55),
27 0 0 24px rgba(90,169,255,.22),
28 0 0 60px rgba(139,92,246,.16);
29 opacity: 0;
30 transform: translateY(6px) scale(.96);
31 transition: opacity 90ms ease-out, transform 90ms ease-out;
32 }
33 .kpi-pv.open { opacity: 1; transform: translateY(0) scale(1); }
34 .kpi-pv-head {
35 font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em;
36 color: #b6c4dd; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;
37 }
38 .kpi-pv-head .pv-count { color: #5aa9ff; font-weight: 600; }
39 .kpi-pv-row {
40 display: flex; align-items: center; gap: 9px;
41 padding: 7px 0; border-top: 1px solid rgba(255,255,255,.06);
42 }
43 .kpi-pv-row:first-of-type { border-top: 0; padding-top: 4px; }
44 .kpi-pv-avatar {
45 width: 26px; height: 26px; border-radius: 50%;
46 display: inline-flex; align-items: center; justify-content: center;
47 font-size: 10.5px; font-weight: 600; color: #fff;
48 flex-shrink: 0;
49 box-shadow: 0 0 0 2px rgba(255,255,255,.04);
50 }
51 .kpi-pv-dot {
52 width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0;
53 box-shadow: 0 0 0 2px rgba(255,255,255,.04);
54 }
55 .kpi-pv-main { flex: 1; min-width: 0; overflow: hidden; }
56 .kpi-pv-name {
57 color: #fff; font-weight: 600; font-size: 13px;
58 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
59 }
60 .kpi-pv-sub {
61 color: #95a3bd; font-size: 11.5px;
62 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
63 }
64 .kpi-pv-meta {
65 flex-shrink: 0; font-size: 11px; color: #b6c4dd; text-align: right;
66 }
67 .kpi-pv-meta .pv-warn { color: #ffb05c; font-weight: 600; }
68 .kpi-pv-meta .pv-ok { color: #5fd38a; font-weight: 600; }
69 .kpi-pv-empty { color: #95a3bd; padding: 6px 0 2px; font-style: italic; }
70 .kpi-pv-footer {
71 margin-top: 8px; padding-top: 8px;
72 border-top: 1px solid rgba(255,255,255,.08);
73 font-size: 11px; color: #5aa9ff; text-align: right;
74 font-weight: 600; letter-spacing: .02em;
75 }
76
77 /* Hidden preview source blocks (cloned into the overlay on hover) */
78 .kpi-pv-src { display: none; }
79</style>
80
81<div class="kpi-grid">
82 <a class="kpi" href="/admin_companies.cgi" data-pv="pv-total">
83 <div class="kpi-label">Total companies</div><div class="kpi-value">$k.companies_n</div>
84 </a>
85 <a class="kpi success" href="/admin_companies.cgi?status=paying" data-pv="pv-paying">
86 <div class="kpi-label">Paying customers</div><div class="kpi-value">$k.paying_n</div>
87 </a>
88 <a class="kpi warn" href="/admin_companies.cgi?status=trial" data-pv="pv-trial">
89 <div class="kpi-label">On trial</div><div class="kpi-value">$k.trial_n</div>
90 </a>
91 <a class="kpi" href="/admin_users.cgi?status=active" data-pv="pv-active">
92 <div class="kpi-label">Active users</div><div class="kpi-value">$k.users_n</div>
93 </a>
94 <a class="kpi accent" href="/admin_users.cgi?sessions=live" data-pv="pv-sessions">
95 <div class="kpi-label">Live sessions</div><div class="kpi-value">$k.sessions_n</div>
96 </a>
97 <a class="kpi" href="/admin_users.cgi?active=7" data-pv="pv-wau">
98 <div class="kpi-label">WAU (7d)</div><div class="kpi-value">$k.wau</div>
99 </a>
100 <a class="kpi" href="/admin_users.cgi?active=30" data-pv="pv-mau">
101 <div class="kpi-label">MAU (30d)</div><div class="kpi-value">$k.mau</div>
102 </a>
103 <a class="kpi success" href="/admin_companies.cgi?recent=30" data-pv="pv-newsignups">
104 <div class="kpi-label">New signups (30d)</div><div class="kpi-value">$k.new_companies_30d</div>
105 </a>
106 <a class="kpi" href="/admin_companies.cgi?sort=projects" data-pv="pv-projects">
107 <div class="kpi-label">Projects</div><div class="kpi-value">$k.projects_n</div>
108 </a>
109 <a class="kpi" href="/admin_companies.cgi?sort=tasks" data-pv="pv-tasks">
110 <div class="kpi-label">Tasks (all-time)</div><div class="kpi-value">$k.tasks_n</div>
111 </a>
112 <a class="kpi success" href="/admin_companies.cgi?sort=tasks_done" data-pv="pv-tasks-done">
113 <div class="kpi-label">Tasks completed</div><div class="kpi-value">$k.tasks_done_n</div>
114 </a>
115 <a class="kpi accent" href="/admin_visitors.cgi" data-pv="pv-actions">
116 <div class="kpi-label">Actions (24h)</div><div class="kpi-value">$k.actions_24h</div>
117 </a>
118</div>
119
120<!-- ============================================================ -->
121<!-- Preview source blocks. JS clones the matching block into the -->
122<!-- floating overlay on hover. -->
123<!-- ============================================================ -->
124
125<div class="kpi-pv-src" data-pv-id="pv-total">
126 <div class="kpi-pv-head"><span>Most recent companies</span><span class="pv-count">$k.companies_n total</span></div>
127 [if:$has_pv_total]
128 [loop:@pv_total]
129 <div class="kpi-pv-row">
130 <span class="kpi-pv-dot" style="background:$loop1.brand_color"></span>
131 <div class="kpi-pv-main">
132 <div class="kpi-pv-name">$loop1.name</div>
133 <div class="kpi-pv-sub">$loop1.plan_tier &middot; $loop1.users_n users</div>
134 </div>
135 <div class="kpi-pv-meta"><span class="ts" data-ts="$loop1.created_epoch" data-fmt="datetime">$loop1.created_at</span></div>
136 </div>
137 [/loop]
138 [/if]
139 [if:!$has_pv_total]<div class="kpi-pv-empty">No companies yet.</div>[/if]
140 <div class="kpi-pv-footer">Click to view all companies &rarr;</div>
141</div>
142
143<div class="kpi-pv-src" data-pv-id="pv-paying">
144 <div class="kpi-pv-head"><span>Paying customers</span><span class="pv-count">$k.paying_n paying</span></div>
145 [if:$has_pv_paying]
146 [loop:@pv_paying]
147 <div class="kpi-pv-row">
148 <span class="kpi-pv-dot" style="background:$loop1.brand_color"></span>
149 <div class="kpi-pv-main">
150 <div class="kpi-pv-name">$loop1.name</div>
151 <div class="kpi-pv-sub">$loop1.plan_tier &middot; $loop1.users_n users</div>
152 </div>
153 <div class="kpi-pv-meta"><span class="pv-ok">PAID</span></div>
154 </div>
155 [/loop]
156 [/if]
157 [if:!$has_pv_paying]<div class="kpi-pv-empty">No paying customers yet.</div>[/if]
158 <div class="kpi-pv-footer">Click to manage paying customers &rarr;</div>
159</div>
160
161<div class="kpi-pv-src" data-pv-id="pv-trial">
162 <div class="kpi-pv-head"><span>Trial customers (great support targets)</span><span class="pv-count">$k.trial_n on trial</span></div>
163 [if:$has_pv_trial]
164 [loop:@pv_trial]
165 <div class="kpi-pv-row">
166 <span class="kpi-pv-dot" style="background:$loop1.brand_color"></span>
167 <div class="kpi-pv-main">
168 <div class="kpi-pv-name">$loop1.name</div>
169 <div class="kpi-pv-sub">$loop1.users_n users &middot; ends <span class="ts" data-ts="$loop1.trial_ends_epoch" data-fmt="datetime">$loop1.trial_ends_at</span></div>
170 </div>
171 <div class="kpi-pv-meta"><span class="pv-warn">$loop1.days_left d left</span></div>
172 </div>
173 [/loop]
174 [/if]
175 [if:!$has_pv_trial]<div class="kpi-pv-empty">No active trials.</div>[/if]
176 <div class="kpi-pv-footer">Click to reach out to trial users &rarr;</div>
177</div>
178
179<div class="kpi-pv-src" data-pv-id="pv-active">
180 <div class="kpi-pv-head"><span>Most recently active users</span><span class="pv-count">$k.users_n active</span></div>
181 [if:$has_pv_active]
182 [loop:@pv_active]
183 <div class="kpi-pv-row">
184 <span class="kpi-pv-avatar" style="background:$loop1.avatar_color">$loop1.initials</span>
185 <div class="kpi-pv-main">
186 <div class="kpi-pv-name">$loop1.display_name</div>
187 <div class="kpi-pv-sub">$loop1.email &middot; $loop1.company_name</div>
188 </div>
189 <div class="kpi-pv-meta"><span class="ts" data-ts="$loop1.last_login_epoch" data-fmt="relative">$loop1.last_login_at</span></div>
190 </div>
191 [/loop]
192 [/if]
193 [if:!$has_pv_active]<div class="kpi-pv-empty">No active users.</div>[/if]
194 <div class="kpi-pv-footer">Click to see all active users &rarr;</div>
195</div>
196
197<div class="kpi-pv-src" data-pv-id="pv-sessions">
198 <div class="kpi-pv-head"><span>Live sessions right now</span><span class="pv-count">$k.sessions_n live</span></div>
199 [if:$has_pv_sessions]
200 [loop:@pv_sessions]
201 <div class="kpi-pv-row">
202 <span class="kpi-pv-avatar" style="background:$loop1.avatar_color">$loop1.initials</span>
203 <div class="kpi-pv-main">
204 <div class="kpi-pv-name">$loop1.display_name</div>
205 <div class="kpi-pv-sub">$loop1.company_name &middot; signed in <span class="ts" data-ts="$loop1.issued_epoch" data-fmt="datetime">$loop1.issued_at</span></div>
206 </div>
207 <div class="kpi-pv-meta"><span class="pv-ok">&#9679; live</span></div>
208 </div>
209 [/loop]
210 [/if]
211 [if:!$has_pv_sessions]<div class="kpi-pv-empty">Nobody is signed in right now.</div>[/if]
212 <div class="kpi-pv-footer">Click to view users with live sessions &rarr;</div>
213</div>
214
215<div class="kpi-pv-src" data-pv-id="pv-wau">
216 <div class="kpi-pv-head"><span>Active in last 7 days</span><span class="pv-count">$k.wau WAU</span></div>
217 [if:$has_pv_wau]
218 [loop:@pv_wau]
219 <div class="kpi-pv-row">
220 <span class="kpi-pv-avatar" style="background:$loop1.avatar_color">$loop1.initials</span>
221 <div class="kpi-pv-main">
222 <div class="kpi-pv-name">$loop1.display_name</div>
223 <div class="kpi-pv-sub">$loop1.email &middot; $loop1.company_name</div>
224 </div>
225 <div class="kpi-pv-meta"><span class="ts" data-ts="$loop1.last_login_epoch" data-fmt="relative">$loop1.last_login_at</span></div>
226 </div>
227 [/loop]
228 [/if]
229 [if:!$has_pv_wau]<div class="kpi-pv-empty">No one active in the last 7 days.</div>[/if]
230 <div class="kpi-pv-footer">Click to see all 7-day-active users &rarr;</div>
231</div>
232
233<div class="kpi-pv-src" data-pv-id="pv-mau">
234 <div class="kpi-pv-head"><span>Active in last 30 days</span><span class="pv-count">$k.mau MAU</span></div>
235 [if:$has_pv_mau]
236 [loop:@pv_mau]
237 <div class="kpi-pv-row">
238 <span class="kpi-pv-avatar" style="background:$loop1.avatar_color">$loop1.initials</span>
239 <div class="kpi-pv-main">
240 <div class="kpi-pv-name">$loop1.display_name</div>
241 <div class="kpi-pv-sub">$loop1.email &middot; $loop1.company_name</div>
242 </div>
243 <div class="kpi-pv-meta"><span class="ts" data-ts="$loop1.last_login_epoch" data-fmt="relative">$loop1.last_login_at</span></div>
244 </div>
245 [/loop]
246 [/if]
247 [if:!$has_pv_mau]<div class="kpi-pv-empty">No one active in the last 30 days.</div>[/if]
248 <div class="kpi-pv-footer">Click to see all 30-day-active users &rarr;</div>
249</div>
250
251<div class="kpi-pv-src" data-pv-id="pv-newsignups">
252 <div class="kpi-pv-head"><span>New signups (last 30 days)</span><span class="pv-count">$k.new_companies_30d new</span></div>
253 [if:$has_pv_newsignups]
254 [loop:@pv_newsignups]
255 <div class="kpi-pv-row">
256 <span class="kpi-pv-dot" style="background:$loop1.brand_color"></span>
257 <div class="kpi-pv-main">
258 <div class="kpi-pv-name">$loop1.name</div>
259 <div class="kpi-pv-sub">$loop1.plan_tier &middot; $loop1.users_n users</div>
260 </div>
261 <div class="kpi-pv-meta"><span class="ts" data-ts="$loop1.created_epoch" data-fmt="datetime">$loop1.created_at</span></div>
262 </div>
263 [/loop]
264 [/if]
265 [if:!$has_pv_newsignups]<div class="kpi-pv-empty">No signups in the last 30 days.</div>[/if]
266 <div class="kpi-pv-footer">Click to see all recent signups &rarr;</div>
267</div>
268
269<div class="kpi-pv-src" data-pv-id="pv-projects">
270 <div class="kpi-pv-head"><span>Top companies by projects</span><span class="pv-count">$k.projects_n total</span></div>
271 [if:$has_pv_projects]
272 [loop:@pv_projects]
273 <div class="kpi-pv-row">
274 <span class="kpi-pv-dot" style="background:$loop1.brand_color"></span>
275 <div class="kpi-pv-main">
276 <div class="kpi-pv-name">$loop1.name</div>
277 <div class="kpi-pv-sub">$loop1.plan_tier</div>
278 </div>
279 <div class="kpi-pv-meta"><strong>$loop1.projects_n</strong></div>
280 </div>
281 [/loop]
282 [/if]
283 [if:!$has_pv_projects]<div class="kpi-pv-empty">No projects yet.</div>[/if]
284 <div class="kpi-pv-footer">Click to sort companies by projects &rarr;</div>
285</div>
286
287<div class="kpi-pv-src" data-pv-id="pv-tasks">
288 <div class="kpi-pv-head"><span>Top companies by tasks</span><span class="pv-count">$k.tasks_n total</span></div>
289 [if:$has_pv_tasks]
290 [loop:@pv_tasks]
291 <div class="kpi-pv-row">
292 <span class="kpi-pv-dot" style="background:$loop1.brand_color"></span>
293 <div class="kpi-pv-main">
294 <div class="kpi-pv-name">$loop1.name</div>
295 <div class="kpi-pv-sub">$loop1.plan_tier</div>
296 </div>
297 <div class="kpi-pv-meta"><strong>$loop1.tasks_n</strong></div>
298 </div>
299 [/loop]
300 [/if]
301 [if:!$has_pv_tasks]<div class="kpi-pv-empty">No tasks yet.</div>[/if]
302 <div class="kpi-pv-footer">Click to sort companies by tasks &rarr;</div>
303</div>
304
305<div class="kpi-pv-src" data-pv-id="pv-tasks-done">
306 <div class="kpi-pv-head"><span>Top companies by tasks completed</span><span class="pv-count">$k.tasks_done_n done</span></div>
307 [if:$has_pv_tasks_done]
308 [loop:@pv_tasks_done]
309 <div class="kpi-pv-row">
310 <span class="kpi-pv-dot" style="background:$loop1.brand_color"></span>
311 <div class="kpi-pv-main">
312 <div class="kpi-pv-name">$loop1.name</div>
313 <div class="kpi-pv-sub">$loop1.plan_tier</div>
314 </div>
315 <div class="kpi-pv-meta"><strong>$loop1.tasks_done_n</strong></div>
316 </div>
317 [/loop]
318 [/if]
319 [if:!$has_pv_tasks_done]<div class="kpi-pv-empty">No completed tasks yet.</div>[/if]
320 <div class="kpi-pv-footer">Click to sort companies by completed &rarr;</div>
321</div>
322
323<div class="kpi-pv-src" data-pv-id="pv-actions">
324 <div class="kpi-pv-head"><span>Latest platform actions</span><span class="pv-count">$k.actions_24h in 24h</span></div>
325 [if:$has_pv_actions]
326 [loop:@pv_actions]
327 <div class="kpi-pv-row">
328 <span class="kpi-pv-avatar" style="background:$loop1.avatar_color">$loop1.initials</span>
329 <div class="kpi-pv-main">
330 <div class="kpi-pv-name">$loop1.action</div>
331 <div class="kpi-pv-sub">$loop1.display_name &middot; $loop1.actor_type</div>
332 </div>
333 <div class="kpi-pv-meta"><span class="ts" data-ts="$loop1.occurred_epoch" data-fmt="relative">$loop1.occurred_at</span></div>
334 </div>
335 [/loop]
336 [/if]
337 [if:!$has_pv_actions]<div class="kpi-pv-empty">No actions in the last 24 hours.</div>[/if]
338 <div class="kpi-pv-footer">Click to view live visitor activity &rarr;</div>
339</div>
340
341<div class="row" style="gap:24px;flex-wrap:wrap">
342 <div class="col" style="min-width:300px;flex:1">
343 <div class="card">
344 <div class="card-header"><h3>Plan distribution</h3></div>
345 [if:$has_pb]
346 [loop:@plan_breakdown]
347 <div style="padding:8px 0;border-bottom:1px solid var(--col-border)">
348 <div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px"><span><span class="badge">$loop1.plan_tier</span></span><strong>$loop1.n</strong></div>
349 <div style="background:var(--col-bg-3);height:6px;border-radius:3px;overflow:hidden"><div style="background:var(--col-primary);height:100%;width:$loop1.pct_css"></div></div>
350 </div>
351 [/loop]
352 [/if]
353 </div>
354 </div>
355 <div class="col" style="min-width:300px;flex:2">
356 <div class="card">
357 <div class="card-header"><h3>Latest signups</h3><a href="/admin_companies.cgi" class="text-muted" style="font-size:13px">All companies</a></div>
358 [if:$has_rs]
359 <table class="table">
360 <thead><tr><th>Company</th><th>Plan</th><th>Users</th><th>Joined</th></tr></thead>
361 <tbody>
362 [loop:@recent_signups]
363 <tr class="clickable" onclick="location.href='/admin_users.cgi?company=$loop1.id'"><td><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:$loop1.brand_color;margin-right:6px"></span>$loop1.name</td><td><span class="badge">$loop1.plan_tier</span></td><td>$loop1.users_n</td><td style="font-size:12px;color:var(--col-text-dim)"><span class="ts" data-ts="$loop1.created_epoch" data-fmt="datetime">$loop1.created_at</span></td></tr>
364 [/loop]
365 </tbody>
366 </table>
367 [/if]
368 </div>
369 </div>
370</div>
371
372<script>
373(function(){
374 // Touch devices: no hover -> skip the overlay entirely. The click still
375 // works because the KPI tile is just a plain <a href>.
376 try { if (window.matchMedia && window.matchMedia('(hover: none)').matches) return; } catch(_) {}
377
378 var overlay = document.createElement('div');
379 overlay.className = 'kpi-pv';
380 document.body.appendChild(overlay);
381
382 var current = null, hideTimer = null;
383 var OFFSET_X = 20, OFFSET_Y = 22;
384
385 function positionAt(mx, my) {
386 // Measure AFTER content is in the DOM so we can flip if it would clip.
387 var rect = overlay.getBoundingClientRect();
388 var vw = window.innerWidth, vh = window.innerHeight;
389 var left = mx + OFFSET_X;
390 var top = my + OFFSET_Y;
391 if (left + rect.width > vw - 8) left = mx - rect.width - OFFSET_X;
392 if (top + rect.height > vh - 8) top = my - rect.height - OFFSET_Y;
393 if (left < 8) left = 8;
394 if (top < 8) top = 8;
395 overlay.style.left = left + 'px';
396 overlay.style.top = top + 'px';
397 }
398
399 function show(el, mx, my) {
400 var key = el.getAttribute('data-pv');
401 if (!key) return;
402 var src = document.querySelector('.kpi-pv-src[data-pv-id="' + key + '"]');
403 if (!src) return;
404 overlay.innerHTML = src.innerHTML;
405 overlay.classList.add('open');
406 // Position twice: once to put it somewhere sane, then a rAF later once
407 // the cloned content has actual layout dimensions.
408 positionAt(mx, my);
409 requestAnimationFrame(function(){ positionAt(mx, my); });
410 current = el;
411 }
412 function hide() {
413 overlay.classList.remove('open');
414 current = null;
415 }
416
417 document.addEventListener('mouseover', function(e){
418 var el = e.target.closest && e.target.closest('a.kpi[data-pv]');
419 if (!el) return;
420 if (el === current) return;
421 clearTimeout(hideTimer);
422 show(el, e.clientX, e.clientY);
423 });
424 document.addEventListener('mousemove', function(e){
425 if (!current) return;
426 var el = e.target.closest && e.target.closest('a.kpi[data-pv]');
427 if (!el || el !== current) return;
428 positionAt(e.clientX, e.clientY);
429 });
430 document.addEventListener('mouseout', function(e){
431 if (!current) return;
432 // Hide when the cursor leaves the KPI -- but not if we're crossing into
433 // a child element of the same tile.
434 var to = e.relatedTarget;
435 if (to && current.contains(to)) return;
436 clearTimeout(hideTimer);
437 hideTimer = setTimeout(hide, 60);
438 });
439 // Kill the overlay on scroll/click so it doesn't get orphaned mid-nav.
440 window.addEventListener('scroll', hide, true);
441 document.addEventListener('click', function(e){
442 var el = e.target.closest && e.target.closest('a.kpi[data-pv]');
443 if (el) hide();
444 });
445})();
446</script>