Added Toast Notification Framework & Documentation

This commit is contained in:
2025-11-26 21:08:47 -07:00
parent d50c1533e5
commit da48568aa4
7 changed files with 504 additions and 1 deletions

View File

@@ -41,10 +41,22 @@ from .devices import routes as device_routes
from .devices.approval import register_admin_endpoints
from .devices.management import register_management
from .filters import management as filters_management
from .notifications import management as notifications_management
from .scheduled_jobs import management as scheduled_jobs_management
from .server import info as server_info, log_management
DEFAULT_API_GROUPS: Sequence[str] = ("core", "auth", "tokens", "enrollment", "devices", "filters", "server", "assemblies", "scheduled_jobs")
DEFAULT_API_GROUPS: Sequence[str] = (
"core",
"auth",
"tokens",
"enrollment",
"devices",
"filters",
"server",
"assemblies",
"scheduled_jobs",
"notifications",
)
_SERVER_SCOPE_PATTERN = re.compile(r"\\b(?:scope|context|agent_context)=([A-Za-z0-9_-]+)", re.IGNORECASE)
_SERVER_AGENT_ID_PATTERN = re.compile(r"\\bagent_id=([^\\s,]+)", re.IGNORECASE)
@@ -279,6 +291,10 @@ def _register_scheduled_jobs(app: Flask, adapters: EngineServiceAdapters) -> Non
scheduled_jobs_management.register_management(app, adapters)
def _register_notifications(app: Flask, adapters: EngineServiceAdapters) -> None:
notifications_management.register_notifications(app, adapters)
def _register_assemblies(app: Flask, adapters: EngineServiceAdapters) -> None:
register_assemblies(app, adapters)
register_execution(app, adapters)
@@ -298,6 +314,7 @@ _GROUP_REGISTRARS: Mapping[str, Callable[[Flask, EngineServiceAdapters], None]]
"server": _register_server,
"assemblies": _register_assemblies,
"scheduled_jobs": _register_scheduled_jobs,
"notifications": _register_notifications,
}
@@ -321,6 +338,8 @@ def register_api(app: Flask, context: EngineContext) -> None:
normalized = [group.strip().lower() for group in enabled_groups if group]
if "filters" not in normalized:
normalized.append("filters")
if "notifications" not in normalized:
normalized.append("notifications")
adapters: Optional[EngineServiceAdapters] = None
for group in normalized:

View File

@@ -0,0 +1,12 @@
# ======================================================
# Data\Engine\services\API\notifications\__init__.py
# Description: Package init for notification API endpoints.
#
# API Endpoints (if applicable): None
# ======================================================
"""Notification API package for the Borealis Engine."""
from .management import register_notifications
__all__ = ["register_notifications"]

View File

@@ -0,0 +1,98 @@
# ======================================================
# Data\Engine\services\API\notifications\management.py
# Description: Notification dispatch endpoint used to surface authenticated toast events to the WebUI via Socket.IO.
#
# API Endpoints (if applicable):
# - POST /api/notifications/notify (Token Authenticated) - Broadcasts a transient notification payload to connected operators.
# ======================================================
"""Notification endpoints for the Borealis Engine runtime."""
from __future__ import annotations
import time
from typing import TYPE_CHECKING, Any, Dict
from flask import Blueprint, Flask, jsonify, request
from ...auth import RequestAuthContext
if TYPE_CHECKING: # pragma: no cover - typing aide
from .. import EngineServiceAdapters
def _clean_text(value: Any, fallback: str = "") -> str:
text = fallback
if isinstance(value, str):
text = value.strip() or fallback
elif value is not None:
try:
text = str(value).strip() or fallback
except Exception:
text = fallback
return text
def register_notifications(app: Flask, adapters: "EngineServiceAdapters") -> None:
"""Expose an authenticated notification endpoint that fans out to WebSocket clients."""
blueprint = Blueprint("notifications", __name__, url_prefix="/api/notifications")
auth = RequestAuthContext(
app=app,
dev_mode_manager=adapters.dev_mode_manager,
config=adapters.config,
logger=adapters.context.logger,
)
def _broadcast(notification: Dict[str, Any]) -> None:
socketio = getattr(adapters.context, "socketio", None)
if not socketio:
return
try:
socketio.emit("borealis_notification", notification)
except Exception:
adapters.context.logger.debug("Failed to emit notification payload.", exc_info=True)
@blueprint.route("/notify", methods=["POST"])
def notify() -> Any:
user, error = auth.require_user()
if error:
return jsonify(error[0]), error[1]
data = request.get_json(silent=True) or {}
title = _clean_text(data.get("title"), "Notification")
message = _clean_text(data.get("message"))
icon = _clean_text(data.get("icon"))
variant_raw = _clean_text(data.get("variant") or data.get("type") or data.get("severity") or "info", "info")
variant = variant_raw.lower()
if variant not in {"info", "warning", "error"}:
variant = "info"
if not message:
return (
jsonify(
{
"error": "invalid_payload",
"message": "Notification message is required.",
}
),
400,
)
now_ts = int(time.time())
payload = {
"id": f"notif-{now_ts}-{int(time.time() * 1000) % 1000}",
"title": title,
"message": message,
"icon": icon or "NotificationsActive",
"variant": variant,
"username": user.get("username"),
"role": user.get("role") or "User",
"created_at": now_ts,
}
_broadcast(payload)
adapters.service_log("notifications", f"Notification sent by {user.get('username')}: {title}")
return jsonify({"status": "sent", "notification": payload})
app.register_blueprint(blueprint)
adapters.service_log("notifications", "Registered notification endpoints.")