ItsBounvy commited on
Commit
b8ecb04
·
verified ·
1 Parent(s): 67504de

deploy: english UI + admin creds + dataset wiring

Browse files
Files changed (2) hide show
  1. templates/base.html +3 -1
  2. templates/register.html +654 -95
templates/base.html CHANGED
@@ -63,11 +63,13 @@
63
  <span class="nav-icon">{{ panel_icon('audit')|safe }}</span> {{ _("Audit") }}
64
  </a>
65
  {% endif %}
 
66
  <a href="/admin/control" class="nav-item {% if request.url.path.startswith('/admin/control') %}active{% endif %}">
67
  <span class="nav-icon">{{ panel_icon('shield')|safe }}</span> {{ _("Admin control") }}
68
  </a>
 
69
 
70
- <div class="sidebar-section-label">{{ _("Account") }}</div>
71
  <a href="/profile" class="nav-item {% if request.url.path == '/profile' %}active{% endif %}">
72
  <span class="nav-icon">{{ panel_icon('user')|safe }}</span> {{ _("Profile") }}
73
  </a>
 
63
  <span class="nav-icon">{{ panel_icon('audit')|safe }}</span> {{ _("Audit") }}
64
  </a>
65
  {% endif %}
66
+ {% if user.role == 'admin' %}
67
  <a href="/admin/control" class="nav-item {% if request.url.path.startswith('/admin/control') %}active{% endif %}">
68
  <span class="nav-icon">{{ panel_icon('shield')|safe }}</span> {{ _("Admin control") }}
69
  </a>
70
+ {% endif %}
71
 
72
+ <div class="sidebar-section-label">{{ _("Account") }}</div>
73
  <a href="/profile" class="nav-item {% if request.url.path == '/profile' %}active{% endif %}">
74
  <span class="nav-icon">{{ panel_icon('user')|safe }}</span> {{ _("Profile") }}
75
  </a>
