bot_host / templates /admin_users.html
ItsBounvy's picture
Upload 21 files
4272701 verified
Raw History Blame Contribute Delete
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 %}