campus-placement-engine / scripts /lan_launcher.py
deeep-sec's picture
feat: add 100% offline standalone launchers, PWA service worker caching & autoscaling fixes (part 2)
482e621 verified
Raw History Blame Contribute Delete
5.72 kB
"""
Single-Command Local & LAN Institutional Placement Engine Launcher.
Detects LAN network interface, runs migrations, verifies frontend assets,
and launches the unified offline server.
"""
from __future__ import annotations
import os
import socket
import subprocess
import sys
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
def get_primary_lan_ip() -> str:
"""Detects active physical LAN IPv4 address without making outbound internet connections."""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# Routing table lookup to private IP space without sending packets
s.connect(("10.255.255.255", 1))
ip = s.getsockname()[0]
if ip and not ip.startswith("127.") and not ip.startswith("169.254."):
return ip
except Exception:
pass
finally:
s.close()
try:
hostname = socket.gethostname()
candidates = socket.gethostbyname_ex(hostname)[2]
# Prefer physical / non-VMware interfaces
for ip in candidates:
if not ip.startswith("127.") and not ip.startswith("169.254.") and not ip.startswith("192.168.65.") and not ip.startswith("192.168.83."):
return ip
for ip in candidates:
if not ip.startswith("127.") and not ip.startswith("169.254."):
return ip
return "127.0.0.1"
except Exception:
return "127.0.0.1"
def get_all_lan_ips() -> list[str]:
"""Returns all reachable non-loopback IPv4 addresses."""
try:
hostname = socket.gethostname()
candidates = socket.gethostbyname_ex(hostname)[2]
return [ip for ip in candidates if not ip.startswith("127.") and not ip.startswith("169.254.")]
except Exception:
return [get_primary_lan_ip()]
def print_banner(lan_ip: str, port: int = 8000) -> None:
all_ips = get_all_lan_ips()
is_offline = (lan_ip == "127.0.0.1" and len([ip for ip in all_ips if ip != "127.0.0.1"]) == 0)
print("=" * 76)
print(" INSTITUTIONAL PLACEMENT ELIGIBILITY & SCHEDULING ENGINE")
if is_offline:
print(" 100% OFFLINE STANDALONE MODE (NO NETWORK REQUIRED)")
else:
print(" HYBRID PUBLIC CLOUD & INSTITUTIONAL NETWORK DEPLOYMENT")
print("=" * 76)
print(f" [+] Status: ONLINE (READY FOR TRAFFIC)")
if is_offline:
print(f" [+] Network: OFFLINE (NO INTERNET OR LAN REQUIRED)")
else:
print(f" [+] Network: CONNECTED ({lan_ip})")
print(f" [+] Security: Role-Based RBAC & Encrypted Student PII")
print(f" [+] Storage Engine: SQLite WAL Mode (Local Database)")
print(f" [+] Constraint Solver: Google OR-Tools CP-SAT (Local In-Memory)")
print(" " + "-" * 70)
print(f" --> Local Browser URL: http://localhost:{port}")
if not is_offline:
print(f" --> Primary LAN Access: http://{lan_ip}:{port}")
for alt_ip in all_ips:
if alt_ip != lan_ip and not alt_ip.startswith("192.168.65.") and not alt_ip.startswith("192.168.83."):
print(f" --> Alternate LAN/Hotspot: http://{alt_ip}:{port}")
print(" " + "-" * 70)
print(f" SHARE THIS LINK WITH ANYONE ON YOUR WI-FI / LAN NETWORK:")
print(f" >>> http://{lan_ip}:{port} <<<")
else:
print(f" --> Alternate Loopback: http://127.0.0.1:{port}")
print(" " + "-" * 70)
print(" [!] OFFLINE STANDALONE: Running 100% locally on this machine.")
print(f" [!] Open http://localhost:{port} in any browser (Chrome, Edge, Firefox).")
print(" " + "-" * 70)
print(" Default Officer Login: username='officer', password='officer123'")
print(" Default Student Login: roll_no='BT210001' or 'PIPE_001', code='123456'")
print("=" * 76)
sys.stdout.flush()
def main() -> None:
port = int(os.getenv("PORT", "8000"))
host = os.getenv("HOST", "0.0.0.0")
# 1. Run migrations
print("[*] Verifying database schema and migrations...")
try:
subprocess.run([sys.executable, "-m", "alembic", "-c", "backend/alembic.ini", "upgrade", "head"], check=True)
except Exception as exc:
print(f"[!] Migration notice: {exc}. Verifying schema directly...")
from backend.app.storage.database import Base, engine
Base.metadata.create_all(bind=engine)
# 2. Seed baseline accounts
print("[*] Seeding default institutional accounts...")
from backend.app.api.auth.seeder import seed_default_accounts
seed_default_accounts()
# 3. Check frontend dist
dist_index = os.path.abspath(os.path.join(os.path.dirname(__file__), "../frontend/dist/index.html"))
if not os.path.exists(dist_index):
print("[*] Compiled frontend not found. Building production bundle...")
subprocess.run(["npm", "run", "build"], cwd=os.path.abspath(os.path.join(os.path.dirname(__file__), "../frontend")), check=True)
lan_ip = get_primary_lan_ip()
print_banner(lan_ip, port)
# 4. Optional browser auto-open
if os.getenv("AUTO_OPEN_BROWSER", "0") == "1":
try:
import threading
import time
import webbrowser
def _open():
time.sleep(1.2)
webbrowser.open(f"http://localhost:{port}")
threading.Thread(target=_open, daemon=True).start()
except Exception:
pass
# 5. Launch unified server
import uvicorn
uvicorn.run("backend.app.api.main:app", host=host, port=port, log_level="info")
if __name__ == "__main__":
main()