Feat: Implement token tracking, soft delete, and Admin UI improvements
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
@@ -154,6 +154,7 @@
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.flex-header {
|
||||
@@ -163,65 +164,61 @@
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
/* Table Styles */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.module-card {
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
th,
|
||||
td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.module-card:hover {
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.module-name {
|
||||
th {
|
||||
font-weight: 600;
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--primary);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.key-box {
|
||||
background: #0f172a;
|
||||
padding: 1rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.key-cell {
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #818cf8;
|
||||
margin: 1.25rem 0;
|
||||
word-break: break-all;
|
||||
border: 1px solid var(--border);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.key-text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.key-text.visible {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.icon-actions {
|
||||
.token-cell {
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
@@ -232,31 +229,23 @@
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-size: 0.85rem;
|
||||
width: auto;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@@ -271,49 +260,17 @@
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -355,24 +312,18 @@
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.module-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #a5b4fc;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
@@ -385,12 +336,48 @@
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(100px);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1000;
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -422,17 +409,36 @@
|
||||
<section id="dashboard-section">
|
||||
<div class="card">
|
||||
<div class="flex-header">
|
||||
<h2>E-Learning Modules</h2>
|
||||
<button id="add-module-btn" class="btn-small">Create New Module</button>
|
||||
</div>
|
||||
<div id="module-list" class="module-grid">
|
||||
<!-- Modules will be injected here -->
|
||||
<h2>Modules & Token Usage</h2>
|
||||
<div style="display:flex; gap:1rem; align-items:center;">
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer; margin-bottom:0;">
|
||||
<input type="checkbox" id="show-archived-toggle" style="width:auto; margin:0;">
|
||||
<span style="font-size:0.9rem; color:var(--text-dim);">Show Archived</span>
|
||||
</label>
|
||||
<button id="add-module-btn" class="btn-small">Create New Module</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="modules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Module Name</th>
|
||||
<th>API Key</th>
|
||||
<th style="text-align: right;">Ingress</th>
|
||||
<th style="text-align: right;">Egress</th>
|
||||
<th style="text-align: right;">Total</th>
|
||||
<th>Meta</th>
|
||||
<th style="text-align: right;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="module-list-body">
|
||||
<!-- Rows injected here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Create Module Modal -->
|
||||
<div id="create-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<h2>Create New Module</h2>
|
||||
@@ -461,6 +467,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Key Modal -->
|
||||
<div id="view-key-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<h2>API Key</h2>
|
||||
<p style="color: var(--text-dim); margin-bottom: 1rem;">Use this key to authenticate your API requests.</p>
|
||||
<div class="form-group">
|
||||
<div style="background: rgba(15, 23, 42, 0.5); padding: 1rem; border-radius: 12px; border: 1px solid var(--border); font-family: monospace; word-break: break-all; color: #818cf8; user-select: text;"
|
||||
id="modal-key-display">
|
||||
<!-- Key injected here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="close-key-modal-btn" class="btn-small btn-outline">Close</button>
|
||||
<button id="copy-modal-key-btn" class="btn-small">Copy Key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<div id="confirm-modal" class="modal hidden">
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
<h2 id="confirm-title">Confirm Action</h2>
|
||||
<p id="confirm-message" style="color: var(--text-dim); margin-bottom: 2rem;">Are you sure?</p>
|
||||
<div class="modal-actions">
|
||||
<button id="confirm-cancel-btn" class="btn-small btn-outline">Cancel</button>
|
||||
<button id="confirm-ok-btn" class="btn-small btn-danger">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast">Action successful!</div>
|
||||
|
||||
<script>
|
||||
@@ -469,7 +505,7 @@
|
||||
const loginSection = document.getElementById('login-section');
|
||||
const dashboardSection = document.getElementById('dashboard-section');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const moduleList = document.getElementById('module-list');
|
||||
const moduleListBody = document.getElementById('module-list-body');
|
||||
|
||||
function showToast(msg) {
|
||||
const toast = document.getElementById('toast');
|
||||
@@ -478,6 +514,10 @@
|
||||
setTimeout(() => toast.classList.remove('show'), 3000);
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
async function apiRequest(endpoint, method = 'GET', body = null) {
|
||||
const options = {
|
||||
method,
|
||||
@@ -495,6 +535,13 @@
|
||||
logout();
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
console.error("API Error:", err);
|
||||
// Only alert if it's not a 401 (already handled)
|
||||
alert(`Error ${response.status}: ${err.detail || 'Unknown error'}`);
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -540,38 +587,72 @@
|
||||
loadModules();
|
||||
}
|
||||
|
||||
const showArchivedToggle = document.getElementById('show-archived-toggle');
|
||||
|
||||
showArchivedToggle.onchange = loadModules;
|
||||
|
||||
async function loadModules() {
|
||||
const modules = await apiRequest('/internal/admin/modules');
|
||||
const includeArchived = showArchivedToggle.checked;
|
||||
const endpoint = includeArchived ? '/internal/admin/modules?include_archived=true' : '/internal/admin/modules';
|
||||
const modules = await apiRequest(endpoint);
|
||||
if (!modules) return;
|
||||
|
||||
moduleList.innerHTML = modules.map(m => `
|
||||
<div class="module-card">
|
||||
<div class="module-name">${m.name}</div>
|
||||
<div class="text-dim" style="font-size: 0.8rem">Created: ${new Date(m.created_at).toLocaleDateString()}</div>
|
||||
|
||||
<div class="module-meta">
|
||||
${m.program ? `<span class="badge" title="Program">${m.program}</span>` : ''}
|
||||
${m.lob ? `<span class="badge" title="Line of Business">${m.lob}</span>` : ''}
|
||||
${m.job_code ? `<span class="badge" title="Job Code">${m.job_code}</span>` : ''}
|
||||
</div>
|
||||
moduleListBody.innerHTML = modules.map(m => {
|
||||
const isArchived = !m.is_active;
|
||||
const rowStyle = isArchived ? 'opacity: 0.6; background: rgba(0,0,0,0.2);' : '';
|
||||
|
||||
<div class="key-box">
|
||||
<span class="key-text" id="key-${m.id}">••••••••••••••••••••••••••••••••</span>
|
||||
<div class="icon-actions">
|
||||
<button class="icon-btn" onclick="toggleKey(${m.id}, '${m.secret_key}')" title="Show/Hide Key">
|
||||
<svg id="eye-icon-${m.id}" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
return `
|
||||
<tr style="${rowStyle}">
|
||||
<td>
|
||||
<div style="font-weight: 600;">
|
||||
${m.name}
|
||||
${isArchived ? '<span class="badge" style="background:rgba(239,68,68,0.2); color:#ef4444; margin-left:8px;">ARCHIVED</span>' : ''}
|
||||
</div>
|
||||
<div class="text-dim" style="font-size: 0.75rem">Created: ${new Date(m.created_at).toLocaleDateString()}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="key-cell">
|
||||
<span class="key-text" id="key-${m.id}">••••••••</span>
|
||||
<button class="icon-btn" onclick="viewKey('${m.secret_key}')" title="View Key" style="display:inline-flex; width:24px; height:24px; vertical-align:middle; margin-left:8px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
</button>
|
||||
<button class="icon-btn" onclick="copyKey('${m.secret_key}')" title="Copy Key">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>
|
||||
<button class="icon-btn" onclick="copyKey('${m.secret_key}')" title="Copy Key" style="display:inline-flex; width:24px; height:24px; vertical-align:middle; margin-left:4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-small btn-outline" onclick="rotateKey(${m.id})">Rotate Key</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteModule(${m.id})">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
</td>
|
||||
<td class="token-cell" title="Ingress Tokens">${formatNumber(m.ingress_tokens)}</td>
|
||||
<td class="token-cell" title="Egress Tokens">${formatNumber(m.egress_tokens)}</td>
|
||||
<td class="token-cell" style="color: var(--primary); font-weight:bold;" title="Total Tokens">${formatNumber(m.total_tokens)}</td>
|
||||
<td>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:4px; max-width:200px;">
|
||||
${m.program ? `<span class="badge" title="Program">${m.program}</span>` : ''}
|
||||
${m.lob ? `<span class="badge" title="Line of Business">${m.lob}</span>` : ''}
|
||||
${m.job_code ? `<span class="badge" title="Job Code">${m.job_code}</span>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions-cell">
|
||||
${!isArchived ? `
|
||||
<button class="icon-btn" onclick="rotateKey(${m.id})" title="Rotate Key">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 16h5v5"></path></svg>
|
||||
</button>
|
||||
<button class="icon-btn btn-danger" onclick="archiveModule(${m.id})" title="Archive Module">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
</button>
|
||||
` : `
|
||||
<button class="icon-btn" onclick="restoreModule(${m.id})" title="Restore Module" style="color:#22c55e; border-color: #22c55e; background: rgba(34,197,94,0.1);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 14 15 14 15 8"></polyline><path d="M20 20v-7a4 4 0 0 0-4-4H4"></path><path d="M4 20v-9"></path></svg>
|
||||
</button>
|
||||
<button class="icon-btn btn-danger" onclick="hardDeleteModule(${m.id})" title="Permanently Delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Modal Logic
|
||||
@@ -613,20 +694,24 @@
|
||||
}
|
||||
};
|
||||
|
||||
window.toggleKey = (id, key) => {
|
||||
const el = document.getElementById(`key-${id}`);
|
||||
const icon = document.getElementById(`eye-icon-${id}`);
|
||||
const isHidden = el.innerText.includes('•');
|
||||
// View Key Modal Logic
|
||||
const viewKeyModal = document.getElementById('view-key-modal');
|
||||
const closeKeyModalBtn = document.getElementById('close-key-modal-btn');
|
||||
const copyModalKeyBtn = document.getElementById('copy-modal-key-btn');
|
||||
const modalKeyDisplay = document.getElementById('modal-key-display');
|
||||
let currentKeyToCopy = '';
|
||||
|
||||
if (isHidden) {
|
||||
el.innerText = key;
|
||||
el.classList.add('visible');
|
||||
icon.innerHTML = '<path d="M9.88 9.88 2 20"></path><path d="M2 12s3-7 10-7a9.77 9.77 0 0 1 5 1.45"></path><path d="M22 12s-3 7-10 7a9.77 9.77 0 0 1-5-1.45"></path><path d="M15.12 15.12a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>';
|
||||
} else {
|
||||
el.innerText = '••••••••••••••••••••••••••••••••';
|
||||
el.classList.remove('visible');
|
||||
icon.innerHTML = '<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle>';
|
||||
}
|
||||
closeKeyModalBtn.onclick = () => viewKeyModal.classList.add('hidden');
|
||||
|
||||
copyModalKeyBtn.onclick = () => {
|
||||
navigator.clipboard.writeText(currentKeyToCopy);
|
||||
showToast('Key copied to clipboard!');
|
||||
};
|
||||
|
||||
window.viewKey = (key) => {
|
||||
currentKeyToCopy = key;
|
||||
modalKeyDisplay.innerText = key;
|
||||
viewKeyModal.classList.remove('hidden');
|
||||
};
|
||||
|
||||
window.copyKey = (key) => {
|
||||
@@ -634,23 +719,95 @@
|
||||
showToast('Key copied to clipboard!');
|
||||
};
|
||||
|
||||
window.rotateKey = async (id) => {
|
||||
if (confirm('Are you sure? Previous key will stop working immediately.')) {
|
||||
await apiRequest(`/internal/admin/modules/${id}/rotate`, 'POST');
|
||||
showToast('Key rotated successfully');
|
||||
loadModules();
|
||||
// Confirmation Modal Logic
|
||||
const confirmModal = document.getElementById('confirm-modal');
|
||||
const confirmTitle = document.getElementById('confirm-title');
|
||||
const confirmMessage = document.getElementById('confirm-message');
|
||||
const confirmOkBtn = document.getElementById('confirm-ok-btn');
|
||||
const confirmCancelBtn = document.getElementById('confirm-cancel-btn');
|
||||
|
||||
let onConfirmHandler = null;
|
||||
|
||||
function openConfirmModal(title, message, isDanger, handler) {
|
||||
confirmTitle.innerText = title;
|
||||
confirmMessage.innerText = message;
|
||||
onConfirmHandler = handler;
|
||||
|
||||
if (isDanger) {
|
||||
confirmOkBtn.classList.add('btn-danger');
|
||||
confirmOkBtn.style.backgroundColor = ''; // Reset inline style if any
|
||||
} else {
|
||||
confirmOkBtn.classList.remove('btn-danger');
|
||||
confirmOkBtn.style.backgroundColor = 'var(--primary)';
|
||||
}
|
||||
|
||||
confirmModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
confirmCancelBtn.onclick = () => {
|
||||
confirmModal.classList.add('hidden');
|
||||
onConfirmHandler = null;
|
||||
};
|
||||
|
||||
window.deleteModule = async (id) => {
|
||||
if (confirm('Delete this module? This cannot be undone.')) {
|
||||
await apiRequest(`/internal/admin/modules/${id}`, 'DELETE');
|
||||
showToast('Module deleted');
|
||||
loadModules();
|
||||
confirmOkBtn.onclick = async () => {
|
||||
if (onConfirmHandler) {
|
||||
await onConfirmHandler();
|
||||
}
|
||||
confirmModal.classList.add('hidden');
|
||||
onConfirmHandler = null;
|
||||
};
|
||||
|
||||
document.getElementById('login-btn').onclick = login;
|
||||
window.rotateKey = (id) => {
|
||||
openConfirmModal(
|
||||
'Rotate API Key',
|
||||
'Are you sure? Previous key will stop working immediately.',
|
||||
false,
|
||||
async () => {
|
||||
await apiRequest(`/internal/admin/modules/${id}/rotate`, 'POST');
|
||||
showToast('Key rotated successfully');
|
||||
loadModules();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
window.archiveModule = (id) => {
|
||||
openConfirmModal(
|
||||
'Archive Module',
|
||||
'Archive this module? It will stop working immediately.',
|
||||
true,
|
||||
async () => {
|
||||
await apiRequest(`/internal/admin/modules/${id}`, 'DELETE');
|
||||
showToast('Module archived');
|
||||
loadModules();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
window.restoreModule = (id) => {
|
||||
openConfirmModal(
|
||||
'Restore Module',
|
||||
'Restore this module? API access will be re-enabled.',
|
||||
false,
|
||||
async () => {
|
||||
await apiRequest(`/internal/admin/modules/${id}/restore`, 'POST');
|
||||
showToast('Module restored');
|
||||
loadModules();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
window.hardDeleteModule = (id) => {
|
||||
openConfirmModal(
|
||||
'Permanent Delete',
|
||||
'PERMANENTLY DELETE this module? This cannot be undone.',
|
||||
true,
|
||||
async () => {
|
||||
await apiRequest(`/internal/admin/modules/${id}?hard_delete=true`, 'DELETE');
|
||||
showToast('Module permanently deleted');
|
||||
loadModules();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
document.getElementById('login-btn').onclick = login;
|
||||
logoutBtn.onclick = logout;
|
||||
|
||||
Reference in New Issue
Block a user