#!/usr/bin/env python3
# Timestamp: 2026-02-02
# File: scitex_dev/dashboard/app.py
"""Flask application factory for the dashboard."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from flask import Flask
[docs]
def create_app() -> Flask:
"""Create and configure the Flask application.
Returns
-------
Flask
Configured Flask application.
"""
try:
from flask import Flask
except ImportError as e:
raise ImportError(
"Flask is required for the dashboard. Install with: pip install flask"
) from e
from pathlib import Path
static_folder = Path(__file__).parent / "static"
app = Flask(__name__, static_folder=str(static_folder), static_url_path="/static")
# Disable JSON key sorting to preserve insertion order (Flask 2.2+)
app.json.sort_keys = False
# Register routes
from .routes import register_routes
register_routes(app)
return app
def _kill_process_on_port(port: int) -> None:
"""Kill any process using the specified port.
Parameters
----------
port : int
Port number to free up.
"""
import subprocess
import sys
try:
if sys.platform == "win32":
# Windows: use netstat and taskkill
result = subprocess.run(
["netstat", "-ano"],
capture_output=True,
text=True,
check=False,
)
for line in result.stdout.splitlines():
if f":{port}" in line and "LISTENING" in line:
pid = line.strip().split()[-1]
subprocess.run(
["taskkill", "/F", "/PID", pid],
capture_output=True,
check=False,
)
print(f"Killed process {pid} on port {port}")
else:
# Unix: use lsof
result = subprocess.run(
["lsof", "-ti", f":{port}"],
capture_output=True,
text=True,
check=False,
)
if result.stdout.strip():
pids = result.stdout.strip().split("\n")
for pid in pids:
subprocess.run(
["kill", "-9", pid], capture_output=True, check=False
)
print(f"Killed process {pid} on port {port}")
except Exception as e:
print(f"Warning: Could not kill process on port {port}: {e}")
[docs]
def run_dashboard(
host: str = "0.0.0.0",
port: int = 5000,
debug: bool = False,
open_browser: bool = True,
force: bool = False,
) -> None:
"""Run the Flask dashboard server.
Parameters
----------
host : str
Host to bind to. Default "127.0.0.1".
port : int
Port to listen on. Default 5000.
debug : bool
Enable Flask debug mode.
open_browser : bool
Open browser automatically.
force : bool
Kill existing process using the port if any.
"""
if force:
_kill_process_on_port(port)
app = create_app()
url = f"http://{host}:{port}"
print(f"Starting SciTeX Version Dashboard at {url}")
print("Press Ctrl+C to stop.")
if open_browser:
import threading
import webbrowser
def open_url():
import time
time.sleep(1) # Wait for server to start
webbrowser.open(url)
threading.Thread(target=open_url, daemon=True).start()
try:
app.run(host=host, port=port, debug=debug, threaded=True)
except KeyboardInterrupt:
print("\nDashboard stopped.")
def run_background(
host: str = "0.0.0.0",
port: int = 5000,
force: bool = False,
) -> None:
"""Launch the dashboard as a detached background subprocess.
Parameters
----------
host : str
Host to bind to. Default "127.0.0.1".
port : int
Port to listen on. Default 5000.
force : bool
Kill existing process using the port if any.
"""
import subprocess
import sys
from scitex_config._ecosystem import local_state
# Runtime-only (per 01_arch_06_local-state-directories.md ยง1):
# logs + pid files go under `runtime/`, never committed to git.
# local_state.runtime_path() auto-creates runtime/ + .gitkeep + README.md.
runtime_dir = local_state.runtime_path("dev")
log_path = runtime_dir / "dashboard.log"
pid_path = runtime_dir / "dashboard.pid"
inline_script = (
f"from scitex_dev.dashboard.app import run_dashboard; "
f"run_dashboard(host={host!r}, port={port!r}, debug=False, open_browser=False, force={force!r})"
)
log_file = open(log_path, "a")
proc = subprocess.Popen(
[sys.executable, "-c", inline_script],
stdout=log_file,
stderr=log_file,
start_new_session=True,
)
pid_path.write_text(str(proc.pid))
def stop_dashboard() -> bool:
"""Stop a running background dashboard process.
Returns
-------
bool
True if the process was successfully stopped, False otherwise.
"""
import os
import signal
from pathlib import Path
pid_path = Path.home() / ".scitex" / "dev" / "runtime" / "dashboard.pid"
if not pid_path.exists():
print("No dashboard PID file found. Is the dashboard running in background?")
return False
try:
pid = int(pid_path.read_text().strip())
os.kill(pid, signal.SIGTERM)
pid_path.unlink()
print(f"Dashboard (PID {pid}) stopped.")
return True
except ProcessLookupError:
print("Process not found. Removing stale PID file.")
pid_path.unlink(missing_ok=True)
return False
except Exception as e:
print(f"Error stopping dashboard: {e}")
return False
# EOF