mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 15:21:57 -06:00
Scaffold Engine application interfaces
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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, ...]:
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
21
Data/Engine/interfaces/http/admin.py
Normal file
21
Data/Engine/interfaces/http/admin.py
Normal 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"]
|
||||
21
Data/Engine/interfaces/http/agents.py
Normal file
21
Data/Engine/interfaces/http/agents.py
Normal 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"]
|
||||
21
Data/Engine/interfaces/http/enrollment.py
Normal file
21
Data/Engine/interfaces/http/enrollment.py
Normal 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"]
|
||||
21
Data/Engine/interfaces/http/health.py
Normal file
21
Data/Engine/interfaces/http/health.py
Normal 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"]
|
||||
21
Data/Engine/interfaces/http/tokens.py
Normal file
21
Data/Engine/interfaces/http/tokens.py
Normal 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"]
|
||||
@@ -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"]
|
||||
|
||||
16
Data/Engine/interfaces/ws/agents/__init__.py
Normal file
16
Data/Engine/interfaces/ws/agents/__init__.py
Normal 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"]
|
||||
20
Data/Engine/interfaces/ws/agents/events.py
Normal file
20
Data/Engine/interfaces/ws/agents/events.py
Normal 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"]
|
||||
16
Data/Engine/interfaces/ws/job_management/__init__.py
Normal file
16
Data/Engine/interfaces/ws/job_management/__init__.py
Normal 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"]
|
||||
19
Data/Engine/interfaces/ws/job_management/events.py
Normal file
19
Data/Engine/interfaces/ws/job_management/events.py
Normal 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"]
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user