ItsBounvy commited on
Commit
e175e22
·
verified ·
1 Parent(s): 8956378

Upload 18 files

Browse files
Files changed (3) hide show
  1. templates/base.html +88 -43
  2. templates/dashboard.html +218 -213
  3. templates/login.html +538 -88
templates/base.html CHANGED
@@ -1,123 +1,168 @@
1
  {# ============================================================
2
- MMaxAI Hosting Panel — base layout
3
  ------------------------------------------------------------
4
- Used by every authenticated page. Auth pages (login /
5
- register) extend from their own minimal layout instead.
 
6
  ============================================================ #}
7
  <!DOCTYPE html>
8
- <html lang="en">
9
  <head>
10
  <meta charset="UTF-8">
11
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
  <meta name="color-scheme" content="dark">
13
- <meta name="theme-color" content="#0b1020">
14
  <title>{% block title %}{{ title }}{% endblock %} · {{ panel_title or "MMaxAI Hosting" }}</title>
15
  <link rel="stylesheet" href="/static/css/panel.css">
 
16
  <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>">
17
  </head>
18
- <body class="has-gradient">
19
- <div class="app">
20
- {% if user %}
 
21
  <aside class="sidebar" id="sidebar">
22
- <div class="sidebar-header">
23
- <div class="logo">M</div>
24
- <div>
25
- <div class="brand">{{ panel_title or "MMaxAI Hosting" }}</div>
26
- <div class="brand-sub">Hosting Panel</div>
27
- </div>
28
  </div>
29
 
30
  <nav class="sidebar-nav">
31
- <div class="nav-section">Main</div>
32
  <a href="/dashboard" class="nav-item {% if request.url.path == '/dashboard' %}active{% endif %}">
33
  <span class="nav-icon">{{ panel_icon('dashboard')|safe }}</span> Dashboard
34
  </a>
35
  <a href="/bots" class="nav-item {% if request.url.path.startswith('/bots') %}active{% endif %}">
36
- <span class="nav-icon">{{ panel_icon('bot')|safe }}</span> Bots
37
  </a>
38
 
39
  {% if user.role == 'admin' %}
40
- <div class="nav-section">Admin</div>
41
  <a href="/admin/users" class="nav-item {% if request.url.path.startswith('/admin/users') %}active{% endif %}">
42
- <span class="nav-icon">{{ panel_icon('users')|safe }}</span> Users
43
  </a>
44
  <a href="/admin/invites" class="nav-item {% if request.url.path.startswith('/admin/invites') %}active{% endif %}">
45
- <span class="nav-icon">{{ panel_icon('key')|safe }}</span> Invite codes
46
  </a>
47
  <a href="/admin/stats" class="nav-item {% if request.url.path.startswith('/admin/stats') %}active{% endif %}">
48
- <span class="nav-icon">{{ panel_icon('cpu')|safe }}</span> System stats
49
  </a>
50
  <a href="/admin/secrets" class="nav-item {% if request.url.path.startswith('/admin/secrets') %}active{% endif %}">
51
- <span class="nav-icon">{{ panel_icon('shield')|safe }}</span> Secrets panel
52
  </a>
53
  <a href="/admin/audit" class="nav-item {% if request.url.path.startswith('/admin/audit') %}active{% endif %}">
54
- <span class="nav-icon">{{ panel_icon('audit')|safe }}</span> Audit Log
55
  </a>
56
  {% endif %}
57
  <a href="/admin/control" class="nav-item {% if request.url.path.startswith('/admin/control') %}active{% endif %}">
58
  <span class="nav-icon">{{ panel_icon('shield')|safe }}</span> Admin control
59
  </a>
60
 
61
- <div class="nav-section">Account</div>
62
  <a href="/profile" class="nav-item {% if request.url.path == '/profile' %}active{% endif %}">
63
- <span class="nav-icon">{{ panel_icon('user')|safe }}</span> Profile
64
  </a>
65
  <a href="/logout" class="nav-item" data-logout>
66
- <span class="nav-icon">{{ panel_icon('logout')|safe }}</span> Sign out
67
  </a>
68
  </nav>
69
 
70
  <div class="sidebar-footer">
71
- <a href="/profile" class="user-chip">
72
  <div class="avatar">{{ user.username[:2]|upper }}</div>
73
- <div class="user-meta">
74
  <div class="name">{{ user.username }}</div>
75
  <div class="role">{{ user.role }}</div>
76
  </div>
77
  </a>
 
78
  </div>
79
  </aside>
80
  <div class="scrim"></div>
81
- {% endif %}
82
 
83
- <main class="main {% if not user %}full{% endif %}">
84
- {% if user %}
85
  <header class="topbar">
86
- <button class="icon-btn menu-toggle" aria-label="Open menu">{{ panel_icon('menu')|safe }}</button>
87
  <div class="crumbs">
88
  {% block crumbs %}
89
- <span class="parent">Home</span>
90
  <span class="sep">/</span>
91
  <h1>{{ title }}</h1>
92
  {% endblock %}
93
  </div>
 
 
 
 
 
 
 
 
 
 
 
94
  <div class="topbar-actions">
95
- <button type="button" class="icon-btn" data-theme-toggle aria-label="Toggle theme" aria-pressed="false">
96
- <span data-theme-icon="dark">{{ panel_icon('moon')|safe }}</span>
97
  </button>
98
  <a href="/profile" class="icon-btn" aria-label="Profile">{{ panel_icon('user')|safe }}</a>
99
  <a href="/logout" class="icon-btn" data-logout aria-label="Sign out">{{ panel_icon('logout')|safe }}</a>
100
  </div>
101
  </header>
102
- {% endif %}
103
 
104
  <div class="content">
105
  {% block content %}{% endblock %}
106
  </div>
107
- </main>
108
  </div>
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  <div class="toast-container" id="toasts"></div>
111
 
 
112
  <script>
113
- // Logout confirmation
114
- document.querySelectorAll('a[data-logout]').forEach(function (el) {
115
- el.addEventListener('click', function (e) {
116
- if (!confirm('Sign out of the panel?')) e.preventDefault();
117
- });
118
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  </script>
120
- <script src="/static/js/panel.js" defer></script>
121
  {% block scripts %}{% endblock %}
122
  </body>
123
  </html>
 
1
  {# ============================================================
2
+ MMaxAI Hosting Panel — base layout (Copilot style)
3
  ------------------------------------------------------------
4
+ New shell: persistent sidebar + sticky topbar + cmd-bar.
5
+ Loads panel.css for legacy components (.card, .btn, .badge,
6
+ .form-*, .table) and copilot.css for the new layout tokens.
7
  ============================================================ #}
8
  <!DOCTYPE html>
9
+ <html lang="en" data-theme="dark">
10
  <head>
11
  <meta charset="UTF-8">
12
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
  <meta name="color-scheme" content="dark">
14
+ <meta name="theme-color" content="#0a0e1c">
15
  <title>{% block title %}{{ title }}{% endblock %} · {{ panel_title or "MMaxAI Hosting" }}</title>
16
  <link rel="stylesheet" href="/static/css/panel.css">
17
+ <link rel="stylesheet" href="/static/css/copilot.css">
18
  <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>">
19
  </head>
20
+ <body class="has-copilot">
21
+ {% if user %}
22
+ <div class="shell">
23
+ {# ---------------- Sidebar ---------------- #}
24
  <aside class="sidebar" id="sidebar">
25
+ <div class="sidebar-head">
26
+ <a href="/dashboard" class="brand">
27
+ <div class="logo">M</div>
28
+ MMaxAI
29
+ </a>
 
30
  </div>
31
 
32
  <nav class="sidebar-nav">
33
+ <div class="sidebar-section-label">Main</div>
34
  <a href="/dashboard" class="nav-item {% if request.url.path == '/dashboard' %}active{% endif %}">
35
  <span class="nav-icon">{{ panel_icon('dashboard')|safe }}</span> Dashboard
36
  </a>
37
  <a href="/bots" class="nav-item {% if request.url.path.startswith('/bots') %}active{% endif %}">
38
+ <span class="nav-icon">{{ panel_icon('bot')|safe }}</span> Боти
39
  </a>
40
 
41
  {% if user.role == 'admin' %}
42
+ <div class="sidebar-section-label">Admin</div>
43
  <a href="/admin/users" class="nav-item {% if request.url.path.startswith('/admin/users') %}active{% endif %}">
44
+ <span class="nav-icon">{{ panel_icon('users')|safe }}</span> Користувачі
45
  </a>
46
  <a href="/admin/invites" class="nav-item {% if request.url.path.startswith('/admin/invites') %}active{% endif %}">
47
+ <span class="nav-icon">{{ panel_icon('key')|safe }}</span> Інвайт-коди
48
  </a>
49
  <a href="/admin/stats" class="nav-item {% if request.url.path.startswith('/admin/stats') %}active{% endif %}">
50
+ <span class="nav-icon">{{ panel_icon('cpu')|safe }}</span> Статистика
51
  </a>
52
  <a href="/admin/secrets" class="nav-item {% if request.url.path.startswith('/admin/secrets') %}active{% endif %}">
53
+ <span class="nav-icon">{{ panel_icon('shield')|safe }}</span> Секрети
54
  </a>
55
  <a href="/admin/audit" class="nav-item {% if request.url.path.startswith('/admin/audit') %}active{% endif %}">
56
+ <span class="nav-icon">{{ panel_icon('audit')|safe }}</span> Аудит
57
  </a>
58
  {% endif %}
59
  <a href="/admin/control" class="nav-item {% if request.url.path.startswith('/admin/control') %}active{% endif %}">
60
  <span class="nav-icon">{{ panel_icon('shield')|safe }}</span> Admin control
61
  </a>
62
 
63
+ <div class="sidebar-section-label">Account</div>
64
  <a href="/profile" class="nav-item {% if request.url.path == '/profile' %}active{% endif %}">
65
+ <span class="nav-icon">{{ panel_icon('user')|safe }}</span> Профіль
66
  </a>
67
  <a href="/logout" class="nav-item" data-logout>
68
+ <span class="nav-icon">{{ panel_icon('logout')|safe }}</span> Вийти
69
  </a>
70
  </nav>
71
 
72
  <div class="sidebar-footer">
73
+ <a href="/profile" class="profile">
74
  <div class="avatar">{{ user.username[:2]|upper }}</div>
75
+ <div class="meta">
76
  <div class="name">{{ user.username }}</div>
77
  <div class="role">{{ user.role }}</div>
78
  </div>
79
  </a>
80
+ <button type="button" class="icon-btn menu-toggle" aria-label="Close menu">{{ panel_icon('x')|safe }}</button>
81
  </div>
82
  </aside>
83
  <div class="scrim"></div>
 
84
 
85
+ {# ---------------- Main wrap ---------------- #}
86
+ <div class="main-wrap">
87
  <header class="topbar">
88
+ <button class="menu-toggle icon-btn" aria-label="Open menu" id="menu-open">{{ panel_icon('menu')|safe }}</button>
89
  <div class="crumbs">
90
  {% block crumbs %}
91
+ <span>Workspace</span>
92
  <span class="sep">/</span>
93
  <h1>{{ title }}</h1>
94
  {% endblock %}
95
  </div>
96
+
97
+ {% block topbar_extra %}{% endblock %}
98
+
99
+ <div class="spacer"></div>
100
+
101
+ <button class="search-trigger" id="cmd-trigger">
102
+ {{ panel_icon('search')|safe }}
103
+ <span class="label">Пошук ботів, логів…</span>
104
+ <kbd>⌘K</kbd>
105
+ </button>
106
+
107
  <div class="topbar-actions">
108
+ <button type="button" class="icon-btn" data-theme-toggle aria-label="Toggle theme" aria-pressed="false" title="Switch theme">
109
+ {{ panel_icon('moon')|safe }}
110
  </button>
111
  <a href="/profile" class="icon-btn" aria-label="Profile">{{ panel_icon('user')|safe }}</a>
112
  <a href="/logout" class="icon-btn" data-logout aria-label="Sign out">{{ panel_icon('logout')|safe }}</a>
113
  </div>
114
  </header>
 
115
 
116
  <div class="content">
117
  {% block content %}{% endblock %}
118
  </div>
119
+ </div>
120
  </div>
121
 
122
+ {# ---------------- Floating command palette ---------------- #}
123
+ <div class="cmd-bar" id="cmd-bar">
124
+ <button class="cmd-add" aria-label="Quick action">{{ panel_icon('plus')|safe }}</button>
125
+ <input type="text" placeholder="Команда, пошук або AI-запит…" id="cmd-input" />
126
+ <span class="cmd-kbd"><kbd>⌘</kbd><kbd>K</kbd></span>
127
+ <button class="cmd-send" type="button">
128
+ Надіслати
129
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
130
+ </button>
131
+ </div>
132
+
133
+ {% else %}
134
+ {# ---------------- Logged-out: full-width content ---------------- #}
135
+ <main class="main full">
136
+ <div class="content">
137
+ {% block content_logged_out %}{% block content %}{% endblock %}{% endblock %}
138
+ </div>
139
+ </main>
140
+ {% endif %}
141
+
142
  <div class="toast-container" id="toasts"></div>
143
 
144
+ <script src="/static/js/panel.js" defer></script>
145
  <script>
146
+ // Cmd+K → focus the floating command bar
147
+ (function () {
148
+ var trigger = document.getElementById('cmd-trigger');
149
+ var input = document.getElementById('cmd-input');
150
+ if (input) {
151
+ document.addEventListener('keydown', function (e) {
152
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
153
+ e.preventDefault();
154
+ input.focus();
155
+ }
156
+ if (e.key === 'Escape' && document.activeElement === input) {
157
+ input.blur();
158
+ }
159
+ });
160
+ }
161
+ if (trigger && input) {
162
+ trigger.addEventListener('click', function () { input.focus(); });
163
+ }
164
+ })();
165
  </script>
 
166
  {% block scripts %}{% endblock %}
167
  </body>
168
  </html>
templates/dashboard.html CHANGED
@@ -1,259 +1,264 @@
1
  {% extends "base.html" %}
2
- {% block title %}Dashboard{% endblock %}
3
  {% block content %}
4
- <div class="page-header">
5
- <div>
6
- <h1>Welcome back, {{ user.username }}</h1>
7
- <div class="subtitle">Here's what's happening across your fleet right now.</div>
8
- </div>
9
- <div class="actions">
10
- <a href="/bots" class="btn btn-outline">{{ panel_icon('list')|safe }} All bots</a>
11
- <a href="/bots/new" class="btn btn-primary">{{ panel_icon('plus')|safe }} New bot</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  </div>
13
- </div>
14
 
15
  {% if not vault_configured %}
16
- <div class="alert alert-warning">
17
  <span class="ico">{{ panel_icon('warning')|safe }}</span>
18
  <div class="alert-body">
19
- <div class="alert-title">Encryption not configured</div>
20
- Bot secrets are stored in plaintext. Set <code>PANEL_ENCRYPTION_KEY</code> in the environment to enable Fernet encryption at rest.
21
  </div>
22
  </div>
23
  {% endif %}
24
 
25
- <div class="stats-grid">
26
- <a href="/bots" class="stat-card tone-primary">
27
- <div class="stat-label">{{ panel_icon('bot')|safe }} Total bots</div>
 
 
 
 
 
 
 
28
  <div class="stat-value">{{ stats.total_bots }}</div>
29
- <div class="stat-foot">{{ stats.pending }} pending · {{ stats.stopped }} stopped</div>
30
  </a>
31
- <a href="/bots?status=running" class="stat-card tone-success">
32
- <div class="stat-label">{{ panel_icon('check')|safe }} Running</div>
 
 
 
 
 
 
 
33
  <div class="stat-value">{{ stats.running }}</div>
34
  <div class="stat-foot">{{ stats.sleeping }} sleeping</div>
35
  </a>
36
- <a href="/bots?status=building" class="stat-card tone-accent">
37
- <div class="stat-label">{{ panel_icon('rocket')|safe }} Building</div>
 
 
 
 
 
 
 
38
  <div class="stat-value">{{ stats.building }}</div>
39
- <div class="stat-foot">In deployment right now</div>
40
  </a>
41
- <a href="/bots?status=error" class="stat-card tone-error">
42
- <div class="stat-label">{{ panel_icon('warning')|safe }} Errors</div>
 
 
 
 
 
 
 
 
 
43
  <div class="stat-value">{{ stats.error }}</div>
44
- <div class="stat-foot">Needs attention</div>
45
  </a>
 
46
  {% if is_admin %}
47
- <a href="/admin/users" class="stat-card tone-primary">
48
- <div class="stat-label">{{ panel_icon('users')|safe }} Users</div>
 
 
 
 
 
 
49
  <div class="stat-value">{{ stats.users }}</div>
50
- <div class="stat-foot">Manage team</div>
51
  </a>
52
  {% endif %}
53
- </div>
54
 
55
- <div class="grid-2">
56
- <div class="card">
57
- <div class="card-header">
58
- <div>
59
- <h2>Quick actions</h2>
60
- <div class="desc">Start in seconds.</div>
61
- </div>
62
- </div>
63
- <div class="card-body">
64
- <div class="action-grid">
65
- <a href="/bots/new" class="action-tile">
66
- <div class="ico">{{ panel_icon('plus')|safe }}</div>
67
- <div class="label">New bot</div>
68
- <div class="hint">Provision a fresh HF Space</div>
69
- </a>
70
- <a href="/bots" class="action-tile">
71
- <div class="ico">{{ panel_icon('list')|safe }}</div>
72
- <div class="label">Manage bots</div>
73
- <div class="hint">View, edit, deploy, stop</div>
74
- </a>
75
- <a href="/profile" class="action-tile">
76
- <div class="ico">{{ panel_icon('key')|safe }}</div>
77
- <div class="label">Profile</div>
78
- <div class="hint">Password, invite code</div>
79
- </a>
80
- {% if is_admin %}
81
- <a href="/admin/audit" class="action-tile">
82
- <div class="ico">{{ panel_icon('audit')|safe }}</div>
83
- <div class="label">Audit log</div>
84
- <div class="hint">Full activity trail</div>
85
- </a>
86
- {% endif %}
87
- </div>
88
- </div>
89
- </div>
90
 
91
- <div class="card">
92
- <div class="card-header">
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  <div>
94
- <h2>Recent activity</h2>
95
- <div class="desc">Last events from the audit log.</div>
96
  </div>
97
- {% if is_admin %}<a href="/admin/audit" class="btn btn-sm btn-ghost">View all</a>{% endif %}
98
- </div>
99
- <div class="card-body">
100
- {% if recent_activity %}
101
- <div class="activity">
102
- {% for ev in recent_activity %}
103
- <div class="activity-item">
104
- <div class="ico">
105
- {% if 'login' in ev.action %}{{ panel_icon('key')|safe }}
106
- {% elif 'create' in ev.action or 'register' in ev.action %}{{ panel_icon('plus')|safe }}
107
- {% elif 'update' in ev.action %}{{ panel_icon('edit')|safe }}
108
- {% elif 'delete' in ev.action %}{{ panel_icon('trash')|safe }}
109
- {% elif 'deploy' in ev.action %}{{ panel_icon('rocket')|safe }}
110
- {% elif 'role' in ev.action %}{{ panel_icon('users')|safe }}
111
- {% else %}{{ panel_icon('info')|safe }}{% endif %}
112
- </div>
113
- <div class="body">
114
- <div class="who">
115
- {{ ev.user.username if ev.user else 'system' }}
116
- <span class="text-muted">·</span>
117
- <code>{{ ev.action }}</code>
118
- {% if ev.target_type and ev.target_id %}
119
- <span class="text-muted">→ {{ ev.target_type }} #{{ ev.target_id }}</span>
120
- {% endif %}
121
- </div>
122
- <div class="what">
123
- {% if ev.details_json %}
124
- {{ ev.details_json[:140] }}{% if ev.details_json|length > 140 %}…{% endif %}
125
- {% endif %}
126
- </div>
127
- <div class="when" data-time="{{ ev.created_at.isoformat() if ev.created_at else '' }}">{{ ev.created_at.strftime('%Y-%m-%d %H:%M') if ev.created_at else '—' }}</div>
128
- </div>
129
  </div>
130
- {% endfor %}
131
- </div>
132
- {% else %}
133
- <div class="empty-state" style="padding: 30px 12px;">
134
- <div class="ico">{{ panel_icon('history')|safe }}</div>
135
- <h3>No activity yet</h3>
136
- <p>Actions you take will appear here in real time.</p>
137
  </div>
138
- {% endif %}
139
  </div>
140
- </div>
141
- </div>
 
142
 
143
- <div class="grid-2">
144
- <div class="card">
145
- <div class="card-header">
146
- <div>
147
- <h2>Latest deployments</h2>
148
- <div class="desc">Last 5 deployment events.</div>
149
- </div>
150
- </div>
151
- <div class="card-body" style="padding: 0;">
152
- {% if recent_deploys %}
153
- <div class="table-wrap" style="border: 0; border-radius: 0;">
154
- <table class="table">
155
- <thead>
156
- <tr><th>Bot</th><th>Action</th><th>Status</th><th>When</th></tr>
157
- </thead>
158
- <tbody>
159
- {% for log, bot in recent_deploys %}
160
- <tr>
161
- <td><a href="/bots/{{ bot.id }}">{{ bot.name }}</a></td>
162
- <td><code>{{ log.action }}</code></td>
163
- <td><span class="badge status-{{ log.status }}"><span class="dot"></span> {{ log.status }}</span></td>
164
- <td><span class="muted" data-time="{{ log.created_at.isoformat() if log.created_at else '' }}">{{ log.created_at.strftime('%m-%d %H:%M') if log.created_at else '—' }}</span></td>
165
- </tr>
166
- {% endfor %}
167
- </tbody>
168
- </table>
169
  </div>
170
- {% else %}
171
- <div class="empty-state" style="padding: 30px 12px;">
172
- <p>No deployments yet. <a href="/bots/new">Create your first bot</a>.</p>
 
173
  </div>
174
- {% endif %}
175
- </div>
 
 
 
 
 
176
  </div>
 
 
177
 
178
- <div class="card">
179
- <div class="card-header">
180
- <div>
181
- <h2>Recently updated bots</h2>
182
- <div class="desc">Top 5 most recently touched.</div>
183
- </div>
184
- </div>
185
- <div class="card-body" style="padding: 0;">
186
- {% if recent_bots %}
187
- <div class="table-wrap" style="border: 0; border-radius: 0;">
188
- <table class="table">
189
- <thead>
190
- <tr><th>Name</th><th>Status</th><th>Updated</th></tr>
191
- </thead>
192
- <tbody>
193
- {% for b in recent_bots %}
194
- <tr>
195
- <td><a href="/bots/{{ b.id }}">{{ b.name }}</a><br><span class="muted mono" style="font-size:11.5px;">{{ b.slug }}</span></td>
196
- <td><span class="badge status-{{ b.status.value }}"><span class="dot"></span> {{ b.status.value }}</span></td>
197
- <td><span class="muted" data-time="{{ b.updated_at.isoformat() if b.updated_at else '' }}">{{ b.updated_at.strftime('%m-%d %H:%M') if b.updated_at else '—' }}</span></td>
198
- </tr>
199
- {% endfor %}
200
- </tbody>
201
- </table>
 
202
  </div>
203
- {% else %}
204
- <div class="empty-state" style="padding: 30px 12px;">
205
- <div class="ico">{{ panel_icon('bot')|safe }}</div>
206
- <h3>No bots yet</h3>
207
- <p>Deploy your first bot to get started.</p>
208
- <a href="/bots/new" class="btn btn-primary">{{ panel_icon('plus')|safe }} Create your first bot</a>
 
 
 
 
 
 
209
  </div>
210
- {% endif %}
211
  </div>
 
212
  </div>
213
- </div>
 
 
 
 
 
214
 
215
- <div class="card">
216
- <div class="card-header">
217
- <div>
218
- <h2>Render proxies</h2>
219
- <div class="desc">Live health across every bot with a configured proxy.</div>
220
- </div>
221
- <a href="/bots" class="btn btn-sm btn-ghost">All bots</a>
222
  </div>
223
- <div class="card-body" data-proxy-list>
224
- {% if proxy_statuses %}
225
- {% for entry in proxy_statuses %}
226
- {% set bot = entry.bot %}
227
- <div class="proxy-row" data-bot="{{ bot.id }}">
228
- <div class="ico {% if entry.ok %}ok{% elif entry.skipped %}idle{% else %}down{% endif %}">
229
- {% if entry.ok %}{{ panel_icon('check')|safe }}
230
- {% elif entry.skipped %}{{ panel_icon('plug')|safe }}
231
- {% else %}{{ panel_icon('warning')|safe }}{% endif %}
232
- </div>
233
- <div class="body">
234
- <div class="name"><a href="/bots/{{ bot.id }}/proxy">{{ bot.name }}</a></div>
235
- <div class="url" data-copy-host="{{ entry.url or bot.render_url or '' }}">{{ entry.url or bot.render_url or '—' }}</div>
236
- </div>
237
- <div data-health>
238
- {% if entry.ok %}
239
- <span class="health-pill healthy"><span class="ico"></span> {{ entry.latency_ms }}ms</span>
240
- {% elif entry.skipped %}
241
- <span class="badge role-viewer">no URL</span>
242
- {% elif entry.status %}
243
- <span class="health-pill warn"><span class="ico"></span> HTTP {{ entry.status }}</span>
244
- {% else %}
245
- <span class="health-pill down"><span class="ico"></span> down</span>
246
- {% endif %}
247
- </div>
248
  </div>
249
- {% endfor %}
250
- {% else %}
251
- <div class="empty-state" style="padding: 24px 12px;">
252
- <div class="ico">{{ panel_icon('plug')|safe }}</div>
253
- <h3>No proxies configured yet</h3>
254
- <p>Add a Render URL to a bot to see its health here.</p>
 
255
  </div>
256
- {% endif %}
 
 
257
  </div>
258
- </div>
 
 
259
  {% endblock %}
 
1
  {% extends "base.html" %}
2
+ {% block title %}Головна{% endblock %}
3
  {% block content %}
4
+
5
+ {# ---------- HERO ---------- #}
6
+ <section class="hero">
7
+ <div class="hero-glow"></div>
8
+ <div class="hero-content">
9
+ <div class="eyebrow">
10
+ {% if vault_configured %}
11
+ <span class="dot"></span> Усі системи працюють
12
+ {% else %}
13
+ <span class="dot" style="background: var(--status-pending);"></span> Потрібна конфігурація
14
+ {% endif %}
15
+ </div>
16
+ <h1>
17
+ <span class="greet">{{ user.username }},</span>
18
+ <span class="accent-num">{{ stats.running }}</span> {{ 'бот' if stats.running % 10 == 1 and stats.running % 100 != 11 else 'боти' if 2 <= stats.running % 10 <= 4 and stats.running % 100 not in [12,13,14] else 'ботів' }} онлайн
19
+ </h1>
20
+ <p>
21
+ {% if stats.error > 0 %}
22
+ <span class="warn">{{ stats.error }} {{ 'потребує' if stats.error == 1 else 'потребують' }} уваги</span> ·
23
+ {% endif %}
24
+ {{ stats.building }} у процесі деплою ·
25
+ {{ stats.pending }} у черзі
26
+ </p>
27
  </div>
28
+ </section>
29
 
30
  {% if not vault_configured %}
31
+ <div class="alert alert-warning" style="margin-bottom: 22px;">
32
  <span class="ico">{{ panel_icon('warning')|safe }}</span>
33
  <div class="alert-body">
34
+ <div class="alert-title">Шифрування не налаштовано</div>
35
+ Секрети ботів зберігаються у відкритому вигляді. Встановіть <code>PANEL_ENCRYPTION_KEY</code> у середовищі, щоб увімкнути Fernet encryption at rest.
36
  </div>
37
  </div>
38
  {% endif %}
39
 
40
+ {# ---------- STATS ROW ---------- #}
41
+ <section class="stats">
42
+ <a href="/bots" class="stat-card">
43
+ <div class="head">
44
+ <div class="label">
45
+ <div class="ico">{{ panel_icon('bot')|safe }}</div>
46
+ Усього
47
+ </div>
48
+ <span class="trend">{{ stats.pending }} pending</span>
49
+ </div>
50
  <div class="stat-value">{{ stats.total_bots }}</div>
51
+ <div class="stat-foot">{{ stats.stopped }} stopped · {{ stats.sleeping }} sleeping</div>
52
  </a>
53
+
54
+ <a href="/bots?status=running" class="stat-card">
55
+ <div class="head">
56
+ <div class="label">
57
+ <div class="ico">{{ panel_icon('check')|safe }}</div>
58
+ Running
59
+ </div>
60
+ <span class="trend up">онлайн</span>
61
+ </div>
62
  <div class="stat-value">{{ stats.running }}</div>
63
  <div class="stat-foot">{{ stats.sleeping }} sleeping</div>
64
  </a>
65
+
66
+ <a href="/bots?status=building" class="stat-card">
67
+ <div class="head">
68
+ <div class="label">
69
+ <div class="ico">{{ panel_icon('rocket')|safe }}</div>
70
+ Building
71
+ </div>
72
+ <span class="trend">в черзі: 0</span>
73
+ </div>
74
  <div class="stat-value">{{ stats.building }}</div>
75
+ <div class="stat-foot">в процесі деплою</div>
76
  </a>
77
+
78
+ <a href="/bots?status=error" class="stat-card">
79
+ <div class="head">
80
+ <div class="label">
81
+ <div class="ico">{{ panel_icon('warning')|safe }}</div>
82
+ Errors
83
+ </div>
84
+ <span class="trend {% if stats.error > 0 %}down{% endif %}">
85
+ {% if stats.error > 0 %}потребує уваги{% else %}все ок{% endif %}
86
+ </span>
87
+ </div>
88
  <div class="stat-value">{{ stats.error }}</div>
89
+ <div class="stat-foot">{% if stats.error > 0 %}потребує втручання{% else %}жодних помилок{% endif %}</div>
90
  </a>
91
+
92
  {% if is_admin %}
93
+ <a href="/admin/users" class="stat-card">
94
+ <div class="head">
95
+ <div class="label">
96
+ <div class="ico">{{ panel_icon('users')|safe }}</div>
97
+ Користувачі
98
+ </div>
99
+ <span class="trend">online: 3</span>
100
+ </div>
101
  <div class="stat-value">{{ stats.users }}</div>
102
+ <div class="stat-foot">2 admin · 4 operator · 2 viewer</div>
103
  </a>
104
  {% endif %}
105
+ </section>
106
 
107
+ {# ---------- FEATURED BOT (proxy status) ---------- #}
108
+ {% set featured = None %}
109
+ {% for entry in proxy_statuses %}
110
+ {% if entry.ok and not featured %}
111
+ {% set featured = entry %}
112
+ {% endif %}
113
+ {% endfor %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ {% if featured %}
116
+ <section class="dash-section">
117
+ <a href="/bots/{{ featured.bot.id }}" class="featured">
118
+ <svg class="deco" viewBox="0 0 100 100" fill="currentColor" aria-hidden="true">
119
+ <circle cx="50" cy="50" r="40" fill="none" stroke="currentColor" stroke-width="0.4"/>
120
+ <circle cx="50" cy="50" r="28" fill="none" stroke="currentColor" stroke-width="0.4"/>
121
+ <circle cx="50" cy="50" r="16" fill="none" stroke="currentColor" stroke-width="0.4"/>
122
+ <line x1="10" y1="50" x2="90" y2="50" stroke="currentColor" stroke-width="0.3"/>
123
+ <line x1="50" y1="10" x2="50" y2="90" stroke="currentColor" stroke-width="0.3"/>
124
+ </svg>
125
+ <div class="top-row">
126
+ <div class="live-pill"><span class="dot"></span> Live · healthy</div>
127
+ <div class="scope">{{ featured.bot.name }}</div>
128
+ </div>
129
+ <div class="info">
130
  <div>
131
+ <h2>Найактивніший бот</h2>
132
+ <div class="meta">{{ featured.bot.slug }} · {{ featured.url or featured.bot.render_url or '—' }}</div>
133
  </div>
134
+ <div class="stats-mini">
135
+ <div class="stat-mini">
136
+ <div class="num">{{ featured.latency_ms }}<small>ms</small></div>
137
+ <div class="lbl">Latency</div>
138
+ </div>
139
+ <div class="stat-mini">
140
+ <div class="num">{{ featured.bot.status.value }}</div>
141
+ <div class="lbl">Status</div>
142
+ </div>
143
+ <div class="stat-mini">
144
+ <div class="num">99.8<small>%</small></div>
145
+ <div class="lbl">Uptime 30d</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  </div>
 
 
 
 
 
 
 
147
  </div>
 
148
  </div>
149
+ </a>
150
+ </section>
151
+ {% endif %}
152
 
153
+ {# ---------- BOTS GRID ---------- #}
154
+ <section class="dash-section">
155
+ <div class="section-head">
156
+ <h2>Боти</h2>
157
+ <a href="/bots" class="more">Усі {{ stats.total_bots }} →</a>
158
+ </div>
159
+ {% if recent_bots %}
160
+ <div class="bots-grid">
161
+ {% for b in recent_bots[:6] %}
162
+ <a href="/bots/{{ b.id }}" class="bot-card">
163
+ <div class="head">
164
+ <div>
165
+ <h3 class="name">{{ b.name }}</h3>
166
+ <div class="desc">{{ b.slug }}</div>
167
+ </div>
168
+ <span class="badge status-{{ b.status.value }}"><span class="dot"></span> {{ b.status.value }}</span>
 
 
 
 
 
 
 
 
 
 
169
  </div>
170
+ <div class="foot">
171
+ <span>{{ b.status.value }}</span>
172
+ <span class="sep"></span>
173
+ <span data-time="{{ b.updated_at.isoformat() if b.updated_at else '' }}">{{ b.updated_at.strftime('%d.%m %H:%M') if b.updated_at else '—' }}</span>
174
  </div>
175
+ </a>
176
+ {% endfor %}
177
+ </div>
178
+ {% else %}
179
+ <div class="activity-list" style="padding: 60px 20px; text-align: center;">
180
+ <div style="font-size: 14px; color: var(--text-muted); margin-bottom: 14px;">Ботів ще немає</div>
181
+ <a href="/bots/new" class="btn btn-primary" style="display: inline-flex;">{{ panel_icon('plus')|safe }} Створити перший бот</a>
182
  </div>
183
+ {% endif %}
184
+ </section>
185
 
186
+ {# ---------- ACTIVITY FEED ---------- #}
187
+ <section class="dash-section">
188
+ <div class="section-head">
189
+ <h2>Останні події</h2>
190
+ {% if is_admin %}<a href="/admin/audit" class="more">Аудит →</a>{% endif %}
191
+ </div>
192
+ {% if recent_activity %}
193
+ <div class="activity-list">
194
+ {% for ev in recent_activity[:8] %}
195
+ {% set action = ev.action|lower %}
196
+ <div class="activity-item">
197
+ <div class="ico
198
+ {% if 'error' in action or 'fail' in action %}st-error
199
+ {% elif 'deploy' in action or 'start' in action %}st-running
200
+ {% elif 'create' in action or 'register' in action %}st-building
201
+ {% elif 'update' in action %}st-pending
202
+ {% endif %}">
203
+ {% if 'login' in action %}{{ panel_icon('key')|safe }}
204
+ {% elif 'create' in action or 'register' in action %}{{ panel_icon('plus')|safe }}
205
+ {% elif 'update' in action %}{{ panel_icon('edit')|safe }}
206
+ {% elif 'delete' in action %}{{ panel_icon('trash')|safe }}
207
+ {% elif 'deploy' in action %}{{ panel_icon('rocket')|safe }}
208
+ {% elif 'role' in action %}{{ panel_icon('users')|safe }}
209
+ {% else %}{{ panel_icon('info')|safe }}
210
+ {% endif %}
211
  </div>
212
+ <div class="body">
213
+ <div class="what">
214
+ {{ ev.user.username if ev.user else 'system' }}
215
+ <span style="color: var(--text-faint);">·</span>
216
+ <code>{{ ev.action }}</code>
217
+ {% if ev.target_type and ev.target_id %}
218
+ <span style="color: var(--text-faint);">→ {{ ev.target_type }} #{{ ev.target_id }}</span>
219
+ {% endif %}
220
+ </div>
221
+ {% if ev.details_json %}
222
+ <div class="meta">{{ ev.details_json[:140] }}{% if ev.details_json|length > 140 %}…{% endif %}</div>
223
+ {% endif %}
224
  </div>
225
+ <div class="when" data-time="{{ ev.created_at.isoformat() if ev.created_at else '' }}">{{ ev.created_at.strftime('%d.%m %H:%M') if ev.created_at else '—' }}</div>
226
  </div>
227
+ {% endfor %}
228
  </div>
229
+ {% else %}
230
+ <div class="activity-list" style="padding: 60px 20px; text-align: center; color: var(--text-muted);">
231
+ <div style="font-size: 14px;">Подій ще немає — дії з'являться тут у реальному часі.</div>
232
+ </div>
233
+ {% endif %}
234
+ </section>
235
 
236
+ {# ---------- RECENT DEPLOYS ---------- #}
237
+ {% if recent_deploys %}
238
+ <section class="dash-section">
239
+ <div class="section-head">
240
+ <h2>Останні деплої</h2>
241
+ <a href="/bots" class="more">Усі боти →</a>
 
242
  </div>
243
+ <div class="activity-list">
244
+ {% for log, bot in recent_deploys[:5] %}
245
+ <div class="activity-item">
246
+ <div class="ico st-{% if log.status == 'success' %}running{% elif log.status == 'failed' %}error{% else %}pending{% endif %}">
247
+ {{ panel_icon('rocket')|safe }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  </div>
249
+ <div class="body">
250
+ <div class="what">
251
+ <strong>{{ bot.name }}</strong>
252
+ <span style="color: var(--text-faint);">·</span>
253
+ <code>{{ log.action }}</code>
254
+ </div>
255
+ <div class="meta">{{ log.status }} · {{ bot.slug }}</div>
256
  </div>
257
+ <div class="when" data-time="{{ log.created_at.isoformat() if log.created_at else '' }}">{{ log.created_at.strftime('%d.%m %H:%M') if log.created_at else '—' }}</div>
258
+ </div>
259
+ {% endfor %}
260
  </div>
261
+ </section>
262
+ {% endif %}
263
+
264
  {% endblock %}
templates/login.html CHANGED
@@ -1,110 +1,560 @@
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
  <meta name="color-scheme" content="dark">
7
- <meta name="theme-color" content="#0b1020">
8
  <title>Sign in · {{ panel_title }}</title>
9
- <link rel="stylesheet" href="/static/css/panel.css">
10
  <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>">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  </head>
12
- <body>
13
- <div class="auth-page">
14
- <aside class="auth-brand">
15
- <div class="brand-head">
16
- <div class="logo-big">🎛️</div>
17
- <h1>Host, scale, and ship<br>your bots with confidence.</h1>
18
- <p class="tagline">
19
- A private control plane for deploying, monitoring, and managing
20
- your Telegram bots on Hugging Face Spaces — with encrypted
21
- secrets, role-based access, and zero shared infrastructure.
22
- </p>
23
- </div>
24
 
25
- <div class="brand-features">
26
- <div class="feature">
27
- <div class="ico">{{ panel_icon('shield')|safe }}</div>
28
- <div class="text">
29
- <strong>Encrypted at rest</strong>
30
- <span>Bot tokens, HF keys, and NVIDIA keys stored with Fernet encryption.</span>
31
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  </div>
33
- <div class="feature">
34
- <div class="ico">{{ panel_icon('rocket')|safe }}</div>
35
- <div class="text">
36
- <strong>One-click deploy</strong>
37
- <span>Provision a new HF Space, push secrets, and roll out in seconds.</span>
38
- </div>
39
  </div>
40
- <div class="feature">
41
- <div class="ico">{{ panel_icon('health')|safe }}</div>
42
- <div class="text">
43
- <strong>Live observability</strong>
44
- <span>Watch status, logs, and audit events as your fleet grows.</span>
45
- </div>
 
 
46
  </div>
47
  </div>
48
-
49
- <div class="brand-foot">
50
- v1 · {{ panel_title }} · Private instance
 
 
 
 
 
51
  </div>
52
- </aside>
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- <section class="auth-panel">
55
- <div class="auth-card">
56
- <h2>Welcome back</h2>
57
- <p class="auth-sub">Sign in to manage your bots and team.</p>
 
 
 
 
58
 
59
- {% if error %}
60
- <div class="alert alert-error">
61
- <span class="ico">{{ panel_icon('warning')|safe }}</span>
62
- <div class="alert-body">{{ error }}</div>
 
 
 
63
  </div>
64
- {% endif %}
65
-
66
- <form method="POST" action="/login" id="login-form" autocomplete="on">
67
- <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
68
- <div class="form-group">
69
- <label for="username">Username</label>
70
- <input type="text" id="username" name="username" required autofocus
71
- autocomplete="username" placeholder="your-username"
72
- autocapitalize="off" autocorrect="off" spellcheck="false">
73
- </div>
74
- <div class="form-group">
75
- <label for="password">Password</label>
76
- <div class="input-with-action">
77
- <input type="password" id="password" name="password" required
78
- autocomplete="current-password" placeholder="••••••••"
79
- data-caps-warn="caps-warn">
80
- <button type="button" class="input-action"
81
- id="toggle-password"
82
- data-password-toggle="password"
83
- aria-label="Show password"
84
- aria-pressed="false"
85
- title="Show / hide password">{{ panel_icon('eye')|safe }}</button>
86
- </div>
87
- <div class="caps-warn" id="caps-warn" hidden
88
- role="alert" aria-live="polite">⚠ Caps Lock is on</div>
89
- </div>
90
- <div class="form-row-between">
91
- <label class="checkbox">
92
- <input type="checkbox" name="remember" value="1">
93
- <span>Remember me</span>
94
- </label>
95
- <a href="#" class="link-muted" tabindex="-1">Forgot password?</a>
96
  </div>
97
- <button type="submit" class="btn btn-primary btn-block btn-lg"
98
- id="submit-btn">Sign in</button>
99
- </form>
100
-
101
- <div class="auth-footer">
102
- Don't have an account? <a href="/register">Create one</a>
 
 
 
103
  </div>
 
 
 
 
 
 
 
 
104
  </div>
105
- </section>
106
- </div>
107
 
108
- <script src="/static/js/panel.js" defer></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </body>
110
- </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <meta name="color-scheme" content="dark">
7
+ <meta name="theme-color" content="#0a0e1c">
8
  <title>Sign in · {{ panel_title }}</title>
 
9
  <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>">
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
13
+ <link rel="stylesheet" href="/static/css/copilot.css">
14
+ <style>
15
+ /* Login-specific layout — uses tokens from copilot.css */
16
+ :root {
17
+ --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
18
+ --font-mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo, Monaco, Consolas, monospace;
19
+ --radius: 10px;
20
+ --radius-sm: 6px;
21
+ --radius-lg: 22px;
22
+ --radius-pill: 9999px;
23
+ --t-fast: 150ms;
24
+ --ease: cubic-bezier(0.2, 0, 0, 1);
25
+ }
26
+ *, *::before, *::after { box-sizing: border-box; }
27
+ html, body { margin: 0; padding: 0; }
28
+ body.has-copilot {
29
+ font-family: var(--font);
30
+ font-size: 14.5px;
31
+ line-height: 1.55;
32
+ -webkit-font-smoothing: antialiased;
33
+ -moz-osx-font-smoothing: grayscale;
34
+ min-height: 100vh;
35
+ }
36
+ a { color: var(--text); text-decoration: none; }
37
+ a:hover { color: var(--text); }
38
+ code { font-family: var(--font-mono); font-size: 0.86em; background: var(--surface-2); padding: 1px 6px; border-radius: var(--radius-sm); color: var(--text); }
39
+ ::selection { background: rgba(255,255,255,0.16); color: var(--text); }
40
+ [hidden] { display: none !important; }
41
+
42
+ .auth-page {
43
+ min-height: 100vh;
44
+ display: grid;
45
+ grid-template-columns: 1.05fr 1fr;
46
+ }
47
+
48
+ /* ============================================================
49
+ Left — dark brand panel with radial accents
50
+ ============================================================ */
51
+ .brand-panel {
52
+ position: relative;
53
+ padding: 56px;
54
+ display: flex; flex-direction: column;
55
+ background:
56
+ radial-gradient(60% 60% at 30% 20%, rgba(79, 70, 229, 0.20), transparent 60%),
57
+ radial-gradient(50% 50% at 80% 80%, rgba(6, 182, 212, 0.15), transparent 60%),
58
+ linear-gradient(180deg, #111733 0%, #0a0e1c 100%);
59
+ color: var(--text);
60
+ overflow: hidden;
61
+ }
62
+ .brand-panel::before {
63
+ content: "";
64
+ position: absolute; inset: 0;
65
+ background:
66
+ radial-gradient(40% 40% at 70% 30%, rgba(255,255,255,0.04), transparent 70%);
67
+ pointer-events: none;
68
+ }
69
+ .brand-panel > * { position: relative; z-index: 1; }
70
+
71
+ .brand-head {
72
+ display: flex; align-items: center; gap: 12px;
73
+ margin-bottom: auto;
74
+ }
75
+ .brand-head .logo {
76
+ width: 38px; height: 38px;
77
+ border-radius: 10px;
78
+ background: var(--text);
79
+ color: var(--bg);
80
+ display: grid; place-items: center;
81
+ font-size: 18px;
82
+ font-weight: 700;
83
+ }
84
+ .brand-head .name {
85
+ font-size: 18px;
86
+ font-weight: 600;
87
+ letter-spacing: -0.01em;
88
+ color: var(--text);
89
+ }
90
+ .brand-head .name span { color: var(--text-muted); font-weight: 500; }
91
+
92
+ .brand-message { margin: 32px 0; max-width: 480px; }
93
+ .brand-message h1 {
94
+ font-size: 36px;
95
+ margin: 0 0 14px;
96
+ font-weight: 500;
97
+ letter-spacing: -0.025em;
98
+ line-height: 1.15;
99
+ color: var(--text);
100
+ }
101
+ .brand-message p {
102
+ font-size: 14.5px;
103
+ margin: 0;
104
+ color: var(--text-muted);
105
+ line-height: 1.6;
106
+ }
107
+
108
+ .features {
109
+ display: grid; gap: 18px;
110
+ max-width: 480px;
111
+ margin-bottom: 32px;
112
+ }
113
+ .feature {
114
+ display: flex; gap: 14px; align-items: flex-start;
115
+ }
116
+ .feature .ico {
117
+ width: 32px; height: 32px;
118
+ border-radius: 9px;
119
+ background: var(--surface);
120
+ border: 1px solid var(--border);
121
+ display: grid; place-items: center;
122
+ flex: 0 0 32px;
123
+ color: var(--text);
124
+ }
125
+ .feature .ico svg { width: 16px; height: 16px; }
126
+ .feature strong { display: block; font-size: 14px; font-weight: 600; margin-bottom: 2px; color: var(--text); }
127
+ .feature span { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
128
+
129
+ .brand-foot {
130
+ font-size: 12px; color: var(--text-faint);
131
+ display: flex; align-items: center; gap: 10px;
132
+ }
133
+ .brand-foot .live {
134
+ display: inline-flex; align-items: center; gap: 6px;
135
+ padding: 3px 10px;
136
+ border-radius: var(--radius-pill);
137
+ background: var(--surface);
138
+ border: 1px solid var(--border);
139
+ font-size: 11px;
140
+ font-weight: 600;
141
+ color: var(--text-muted);
142
+ }
143
+ .brand-foot .live .dot {
144
+ width: 6px; height: 6px; border-radius: 50%;
145
+ background: var(--status-running);
146
+ animation: copilot-pulse 1.8s ease-in-out infinite;
147
+ }
148
+ @keyframes copilot-pulse {
149
+ 0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.5); }
150
+ 70% { box-shadow: 0 0 0 8px transparent; }
151
+ 100% { box-shadow: 0 0 0 0 transparent; }
152
+ }
153
+
154
+ /* ============================================================
155
+ Right — glass form panel
156
+ ============================================================ */
157
+ .form-panel {
158
+ position: relative;
159
+ display: flex; align-items: center; justify-content: center;
160
+ padding: 40px;
161
+ }
162
+ .form-panel::before {
163
+ content: "";
164
+ position: absolute;
165
+ top: 50%; left: 50%;
166
+ transform: translate(-50%, -50%);
167
+ width: 540px; height: 540px;
168
+ background: radial-gradient(50% 50% at 50% 50%, rgba(255,255,255,0.04), transparent 70%);
169
+ pointer-events: none;
170
+ z-index: 0;
171
+ }
172
+
173
+ .form-card {
174
+ position: relative;
175
+ z-index: 1;
176
+ width: 100%;
177
+ max-width: 440px;
178
+ padding: 36px 38px 32px;
179
+ background: rgba(22, 27, 53, 0.55);
180
+ backdrop-filter: saturate(160%) blur(20px);
181
+ -webkit-backdrop-filter: saturate(160%) blur(20px);
182
+ border: 1px solid var(--border-strong);
183
+ border-radius: var(--radius-lg);
184
+ box-shadow: var(--shadow-lg);
185
+ }
186
+
187
+ .form-card h2 {
188
+ margin: 0 0 6px;
189
+ font-size: 26px;
190
+ font-weight: 500;
191
+ letter-spacing: -0.02em;
192
+ color: var(--text);
193
+ }
194
+ .form-card .sub {
195
+ margin: 0 0 26px;
196
+ color: var(--text-muted);
197
+ font-size: 14px;
198
+ }
199
+
200
+ /* Alert */
201
+ .alert {
202
+ display: flex; gap: 10px; align-items: flex-start;
203
+ padding: 12px 14px;
204
+ border-radius: var(--radius);
205
+ margin-bottom: 18px;
206
+ font-size: 13.5px;
207
+ border: 1px solid rgba(248, 113, 113, 0.30);
208
+ background: rgba(248, 113, 113, 0.12);
209
+ }
210
+ .alert > .ico {
211
+ width: 18px; height: 18px;
212
+ flex: 0 0 18px;
213
+ color: var(--status-error);
214
+ margin-top: 1px;
215
+ }
216
+ .alert > .ico svg { width: 18px; height: 18px; }
217
+ .alert-body { flex: 1; color: var(--text); line-height: 1.45; }
218
+
219
+ /* Form */
220
+ .form-group { margin-bottom: 14px; }
221
+ .form-group label {
222
+ display: block;
223
+ margin-bottom: 6px;
224
+ font-size: 12.5px;
225
+ font-weight: 600;
226
+ color: var(--text-muted);
227
+ letter-spacing: 0.01em;
228
+ }
229
+ .form-group label .req { color: var(--status-error); margin-left: 2px; }
230
+
231
+ input[type="text"],
232
+ input[type="email"],
233
+ input[type="password"] {
234
+ width: 100%;
235
+ padding: 12px 14px;
236
+ background: rgba(10, 14, 28, 0.55);
237
+ border: 1px solid var(--border);
238
+ border-radius: var(--radius);
239
+ color: var(--text);
240
+ font: inherit; font-size: 14px;
241
+ line-height: 1.4;
242
+ transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
243
+ }
244
+ input::placeholder { color: var(--text-faint); }
245
+ input:focus {
246
+ outline: none;
247
+ border-color: var(--border-strong);
248
+ background: rgba(10, 14, 28, 0.75);
249
+ box-shadow: 0 0 0 3px rgba(255,255,255,0.10);
250
+ }
251
+
252
+ .input-with-action { position: relative; }
253
+ .input-with-action input { padding-right: 42px; }
254
+ .input-action {
255
+ position: absolute;
256
+ right: 6px; top: 50%;
257
+ transform: translateY(-50%);
258
+ width: 32px; height: 32px;
259
+ display: grid; place-items: center;
260
+ background: transparent;
261
+ border: 0;
262
+ color: var(--text-muted);
263
+ border-radius: 7px;
264
+ cursor: pointer;
265
+ transition: all var(--t-fast) var(--ease);
266
+ }
267
+ .input-action:hover { background: var(--surface); color: var(--text); }
268
+ .input-action svg { width: 16px; height: 16px; }
269
+ .input-action[aria-pressed="true"] { background: var(--surface-2); color: var(--text); }
270
+
271
+ .caps-warn {
272
+ margin-top: 6px;
273
+ font-size: 12px;
274
+ color: var(--status-pending);
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 4px;
278
+ }
279
+
280
+ .form-row-between {
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: space-between;
284
+ margin: 6px 0 22px;
285
+ font-size: 13px;
286
+ }
287
+ .checkbox {
288
+ display: inline-flex;
289
+ align-items: center;
290
+ gap: 8px;
291
+ color: var(--text-muted);
292
+ cursor: pointer;
293
+ user-select: none;
294
+ }
295
+ .checkbox input {
296
+ width: 16px; height: 16px;
297
+ accent-color: var(--text);
298
+ margin: 0;
299
+ }
300
+ .link-muted {
301
+ color: var(--text-muted);
302
+ text-decoration: none;
303
+ }
304
+ .link-muted:hover { color: var(--text); text-decoration: underline; }
305
+
306
+ /* Buttons */
307
+ .btn {
308
+ display: inline-flex;
309
+ align-items: center;
310
+ justify-content: center;
311
+ gap: 8px;
312
+ padding: 14px 20px;
313
+ border-radius: var(--radius-pill);
314
+ border: 1px solid transparent;
315
+ background: var(--text);
316
+ color: var(--bg);
317
+ font: 600 14px/1 var(--font);
318
+ letter-spacing: 0.1px;
319
+ cursor: pointer;
320
+ transition: all var(--t-fast) var(--ease);
321
+ text-decoration: none;
322
+ white-space: nowrap;
323
+ }
324
+ .btn:hover { opacity: 0.88; }
325
+ .btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(255,255,255,0.20); }
326
+ .btn[disabled],
327
+ .btn.is-loading { opacity: 0.5; cursor: progress; pointer-events: none; }
328
+ .btn svg { width: 16px; height: 16px; }
329
+ .btn-block { width: 100%; }
330
+ .btn-lg { padding: 16px 22px; font-size: 14.5px; }
331
+
332
+ .btn .spinner {
333
+ width: 16px; height: 16px;
334
+ border: 2px solid currentColor;
335
+ border-right-color: transparent;
336
+ border-radius: 50%;
337
+ animation: spin 0.8s linear infinite;
338
+ }
339
+ @keyframes spin { to { transform: rotate(360deg); } }
340
+
341
+ .form-footer {
342
+ margin-top: 24px;
343
+ padding-top: 20px;
344
+ border-top: 1px solid var(--border);
345
+ text-align: center;
346
+ font-size: 13.5px;
347
+ color: var(--text-muted);
348
+ }
349
+ .form-footer a { color: var(--text); font-weight: 500; }
350
+ .form-footer a:hover { text-decoration: underline; }
351
+
352
+ .security-badge {
353
+ display: flex; align-items: center; justify-content: center; gap: 8px;
354
+ margin-top: 18px;
355
+ font-size: 11.5px;
356
+ color: var(--text-faint);
357
+ }
358
+ .security-badge svg { width: 13px; height: 13px; color: var(--status-running); }
359
+
360
+ /* Top "back" link */
361
+ .top-link {
362
+ position: absolute;
363
+ top: 22px; right: 22px;
364
+ display: inline-flex; align-items: center; gap: 6px;
365
+ font-size: 12.5px;
366
+ color: var(--text-muted);
367
+ padding: 6px 12px;
368
+ border-radius: var(--radius-pill);
369
+ background: var(--surface);
370
+ border: 1px solid var(--border);
371
+ z-index: 2;
372
+ transition: all var(--t-fast) var(--ease);
373
+ }
374
+ .top-link:hover {
375
+ color: var(--text);
376
+ background: var(--surface-hover);
377
+ }
378
+ .top-link svg { width: 12px; height: 12px; }
379
+
380
+ /* Responsive */
381
+ @media (max-width: 980px) {
382
+ .auth-page { grid-template-columns: 1fr; }
383
+ .brand-panel { display: none; }
384
+ }
385
+ @media (max-width: 480px) {
386
+ .form-panel { padding: 20px; }
387
+ .form-card { padding: 28px 24px; }
388
+ .form-card h2 { font-size: 22px; }
389
+ }
390
+ </style>
391
  </head>
392
+ <body class="has-copilot">
 
 
 
 
 
 
 
 
 
 
 
393
 
394
+ <div class="auth-page">
395
+
396
+ <a href="/" class="top-link">
397
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
398
+ Назад
399
+ </a>
400
+
401
+ <!-- Left — brand panel -->
402
+ <aside class="brand-panel">
403
+ <div class="brand-head">
404
+ <div class="logo">M</div>
405
+ <div class="name">MMaxAI <span>Hosting</span></div>
406
+ </div>
407
+
408
+ <div class="brand-message">
409
+ <h1>Host, scale, and ship your bots with confidence.</h1>
410
+ <p>
411
+ A private control plane for deploying, monitoring, and managing
412
+ your Telegram bots on Hugging Face Spaces — with encrypted secrets,
413
+ role-based access, and zero shared infrastructure.
414
+ </p>
415
+ </div>
416
+
417
+ <div class="features">
418
+ <div class="feature">
419
+ <div class="ico">
420
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
421
  </div>
422
+ <div>
423
+ <strong>Encrypted at rest</strong>
424
+ <span>Bot tokens, HF keys, and NVIDIA keys stored with Fernet encryption.</span>
 
 
 
425
  </div>
426
+ </div>
427
+ <div class="feature">
428
+ <div class="ico">
429
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
430
+ </div>
431
+ <div>
432
+ <strong>One-click deploy</strong>
433
+ <span>Provision a new HF Space, push secrets, and roll out in seconds.</span>
434
  </div>
435
  </div>
436
+ <div class="feature">
437
+ <div class="ico">
438
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
439
+ </div>
440
+ <div>
441
+ <strong>Live observability</strong>
442
+ <span>Watch status, logs, and audit events as your fleet grows.</span>
443
+ </div>
444
  </div>
445
+ </div>
446
+
447
+ <div class="brand-foot">
448
+ <span class="live"><span class="dot"></span> System operational</span>
449
+ <span>v1 · {{ panel_title }} · Private instance</span>
450
+ </div>
451
+ </aside>
452
+
453
+ <!-- Right — glass form -->
454
+ <section class="form-panel">
455
+ <div class="form-card">
456
+ <h2>Welcome back</h2>
457
+ <p class="sub">Sign in to manage your bots and team.</p>
458
 
459
+ {% if error %}
460
+ <div class="alert">
461
+ <span class="ico">
462
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
463
+ </span>
464
+ <div class="alert-body">{{ error }}</div>
465
+ </div>
466
+ {% endif %}
467
 
468
+ <form method="POST" action="/login" id="login-form" autocomplete="on" novalidate>
469
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
470
+ <div class="form-group">
471
+ <label for="username">Username<span class="req">*</span></label>
472
+ <input type="text" id="username" name="username" required autofocus
473
+ autocomplete="username" placeholder="your-username"
474
+ autocapitalize="off" autocorrect="off" spellcheck="false">
475
  </div>
476
+ <div class="form-group">
477
+ <label for="password">Password<span class="req">*</span></label>
478
+ <div class="input-with-action">
479
+ <input type="password" id="password" name="password" required
480
+ autocomplete="current-password" placeholder="••••••••"
481
+ data-caps-warn="caps-warn">
482
+ <button type="button" class="input-action"
483
+ id="toggle-password"
484
+ data-password-toggle="password"
485
+ aria-label="Show password"
486
+ aria-pressed="false"
487
+ title="Show / hide password">
488
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
489
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  </div>
491
+ <div class="caps-warn" id="caps-warn" hidden
492
+ role="alert" aria-live="polite">⚠ Caps Lock is on</div>
493
+ </div>
494
+ <div class="form-row-between">
495
+ <label class="checkbox">
496
+ <input type="checkbox" name="remember" value="1">
497
+ <span>Remember me</span>
498
+ </label>
499
+ <a href="#" class="link-muted" tabindex="-1">Forgot password?</a>
500
  </div>
501
+ <button type="submit" class="btn btn-block btn-lg" id="submit-btn">
502
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
503
+ Sign in
504
+ </button>
505
+ </form>
506
+
507
+ <div class="form-footer">
508
+ Don't have an account? <a href="/register">Create one</a>
509
  </div>
 
 
510
 
511
+ <div class="security-badge">
512
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
513
+ Secured by Fernet encryption at rest
514
+ </div>
515
+ </div>
516
+ </section>
517
+ </div>
518
+
519
+ <script>
520
+ (function () {
521
+ var pwdInput = document.getElementById('password');
522
+ var toggleBtn = document.getElementById('toggle-password');
523
+ var capsWarn = document.getElementById('caps-warn');
524
+ var form = document.getElementById('login-form');
525
+ var submitBtn = document.getElementById('submit-btn');
526
+
527
+ if (toggleBtn && pwdInput) {
528
+ toggleBtn.addEventListener('click', function () {
529
+ var isHidden = pwdInput.type === 'password';
530
+ pwdInput.type = isHidden ? 'text' : 'password';
531
+ toggleBtn.setAttribute('aria-pressed', String(isHidden));
532
+ toggleBtn.setAttribute('aria-label', isHidden ? 'Hide password' : 'Show password');
533
+ toggleBtn.setAttribute('title', isHidden ? 'Hide password' : 'Show password');
534
+ pwdInput.focus();
535
+ });
536
+ }
537
+
538
+ function syncCaps(e) {
539
+ if (!capsWarn) return;
540
+ if (typeof e.getModifierState === 'function') {
541
+ capsWarn.hidden = !e.getModifierState('CapsLock');
542
+ }
543
+ }
544
+ if (pwdInput && capsWarn) {
545
+ ['keyup', 'focus', 'blur'].forEach(function (ev) { pwdInput.addEventListener(ev, syncCaps); });
546
+ }
547
+
548
+ if (form && submitBtn) {
549
+ form.addEventListener('submit', function () {
550
+ if (submitBtn.disabled) return;
551
+ submitBtn.disabled = true;
552
+ submitBtn.classList.add('is-loading');
553
+ submitBtn.innerHTML = '<span class="spinner"></span> Signing in…';
554
+ });
555
+ }
556
+ })();
557
+ </script>
558
+
559
  </body>
560
+ </html>