Update on auto setup

This commit is contained in:
Ace
2026-06-26 13:07:40 +02:00
parent 5feb95187c
commit 9ce9506e8e
9 changed files with 769 additions and 265 deletions
+139 -139
View File
@@ -5,6 +5,7 @@
<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;
@@ -18,7 +19,8 @@
overflow: hidden;
}
/* Robot */
/* ── Robot Animation ────────────────────────────────────────────── */
/* Animated robot mascot that bobs, swings arms, and blinks while loading */
.robot {
position: relative;
width: 120px;
@@ -128,7 +130,9 @@
100% { transform: translateY(-4px) rotate(8deg); }
}
/* Status indicators */
/* ── 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;
@@ -207,7 +211,7 @@
color: #ef4444;
}
/* Loading text */
/* ── Loading Text ──────────────────────────────────────────────── */
.loading-text {
font-size: 16px;
font-weight: 600;
@@ -222,7 +226,9 @@
50% { opacity: 1; }
}
/* Launch gear (inline, below everything) */
/* ── 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;
@@ -255,7 +261,9 @@
100% { transform: scale(1); opacity: 1; }
}
/* Error state */
/* ── 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;
@@ -294,7 +302,12 @@
display: inline-block;
}
/* ── Setup Wizard ──────────────────────────────────────────── */
/* ── 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;
@@ -416,7 +429,7 @@
</head>
<body>
<!-- Robot -->
<!-- ── Robot Mascot ────────────────────────────────────────────── -->
<div class="robot">
<div class="robot-antenna"></div>
<div class="robot-head">
@@ -431,7 +444,7 @@
</div>
</div>
<!-- Services -->
<!-- ── Service Status Indicators ───────────────────────────────── -->
<div class="services">
<div class="service" id="svc-ai">
<span class="service-name">AI Server</span>
@@ -464,26 +477,27 @@
<div class="loading-text" id="loading-text">Loading...</div>
<!-- Launch gear (inline, below everything) -->
<!-- ── Launch Gear ──────────────────────────────────────────── -->
<div class="launch-overlay" id="launch-overlay">
<div class="launch-gear"></div>
<div class="launch-text">🚀 Launching 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 ── -->
<!-- ══════════════════════════════════════════════════════════════
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 class="setup-dot" id="sdot-5"></div>
</div>
<!-- Step 1: Welcome -->
<!-- Step 1: Environment check -->
<div class="setup-step active" id="sstep-1">
<div class="robot" style="margin-bottom:20px">
<div class="robot-antenna"></div>
@@ -504,43 +518,22 @@
<button class="setup-btn" id="welcome-next" onclick="goStep(2)">Next →</button>
</div>
<!-- Step 2: Browser Profile -->
<!-- Step 2: Browser auto-detection & selection -->
<div class="setup-step" id="sstep-2">
<div class="setup-title">Browser Profile</div>
<div class="setup-subtitle">We need a Firefox or Chrome profile with Facebook logged in</div>
<div class="setup-card">
<div class="check-row">
<span class="check-icon" id="prof-auto-icon"></span>
<span class="check-label">Auto-detected</span>
<span class="check-value" id="prof-auto-val">scanning...</span>
</div>
<div style="margin-top:14px;font-size:13px;opacity:0.5;margin-bottom:8px">Or enter the path manually:</div>
<input class="setup-input" id="prof-input" placeholder="C:\Users\You\AppData\Roaming\Mozilla\Firefox\Profiles\..." oninput="onProfileInput()">
<div style="margin-top:6px;font-size:12px;color:#22d3ee" id="prof-feedback"></div>
<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="prof-next" onclick="saveProfile()" disabled>Next</button>
<button class="setup-btn" id="browser-confirm" onclick="confirmBrowser()" disabled>Confirm</button>
</div>
</div>
<!-- Step 3: Facebook Login -->
<!-- Step 3: Ollama Model Pull -->
<div class="setup-step" id="sstep-3">
<div class="setup-title">Facebook Login</div>
<div class="setup-subtitle">Make sure you're logged into Facebook in your browser</div>
<div class="setup-card" style="text-align:center">
<div style="font-size:48px;margin-bottom:12px" id="fb-icon">🔍</div>
<div style="font-size:14px;opacity:0.7" id="fb-status">Click "Check Login" to verify</div>
<div class="progress-bar" id="fb-progress" style="display:none;margin-top:16px"><div class="progress-fill" id="fb-progress-fill"></div></div>
</div>
<div class="setup-btns">
<button class="setup-btn outline" onclick="goStep(2)">← Back</button>
<button class="setup-btn" id="fb-check-btn" onclick="checkFacebookLogin()">Check Login</button>
</div>
</div>
<!-- Step 4: Ollama Model -->
<div class="setup-step" id="sstep-4">
<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">
@@ -554,8 +547,8 @@
</div>
</div>
<!-- Step 5: Done -->
<div class="setup-step" id="sstep-5">
<!-- 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>
@@ -567,13 +560,18 @@
</div>
<script>
// ══════════════════════════════════════════════════════════════════
// Client-side JavaScript
// ══════════════════════════════════════════════════════════════════
// ── State ──
let setupData = null;
let currentStep = 1;
const TOTAL_STEPS = 5;
const TOTAL_STEPS = 4;
let selectedBrowser = "";
// ── Normal loading state ──
const MAX_ATTEMPTS = 30;
const MAX_ATTEMPTS = 30; // 30 attempts × 2s = 60s timeout
let attempts = 0;
const CHECKS = [
{ id: 'ai', key: 'ai', ready: false, failed: false },
@@ -582,6 +580,7 @@ const CHECKS = [
];
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) {
@@ -593,21 +592,32 @@ function goStep(n) {
el.classList.toggle('current', i + 1 === n);
el.classList.toggle('done', i + 1 < n);
});
if (n === 2) renderBrowserCards();
}
// ── Step 1: Welcome / Env check ──
// ── 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, profile_detected: false, facebook_logged_in: false };
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');
setEnvStatus('profile', setupData.profile_detected, setupData.profile_path || 'Not found');
setEnvStatus('fb', setupData.facebook_logged_in, setupData.facebook_logged_in ? 'Logged in' : 'Not checked');
// 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');
@@ -615,8 +625,6 @@ async function checkEnv() {
return;
}
document.getElementById('setup-wizard').classList.add('active');
// Pre-fill step 2 fields
updateProfileUI();
}
function setEnvStatus(id, ok, label) {
@@ -624,94 +632,85 @@ function setEnvStatus(id, ok, label) {
document.getElementById('env-' + id + '-val').textContent = label;
}
// ── Step 2: Browser Profile ──
function updateProfileUI() {
const icon = document.getElementById('prof-auto-icon');
const val = document.getElementById('prof-auto-val');
const input = document.getElementById('prof-input');
if (setupData.profile_path) {
icon.textContent = '✅';
val.textContent = setupData.profile_path;
input.value = setupData.profile_path;
document.getElementById('prof-next').disabled = false;
} else {
icon.textContent = '❌';
val.textContent = 'None detected';
// ── 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 onProfileInput() {
const val = document.getElementById('prof-input').value.trim();
const fb = document.getElementById('prof-feedback');
const btn = document.getElementById('prof-next');
if (!val) { fb.textContent = ''; btn.disabled = true; return }
fb.textContent = 'Entered: ' + val;
btn.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)'
})
}
async function saveProfile() {
const path = document.getElementById('prof-input').value.trim();
if (!path) return;
const btn = document.getElementById('prof-next');
btn.textContent = 'Saving...';
btn.disabled = true;
// 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({ path }) });
const data = await res.json();
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.profile_detected = true;
setupData.profile_path = path;
goStep(3);
setupData.selected_browser = selectedBrowser
goStep(3)
} else {
document.getElementById('prof-feedback').textContent = 'Error: ' + (data.error || 'failed');
}
} catch { document.getElementById('prof-feedback').textContent = 'Error: could not save'; }
btn.textContent = 'Next →';
btn.disabled = false;
}
// ── Step 3: Facebook Login ──
async function checkFacebookLogin() {
const btn = document.getElementById('fb-check-btn');
const status = document.getElementById('fb-status');
const icon = document.getElementById('fb-icon');
const prog = document.getElementById('fb-progress');
const fill = document.getElementById('fb-progress-fill');
btn.disabled = true;
btn.textContent = 'Checking...';
prog.style.display = 'block';
fill.style.width = '50%';
status.textContent = 'Opening Facebook...';
try {
const path = setupData.profile_path || '';
const res = await fetch('/setup/check-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile_path: path }) });
const data = await res.json();
if (data.logged_in) {
fill.style.width = '100%';
icon.textContent = '✅';
status.textContent = 'You are logged into Facebook!';
status.className = 'status-ok';
setupData.facebook_logged_in = true;
setTimeout(() => goStep(4), 1000);
} else {
fill.style.width = '100%';
icon.textContent = '❌';
status.textContent = 'Not logged in. Log into Facebook in your browser and try again.';
status.className = 'status-err';
btn.textContent = 'Retry';
btn.disabled = false;
alert('Failed to save: ' + (data.error || 'unknown'))
btn.disabled = false
}
} catch {
icon.textContent = '❌';
status.textContent = 'Could not reach the scraper service';
status.className = 'status-err';
btn.textContent = 'Retry';
btn.disabled = false;
alert('Could not save profile')
btn.disabled = false
}
btn.textContent = 'Confirm →'
}
// ── Step 4: Ollama Model ──
// ── 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;
@@ -720,7 +719,6 @@ async function pullModel() {
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();
@@ -745,12 +743,12 @@ async function pollPullProgress() {
const data = await res.json();
fill.style.width = data.progress + '%';
if (data.status === 'done') {
status.textContent = '✅ Model downloaded successfully!';
status.textContent = '✅ Model downloaded!';
status.className = 'status-ok';
btn.textContent = 'Done';
setupData.model_available = true;
pullPolling = false;
setTimeout(() => goStep(5), 1500);
setTimeout(() => goStep(4), 1500);
return;
}
if (data.status === 'failed') {
@@ -769,25 +767,26 @@ async function pollPullProgress() {
}
}
// ── Step 5: Finish ──
// ── 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 ──
function startNormalFlow() {
poll();
}
// ── 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;
}
} catch { for (const svc of CHECKS) svc.ready = false }
}
function updateUI() {
@@ -829,7 +828,8 @@ async function poll() {
}
// ── Boot ──
// Entry point: check environment → shows wizard or starts normal flow
checkEnv();
</script>
</body>
</html>
</html>