Download templates/admin_users.html from ItsBounvy/bot_host: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/ItsBounvy/bot_host/resolve/main/templates/admin_users.html
- Command line
-
hf download hf://spaces/ItsBounvy/bot_host/templates/admin_users.html
-
curl -L -o admin_users.html https://huggingface.co/spaces/ItsBounvy/bot_host/resolve/main/templates/admin_users.html
13.8 kB
| {% extends "base.html" %} | |
| {% block title %}{{ _("Users") }}{% endblock %} | |
| {% block content %} | |
| <div class="page-header"> | |
| <div> | |
| <h1>{{ _("Users") }}</h1> | |
| <div class="subtitle">{{ users|length }} user{{ '' if users|length == 1 else 's' }} in the panel.</div> | |
| </div> | |
| <div style="display: flex; gap: 8px;"> | |
| <a href="/api/admin/users" target="_blank" class="btn btn-sm" | |
| style="background: var(--surface-2); color: var(--text-1);"> | |
| {{ panel_icon('cpu')|safe }} JSON | |
| </a> | |
| </div> | |
| </div> | |
| {# ============== Bulk action bar ============== #} | |
| <form method="POST" action="/admin/users/bulk/plan" id="bulk-form" | |
| style="margin-bottom: 18px;"> | |
| <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | |
| <input type="hidden" name="user_ids" id="bulk-user-ids" value=""> | |
| <input type="hidden" name="preset" id="bulk-preset" value=""> | |
| <div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 12px 14px; | |
| background: var(--surface-2); border-radius: 10px; border: 1px solid var(--border);"> | |
| <label style="display:flex;align-items:center;gap:8px;"> | |
| <input type="checkbox" id="select-all"> | |
| <strong>{{ _("Select") }}</strong> | |
| <span class="muted" id="selected-count">0</span> | |
| </label> | |
| <span class="muted">|</span> | |
| <span class="muted">{{ _("Apply plan preset to selection:") }}</span> | |
| {% for key, p in [ | |
| ('free', {'label': 'Free'}), | |
| ('trial', {'label': 'Trial'}), | |
| ('std', {'label': 'Standard'}), | |
| ('pro', {'label': 'Pro'}), | |
| ('biz', {'label': 'Business'}), | |
| ('unlim', {'label': 'Unlimited'}), | |
| ] %} | |
| <button type="button" class="btn btn-sm bulk-preset-btn" | |
| data-preset="{{ key }}" | |
| style="background: var(--surface); color: var(--text-1);"> | |
| {{ p.label }} | |
| </button> | |
| {% endfor %} | |
| </div> | |
| </form> | |
| {# flash messages #} | |
| {% if request.query_params.get('bulk') == 'ok' %} | |
| <div class="card" style="border-left: 4px solid var(--success); padding: 12px 16px; margin-bottom: 16px;"> | |
| <strong>{{ _("Bulk plan applied") }}</strong> | |
| <span class="muted">— {{ _("preset") }} <code>{{ request.query_params.get('preset') }}</code> | |
| {{ _("to") }} {{ request.query_params.get('count') }} {{ _("user(s)") }}.</span> | |
| </div> | |
| {% endif %} | |
| <div class="toolbar" data-toolbar="#users-table"> | |
| <div class="search"> | |
| <span class="ico">{{ panel_icon('search')|safe }}</span> | |
| <input type="search" placeholder="Search by username, email, or role…"> | |
| </div> | |
| <div class="filter-chips"> | |
| <button type="button" class="chip active" data-filter="all">All <span class="count">{{ users|length }}</span></button> | |
| <button type="button" class="chip" data-filter="admin">Admin <span class="count">{{ users|selectattr('role.value', 'equalto', 'admin')|list|length }}</span></button> | |
| <button type="button" class="chip" data-filter="operator">Operator <span class="count">{{ users|selectattr('role.value', 'equalto', 'operator')|list|length }}</span></button> | |
| <button type="button" class="chip" data-filter="viewer">Viewer <span class="count">{{ users|selectattr('role.value', 'equalto', 'viewer')|list|length }}</span></button> | |
| <button type="button" class="chip" data-filter="disabled">Disabled <span class="count">{{ users|selectattr('is_active', 'equalto', false)|list|length }}</span></button> | |
| </div> | |
| </div> | |
| <div class="card flush"> | |
| <div class="table-wrap" style="border: 0; border-radius: 0;"> | |
| <table class="table" id="users-table"> | |
| <thead> | |
| <tr> | |
| <th style="width: 32px;"></th> | |
| <th>User</th> | |
| <th>Email</th> | |
| <th>Role</th> | |
| <th>Plan <span class="muted" style="font-weight: 400; font-size: 11px;">(CPU · RAM MB · Disk MB · Max bots)</span></th> | |
| <th>Status</th> | |
| <th>Last login</th> | |
| <th>Created</th> | |
| <th class="actions-cell">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for u in users %} | |
| <tr data-status="{{ 'disabled' if not u.is_active else u.role.value }}" data-search="{{ u.username }} {{ u.email or '' }} {{ u.role.value }}"> | |
| <td> | |
| <input type="checkbox" class="user-checkbox" data-user-id="{{ u.id }}"> | |
| </td> | |
| <td> | |
| <div style="display: flex; align-items: center; gap: 10px;"> | |
| <div class="avatar" style="width: 30px; height: 30px; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--accent-2)); display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 12px; flex: 0 0 30px;"> | |
| {{ u.username[:2]|upper }} | |
| </div> | |
| <div> | |
| <a href="/admin/users/{{ u.id }}" style="font-weight: 600; color: var(--text-1); text-decoration: none;">{{ u.username }}</a> | |
| <div class="muted mono" style="font-size: 11px;">#{{ u.id }}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td>{{ u.email or '—' }}</td> | |
| <td> | |
| <form method="POST" action="/admin/users/{{ u.id }}/role" class="inline"> | |
| <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | |
| <select name="role" onchange="this.form.submit()" class="role-select" data-role="{{ u.role.value }}"> | |
| <option value="admin" {% if u.role.value == 'admin' %}selected{% endif %}>admin</option> | |
| <option value="operator" {% if u.role.value == 'operator' %}selected{% endif %}>operator</option> | |
| <option value="viewer" {% if u.role.value == 'viewer' %}selected{% endif %}>viewer</option> | |
| </select> | |
| </form> | |
| </td> | |
| <td> | |
| {% set us = usage.get(u.id) %} | |
| <form method="POST" action="/admin/users/{{ u.id }}/plan" | |
| class="inline plan-form" | |
| data-confirm="Update plan for {{ u.username }}?" | |
| style="display: flex; gap: 4px; align-items: center; flex-wrap: nowrap;"> | |
| <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | |
| <input type="number" name="cpu_cores" value="{{ u.cpu_quota_cores }}" step="0.05" min="0" max="16" | |
| style="width: 64px; padding: 4px 6px; font-size: 12px;" | |
| title="Total CPU cores across all this user's bot Spaces · used {{ us.cpu_alloc_cores if us else 0 }}"> | |
| <span class="muted">·</span> | |
| <input type="number" name="ram_mb" value="{{ u.ram_quota_mb }}" step="64" min="0" max="65536" | |
| style="width: 72px; padding: 4px 6px; font-size: 12px;" | |
| title="Total RAM MB · used {{ us.ram_alloc_mb if us else 0 }}"> | |
| <span class="muted">·</span> | |
| <input type="number" name="storage_mb" value="{{ u.storage_quota_mb }}" step="256" min="0" max="1048576" | |
| style="width: 80px; padding: 4px 6px; font-size: 12px;" | |
| title="HF Dataset storage cap · used {{ us.storage_used_mb if us else 0 }} MB"> | |
| <span class="muted">·</span> | |
| <input type="number" name="max_bots" value="{{ u.plan_max_bots }}" step="1" min="0" max="1000" | |
| style="width: 56px; padding: 4px 6px; font-size: 12px;" | |
| title="Max bots · current {{ us.bots_total if us else 0 }}"> | |
| <button type="submit" class="btn btn-sm btn-secondary" title="Save plan">{{ panel_icon('check')|safe }}</button> | |
| </form> | |
| {% if us %} | |
| <div style="display:flex;gap:6px;margin-top:4px;font-size:10px;color:var(--text-muted);align-items:center;"> | |
| <span title="CPU used" style="display:flex;align-items:center;gap:3px;"> | |
| CPU <strong style="color:{% if us.cpu_percent > 100 %}var(--error){% elif us.cpu_percent > 80 %}var(--warning){% else %}var(--text-1){% endif %};">{{ us.cpu_percent }}%</strong> | |
| </span> | |
| <span title="RAM used" style="display:flex;align-items:center;gap:3px;"> | |
| RAM <strong style="color:{% if us.ram_percent > 100 %}var(--error){% elif us.ram_percent > 80 %}var(--warning){% else %}var(--text-1){% endif %};">{{ us.ram_percent }}%</strong> | |
| </span> | |
| <span title="Storage used" style="display:flex;align-items:center;gap:3px;"> | |
| Disk <strong style="color:{% if us.storage_percent > 100 %}var(--error){% elif us.storage_percent > 80 %}var(--warning){% else %}var(--text-1){% endif %};">{{ us.storage_percent }}%</strong> | |
| </span> | |
| {% if us.bots_at_max %} | |
| <span class="badge tone-warning" style="font-size: 10px;">{{ _("at cap") }}</span> | |
| {% endif %} | |
| </div> | |
| {% endif %} | |
| </td> | |
| <td> | |
| {% if u.is_active %} | |
| <span class="badge status-success"><span class="dot"></span> Active</span> | |
| {% else %} | |
| <span class="badge status-error"><span class="dot"></span> Disabled</span> | |
| {% endif %} | |
| </td> | |
| <td><span data-time="{{ u.last_login.isoformat() if u.last_login else '' }}">{{ u.last_login.strftime('%Y-%m-%d %H:%M') if u.last_login else 'Never' }}</span></td> | |
| <td><span data-time="{{ u.created_at.isoformat() if u.created_at else '' }}">{{ u.created_at.strftime('%Y-%m-%d') if u.created_at else '—' }}</span></td> | |
| <td class="actions-cell"> | |
| <div class="row-actions"> | |
| <a href="/admin/users/{{ u.id }}" class="btn btn-sm" | |
| style="background: var(--surface-2); color: var(--text-1);" | |
| title="{{ _('Open user detail') }}"> | |
| {{ panel_icon('ext')|safe }} | |
| </a> | |
| <form method="POST" action="/admin/users/{{ u.id }}/toggle" class="inline" data-confirm="{{ 'Disable' if u.is_active else 'Enable' }} user {{ u.username }}?"> | |
| <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | |
| <button type="submit" class="btn btn-sm {% if u.is_active %}btn-danger{% else %}btn-success{% endif %}"> | |
| {% if u.is_active %}{{ panel_icon('stop')|safe }} Disable{% else %}{{ panel_icon('check')|safe }} Enable{% endif %} | |
| </button> | |
| </form> | |
| </div> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <script> | |
| // ----- Bulk select ----- | |
| const form = document.getElementById('bulk-form'); | |
| const idsInput = document.getElementById('bulk-user-ids'); | |
| const presetInput = document.getElementById('bulk-preset'); | |
| const counter = document.getElementById('selected-count'); | |
| const selectAll = document.getElementById('select-all'); | |
| function recomputeSelection() { | |
| const checks = Array.from(document.querySelectorAll('.user-checkbox')); | |
| const checked = checks.filter(c => c.checked).map(c => c.dataset.userId); | |
| idsInput.value = checked.join(','); | |
| counter.textContent = `${checked.length} selected`; | |
| // Master checkbox state | |
| if (selectAll) { | |
| selectAll.indeterminate = checked.length > 0 && checked.length < checks.length; | |
| selectAll.checked = checked.length === checks.length && checks.length > 0; | |
| } | |
| } | |
| document.querySelectorAll('.user-checkbox').forEach(cb => { | |
| cb.addEventListener('change', recomputeSelection); | |
| }); | |
| if (selectAll) { | |
| selectAll.addEventListener('change', () => { | |
| document.querySelectorAll('.user-checkbox').forEach(cb => { | |
| cb.checked = selectAll.checked; | |
| }); | |
| recomputeSelection(); | |
| }); | |
| } | |
| document.querySelectorAll('.bulk-preset-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const checked = idsInput.value; | |
| if (!checked) { | |
| alert('{{ _("Select users first.") }}'); | |
| return; | |
| } | |
| const preset = btn.dataset.preset; | |
| const label = btn.textContent.trim(); | |
| if (!confirm(`{{ _("Apply preset \\u00BB{label}\\u00AB to {n} user(s)?")|e }}` | |
| .replace('{label}', label) | |
| .replace('{n}', checked.split(',').length))) { | |
| return; | |
| } | |
| presetInput.value = preset; | |
| form.submit(); | |
| }); | |
| }); | |
| // Inline confirm on plan-form submit — keep this from the original | |
| // implementation so the existing UX (modal) is preserved. | |
| document.querySelectorAll('form.plan-form').forEach(function (f) { | |
| f.addEventListener('submit', function (e) { | |
| if (!confirm(f.dataset.confirm || 'Update plan?')) { | |
| e.preventDefault(); | |
| } | |
| }); | |
| }); | |
| </script> | |
| {% endblock %} |