Current state
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
param(
|
||||
[string]$BackupDir = "$PSScriptRoot\..\backups",
|
||||
[int]$RetentionDays = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$startTime = Get-Date
|
||||
|
||||
# Ensure backup directory exists
|
||||
if (-not (Test-Path $BackupDir)) {
|
||||
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Determine database URL from env or .env.local
|
||||
$dbUrl = $env:DATABASE_URL
|
||||
if (-not $dbUrl -and (Test-Path "$PSScriptRoot\..\.env.local")) {
|
||||
$envContent = Get-Content "$PSScriptRoot\..\.env.local" -Raw
|
||||
if ($envContent -match 'DATABASE_URL=(.+)') {
|
||||
$dbUrl = $Matches[1].Trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $dbUrl) {
|
||||
Write-Error "DATABASE_URL not found. Set it as an environment variable or in .env.local"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse the database URL
|
||||
$uri = [System.Uri]$dbUrl
|
||||
$pgUser = $uri.UserInfo.Split(':')[0]
|
||||
$pgPass = $uri.UserInfo.Split(':')[1]
|
||||
$pgHost = $uri.Host
|
||||
$pgPort = $uri.Port
|
||||
$pgDb = $uri.AbsolutePath.TrimStart('/')
|
||||
|
||||
# Set PGPASSWORD for pg_dump
|
||||
$env:PGPASSWORD = $pgPass
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$filename = "crm_backup_$timestamp.sql"
|
||||
$filepath = Join-Path $BackupDir $filename
|
||||
|
||||
Write-Output "Starting backup of database '$pgDb' to $filepath"
|
||||
Write-Output "Database: $pgHost:$pgPort/$pgDb"
|
||||
|
||||
try {
|
||||
# Run pg_dump
|
||||
$pgDumpPath = if (Get-Command pg_dump -ErrorAction SilentlyContinue) {
|
||||
"pg_dump"
|
||||
} else {
|
||||
"$env:TEMP\pg\pgsql\bin\pg_dump.exe"
|
||||
}
|
||||
|
||||
$output = & $pgDumpPath -h $pgHost -p $pgPort -U $pgUser -d $pgDb --format=custom --verbose --file $filename 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
if ($exitCode -ne 0 -or -not (Test-Path $filename)) {
|
||||
throw "pg_dump failed with exit code $exitCode"
|
||||
}
|
||||
|
||||
Move-Item -Path $filename -Destination $filepath -Force
|
||||
$fileInfo = Get-Item $filepath
|
||||
$fileSize = $fileInfo.Length
|
||||
|
||||
# Verify backup by checking if file is valid custom format
|
||||
$verifyOutput = & $pgDumpPath -h $pgHost -p $pgPort -U $pgUser -d $pgDb --format=custom --schema-only --file "$filename.verify" 2>&1
|
||||
$verifyExitCode = $LASTEXITCODE
|
||||
if (Test-Path "$filename.verify") { Remove-Item "$filename.verify" -Force }
|
||||
|
||||
$verificationStatus = if ($verifyExitCode -eq 0) { "verified" } else { "failed" }
|
||||
|
||||
# Calculate duration
|
||||
$endTime = Get-Date
|
||||
$durationSeconds = [math]::Round(($endTime - $startTime).TotalSeconds)
|
||||
|
||||
# Log the backup
|
||||
$logQuery = @"
|
||||
INSERT INTO backup_logs (backup_type, status, file_name, file_size_bytes, pg_dump_exit_code, verification_status, started_at, completed_at, duration_seconds, retention_days)
|
||||
VALUES ('full', 'completed', '$filename', $fileSize, $exitCode, '$verificationStatus', '$($startTime.ToString("yyyy-MM-dd HH:mm:ss"))', '$($endTime.ToString("yyyy-MM-dd HH:mm:ss"))', $durationSeconds, $RetentionDays);
|
||||
"@
|
||||
|
||||
# Try to log to database (may fail if db is not reachable for logging, that's OK)
|
||||
try {
|
||||
$env:PGPASSWORD = $pgPass
|
||||
& $env:TEMP\pg\pgsql\bin\psql.exe -h $pgHost -p $pgPort -U $pgUser -d $pgDb -c $logQuery 2>&1 | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Could not log backup to database: $_"
|
||||
}
|
||||
|
||||
Write-Output "Backup completed successfully:"
|
||||
Write-Output " File: $filename"
|
||||
Write-Output " Size: $([math]::Round($fileSize / 1MB, 2)) MB"
|
||||
Write-Output " Duration: ${durationSeconds}s"
|
||||
Write-Output " Status: $verificationStatus"
|
||||
|
||||
# Cleanup old backups (beyond retention period)
|
||||
$cutoff = (Get-Date).AddDays(-$RetentionDays)
|
||||
$oldBackups = Get-ChildItem $BackupDir -Filter "crm_backup_*.sql" | Where-Object {
|
||||
$_.CreationTime -lt $cutoff
|
||||
}
|
||||
foreach ($old in $oldBackups) {
|
||||
Remove-Item $old.FullName -Force
|
||||
Write-Output "Removed old backup: $($old.Name)"
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Error "Backup failed: $_"
|
||||
|
||||
# Log failure
|
||||
$endTime = Get-Date
|
||||
$durationSeconds = [math]::Round(($endTime - $startTime).TotalSeconds)
|
||||
$errorMsg = $_.ToString().Replace("'", "''")
|
||||
$failQuery = @"
|
||||
INSERT INTO backup_logs (backup_type, status, file_name, error_message, pg_dump_exit_code, verification_status, started_at, completed_at, duration_seconds, retention_days)
|
||||
VALUES ('full', 'failed', '$filename', '$errorMsg', $exitCode, 'failed', '$($startTime.ToString("yyyy-MM-dd HH:mm:ss"))', '$($endTime.ToString("yyyy-MM-dd HH:mm:ss"))', $durationSeconds, $RetentionDays);
|
||||
"@
|
||||
try {
|
||||
$env:PGPASSWORD = $pgPass
|
||||
& $env:TEMP\pg\pgsql\bin\psql.exe -h $pgHost -p $pgPort -U $pgUser -d $pgDb -c $failQuery 2>&1 | Out-Null
|
||||
} catch {}
|
||||
|
||||
exit 1
|
||||
} finally {
|
||||
Remove-Item "Env:PGPASSWORD" -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// ── Ollama Launcher ────────────────────────────────────────────────
|
||||
// Checks if Ollama is already running. If not, finds the binary and
|
||||
// starts it as a detached background process.
|
||||
// Cross-platform: uses tasklist (Windows) or pgrep (Linux/Mac) to
|
||||
// detect running process; uses where/which to find the binary.
|
||||
|
||||
import { execSync, spawn } from "node:child_process"
|
||||
import { platform } from "node:os"
|
||||
|
||||
function isRunning() {
|
||||
try {
|
||||
if (platform() === "win32") {
|
||||
execSync('tasklist /FI "IMAGENAME eq ollama.exe" 2>nul | findstr ollama', { stdio: "pipe", timeout: 3000 })
|
||||
} else {
|
||||
execSync('pgrep -x ollama', { stdio: "pipe", timeout: 3000 })
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function findOllama() {
|
||||
if (platform() === "win32") {
|
||||
// Try PATH first, then common install locations
|
||||
try {
|
||||
return execSync("where ollama", { encoding: "utf8", timeout: 3000 }).trim().split("\n")[0]
|
||||
} catch {}
|
||||
const local = `${process.env.LOCALAPPDATA}\\Programs\\Ollama\\ollama.exe`
|
||||
const programFiles = `${process.env.PROGRAMFILES}\\Ollama\\ollama.exe`
|
||||
if (local) try { execSync(`"${local}" --version`, { stdio: "ignore", timeout: 2000 }); return local } catch {}
|
||||
if (programFiles) try { execSync(`"${programFiles}" --version`, { stdio: "ignore", timeout: 2000 }); return programFiles } catch {}
|
||||
} else {
|
||||
try { return execSync("which ollama", { encoding: "utf8", timeout: 3000 }).trim() } catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isRunning()) {
|
||||
const bin = findOllama()
|
||||
if (!bin) {
|
||||
console.error("Ollama not found. Install from https://ollama.com")
|
||||
process.exit(1)
|
||||
}
|
||||
console.log("Starting Ollama...")
|
||||
// Spawn detached so it outlives this script and the npm process
|
||||
if (platform() === "win32") {
|
||||
spawn(bin, ["serve"], { stdio: "ignore", detached: true, windowsHide: true }).unref()
|
||||
} else {
|
||||
spawn(bin, ["serve"], { stdio: "ignore", detached: true }).unref()
|
||||
}
|
||||
// Give it a moment to start listening
|
||||
execSync("sleep 3", { stdio: "ignore", timeout: 5000 })
|
||||
} else {
|
||||
console.log("Ollama already running")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// ── Browser Opener ─────────────────────────────────────────────────
|
||||
// Opens the splash page in the user's default browser after an 8-second
|
||||
// delay. The delay gives all services time to start before the user
|
||||
// sees the loading screen.
|
||||
// Cross-platform: uses start (Windows), open (Mac), or xdg-open (Linux).
|
||||
|
||||
import { execSync } from "node:child_process"
|
||||
import { platform } from "node:os"
|
||||
|
||||
const url = process.argv[2] || "http://localhost:3001/splash"
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
async function main() {
|
||||
await sleep(8000)
|
||||
try {
|
||||
if (platform() === "win32") {
|
||||
execSync(`start "" "${url}"`, { stdio: "ignore", timeout: 5000 })
|
||||
} else if (platform() === "darwin") {
|
||||
execSync(`open "${url}"`, { stdio: "ignore", timeout: 5000 })
|
||||
} else {
|
||||
execSync(`xdg-open "${url}"`, { stdio: "ignore", timeout: 5000 })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to open browser:", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
// ── Port Precheck ──────────────────────────────────────────────────
|
||||
// Kills any existing processes on ports 3001, 3006, 3007, 3008.
|
||||
// These are the AI server, Next.js frontend, Signaling server, and
|
||||
// Python scraper respectively.
|
||||
// Runs before anything else starts to avoid EADDRINUSE errors.
|
||||
|
||||
import { execSync } from "node:child_process"
|
||||
import { platform } from "node:os"
|
||||
|
||||
const PORTS = [3001, 3006, 3007, 3008]
|
||||
|
||||
if (platform() === "win32") {
|
||||
// Windows: use netstat + findstr to find listening PIDs, then taskkill
|
||||
for (const port of PORTS) {
|
||||
try {
|
||||
const out = execSync(`netstat -ano | findstr "LISTENING" | findstr ":${port} "`, { encoding: "utf8", timeout: 5000 })
|
||||
const lines = out.trim().split("\n").filter(Boolean)
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
const pid = parts[parts.length - 1]
|
||||
if (pid) {
|
||||
try { execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore", timeout: 3000 }); console.log(`Freed port ${port} (PID ${pid})`) } catch {}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} else {
|
||||
// Linux/Mac: use lsof -ti to find PIDs, then kill -9
|
||||
for (const port of PORTS) {
|
||||
try {
|
||||
const pid = execSync(`lsof -ti:${port} 2>/dev/null`, { encoding: "utf8", timeout: 5000 }).trim()
|
||||
if (pid) {
|
||||
execSync(`kill -9 ${pid}`, { stdio: "ignore", timeout: 3000 })
|
||||
console.log(`Freed port ${port} (PID ${pid})`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// ── Python Runner ──────────────────────────────────────────────────
|
||||
// Detects the system's Python executable (python vs python3) and runs
|
||||
// a given script with arguments. Used by the dev:browser-use npm script.
|
||||
// Avoids shell:true — spawns Python directly with its full path.
|
||||
// Cross-platform: uses where (Windows) or which (Linux/Mac).
|
||||
|
||||
import { execSync, spawn } from "node:child_process"
|
||||
import { platform } from "node:os"
|
||||
|
||||
function detectPython() {
|
||||
const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"]
|
||||
for (const cmd of candidates) {
|
||||
try {
|
||||
const out = execSync(platform() === "win32" ? `where ${cmd}` : `which ${cmd}`, { encoding: "utf8", timeout: 5000 })
|
||||
const path = out.trim().split("\n")[0].replace(/\r$/, "")
|
||||
if (path) return path
|
||||
} catch {}
|
||||
}
|
||||
console.error("Python not found. Install Python 3 from https://python.org")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const PYTHON = detectPython()
|
||||
const script = process.argv[2]
|
||||
const args = process.argv.slice(3)
|
||||
|
||||
if (!script) {
|
||||
console.error("Usage: node run-python.mjs <script> [args...]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Spawn Python with inherited stdio so the script's output is visible
|
||||
const proc = spawn(PYTHON, [script, ...args], { stdio: "inherit" })
|
||||
proc.on("exit", (code) => process.exit(code ?? 1))
|
||||
@@ -0,0 +1,82 @@
|
||||
// ── One-command Setup ──────────────────────────────────────────────
|
||||
// Run via: npm run setup
|
||||
// Does the following in order:
|
||||
// 1. npm install (Node.js dependencies)
|
||||
// 2. pip install -r requirements.txt (Python dependencies)
|
||||
// 3. playwright install firefox chromium (Playwright browsers)
|
||||
// 4. Copies .env.example to .env.local if not exists
|
||||
//
|
||||
// All steps are cross-platform (Windows, Mac, Linux).
|
||||
// Uses execSync for simplicity since each step blocks the next.
|
||||
|
||||
import { execSync } from "node:child_process"
|
||||
import { existsSync, copyFileSync } from "node:fs"
|
||||
import { platform } from "node:os"
|
||||
|
||||
const SEP = platform() === "win32" ? "&" : ";"
|
||||
|
||||
// Auto-detect Python executable (python vs python3)
|
||||
function detectPython() {
|
||||
const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"]
|
||||
for (const cmd of candidates) {
|
||||
try {
|
||||
execSync(`${cmd} --version`, { stdio: "pipe", timeout: 5000 })
|
||||
return cmd
|
||||
} catch {}
|
||||
}
|
||||
console.error("Python not found. Install Python 3 from https://python.org")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Auto-detect pip (pip vs pip3), fall back to python -m pip
|
||||
function detectPip(python) {
|
||||
const candidates = platform() === "win32" ? ["pip", "pip3"] : ["pip3", "pip"]
|
||||
for (const cmd of candidates) {
|
||||
try {
|
||||
execSync(`${cmd} --version`, { stdio: "pipe", timeout: 5000 })
|
||||
return cmd
|
||||
} catch {}
|
||||
}
|
||||
return `${python} -m pip`
|
||||
}
|
||||
|
||||
const PY = detectPython()
|
||||
const PIP = detectPip(PY)
|
||||
|
||||
function run(cmd, label) {
|
||||
console.log(`\n── ${label} ──`)
|
||||
try {
|
||||
execSync(cmd, { stdio: "inherit", timeout: 120000 })
|
||||
} catch (e) {
|
||||
console.error(` ✗ Failed: ${e.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("=== CoastIT CRM Setup ===\n")
|
||||
|
||||
// 1. Node dependencies
|
||||
run("npm install", "Installing Node.js dependencies")
|
||||
|
||||
// 2. Python dependencies (run from browser-use-service directory)
|
||||
run(`cd browser-use-service ${SEP} ${PIP} install -r requirements.txt`, "Installing Python dependencies")
|
||||
|
||||
// 3. Playwright browsers (Firefox for primary scraping, Chromium for Chrome/Edge/Opera + Agent fallback)
|
||||
run(`${PY} -m playwright install firefox chromium`, "Installing Playwright browsers")
|
||||
|
||||
// 4. .env file — create from template if it doesn't exist
|
||||
if (!existsSync(".env.local")) {
|
||||
console.log("\n── Creating .env.local ──")
|
||||
copyFileSync(".env.example", ".env.local")
|
||||
console.log(" ✓ Created .env.local from .env.example — edit it with your settings")
|
||||
} else {
|
||||
console.log("\n── .env.local already exists, skipping ──")
|
||||
}
|
||||
|
||||
// 5. Remaining manual steps
|
||||
console.log("\n── Next steps ──")
|
||||
console.log(" 1. Make sure PostgreSQL is running with database 'crm'")
|
||||
console.log(" 2. Pull the Ollama model: ollama pull dolphin-llama3:8b")
|
||||
console.log(" 3. Edit .env.local with your settings")
|
||||
console.log(" 4. Run: npm run dev")
|
||||
console.log("\n=== Setup complete! ===")
|
||||
Reference in New Issue
Block a user