mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 11:15:43 +02:00
836 lines
29 KiB
HTML
836 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Loading CoastIT CRM</title>
|
||
<style>
|
||
/* ── Global Reset ──────────────────────────────────────────────── */
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
background: #0a0a1a;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||
color: #fff;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ── Robot Animation ────────────────────────────────────────────── */
|
||
/* Animated robot mascot that bobs, swings arms, and blinks while loading */
|
||
.robot {
|
||
position: relative;
|
||
width: 120px;
|
||
height: 160px;
|
||
margin-bottom: 40px;
|
||
animation: float 2s ease-in-out infinite;
|
||
}
|
||
@keyframes float {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-12px); }
|
||
}
|
||
|
||
.robot-head {
|
||
width: 70px;
|
||
height: 60px;
|
||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||
border-radius: 16px 16px 8px 8px;
|
||
margin: 0 auto 4px;
|
||
position: relative;
|
||
animation: bob 0.6s ease-in-out infinite alternate;
|
||
box-shadow: 0 0 30px rgba(99,102,241,0.3);
|
||
}
|
||
@keyframes bob {
|
||
0% { transform: rotate(-3deg); }
|
||
100% { transform: rotate(3deg); }
|
||
}
|
||
|
||
.robot-eye {
|
||
position: absolute;
|
||
width: 10px;
|
||
height: 10px;
|
||
background: #22d3ee;
|
||
border-radius: 50%;
|
||
top: 20px;
|
||
box-shadow: 0 0 8px #22d3ee;
|
||
animation: blink 3s infinite;
|
||
}
|
||
.robot-eye.left { left: 16px; }
|
||
.robot-eye.right { right: 16px; }
|
||
@keyframes blink {
|
||
0%, 94%, 100% { transform: scaleY(1); }
|
||
97% { transform: scaleY(0.1); }
|
||
}
|
||
|
||
.robot-antenna {
|
||
width: 4px;
|
||
height: 12px;
|
||
background: #8b5cf6;
|
||
margin: -10px auto 0;
|
||
border-radius: 2px;
|
||
animation: antenna-pulse 1s ease-in-out infinite;
|
||
}
|
||
.robot-antenna::after {
|
||
content: '';
|
||
display: block;
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #22d3ee;
|
||
border-radius: 50%;
|
||
margin: -2px auto 0;
|
||
box-shadow: 0 0 10px #22d3ee;
|
||
}
|
||
@keyframes antenna-pulse {
|
||
0%, 100% { opacity: 0.7; }
|
||
50% { opacity: 1; }
|
||
}
|
||
|
||
.robot-body {
|
||
width: 60px;
|
||
height: 50px;
|
||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||
border-radius: 6px;
|
||
margin: 0 auto;
|
||
position: relative;
|
||
box-shadow: 0 0 20px rgba(99,102,241,0.2);
|
||
}
|
||
|
||
.robot-arm {
|
||
position: absolute;
|
||
width: 12px;
|
||
height: 36px;
|
||
background: #6366f1;
|
||
border-radius: 6px;
|
||
top: 6px;
|
||
animation: swing 0.8s ease-in-out infinite alternate;
|
||
}
|
||
.robot-arm.left { left: -16px; transform-origin: top center; }
|
||
.robot-arm.right { right: -16px; transform-origin: top center; animation-delay: 0.4s; }
|
||
@keyframes swing {
|
||
0% { transform: rotate(-25deg); }
|
||
100% { transform: rotate(25deg); }
|
||
}
|
||
|
||
.robot-leg {
|
||
position: absolute;
|
||
width: 14px;
|
||
height: 30px;
|
||
background: #4f46e5;
|
||
border-radius: 4px;
|
||
bottom: -28px;
|
||
animation: step 0.6s ease-in-out infinite alternate;
|
||
}
|
||
.robot-leg.left { left: 8px; }
|
||
.robot-leg.right { right: 8px; animation-delay: 0.3s; }
|
||
@keyframes step {
|
||
0% { transform: translateY(0) rotate(-8deg); }
|
||
100% { transform: translateY(-4px) rotate(8deg); }
|
||
}
|
||
|
||
/* ── Status Indicators ────────────────────────────────────────── */
|
||
/* Shows AI Server, Scraper, and Frontend status dots side by side.
|
||
Each transitions from dim → cyan (ready) or red (failed). */
|
||
.services {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 32px;
|
||
min-width: 300px;
|
||
}
|
||
.service {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 16px;
|
||
background: rgba(255,255,255,0.04);
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(255,255,255,0.06);
|
||
transition: all 0.3s;
|
||
}
|
||
.service.ready {
|
||
border-color: rgba(34,211,238,0.3);
|
||
background: rgba(34,211,238,0.06);
|
||
}
|
||
.service.failed {
|
||
border-color: rgba(239,68,68,0.3);
|
||
background: rgba(239,68,68,0.06);
|
||
}
|
||
|
||
.service-name {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
opacity: 0.7;
|
||
}
|
||
.service.ready .service-name {
|
||
opacity: 1;
|
||
color: #22d3ee;
|
||
}
|
||
.service.failed .service-name {
|
||
opacity: 1;
|
||
color: #ef4444;
|
||
}
|
||
|
||
.status-dots {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
.status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: rgba(255,255,255,0.1);
|
||
transition: all 0.4s;
|
||
}
|
||
.service.ready .status-dot {
|
||
background: #22d3ee;
|
||
box-shadow: 0 0 6px rgba(34,211,238,0.5);
|
||
}
|
||
.service.failed .status-dot {
|
||
background: #ef4444;
|
||
box-shadow: 0 0 6px rgba(239,68,68,0.5);
|
||
}
|
||
.status-dot:nth-child(2) { transition-delay: 0.1s; }
|
||
.status-dot:nth-child(3) { transition-delay: 0.2s; }
|
||
|
||
.status-text {
|
||
font-size: 11px;
|
||
opacity: 0.4;
|
||
min-width: 50px;
|
||
text-align: right;
|
||
}
|
||
.service.ready .status-text {
|
||
opacity: 0.8;
|
||
color: #22d3ee;
|
||
}
|
||
.service.failed .status-text {
|
||
opacity: 0.8;
|
||
color: #ef4444;
|
||
}
|
||
|
||
/* ── Loading Text ──────────────────────────────────────────────── */
|
||
.loading-text {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
background: linear-gradient(135deg, #6366f1, #22d3ee);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
animation: pulse-text 2s ease-in-out infinite;
|
||
}
|
||
@keyframes pulse-text {
|
||
0%, 100% { opacity: 0.6; }
|
||
50% { opacity: 1; }
|
||
}
|
||
|
||
/* ── Launch Gear ────────────────────────────────────────────────── */
|
||
/* Appears below the loading text after all services are ready.
|
||
Shows a spinning gear + "Launching gear!" text, then redirects to /login. */
|
||
.launch-overlay {
|
||
display: none;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-top: 8px;
|
||
}
|
||
.launch-overlay.active {
|
||
display: flex;
|
||
}
|
||
.launch-gear {
|
||
width: 80px;
|
||
height: 80px;
|
||
border: 4px solid #6366f1;
|
||
border-top-color: #22d3ee;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
.launch-text {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #22d3ee;
|
||
animation: scale-in 0.5s ease-out;
|
||
}
|
||
@keyframes scale-in {
|
||
0% { transform: scale(0.5); opacity: 0; }
|
||
100% { transform: scale(1); opacity: 1; }
|
||
}
|
||
|
||
/* ── Error State ────────────────────────────────────────────────── */
|
||
/* When a service fails to start within the timeout (60s), show
|
||
red text, an error message, and a retry button. */
|
||
.loading-text.error {
|
||
background: linear-gradient(135deg, #ef4444, #f97316);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.error-msg {
|
||
display: none;
|
||
font-size: 13px;
|
||
color: #ef4444;
|
||
margin-top: 4px;
|
||
text-align: center;
|
||
}
|
||
.error-msg.active {
|
||
display: block;
|
||
}
|
||
|
||
.retry-btn {
|
||
display: none;
|
||
margin-top: 12px;
|
||
padding: 8px 24px;
|
||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||
border: none;
|
||
border-radius: 8px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.retry-btn:hover {
|
||
opacity: 0.85;
|
||
}
|
||
.retry-btn.active {
|
||
display: inline-block;
|
||
}
|
||
|
||
/* ── Setup Wizard ──────────────────────────────────────────────── */
|
||
/* Full-screen 4-step wizard for first-time setup:
|
||
1. Environment check (Ollama, model, browser, Facebook login)
|
||
2. Browser auto-detection & selection
|
||
3. Ollama model pull with progress bar
|
||
4. Done — close wizard, start normal loading flow */
|
||
.setup-wizard {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: #0a0a1a;
|
||
z-index: 200;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px 20px;
|
||
overflow-y: auto;
|
||
}
|
||
.setup-wizard.active { display: flex; }
|
||
|
||
.setup-step {
|
||
display: none;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
max-width: 520px;
|
||
width: 100%;
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
.setup-step.active { display: flex; }
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||
|
||
.setup-steps {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 32px;
|
||
}
|
||
.setup-dot {
|
||
width: 10px; height: 10px;
|
||
border-radius: 50%;
|
||
background: rgba(255,255,255,0.15);
|
||
transition: all 0.3s;
|
||
}
|
||
.setup-dot.done { background: #22d3ee; box-shadow: 0 0 6px rgba(34,211,238,0.5); }
|
||
.setup-dot.current { background: #6366f1; box-shadow: 0 0 6px rgba(99,102,241,0.5); }
|
||
|
||
.setup-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
|
||
.setup-subtitle { font-size: 14px; opacity: 0.5; margin-bottom: 28px; text-align: center; }
|
||
|
||
.setup-card {
|
||
background: rgba(255,255,255,0.04);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
width: 100%;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.check-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||
}
|
||
.check-row:last-child { border-bottom: none; }
|
||
.check-icon { font-size: 18px; min-width: 24px; text-align: center; }
|
||
.check-label { flex: 1; font-size: 14px; }
|
||
.check-value { font-size: 12px; opacity: 0.5; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
|
||
.setup-input {
|
||
width: 100%;
|
||
padding: 10px 14px;
|
||
background: rgba(255,255,255,0.06);
|
||
border: 1px solid rgba(255,255,255,0.12);
|
||
border-radius: 8px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
outline: none;
|
||
transition: border-color 0.2s;
|
||
}
|
||
.setup-input:focus { border-color: #6366f1; }
|
||
|
||
.setup-btn {
|
||
padding: 10px 28px;
|
||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||
border: none;
|
||
border-radius: 8px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.setup-btn:hover { opacity: 0.85; }
|
||
.setup-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.setup-btn.outline {
|
||
background: transparent;
|
||
border: 1px solid rgba(255,255,255,0.15);
|
||
}
|
||
.setup-btn.outline:hover { background: rgba(255,255,255,0.06); }
|
||
|
||
.setup-btns { display: flex; gap: 12px; margin-top: 8px; }
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: rgba(255,255,255,0.08);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin: 12px 0;
|
||
}
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #6366f1, #22d3ee);
|
||
border-radius: 4px;
|
||
transition: width 0.5s ease;
|
||
width: 0%;
|
||
}
|
||
|
||
.status-ok { color: #22d3ee; }
|
||
.status-err { color: #ef4444; }
|
||
.status-warn { color: #f59e0b; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ── Robot Mascot ────────────────────────────────────────────── -->
|
||
<div class="robot">
|
||
<div class="robot-antenna"></div>
|
||
<div class="robot-head">
|
||
<div class="robot-eye left"></div>
|
||
<div class="robot-eye right"></div>
|
||
</div>
|
||
<div class="robot-body">
|
||
<div class="robot-arm left"></div>
|
||
<div class="robot-arm right"></div>
|
||
<div class="robot-leg left"></div>
|
||
<div class="robot-leg right"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Service Status Indicators ───────────────────────────────── -->
|
||
<div class="services">
|
||
<div class="service" id="svc-ai">
|
||
<span class="service-name">AI Server</span>
|
||
<div class="status-dots">
|
||
<div class="status-dot"></div>
|
||
<div class="status-dot"></div>
|
||
<div class="status-dot"></div>
|
||
</div>
|
||
<span class="status-text" id="status-ai">waiting...</span>
|
||
</div>
|
||
<div class="service" id="svc-scraper">
|
||
<span class="service-name">Scraper</span>
|
||
<div class="status-dots">
|
||
<div class="status-dot"></div>
|
||
<div class="status-dot"></div>
|
||
<div class="status-dot"></div>
|
||
</div>
|
||
<span class="status-text" id="status-scraper">waiting...</span>
|
||
</div>
|
||
<div class="service" id="svc-frontend">
|
||
<span class="service-name">Frontend</span>
|
||
<div class="status-dots">
|
||
<div class="status-dot"></div>
|
||
<div class="status-dot"></div>
|
||
<div class="status-dot"></div>
|
||
</div>
|
||
<span class="status-text" id="status-frontend">waiting...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="loading-text" id="loading-text">Loading...</div>
|
||
|
||
<!-- ── Launch Gear ──────────────────────────────────────────── -->
|
||
<div class="launch-overlay" id="launch-overlay">
|
||
<div class="launch-gear"></div>
|
||
<div class="launch-text">Launching gear!</div>
|
||
</div>
|
||
|
||
<div class="error-msg" id="error-msg"></div>
|
||
<button class="retry-btn" id="retry-btn" onclick="location.reload()">Try Again</button>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
Setup Wizard (4 steps, shown only on first run)
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<div class="setup-wizard" id="setup-wizard">
|
||
<div class="setup-steps">
|
||
<div class="setup-dot current" id="sdot-1"></div>
|
||
<div class="setup-dot" id="sdot-2"></div>
|
||
<div class="setup-dot" id="sdot-3"></div>
|
||
<div class="setup-dot" id="sdot-4"></div>
|
||
</div>
|
||
|
||
<!-- Step 1: Environment check -->
|
||
<div class="setup-step active" id="sstep-1">
|
||
<div class="robot" style="margin-bottom:20px">
|
||
<div class="robot-antenna"></div>
|
||
<div class="robot-head"><div class="robot-eye left"></div><div class="robot-eye right"></div></div>
|
||
<div class="robot-body">
|
||
<div class="robot-arm left"></div><div class="robot-arm right"></div>
|
||
<div class="robot-leg left"></div><div class="robot-leg right"></div>
|
||
</div>
|
||
</div>
|
||
<div class="setup-title">Welcome to CoastIT CRM</div>
|
||
<div class="setup-subtitle">Let's check your environment before we start</div>
|
||
<div class="setup-card" id="env-checks">
|
||
<div class="check-row"><span class="check-icon" id="env-ollama">⏳</span><span class="check-label">Ollama</span><span class="check-value" id="env-ollama-val">checking...</span></div>
|
||
<div class="check-row"><span class="check-icon" id="env-model">⏳</span><span class="check-label">AI Model</span><span class="check-value" id="env-model-val">checking...</span></div>
|
||
<div class="check-row"><span class="check-icon" id="env-profile">⏳</span><span class="check-label">Browser Profile</span><span class="check-value" id="env-profile-val">checking...</span></div>
|
||
<div class="check-row"><span class="check-icon" id="env-fb">⏳</span><span class="check-label">Facebook Login</span><span class="check-value" id="env-fb-val">checking...</span></div>
|
||
</div>
|
||
<button class="setup-btn" id="welcome-next" onclick="goStep(2)">Next →</button>
|
||
</div>
|
||
|
||
<!-- Step 2: Browser auto-detection & selection -->
|
||
<div class="setup-step" id="sstep-2">
|
||
<div class="setup-title">Detect your browser</div>
|
||
<div class="setup-subtitle">We found these browsers with Facebook login. Click one to select it.</div>
|
||
<div class="setup-card" id="browser-list">
|
||
<div id="browser-scanning" style="text-align:center;padding:16px;opacity:0.6">🔍 Scanning for browsers...</div>
|
||
<div id="browser-results" style="display:none"></div>
|
||
</div>
|
||
<div class="setup-btns">
|
||
<button class="setup-btn outline" onclick="goStep(1)">← Back</button>
|
||
<button class="setup-btn" id="browser-confirm" onclick="confirmBrowser()" disabled>Confirm →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: Ollama Model Pull -->
|
||
<div class="setup-step" id="sstep-3">
|
||
<div class="setup-title">AI Model</div>
|
||
<div class="setup-subtitle">We need to pull the AI model for the scraper to work</div>
|
||
<div class="setup-card" style="text-align:center">
|
||
<div style="font-size:14px;margin-bottom:8px;font-weight:600" id="model-name">dolphin-llama3:8b</div>
|
||
<div class="progress-bar"><div class="progress-fill" id="model-progress-fill"></div></div>
|
||
<div style="font-size:12px;opacity:0.5;margin-top:6px" id="model-status">Not downloaded yet</div>
|
||
</div>
|
||
<div class="setup-btns">
|
||
<button class="setup-btn outline" onclick="goStep(3)">← Back</button>
|
||
<button class="setup-btn" id="model-pull-btn" onclick="pullModel()">Pull Model</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 4: Done → Launch -->
|
||
<div class="setup-step" id="sstep-4">
|
||
<div style="font-size:64px;margin-bottom:16px">🎉</div>
|
||
<div class="setup-title">All set!</div>
|
||
<div class="setup-subtitle">Your environment is configured. We'll now start all services.</div>
|
||
<div class="setup-card" id="final-checks" style="text-align:center">
|
||
<div style="font-size:13px;opacity:0.7" id="final-summary">All checks passed</div>
|
||
</div>
|
||
<button class="setup-btn" onclick="finishSetup()" style="margin-top:8px">Launch 🚀</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// Client-side JavaScript
|
||
// ══════════════════════════════════════════════════════════════════
|
||
|
||
// ── State ──
|
||
let setupData = null;
|
||
let currentStep = 1;
|
||
const TOTAL_STEPS = 4;
|
||
let selectedBrowser = "";
|
||
|
||
// ── Normal loading state ──
|
||
const MAX_ATTEMPTS = 30; // 30 attempts × 2s = 60s timeout
|
||
let attempts = 0;
|
||
const CHECKS = [
|
||
{ id: 'ai', key: 'ai', ready: false, failed: false },
|
||
{ id: 'scraper', key: 'scraper', ready: false, failed: false },
|
||
{ id: 'frontend',key: 'frontend', ready: false, failed: false },
|
||
];
|
||
const MSGS = { ai: 'AI Ready', scraper: 'Scraper Ready', frontend: 'Frontend Ready' };
|
||
const NAMES = { ai: 'AI Server', scraper: 'Scraper', frontend: 'Frontend' };
|
||
const BROWSE_ICONS = { firefox: '🦊', opera: '🔵', chrome: '🌐', edge: '🔷' };
|
||
|
||
// ── Step navigation ──
|
||
function goStep(n) {
|
||
currentStep = n;
|
||
document.querySelectorAll('.setup-step').forEach((el, i) => {
|
||
el.classList.toggle('active', i + 1 === n);
|
||
});
|
||
document.querySelectorAll('.setup-dot').forEach((el, i) => {
|
||
el.classList.toggle('current', i + 1 === n);
|
||
el.classList.toggle('done', i + 1 < n);
|
||
});
|
||
if (n === 2) renderBrowserCards();
|
||
}
|
||
|
||
// ── Step 1: Welcome + Environment check ──
|
||
// Fetches /setup/status from the AI server, displays checkmarks/crosses
|
||
// for each environment requirement. If first_run is false, skips the
|
||
// wizard entirely and goes straight to the normal loading flow.
|
||
async function checkEnv() {
|
||
try {
|
||
const res = await fetch('/setup/status');
|
||
setupData = await res.json();
|
||
} catch {
|
||
setupData = { first_run: true, ollama_running: false, model_available: false, facebook_logged_in: false, browsers: {} };
|
||
}
|
||
|
||
setEnvStatus('ollama', setupData.ollama_running, setupData.ollama_running ? 'Running' : 'Not running');
|
||
setEnvStatus('model', setupData.model_available, setupData.model_available ? `${setupData.model_name} ✓` : 'Not pulled');
|
||
|
||
// Browser summary
|
||
const detected = Object.entries(setupData.browsers || {}).filter(([, v]) => v.path)
|
||
const loggedIn = detected.filter(([, v]) => v.logged_in)
|
||
const browserSummary = loggedIn.length
|
||
? loggedIn.map(([b]) => `${b.charAt(0).toUpperCase()+b.slice(1)} ✓`).join(', ')
|
||
: detected.length ? 'Found but not logged into Facebook' : 'None detected'
|
||
setEnvStatus('profile', detected.length > 0, browserSummary)
|
||
setEnvStatus('fb', setupData.facebook_logged_in, setupData.facebook_logged_in ? 'Logged in' : loggedIn.length ? '' : 'Not checked')
|
||
|
||
if (!setupData.first_run) {
|
||
document.getElementById('setup-wizard').classList.remove('active');
|
||
startNormalFlow();
|
||
return;
|
||
}
|
||
document.getElementById('setup-wizard').classList.add('active');
|
||
}
|
||
|
||
function setEnvStatus(id, ok, label) {
|
||
document.getElementById('env-' + id).textContent = ok ? '✅' : '❌';
|
||
document.getElementById('env-' + id + '-val').textContent = label;
|
||
}
|
||
|
||
// ── Step 2: Browser selection ──
|
||
// Renders clickable cards for each detected browser.
|
||
// Auto-selects the first one that has Facebook login.
|
||
// No manual path input needed — all detection is automatic.
|
||
function renderBrowserCards() {
|
||
const scanning = document.getElementById('browser-scanning');
|
||
const results = document.getElementById('browser-results');
|
||
const browsers = setupData.browsers || {};
|
||
const detected = Object.entries(browsers).filter(([, v]) => v.path)
|
||
|
||
if (detected.length === 0) {
|
||
scanning.textContent = '❌ No browsers with Facebook detected. Log in on your browser and click Retry.';
|
||
const retryBtn = document.createElement('button');
|
||
retryBtn.className = 'setup-btn';
|
||
retryBtn.textContent = 'Retry Detection';
|
||
retryBtn.onclick = () => location.reload();
|
||
scanning.after(retryBtn);
|
||
return;
|
||
}
|
||
|
||
scanning.style.display = 'none';
|
||
results.style.display = 'block';
|
||
|
||
// Auto-select first logged-in browser, or first detected
|
||
const firstLoggedIn = detected.find(([, v]) => v.logged_in)
|
||
selectedBrowser = firstLoggedIn ? firstLoggedIn[0] : detected[0][0]
|
||
|
||
results.innerHTML = detected.map(([name, info]) => {
|
||
const isSel = name === selectedBrowser
|
||
const icon = BROWSE_ICONS[name] || '🌐'
|
||
const status = info.logged_in ? '✅ Logged in' : info.path ? '⚠️ Not logged in' : '❌ Not found'
|
||
const pathShort = info.path ? info.path.length > 45 ? '...' + info.path.slice(-42) : info.path : ''
|
||
return `<div class="browser-card ${isSel ? 'selected' : ''}" data-browser="${name}" onclick="pickBrowser('${name}')" style="display:flex;align-items:center;gap:12px;padding:12px;border:2px solid ${isSel ? '#6366f1' : 'rgba(255,255,255,0.08)'};border-radius:10px;margin-bottom:8px;cursor:pointer;transition:all 0.2s">
|
||
<span style="font-size:24px">${icon}</span>
|
||
<div style="flex:1"><strong style="text-transform:capitalize">${name}</strong><div style="font-size:12px;opacity:0.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${pathShort}</div></div>
|
||
<span style="font-size:13px">${status}</span>
|
||
</div>`
|
||
}).join('')
|
||
|
||
document.getElementById('browser-confirm').disabled = false
|
||
}
|
||
|
||
function pickBrowser(name) {
|
||
selectedBrowser = name
|
||
document.querySelectorAll('.browser-card').forEach(el => {
|
||
el.style.borderColor = el.dataset.browser === name ? '#6366f1' : 'rgba(255,255,255,0.08)'
|
||
})
|
||
}
|
||
|
||
// Saves the selected browser to .env.local via the AI server's /setup/profile endpoint
|
||
async function confirmBrowser() {
|
||
if (!selectedBrowser) return
|
||
const btn = document.getElementById('browser-confirm')
|
||
btn.textContent = 'Saving...'
|
||
btn.disabled = true
|
||
|
||
const path = setupData.browsers[selectedBrowser].path
|
||
try {
|
||
const res = await fetch('/setup/profile', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ browser: selectedBrowser, path })
|
||
})
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
setupData.selected_browser = selectedBrowser
|
||
goStep(3)
|
||
} else {
|
||
alert('Failed to save: ' + (data.error || 'unknown'))
|
||
btn.disabled = false
|
||
}
|
||
} catch {
|
||
alert('Could not save profile')
|
||
btn.disabled = false
|
||
}
|
||
btn.textContent = 'Confirm →'
|
||
}
|
||
|
||
// ── Step 3: Ollama Model Pull ──
|
||
// Kicks off an "ollama pull" via the AI server and polls for progress.
|
||
let pullPolling = false;
|
||
async function pullModel() {
|
||
if (pullPolling) return;
|
||
const btn = document.getElementById('model-pull-btn');
|
||
const fill = document.getElementById('model-progress-fill');
|
||
const status = document.getElementById('model-status');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Starting...';
|
||
try {
|
||
const res = await fetch('/setup/ollama/pull', { method: 'POST' });
|
||
const data = await res.json();
|
||
if (data.status === 'already_running') { btn.textContent = 'Already running'; return }
|
||
btn.textContent = 'Downloading...';
|
||
pullPolling = true;
|
||
pollPullProgress();
|
||
} catch {
|
||
status.textContent = 'Failed to start download';
|
||
status.className = 'status-err';
|
||
btn.disabled = false;
|
||
btn.textContent = 'Retry';
|
||
}
|
||
}
|
||
|
||
async function pollPullProgress() {
|
||
const fill = document.getElementById('model-progress-fill');
|
||
const status = document.getElementById('model-status');
|
||
const btn = document.getElementById('model-pull-btn');
|
||
try {
|
||
const res = await fetch('/setup/ollama/pull/progress');
|
||
const data = await res.json();
|
||
fill.style.width = data.progress + '%';
|
||
if (data.status === 'done') {
|
||
status.textContent = '✅ Model downloaded!';
|
||
status.className = 'status-ok';
|
||
btn.textContent = 'Done';
|
||
setupData.model_available = true;
|
||
pullPolling = false;
|
||
setTimeout(() => goStep(4), 1500);
|
||
return;
|
||
}
|
||
if (data.status === 'failed') {
|
||
status.textContent = '❌ Download failed: ' + data.message;
|
||
status.className = 'status-err';
|
||
btn.textContent = 'Retry';
|
||
btn.disabled = false;
|
||
pullPolling = false;
|
||
return;
|
||
}
|
||
status.textContent = data.message || `Downloading... ${data.progress}%`;
|
||
setTimeout(pollPullProgress, 1000);
|
||
} catch {
|
||
status.textContent = 'Checking...';
|
||
setTimeout(pollPullProgress, 2000);
|
||
}
|
||
}
|
||
|
||
// ── Step 4: Finish Setup ──
|
||
// Closes the wizard and starts the normal loading flow.
|
||
function finishSetup() {
|
||
document.getElementById('setup-wizard').classList.remove('active');
|
||
startNormalFlow();
|
||
}
|
||
|
||
// ── Normal Loading Flow ──
|
||
// Polls /status every 2 seconds for up to 60 seconds.
|
||
// Tracks each service's state (ready/waiting/failed).
|
||
// All ready → show launch gear, redirect to /login after 5s.
|
||
// Any failed → show error message + retry button.
|
||
function startNormalFlow() { poll() }
|
||
|
||
async function checkAllServices() {
|
||
try {
|
||
const res = await fetch('/status', { cache: 'no-store' });
|
||
const data = await res.json();
|
||
for (const svc of CHECKS) svc.ready = data[svc.key] === true;
|
||
} catch { for (const svc of CHECKS) svc.ready = false }
|
||
}
|
||
|
||
function updateUI() {
|
||
let allReady = true, anyFailed = false;
|
||
const failedNames = [];
|
||
for (const svc of CHECKS) {
|
||
const el = document.getElementById('svc-' + svc.id);
|
||
const st = document.getElementById('status-' + svc.id);
|
||
el.classList.remove('ready', 'failed');
|
||
if (svc.ready) { el.classList.add('ready'); st.textContent = MSGS[svc.id] }
|
||
else if (svc.failed) { el.classList.add('failed'); st.textContent = 'Failed'; anyFailed = true; allReady = false; failedNames.push(NAMES[svc.id]) }
|
||
else { st.textContent = 'waiting...'; allReady = false }
|
||
}
|
||
const lt = document.getElementById('loading-text'), em = document.getElementById('error-msg'), rb = document.getElementById('retry-btn'), lo = document.getElementById('launch-overlay');
|
||
if (anyFailed) {
|
||
lt.className = 'loading-text error'; lt.textContent = 'Something went wrong';
|
||
em.textContent = failedNames.join(', ') + ' failed to start. Check your terminal.';
|
||
em.classList.add('active'); rb.classList.add('active'); lo.classList.remove('active');
|
||
} else if (allReady) {
|
||
lt.className = 'loading-text'; lt.textContent = 'All systems ready!';
|
||
em.classList.remove('active'); rb.classList.remove('active');
|
||
lo.classList.add('active');
|
||
setTimeout(() => { window.location.href = 'http://localhost:3006/login'; }, 5000);
|
||
} else {
|
||
lt.className = 'loading-text';
|
||
lt.textContent = `Loading... (${CHECKS.filter(s => s.ready).length}/${CHECKS.length})`;
|
||
}
|
||
}
|
||
|
||
async function poll() {
|
||
attempts++;
|
||
if (attempts > MAX_ATTEMPTS) {
|
||
for (const svc of CHECKS) { if (!svc.ready) svc.failed = true }
|
||
updateUI(); return;
|
||
}
|
||
await checkAllServices(); updateUI();
|
||
if (!CHECKS.every(s => s.ready) && !CHECKS.some(s => s.failed))
|
||
setTimeout(poll, 2000);
|
||
}
|
||
|
||
// ── Boot ──
|
||
// Entry point: check environment → shows wizard or starts normal flow
|
||
checkEnv();
|
||
</script>
|
||
</body>
|
||
</html>
|