Scaffold Engine application interfaces

This commit is contained in:
2025-10-22 05:33:30 -06:00
parent fbaca54be8
commit 5ec5ee8f7a
17 changed files with 273 additions and 14 deletions

View File

@@ -10,7 +10,7 @@
- 2.2 Add settings dataclasses for Flask, Socket.IO, and DB paths; inject them via `server.py`.
- 2.3 Commit once the Engine can start with equivalent config but no real routes.
3. Copy Flask application scaffolding
[COMPLETED] 3. Copy Flask application scaffolding
- 3.1 Port proxy/CORS/static setup from `Data/Server/server.py` into Engine `server.py` using dependency injection.
- 3.2 Stub out blueprint/Socket.IO registration hooks that mirror names from legacy code (no logic yet).
- 3.3 Smoke-test app startup via `python Data/Engine/bootstrapper.py` (or Flask CLI) to ensure no regressions.

View File

@@ -10,7 +10,7 @@ The Engine mirrors the legacy defaults so it can boot without additional configu
| --- | --- | --- |
| `BOREALIS_ROOT` | Overrides automatic project root detection. Useful when running from a packaged location. | Directory two levels above `Data/Engine/` |
| `BOREALIS_DATABASE_PATH` | Path to the SQLite database. | `<project_root>/database.db` |
| `BOREALIS_STATIC_ROOT` | Directory that serves static assets for the SPA. | `<project_root>/Data/Server/dist` |
| `BOREALIS_STATIC_ROOT` | Directory that serves static assets for the SPA. | First existing path among `Data/Server/web-interface/build`, `Data/Server/WebUI/build`, `Data/WebUI/build` |
| `BOREALIS_CORS_ALLOWED_ORIGINS` | Comma-delimited list of origins granted CORS access. Use `*` for all origins. | `*` |
| `BOREALIS_FLASK_SECRET_KEY` | Secret key for Flask session signing. | `change-me` |
| `BOREALIS_DEBUG` | Enables debug logging, disables secure-cookie requirements, and allows Werkzeug debug mode. | `false` |
@@ -28,3 +28,7 @@ The Engine mirrors the legacy defaults so it can boot without additional configu
3. The resulting runtime object exposes the Flask app, resolved settings, and optional Socket.IO server. `bootstrapper.main()` runs the appropriate server based on whether Socket.IO is present.
As migration continues, services, repositories, interfaces, and integrations will live under their respective subpackages while maintaining isolation from the legacy server.
## Interface scaffolding
The Engine currently exposes placeholder HTTP blueprints under `Data/Engine/interfaces/http/` (agents, enrollment, tokens, admin, and health) so that future commits can drop in real routes without reshaping the bootstrap wiring. WebSocket namespaces follow the same pattern in `Data/Engine/interfaces/ws/`, with feature-oriented modules (e.g., `agents`, `job_management`) registered by `bootstrapper.bootstrap()` when Socket.IO is available. These stubs intentionally contain no business logic yet—they merely ensure the application factory exercises the full wiring path.

View File

@@ -8,7 +8,11 @@ from typing import Optional
from flask import Flask
from .config import EngineSettings, configure_logging, load_environment
from .interfaces import create_socket_server, register_http_interfaces
from .interfaces import (
create_socket_server,
register_http_interfaces,
register_ws_interfaces,
)
from .server import create_app
@@ -30,6 +34,7 @@ def bootstrap() -> EngineRuntime:
app = create_app(settings)
register_http_interfaces(app)
socketio = create_socket_server(app, settings.socketio)
register_ws_interfaces(socketio)
logger.info("bootstrap-complete")
return EngineRuntime(app=app, settings=settings, socketio=socketio)

View File

@@ -81,7 +81,21 @@ def _resolve_static_root(project_root: Path) -> Path:
candidate = os.getenv("BOREALIS_STATIC_ROOT")
if candidate:
return Path(candidate).expanduser().resolve()
return (project_root / "Data" / "Server" / "dist").resolve()
candidates = (
project_root / "Data" / "Server" / "web-interface" / "build",
project_root / "Data" / "Server" / "WebUI" / "build",
project_root / "Data" / "WebUI" / "build",
)
for path in candidates:
resolved = path.resolve()
if resolved.is_dir():
return resolved
# Fall back to the first candidate even if it does not yet exist so the
# Flask factory still initialises; individual requests will surface 404s
# until an asset build is available, matching the legacy behaviour.
return candidates[0].resolve()
def _parse_origins(raw: str | None) -> Tuple[str, ...]:

