auth-yes/replace_invites.py
google-labs-jules[bot] 4c7d8d4af7 feat(ui): decompose admin pages into reusable AdminTable and AdminModal components
Refactored AdminInvitesPage, AdminUserDetailsPage, AdminRolesPage, and AdminAppsPage to use the new pure Hono SSR JSX stateless components.
Fixed missing import definitions in AdminRolesPage.
Moved task file to complete state.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-26 00:34:29 +00:00

247 lines
9.0 KiB
Python

import re
with open('ui/components/AdminInvitesPage.tsx', 'r') as f:
content = f.read()
start_marker = "{/* Desktop Ledger Table (≥ 768px) */}"
end_marker = " <style>"
start_idx = content.find(start_marker)
end_idx = content.find(end_marker)
if start_idx != -1 and end_idx != -1:
new_content = content[:start_idx] + """<AdminTable
id="invitesTable"
headers={[
"Invite Code",
"Target App / Scope",
"Role",
"Capacity & Usage",
"Status",
"Expires",
"Actions",
]}
isEmpty={invites.length === 0}
emptyState="No active or historical invite tokens found."
desktopRows={invites.map((inv) => {
const usesCount = inv.uses_count || 0;
const maxUses = inv.max_uses;
const isUnlimited = maxUses === null;
const isExhausted = !isUnlimited && usesCount >= maxUses;
const isExpired = new Date(inv.expires_at) < new Date();
const isActive = !isExhausted && !isExpired;
return (
<tr
key={inv.id}
class="invite-row"
data-search={`${inv.code} ${
inv.app_name || ""
} ${inv.role}`.toLowerCase()}
>
<td>
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-weight: 700; font-family: monospace; color: var(--primary);">
{inv.code}
</code>
</td>
<td style="font-size: 0.85rem;">
{inv.app_name || (
<span style="color: var(--text-muted); font-style: italic;">
Global / Open
</span>
)}
</td>
<td style="font-family: monospace; font-size: 0.85rem; color: var(--text-secondary);">
{inv.role}
</td>
<td style="font-size: 0.85rem;">
{isUnlimited
? "Unlimited"
: `${usesCount} / ${maxUses}`}
</td>
<td>
{isExhausted && (
<span class="badge badge-secondary">Exhausted</span>
)}
{isExpired && !isExhausted && (
<span class="badge badge-danger">Expired</span>
)}
{isActive && (
<span class="badge badge-success">Active</span>
)}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(inv.expires_at).toLocaleDateString()}
</td>
<td>
<div style="display: flex; gap: 0.35rem; flex-wrap: wrap;">
{isActive && (
<button
type="button"
class="btn-primary"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`copyInviteLink('${inv.code}')`}
>
Copy Link
</button>
)}
{usesCount > 0 && (
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`showRedemptionsModal('${inv.id}', '${inv.code}')`}
>
Claimed ({usesCount})
</button>
)}
{isActive && (
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`revokeInvite('${inv.id}', '${inv.code}')`}
>
Revoke
</button>
)}
</div>
</td>
</tr>
);
})}
mobileCards={invites.map((inv) => {
const usesCount = inv.uses_count || 0;
const maxUses = inv.max_uses;
const isUnlimited = maxUses === null;
const isExhausted = !isUnlimited && usesCount >= maxUses;
const isExpired = new Date(inv.expires_at) < new Date();
const isActive = !isExhausted && !isExpired;
return (
<div
class="card invite-card"
key={inv.id}
data-search={`${inv.code} ${inv.app_name || ""} ${inv.role}`
.toLowerCase()}
style="margin-bottom: 0; padding: 1rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<div>
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-weight: 700; font-family: monospace; font-size: 1rem; color: var(--primary);">
{inv.code}
</code>
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 0.25rem;">
{inv.app_name || "Global / Open"} • Role:{" "}
<strong>{inv.role}</strong>
</div>
</div>
{isExhausted && (
<span class="badge badge-secondary">Exhausted</span>
)}
{isExpired && !isExhausted && (
<span class="badge badge-danger">Expired</span>
)}
{isActive && <span class="badge badge-success">Active</span>}
</div>
<div style="margin-bottom: 0.75rem;">
<div style="font-size: 0.8rem; color: var(--text-secondary); display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
<span>
Capacity:{" "}
{isUnlimited ? "Unlimited" : `${usesCount} / ${maxUses}`}
</span>
<span>
Expires: {new Date(inv.expires_at).toLocaleDateString()}
</span>
</div>
{!isUnlimited && (
<div style="background: var(--surface-muted); border-radius: 3px; height: 6px; width: 100%; overflow: hidden;">
<div
style={`background: ${
isExhausted ? "var(--text-muted)" : "var(--success)"
}; height: 100%; width: ${
Math.min(100, (usesCount / maxUses) * 100)
}%;`}
/>
</div>
)}
</div>
<div style="display: flex; gap: 0.5rem;">
{isActive && (
<button
type="button"
class="btn-primary"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
onclick={`copyInviteLink('${inv.code}')`}
>
Copy Link
</button>
)}
{usesCount > 0 && (
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
onclick={`showRedemptionsModal('${inv.id}', '${inv.code}')`}
>
Claimed ({usesCount})
</button>
)}
{isActive && (
<button
type="button"
class="btn-danger"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
onclick={`revokeInvite('${inv.id}', '${inv.code}')`}
>
Revoke
</button>
)}
</div>
</div>
);
})}
/>
{/* Redemptions Modal */}
<AdminModal
id="redemptions-modal"
title={
<span>
Users Claimed:{" "}
<code id="modal-invite-code" style="color: var(--primary);">
</code>
</span>
}
onClose="closeRedemptionsModal()"
>
<div
id="modal-redemptions-content"
style="max-height: 350px; overflow-y: auto;"
>
<p style="color: var(--text-muted); font-size: 0.9rem;">
Loading claimed users...
</p>
</div>
<div style="text-align: right; margin-top: 1rem;">
<button
type="button"
class="btn-outline"
onclick="closeRedemptionsModal()"
>
Close
</button>
</div>
</AdminModal>
""" + content[end_idx:]
with open('ui/components/AdminInvitesPage.tsx', 'w') as f:
f.write(new_content)
print("Replaced!")
else:
print("Markers not found")