chore: clean up temporary python and text scratch files from repo root

This commit is contained in:
Tyler Gillispie 2026-08-25 17:37:04 -07:00
parent 4c7d8d4af7
commit 54c98c1e4a
9 changed files with 0 additions and 2260 deletions

View File

@ -1,352 +0,0 @@
{/* Desktop Ledger Table (≥ 768px) */}
<div class="card desktop-only" style="display: none;">
<div class="table-container">
<table id="invitesTable">
<thead>
<tr>
<th>Invite Code</th>
<th>Target App / Scope</th>
<th>Role</th>
<th>Capacity & Usage</th>
<th>Status</th>
<th>Expires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{invites.length === 0
? (
<tr>
<td
colSpan={7}
style="text-align: center; color: var(--text-muted); padding: 2rem;"
>
No active or historical invite tokens found.
</td>
</tr>
)
: (
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>
{inv.app_name
? (
<strong style="color: var(--text-primary);">
{inv.app_name}
</strong>
)
: inv.role === "admin"
? <span class="badge badge-info">Global Admin</span>
: (
<span class="badge badge-secondary">
General (Open)
</span>
)}
</td>
<td>
<span class="badge badge-info">{inv.role}</span>
</td>
<td>
<div style="min-width: 110px;">
{isUnlimited
? (
<span style="font-size: 0.85rem; font-weight: 600; color: var(--primary);">
{usesCount} claimed (Unlimited)
</span>
)
: (
<div>
<span style="font-size: 0.85rem; font-weight: 600; color: var(--text-primary);">
{usesCount} / {maxUses} used
</span>
<div style="background: var(--surface-muted); border-radius: 3px; height: 6px; width: 100%; margin-top: 4px; overflow: hidden;">
<div
style={`background: ${
isExhausted
? "var(--text-muted)"
: "var(--success)"
}; height: 100%; width: ${
Math.min(
100,
(usesCount / maxUses) * 100,
)
}%;`}
/>
</div>
</div>
)}
</div>
</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>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Mobile Adaptive Cards View (< 768px) */}
<div
id="invitesMobileDeck"
class="mobile-only"
style="display: flex; flex-direction: column; gap: 0.75rem;"
>
{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>
);
})}
</div>
{/* Redemptions Modal */}
<div
id="redemptions-modal"
style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); z-index: 9999; justify-content: center; align-items: center;"
>
<div style="background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); width: 90%; max-width: 550px; padding: 1.5rem; box-shadow: var(--shadow-lg);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="margin: 0; font-size: 1.1rem; color: var(--text-primary);">
Users Claimed:{" "}
<code id="modal-invite-code" style="color: var(--primary);">
</code>
</h3>
<button
type="button"
onclick="closeRedemptionsModal()"
style="background: none; border: none; font-size: 1.2rem; cursor: pointer; color: var(--text-muted);"
>
&times;
</button>
</div>
<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>
</div>
</div>
<style>
{`
@media (min-width: 768px) {
.desktop-only { display: block !important; }
.mobile-only { display: none !important; }
}
@media (max-width: 767px) {
.desktop-only { display: none !important; }
.mobile-only { display: flex !important; }
}
`}
</style>
<script
dangerouslySetInnerHTML={{
__html: `
const ROLES_CATALOG = ${JSON.stringify(allRoles)};
function updateInviteRoleOptions() {
const appSelect = document.getElementById('inviteAppId');
const roleSelect = document.getElementById('inviteRole');
if (!appSelect || !roleSelect) return;
const appId = appSelect.value;
roleSelect.innerHTML = '';
const available = ROLES_CATALOG.filter(r => !r.app_id || r.app_id === appId);
if (available.length === 0) {
const opt = document.createElement('option');
opt.value = 'user';
opt.textContent = 'user';
roleSelect.appendChild(opt);
return;
}
available.forEach(r => {
const opt = document.createElement('option');
opt.value = r.name;
opt.textContent = r.name + (r.app_id ? ' (App Custom)' : ' (Global)');
roleSelect.appendChild(opt);
});
}
if (document.getElementById('inviteAppId')) {
updateInviteRoleOptions();
}
function showNotice(msg, isError) {
const banner = document.getElementById('status-banner');
banner.textContent = msg;
banner.style.display = 'block';
banner.style.background = isError ? 'var(--danger-bg)' : 'var(--success-bg)';

View File

@ -1,132 +0,0 @@
onclick="closeRoleDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
</AdminModal>
<div class="card">
{/* Instant Search and Scope Filters */}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; gap: 0.75rem;">
<div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; flex: 1;">
{/* Search */}
<div style="position: relative; min-width: 200px; max-width: 300px; width: 100%;">
<input
type="text"
id="roleSearchInput"
placeholder="Search roles..."
oninput="filterRoles()"
style="width: 100%; padding: 0.45rem 0.85rem 0.45rem 2.1rem; font-size: 0.85rem;"
/>
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
style="position: absolute; left: 0.7rem; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none;"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
{/* Scope dropdown */}
<div style="display: flex; gap: 0.4rem; align-items: center;">
<label style="font-weight: 600; font-size: 0.85rem; color: var(--text-secondary); white-space: nowrap;">
Scope:
</label>
<select
id="filterScopeSelect"
onchange="filterRoles()"
style="padding: 0.4rem 0.75rem; font-size: 0.85rem;"
>
<option value="all">All Roles</option>
<option value="global">Global (Shared) Only</option>
{apps.map((app) => (
<option value={app.id}>
{app.name} Only
</option>
))}
</select>
</div>
</div>
<span
id="roleCountDisplay"
style="font-size: 0.85rem; color: var(--text-muted);"
>
Showing {roles.length} roles
</span>
</div>
{/* Desktop Table View (≥ 768px) */}
<div class="table-container desktop-only" style="display: none;">
<table id="rolesTable">
<thead>
<tr>
<th>Role Identifier</th>
<th>Scope</th>
<th>Description</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{roles.length === 0
? (
<tr>
<td
colSpan={5}
style="text-align: center; color: var(--text-muted); padding: 2rem;"
>
No roles found.
</td>
</tr>
)
: (
roles.map((r) => {
const isGlobal = !r.app_id;
const isCoreAdmin = isGlobal && r.name === "admin";
return (
<tr
key={r.id}
class="role-row"
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
>
<td>
<strong style="font-family: monospace; font-size: 0.95rem; color: var(--text-primary);">
{r.name}
</strong>
</td>
<td>
{isGlobal
? (
<span class="badge badge-info">
Global (Shared)
</span>
)
: (
<span class="badge badge-warning">
{r.app_name || "App-Specific"}
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
{r.description || "-"}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(r.created_at).toLocaleDateString()}
</td>
<td>
{!isCoreAdmin
? (
<div style="display: flex; gap: 0.35rem;">
<button

View File

@ -1,8 +0,0 @@
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
content = f.read()
import_str = """import { AdminTable } from "./admin/AdminTable.tsx";"""
content = content.replace(import_str, """import { AdminTable as _AdminTable } from "./admin/AdminTable.tsx";""")
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
f.write(content)

View File

@ -1,246 +0,0 @@
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")

View File

@ -1,568 +0,0 @@
import re
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
content = f.read()
import_str = """import { AdminLayout } from "./AdminLayout.tsx";"""
content = content.replace(import_str, """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";\nimport { AdminModal } from "./admin/AdminModal.tsx";""")
role_form = """ <div
id="roleFormCard"
class="card"
style="display: none; border-left: 4px solid var(--primary); margin-bottom: 1.5rem;"
>
<h3
id="roleFormTitle"
style="margin: 0 0 0.5rem 0; color: var(--text-primary);"
>
Create New Role
</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
Define a global shared role or an application-scoped custom grant.
</p>
<form id="roleForm" onsubmit="handleSaveRole(event)">
<input type="hidden" id="editRoleId" value="" />
<div
id="scopeSelectContainer"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
>
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Scope (Applicability) *
</label>
<select
id="roleScope"
onchange="handleScopeChange()"
style="width: 100%;"
>
<option value="global">
Global (Shared across ALL applications)
</option>
<option value="app_specific">
Application-Specific (Scoped to single app)
</option>
</select>
</div>
<div id="appSelectContainer" style="display: none;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Target Application *
</label>
<select
id="roleAppId"
style="width: 100%;"
>
{apps.map((app) => (
<option value={app.id}>
{app.name} ({app.spiffe_id})
</option>
))}
</select>
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Role Name (Identifier) *
</label>
<input
type="text"
id="roleName"
placeholder="e.g. read_only, viewer, admin"
required
style="width: 100%; font-family: monospace; font-size: 0.9rem;"
/>
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Description
</label>
<input
type="text"
id="roleDescription"
placeholder="e.g. Read-only access to basic features"
style="width: 100%;"
/>
</div>
<div style="display: flex; gap: 0.75rem;">
<button type="submit" class="btn-primary" style="min-height: 42px;">
Save Role
</button>
<button
type="button"
class="btn-outline"
onclick="closeRoleForm()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
</div>"""
role_form_replacement = """ <AdminModal
id="roleFormModal"
title="Create New Role"
onClose="closeRoleForm()"
>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
Define a global shared role or an application-scoped custom grant.
</p>
<form id="roleForm" onsubmit="handleSaveRole(event)">
<input type="hidden" id="editRoleId" value="" />
<div
id="scopeSelectContainer"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
>
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Scope (Applicability) *
</label>
<select
id="roleScope"
onchange="handleScopeChange()"
style="width: 100%;"
>
<option value="global">
Global (Shared across ALL applications)
</option>
<option value="app_specific">
Application-Specific (Scoped to single app)
</option>
</select>
</div>
<div id="appSelectContainer" style="display: none;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Target Application *
</label>
<select
id="roleAppId"
style="width: 100%;"
>
{apps.map((app) => (
<option value={app.id}>
{app.name} ({app.spiffe_id})
</option>
))}
</select>
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Role Name (Identifier) *
</label>
<input
type="text"
id="roleName"
placeholder="e.g. read_only, viewer, admin"
required
style="width: 100%; font-family: monospace; font-size: 0.9rem;"
/>
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Description
</label>
<input
type="text"
id="roleDescription"
placeholder="e.g. Read-only access to basic features"
style="width: 100%;"
/>
</div>
<div style="display: flex; gap: 0.75rem;">
<button type="submit" class="btn-primary" style="min-height: 42px;">
Save Role
</button>
<button
type="button"
class="btn-outline"
onclick="closeRoleForm()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
</AdminModal>"""
content = content.replace(role_form, role_form_replacement)
roles_table_desktop_mobile = """ <div class="table-container desktop-only" style="display: none;">
<table id="rolesTable">
<thead>
<tr>
<th>Role Identifier</th>
<th>Scope</th>
<th>Description</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{roles.length === 0
? (
<tr>
<td
colSpan={5}
style="text-align: center; color: var(--text-muted); padding: 2rem;"
>
No roles found.
</td>
</tr>
)
: (
roles.map((r) => {
const isGlobal = !r.app_id;
return (
<tr
key={r.id}
class="role-row"
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
>
<td>
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.9rem; font-family: monospace; font-weight: 700; color: var(--primary);">
{r.name}
</code>
</td>
<td>
{isGlobal
? <span class="badge badge-info">Global</span>
: (
<span class="badge badge-warning">
{r.app_name || "App Scoped"}
</span>
)}
</td>
<td style="color: var(--text-secondary); max-width: 250px; font-size: 0.85rem;">
{r.description || "-"}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(r.created_at).toLocaleDateString()}
</td>
<td>
<div style="display: flex; gap: 0.35rem;">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`openEditRoleForm(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit
</button>
{r.name !== "admin" && (
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
)}
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Mobile View (< 768px) */}
<div
id="rolesMobileDeck"
class="mobile-only"
style="display: flex; flex-direction: column; gap: 0.75rem;"
>
{roles.length === 0
? (
<div class="card" style="text-align: center; padding: 2rem;">
<p style="color: var(--text-muted); margin: 0;">
No roles found.
</p>
</div>
)
: (
roles.map((r) => {
const isGlobal = !r.app_id;
return (
<div
class="card role-card"
key={r.id}
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
style="margin-bottom: 0; padding: 1rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<strong style="font-family: monospace; font-size: 1rem; color: var(--text-primary);">
{r.name}
</strong>
{isGlobal
? <span class="badge badge-info">Global</span>
: (
<span class="badge badge-warning">
{r.app_name || "App Scoped"}
</span>
)}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem;">
<p style="margin: 0 0 0.25rem 0;">
{r.description || "No description provided."}
</p>
<small style="color: var(--text-muted);">
Created: {new Date(r.created_at).toLocaleDateString()}
</small>
</div>
<div style="display: flex; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`openEditRoleForm(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit Role
</button>
{r.name !== "admin" && (
<button
type="button"
class="btn-danger"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
)}
</div>
</div>
);
})
)}
</div>"""
roles_table_replacement = """ <AdminTable
id="rolesTable"
headers={[
"Role Identifier",
"Scope",
"Description",
"Created",
"Actions",
]}
isEmpty={roles.length === 0}
emptyState="No roles found."
desktopRows={roles.map((r) => {
const isGlobal = !r.app_id;
return (
<tr
key={r.id}
class="role-row"
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
>
<td>
<code style="background: var(--surface-muted); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.9rem; font-family: monospace; font-weight: 700; color: var(--primary);">
{r.name}
</code>
</td>
<td>
{isGlobal
? <span class="badge badge-info">Global</span>
: (
<span class="badge badge-warning">
{r.app_name || "App Scoped"}
</span>
)}
</td>
<td style="color: var(--text-secondary); max-width: 250px; font-size: 0.85rem;">
{r.description || "-"}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(r.created_at).toLocaleDateString()}
</td>
<td>
<div style="display: flex; gap: 0.35rem;">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`openEditRoleForm(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit
</button>
{r.name !== "admin" && (
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
)}
</div>
</td>
</tr>
);
})}
mobileCards={roles.map((r) => {
const isGlobal = !r.app_id;
return (
<div
class="card role-card"
key={r.id}
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
style="margin-bottom: 0; padding: 1rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<strong style="font-family: monospace; font-size: 1rem; color: var(--text-primary);">
{r.name}
</strong>
{isGlobal
? <span class="badge badge-info">Global</span>
: (
<span class="badge badge-warning">
{r.app_name || "App Scoped"}
</span>
)}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem;">
<p style="margin: 0 0 0.25rem 0;">
{r.description || "No description provided."}
</p>
<small style="color: var(--text-muted);">
Created: {new Date(r.created_at).toLocaleDateString()}
</small>
</div>
<div style="display: flex; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`openEditRoleForm(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit Role
</button>
{r.name !== "admin" && (
<button
type="button"
class="btn-danger"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
)}
</div>
</div>
);
})}
/>"""
content = content.replace(roles_table_desktop_mobile, roles_table_replacement)
js_repl1 = """ function openCreateRoleForm() {
document.getElementById('editRoleId').value = '';
document.getElementById('roleFormTitle').textContent = 'Create New Role';
document.getElementById('roleScope').value = 'global';
document.getElementById('roleScope').disabled = false;
document.getElementById('roleAppId').value = document.getElementById('roleAppId').options[0]?.value || '';
document.getElementById('roleName').value = '';
document.getElementById('roleName').readOnly = false;
document.getElementById('roleDescription').value = '';
handleScopeChange();
document.getElementById('roleFormCard').style.display = 'block';
document.getElementById('roleFormCard').scrollIntoView({ behavior: 'smooth' });
}"""
js_repl1_new = """ function openCreateRoleForm() {
document.getElementById('editRoleId').value = '';
document.querySelector('#roleFormModal h3').textContent = 'Create New Role';
document.getElementById('roleScope').value = 'global';
document.getElementById('roleScope').disabled = false;
document.getElementById('roleAppId').value = document.getElementById('roleAppId').options[0]?.value || '';
document.getElementById('roleName').value = '';
document.getElementById('roleName').readOnly = false;
document.getElementById('roleDescription').value = '';
handleScopeChange();
document.getElementById('roleFormModal').style.display = 'flex';
}"""
content = content.replace(js_repl1, js_repl1_new)
js_repl2 = """ function openEditRoleForm(roleJson) {
const r = JSON.parse(roleJson);
document.getElementById('editRoleId').value = r.id;
document.getElementById('roleFormTitle').textContent = 'Edit Role: ' + r.name;
document.getElementById('roleScope').value = r.app_id ? 'app_specific' : 'global';
document.getElementById('roleScope').disabled = true; // Cannot change scope after creation
document.getElementById('roleAppId').value = r.app_id || document.getElementById('roleAppId').options[0]?.value || '';
document.getElementById('roleName').value = r.name;
document.getElementById('roleName').readOnly = (r.name === 'admin'); // cannot rename admin
document.getElementById('roleDescription').value = r.description || '';
handleScopeChange();
document.getElementById('roleFormCard').style.display = 'block';
document.getElementById('roleFormCard').scrollIntoView({ behavior: 'smooth' });
}"""
js_repl2_new = """ function openEditRoleForm(roleJson) {
const r = JSON.parse(roleJson);
document.getElementById('editRoleId').value = r.id;
document.querySelector('#roleFormModal h3').textContent = 'Edit Role: ' + r.name;
document.getElementById('roleScope').value = r.app_id ? 'app_specific' : 'global';
document.getElementById('roleScope').disabled = true; // Cannot change scope after creation
document.getElementById('roleAppId').value = r.app_id || document.getElementById('roleAppId').options[0]?.value || '';
document.getElementById('roleName').value = r.name;
document.getElementById('roleName').readOnly = (r.name === 'admin'); // cannot rename admin
document.getElementById('roleDescription').value = r.description || '';
handleScopeChange();
document.getElementById('roleFormModal').style.display = 'flex';
}"""
content = content.replace(js_repl2, js_repl2_new)
js_repl3 = """ function closeRoleForm() {
document.getElementById('roleFormCard').style.display = 'none';
}"""
js_repl3_new = """ function closeRoleForm() {
document.getElementById('roleFormModal').style.display = 'none';
}"""
content = content.replace(js_repl3, js_repl3_new)
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
f.write(content)
print("Replaced Roles table!")

View File

@ -1,10 +0,0 @@
import re
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
content = f.read()
import_str = """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";\nimport { AdminModal } from "./admin/AdminModal.tsx";"""
content = content.replace(import_str, """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";\nimport { AdminModal } from "./admin/AdminModal.tsx";\n\n// We need to use these imports, they are falsely flagged by lint because they are only used in JSX.\n// They are used, but we'll import them anyway to appease the linter if Deno 2 has a bug.\n// In Hono JSX, imports might not be strictly recognized.""")
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
f.write(content)

View File

@ -1,204 +0,0 @@
import re
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
content = f.read()
role_form = """ <div
id="roleFormCard"
class="card"
style="display: none; border-left: 4px solid var(--primary); margin-bottom: 1.5rem;"
>
<h3
id="roleFormTitle"
style="margin: 0 0 0.5rem 0; color: var(--text-primary);"
>
Create New Role
</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
Define a global shared role or an application-scoped custom grant.
</p>
<form id="roleForm" onsubmit="handleSaveRole(event)">
<input type="hidden" id="editRoleId" value="" />
<div
id="scopeSelectContainer"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
>
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Scope (Applicability) *
</label>
<select
id="roleScope"
onchange="handleScopeChange()"
style="width: 100%;"
>
<option value="global">
Global (Shared across ALL applications)
</option>
<option value="app_specific">
Application-Specific (Scoped to single app)
</option>
</select>
</div>
<div id="appSelectContainer" style="display: none;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Target Application *
</label>
<select
id="roleAppId"
style="width: 100%;"
>
{apps.map((app) => (
<option value={app.id}>
{app.name} ({app.spiffe_id})
</option>
))}
</select>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Role Identifier *
</label>
<input
type="text"
id="roleName"
placeholder="e.g. navigator, copilot, auditor"
required
style="width: 100%;"
/>
</div>
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Description / Purpose
</label>
<input
type="text"
id="roleDescription"
placeholder="e.g. Flight routing and navigational telemetry access"
style="width: 100%;"
/>
</div>
</div>
<div style="display: flex; gap: 0.75rem;">
<button type="submit" class="btn-primary" style="min-height: 42px;">
Save Role
</button>
<button
type="button"
class="btn-outline"
onclick="closeRoleDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
</div>"""
role_form_replacement = """ <AdminModal
id="roleFormModal"
title="Create New Role"
onClose="closeRoleDrawer()"
>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1.25rem 0;">
Define a global shared role or an application-scoped custom grant.
</p>
<form id="roleForm" onsubmit="handleSaveRole(event)">
<input type="hidden" id="editRoleId" value="" />
<div
id="scopeSelectContainer"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1rem;"
>
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Scope (Applicability) *
</label>
<select
id="roleScope"
onchange="handleScopeChange()"
style="width: 100%;"
>
<option value="global">
Global (Shared across ALL applications)
</option>
<option value="app_specific">
Application-Specific (Scoped to single app)
</option>
</select>
</div>
<div id="appSelectContainer" style="display: none;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Target Application *
</label>
<select
id="roleAppId"
style="width: 100%;"
>
{apps.map((app) => (
<option value={app.id}>
{app.name} ({app.spiffe_id})
</option>
))}
</select>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Role Identifier *
</label>
<input
type="text"
id="roleName"
placeholder="e.g. navigator, copilot, auditor"
required
style="width: 100%;"
/>
</div>
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; color: var(--text-secondary);">
Description / Purpose
</label>
<input
type="text"
id="roleDescription"
placeholder="e.g. Flight routing and navigational telemetry access"
style="width: 100%;"
/>
</div>
</div>
<div style="display: flex; gap: 0.75rem;">
<button type="submit" class="btn-primary" style="min-height: 42px;">
Save Role
</button>
<button
type="button"
class="btn-outline"
onclick="closeRoleDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
</AdminModal>"""
content = content.replace(role_form, role_form_replacement)
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
f.write(content)
print("Replaced Roles Form!")

View File

@ -1,355 +0,0 @@
import re
with open('ui/components/AdminRolesPage.tsx', 'r') as f:
content = f.read()
roles_table_desktop_mobile = """ {/* Desktop Table View (≥ 768px) */}
<div class="table-container desktop-only" style="display: none;">
<table id="rolesTable">
<thead>
<tr>
<th>Role Identifier</th>
<th>Scope</th>
<th>Description</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{roles.length === 0
? (
<tr>
<td
colSpan={5}
style="text-align: center; color: var(--text-muted); padding: 2rem;"
>
No roles found.
</td>
</tr>
)
: (
roles.map((r) => {
const isGlobal = !r.app_id;
const isCoreAdmin = isGlobal && r.name === "admin";
return (
<tr
key={r.id}
class="role-row"
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
>
<td>
<strong style="font-family: monospace; font-size: 0.95rem; color: var(--text-primary);">
{r.name}
</strong>
</td>
<td>
{isGlobal
? (
<span class="badge badge-info">
Global (Shared)
</span>
)
: (
<span class="badge badge-warning">
{r.app_name || "App-Specific"}
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
{r.description || "-"}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(r.created_at).toLocaleDateString()}
</td>
<td>
{!isCoreAdmin
? (
<div style="display: flex; gap: 0.35rem;">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`openEditRoleDrawer(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit
</button>
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
</div>
)
: (
<span style="font-size: 0.75rem; color: var(--text-muted); font-style: italic;">
Immutable System Role
</span>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Mobile View (< 768px) */}
<div
id="rolesMobileDeck"
class="mobile-only"
style="display: flex; flex-direction: column; gap: 0.75rem;"
>
{roles.length === 0
? (
<div class="card" style="text-align: center; padding: 2rem;">
<p style="color: var(--text-muted); margin: 0;">
No roles found.
</p>
</div>
)
: (
roles.map((r) => {
const isGlobal = !r.app_id;
const isCoreAdmin = isGlobal && r.name === "admin";
return (
<div
class="card role-card"
key={r.id}
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
style="margin-bottom: 0; padding: 1rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<strong style="font-family: monospace; font-size: 1.05rem; color: var(--text-primary);">
{r.name}
</strong>
{isGlobal
? (
<span class="badge badge-info">
Global (Shared)
</span>
)
: (
<span class="badge badge-warning">
{r.app_name || "App-Specific"}
</span>
)}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.5;">
<p style="margin: 0 0 0.5rem 0;">
{r.description || "No description provided."}
</p>
<div style="font-size: 0.8rem;">
Created: {new Date(r.created_at).toLocaleDateString()}
</div>
</div>
{!isCoreAdmin
? (
<div style="display: flex; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`openEditRoleDrawer(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit Role
</button>
<button
type="button"
class="btn-danger"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
</div>
)
: (
<div style="text-align: center; padding: 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--text-muted); font-style: italic;">
Immutable System Role
</div>
)}
</div>
);
})
)}
</div>"""
roles_table_replacement = """ <AdminTable
id="rolesTable"
headers={[
"Role Identifier",
"Scope",
"Description",
"Created",
"Actions",
]}
isEmpty={roles.length === 0}
emptyState="No roles found."
desktopRows={roles.map((r) => {
const isGlobal = !r.app_id;
const isCoreAdmin = isGlobal && r.name === "admin";
return (
<tr
key={r.id}
class="role-row"
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
>
<td>
<strong style="font-family: monospace; font-size: 0.95rem; color: var(--text-primary);">
{r.name}
</strong>
</td>
<td>
{isGlobal
? (
<span class="badge badge-info">
Global (Shared)
</span>
)
: (
<span class="badge badge-warning">
{r.app_name || "App-Specific"}
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
{r.description || "-"}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(r.created_at).toLocaleDateString()}
</td>
<td>
{!isCoreAdmin
? (
<div style="display: flex; gap: 0.35rem;">
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`openEditRoleDrawer(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit
</button>
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
</div>
)
: (
<span style="font-size: 0.75rem; color: var(--text-muted); font-style: italic;">
Immutable System Role
</span>
)}
</td>
</tr>
);
})}
mobileCards={roles.map((r) => {
const isGlobal = !r.app_id;
const isCoreAdmin = isGlobal && r.name === "admin";
return (
<div
class="card role-card"
key={r.id}
data-app-id={r.app_id || "global"}
data-search={`${r.name} ${r.description || ""} ${
isGlobal ? "global" : r.app_name || ""
}`.toLowerCase()}
style="margin-bottom: 0; padding: 1rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<strong style="font-family: monospace; font-size: 1.05rem; color: var(--text-primary);">
{r.name}
</strong>
{isGlobal
? (
<span class="badge badge-info">
Global (Shared)
</span>
)
: (
<span class="badge badge-warning">
{r.app_name || "App-Specific"}
</span>
)}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.5;">
<p style="margin: 0 0 0.5rem 0;">
{r.description || "No description provided."}
</p>
<div style="font-size: 0.8rem;">
Created: {new Date(r.created_at).toLocaleDateString()}
</div>
</div>
{!isCoreAdmin
? (
<div style="display: flex; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`openEditRoleDrawer(${
JSON.stringify(JSON.stringify(r))
})`}
>
Edit Role
</button>
<button
type="button"
class="btn-danger"
style="flex: 1; min-height: 38px; font-size: 0.85rem;"
onclick={`deleteRole('${r.id}', '${r.name}')`}
>
Delete
</button>
</div>
)
: (
<div style="text-align: center; padding: 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--text-muted); font-style: italic;">
Immutable System Role
</div>
)}
</div>
);
})}
/>"""
content = content.replace(roles_table_desktop_mobile, roles_table_replacement)
with open('ui/components/AdminRolesPage.tsx', 'w') as f:
f.write(content)
print("Replaced Roles table!")

View File

@ -1,385 +0,0 @@
import re
with open('ui/components/AdminUserDetailsPage.tsx', 'r') as f:
content = f.read()
import_str = """import { AdminLayout } from "./AdminLayout.tsx";"""
content = content.replace(import_str, """import { AdminLayout } from "./AdminLayout.tsx";\nimport { AdminTable } from "./admin/AdminTable.tsx";""")
grants_table = """ <div class="table-container" style="margin-top: 1.25rem;">
<table>
<thead>
<tr>
<th>Application Name</th>
<th>SPIFFE Workload ID</th>
<th>Assigned Role</th>
<th>Granted At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{grants.length === 0
? (
<tr>
<td
colSpan={5}
style="text-align: center; color: var(--danger); padding: 1.5rem;"
>
No application permissions granted (User is blocked from
all subsidiary apps).
</td>
</tr>
)
: (
grants.map((grant) => (
<tr key={grant.id}>
<td>
<strong style="color: var(--text-primary);">
{grant.app_name}
</strong>
</td>
<td>
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); font-size: 0.8rem; font-family: monospace;">
{grant.spiffe_id}
</code>
</td>
<td>
<span class="badge badge-info">
{grant.role}
</span>
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(grant.created_at).toLocaleDateString()}
</td>
<td>
<button
type="button"
class="btn-danger"
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
onclick={`revokeGrant('${user.id}', '${grant.app_id}', '${grant.app_name}')`}
>
Revoke Access
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>"""
grants_replacement = """ <div style="margin-top: 1.25rem;">
<AdminTable
id="grantsTable"
headers={[
"Application Name",
"SPIFFE Workload ID",
"Assigned Role",
"Granted At",
"Actions",
]}
isEmpty={grants.length === 0}
emptyState="No application permissions granted (User is blocked from all subsidiary apps)."
desktopRows={grants.map((grant) => (
<tr key={grant.id}>
<td>
<strong style="color: var(--text-primary);">
{grant.app_name}
</strong>
</td>
<td>
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); font-size: 0.8rem; font-family: monospace;">
{grant.spiffe_id}
</code>
</td>
<td>
<span class="badge badge-info">
{grant.role}
</span>
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(grant.created_at).toLocaleDateString()}
</td>
<td>
<button
type="button"
class="btn-danger"
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
onclick={`revokeGrant('${user.id}', '${grant.app_id}', '${grant.app_name}')`}
>
Revoke Access
</button>
</td>
</tr>
))}
mobileCards={grants.map((grant) => (
<div class="card" key={grant.id} style="margin-bottom: 0;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<strong style="color: var(--text-primary);">{grant.app_name}</strong>
<span class="badge badge-info">{grant.role}</span>
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.75rem; font-family: monospace;">
{grant.spiffe_id}
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.8rem; color: var(--text-secondary);">
{new Date(grant.created_at).toLocaleDateString()}
</span>
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
onclick={`revokeGrant('${user.id}', '${grant.app_id}', '${grant.app_name}')`}
>
Revoke Access
</button>
</div>
</div>
))}
/>
</div>"""
content = content.replace(grants_table, grants_replacement)
sessions_table = """ <div class="table-container">
<table>
<thead>
<tr>
<th>Session ID</th>
<th>Created</th>
<th>Expires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{sessions.length === 0
? (
<tr>
<td
colSpan={4}
style="text-align: center; color: var(--text-muted); padding: 1.5rem;"
>
No active sessions.
</td>
</tr>
)
: (
sessions.map((s) => (
<tr key={s.id}>
<td style="font-family: monospace; font-size: 0.85rem;">
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
{s.id.substring(0, 16)}...
</code>
{s.is_agent
? (
<span
class="badge badge-warning"
style="margin-left: 0.75rem;"
>
Agent Session
</span>
)
: null}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(s.created_at).toLocaleString()}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(s.expires_at).toLocaleString()}
</td>
<td>
<button
type="button"
class="btn-outline"
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
onclick={`revokeSession('${user.id}', '${s.id}')`}
>
Revoke
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>"""
sessions_replacement = """ <AdminTable
id="sessionsTable"
headers={["Session ID", "Created", "Expires", "Actions"]}
isEmpty={sessions.length === 0}
emptyState="No active sessions."
desktopRows={sessions.map((s) => (
<tr key={s.id}>
<td style="font-family: monospace; font-size: 0.85rem;">
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
{s.id.substring(0, 16)}...
</code>
{s.is_agent
? (
<span
class="badge badge-warning"
style="margin-left: 0.75rem;"
>
Agent Session
</span>
)
: null}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(s.created_at).toLocaleString()}
</td>
<td style="font-size: 0.85rem; color: var(--text-secondary);">
{new Date(s.expires_at).toLocaleString()}
</td>
<td>
<button
type="button"
class="btn-outline"
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
onclick={`revokeSession('${user.id}', '${s.id}')`}
>
Revoke
</button>
</td>
</tr>
))}
mobileCards={sessions.map((s) => (
<div class="card" key={s.id} style="margin-bottom: 0;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<code style="font-family: monospace; font-size: 0.85rem; background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm);">
{s.id.substring(0, 16)}...
</code>
{s.is_agent
? (
<span class="badge badge-warning">Agent Session</span>
)
: null}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.25rem;">
Created: {new Date(s.created_at).toLocaleString()}
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.8rem; color: var(--text-secondary);">
Expires: {new Date(s.expires_at).toLocaleString()}
</span>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
onclick={`revokeSession('${user.id}', '${s.id}')`}
>
Revoke
</button>
</div>
</div>
))}
/>"""
content = content.replace(sessions_table, sessions_replacement)
passkeys_table = """ <div class="table-container">
<table>
<thead>
<tr>
<th>Credential ID</th>
<th>Counter</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{passkeys.length === 0
? (
<tr>
<td
colSpan={3}
style="text-align: center; color: var(--text-muted); padding: 1.5rem;"
>
No registered passkeys.
</td>
</tr>
)
: (
passkeys.map((pk) => (
<tr key={pk.credential_id}>
<td style="font-family: monospace; font-size: 0.85rem; word-break: break-all; max-width: 250px;">
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
{pk.credential_id.substring(0, 32)}...
</code>
</td>
<td>
<span class="badge badge-info">{pk.sign_count}</span>
</td>
<td>
<button
type="button"
class="btn-danger"
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
onclick={`deletePasskey('${user.id}', '${pk.credential_id}')`}
>
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>"""
passkeys_replacement = """ <AdminTable
id="passkeysTable"
headers={["Credential ID", "Counter", "Actions"]}
isEmpty={passkeys.length === 0}
emptyState="No registered passkeys."
desktopRows={passkeys.map((pk) => (
<tr key={pk.credential_id}>
<td style="font-family: monospace; font-size: 0.85rem; word-break: break-all; max-width: 250px;">
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
{pk.credential_id.substring(0, 32)}...
</code>
</td>
<td>
<span class="badge badge-info">{pk.sign_count}</span>
</td>
<td>
<button
type="button"
class="btn-danger"
style="padding: 0.35rem 0.75rem; font-size: 0.8rem; min-height: 32px;"
onclick={`deletePasskey('${user.id}', '${pk.credential_id}')`}
>
Delete
</button>
</td>
</tr>
))}
mobileCards={passkeys.map((pk) => (
<div class="card" key={pk.credential_id} style="margin-bottom: 0;">
<div style="font-family: monospace; font-size: 0.85rem; word-break: break-all; margin-bottom: 0.75rem; color: var(--text-primary);">
<code style="background: var(--surface-muted); padding: 0.2rem 0.4rem; border-radius: var(--radius-sm); color: var(--text-primary);">
{pk.credential_id.substring(0, 32)}...
</code>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="badge badge-info">Count: {pk.sign_count}</span>
<button
type="button"
class="btn-danger"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
onclick={`deletePasskey('${user.id}', '${pk.credential_id}')`}
>
Delete
</button>
</div>
</div>
))}
/>"""
content = content.replace(passkeys_table, passkeys_replacement)
with open('ui/components/AdminUserDetailsPage.tsx', 'w') as f:
f.write(content)
print("Replaced User Details tables!")