templates/register.html CHANGED
@@ -1,135 +1,694 @@
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>{{ _("Create your account") }} · {{ 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>{{ _("Spin up your first bot<br>in just a few minutes.") }}</h1>
18
- <p class="tagline">
19
- {{ _("Create an account, claim your invite slot, and start shipping bots to production in minutes — not hours.") }}
 
 
 
 
20
  </p>
21
  </div>
22
 
23
- <div class="brand-features">
24
  <div class="feature">
25
- <div class="ico">{{ panel_icon('shield')|safe }}</div>
26
- <div class="text">
 
 
27
  <strong>{{ _("Encrypted at rest") }}</strong>
28
- <span>{{ _("Every secret you store is encrypted with Fernet.") }}</span>
29
  </div>
30
  </div>
31
  <div class="feature">
32
- <div class="ico">{{ panel_icon('users')|safe }}</div>
33
- <div class="text">
 
 
34
  <strong>{{ _("Invite-only access") }}</strong>
35
- <span>{% if invite_required %}{{ _("An invite code is required.") }}{% else %}{{ _("Self-serve registration.") }}{% endif %}</span>
36
  </div>
37
  </div>
38
  <div class="feature">
39
- <div class="ico">{{ panel_icon('gear')|safe }}</div>
40
- <div class="text">
 
 
41
  <strong>{{ _("Bring your own keys") }}</strong>
42
  <span>{{ _("Use your own Telegram, Hugging Face, and NVIDIA credentials.") }}</span>
43
  </div>
44
  </div>
45
  </div>
46
 
47
- <div class="brand-foot">
48
- v1 · {{ panel_title }} · Private instance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
- </aside>
51
 
52
- <section class="auth-panel">
53
- <div class="auth-card">
54
- <h2>{{ _("Create your account") }}</h2>
55
- <p class="auth-sub">{{ _("Pick a username and a strong password to get started.") }}</p>
56
 
57
- {% if error %}
58
- <div class="alert alert-error">
59
- <span class="ico">{{ panel_icon('warning')|safe }}</span>
60
- <div class="alert-body">{{ error }}</div>
 
 
61
  </div>
62
- {% endif %}
63
 
64
- <form method="POST" action="/register" autocomplete="on">
65
- <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
66
- <div class="form-group">
67
- <label for="username">{{ _("Username") }} <span class="req">*</span></label>
68
- <input type="text" id="username" name="username" required autofocus
69
- autocomplete="username" placeholder="{{ _("your-username") }}"
70
- pattern="[a-zA-Z0-9_-]{3,32}"
71
- autocapitalize="off" autocorrect="off" spellcheck="false">
72
- </div>
73
- <div class="form-group">
74
- <label for="email">{{ _("Email") }}</label>
75
- <input type="email" id="email" name="email" autocomplete="email" placeholder="{{ _("you@example.com") }}">
76
- </div>
77
- <div class="form-group">
78
- <label for="password">{{ _("Password") }} <span class="req">*</span></label>
79
- <div class="input-with-action">
80
- <input type="password" id="password" name="password" required
81
- minlength="8" autocomplete="new-password"
82
- placeholder="{{ _("At least 8 characters") }}"
83
- data-password-strength
84
- data-caps-warn="caps-warn-reg">
85
- <button type="button" class="input-action"
86
- data-password-toggle="password"
87
- aria-label="{{ _("Show password") }}"
88
- aria-pressed="false"
89
- title="{{ _("Show / hide password") }}">{{ panel_icon('eye')|safe }}</button>
90
- </div>
91
- <div class="strength">
92
- <span class="strength-bar"></span>
93
- <span class="strength-bar"></span>
94
- <span class="strength-bar"></span>
95
- </div>
96
- <div class="caps-warn" id="caps-warn-reg" hidden
97
- role="alert" aria-live="polite">{{ _("⚠ Caps Lock is on") }}</div>
98
- <span class="help">{{ _("Minimum 8 chars. Mix upper / lower / digits / symbols for strength.") }}</span>
99
  </div>
100
- <div class="form-group">
101
- <label for="password_confirm">{{ _("Confirm password") }} <span class="req">*</span></label>
102
- <div class="input-with-action">
103
- <input type="password" id="password_confirm" name="password_confirm"
104
- required minlength="8" autocomplete="new-password"
105
- data-caps-warn="caps-warn-conf">
106
- <button type="button" class="input-action"
107
- data-password-toggle="password_confirm"
108
- aria-label="{{ _("Show password") }}" aria-pressed="false"
109
- title="{{ _("Show / hide password") }}">{{ panel_icon('eye')|safe }}</button>
110
- </div>
111
- <div class="caps-warn" id="caps-warn-conf" hidden
112
- role="alert" aria-live="polite">{{ _("⚠ Caps Lock is on") }}</div>
113
  </div>
114
- {% if invite_required %}
115
- <div class="form-group">
116
- <label for="invite_code">{{ _("Invite code") }} <span class="req">*</span></label>
117
- <input type="text" id="invite_code" name="invite_code" required
118
- placeholder="{{ _("Paste the code you received") }}" autocomplete="off"
119
- autocapitalize="characters" spellcheck="false">
120
- <span class="help">{{ _("Ask an existing member for an invite code.") }}</span>
 
 
 
 
 
 
 
 
 
 
 
 
121
  </div>
122
- {% endif %}
123
- <button type="submit" class="btn btn-primary btn-block btn-lg">{{ _("Create account") }}</button>
124
- </form>
125
 
126
- <div class="auth-footer">
127
- {{ _("Already have an account?") }} <a href="/login">{{ _("Sign in") }}</a>
 
 
 
 
 
128
  </div>
 
 
 
 
 
 
 
 
 
 
129
  </div>
130
- </section>
131
- </div>
132
 
133
- <script src="/static/js/panel.js" defer></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </body>
135
- </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>{{ _("Create your account") }} · {{ 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
+ /* Register-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
+ .help {
281
+ display: block;
282
+ margin-top: 6px;
283
+ font-size: 12px;
284
+ color: var(--text-muted);
285
+ line-height: 1.45;
286
+ }
287
+
288
+ .strength {
289
+ display: flex;
290
+ gap: 4px;
291
+ margin-top: 8px;
292
+ }
293
+ .strength-bar {
294
+ flex: 1;
295
+ height: 4px;
296
+ border-radius: 2px;
297
+ background: var(--surface);
298
+ border: 1px solid var(--border);
299
+ transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
300
+ }
301
+ .strength-bar.is-weak { background: rgba(248, 113, 113, 0.55); border-color: rgba(248, 113, 113, 0.55); }
302
+ .strength-bar.is-ok { background: rgba(251, 191, 36, 0.55); border-color: rgba(251, 191, 36, 0.55); }
303
+ .strength-bar.is-strong { background: rgba(74, 222, 128, 0.55); border-color: rgba(74, 222, 128, 0.55); }
304
+
305
+ /* Buttons */
306
+ .btn {
307
+ display: inline-flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ gap: 8px;
311
+ padding: 14px 20px;
312
+ border-radius: var(--radius-pill);
313
+ border: 1px solid transparent;
314
+ background: var(--text);
315
+ color: var(--bg);
316
+ font: 600 14px/1 var(--font);
317
+ letter-spacing: 0.1px;
318
+ cursor: pointer;
319
+ transition: all var(--t-fast) var(--ease);
320
+ text-decoration: none;
321
+ white-space: nowrap;
322
+ }
323
+ .btn:hover { opacity: 0.88; }
324
+ .btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(255,255,255,0.20); }
325
+ .btn[disabled],
326
+ .btn.is-loading { opacity: 0.5; cursor: progress; pointer-events: none; }
327
+ .btn svg { width: 16px; height: 16px; }
328
+ .btn-block { width: 100%; }
329
+ .btn-lg { padding: 16px 22px; font-size: 14.5px; }
330
+
331
+ .btn .spinner {
332
+ width: 16px; height: 16px;
333
+ border: 2px solid currentColor;
334
+ border-right-color: transparent;
335
+ border-radius: 50%;
336
+ animation: spin 0.8s linear infinite;
337
+ }
338
+ @keyframes spin { to { transform: rotate(360deg); } }
339
+
340
+ .form-footer {
341
+ margin-top: 24px;
342
+ padding-top: 20px;
343
+ border-top: 1px solid var(--border);
344
+ text-align: center;
345
+ font-size: 13.5px;
346
+ color: var(--text-muted);
347
+ }
348
+ .form-footer a { color: var(--text); font-weight: 500; }
349
+ .form-footer a:hover { text-decoration: underline; }
350
+
351
+ .security-badge {
352
+ display: flex; align-items: center; justify-content: center; gap: 8px;
353
+ margin-top: 18px;
354
+ font-size: 11.5px;
355
+ color: var(--text-faint);
356
+ }
357
+ .security-badge svg { width: 13px; height: 13px; color: var(--status-running); }
358
+
359
+ /* Top "back" link */
360
+ .top-link {
361
+ position: absolute;
362
+ top: 22px; right: 22px;
363
+ display: inline-flex; align-items: center; gap: 6px;
364
+ font-size: 12.5px;
365
+ color: var(--text-muted);
366
+ padding: 6px 12px;
367
+ border-radius: var(--radius-pill);
368
+ background: var(--surface);
369
+ border: 1px solid var(--border);
370
+ z-index: 2;
371
+ transition: all var(--t-fast) var(--ease);
372
+ }
373
+ .top-link:hover {
374
+ color: var(--text);
375
+ background: var(--surface-hover);
376
+ }
377
+ .top-link svg { width: 12px; height: 12px; }
378
+
379
+ /* Responsive */
380
+ @media (max-width: 980px) {
381
+ .auth-page { grid-template-columns: 1fr; }
382
+ .brand-panel { display: none; }
383
+ }
384
+ @media (max-width: 480px) {
385
+ .form-panel { padding: 20px; }
386
+ .form-card { padding: 28px 24px; }
387
+ .form-card h2 { font-size: 22px; }
388
+ }
389
+ </style>
390
  </head>
391
+ <body class="has-copilot">
392
+
393
+ <div class="auth-page">
394
+
395
+ <a href="/" class="top-link">
396
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
397
+ {{ _("Back") }}
398
+ </a>
399
+
400
+ <!-- Left — brand panel -->
401
+ <aside class="brand-panel">
402
  <div class="brand-head">
403
+ <div class="logo">M</div>
404
+ <div class="name">MMaxAI <span>Hosting</span></div>
405
+ </div>
406
+
407
+ <div class="brand-message">
408
+ <h1>{{ _("Set up your hosting workspace in just a few minutes.") }}</h1>
409
+ <p>
410
+ {{ _("Create an account, claim your invite slot, and start shipping bots to production in minutes — not hours. Your data is encrypted at rest, and your keys stay yours.") }}
411
  </p>
412
  </div>
413
 
414
+ <div class="features">
415
  <div class="feature">
416
+ <div class="ico">
417
+ <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>
418
+ </div>
419
+ <div>
420
  <strong>{{ _("Encrypted at rest") }}</strong>
421
+ <span>{{ _("Bot tokens, HF keys, and NVIDIA keys stored with Fernet encryption.") }}</span>
422
  </div>
423
  </div>
424
  <div class="feature">
425
+ <div class="ico">
426
+ <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>
427
+ </div>
428
+ <div>
429
  <strong>{{ _("Invite-only access") }}</strong>
430
+ <span>{% if invite_required %}{{ _("An invite code is required to register.") }}{% else %}{{ _("Self-serve registration, with optional invite codes for teams.") }}{% endif %}</span>
431
  </div>
432
  </div>
433
  <div class="feature">
434
+ <div class="ico">
435
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="m10.85 12.15 8.65-8.65"/><path d="m18 5 2 2"/><path d="m15 8 2 2"/></svg>
436
+ </div>
437
+ <div>
438
  <strong>{{ _("Bring your own keys") }}</strong>
439
  <span>{{ _("Use your own Telegram, Hugging Face, and NVIDIA credentials.") }}</span>
440
  </div>
441
  </div>
442
  </div>
443
 
444
+ <div class="brand-foot">
445
+ <span class="live"><span class="dot"></span> System operational</span>
446
+ <span>v1 · {{ panel_title }} · Private instance</span>
447
+ </div>
448
+ </aside>
449
+
450
+ <!-- Right — glass form -->
451
+ <section class="form-panel">
452
+ <div class="form-card">
453
+ <h2>{{ _("Create your account") }}</h2>
454
+ <p class="sub">{{ _("Pick a username and a strong password to get started.") }}</p>
455
+
456
+ {% if error %}
457
+ <div class="alert">
458
+ <span class="ico">
459
+ <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>
460
+ </span>
461
+ <div class="alert-body">{{ error }}</div>
462
+ </div>
463
+ {% endif %}
464
+
465
+ <div class="alert" id="client-error" hidden>
466
+ <span class="ico">
467
+ <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>
468
+ </span>
469
+ <div class="alert-body" id="client-error-body"></div>
470
  </div>
 
471
 
472
+ <form method="POST" action="/register" id="register-form" autocomplete="on" novalidate>
473
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 
 
474
 
475
+ <div class="form-group">
476
+ <label for="username">{{ _("Username") }}<span class="req">*</span></label>
477
+ <input type="text" id="username" name="username" required autofocus
478
+ autocomplete="username" placeholder="{{ _("your-username") }}"
479
+ pattern="[a-zA-Z0-9_-]{3,32}"
480
+ autocapitalize="off" autocorrect="off" spellcheck="false">
481
  </div>
 
482
 
483
+ <div class="form-group">
484
+ <label for="email">{{ _("Email") }}</label>
485
+ <input type="email" id="email" name="email"
486
+ autocomplete="email" placeholder="{{ _("you@example.com") }}">
487
+ </div>
488
+
489
+ <div class="form-group">
490
+ <label for="password">{{ _("Password") }}<span class="req">*</span></label>
491
+ <div class="input-with-action">
492
+ <input type="password" id="password" name="password" required
493
+ minlength="8" autocomplete="new-password"
494
+ placeholder="{{ _("At least 8 characters") }}"
495
+ data-caps-warn="caps-warn-pwd"
496
+ data-strength-meter>
497
+ <button type="button" class="input-action"
498
+ id="toggle-password"
499
+ data-password-toggle="password"
500
+ aria-label="{{ _("Show password") }}"
501
+ aria-pressed="false"
502
+ title="{{ _("Show / hide password") }}">
503
+ <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>
504
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  </div>
506
+ <div class="strength" data-strength-bars>
507
+ <span class="strength-bar"></span>
508
+ <span class="strength-bar"></span>
509
+ <span class="strength-bar"></span>
 
 
 
 
 
 
 
 
 
510
  </div>
511
+ <div class="caps-warn" id="caps-warn-pwd" hidden
512
+ role="alert" aria-live="polite">{{ _("⚠ Caps Lock is on") }}</div>
513
+ <span class="help">{{ _("Minimum 8 chars. Mix upper / lower / digits / symbols for strength.") }}</span>
514
+ </div>
515
+
516
+ <div class="form-group">
517
+ <label for="password_confirm">{{ _("Confirm password") }}<span class="req">*</span></label>
518
+ <div class="input-with-action">
519
+ <input type="password" id="password_confirm" name="password_confirm"
520
+ required minlength="8" autocomplete="new-password"
521
+ data-caps-warn="caps-warn-confirm">
522
+ <button type="button" class="input-action"
523
+ id="toggle-password-confirm"
524
+ data-password-toggle="password_confirm"
525
+ aria-label="{{ _("Show password") }}"
526
+ aria-pressed="false"
527
+ title="{{ _("Show / hide password") }}">
528
+ <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>
529
+ </button>
530
  </div>
531
+ <div class="caps-warn" id="caps-warn-confirm" hidden
532
+ role="alert" aria-live="polite">{{ _("⚠ Caps Lock is on") }}</div>
533
+ </div>
534
 
535
+ {% if invite_required %}
536
+ <div class="form-group">
537
+ <label for="invite_code">{{ _("Invite code") }}<span class="req">*</span></label>
538
+ <input type="text" id="invite_code" name="invite_code" required
539
+ placeholder="{{ _("Paste the code you received") }}" autocomplete="off"
540
+ autocapitalize="characters" spellcheck="false">
541
+ <span class="help">{{ _("Ask an existing member for an invite code.") }}</span>
542
  </div>
543
+ {% endif %}
544
+
545
+ <button type="submit" class="btn btn-block btn-lg" id="submit-btn">
546
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
547
+ {{ _("Create account") }}
548
+ </button>
549
+ </form>
550
+
551
+ <div class="form-footer">
552
+ {{ _("Already have an account?") }} <a href="/login">{{ _("Sign in") }}</a>
553
  </div>
 
 
554
 
555
+ <div class="security-badge">
556
+ <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>
557
+ {{ _("Secured by Fernet encryption at rest") }}
558
+ </div>
559
+ </div>
560
+ </section>
561
+ </div>
562
+
563
+ <script>
564
+ (function () {
565
+ // Password show/hide toggles
566
+ document.querySelectorAll('[data-password-toggle]').forEach(function (btn) {
567
+ var inputId = btn.getAttribute('data-password-toggle');
568
+ var input = document.getElementById(inputId);
569
+ if (!input) return;
570
+ btn.addEventListener('click', function () {
571
+ var isHidden = input.type === 'password';
572
+ input.type = isHidden ? 'text' : 'password';
573
+ btn.setAttribute('aria-pressed', String(isHidden));
574
+ btn.setAttribute('aria-label', isHidden ? '{{ _("Hide password") }}' : '{{ _("Show password") }}');
575
+ btn.setAttribute('title', isHidden ? '{{ _("Hide password") }}' : '{{ _("Show password") }}');
576
+ input.focus();
577
+ });
578
+ });
579
+
580
+ // Caps Lock warning
581
+ function syncCaps(e) {
582
+ var warnId = e.target.getAttribute('data-caps-warn');
583
+ if (!warnId) return;
584
+ var warn = document.getElementById(warnId);
585
+ if (!warn) return;
586
+ if (typeof e.getModifierState === 'function') {
587
+ warn.hidden = !e.getModifierState('CapsLock');
588
+ }
589
+ }
590
+ document.querySelectorAll('[data-caps-warn]').forEach(function (input) {
591
+ ['keyup', 'focus', 'blur'].forEach(function (ev) { input.addEventListener(ev, syncCaps); });
592
+ });
593
+
594
+ // Password strength meter (weak / ok / strong by simple entropy heuristic)
595
+ function scorePassword(p) {
596
+ if (!p) return 0;
597
+ var score = 0;
598
+ if (p.length >= 8) score++;
599
+ if (p.length >= 12) score++;
600
+ if (/[a-z]/.test(p) && /[A-Z]/.test(p)) score++;
601
+ if (/\d/.test(p) && /[^A-Za-z0-9]/.test(p)) score++;
602
+ if (/[^A-Za-z0-9]/.test(p) && p.length >= 10) score++;
603
+ if (score >= 4) return 3;
604
+ if (score >= 2) return 2;
605
+ if (score >= 1) return 1;
606
+ return 0;
607
+ }
608
+ var pwdInput = document.getElementById('password');
609
+ var bars = document.querySelectorAll('[data-strength-bars] .strength-bar');
610
+ if (pwdInput && bars.length) {
611
+ pwdInput.addEventListener('input', function () {
612
+ var score = scorePassword(pwdInput.value);
613
+ var classes = ['is-weak', 'is-ok', 'is-strong'];
614
+ bars.forEach(function (bar, idx) {
615
+ classes.forEach(function (c) { bar.classList.remove(c); });
616
+ if (idx < score && score > 0) {
617
+ bar.classList.add(classes[Math.max(0, score - 2)]);
618
+ }
619
+ });
620
+ });
621
+ }
622
+
623
+ // Client-side validation + submit spinner
624
+ var form = document.getElementById('register-form');
625
+ var submitBtn = document.getElementById('submit-btn');
626
+ var clientError = document.getElementById('client-error');
627
+ var clientErrorBody = document.getElementById('client-error-body');
628
+
629
+ function showClientError(msg) {
630
+ if (clientErrorBody) clientErrorBody.textContent = msg;
631
+ if (clientError) clientError.hidden = false;
632
+ window.scrollTo({ top: 0, behavior: 'smooth' });
633
+ }
634
+ function hideClientError() {
635
+ if (clientError) clientError.hidden = true;
636
+ }
637
+
638
+ function validateForm() {
639
+ hideClientError();
640
+ var errors = [];
641
+
642
+ var username = document.getElementById('username');
643
+ var password = document.getElementById('password');
644
+ var passwordConfirm = document.getElementById('password_confirm');
645
+ var inviteCode = document.getElementById('invite_code');
646
+
647
+ if (!username || !username.value.trim()) {
648
+ errors.push('{{ _("Username is required.") }}');
649
+ } else if (!/^[a-zA-Z0-9_-]{3,32}$/.test(username.value.trim())) {
650
+ errors.push('{{ _("Username must be 3–32 characters and contain only letters, numbers, underscores, or hyphens.") }}');
651
+ }
652
+
653
+ if (!password || !password.value) {
654
+ errors.push('{{ _("Password is required.") }}');
655
+ } else if (password.value.length < 8) {
656
+ errors.push('{{ _("Password must be at least 8 characters.") }}');
657
+ }
658
+
659
+ if (!passwordConfirm || !passwordConfirm.value) {
660
+ errors.push('{{ _("Please confirm your password.") }}');
661
+ } else if (password && password.value !== passwordConfirm.value) {
662
+ errors.push('{{ _("Passwords do not match.") }}');
663
+ }
664
+
665
+ if (inviteCode && inviteCode.offsetParent !== null && inviteCode.hasAttribute('required')) {
666
+ if (!inviteCode.value.trim()) {
667
+ errors.push('{{ _("Invite code is required.") }}');
668
+ }
669
+ }
670
+
671
+ if (errors.length) {
672
+ showClientError(errors.join(' '));
673
+ return false;
674
+ }
675
+ return true;
676
+ }
677
+
678
+ if (form && submitBtn) {
679
+ form.addEventListener('submit', function (e) {
680
+ if (submitBtn.disabled) return;
681
+ if (!validateForm()) {
682
+ e.preventDefault();
683
+ return;
684
+ }
685
+ submitBtn.disabled = true;
686
+ submitBtn.classList.add('is-loading');
687
+ submitBtn.innerHTML = '<span class="spinner"></span> {{ _("Creating account…") }}';
688
+ });
689
+ }
690
+ })();
691
+ </script>
692
+
693
  </body>
694
+ </html>