Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_admin.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_admin.html

added on local at 2026-07-01 16:01:42

Added
+278
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to c121a45099fe
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Admin Console (company-staff only).
3 - Platform KPI tiles across the top
4 - User search + table; click any row to open /admin_user.cgi
5 The sidebar entry that links here is gated in Wrapper.pm; this
6 page additionally calls Admin->require_admin so a hand-typed URL
7 can't reach it either.
8===================================================================== -->
9<style>
10 .adm-search {
11 display: flex; gap: 8px; align-items: center;
12 margin: 22px 0 14px;
13 }
14 .adm-search input {
15 flex: 1;
16 background: var(--col-surface-1);
17 border: 1px solid var(--col-border);
18 color: var(--col-text);
19 padding: 10px 14px;
20 border-radius: 10px;
21 font-size: 14px;
22 font-family: inherit;
23 }
24 .adm-search input:focus {
25 outline: none; border-color: var(--col-accent);
26 }
27 .adm-search .summary {
28 font-size: 11px; color: var(--col-text-3);
29 letter-spacing: 1.5px; text-transform: uppercase;
30 margin-left: 8px;
31 }
32
33 .adm-utable {
34 width: 100%;
35 border-collapse: separate; border-spacing: 0;
36 background: var(--col-surface-1);
37 border: 1px solid var(--col-border);
38 border-radius: 12px;
39 overflow: hidden;
40 }
41 .adm-utable thead th {
42 background: var(--col-surface-2);
43 padding: 11px 14px;
44 text-align: left;
45 font-size: 10px;
46 letter-spacing: 1.5px;
47 text-transform: uppercase;
48 color: var(--col-text-3);
49 font-weight: 700;
50 border-bottom: 1px solid var(--col-border);
51 }
52 .adm-utable tbody td {
53 padding: 12px 14px;
54 border-bottom: 1px solid var(--col-border);
55 font-size: 13px; color: var(--col-text-2);
56 vertical-align: middle;
57 }
58 .adm-utable tbody tr:last-child td { border-bottom: none; }
59 .adm-utable tbody tr {
60 cursor: pointer; transition: background 0.12s;
61 }
62 .adm-utable tbody tr:hover td { background: rgba(124,58,237,0.08); }
63 .adm-utable a { color: inherit; text-decoration: none; }
64
65 .adm-uname { color: var(--col-text); font-weight: 700; }
66 .adm-uid { color: var(--col-text-3); font-size: 11px; font-family: var(--font-mono, monospace); }
67 .adm-pill {
68 font-size: 9px; font-weight: 700;
69 padding: 3px 8px; border-radius: 6px;
70 letter-spacing: 1px; text-transform: uppercase;
71 }
72 .adm-pill.plan-free { background: rgba(100,116,139,0.20); color: #94a3b8; }
73 .adm-pill.plan-starter { background: rgba(255,122,61,0.18); color: #ffb347; }
74 .adm-pill.plan-pro { background: rgba(124,58,237,0.18); color: #a78bfa; }
75 .adm-pill.plan-studio { background: rgba(245,158,11,0.18); color: #fbbf24; }
76 .adm-pill.is-admin { background: rgba(34,197,94,0.18); color: #4ade80; }
77 .adm-pill.is-suspended { background: rgba(245,158,11,0.20); color: #fbbf24; }
78 .adm-pill.is-closed { background: rgba(239,68,68,0.20); color: #f87171; }
79
80 .adm-empty {
81 text-align: center;
82 padding: 60px 24px;
83 color: var(--col-text-3);
84 }
85 .adm-empty .lg { font-size: 14px; color: var(--col-text-2); margin-bottom: 6px; }
86 .adm-empty .sm { font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; }
87</style>
88
89<div class="page-head">
90 <div>
91 <span class="page-eyebrow"><span class="dot"></span> Admin &middot; Company staff only</span>
92 <h1 class="page-title">Operations console</h1>
93 <p class="page-subtitle">Platform-wide view across every user. Search for a user to drill into their account.</p>
94 </div>
95 <div class="page-actions">
96 [if:$not_tutorial_mode]<a href="/admin.cgi?tutorial=1" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Sample Data View</a>[/if]
97 [if:$tutorial_mode]<a href="/admin.cgi" class="btn btn-secondary"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>Exit sample view</a>[/if]
98 </div>
99</div>
100
101[if:$tutorial_mode]
102<!-- Tutorial banner + intro: example-data walkthrough for staff being
103 onboarded into the admin console. Styles live in site.css. -->
104<div class="tutorial-banner">
105 <div class="tutorial-banner-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
106 <div class="tutorial-banner-body">
107 <div class="tutorial-banner-title">Tutorial &middot; Sample data only</div>
108 <div class="tutorial-banner-text">Every KPI tile, the search results, and the user table below are filled with example data. None of this reflects the real platform &mdash; it is here so staff being onboarded into the Admin Console can see how the page reads on a populated platform.</div>
109 </div>
110 <a href="/admin.cgi" class="btn btn-secondary btn-sm">Exit sample view</a>
111</div>
112
113<div class="tutorial-intro">
114 <div class="tutorial-intro-eyebrow">A walkthrough of the Operations Console</div>
115 <h2 class="tutorial-intro-title">This is what the admin console looks like on a populated platform.</h2>
116 <p class="tutorial-intro-text">The numbers belong to an imaginary deployment with <strong>1,247 users</strong>, <strong>1,089 active accounts</strong>, <strong>86 signups in the last 30 days</strong>, <strong>1,154 tracked sites</strong>, <strong>8,372 active experiments</strong>, and <strong>\$42,810 revenue</strong> over the past month. The user table beneath the KPI tiles shows a mix of plan tiers (Free / Starter / Pro / Enterprise), an admin account, and one suspended and one closed account so every row pill and status variant in the UI is visible at once.</p>
117 <p class="tutorial-intro-text">On the live page, the KPI tiles read directly from <code>users</code>, <code>sites</code>, <code>ab_tests</code>, and <code>invoices</code> &mdash; counts, status filters, and a 30-day revenue sum across paid subscription charges. The search box runs a free-text match against email, display name, site domain and numeric user id; clicking any row navigates to <code>/admin_user.cgi</code> for that account where staff can adjust plan tier, toggle admin, suspend, close, impersonate, and audit-log. Each KPI tile in the row above also carries a <strong>click-for-detail</strong> popup explaining its data source and how to interpret movement in that number.</p>
118 <p class="tutorial-intro-text tutorial-intro-foot">When you are ready to see real data, click <a href="/admin.cgi" class="text-brand fw-700">Exit sample view</a> to return to the live console.</p>
119</div>
120[/if]
121
122<!-- Platform KPI tiles -->
123<div class="stat-row">
124 <div class="stat" data-kpi-detail='{
125 "eyebrow":"Users . platform-wide total",
126 "description":"Every user account on ABForge regardless of status (active, suspended, closed, pending). Active is the slice you can actually transact with; the rest is here for support and admin context.",
127 "methodology":"<code>SELECT COUNT(*) FROM users</code> -- no filter. Active count: <code>WHERE account_status=&#39;active&#39;</code>. New-in-30d: <code>WHERE created_at &gt;= DATE_SUB(NOW(), INTERVAL 30 DAY)</code>. Suspended and closed are filtered separately in the search below.",
128 "kpis":[
129 {"label":"All users","value":"$u_total","sub":"every status"},
130 {"label":"Active","value":"$u_active","sub":"can sign in &amp; transact","color":"#4ade80"},
131 {"label":"New (30d)","value":"$u_new_30d","sub":"signups this month"}
132 ],
133 "interpretation":"User count grows monotonically -- it never shrinks unless we hard-delete. Active is the meaningful number for capacity planning. The new-in-30d rate is your growth pulse; healthy SaaS sees this number stay roughly constant or accelerate. A sudden drop usually means signup is broken (test the /signup.cgi flow) or a marketing channel went dark.",
134 "drilldowns":[
135 {"label":"Search all users","href":"/admin.cgi"},
136 {"label":"Recent signups","href":"/admin.cgi?sort=newest"}
137 ]
138 }'>
139 <div class="stat-label">Users</div>
140 <div class="stat-value">$u_total</div>
141 <div class="stat-meta"><span class="stat-meta-label">$u_active active &middot; $u_new_30d new (30d)</span></div>
142 </div>
143 <div class="stat" data-kpi-detail='{
144 "eyebrow":"Sites . customer sites tracked on the platform",
145 "description":"Every site created across every user account. One user can have multiple sites (we permit that even though the UI currently exposes one per user). Subdomain reservations show up here too.",
146 "methodology":"<code>SELECT COUNT(*) FROM sites</code>. Includes draft and live sites. Suspended user sites are still counted -- they exist in the DB until the account is hard-deleted.",
147 "kpis":[
148 {"label":"Sites","value":"$sf_total","sub":"created across all users"},
149 {"label":"Users","value":"$u_total","sub":"for reference"}
150 ],
151 "interpretation":"Site count should track user count closely (most users have exactly one). If the gap widens, somebody is building multiple test stores -- check if a single power user is responsible. A sharp drop in this number is suspicious; sites should not disappear unless an account closed."
152 }'>
153 <div class="stat-label">Sites</div>
154 <div class="stat-value">$sf_total</div>
155 <div class="stat-meta"><span class="stat-meta-label">created across all users</span></div>
156 </div>
157 <div class="stat" data-kpi-detail='{
158 "eyebrow":"Experiments active . live A/B + MVT tests",
159 "description":"Total experiments across every site that are NOT archived. Includes draft, running, and paused experiments. This is the closest single proxy for platform engagement -- accounts that run tests stick around.",
160 "methodology":"<code>SELECT COUNT(*) FROM ab_tests WHERE archived_at IS NULL</code>. Excludes archived rows. Per-user breakdown is available in the search-results table at the bottom of this page (sort by site).",
161 "kpis":[
162 {"label":"In flight","value":"$m_total","sub":"all non-archived experiments"},
163 {"label":"Per active user","value":"average","sub":"divides by $u_active"}
164 ],
165 "interpretation":"Healthy growth pattern: 1-3 new experiments per active user per month. Far below that and users are not engaging; far above and you may have a few power users skewing the number -- look at the per-user distribution in the search table to tell which.",
166 "drilldowns":[
167 {"label":"Search users","href":"/admin.cgi"}
168 ]
169 }'>
170 <div class="stat-label">Experiments active</div>
171 <div class="stat-value">$m_total</div>
172 <div class="stat-meta"><span class="stat-meta-label">running across all sites</span></div>
173 </div>
174 <div class="stat" data-kpi-detail='{
175 "eyebrow":"Revenue (30d) . platform-wide MRR proxy",
176 "description":"Sum of every paid subscription charge across every customer in the last 30 days. This is the headline platform-revenue number that drives every other operational decision.",
177 "methodology":"<code>SUM(invoices.total_cents) WHERE status=&#39;paid&#39; AND created_at &gt;= NOW() - INTERVAL 30 DAY</code>, across all customers.",
178 "kpis":[
179 {"label":"Revenue (30d)","value":"$rev_30d","sub":"paid invoices","color":"#4ade80"},
180 {"label":"Approx MRR","value":"30d run-rate","sub":"sliding 30-day window"}
181 ],
182 "interpretation":"Trending up = users are upgrading and renewing. Trending down for two consecutive 30-day windows is a real signal -- check if it is concentrated in a few accounts (a churned power-user) or spread across many (broad demand drop).",
183 "drilldowns":[
184 {"label":"Stripe payments","href":"https://dashboard.stripe.com/payments","target":"_blank"}
185 ]
186 }'>
187 <div class="stat-label">Revenue (30d)</div>
188 <div class="stat-value">$rev_30d</div>
189 <div class="stat-meta"><span class="stat-meta-label">paid invoices, all customers</span></div>
190 </div>
191</div>
192
193<!-- Search -->
194<form method="GET" action="/admin.cgi" class="adm-search">
195 <input type="text" name="q" value="$q" placeholder="Search by email, name, site, or user id..." autofocus>
196 <button class="btn btn-primary btn-sm" type="submit">Search</button>
197 [if:$has_query]<a class="btn btn-secondary btn-sm" href="/admin.cgi">Clear</a>[/if]
198 <span class="summary">$result_count results</span>
199</form>
200
201<!-- Results -->
202[if:$has_results]
203<style>
204 .au-table-wrap { overflow-x: auto; }
205 table.au-table { width: 100%; border-collapse: separate; border-spacing: 0; }
206 table.au-table th { text-align: left; font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--col-text-dim, #94a3b8); padding: 10px 12px; font-weight: 700; border-bottom: 1px solid var(--col-border, #1e293b); }
207 table.au-table td { padding: 12px; border-bottom: 1px solid var(--col-border, #1e293b); color: var(--col-text, #e2e8f0); font-size: 13px; vertical-align: middle; white-space: nowrap; }
208 table.au-table tr:last-child td { border-bottom: none; }
209 table.au-table tr.au-clickable { cursor: pointer; transition: background 0.12s; }
210 table.au-table tr.au-clickable:hover { background: rgba(255,255,255,0.04); }
211 .au-av { width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 12px; flex-shrink: 0; }
212 .au-name-cell { display: flex; align-items: center; gap: 10px; }
213 .au-pill { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 600; background: var(--col-bg-3, rgba(255,255,255,0.06)); color: var(--col-text-dim, #94a3b8); border: 1px solid var(--col-border-2, rgba(255,255,255,0.10)); }
214 .au-pill.super { background: rgba(239,68,68,0.18); color: #f87171; border-color: rgba(239,68,68,0.35); }
215 .au-imp-btn { background: #fbbf24; color: #1a1303; border: 1px solid #fbbf24; padding: 6px 12px; border-radius: 6px; font-weight: 700; font-size: 12px; cursor: pointer; }
216 .au-imp-btn:hover { background: #f59e0b; border-color: #f59e0b; }
217</style>
218<div class="au-table-wrap">
219<table class="au-table">
220 <thead>
221 <tr>
222 <th>Member</th>
223 <th>Email</th>
224 <th>Storefront</th>
225 <th>Role</th>
226 <th>Status</th>
227 <th>Last login</th>
228 <th></th>
229 </tr>
230 </thead>
231 <tbody>
232 [loop:@users]
233 <tr class="au-clickable" data-href="/admin_user.cgi?u=$loop1.id">
234 <td><div class="au-name-cell"><div class="au-av" style="background:$loop1.avatar_color">$loop1.initials</div><div>$loop1.display_name [if:$loop1.is_admin_flag]<span class="au-pill super">ADMIN</span>[/if]</div></div></td>
235 <td>$loop1.email</td>
236 <td>$loop1.storefront_name</td>
237 <td><span class="au-pill">$loop1.plan_tier</span></td>
238 <td><span class="au-pill">$loop1.account_status</span></td>
239 <td style="font-size:12px;color:var(--col-text-dim, #94a3b8)">$loop1.last_login_at</td>
240 <td>
241 [if:!$loop1.is_self]
242 <form method="POST" action="/admin_action.cgi" style="display:inline;margin:0" onclick="event.stopPropagation()">
243 <input type="hidden" name="act" value="impersonate">
244 <input type="hidden" name="u" value="$loop1.id">
245 <button type="submit" class="au-imp-btn">Impersonate</button>
246 </form>
247 [/if]
248 </td>
249 </tr>
250 [/loop]
251 </tbody>
252</table>
253</div>
254<script>
255 document.querySelectorAll('table.au-table tr.au-clickable').forEach(function(tr){
256 tr.addEventListener('click', function(e){
257 if (e.target.closest('a,button,form,input')) return;
258 window.location.href = tr.dataset.href;
259 });
260 });
261</script>
262
263[/if]
264
265[if:!$has_results]
266<div class="adm-empty">
267 [if:$has_query]
268 <div class="lg">No users match "$q".</div>
269 <div class="sm">Try part of an email, display name, site name, or numeric user id.</div>
270 [/if]
271 [if:!$has_query]
272 <div class="lg">No users yet.</div>
273 <div class="sm">Users will appear here as they sign up.</div>
274 [/if]
275</div>
276[/if]
277
278<div class="adm-foot-spacer" data-end="admin-console">&nbsp;</div>