Files
tfm_ainventory/start_servers.py
Daniel Bedeleanu b1a63e98ab fix(6): fix TypeScript type consistency issues
- Make Item.id required (items from DB always have id)
- Use shared Item type from db.ts in QuantityAdjustmentModal
- Show full npm build output instead of silencing errors
- Ensures all modals use consistent type definitions
2026-04-22 18:50:06 +03:00

292 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
TFM aInventory Standalone Server Launcher
Starts both backend (FastAPI) and frontend (Next.js) servers
Manages processes, logs, and graceful shutdown
"""
import os
import sys
import subprocess
import signal
import time
from pathlib import Path
class StandaloneServerManager:
def __init__(self):
self.root_dir = Path(__file__).parent
self.backend_process = None
self.frontend_process = None
self.load_config()
self.setup_directories()
def load_config(self):
"""Load configuration from inventory.env"""
env_file = self.root_dir / "inventory.env"
if not env_file.exists():
self.error(f"inventory.env not found at {env_file}")
self.config = {}
with open(env_file) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
self.config[key.strip()] = value.strip().strip('"').strip("'")
self.backend_port = int(self.config.get('BACKEND_PORT', 8000))
self.frontend_port = int(self.config.get('FRONTEND_PORT', 3000))
self.data_dir = self.root_dir / self.config.get('DATA_DIR', 'data')
self.logs_dir = self.root_dir / self.config.get('LOGS_DIR', 'logs')
self.log_level = self.config.get('LOG_LEVEL', 'INFO').lower()
self.log(f"✓ Configuration loaded (Backend: {self.backend_port}, Frontend: {self.frontend_port})")
def setup_directories(self):
"""Create necessary directories"""
self.logs_dir.mkdir(parents=True, exist_ok=True)
self.data_dir.mkdir(parents=True, exist_ok=True)
(self.data_dir / "temp").mkdir(parents=True, exist_ok=True)
self.log(f"✓ Directories ready")
def check_ports(self):
"""Check if required ports are available"""
import socket
for port in [self.backend_port, self.frontend_port]:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('127.0.0.1', port))
sock.close()
if result == 0:
self.error(f"Port {port} is already in use")
except Exception as e:
self.log(f"⚠ Could not check port {port}: {e}")
self.log(f"✓ Required ports available")
def setup_backend(self):
"""Setup Python virtual environment and dependencies"""
venv_dir = self.root_dir / ".venv"
backend_dir = self.root_dir / "backend"
# Create venv if needed
if not venv_dir.exists():
self.log("Creating Python virtual environment...")
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
# Install dependencies
pip_exe = venv_dir / "bin" / "pip"
python_exe = venv_dir / "bin" / "python"
requirements = backend_dir / "requirements.txt"
if not python_exe.exists():
self.error(f"Virtual environment failed - python not found at {python_exe}")
# Check if fastapi is installed
try:
subprocess.run(
[str(python_exe), "-c", "import fastapi"],
check=True,
capture_output=True
)
except subprocess.CalledProcessError:
self.log("Installing Python dependencies...")
subprocess.run([str(pip_exe), "install", "-q", "-r", str(requirements)], check=True)
self.python_exe = python_exe
def setup_frontend(self):
"""Setup frontend dependencies"""
frontend_dir = self.root_dir / "frontend"
# Install dependencies if needed
if not (frontend_dir / "node_modules").exists():
self.log("Installing npm dependencies...")
subprocess.run(
["npm", "ci", "--silent"],
cwd=str(frontend_dir),
check=True,
capture_output=True
)
# Build if needed
if not (frontend_dir / ".next" / "standalone").exists():
self.log("Building Next.js application...")
result = subprocess.run(
["npm", "run", "build"],
cwd=str(frontend_dir),
capture_output=False
)
if result.returncode != 0:
self.error(f"Frontend build failed with exit code {result.returncode}")
def start_backend(self):
"""Start FastAPI backend server using uvicorn from project root"""
self.log(f"Starting backend on port {self.backend_port}...")
backend_log = self.logs_dir / "backend.log"
env = os.environ.copy()
env['PATH'] = f"{self.python_exe.parent}:{env['PATH']}"
# Use uvicorn CLI from project root with full module path (backend.main:app)
with open(backend_log, 'w') as log_f:
self.backend_process = subprocess.Popen(
[
str(self.python_exe),
"-m",
"uvicorn",
"backend.main:app",
"--host", "0.0.0.0",
"--port", str(self.backend_port),
"--log-level", self.log_level
],
cwd=str(self.root_dir),
stdout=log_f,
stderr=subprocess.STDOUT,
env=env,
preexec_fn=os.setsid # Create new process group to avoid signal issues
)
self.log(f"✓ Backend started (PID: {self.backend_process.pid})")
def start_frontend(self):
"""Start Next.js frontend server"""
self.log(f"Starting frontend on port {self.frontend_port}...")
frontend_log = self.logs_dir / "frontend.log"
frontend_dir = self.root_dir / "frontend"
env = os.environ.copy()
env['PORT'] = str(self.frontend_port)
env['NODE_ENV'] = 'production'
with open(frontend_log, 'w') as log_f:
self.frontend_process = subprocess.Popen(
["node", ".next/standalone/server.js"],
cwd=str(frontend_dir),
stdout=log_f,
stderr=subprocess.STDOUT,
env=env,
preexec_fn=os.setsid # Create new process group
)
self.log(f"✓ Frontend started (PID: {self.frontend_process.pid})")
def wait_for_startup(self):
"""Wait a bit and check if servers started"""
time.sleep(3)
# Check backend
try:
import urllib.request
urllib.request.urlopen(f"http://localhost:{self.backend_port}/health", timeout=2)
self.log(f"✓ Backend responding at http://localhost:{self.backend_port}/health")
except Exception:
self.log(f"⚠ Backend not responding yet (may still be initializing)")
# Check frontend
try:
import urllib.request
urllib.request.urlopen(f"http://localhost:{self.frontend_port}/", timeout=2)
self.log(f"✓ Frontend responding at http://localhost:{self.frontend_port}")
except Exception:
self.log(f"⚠ Frontend not responding yet (may still be initializing)")
def print_summary(self):
"""Print summary of running servers"""
print("\n" + "="*60)
print(" ✓ Standalone servers started successfully!")
print("="*60 + "\n")
print("Access points:")
print(f" Frontend: http://localhost:{self.frontend_port}")
print(f" Backend: http://localhost:{self.backend_port}")
print(f" API Docs: http://localhost:{self.backend_port}/docs")
print("\nProcess IDs:")
print(f" Backend: {self.backend_process.pid}")
print(f" Frontend: {self.frontend_process.pid}")
print("\nLog files:")
print(f" Backend: {self.logs_dir / 'backend.log'}")
print(f" Frontend: {self.logs_dir / 'frontend.log'}")
print("\nMonitoring:")
print(f" Backend: tail -f {self.logs_dir / 'backend.log'}")
print(f" Frontend: tail -f {self.logs_dir / 'frontend.log'}")
print(f" All logs: tail -f {self.logs_dir}/*.log")
print("\nTo stop servers:")
print(f" Press Ctrl+C in this terminal")
print("="*60 + "\n")
def run(self):
"""Run the server manager"""
try:
print("\n=== TFM aInventory Standalone Server Start ===\n")
self.check_ports()
self.setup_backend()
self.setup_frontend()
self.start_backend()
self.start_frontend()
self.wait_for_startup()
self.print_summary()
# Monitor processes
self.log("✓ Servers are running! Press Ctrl+C to stop.")
while True:
# Check if processes are alive
if self.backend_process.poll() is not None:
self.log(f"⚠ Backend process exited with code {self.backend_process.returncode}")
self.log(" Check logs/backend.log for details")
if self.frontend_process.poll() is not None:
self.log(f"⚠ Frontend process exited with code {self.frontend_process.returncode}")
self.log(" Check logs/frontend.log for details")
time.sleep(5)
except KeyboardInterrupt:
self.log("Received interrupt signal, shutting down...")
self.cleanup()
except Exception as e:
self.error(str(e))
def cleanup(self):
"""Gracefully shutdown servers"""
import signal
if self.backend_process:
try:
os.killpg(os.getpgid(self.backend_process.pid), signal.SIGTERM)
self.backend_process.wait(timeout=5)
except Exception:
try:
self.backend_process.kill()
except Exception:
pass
if self.frontend_process:
try:
os.killpg(os.getpgid(self.frontend_process.pid), signal.SIGTERM)
self.frontend_process.wait(timeout=5)
except Exception:
try:
self.frontend_process.kill()
except Exception:
pass
self.log("✓ Servers stopped")
sys.exit(0)
def log(self, message):
"""Print log message"""
print(f"[INFO] {message}")
def error(self, message):
"""Print error and exit"""
print(f"[ERROR] {message}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
manager = StandaloneServerManager()
manager.run()