View File

@@ -3,9 +3,10 @@
from __future__ import annotations
from .http import register_http_interfaces
from .ws import create_socket_server
from .ws import create_socket_server, register_ws_interfaces
__all__ = [
"register_http_interfaces",
"create_socket_server",
"register_ws_interfaces",
]

View File

@@ -4,6 +4,16 @@ from __future__ import annotations
from flask import Flask
from . import admin, agents, enrollment, health, tokens
_REGISTRARS = (
health.register,
agents.register,
enrollment.register,
tokens.register,
admin.register,
)
def register_http_interfaces(app: Flask) -> None:
"""Attach HTTP blueprints to *app*.
@@ -11,8 +21,8 @@ def register_http_interfaces(app: Flask) -> None:
The implementation is intentionally minimal for the initial scaffolding.
"""
# Future phases will import and register blueprints here.
return None
for registrar in _REGISTRARS:
registrar(app)
__all__ = ["register_http_interfaces"]

View File

@@ -0,0 +1,21 @@
"""Administrative HTTP interface placeholders for the Engine."""
from __future__ import annotations
from flask import Blueprint, Flask
blueprint = Blueprint("engine_admin", __name__, url_prefix="/api/admin")
def register(app: Flask) -> None:
"""Attach administrative routes to *app*.
Concrete endpoints will be migrated in subsequent phases.
"""
if "engine_admin" not in app.blueprints:
app.register_blueprint(blueprint)
__all__ = ["register", "blueprint"]

View File

@@ -0,0 +1,21 @@
"""Agent HTTP interface placeholders for the Engine."""
from __future__ import annotations
from flask import Blueprint, Flask
blueprint = Blueprint("engine_agents", __name__, url_prefix="/api/agents")
def register(app: Flask) -> None:
"""Attach agent management routes to *app*.
Implementation will be populated as services migrate from the legacy server.
"""
if "engine_agents" not in app.blueprints:
app.register_blueprint(blueprint)
__all__ = ["register", "blueprint"]

View File

@@ -0,0 +1,21 @@
"""Enrollment HTTP interface placeholders for the Engine."""
from __future__ import annotations
from flask import Blueprint, Flask
blueprint = Blueprint("engine_enrollment", __name__, url_prefix="/api/enrollment")
def register(app: Flask) -> None:
"""Attach enrollment routes to *app*.
Implementation will be ported during later migration phases.
"""
if "engine_enrollment" not in app.blueprints:
app.register_blueprint(blueprint)
__all__ = ["register", "blueprint"]

View File

@@ -0,0 +1,21 @@
"""Health check HTTP interface placeholders for the Engine."""
from __future__ import annotations
from flask import Blueprint, Flask
blueprint = Blueprint("engine_health", __name__)
def register(app: Flask) -> None:
"""Attach health-related routes to *app*.
Routes will be populated in later migration phases.
"""
if "engine_health" not in app.blueprints:
app.register_blueprint(blueprint)
__all__ = ["register", "blueprint"]

View File

@@ -0,0 +1,21 @@
"""Token management HTTP interface placeholders for the Engine."""
from __future__ import annotations
from flask import Blueprint, Flask
blueprint = Blueprint("engine_tokens", __name__, url_prefix="/api/tokens")
def register(app: Flask) -> None:
"""Attach token management routes to *app*.
Implementation will be introduced as authentication services are migrated.
"""
if "engine_tokens" not in app.blueprints:
app.register_blueprint(blueprint)
__all__ = ["register", "blueprint"]

View File

