bot_host / templates /base.html
ItsBounvy's picture
deploy: english UI + admin creds + dataset wiring
041cd0c verified
Raw History Blame Contribute Delete
17.3 kB
{# ============================================================
MMaxAI Hosting Panel — base layout (Copilot style)
------------------------------------------------------------
New shell: persistent sidebar + sticky topbar + cmd-bar.
Loads panel.css for legacy components (.card, .btn, .badge,
.form-*, .table) and copilot.css for the new layout tokens.
============================================================ #}
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#0a0e1c">
<title>{% block title %}{{ title }}{% endblock %} · {{ panel_title or "MMaxAI Hosting" }}</title>
<link rel="stylesheet" href="/static/css/panel.css?v={{ panel_assets_v() }}">
<link rel="stylesheet" href="/static/css/copilot.css?v={{ panel_assets_v() }}">
<link rel="stylesheet" href="/static/css/ai_chat.css?v={{ panel_assets_v() }}">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎛️</text></svg>">
</head>
<body class="has-copilot">
{% if user %}
<div class="shell">
{# ---------------- Sidebar ---------------- #}
<aside class="sidebar" id="sidebar">
<div class="sidebar-head">
<a href="/dashboard" class="brand">
<div class="logo">M</div>
MMaxAI
</a>
</div>
<nav class="sidebar-nav">
<div class="sidebar-section-label">{{ _("Main") }}</div>
<a href="/dashboard" class="nav-item {% if request.url.path == '/dashboard' %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('dashboard')|safe }}</span> {{ _("Dashboard") }}
</a>
<a href="/bots" class="nav-item {% if request.url.path.startswith('/bots') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('bot')|safe }}</span> {{ _("Bots") }}
</a>
{% if user.role == 'admin' %}
<div class="sidebar-section-label">{{ _("Admin") }}</div>
<a href="/admin/users" class="nav-item {% if request.url.path.startswith('/admin/users') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('users')|safe }}</span> {{ _("Users") }}
</a>
<a href="/admin/invites" class="nav-item {% if request.url.path.startswith('/admin/invites') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('key')|safe }}</span> {{ _("Invite codes") }}
</a>
<a href="/admin/stats" class="nav-item {% if request.url.path.startswith('/admin/stats') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('cpu')|safe }}</span> {{ _("Statistics") }}
</a>
<a href="/admin/metrics" class="nav-item {% if request.url.path.startswith('/admin/metrics') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('chart')|safe }}</span> {{ _("Metrics") }}
</a>
<a href="/admin/alerts" class="nav-item {% if request.url.path.startswith('/admin/alerts') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('bell')|safe }}</span> {{ _("Alerts") }}
</a>
<a href="/admin/secrets" class="nav-item {% if request.url.path.startswith('/admin/secrets') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('shield')|safe }}</span> {{ _("Secrets") }}
</a>
<a href="/admin/banners" class="nav-item {% if request.url.path.startswith('/admin/banners') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('banner')|safe }}</span> {{ _("Banners") }}
</a>
<a href="/admin/audit" class="nav-item {% if request.url.path.startswith('/admin/audit') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('audit')|safe }}</span> {{ _("Audit") }}
</a>
{% endif %}
{% if user.role == 'admin' %}
<a href="/admin/control" class="nav-item {% if request.url.path.startswith('/admin/control') %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('shield')|safe }}</span> {{ _("Admin control") }}
</a>
{% endif %}
<div class="sidebar-section-label">{{ _("Account") }}</div>
<a href="/profile" class="nav-item {% if request.url.path == '/profile' %}active{% endif %}">
<span class="nav-icon">{{ panel_icon('user')|safe }}</span> {{ _("Profile") }}
</a>
<a href="/logout" class="nav-item" data-logout>
<span class="nav-icon">{{ panel_icon('logout')|safe }}</span> {{ _("Sign out") }}
</a>
</nav>
<div class="sidebar-footer">
<a href="/profile" class="profile">
<div class="avatar">{{ user.username[:2]|upper }}</div>
<div class="meta">
<div class="name">{{ user.username }}</div>
<div class="role">{{ user.role }}</div>
</div>
</a>
<button type="button" class="icon-btn menu-toggle" aria-label="Close menu">{{ panel_icon('x')|safe }}</button>
</div>
</aside>
<div class="scrim"></div>
{# ---------------- Main wrap ---------------- #}
<div class="main-wrap">
<header class="topbar">
<button class="menu-toggle icon-btn" aria-label="Open menu" id="menu-open">{{ panel_icon('menu')|safe }}</button>
<div class="crumbs">
{% block crumbs %}
<span>Workspace</span>
<span class="sep">/</span>
<h1>{{ title }}</h1>
{% endblock %}
</div>
{% block topbar_extra %}{% endblock %}
<div class="spacer"></div>
<button class="search-trigger" id="cmd-trigger">
{{ panel_icon('search')|safe }}
<span class="label">Search bots, logs…</span>
<kbd>⌘K</kbd>
</button>
<button class="ai-trigger ai-trigger-primary" id="ai-trigger" type="button" title="{{ _('Ask the panel assistant') }}" aria-label="{{ _('Open AI assistant') }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="14" height="14">
<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/>
<path d="M5 3v4"/>
<path d="M19 17v4"/>
<path d="M3 5h4"/>
<path d="M17 19h4"/>
</svg>
<span class="label">{{ _("Ask AI") }}</span>
<kbd>/</kbd>
</button>
<div class="topbar-actions">
<button type="button" class="icon-btn lang-toggle" aria-label="Switch language" title="Switch language" id="lang-toggle-btn">
{{ panel_icon('globe')|safe }}
</button>
<button type="button" class="icon-btn" data-theme-toggle aria-label="Toggle theme" aria-pressed="false" title="Switch theme">
{{ panel_icon('moon')|safe }}
</button>
<a href="/profile" class="icon-btn" aria-label="Profile">{{ panel_icon('user')|safe }}</a>
<a href="/logout" class="icon-btn" data-logout aria-label="Sign out">{{ panel_icon('logout')|safe }}</a>
</div>
</header>
{% if request.state.plan_tier_outdated %}
<div class="card" id="plan-tier-outdated-banner"
style="margin: 16px 24px 0; padding: 14px 18px; border-left: 4px solid var(--accent, #6c8cff);
background: var(--surface-2, #1a1d2b); display:flex; align-items:center; gap:14px;
flex-wrap:wrap;">
<span style="font-size:20px;line-height:1;">&#9888;&#65039;</span>
<div style="flex:1; min-width:240px;">
<strong>{{ _("Site updated") }}</strong>
<span class="muted">{{ _("Your account was on an old plan. We have automatically moved you to the free tier.") }}</span>
</div>
<a href="https://t.me/helpforwacodebot" target="_blank" rel="noopener"
class="btn btn-sm"
style="background: var(--accent, #6c8cff); color: #fff; padding: 6px 12px; border-radius: 6px; text-decoration: none; font-weight: 600;">
{{ _("Need help? Telegram") }} @helpforwacodebot
</a>
</div>
{% endif %}
<div class="content">
{% block content %}{% endblock %}
</div>
</div>
</div>
{# ---------------- Language switcher dropdown ---------------- #}
<div class="lang-dropdown" id="lang-dropdown" style="display: none; position: absolute; top: 52px; right: 160px; z-index: 1000; background: var(--surface, #1a1d2b); border: 1px solid var(--border, #2a2d3a); border-radius: 10px; box-shadow: var(--shadow-lg, 0 10px 40px rgba(0,0,0,.4)); min-width: 160px; padding: 6px;">
{% for lang in available_languages() %}
<a href="/lang?lang={{ lang.code }}&next_url={{ request.url.path }}"
class="lang-option {% if request.cookies.get('panel_lang', _detect_lang()) == lang.code %}active{% endif %}"
style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; color: var(--text); text-decoration: none; font-size: 13px; transition: background 120ms ease;">
<span style="width: 20px; text-align: center;">{{ lang.name }}</span>
</a>
{% endfor %}
</div>
{% else %}
{# ---------------- Logged-out fallback ---------------- #}
{# Public pages (login/register) do NOT extend base.html — they have their
own full document. This branch only fires if a child template was
wrongly rendered for an unauthenticated request. Render an empty
shell so the child template's `{% block content %}` override still works
without nesting. Note: jinja forbids defining the same block twice
in one template, even across mutually-exclusive branches — so the
fallback here uses a distinct ``public_content`` block name. #}
<main class="main full">
<div class="content">
{% block public_content %}{% endblock %}
</div>
</main>
{% endif %}
<div class="toast-container" id="toasts"></div>
{# ---------------- Flash messages ---------------- #}
{# Server queues one-shot toasts in the ``panel_flash`` cookie via
``_flash_set`` in routes.py. ``_flash_middleware`` in app.py pops
the cookie on EVERY request — not just /login and /register — and
stuffs the decoded payload on ``request.state.flashes``. Any
template that extends ``base.html`` therefore gets the toast
regardless of where the POST->302 landed (e.g. POST /login
redirects to /dashboard, which used to silently miss the
"Welcome back!" toast). ``panel_flash.js`` reads the array and
renders toasts top-right with the upgraded animations. #}
<script>
window.PANEL_FLASHES = {{ panel_flashes() | tojson }};
</script>
<script src="/static/js/panel_flash.js?v={{ panel_assets_v() }}" defer></script>
{# ---------------- AI chat overlay ---------------- #}
<div class="ai-overlay" id="ai-overlay" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="ai-overlay-title">
<div class="ai-backdrop" data-ai-close></div>
<div class="ai-panel">
<header class="ai-head">
<div class="ai-head-left">
<svg class="ai-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="18" height="18">
<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/>
<path d="M5 3v4"/>
<path d="M19 17v4"/>
<path d="M3 5h4"/>
<path d="M17 19h4"/>
</svg>
<div>
<div id="ai-overlay-title" class="ai-title">{{ _("Panel assistant") }}</div>
<div class="ai-sub" id="ai-overlay-sub">{{ _("Powered by NVIDIA Nemotron") }}</div>
</div>
</div>
<div class="ai-head-right">
<span class="ai-model-tag" id="ai-model-tag" hidden></span>
<button class="ai-icon" type="button" data-ai-clear title="Clear conversation" aria-label="Clear conversation">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16"><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></svg>
</button>
<button class="ai-icon" type="button" data-ai-close title="Close" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</header>
<div class="ai-transcript" id="ai-transcript" aria-live="polite">
<div class="ai-msg ai-msg-assistant" data-empty>
<div class="ai-bubble">
{{ _("Ask me about bots, deploys, logs — or just say \u201cwhat does this panel do?\u201d.") | safe }}
<div class="ai-hint">{{ _("Try:") }} <button type="button" class="ai-quick" data-ai-quick="{{ _('how do I deploy a bot?') }}">{{ _("how do I deploy a bot?") }}</button> <button type="button" class="ai-quick" data-ai-quick="{{ _('what to check when a bot is in ERROR?') }}">{{ _("what to check when a bot is in ERROR?") }}</button> <button type="button" class="ai-quick" data-ai-quick="{{ _('explain the quota system') }}">{{ _("explain the quota system") }}</button></div>
</div>
</div>
</div>
<form class="ai-form" id="ai-form" autocomplete="off">
<textarea id="ai-input" name="message" rows="1" placeholder="{{ _('Ask anything… (Enter — send, Shift+Enter — newline, Esc — close)') }}" maxlength="8000"></textarea>
<button type="submit" class="ai-send" id="ai-send" title="Send" aria-label="Send">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="14" height="14"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</button>
</form>
</div>
</div>
<script src="/static/js/panel.js?v={{ panel_assets_v() }}" defer></script>
<script src="/static/js/ai_chat.js?v={{ panel_assets_v() }}" defer></script>
<script>
// Language switcher dropdown
(function () {
var btn = document.getElementById('lang-toggle-btn');
var dropdown = document.getElementById('lang-dropdown');
if (!btn || !dropdown) return;
btn.addEventListener('click', function (e) {
e.stopPropagation();
var visible = dropdown.style.display === 'none';
dropdown.style.display = visible ? 'block' : 'none';
});
document.addEventListener('click', function () {
dropdown.style.display = 'none';
});
dropdown.addEventListener('click', function (e) {
e.stopPropagation();
});
})();
</script>
{% block scripts %}{% endblock %}
</body>
</html>