File size: 5,718 Bytes
788eab4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482e621
788eab4
 
482e621
 
 
 
788eab4
482e621
 
 
 
 
35e8172
482e621
 
788eab4
482e621
 
 
 
 
 
 
 
 
 
 
 
 
 
788eab4
 
482e621
788eab4
 
 
 
 
 
 
 
 
 
 
482e621
788eab4
482e621
788eab4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482e621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788eab4
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""
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()