@@ -2,11 +2,13 @@
from __future__ import annotations
from typing import Optional
from typing import Any, Optional
from flask import Flask
from ...config import SocketIOSettings
from .agents import register as register_agent_events
from .job_management import register as register_job_events
try: # pragma: no cover - import guard
from flask_socketio import SocketIO
@@ -31,4 +33,14 @@ def create_socket_server(app: Flask, settings: SocketIOSettings) -> Optional[Soc
return socketio
__all__ = ["create_socket_server"]
def register_ws_interfaces(socketio: Any) -> None:
"""Attach placeholder namespaces for the Engine Socket.IO server."""
if socketio is None: # pragma: no cover - guard
return
for registrar in (register_agent_events, register_job_events):
registrar(socketio)
__all__ = ["create_socket_server", "register_ws_interfaces"]

View File

@@ -0,0 +1,16 @@
"""Agent WebSocket namespace wiring for the Engine."""
from __future__ import annotations
from typing import Any
from . import events
def register(socketio: Any) -> None:
"""Register agent namespaces on the given Socket.IO *socketio* instance."""
events.register(socketio)
__all__ = ["register"]

View File

@@ -0,0 +1,20 @@
"""Agent WebSocket event placeholders for the Engine."""
from __future__ import annotations
from typing import Any
def register(socketio: Any) -> None:
"""Register agent-related namespaces on *socketio*.
The concrete event handlers will be migrated in later phases.
"""
if socketio is None: # pragma: no cover - guard
return
# Placeholder for namespace registration, e.g. ``socketio.on_namespace(...)``.
return
__all__ = ["register"]

View File

@@ -0,0 +1,16 @@
"""Job management WebSocket namespace wiring for the Engine."""
from __future__ import annotations
from typing import Any
from . import events
def register(socketio: Any) -> None:
"""Register job management namespaces on the given Socket.IO *socketio*."""
events.register(socketio)
__all__ = ["register"]

View File

@@ -0,0 +1,19 @@
"""Job management WebSocket event placeholders for the Engine."""
from __future__ import annotations
from typing import Any
def register(socketio: Any) -> None:
"""Register job management namespaces on *socketio*.
Concrete handlers will be migrated in later phases.
"""
if socketio is None: # pragma: no cover - guard
return
return
__all__ = ["register"]

View File

@@ -4,17 +4,51 @@ from __future__ import annotations
from pathlib import Path
from flask import Flask
from flask import Flask, request, send_from_directory
from flask_cors import CORS
from werkzeug.exceptions import NotFound
from werkzeug.middleware.proxy_fix import ProxyFix
from .config import EngineSettings
def _resolve_static_folder(static_root: Path) -> tuple[str | None, str]:
if static_root.exists():
return str(static_root), "/"
return None, "/static"
def _resolve_static_folder(static_root: Path) -> tuple[str, str]:
return str(static_root), ""
def _register_spa_routes(app: Flask, assets_root: Path) -> None:
"""Serve the Borealis single-page application from *assets_root*.
The logic mirrors the legacy server by routing any unknown front-end paths
back to ``index.html`` so the React router can take over.
"""
static_folder = assets_root
@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
def serve_frontend(path: str) -> object:
candidate = (static_folder / path).resolve()
if path and candidate.is_file():
return send_from_directory(str(static_folder), path)
try:
return send_from_directory(str(static_folder), "index.html")
except Exception as exc: # pragma: no cover - passthrough
raise NotFound() from exc
@app.errorhandler(404)
def spa_fallback(error: Exception) -> object: # pragma: no cover - routing
request_path = (request.path or "").strip()
if request_path.startswith("/api") or request_path.startswith("/socket.io"):
return error
if "." in Path(request_path).name:
return error
if request.method not in {"GET", "HEAD"}:
return error
try:
return send_from_directory(str(static_folder), "index.html")
except Exception:
return error
def create_app(settings: EngineSettings) -> Flask:
@@ -35,6 +69,7 @@ def create_app(settings: EngineSettings) -> Flask:
SESSION_COOKIE_SAMESITE="Lax",
ENGINE_DATABASE_PATH=str(settings.database_path),
)
app.config.setdefault("PREFERRED_URL_SCHEME", "https")
# Respect upstream proxy headers when Borealis is hosted behind a TLS terminator.
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) # type: ignore[assignment]
@@ -45,6 +80,8 @@ def create_app(settings: EngineSettings) -> Flask:
supports_credentials=True,
)
_register_spa_routes(app, Path(static_folder))
return app