252 lines
9.2 KiB
Python
252 lines
9.2 KiB
Python
# version: v3
|
|
"""
|
|
================================================================================
|
|
Quantum Terminal Consumer — Startup Gate & Route Wiring
|
|
================================================================================
|
|
v3 — consumer_gate() now accepts `skip_sync=False`. data_server's lifespan
|
|
calls it with `skip_sync=True` so the auth phase runs synchronously
|
|
(needed for login routing) while the slow sync_all() is kicked off in
|
|
a background thread after lifespan returns. HTTP routes become
|
|
available immediately; frontend polls /api/sync/status for the SYNCING
|
|
pill. Eliminates the "15-minute loading screen" symptom caused by
|
|
persistent 5xx responses on specific data endpoints.
|
|
|
|
v2 — Wire /api/version (version_info.py) so the app can report its running
|
|
version and check for updates via /api/version endpoint.
|
|
Bridges the consumer-specific modules (data_sync_client)
|
|
into the existing data_server.py with MINIMAL changes to the PRO codebase.
|
|
|
|
Usage in data_server.py lifespan:
|
|
from consumer_startup import consumer_gate, wire_consumer_routes
|
|
|
|
# In lifespan, BEFORE provider connect:
|
|
gate_result = consumer_gate()
|
|
if not gate_result["ok"]:
|
|
# Terminal won't render — gate_result has details
|
|
pass
|
|
|
|
# After app creation:
|
|
wire_consumer_routes(app)
|
|
|
|
This module does NOT introduce any calculation triggers.
|
|
================================================================================
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
log = logging.getLogger("mk.consumer_startup")
|
|
|
|
|
|
# ============================================================
|
|
# 1. CONSUMER STARTUP GATE (Rule C2)
|
|
# ============================================================
|
|
|
|
def consumer_gate(skip_sync: bool = False) -> dict:
|
|
"""
|
|
Run the consumer startup gate (Rule C2).
|
|
|
|
Three conditions must be met before terminal renders:
|
|
1. Auth always passes (Quantum Terminal is free)
|
|
2. data_sync_client completes download OR loads from cache
|
|
3. MT5 connection established (handled by existing data_server code)
|
|
|
|
This function handles conditions 1 and 2.
|
|
|
|
v3: when `skip_sync=True`, only the auth phase runs (fast). Caller is
|
|
responsible for running sync in a background thread so the FastAPI
|
|
lifespan isn't blocked by a slow/unhealthy server. `sync_summary`
|
|
will be returned as {"in_progress": True} in that case — populate it
|
|
from the thread when sync completes.
|
|
Returns dict with gate status:
|
|
{
|
|
"ok": bool,
|
|
"auth_result": AuthResult dict,
|
|
"sync_summary": dict,
|
|
"needs_login": bool,
|
|
"needs_update": bool,
|
|
"offline_mode": bool,
|
|
}
|
|
"""
|
|
result = {
|
|
"ok": False,
|
|
"auth_result": None,
|
|
"sync_summary": None,
|
|
"needs_login": False,
|
|
"needs_update": False,
|
|
"offline_mode": False,
|
|
}
|
|
|
|
# ── Gate 1: Auth validation ──
|
|
# Removed in Quantum Terminal: Always authenticated
|
|
result["auth_result"] = {"status": "ok", "message": "Quantum Terminal - Free Edition"}
|
|
result["needs_login"] = False
|
|
|
|
# ── Gate 2: Data sync ──
|
|
# v3: skip_sync=True → lifespan will run sync_all() in a background
|
|
# thread instead, so the HTTP server is responsive immediately.
|
|
if skip_sync:
|
|
result["sync_summary"] = {"in_progress": True}
|
|
else:
|
|
try:
|
|
from data_sync_client import get_sync_client
|
|
sync_client = get_sync_client()
|
|
sync_summary = sync_client.sync_all()
|
|
result["sync_summary"] = sync_summary
|
|
|
|
# Rule C6: format version mismatch blocks terminal
|
|
if not sync_summary.get("format_version_ok", True):
|
|
log.error("Startup gate: FORMAT VERSION MISMATCH — update required")
|
|
result["needs_update"] = True
|
|
result["ok"] = False
|
|
return result
|
|
|
|
if sync_summary.get("offline_mode", False):
|
|
result["offline_mode"] = True
|
|
|
|
except ImportError:
|
|
log.warning("data_sync_client not available — skipping data sync (dev mode)")
|
|
result["sync_summary"] = {"note": "dev mode — no sync"}
|
|
except Exception as e:
|
|
log.error(f"Data sync error: {e}")
|
|
result["sync_summary"] = {"error": str(e)}
|
|
|
|
result["ok"] = True
|
|
log.info("Consumer startup gate PASSED")
|
|
return result
|
|
|
|
|
|
# ============================================================
|
|
# 2. ROUTE WIRING
|
|
# ============================================================
|
|
|
|
def wire_consumer_routes(app):
|
|
"""
|
|
Wire consumer-specific FastAPI routes into the existing app.
|
|
Call this AFTER app = FastAPI() in data_server.py.
|
|
|
|
Adds:
|
|
/api/auth/* — login, register, logout, status, refresh
|
|
/api/sync/* — sync status, cone/options data, staleness, refresh
|
|
/api/consumer/* — consumer-specific status endpoints
|
|
"""
|
|
routes_added = []
|
|
|
|
# Auth routes
|
|
# Removed in Quantum Terminal
|
|
|
|
# Data sync routes
|
|
try:
|
|
from data_sync_client import create_data_sync_routes
|
|
app.include_router(create_data_sync_routes())
|
|
routes_added.append("data_sync")
|
|
log.info("Consumer data sync routes wired: /api/sync/*")
|
|
except ImportError:
|
|
log.warning("data_sync_client not available — sync routes not wired")
|
|
except Exception as e:
|
|
log.error(f"Failed to wire data sync routes: {e}")
|
|
|
|
# v2: version/update-check route
|
|
try:
|
|
from version_info import create_version_router, CURRENT_VERSION
|
|
app.include_router(create_version_router())
|
|
routes_added.append("version")
|
|
log.info(f"Consumer version route wired: /api/version (running v{CURRENT_VERSION})")
|
|
except ImportError:
|
|
log.warning("version_info not available — /api/version not wired")
|
|
except Exception as e:
|
|
log.error(f"Failed to wire version route: {e}")
|
|
|
|
# Consumer status endpoint
|
|
from fastapi import APIRouter
|
|
consumer_router = APIRouter(tags=["consumer"])
|
|
|
|
@consumer_router.get("/api/consumer/status")
|
|
async def consumer_status():
|
|
"""Consumer terminal status — used by frontend to decide what to show."""
|
|
status = {
|
|
"is_consumer": True,
|
|
"routes_available": routes_added,
|
|
}
|
|
|
|
# Auth status
|
|
status["authenticated"] = True
|
|
status["user_email"] = "free@quantumterminal"
|
|
status["subscription_status"] = "active"
|
|
|
|
# Sync status
|
|
try:
|
|
from data_sync_client import get_sync_client
|
|
sc = get_sync_client()
|
|
status["synced_tickers"] = sc.synced_tickers
|
|
status["offline_mode"] = sc.is_offline
|
|
status["format_version_ok"] = sc.format_version_ok
|
|
status["sync_in_progress"] = sc.is_syncing # drives the top-bar pill
|
|
status["last_synced_at"] = sc.last_synced_at
|
|
|
|
staleness = sc.get_overall_staleness()
|
|
status["analysis_date"] = staleness.display_date
|
|
status["staleness_level"] = staleness.level.value
|
|
except Exception:
|
|
status["synced_tickers"] = []
|
|
status["offline_mode"] = True
|
|
status["sync_in_progress"] = False
|
|
|
|
return status
|
|
|
|
@consumer_router.post("/api/consumer/restart")
|
|
async def consumer_restart():
|
|
"""
|
|
Restart the terminal backend process. Spawns a fresh instance and
|
|
exits the current one after a brief delay so the HTTP response can
|
|
flush before the process exits.
|
|
|
|
Rule C1 compliance: this restarts the display server only — no
|
|
calculation modules are involved.
|
|
"""
|
|
import sys, os, subprocess, threading
|
|
import time as _time
|
|
|
|
def _do_restart():
|
|
_time.sleep(1.0)
|
|
try:
|
|
creationflags = 0
|
|
if os.name == "nt":
|
|
creationflags = getattr(subprocess, "DETACHED_PROCESS", 0) | \
|
|
getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
subprocess.Popen(
|
|
[sys.executable] + sys.argv,
|
|
close_fds=True,
|
|
creationflags=creationflags,
|
|
)
|
|
except Exception as e:
|
|
log.error(f"Restart spawn failed: {e}")
|
|
os._exit(0)
|
|
|
|
threading.Thread(target=_do_restart, daemon=True).start()
|
|
log.warning("Consumer restart requested via /api/consumer/restart")
|
|
return {"status": "restarting", "delay_ms": 1000}
|
|
|
|
app.include_router(consumer_router)
|
|
log.info("Consumer status route wired: /api/consumer/status")
|
|
|
|
return routes_added
|
|
|
|
|
|
# ============================================================
|
|
# 3. BLOCKED ENDPOINTS (Rule C1)
|
|
# ============================================================
|
|
|
|
# These endpoint prefixes must NEVER exist in the consumer terminal.
|
|
# consumer_verify.py test script checks for them.
|
|
BLOCKED_ENDPOINT_PREFIXES = [
|
|
"/api/calculate",
|
|
"/api/calibrate",
|
|
"/api/calibration/run",
|
|
"/api/run-backtest",
|
|
"/api/wf/run",
|
|
"/api/flow/calibrate",
|
|
"/api/anchor/calibrate",
|
|
"/api/dev/clear-cache", # calc target clears calculation data
|
|
]
|