mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 03:05:43 +02:00
Merge branch 'main' of https://git.coastit.co.za/caitlin/CRM_ENVR
This commit is contained in:
@@ -256,11 +256,47 @@ const server = http.createServer(async (req, res) => {
|
||||
const { pathname } = parseURL(req)
|
||||
|
||||
try {
|
||||
// GET /splash — loading screen
|
||||
if (req.method === "GET" && pathname === "/splash") {
|
||||
const splashPath = path.join(ROOT, "splash.html")
|
||||
if (fs.existsSync(splashPath)) {
|
||||
const html = fs.readFileSync(splashPath, "utf-8")
|
||||
res.writeHead(200, { "Content-Type": "text/html" })
|
||||
res.end(html)
|
||||
return
|
||||
}
|
||||
sendJSON(res, 200, { status: "ok" })
|
||||
return
|
||||
}
|
||||
|
||||
// GET /health
|
||||
if (req.method === "GET" && pathname === "/health") {
|
||||
return sendJSON(res, 200, { status: "ok", model: MODEL })
|
||||
}
|
||||
|
||||
// GET /status — combined health of all services
|
||||
if (req.method === "GET" && pathname === "/status") {
|
||||
const { default: http } = await import("http")
|
||||
const results = { ai: true }
|
||||
// Check scraper
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const r = http.get("http://127.0.0.1:3008/health", { timeout: 3000 }, (res) => { res.resume(); resolve() })
|
||||
r.on("error", reject)
|
||||
})
|
||||
results.scraper = true
|
||||
} catch { results.scraper = false }
|
||||
// Check frontend
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const r = http.get("http://127.0.0.1:3006", { timeout: 3000 }, (res) => { res.resume(); resolve() })
|
||||
r.on("error", reject)
|
||||
})
|
||||
results.frontend = true
|
||||
} catch { results.frontend = false }
|
||||
return sendJSON(res, 200, results)
|
||||
}
|
||||
|
||||
// GET /ai/jobs
|
||||
if (req.method === "GET" && pathname === "/ai/jobs") {
|
||||
return sendJSON(res, 200, { jobs: loadedJobs })
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "npm run dev:precheck & npm run dev:ollama & npm run dev:start",
|
||||
"dev:signaling": "node signaling-server.mjs",
|
||||
"dev:start": "concurrently -n AI,BROWSE,SIGNAL,NEXT -c cyan,magenta,yellow,green \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:signaling\" \"npm run dev:next\"",
|
||||
"dev:open": "powershell -NoProfile -Command \"Start-Sleep 8; Start-Process 'http://localhost:3001/splash'\"",
|
||||
"dev:start": "concurrently -n AI,BROWSE,SIGNAL,NEXT,OPEN -c cyan,magenta,yellow,green,white \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:signaling\" \"npm run dev:next\" \"npm run dev:open\"",
|
||||
"dev:next": "next dev -p 3006",
|
||||
"dev:precheck": "powershell -NoProfile -Command \"Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3001,3006,3007,3008 } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue; Write-Host ('Freed port '+$_.LocalPort) }\"",
|
||||
"dev:ollama": "powershell -NoProfile -Command \"$ollama = if (Get-Command ollama -ErrorAction SilentlyContinue) { 'ollama' } else { Join-Path $env:LOCALAPPDATA 'Programs\\Ollama\\ollama.exe' }; if (-not (Get-Process ollama -ErrorAction SilentlyContinue)) { Start-Process $ollama -ArgumentList 'serve' -WindowStyle Hidden; Start-Sleep 3 }; exit 0\"",
|
||||
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
<!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>
|
||||
* { 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 */
|
||||
.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 */
|
||||
.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 (inline, below everything) */
|
||||
.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 */
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Robot -->
|
||||
<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>
|
||||
|
||||
<!-- Services -->
|
||||
<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 (inline, below everything) -->
|
||||
<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>
|
||||
|
||||
<script>
|
||||
const MAX_ATTEMPTS = 30;
|
||||
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' };
|
||||
|
||||
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;
|
||||
let anyFailed = false;
|
||||
const failedNames = [];
|
||||
|
||||
for (const svc of CHECKS) {
|
||||
const el = document.getElementById('svc-' + svc.id);
|
||||
const statusText = document.getElementById('status-' + svc.id);
|
||||
el.classList.remove('ready', 'failed');
|
||||
|
||||
if (svc.ready) {
|
||||
el.classList.add('ready');
|
||||
statusText.textContent = MSGS[svc.id];
|
||||
} else if (svc.failed) {
|
||||
el.classList.add('failed');
|
||||
statusText.textContent = 'Failed';
|
||||
anyFailed = true;
|
||||
allReady = false;
|
||||
failedNames.push(NAMES[svc.id]);
|
||||
} else {
|
||||
statusText.textContent = 'waiting...';
|
||||
allReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
const loadingText = document.getElementById('loading-text');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
const retryBtn = document.getElementById('retry-btn');
|
||||
|
||||
if (anyFailed) {
|
||||
loadingText.className = 'loading-text error';
|
||||
loadingText.textContent = 'Something went wrong';
|
||||
errorMsg.textContent = failedNames.join(', ') + ' failed to start. Check your terminal for details.';
|
||||
errorMsg.classList.add('active');
|
||||
retryBtn.classList.add('active');
|
||||
document.getElementById('launch-overlay').classList.remove('active');
|
||||
} else if (allReady) {
|
||||
loadingText.className = 'loading-text';
|
||||
loadingText.textContent = 'All systems ready!';
|
||||
errorMsg.classList.remove('active');
|
||||
retryBtn.classList.remove('active');
|
||||
document.getElementById('launch-overlay').classList.add('active');
|
||||
setTimeout(() => { window.location.href = 'http://localhost:3006/login'; }, 5000);
|
||||
} else {
|
||||
loadingText.className = 'loading-text';
|
||||
const ready = CHECKS.filter(s => s.ready).length;
|
||||
loadingText.textContent = `Loading... (${ready}/${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);
|
||||
}
|
||||
}
|
||||
|
||||
poll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user