Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/TEMPLATES/tf_admin_metrics.html
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
to eba99f51bb13
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 · $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 →</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 · $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 →</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 · 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 →</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 · $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 →</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 · 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">● 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 →</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 · $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 →</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 · $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 →</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 · $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 →</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 →</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 →</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 →</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 · $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 →</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> |