universal-chatbot-saas 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin - Bot Management</title>
7
+ <style>
8
+ body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f4f7fb;color:#0f172a}
9
+ .wrap{max-width:1280px;margin:0 auto;padding:24px}
10
+ header,section{background:#fff;border-radius:16px;box-shadow:0 10px 30px rgba(15,23,42,.08)}
11
+ header{padding:20px 24px;margin-bottom:18px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
12
+ h1{margin:0;font-size:28px;color:#0ea5a4}
13
+ .sub{color:#64748b;font-size:14px;margin-top:4px}
14
+ .controls{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 18px}
15
+ .controls input,.controls select{flex:1 1 220px}
16
+ button,a.btn{border:none;border-radius:10px;padding:10px 14px;font-size:14px;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:8px}
17
+ .primary{background:#0ea5a4;color:#fff}.secondary{background:#e2e8f0;color:#0f172a}.danger{background:#ef4444;color:#fff}
18
+ input,textarea,select{border:1px solid #cbd5e1;border-radius:12px;padding:12px 14px;font-size:14px;background:#fff;transition:border-color .15s ease,box-shadow .15s ease}
19
+ input:focus,textarea:focus,select:focus{outline:none;border-color:#0ea5a4;box-shadow:0 0 0 4px rgba(14,165,164,.12)}
20
+ input[readonly]{background:#f8fafc;color:#475569}
21
+ .card{padding:18px}
22
+ .table-shell{overflow:auto}
23
+ table{width:100%;min-width:980px;border-collapse:collapse}
24
+ th,td{padding:10px 8px;border-bottom:1px solid #e2e8f0;text-align:left;vertical-align:top;font-size:14px}
25
+ tr:hover{background:#f8fafc}
26
+ .status{display:none;margin:0 0 16px;padding:12px 14px;border-radius:10px;font-size:14px}
27
+ .status.ok{display:block;background:#dcfce7;color:#166534}.status.err{display:block;background:#fee2e2;color:#991b1b}
28
+ .actions{display:flex;gap:8px;flex-wrap:wrap}
29
+ .action-btn{padding:8px 12px;font-size:13px}
30
+ .empty{padding:20px 10px;text-align:center;color:#64748b}
31
+ .pager{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-top:12px;color:#64748b;font-size:13px}
32
+ .modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.55);display:none;align-items:center;justify-content:center;padding:18px;z-index:40}
33
+ .modal-backdrop.visible{display:flex}
34
+ .modal{width:min(900px,100%);max-height:min(90vh,900px);overflow:auto;background:#fff;border-radius:18px;box-shadow:0 30px 80px rgba(15,23,42,.3);padding:20px}
35
+ .modal-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:16px}
36
+ .modal-close{background:#e2e8f0;color:#0f172a;border:none;border-radius:10px;padding:8px 12px;cursor:pointer}
37
+ .row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
38
+ .field{margin-bottom:14px}
39
+ label{display:block;font-size:13px;font-weight:600;margin-bottom:6px}
40
+ .modal .field input,.modal .field select,.modal .field textarea{width:100%;box-sizing:border-box}
41
+ textarea{min-height:120px;resize:vertical}
42
+ .checks{display:flex;gap:16px;flex-wrap:wrap}
43
+ .check{display:flex;align-items:center;gap:8px}
44
+ .modal-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;padding-top:16px;border-top:1px solid #e2e8f0}
45
+ .api-box{background:#f8fafc;border:1px dashed #cbd5e1;padding:12px;border-radius:10px;font-family:monospace;font-size:12px;word-break:break-all}
46
+ @media (max-width:900px){.row{grid-template-columns:1fr}.modal{padding:16px}.controls .btn,.controls a.btn,.controls select{flex:1 1 140px}}
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <div class="wrap">
51
+ <header>
52
+ <div>
53
+ <h1>Bot Management</h1>
54
+ <div class="sub">Create and manage bot identities, models, prompts, and API keys.</div>
55
+ </div>
56
+ <a class="btn secondary" href="/portal/index.html">Knowledge Portal</a>
57
+ </header>
58
+
59
+ <div id="status" class="status"></div>
60
+
61
+ <div class="controls">
62
+ <button class="primary" id="newBtn">+ New Bot</button>
63
+ <button class="secondary" id="refreshBtn">Refresh</button>
64
+ <input id="search" type="text" placeholder="Search by id, name, provider, model">
65
+ <select id="pageSize">
66
+ <option value="5">5 / page</option>
67
+ <option value="10" selected>10 / page</option>
68
+ <option value="20">20 / page</option>
69
+ <option value="50">50 / page</option>
70
+ </select>
71
+ <a class="btn secondary" href="/portal/admin-users.html">Users</a>
72
+ </div>
73
+
74
+ <section class="card">
75
+ <div class="table-shell">
76
+ <table>
77
+ <thead>
78
+ <tr>
79
+ <th>Bot ID</th>
80
+ <th>Name</th>
81
+ <th>Provider</th>
82
+ <th>Model</th>
83
+ <th>Status</th>
84
+ <th>Actions</th>
85
+ </tr>
86
+ </thead>
87
+ <tbody id="rows"></tbody>
88
+ </table>
89
+ </div>
90
+ <div class="pager">
91
+ <span id="countInfo"></span>
92
+ <span>
93
+ <button class="secondary" id="prevBtn">Previous</button>
94
+ <span id="pageInfo" style="padding:0 8px">Page 1 of 1</span>
95
+ <button class="secondary" id="nextBtn">Next</button>
96
+ </span>
97
+ </div>
98
+ </section>
99
+ </div>
100
+
101
+ <div id="modalBackdrop" class="modal-backdrop" aria-hidden="true">
102
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="formTitle">
103
+ <div class="modal-head">
104
+ <div>
105
+ <h2 id="formTitle" style="margin:0">New Bot</h2>
106
+ <div class="sub">Use the modal to add or edit bot records.</div>
107
+ </div>
108
+ <button class="modal-close" id="closeModalBtn" type="button">Close</button>
109
+ </div>
110
+
111
+ <div class="row">
112
+ <div class="field"><label>Bot ID *</label><input id="botId" type="text" placeholder="support-bot"></div>
113
+ <div class="field"><label>Bot Name *</label><input id="botName" type="text" placeholder="Support Assistant"></div>
114
+ </div>
115
+ <div class="row">
116
+ <div class="field"><label>Provider</label><select id="provider"><option value="groq">Groq</option><option value="openai">OpenAI</option><option value="claude">Claude</option><option value="mistral">Mistral</option></select></div>
117
+ <div class="field"><label>Model</label><input id="model" type="text" placeholder="llama-3.3-70b-versatile"></div>
118
+ </div>
119
+ <div class="row">
120
+ <div class="field"><label>Guard Model</label><input id="guardModel" type="text" placeholder="meta-llama/llama-prompt-guard-2-86m"></div>
121
+ <div class="field"><label>API Key</label><input id="apiKey" type="text" placeholder="leave blank to auto-generate"></div>
122
+ </div>
123
+ <div class="field"><label>Description</label><textarea id="description" placeholder="Short description"></textarea></div>
124
+ <div class="field"><label>System Prompt</label><textarea id="systemPrompt" placeholder="System prompt for this bot"></textarea></div>
125
+ <div class="checks">
126
+ <label class="check"><input id="enableGuard" type="checkbox" checked> Enable Prompt Guard</label>
127
+ <label class="check"><input id="isActive" type="checkbox" checked> Active</label>
128
+ </div>
129
+ <div class="modal-actions">
130
+ <button class="primary" id="saveBtn">Save Bot</button>
131
+ <button class="secondary" id="cancelBtn">Cancel</button>
132
+ <button class="danger" id="deleteBtn" style="display:none">Delete Bot</button>
133
+ </div>
134
+ <div id="apiKeyWrap" style="display:none;margin-top:16px">
135
+ <div class="sub" style="margin-bottom:8px">New bot API key</div>
136
+ <div class="api-box" id="apiKeyValue"></div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ <script src="admin-bots.js"></script>
141
+ </body>
142
+ </html>
@@ -0,0 +1,265 @@
1
+ let bots = [];
2
+ let selectedBotId = null;
3
+ let editingBotId = null;
4
+ let currentPage = 1;
5
+ let pageSize = 10;
6
+
7
+ const el = {
8
+ status: document.getElementById('status'),
9
+ rows: document.getElementById('rows'),
10
+ countInfo: document.getElementById('countInfo'),
11
+ pageInfo: document.getElementById('pageInfo'),
12
+ prevBtn: document.getElementById('prevBtn'),
13
+ nextBtn: document.getElementById('nextBtn'),
14
+ search: document.getElementById('search'),
15
+ pageSize: document.getElementById('pageSize'),
16
+ newBtn: document.getElementById('newBtn'),
17
+ refreshBtn: document.getElementById('refreshBtn'),
18
+ modalBackdrop: document.getElementById('modalBackdrop'),
19
+ closeModalBtn: document.getElementById('closeModalBtn'),
20
+ formTitle: document.getElementById('formTitle'),
21
+ botId: document.getElementById('botId'),
22
+ botName: document.getElementById('botName'),
23
+ provider: document.getElementById('provider'),
24
+ model: document.getElementById('model'),
25
+ guardModel: document.getElementById('guardModel'),
26
+ apiKey: document.getElementById('apiKey'),
27
+ description: document.getElementById('description'),
28
+ systemPrompt: document.getElementById('systemPrompt'),
29
+ enableGuard: document.getElementById('enableGuard'),
30
+ isActive: document.getElementById('isActive'),
31
+ saveBtn: document.getElementById('saveBtn'),
32
+ cancelBtn: document.getElementById('cancelBtn'),
33
+ deleteBtn: document.getElementById('deleteBtn'),
34
+ apiKeyWrap: document.getElementById('apiKeyWrap'),
35
+ apiKeyValue: document.getElementById('apiKeyValue')
36
+ };
37
+
38
+ function showStatus(message, ok = true) {
39
+ el.status.className = `status ${ok ? 'ok' : 'err'}`;
40
+ el.status.textContent = message;
41
+ }
42
+
43
+ function escapeHtml(text) {
44
+ return String(text || '')
45
+ .replace(/&/g, '&amp;')
46
+ .replace(/</g, '&lt;')
47
+ .replace(/>/g, '&gt;')
48
+ .replace(/"/g, '&quot;')
49
+ .replace(/'/g, '&#39;');
50
+ }
51
+
52
+ function isBotActive(bot) {
53
+ if (typeof bot.isActive !== 'undefined') return Boolean(bot.isActive);
54
+ return bot.is_active !== false;
55
+ }
56
+
57
+ function isGuardEnabled(bot) {
58
+ if (typeof bot.enableGuard !== 'undefined') return Boolean(bot.enableGuard);
59
+ return bot.enable_guard !== false;
60
+ }
61
+
62
+ function getVisibleBots() {
63
+ const query = String(el.search.value || '').trim().toLowerCase();
64
+ return bots.filter((bot) => {
65
+ if (!query) return true;
66
+ const haystack = [bot.id, bot.name, bot.provider, bot.model, bot.description]
67
+ .map((value) => String(value || '').toLowerCase())
68
+ .join(' ');
69
+ return haystack.includes(query);
70
+ });
71
+ }
72
+
73
+ function renderBots() {
74
+ const filtered = getVisibleBots();
75
+ const total = filtered.length;
76
+ const totalPages = Math.max(Math.ceil(total / pageSize), 1);
77
+ if (currentPage > totalPages) currentPage = totalPages;
78
+ const start = (currentPage - 1) * pageSize;
79
+ const pageItems = filtered.slice(start, start + pageSize);
80
+
81
+ el.countInfo.textContent = total ? `Showing ${start + 1}-${Math.min(start + pageSize, total)} of ${total}` : 'No bots found';
82
+ el.pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
83
+ el.prevBtn.disabled = currentPage <= 1;
84
+ el.nextBtn.disabled = currentPage >= totalPages;
85
+
86
+ if (!pageItems.length) {
87
+ el.rows.innerHTML = '<tr><td colspan="6"><div class="empty">No matching bot records.</div></td></tr>';
88
+ return;
89
+ }
90
+
91
+ el.rows.innerHTML = pageItems.map((bot) => `
92
+ <tr data-id="${escapeHtml(bot.id)}">
93
+ <td>${escapeHtml(bot.id)}</td>
94
+ <td>${escapeHtml(bot.name || '')}</td>
95
+ <td>${escapeHtml(bot.provider || '')}</td>
96
+ <td>${escapeHtml(bot.model || '')}</td>
97
+ <td>${isBotActive(bot) ? 'Active' : 'Inactive'}</td>
98
+ <td>
99
+ <div class="actions">
100
+ <button class="secondary action-btn" data-action="edit" data-id="${escapeHtml(bot.id)}">Edit</button>
101
+ <button class="danger action-btn" data-action="delete" data-id="${escapeHtml(bot.id)}">Delete</button>
102
+ </div>
103
+ </td>
104
+ </tr>
105
+ `).join('');
106
+
107
+ Array.from(el.rows.querySelectorAll('button[data-action]')).forEach((button) => {
108
+ button.addEventListener('click', (event) => {
109
+ event.stopPropagation();
110
+ const id = button.getAttribute('data-id');
111
+ if (!id) return;
112
+ const action = button.getAttribute('data-action');
113
+ if (action === 'edit') {
114
+ openModalForEdit(id);
115
+ } else if (action === 'delete') {
116
+ deleteBot(id).catch((error) => showStatus(error.message, false));
117
+ }
118
+ });
119
+ });
120
+ }
121
+
122
+ function openModal() {
123
+ el.modalBackdrop.classList.add('visible');
124
+ el.modalBackdrop.setAttribute('aria-hidden', 'false');
125
+ }
126
+
127
+ function closeModal() {
128
+ el.modalBackdrop.classList.remove('visible');
129
+ el.modalBackdrop.setAttribute('aria-hidden', 'true');
130
+ editingBotId = null;
131
+ selectedBotId = null;
132
+ el.deleteBtn.style.display = 'none';
133
+ el.apiKeyWrap.style.display = 'none';
134
+ }
135
+
136
+ function fillForm(bot = {}) {
137
+ el.botId.value = bot.id || '';
138
+ el.botName.value = bot.name || '';
139
+ el.provider.value = bot.provider || 'groq';
140
+ el.model.value = bot.model || '';
141
+ el.guardModel.value = bot.guard_model || bot.guardModel || '';
142
+ el.apiKey.value = '';
143
+ el.description.value = bot.description || '';
144
+ el.systemPrompt.value = bot.system_prompt || bot.systemPrompt || '';
145
+ el.enableGuard.checked = isGuardEnabled(bot);
146
+ el.isActive.checked = isBotActive(bot);
147
+ }
148
+
149
+ function openModalForNew() {
150
+ editingBotId = null;
151
+ selectedBotId = null;
152
+ el.formTitle.textContent = 'New Bot';
153
+ el.botId.readOnly = false;
154
+ fillForm({ provider: 'groq', is_active: true, enable_guard: true });
155
+ el.deleteBtn.style.display = 'none';
156
+ el.apiKeyWrap.style.display = 'none';
157
+ openModal();
158
+ }
159
+
160
+ function openModalForEdit(botId) {
161
+ const bot = bots.find((item) => String(item.id) === String(botId));
162
+ if (!bot) return;
163
+ editingBotId = bot.id;
164
+ selectedBotId = bot.id;
165
+ el.formTitle.textContent = `Edit Bot: ${bot.name || bot.id}`;
166
+ el.botId.readOnly = true;
167
+ fillForm(bot);
168
+ el.deleteBtn.style.display = 'inline-flex';
169
+ el.apiKeyWrap.style.display = 'none';
170
+ openModal();
171
+ }
172
+
173
+ async function loadBotsFromServer() {
174
+ const response = await fetch('/admin/bots');
175
+ if (!response.ok) {
176
+ throw new Error(`Failed to load bots (${response.status})`);
177
+ }
178
+ bots = await response.json();
179
+ renderBots();
180
+ }
181
+
182
+ async function saveBot() {
183
+ const id = String(el.botId.value || '').trim();
184
+ const name = String(el.botName.value || '').trim();
185
+ if (!id) throw new Error('Bot ID is required');
186
+ if (!name) throw new Error('Bot name is required');
187
+
188
+ const payload = {
189
+ id,
190
+ name,
191
+ provider: el.provider.value,
192
+ model: String(el.model.value || '').trim(),
193
+ guardModel: String(el.guardModel.value || '').trim(),
194
+ apiKey: String(el.apiKey.value || '').trim(),
195
+ description: String(el.description.value || '').trim(),
196
+ systemPrompt: String(el.systemPrompt.value || '').trim(),
197
+ enableGuard: el.enableGuard.checked,
198
+ isActive: el.isActive.checked
199
+ };
200
+
201
+ const response = editingBotId
202
+ ? await fetch(`/admin/bots/${encodeURIComponent(editingBotId)}`, {
203
+ method: 'PUT',
204
+ headers: { 'Content-Type': 'application/json' },
205
+ body: JSON.stringify(payload)
206
+ })
207
+ : await fetch('/admin/bots', {
208
+ method: 'POST',
209
+ headers: { 'Content-Type': 'application/json' },
210
+ body: JSON.stringify(payload)
211
+ });
212
+
213
+ if (!response.ok) {
214
+ const error = await response.json().catch(() => ({}));
215
+ throw new Error(error.error || `Request failed (${response.status})`);
216
+ }
217
+
218
+ const result = await response.json().catch(() => ({}));
219
+ if (!editingBotId && (result.apiKey || result.api_key)) {
220
+ el.apiKeyWrap.style.display = 'block';
221
+ el.apiKeyValue.textContent = result.apiKey || result.api_key;
222
+ }
223
+
224
+ showStatus(editingBotId ? 'Bot updated' : 'Bot created');
225
+ await loadBotsFromServer();
226
+ if (editingBotId) {
227
+ closeModal();
228
+ }
229
+ }
230
+
231
+ async function deleteBot(id = selectedBotId) {
232
+ if (!id) return;
233
+ if (!confirm('Delete this bot?')) return;
234
+ const response = await fetch(`/admin/bots/${encodeURIComponent(id)}`, { method: 'DELETE' });
235
+ if (!response.ok) {
236
+ const error = await response.json().catch(() => ({}));
237
+ throw new Error(error.error || `Request failed (${response.status})`);
238
+ }
239
+ showStatus('Bot deleted');
240
+ closeModal();
241
+ await loadBotsFromServer();
242
+ }
243
+
244
+ el.newBtn.addEventListener('click', openModalForNew);
245
+ el.refreshBtn.addEventListener('click', () => loadBotsFromServer().catch((error) => showStatus(error.message, false)));
246
+ el.saveBtn.addEventListener('click', () => saveBot().catch((error) => showStatus(error.message, false)));
247
+ el.cancelBtn.addEventListener('click', closeModal);
248
+ el.closeModalBtn.addEventListener('click', closeModal);
249
+ el.deleteBtn.addEventListener('click', () => deleteBot().catch((error) => showStatus(error.message, false)));
250
+ el.search.addEventListener('input', () => { currentPage = 1; renderBots(); });
251
+ el.pageSize.addEventListener('change', () => { pageSize = Number(el.pageSize.value || 10); currentPage = 1; renderBots(); });
252
+ el.prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage -= 1; renderBots(); } });
253
+ el.nextBtn.addEventListener('click', () => { currentPage += 1; renderBots(); });
254
+ el.modalBackdrop.addEventListener('click', (event) => {
255
+ if (event.target === el.modalBackdrop) closeModal();
256
+ });
257
+ document.addEventListener('keydown', (event) => {
258
+ if (event.key === 'Escape' && el.modalBackdrop.classList.contains('visible')) {
259
+ closeModal();
260
+ }
261
+ });
262
+
263
+ document.addEventListener('DOMContentLoaded', () => {
264
+ loadBotsFromServer().catch((error) => showStatus(error.message, false));
265
+ });
@@ -0,0 +1,149 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin - User Access</title>
7
+ <style>
8
+ *{box-sizing:border-box}
9
+ body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f5f7fb;color:#0f172a}
10
+ .container{max-width:1260px;margin:0 auto;padding:24px}
11
+ header,section{background:#fff;border-radius:16px;box-shadow:0 10px 30px rgba(15,23,42,.08)}
12
+ header{padding:20px 22px;margin-bottom:18px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
13
+ h1{margin:0;color:#0ea5a4;font-size:28px}
14
+ .subtitle{margin-top:4px;color:#64748b;font-size:14px}
15
+ .nav-link{display:inline-flex;align-items:center;gap:8px;text-decoration:none;background:#e2e8f0;color:#0f172a;padding:10px 14px;border-radius:10px;font-size:14px}
16
+ .card{padding:18px}
17
+ .controls{display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap}
18
+ .controls input,.controls select{flex:1 1 180px}
19
+ button{border:1px solid transparent;border-radius:10px;padding:10px 14px;cursor:pointer;font-size:14px;font-family:inherit}
20
+ button.primary{background:#0ea5a4;color:#fff}
21
+ button.secondary{background:#e2e8f0;color:#0f172a}
22
+ button.danger{background:#ef4444;color:#fff}
23
+ button:disabled{opacity:.65;cursor:not-allowed}
24
+ input[type="text"],input[type="password"],select{border:1px solid #cbd5e1;border-radius:10px;padding:10px 14px;background:#fff;font-size:14px;font-family:inherit}
25
+ input[type="text"],input[type="password"]{cursor:text}
26
+ select{cursor:pointer}
27
+ input[type="checkbox"]{width:16px;height:16px;margin:0;cursor:pointer;accent-color:#0ea5a4}
28
+ input[type="text"]:focus,input[type="password"]:focus,select:focus{outline:none;border-color:#0ea5a4;box-shadow:0 0 0 3px rgba(14,165,164,.12)}
29
+ .table-shell{overflow:auto}
30
+ table{width:100%;min-width:920px;border-collapse:collapse;font-size:14px}
31
+ th,td{text-align:left;padding:10px;border-bottom:1px solid #e2e8f0;vertical-align:top}
32
+ tr:hover{background:#f8fafc}
33
+ .chips{display:flex;flex-wrap:wrap;gap:6px}
34
+ .chip{background:#e2e8f0;color:#334155;border-radius:999px;padding:3px 10px;font-size:12px}
35
+ .status{display:none;margin-bottom:16px;border-radius:10px;padding:12px 14px;font-size:14px}
36
+ .status.success{display:block;background:#dcfce7;color:#166534}
37
+ .status.error{display:block;background:#fee2e2;color:#991b1b}
38
+ .muted{color:#64748b;font-size:12px}
39
+ .actions{display:flex;gap:8px;flex-wrap:wrap}
40
+ .action-btn{padding:8px 12px;font-size:13px}
41
+ .pager{margin-top:12px;display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;color:#64748b;font-size:13px}
42
+ .empty{padding:24px 8px;color:#64748b;text-align:center}
43
+ .modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.55);display:none;align-items:center;justify-content:center;padding:18px;z-index:40}
44
+ .modal-backdrop.visible{display:flex}
45
+ .modal{width:min(760px,100%);max-height:min(90vh,900px);overflow:auto;background:#fff;border-radius:18px;box-shadow:0 30px 80px rgba(15,23,42,.3);padding:20px}
46
+ .modal-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:16px}
47
+ .modal-close{background:#e2e8f0;color:#0f172a;border:none;border-radius:10px;padding:8px 12px;cursor:pointer}
48
+ .form-group{margin-bottom:14px}
49
+ label{display:block;font-weight:600;margin-bottom:6px;color:#0f172a;font-size:14px}
50
+ .modal .form-group input[type="text"],.modal .form-group input[type="password"],.modal .form-group select{width:100%}
51
+ .checkbox-row{display:flex;align-items:center;gap:8px}
52
+ .checkbox-row label{display:inline;margin:0}
53
+ .bot-list{max-height:220px;overflow:auto;border:1px solid #e2e8f0;border-radius:10px;padding:10px;background:#f8fafc}
54
+ .bot-item{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:14px}
55
+ .bot-item:last-child{margin-bottom:0}
56
+ .modal-actions{display:flex;gap:10px;margin-top:16px;padding-top:16px;border-top:1px solid #e2e8f0;flex-wrap:wrap}
57
+ @media (max-width:980px){.modal{padding:16px}.controls .secondary,.controls select{flex:1 1 140px}}
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="container">
62
+ <header>
63
+ <div>
64
+ <h1>User Access Management</h1>
65
+ <div class="subtitle">Create portal users and assign one or more bots per user.</div>
66
+ </div>
67
+ <a class="nav-link" href="/portal/admin-bots.html">Bot Management</a>
68
+ </header>
69
+
70
+ <div class="status" id="status"></div>
71
+
72
+ <section class="card">
73
+ <div class="controls">
74
+ <button class="primary" id="newUserBtn">+ New User</button>
75
+ <button class="secondary" id="refreshBtn">Refresh</button>
76
+ <input id="searchInput" type="text" placeholder="Search username or bot" />
77
+ <select id="activeFilter">
78
+ <option value="all">All Users</option>
79
+ <option value="active">Active Only</option>
80
+ <option value="inactive">Inactive Only</option>
81
+ </select>
82
+ <select id="pageSize">
83
+ <option value="5">5 / page</option>
84
+ <option value="10" selected>10 / page</option>
85
+ <option value="20">20 / page</option>
86
+ <option value="50">50 / page</option>
87
+ </select>
88
+ </div>
89
+ <div class="table-shell">
90
+ <table>
91
+ <thead>
92
+ <tr>
93
+ <th>Username</th>
94
+ <th>Active</th>
95
+ <th>Bots</th>
96
+ <th>Actions</th>
97
+ </tr>
98
+ </thead>
99
+ <tbody id="usersTableBody"></tbody>
100
+ </table>
101
+ </div>
102
+ <div class="pager">
103
+ <div id="pagerInfo"></div>
104
+ <div>
105
+ <button class="secondary" id="prevPageBtn">Previous</button>
106
+ <span id="pageIndicator" style="padding:0 8px;">Page 1 of 1</span>
107
+ <button class="secondary" id="nextPageBtn">Next</button>
108
+ </div>
109
+ </div>
110
+ </section>
111
+ </div>
112
+
113
+ <div id="modalBackdrop" class="modal-backdrop" aria-hidden="true">
114
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="formTitle">
115
+ <div class="modal-head">
116
+ <div>
117
+ <h2 id="formTitle" style="margin:0;font-size:20px;">New User</h2>
118
+ <div class="muted" style="margin-top:4px;">Use the modal to add or edit user records.</div>
119
+ </div>
120
+ <button class="modal-close" id="closeModalBtn" type="button">Close</button>
121
+ </div>
122
+
123
+ <div class="form-group">
124
+ <label for="username">Username *</label>
125
+ <input id="username" type="text" autocomplete="off" />
126
+ </div>
127
+ <div class="form-group">
128
+ <label for="password">Password <span id="passwordHint" class="muted">*</span></label>
129
+ <input id="password" type="password" autocomplete="new-password" />
130
+ </div>
131
+ <div class="form-group checkbox-row">
132
+ <input id="isActive" type="checkbox" checked />
133
+ <label for="isActive" style="margin:0;">User Active</label>
134
+ </div>
135
+ <div class="form-group">
136
+ <label>Bot Access</label>
137
+ <div id="botChecklist" class="bot-list"></div>
138
+ <div class="muted" style="margin-top:6px;">Select multiple bots this user can access.</div>
139
+ </div>
140
+ <div class="modal-actions">
141
+ <button class="primary" id="saveBtn">Save User</button>
142
+ <button class="secondary" id="cancelBtn">Cancel</button>
143
+ <button class="danger" id="deleteBtn" style="display:none;">Delete User</button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ <script src="admin-users.js"></script>
148
+ </body>
149
+ </html>