PageSourceSearch

https://agent.slowmist.ai/login

html slowmist.ai collected 2026-10-05 12:39:31 UTC 27,944 bytes, 863 lines download raw bytes

1<!DOCTYPE html>
2<html lang="zh-CN">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6    <title>MistAgent — Sign In</title>
7    <link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
8    <link rel="preconnect" href="https://fonts.googleapis.com">
9    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11    
11<script defer src="https://unpkg.com/[email protected]/dist/umd/lucide.min.js"></script>
11
12    
12<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
12
13    <style>
14        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
15
16        :root {
17            --bg-light: #FAFAFA;
18            --bg-white: #FFFFFF;
19            --bg-subtle: #F5F5F5;
20            --text-black: #000000;
21            --text-dark: #171717;
22            --text-light: #888888;
23            --text-muted: #A1A1A1;
24            --border-light: #EAEAEA;
25            --border-medium: #D4D4D4;
26            --accent-blue: #3B82F6;
27            --danger: #EF4444;
28        }
29
30        body {
31            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
32            background: var(--bg-light);
33            color: var(--text-dark);
34            height: 100vh; height: 100dvh;
35            overflow: hidden;
36            position: relative;
37        }
38
39        /* Watermark background */
40        .watermark {
41            position: fixed;
42            inset: 0;
43            background: url('/static/images/cglogo-large.png') center / contain no-repeat;
44            opacity: 0.08;
45            pointer-events: none;
46            z-index: 0;
47        }
48
49        /* Top navigation bar */
50        .top-bar {
51            position: absolute;
52            top: 0; left: 0; right: 0;
53            height: 56px;
54            display: flex;
55            align-items: center;
56            justify-content: space-between;
57            padding: 0 32px;
58            z-index: 2;
59        }
60
61        .top-logo {
62            display: flex;
63            align-items: center;
64            gap: 10px;
65        }
66
67        .top-logo img {
68            height: 34px;
69            object-fit: contain;
70            filter: contrast(2.2) brightness(0.7);
71        }
72
73        .top-logo span {
74            font-size: 16px;
75            font-weight: 700;
76            color: var(--text-dark);
77            letter-spacing: -0.3px;
78        }
79
80        .top-help {
81            display: flex;
82            align-items: center;
83            gap: 6px;
84            color: var(--text-muted);
85            font-size: 13px;
86            font-weight: 500;
87            cursor: pointer;
88            transition: color 0.2s;
89        }
90
91        .top-help:hover {
92            color: var(--text-light);
93        }
94
95        .top-help i, .top-help svg {
96            width: 16px;
97            height: 16px;
98        }
99
100        /* Center container */
101        .login-wrapper {
102            position: relative;
103            z-index: 1;
104            display: flex;
105            align-items: center;
106            justify-content: center;
107            height: 100vh; height: 100dvh;
108        }
109
110        /* Login card */
111        .login-card {
112            width: 420px;
113            max-width: calc(100vw - 40px);
114            background: rgba(255, 255, 255, 0.25);
115            backdrop-filter: blur(6px);
116            -webkit-backdrop-filter: blur(6px);
117            border: 1px solid var(--border-light);
118            border-radius: 16px;
119            padding: 40px 36px;
120            display: flex;
121            flex-direction: column;
122            align-items: center;
123            gap: 28px;
124            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.06);
125            opacity: 0;
126            transform: translateY(20px);
127            animation: cardIn 0.6s ease-out 0.2s forwards;
128            position: relative;
129            overflow: hidden;
130        }
131
132        /* Card internal watermark — hidden on desktop, visible on mobile */
133        .login-card::before {
134            content: '';
135            position: absolute;
136            inset: 0;
137            background: url('/static/images/cglogo-large.png') center / 120% no-repeat;
138            opacity: 0;
139            pointer-events: none;
140            z-index: 0;
141        }
142
143        .login-card > * {
144            position: relative;
145            z-index: 1;
146        }
147
148        @keyframes cardIn {
149            to {
150                opacity: 1;
151                transform: translateY(0);
152            }
153        }
154
155        /* Header area */
156        .header-area {
157            display: flex;
158            flex-direction: column;
159            align-items: center;
160            gap: 16px;
161            width: 100%;
162        }
163
164        .header-logo {
165            width: 48px;
166            height: 48px;
167            object-fit: contain;
168        }
169
170        .title-group {
171            display: flex;
172            flex-direction: column;
173            align-items: center;
174            gap: 6px;
175        }
176
177        .title-group h1 {
178            font-size: 22px;
179            font-weight: 700;
180            color: var(--text-black);
181            letter-spacing: -0.5px;
182        }
183
184        .title-group p {
185            font-size: 13px;
186            color: var(--text-light);
187            text-align: center;
188        }
189
190        /* Separator */
191        .separator {
192            width: 100%;
193            height: 1px;
194            background: var(--border-light);
195        }
196
197        /* Form */
198        .form-fields {
199            display: flex;
200            flex-direction: column;
201            gap: 18px;
202            width: 100%;
203        }
204
205        .field-group {
206            display: flex;
207            flex-direction: column;
208            gap: 6px;
209        }
210
211        .field-label-row {
212            display: flex;
213            justify-content: space-between;
214            align-items: center;
215        }
216
217        .field-label {
218            font-size: 13px;
219            font-weight: 500;
220            color: var(--text-dark);
221        }
222
223        .forgot-link {
224            font-size: 12px;
225            font-weight: 500;
226            color: var(--accent-blue);
227            text-decoration: none;
228            cursor: pointer;
229            transition: opacity 0.2s;
230        }
231
232        .forgot-link:hover {
233            opacity: 0.8;
234        }
235
236        .field-input-wrapper {
237            display: flex;
238            align-items: center;
239            gap: 10px;
240            width: 100%;
241            height: 44px;
242            background: var(--bg-subtle);
243            border: 1px solid var(--border-light);
244            border-radius: 8px;
245            padding: 0 14px;
246            transition: border-color 0.2s, box-shadow 0.2s;
247        }
248
249        .field-input-wrapper:focus-within {
250            border-color: var(--border-medium);
251            box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.04);
252        }
253
254        .field-input-wrapper i,
255        .field-input-wrapper svg {
256            width: 16px;
257            height: 16px;
258            color: var(--text-muted);
259            flex-shrink: 0;
260        }
261
262        .field-input {
263            flex: 1;
264            border: none;
265            background: transparent;
266            font-family: 'Inter', sans-serif;
267            font-size: 13px;
268            color: var(--text-dark);
269            outline: none;
270        }
271
272        .field-input::placeholder {
273            color: var(--text-muted);
274        }
275
276        /* Override browser autofill blue background */
277        .field-input:-webkit-autofill,
278        .field-input:-webkit-autofill:hover,
279        .field-input:-webkit-autofill:focus {
280            -webkit-box-shadow: 0 0 0 1000px var(--bg-subtle) inset;
281            -webkit-text-fill-color: var(--text-dark);
282            transition: background-color 5000s ease-in-out 0s;
283        }
284
285        .toggle-pw {
286            cursor: pointer;
287            flex-shrink: 0;
288            opacity: 0.6;
289            transition: opacity 0.2s;
290        }
291
292        .toggle-pw:hover {
293            opacity: 1;
294        }
295
296        /* Error message */
297        .error-msg {
298            display: none;
299            width: 100%;
300            padding: 10px 14px;
301            background: rgba(239, 68, 68, 0.08);
302            border: 1px solid rgba(239, 68, 68, 0.2);
303            border-radius: 8px;
304            color: var(--danger);
305            font-size: 12px;
306            text-align: center;
307        }
308
309        .error-msg.visible {
310            display: block;
311        }
312
313        /* Login button */
314        .login-btn {
315            width: 100%;
316            height: 44px;
317            display: flex;
318            align-items: center;
319            justify-content: center;
320            gap: 8px;
321            background: var(--text-black);
322            border: none;
323            border-radius: 8px;
324            color: #FFFFFF;
325            font-family: 'Inter', sans-serif;
326            font-size: 14px;
327            font-weight: 600;
328            cursor: pointer;
329            transition: background 0.2s, transform 0.1s;
330        }
331
332        .login-btn:hover {
333            background: #1a1a1a;
334        }
335
336        .login-btn:active {
337            transform: scale(0.98);
338        }
339
340        .login-btn:disabled {
341            opacity: 0.5;
342            cursor: not-allowed;
343        }
344
345        .login-btn i, .login-btn svg {
346            width: 16px;
347            height: 16px;
348        }
349
350        /* OAuth buttons */
351        .oauth-section {
352            display: none;
353            flex-direction: column;
354            gap: 12px;
355            width: 100%;
356        }
357
358        .oauth-section.visible {
359            display: flex;
360        }
361
362        .oauth-divider {
363            display: flex;
364            align-items: center;
365            gap: 12px;
366            width: 100%;
367            color: var(--text-muted);
368            font-size: 12px;
369        }
370
371        .oauth-divider::before,
372        .oauth-divider::after {
373            content: '';
374            flex: 1;
375            height: 1px;
376            background: var(--border-light);
377        }
378
379        .oauth-btn {
380            width: 100%;
381            height: 44px;
382            display: flex;
383            align-items: center;
384            justify-content: center;
385            gap: 10px;
386            background: var(--bg-white);
387            border: 1px solid var(--border-light);
388            border-radius: 8px;
389            color: var(--text-dark);
390            font-family: 'Inter', sans-serif;
391            font-size: 13px;
392            font-weight: 500;
393            cursor: pointer;
394            transition: background 0.2s, border-color 0.2s;
395        }
396
397        .oauth-btn:hover:not(:disabled) {
398            background: var(--bg-subtle);
399            border-color: var(--border-medium);
400        }
401
402        .oauth-btn:disabled {
403            opacity: 0.45;
404            cursor: not-allowed;
405        }
406
407        .oauth-btn .coming-soon {
408            font-size: 11px;
409            color: var(--text-muted);
410            margin-left: 2px;
411        }
412
413        .oauth-btn svg {
414            width: 18px;
415            height: 18px;
416        }
417
418        /* Sign up row */
419        .signup-row {
420            display: flex;
421            align-items: center;
422            justify-content: center;
423            gap: 4px;
424            font-size: 12px;
425        }
426
427        .signup-row span {
428            color: var(--text-light);
429        }
430
431        .signup-row a {
432            color: var(--text-black);
433            font-weight: 600;
434            text-decoration: none;
435            cursor: pointer;
436        }
437
438        .signup-row a:hover {
439            text-decoration: underline;
440        }
441
442        /* Footer */
443        .footer {
444            position: absolute;
445            bottom: 16px;
446            left: 0;
447            right: 0;
448            text-align: center;
449            font-size: 11px;
450            color: var(--text-muted);
451            z-index: 2;
452        }
453
454        /* Mobile brand tagline — hidden on desktop */
455        .mobile-brand {
456            display: none;
457            text-align: center;
458            padding: 20px 0 0;
459        }
460
461        .mobile-brand .tagline {
462            font-size: 12px;
463            color: var(--text-muted);
464            letter-spacing: 0.5px;
465        }
466
467        .mobile-brand .features {
468            display: flex;
469            justify-content: center;
470            gap: 16px;
471            margin-top: 12px;
472            flex-wrap: wrap;
473        }
474
475        .mobile-brand .feature-item {
476            display: flex;
477            align-items: center;
478            gap: 4px;
479            font-size: 11px;
480            color: var(--text-light);
481        }
482
483        .mobile-brand .feature-item i,
484        .mobile-brand .feature-item svg {
485            width: 12px;
486            height: 12px;
487        }
488
489        /* Responsive */
490        @media (max-width: 480px) {
491            .top-bar {
492                display: none;
493            }
494            .login-card {
495                padding: 24px 20px;
496                border-radius: 12px;
497                gap: 18px;
498            }
499            .login-card::before {
500                opacity: 0.06;
501                background-size: 140%;
502            }
503            .header-area {
504                gap: 10px;
505            }
506            .header-logo {
507                width: 36px;
508                height: 36px;
509            }
510            .title-group h1 {
511                font-size: 18px;
512            }
513            .title-group p {
514                font-size: 12px;
515            }
516            .form-fields {
517                gap: 14px;
518            }
519            .field-input {
520                font-size: 16px;
521            }
522            .mobile-brand {
523                display: block;
524                padding: 12px 0 0;
525            }
526            .footer {
527                position: relative;
528                bottom: auto;
529                padding-top: 8px;
530            }
531            .login-wrapper {
532                height: auto;
533                min-height: 100vh;
534                min-height: 100dvh;
535                padding: 16px 0;
536                flex-direction: column;
537                gap: 0;
538            }
539        }
540    </style>
541</head>
542<body>
543    <!-- Watermark -->
544    <div class="watermark"></div>
545
546    <!-- Top bar -->
547    <div class="top-bar">
548        <a class="top-logo" href="https://slowmist.ai" target="_blank">
549            <img src="/static/images/MistAgent-logo.png" alt="MistAgent">
550        </a>
551        <div class="top-help">
552            <i data-lucide="life-buoy"></i>
553            Need help?
554        </div>
555    </div>
556
557    <!-- Login -->
558    <div class="login-wrapper">
559        <div class="login-card" id="loginCard">
560            <!-- Header -->
561            <div class="header-area">
562                <img class="header-logo" src="/static/images/wuqiong.png" alt="MistAgent">
563                <div class="title-group">
564                    <h1>Welcome to MistAgent</h1>
565                    <p>Sign in to continue to your workspace</p>
566                </div>
567            </div>
568
569            <div class="separator"></div>
570
571            <!-- Error -->
572            <div class="error-msg" id="errorMsg"></div>
573
574            <!-- Form -->
575            <form id="loginForm" autocomplete="on" style="width:100%;display:flex;flex-direction:column;gap:20px;">
576                <div class="form-fields">
577                    <div class="field-group">
578                        <label class="field-label">Email address</label>
579                        <div class="field-input-wrapper">
580                            <i data-lucide="mail"></i>
581                            <input type="email" class="field-input" id="emailInput"
582                                   placeholder="[email protected]" autocomplete="email" autofocus>
583                        </div>
584                    </div>
585
586                    <div class="field-group">
587                        <div class="field-label-row">
588                            <label class="field-label">Password</label>
589                            <a href="/forgot-password" class="forgot-link">Forgot?</a>
590                        </div>
591                        <div class="field-input-wrapper">
592                            <i data-lucide="lock"></i>
593                            <input type="password" class="field-input" id="passwordInput"
594                                   placeholder="••••••••" autocomplete="current-password">
595                            <i data-lucide="eye-off" class="toggle-pw" id="togglePw"></i>
596                        </div>
597                    </div>
598                </div>
599
600                <button type="submit" class="login-btn" id="authBtn">
601                    Sign In
602                    <i data-lucide="arrow-right"></i>
603                </button>
604            </form>
605
606            <!-- OAuth -->
607            <div class="oauth-section" id="oauthSection">
608                <div class="oauth-divider">or continue with</div>
609                <button type="button" class="oauth-btn" id="googleBtn" disabled>
610                    <svg viewBox="0 0 24 24">
610<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
611                    Google <span class="coming-soon">(Coming Soon)</span>
612                </button>
613                <button type="button" class="oauth-btn" id="githubBtn" disabled>
614                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
615                    GitHub <span class="coming-soon">(Coming Soon)</span>
616                </button>
617            </div>
618
619            <div class="signup-row">
620                <span>Don't have an account?</span>
621                <a href="/register">Create one</a>
622            </div>
623        </div>
624
625        <!-- Mobile brand tagline -->
626        <div class="mobile-brand">
627            <div class="tagline">AI-powered security analysis platform</div>
628            <div class="features">
629                <span class="feature-item"><i data-lucide="shield-check"></i> Audit</span>
630                <span class="feature-item"><i data-lucide="code"></i> Analysis</span>
631                <span class="feature-item"><i data-lucide="brain"></i> AI Detection</span>
632            </div>
633        </div>
634    </div>
635
636    <!-- Footer -->
637    <div class="footer">&copy; 2026 SlowMist. All rights reserved.</div>
638
639    
639<script>
640    const API_BASE = '/api/v1';
641    let supabaseClient = null;
642
643    document.addEventListener('DOMContentLoaded', async () => {
644        lucide.createIcons();
645        setupPasswordToggle();
646        await checkAuthAndBoot();
647    });
648
649    // Password visibility toggle
650    function setupPasswordToggle() {
651        const toggle = document.getElementById('togglePw');
652        const pwInput = document.getElementById('passwordInput');
653        if (!toggle || !pwInput) return;
654
655        toggle.addEventListener('click', () => {
656            const isPassword = pwInput.type === 'password';
657            pwInput.type = isPassword ? 'text' : 'password';
658            toggle.setAttribute('data-lucide', isPassword ? 'eye' : 'eye-off');
659            lucide.createIcons({ nodes: [toggle.parentElement] });
660            const newToggle = toggle.parentElement.querySelector('.toggle-pw');
661            if (newToggle && newToggle !== toggle) {
662                newToggle.addEventListener('click', () => setupPasswordToggle());
663            }
664        });
665    }
666
667    async function checkAuthAndBoot() {
668        // Get auth status + Supabase config
669        try {
670            const statusRes = await fetch(`${API_BASE}/auth/status`);
671            if (statusRes.ok) {
672                const statusData = await statusRes.json();
673                if (!statusData.auth_enabled) {
674                    window.location.href = '/';
675                    return;
676                }
677                // Initialize Supabase client
678                if (statusData.supabase_url && statusData.supabase_anon_key && window.supabase) {
679                    supabaseClient = window.supabase.createClient(
680                        statusData.supabase_url,
681                        statusData.supabase_anon_key
682                    );
683                    // Show OAuth buttons
684                    document.getElementById('oauthSection').classList.add('visible');
685
686                    // Handle OAuth redirect callback (hash fragment)
687                    await handleOAuthCallback();
688                }
689            }
690        } catch {}
691
692        // Check if already logged in
693        const token = localStorage.getItem('auth_token');
694        if (token) {
695            try {
696                const meRes = await fetch(`${API_BASE}/auth/me`, {
697                    headers: { 'Authorization': `Bearer ${token}` }
698                });
699                if (meRes.ok) {
700                    window.location.href = '/';
701                    return;
702                }
703                localStorage.removeItem('auth_token');
704                localStorage.removeItem('refresh_token');
705                localStorage.removeItem('user_info');
706            } catch {}
707        }
708    }
709
710    // Handle OAuth redirect (Supabase puts tokens in URL hash)
711    async function handleOAuthCallback() {
712        if (!supabaseClient) return;
713
714        // Check for hash params from OAuth redirect
715        const hash = window.location.hash;
716        if (hash && hash.includes('access_token')) {
717            try {
718                const { data: { session }, error } = await supabaseClient.auth.getSession();
719                if (session && !error) {
720                    localStorage.setItem('auth_token', session.access_token);
721                    localStorage.setItem('refresh_token', session.refresh_token || '');
722                    // Clear hash
723                    window.history.replaceState(null, '', window.location.pathname);
724                    window.location.href = '/';
725                    return;
726                }
727            } catch {}
728        }
729
730        // Also check URL query params (Supabase PKCE flow)
731        const params = new URLSearchParams(window.location.search);
732        if (params.has('code')) {
733            try {
734                const { data: { session }, error } = await supabaseClient.auth.exchangeCodeForSession(params.get('code'));
735                if (session && !error) {
736                    localStorage.setItem('auth_token', session.access_token);
737                    localStorage.setItem('refresh_token', session.refresh_token || '');
738                    window.history.replaceState(null, '', window.location.pathname);
739                    window.location.href = '/';
740                    return;
741                }
742            } catch {}
743        }
744    }
745
746    // OAuth sign-in
747    async function signInWithOAuth(provider) {
748        if (!supabaseClient) {
749            showError('OAuth not available. Supabase not configured.');
750            return;
751        }
752        try {
753            const { error } = await supabaseClient.auth.signInWithOAuth({
754                provider: provider,
755                options: {
756                    redirectTo: window.location.origin + '/login',
757                },
758            });
759            if (error) showError(error.message);
760        } catch (e) {
761            showError('OAuth error: ' + e.message);
762        }
763    }
764
765    // Error handling
766    function showError(msg) {
767        const el = document.getElementById('errorMsg');
768        el.textContent = msg;
769        el.classList.add('visible');
770        const card = document.getElementById('loginCard');
771        // Keep card visible — cardIn animation (forwards) set opacity:1,
772        // replacing it with shake would revert to base CSS opacity:0
773        card.style.opacity = '1';
774        card.style.transform = 'translateY(0)';
775        card.style.animation = 'none';
776        card.offsetHeight;
777        card.style.animation = 'shake 0.4s ease-out';
778    }
779
780    function hideError() {
781        document.getElementById('errorMsg').classList.remove('visible');
782    }
783
784    // Shake animation
785    const shakeStyle = document.createElement('style');
786    shakeStyle.textContent = `
787        @keyframes shake {
788            0%, 100% { transform: translateX(0); }
789            20% { transform: translateX(-6px); }
790            40% { transform: translateX(6px); }
791            60% { transform: translateX(-4px); }
792            80% { transform: translateX(4px); }
793        }
794    `;
795    document.head.appendChild(shakeStyle);
796
797    // Login form submission (backend proxy — supports username + email)
798    document.getElementById('loginForm').addEventListener('submit', async (e) => {
799        e.preventDefault();
800        hideError();
801
802        const email = document.getElementById('emailInput').value.trim();
803        const password = document.getElementById('passwordInput').value;
804        if (!email || !password) {
805            showError('Please enter both email and password.');
806            return;
807        }
808
809        const btn = document.getElementById('authBtn');
810        btn.disabled = true;
811        const originalHTML = btn.innerHTML;
812        btn.textContent = 'Signing in...';
813
814        try {
815            if (!supabaseClient) {
816                showError('Authentication is not available. Supabase not configured.');
817                return;
818            }
819
820            const { data, error } = await supabaseClient.auth.signInWithPassword({
821                email: email,
822                password: password,
823            });
824
825            if (error) {
826                if (error.message && error.message.toLowerCase().includes('rate limit')) {
827                    showError('Too many login attempts. Please wait a few minutes before trying again.');
828                } else {
829                    showError('Invalid email or password.');
830                }
831                return;
832            }
833
834            if (data.session) {
835                localStorage.setItem('auth_token', data.session.access_token);
836                if (data.session.refresh_token) localStorage.setItem('refresh_token', data.session.refresh_token);
837
838                btn.textContent = 'Success!';
839                btn.style.background = '#22C55E';
840
841                const card = document.getElementById('loginCard');
842                card.style.transition = 'opacity 0.3s, transform 0.3s';
843                card.style.opacity = '0';
844                card.style.transform = 'translateY(-10px)';
845
846                setTimeout(() => { window.location.href = '/'; }, 400);
847                return;
848            }
849
850            showError('Authentication failed.');
851        } catch {
852            showError('Network error. Please try again.');
853        } finally {
854            btn.disabled = false;
855            if (btn.textContent !== 'Success!') {
856                btn.innerHTML = originalHTML;
857                lucide.createIcons({ nodes: [btn] });
858            }
859        }
860    });
861    </script>
861
862</body>
863</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.