Updated Ai to pull from facebook, added a loading SVG to make user wait for server too boot up, and added commands that can be used to start off the AI exstraction

This commit is contained in:
Ace
2026-06-24 17:17:38 +02:00
parent 8a6ad5c6c6
commit b245b352e7
7 changed files with 307 additions and 88 deletions
+48
View File
@@ -96,7 +96,55 @@ function appendToImprovementLog(entry) {
} }
// ── Chat handler ──────────────────────────────────────────────── // ── Chat handler ────────────────────────────────────────────────
async function scrapeFacebook() {
const profilePath = process.env.FX_PROFILE || ""
const urlPath = `/scrape/facebook?force=true${profilePath ? `&profile_path=${encodeURIComponent(profilePath)}` : ""}`
const logPath = "C:\\Users\\USER-PC\\AppData\\Local\\Temp\\opencode\\ai-scrape-debug.log"
try {
const body = await new Promise((resolve, reject) => {
const req = http.request({ hostname: "127.0.0.1", port: 3008, path: urlPath, method: "POST", timeout: 360000 }, (res) => {
let data = ""
res.on("data", (c) => data += c)
res.on("end", () => resolve(data))
res.on("error", reject)
})
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) })
req.on("error", reject)
req.end()
})
const data = JSON.parse(body)
return data
} catch (e) {
return null
}
}
function formatLeads(leads) {
if (!leads || leads.length === 0) return "No leads found from the latest scrape."
let output = `**${leads.length} leads found:**\n\n`
for (let i = 0; i < leads.length; i++) {
const l = leads[i]
output += `${i + 1}. ${l.title || "No title"}\n`
if (l.author) output += ` Author: ${l.author}\n`
if (l.date) output += ` Date: ${l.date}\n`
if (l.url) output += ` URL: ${l.url}\n`
output += "\n"
}
return output.trim()
}
async function handleChat(userMessage, userId, userRole) { async function handleChat(userMessage, userId, userRole) {
const lowerMsg = userMessage.toLowerCase()
const triggerWords = ["lists", "listings", "leads", "recent leads", "pull leads", "show me leads", "show listings"]
if (triggerWords.some(w => lowerMsg.includes(w))) {
const result = await scrapeFacebook()
if (result && result.success) {
return formatLeads(result.leads)
}
return "Scraper returned no results or encountered an error. Try again later."
}
const jobs = loadedJobs const jobs = loadedJobs
const instructions = readInstructions() const instructions = readInstructions()
+43 -24
View File
@@ -369,7 +369,24 @@ async def _get_article_elements(page) -> list[dict]:
return results; return results;
}''') }''')
async def search_facebook(page, query: str) -> list[dict]: async def _ensure_page(page, context):
try:
await page.evaluate('1')
return page
except Exception:
logger.warning("Page was closed mid-scrape, creating a fresh page")
page = await context.new_page()
try:
await page.goto('https://www.google.com/', wait_until='domcontentloaded', timeout=15000)
await page.wait_for_timeout(random.randint(1000, 3000))
except Exception:
logger.warning("Google navigation failed during page recreation")
await page.goto('https://www.facebook.com/', wait_until='domcontentloaded', timeout=30000)
await page.wait_for_timeout(random.randint(3000, 8000))
return page
async def search_facebook(page, context, query: str):
page = await _ensure_page(page, context)
url = f'https://www.facebook.com/search/posts/?q={urllib.parse.quote(query)}' url = f'https://www.facebook.com/search/posts/?q={urllib.parse.quote(query)}'
try: try:
await page.goto(url, wait_until='domcontentloaded', timeout=30000) await page.goto(url, wait_until='domcontentloaded', timeout=30000)
@@ -400,9 +417,9 @@ async def search_facebook(page, query: str) -> list[dict]:
raw = await page.evaluate('document.body.innerText') raw = await page.evaluate('document.body.innerText')
posts = _extract_posts_from_text(raw, url) posts = _extract_posts_from_text(raw, url)
except Exception as e: except Exception as e:
logger.warning("Facebook search failed: %s", e) logger.warning("Facebook search '%s' failed: %s", query, e)
return [] return page, []
return posts return page, posts
def cleanup_chrome(): def cleanup_chrome():
import subprocess, signal import subprocess, signal
@@ -425,7 +442,7 @@ async def scrape_facebook(profile_path: str | None = None, force: bool = False)
) )
viewport = random.choice(VIEWPORTS) viewport = random.choice(VIEWPORTS)
context = await browser.new_context( context = await browser.new_context(
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0', user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
viewport=viewport, viewport=viewport,
) )
@@ -445,8 +462,11 @@ async def scrape_facebook(profile_path: str | None = None, force: bool = False)
page = await context.new_page() page = await context.new_page()
# Navigate through google first for a legitimate Referer header # Navigate through google first for a legitimate Referer header
await page.goto('https://www.google.com/', wait_until='domcontentloaded', timeout=15000) try:
await page.wait_for_timeout(random.randint(1000, 3000)) await page.goto('https://www.google.com/', wait_until='domcontentloaded', timeout=15000)
await page.wait_for_timeout(random.randint(1000, 3000))
except Exception:
logger.warning("Google navigation failed (IP may be blocked), trying Facebook directly")
await page.goto('https://www.facebook.com/', wait_until='domcontentloaded', timeout=30000) await page.goto('https://www.facebook.com/', wait_until='domcontentloaded', timeout=30000)
await page.wait_for_timeout(random.randint(3000, 8000)) await page.wait_for_timeout(random.randint(3000, 8000))
@@ -472,23 +492,22 @@ async def scrape_facebook(profile_path: str | None = None, force: bool = False)
all_posts = [] all_posts = []
searches = random.sample(FB_SEARCHES, k=random.randint(5, 8)) searches = random.sample(FB_SEARCHES, k=random.randint(5, 8))
for i, query in enumerate(searches): for i, query in enumerate(searches):
try: page, posts = await search_facebook(page, context, query)
posts = await search_facebook(page, query) all_posts.extend(posts)
all_posts.extend(posts) if not posts:
# Between searches: random break with possible small scroll continue
if random.random() < 0.4: # Between searches: random break with possible small scroll
await page.evaluate(f"window.scrollBy(0, {random.randint(-300, 300)})") if random.random() < 0.4:
delay = random.uniform(8, 25) await page.evaluate(f"window.scrollBy(0, {random.randint(-300, 300)})")
await page.wait_for_timeout(int(delay * 1000)) delay = random.uniform(8, 25)
# Tab switch: 15% chance after 2nd-3rd search await page.wait_for_timeout(int(delay * 1000))
if i == random.randint(1, 2) and random.random() < 0.15: # Tab switch: 15% chance after 2nd-3rd search
new_page = await context.new_page() if i == random.randint(1, 2) and random.random() < 0.15:
await new_page.goto('https://www.messenger.com/', wait_until='domcontentloaded', timeout=15000) new_page = await context.new_page()
await new_page.wait_for_timeout(random.randint(3000, 8000)) await new_page.goto('https://www.messenger.com/', wait_until='domcontentloaded', timeout=15000)
await new_page.close() await new_page.wait_for_timeout(random.randint(3000, 8000))
await page.wait_for_timeout(random.randint(1000, 3000)) await new_page.close()
except Exception as e: page = await _ensure_page(page, context)
logger.warning("Facebook search '%s' failed: %s", query, e)
# Idle before closing — human would pause # Idle before closing — human would pause
if random.random() < 0.5: if random.random() < 0.5:
+21
View File
@@ -0,0 +1,21 @@
import asyncio, sys, logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s')
sys.path.insert(0, '.')
from main import get_fb_cookies, scrape_facebook
async def test():
print('Step 1: get cookies')
cookies = await get_fb_cookies()
print(f'Got {len(cookies)} cookies')
if not cookies:
print('No cookies - will still try scrape')
print('Step 2: run scrape_facebook')
result = await scrape_facebook(force=True)
print(f'Result success={result["success"]} leads={len(result["leads"])} error={result.get("error")}')
if result.get('leads'):
for l in result['leads'][:3]:
print(f' - {l["title"][:100]}')
if result.get('error'):
print(f' Error: {result["error"]}')
asyncio.run(test())
+1 -1
View File
@@ -9,7 +9,7 @@
"dev:precheck": "powershell -NoProfile -Command \"Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3001,3006,3008 } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue; Write-Host ('Freed port '+$_.LocalPort) }\"", "dev:precheck": "powershell -NoProfile -Command \"Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3001,3006,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\"", "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\"",
"dev:rust": "node ai-server/index.mjs", "dev:rust": "node ai-server/index.mjs",
"dev:browser-use": "cd browser-use-service && python main.py", "dev:browser-use": "set FX_PROFILE=C:\\Users\\USER-PC\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\h8p11vlj.default-release && cd browser-use-service && python main.py",
"build": "next build", "build": "next build",
"start": "next start -p 3006", "start": "next start -p 3006",
"lint": "eslint" "lint": "eslint"
+25 -4
View File
@@ -1,8 +1,29 @@
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { chatWithAI } from "@/lib/ai"
import { getSessionUser } from "@/lib/auth"
// This route handler has a known issue with Next.js 15 fetch augmentation
// on this platform. The client-side code calls the AI server directly
// via browser fetch (CORS is open). This handler returns a fallback.
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
return NextResponse.json({ error: "AI service unavailable on server. Use client-side mode." }, { status: 503 }) try {
const user = await getSessionUser()
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { message } = await request.json()
if (!message || typeof message !== "string") {
return NextResponse.json({ error: "Message is required" }, { status: 400 })
}
const sessionCookie = request.cookies.get("session")
const jwtToken = sessionCookie?.value
if (!jwtToken) {
return NextResponse.json({ error: "No session token" }, { status: 401 })
}
const response = await chatWithAI(message, jwtToken)
return NextResponse.json({ response })
} catch (error: any) {
console.error("AI chat error:", error)
return NextResponse.json({ error: error.message || "AI service error" }, { status: 500 })
}
} }
+110 -31
View File
@@ -1,7 +1,7 @@
"use client" "use client"
import { useState, useRef, useEffect, Fragment } from "react" import { useState, useRef, useEffect, Fragment } from "react"
import { Send, Loader2, Bot, User, RefreshCw, AlertCircle } from "lucide-react" import { Send, Bot, User, RefreshCw, AlertCircle, Check, Terminal } from "lucide-react"
function linkifyText(text: string) { function linkifyText(text: string) {
const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/ const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/
@@ -24,11 +24,28 @@ export function AIChat() {
const [input, setInput] = useState("") const [input, setInput] = useState("")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
const [ollamaStatus, setOllamaStatus] = useState<boolean | null>(null) const [bootState, setBootState] = useState<"booting" | "ready" | "error">("booting")
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const checkServer = async () => {
try {
const res = await fetch("/api/ai/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "ping" }),
})
if (res.status !== 503) {
setBootState("ready")
} else {
setTimeout(checkServer, 2000)
}
} catch {
setTimeout(checkServer, 2000)
}
}
useEffect(() => { useEffect(() => {
fetch(`${AI_API}/ai/jobs`) fetch("/api/ai/jobs")
.then((r) => r.json()) .then((r) => r.json())
.then((data) => { .then((data) => {
if (data.jobs?.length) { if (data.jobs?.length) {
@@ -56,28 +73,13 @@ export function AIChat() {
}, },
]) ])
}) })
checkServer()
}, []) }, [])
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages]) }, [messages])
const AI_API = process.env.NEXT_PUBLIC_AI_URL || "http://127.0.0.1:3001"
const checkOllama = async () => {
try {
const res = await fetch(`${AI_API}/health`)
const data = await res.json()
setOllamaStatus(data.status === "ok")
} catch {
setOllamaStatus(false)
}
}
useEffect(() => { checkOllama() }, [])
const sendMessage = async () => { const sendMessage = async () => {
const msg = input.trim() const msg = input.trim()
if (!msg || loading) return if (!msg || loading) return
@@ -88,7 +90,7 @@ export function AIChat() {
setLoading(true) setLoading(true)
try { try {
const res = await fetch(`${AI_API}/ai/chat`, { const res = await fetch("/api/ai/chat", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: msg }), body: JSON.stringify({ message: msg }),
@@ -120,17 +122,86 @@ export function AIChat() {
} }
} }
const bootOverlay = (
<div className="flex-1 flex items-center justify-center">
<style>{`
@keyframes walk {
0%, 100% { transform: translateX(0) translateY(0); }
25% { transform: translateX(12px) translateY(-4px); }
50% { transform: translateX(24px) translateY(0); }
75% { transform: translateX(12px) translateY(-4px); }
}
@keyframes legLeft {
0%, 100% { transform: rotate(-10deg); }
50% { transform: rotate(10deg); }
}
@keyframes legRight {
0%, 100% { transform: rotate(10deg); }
50% { transform: rotate(-10deg); }
}
@keyframes armLeft {
0%, 100% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
}
@keyframes armRight {
0%, 100% { transform: rotate(-15deg); }
50% { transform: rotate(15deg); }
}
@keyframes blink {
0%, 45%, 55%, 100% { height: 4px; }
50% { height: 1px; }
}
.robot-walk { animation: walk 0.6s ease-in-out infinite; }
.robot-leg-l { transform-origin: top center; animation: legLeft 0.3s ease-in-out infinite; }
.robot-leg-r { transform-origin: top center; animation: legRight 0.3s ease-in-out infinite; }
.robot-arm-l { transform-origin: top center; animation: armLeft 0.3s ease-in-out infinite; }
.robot-arm-r { transform-origin: top center; animation: armRight 0.3s ease-in-out infinite; }
.robot-eye { animation: blink 2s ease-in-out infinite; }
`}</style>
<div className="flex flex-col items-center gap-4">
<p className="text-sm text-[#6a6a75]">Servers booting...</p>
<div className="h-[50px] w-[100px] bg-[#1a1a24] border border-[#2a2a35] rounded-lg flex items-center justify-center overflow-hidden">
<div className="robot-walk relative">
<svg width="40" height="36" viewBox="0 0 40 36" fill="none">
<rect x="10" y="2" width="20" height="16" rx="3" fill="#1BB0CE" opacity="0.9"/>
<rect x="6" y="6" width="6" height="2" rx="1" className="robot-arm-l" fill="#1BB0CE" opacity="0.7"/>
<rect x="28" y="6" width="6" height="2" rx="1" className="robot-arm-r" fill="#1BB0CE" opacity="0.7"/>
<rect x="14" y="5" width="4" height="4" rx="1" fill="#0d1117"/>
<rect x="22" y="5" width="4" height="4" rx="1" fill="#0d1117"/>
<circle cx="16" cy="7" r="1.5" className="robot-eye" fill="#1BB0CE"/>
<circle cx="24" cy="7" r="1.5" className="robot-eye" fill="#1BB0CE"/>
<rect x="17" y="10" width="6" height="2" rx="1" fill="#0d1117"/>
<rect x="12" y="18" width="5" height="10" rx="2" className="robot-leg-l" fill="#1BB0CE" opacity="0.8"/>
<rect x="23" y="18" width="5" height="10" rx="2" className="robot-leg-r" fill="#1BB0CE" opacity="0.8"/>
</svg>
</div>
</div>
</div>
</div>
)
const readyOverlay = (
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="h-[50px] w-[100px] bg-[#1a1a24] border border-[#2a2a35] rounded-lg flex items-center justify-center">
<Check className="h-6 w-6 text-green-400" />
</div>
<div className="text-center space-y-1">
<p className="text-xs text-[#6a6a75]">Try these commands:</p>
<div className="flex gap-2">
<code className="text-xs px-2 py-1 rounded bg-[#2a2a35] text-[#1BB0CE] flex items-center gap-1"><Terminal className="h-3 w-3" /> lists</code>
<code className="text-xs px-2 py-1 rounded bg-[#2a2a35] text-[#1BB0CE] flex items-center gap-1"><Terminal className="h-3 w-3" /> leads</code>
</div>
</div>
</div>
</div>
)
if (bootState === "booting") return bootOverlay
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{ollamaStatus === false && ( {bootState === "ready" && readyOverlay}
<div className="flex items-center gap-2 px-4 py-2 bg-amber-500/10 border-b border-amber-500/20 text-amber-400 text-xs">
<AlertCircle className="h-3.5 w-3.5 flex-none" />
<span className="flex-1">Ollama not responding. Start it with <code className="bg-amber-500/20 px-1 rounded">ollama serve</code></span>
<button type="button" onClick={checkOllama} className="hover:text-amber-300">
<RefreshCw className="h-3.5 w-3.5" />
</button>
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin"> <div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin">
{messages.map((msg, i) => ( {messages.map((msg, i) => (
@@ -162,7 +233,10 @@ export function AIChat() {
<Bot className="h-4 w-4 text-[#1BB0CE]" /> <Bot className="h-4 w-4 text-[#1BB0CE]" />
</div> </div>
<div className="max-w-[75%] rounded-lg px-4 py-2.5 bg-[#1a1a24] border border-[#2a2a35]"> <div className="max-w-[75%] rounded-lg px-4 py-2.5 bg-[#1a1a24] border border-[#2a2a35]">
<Loader2 className="h-4 w-4 animate-spin text-[#1BB0CE]" /> <svg className="h-4 w-4 animate-spin text-[#1BB0CE]" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" strokeLinecap="round"/>
</svg>
</div> </div>
</div> </div>
)} )}
@@ -191,7 +265,12 @@ export function AIChat() {
disabled={loading || !input.trim()} disabled={loading || !input.trim()}
className="h-9 w-9 rounded-lg bg-[#1BB0CE] hover:bg-[#1BB0CE]/80 disabled:opacity-40 flex items-center justify-center flex-none transition-colors" className="h-9 w-9 rounded-lg bg-[#1BB0CE] hover:bg-[#1BB0CE]/80 disabled:opacity-40 flex items-center justify-center flex-none transition-colors"
> >
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} {loading ? (
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" strokeLinecap="round"/>
</svg>
) : <Send className="h-4 w-4" />}
</button> </button>
</div> </div>
</div> </div>
+58 -27
View File
@@ -1,45 +1,76 @@
import http from "http"
const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001" const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001"
function parseUrl(url: string) {
const u = new URL(url)
return { hostname: u.hostname, port: parseInt(u.port) || 80, path: u.pathname + u.search }
}
export async function chatWithAI(message: string, jwtToken: string) { export async function chatWithAI(message: string, jwtToken: string) {
const url = `${AI_SERVICE}/ai/chat`
const body = JSON.stringify({ message }) const body = JSON.stringify({ message })
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/chat`)
const res = await fetch(url, { const text = await new Promise<string>((resolve, reject) => {
method: "POST", const req = http.request(
headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwtToken}` }, { hostname, port, path, method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}`, "Content-Length": Buffer.byteLength(body) } },
body, (res) => {
let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(data)
} else {
reject(new Error(`AI error ${res.statusCode}: ${data.substring(0, 200)}`))
}
})
}
)
req.on("error", reject)
req.write(body)
req.end()
}) })
const text = await res.text()
if (!res.ok) {
throw new Error(`AI error ${res.status}: ${text.substring(0, 200)}`)
}
const data = JSON.parse(text) const data = JSON.parse(text)
return data.response || "" return data.response || ""
} }
export async function fetchJobs() { export async function checkAiServiceStatus() {
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/health`)
try { try {
const res = await fetch(`${AI_SERVICE}/ai/jobs`) await new Promise<void>((resolve, reject) => {
if (!res.ok) return [] const req = http.get({ hostname, port, path, timeout: 3000 }, (res) => {
const data = await res.json() let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => {
try { resolve(JSON.parse(data).status === "ok" ? undefined : reject(new Error("not ok"))) } catch { reject(new Error("bad response")) }
})
})
req.on("error", reject)
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) })
})
return true
} catch {
return false
}
}
export async function fetchJobs() {
const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/jobs`)
try {
const text = await new Promise<string>((resolve, reject) => {
const req = http.get({ hostname, port, path, timeout: 5000 }, (res) => {
let data = ""
res.on("data", (chunk) => data += chunk)
res.on("end", () => resolve(data))
})
req.on("error", reject)
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) })
})
const data = JSON.parse(text)
return data.jobs || [] return data.jobs || []
} catch { } catch {
console.warn("Failed to fetch AI jobs") console.warn("Failed to fetch AI jobs")
return [] return []
} }
} }
export async function checkAiServiceStatus() {
try {
const res = await fetch(`${AI_SERVICE}/health`)
if (!res.ok) return false
const data = await res.json()
return data.status === "ok"
} catch {
console.warn("Failed to check AI service status")
return false
}
}