mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-14 22:35:47 -07:00
Added Toast Notification Framework & Documentation
This commit is contained in:
@@ -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:
|
||||
|
||||
12
Data/Engine/services/API/notifications/__init__.py
Normal file
12
Data/Engine/services/API/notifications/__init__.py
Normal 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"]
|
||||
98
Data/Engine/services/API/notifications/management.py
Normal file
98
Data/Engine/services/API/notifications/management.py
Normal 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.")
|
||||
@@ -51,6 +51,7 @@ import ServerInfo from "./Admin/Server_Info.jsx";
|
||||
import PageTemplate from "./Admin/Page_Template.jsx";
|
||||
import LogManagement from "./Admin/Log_Management.jsx";
|
||||
import DeviceApprovals from "./Devices/Device_Approvals.jsx";
|
||||
import Notifications from "./Notifications.jsx";
|
||||
|
||||
// Networking Imports
|
||||
import { io } from "socket.io-client";
|
||||
@@ -1369,6 +1370,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<Notifications socket={window.BorealisSocket} currentUser={user} />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100vw",
|
||||
|
||||
273
Data/Engine/web-interface/src/Notifications.jsx
Normal file
273
Data/Engine/web-interface/src/Notifications.jsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import {
|
||||
NotificationsActive as NotificationsActiveIcon,
|
||||
InfoOutlined as InfoOutlinedIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
WarningAmber as WarningAmberIcon,
|
||||
ErrorOutline as ErrorOutlineIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
FilterAlt as FilterAltIcon,
|
||||
CloudDone as CloudDoneIcon,
|
||||
Update as UpdateIcon,
|
||||
DeviceHub as DeviceHubIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const ICON_MAP = {
|
||||
notification: NotificationsActiveIcon,
|
||||
notifications: NotificationsActiveIcon,
|
||||
notificationsactive: NotificationsActiveIcon,
|
||||
info: InfoOutlinedIcon,
|
||||
success: CheckCircleIcon,
|
||||
check: CheckCircleIcon,
|
||||
warning: WarningAmberIcon,
|
||||
error: ErrorOutlineIcon,
|
||||
alert: ErrorOutlineIcon,
|
||||
schedule: ScheduleIcon,
|
||||
clock: ScheduleIcon,
|
||||
job: ScheduleIcon,
|
||||
filter: FilterAltIcon,
|
||||
filters: FilterAltIcon,
|
||||
done: CloudDoneIcon,
|
||||
synced: CloudDoneIcon,
|
||||
update: UpdateIcon,
|
||||
device: DeviceHubIcon,
|
||||
};
|
||||
|
||||
const THEMES = {
|
||||
info: {
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(19,27,48,0.96) 0%, rgba(25,36,60,0.94) 40%, rgba(32,52,82,0.9) 68%, rgba(44,70,104,0.86) 100%)",
|
||||
glow: "0 12px 30px rgba(4, 7, 17, 0.55), 0 0 0 1px rgba(126, 192, 255, 0.14)",
|
||||
border: "1px solid rgba(148, 208, 255, 0.18)",
|
||||
text: "#dfe8fa",
|
||||
icon: "#c5ddff",
|
||||
accentA: "rgba(125, 211, 252, 0.08)",
|
||||
accentB: "rgba(192, 132, 252, 0.07)",
|
||||
},
|
||||
warning: {
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(44,35,10,0.95) 0%, rgba(54,42,14,0.92) 36%, rgba(64,50,18,0.9) 68%, rgba(78,60,20,0.86) 100%)",
|
||||
glow: "0 12px 30px rgba(18,12,4,0.6), 0 0 0 1px rgba(255, 210, 125, 0.16)",
|
||||
border: "1px solid rgba(255, 219, 128, 0.24)",
|
||||
text: "#f6edd8",
|
||||
icon: "#ffe7a3",
|
||||
accentA: "rgba(255, 214, 140, 0.1)",
|
||||
accentB: "rgba(255, 186, 92, 0.08)",
|
||||
},
|
||||
error: {
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(48,16,18,0.95) 0%, rgba(62,20,24,0.92) 36%, rgba(78,26,30,0.9) 68%, rgba(94,30,34,0.86) 100%)",
|
||||
glow: "0 12px 30px rgba(18,4,4,0.6), 0 0 0 1px rgba(255, 158, 158, 0.16)",
|
||||
border: "1px solid rgba(255, 158, 158, 0.22)",
|
||||
text: "#f9e2e2",
|
||||
icon: "#ffc4c4",
|
||||
accentA: "rgba(255, 163, 163, 0.08)",
|
||||
accentB: "rgba(255, 116, 116, 0.08)",
|
||||
},
|
||||
};
|
||||
|
||||
function themeForVariant(variant) {
|
||||
return THEMES[variant] || THEMES.info;
|
||||
}
|
||||
|
||||
const toKey = (value) => (value || "").toString().replace(/[^a-z0-9]/gi, "").toLowerCase();
|
||||
|
||||
function resolveIcon(name) {
|
||||
const mapped = ICON_MAP[toKey(name)];
|
||||
return mapped || NotificationsActiveIcon;
|
||||
}
|
||||
|
||||
function NotificationCard({ item, index }) {
|
||||
const Icon = useMemo(() => resolveIcon(item.icon), [item.icon]);
|
||||
const delay = Math.min(index * 40, 260);
|
||||
const animationName = item.closing ? "fadeOut" : "dropIn";
|
||||
const theme = themeForVariant(item.variant);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 360,
|
||||
maxWidth: "92vw",
|
||||
display: "flex",
|
||||
gap: 1.5,
|
||||
alignItems: "center",
|
||||
backgroundImage: theme.background,
|
||||
borderRadius: 2.5,
|
||||
boxShadow: theme.glow,
|
||||
border: theme.border,
|
||||
backdropFilter: "blur(14px)",
|
||||
color: theme.text,
|
||||
p: 1.6,
|
||||
opacity: item.closing ? 1 : 0,
|
||||
transform: item.closing ? "translateY(0) scale(1)" : "translateY(-10px) scale(0.98)",
|
||||
animation: `${animationName} 0.45s ease forwards`,
|
||||
animationDelay: `${delay}ms`,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
"&::after": {
|
||||
content: "\"\"",
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background:
|
||||
`radial-gradient(120% 120% at 8% 8%, ${theme.accentA}, transparent 52%), ` +
|
||||
`radial-gradient(120% 120% at 80% 10%, ${theme.accentB}, transparent 60%)`,
|
||||
pointerEvents: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: theme.icon,
|
||||
}}
|
||||
>
|
||||
<Icon sx={{ fontSize: 26, filter: "drop-shadow(0 2px 6px rgba(0,0,0,0.35))" }} />
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.25, minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color: theme.text,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.2,
|
||||
lineHeight: 1.15,
|
||||
textShadow: "0 1px 0 rgba(0,0,0,0.2)",
|
||||
}}
|
||||
noWrap
|
||||
>
|
||||
{item.title || "Notification"}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: theme.text,
|
||||
opacity: 0.9,
|
||||
lineHeight: 1.35,
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
>
|
||||
{item.message}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Notifications({ socket, currentUser }) {
|
||||
const [items, setItems] = useState([]);
|
||||
const timeouts = useRef({});
|
||||
const normalizedUser = toKey(currentUser);
|
||||
|
||||
const clearTimeoutFor = useCallback((id) => {
|
||||
const handle = timeouts.current[id];
|
||||
if (handle) {
|
||||
clearTimeout(handle);
|
||||
delete timeouts.current[id];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scheduleRemoval = useCallback((id, ttlMs) => {
|
||||
clearTimeoutFor(id);
|
||||
const safeTtl = Math.max(1000, ttlMs || 5200);
|
||||
const exitDuration = 420;
|
||||
const fadeAfter = Math.max(600, safeTtl - exitDuration);
|
||||
timeouts.current[id] = setTimeout(() => {
|
||||
setItems((prev) =>
|
||||
prev.map((entry) => (entry.id === id ? { ...entry, closing: true } : entry))
|
||||
);
|
||||
timeouts.current[id] = setTimeout(() => {
|
||||
setItems((prev) => prev.filter((entry) => entry.id !== id));
|
||||
clearTimeoutFor(id);
|
||||
}, exitDuration);
|
||||
}, fadeAfter);
|
||||
}, [clearTimeoutFor]);
|
||||
|
||||
const pushNotification = useCallback(
|
||||
(raw) => {
|
||||
if (!raw) return;
|
||||
const owner = toKey(raw.username);
|
||||
if (owner && normalizedUser && owner !== normalizedUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = raw.id || `notif-${Date.now()}`;
|
||||
const entry = {
|
||||
id,
|
||||
title: raw.title || "Notification",
|
||||
message: raw.message || "",
|
||||
icon: raw.icon || "notification",
|
||||
variant: (raw.variant || raw.severity || raw.type || "info").toLowerCase(),
|
||||
created_at: raw.created_at || Math.floor(Date.now() / 1000),
|
||||
closing: false,
|
||||
};
|
||||
if (!entry.message) return;
|
||||
|
||||
setItems((prev) => {
|
||||
const withoutExisting = prev.filter((item) => item.id !== id);
|
||||
return [entry, ...withoutExisting].slice(0, 5);
|
||||
});
|
||||
scheduleRemoval(id, (raw.ttl_ms && Number(raw.ttl_ms)) || 5200);
|
||||
},
|
||||
[normalizedUser, scheduleRemoval]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket || typeof socket.on !== "function") return;
|
||||
const handler = (data) => pushNotification(data || {});
|
||||
socket.on("borealis_notification", handler);
|
||||
return () => {
|
||||
try {
|
||||
socket.off("borealis_notification", handler);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
};
|
||||
}, [socket, pushNotification]);
|
||||
|
||||
useEffect(() => () => Object.keys(timeouts.current).forEach(clearTimeoutFor), [clearTimeoutFor]);
|
||||
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes dropIn {
|
||||
0% { opacity: 0; transform: translateY(-14px) scale(0.97); }
|
||||
50% { opacity: 0.9; transform: translateY(0) scale(1.01); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
100% { opacity: 0; transform: translateY(-8px) scale(0.98); }
|
||||
}
|
||||
`}</style>
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 18,
|
||||
right: 18,
|
||||
zIndex: 2400,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: 1.25,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<Box key={item.id} sx={{ pointerEvents: "auto" }}>
|
||||
<NotificationCard item={item} index={index} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
97
Docs/Codex/TOAST_NOTIFICATIONS.md
Normal file
97
Docs/Codex/TOAST_NOTIFICATIONS.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Codex Guide: Toast Notifications (Borealis WebUI)
|
||||
|
||||
Use this guide to add, configure, and test transient toast notifications across Borealis. It documents the backend endpoint, frontend listener, payload contract, and quick Firefox console commands you can hand to operators for validation.
|
||||
|
||||
## Components & Paths
|
||||
- Backend endpoint: `Data/Engine/services/API/notifications/management.py` (registered as `/api/notifications/notify`).
|
||||
- Frontend listener + renderer: `Data/Engine/web-interface/src/Notifications.jsx` (mounted in `App.jsx`).
|
||||
- Transport: Socket.IO event `borealis_notification` broadcast to connected WebUI clients.
|
||||
|
||||
## Backend Behavior
|
||||
- Auth: Uses `RequestAuthContext.require_user()`; session/Bearer must be present. Returns `401/403` otherwise.
|
||||
- Route: `POST /api/notifications/notify`
|
||||
- Emits `borealis_notification` over Socket.IO (no persistence).
|
||||
- Logs via `service_log("notifications", ...)`.
|
||||
- Validation: Requires `message` in payload. `title` defaults to `"Notification"` if omitted.
|
||||
- Registration: API group `notifications` is enabled by default via `DEFAULT_API_GROUPS` and `_GROUP_REGISTRARS` in `Data/Engine/services/API/__init__.py`.
|
||||
|
||||
## Payload Schema
|
||||
Send JSON body (session-authenticated):
|
||||
- `title` (string, optional): Heading line. Default `"Notification"`.
|
||||
- `message` (string, required): Body copy.
|
||||
- `icon` (string, optional): Material icon name hint (e.g., `info`, `filter`, `schedule`, `warning`, `error`). Falls back to `NotificationsActive`.
|
||||
- `variant` (string, optional): Visual theme. Accepted: `info` | `warning` | `error` (case-insensitive). Aliases: `type` or `severity`. Defaults to `info`.
|
||||
- `ttl_ms` (number, optional): Client-side lifetime in milliseconds; defaults to ~5200ms before fade-out.
|
||||
|
||||
Notes:
|
||||
- Payload is fanned out verbatim to the WebUI (plus server-added fields: `id`, `username`, `role`, `created_at`).
|
||||
- The client caps the visible stack to the 5 most recent items (newest on top).
|
||||
- Non-empty `message` is mandatory; otherwise HTTP 400.
|
||||
|
||||
## Frontend Rendering Rules
|
||||
- Component: `Notifications.jsx` listens to `borealis_notification` on `window.BorealisSocket`.
|
||||
- Stack position: fixed top-right, high z-index, pointer events enabled on toasts only.
|
||||
- Auto-dismiss: ~5s default; each item fades out and is removed.
|
||||
- Theme by `variant`:
|
||||
- `info` (default): Borealis blue aurora gradient.
|
||||
- `warning`: Muted amber gradient.
|
||||
- `error`: Deep red gradient.
|
||||
- Icon: No container; uses the provided Material icon hint. Small drop shadow for legibility.
|
||||
|
||||
## Implementation Steps (Recap)
|
||||
1) Backend: Ensure `/api/notifications/notify` is registered (already in repo). New services should import `register_notifications` if API groups are customized.
|
||||
2) Emit: From any authenticated server flow, POST to `/api/notifications/notify` with the payload above.
|
||||
3) Frontend: `App.jsx` mounts `Notifications` globally; no per-page wiring needed.
|
||||
4) Test: Use the Firefox console examples below while logged in to confirm toast rendering.
|
||||
|
||||
## Firefox Console Examples (run while signed in)
|
||||
Info (default blue):
|
||||
```js
|
||||
fetch("/api/notifications/notify", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: "Test Notification",
|
||||
message: "Hello from the console!",
|
||||
icon: "info",
|
||||
variant: "info"
|
||||
})
|
||||
}).then(r => r.json()).then(console.log).catch(console.error);
|
||||
```
|
||||
|
||||
Warning (amber):
|
||||
```js
|
||||
fetch("/api/notifications/notify", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: "Heads up",
|
||||
message: "This is a warning example.",
|
||||
icon: "warning",
|
||||
variant: "warning"
|
||||
})
|
||||
}).then(r => r.json()).then(console.log).catch(console.error);
|
||||
```
|
||||
|
||||
Error (red):
|
||||
```js
|
||||
fetch("/api/notifications/notify", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: "Error encountered",
|
||||
message: "Something failed during processing.",
|
||||
icon: "error",
|
||||
variant: "error"
|
||||
})
|
||||
}).then(r => r.json()).then(console.log).catch(console.error);
|
||||
```
|
||||
|
||||
## Usage Notes & Tips
|
||||
- Keep `message` concise; multiline is supported via `\n`.
|
||||
- Use `icon` to match the source feature (e.g., `filter`, `schedule`, `device`, `error`).
|
||||
- The server adds `username`/`role` to payloads; the client currently shows all variants regardless of role (filtering is per-username match when present).
|
||||
- If sockets are unavailable, the endpoint still returns 200; toasts simply will not render until Socket.IO is connected.
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Applies to all Borealis frontends. Use `Data/Engine/web-interface/src/Admin/Page_Template.jsx` as the canonical visual reference (no API/business logic). Keep this doc as the single source of truth for styling rules and AG Grid behavior.
|
||||
|
||||
- Toast notifications: see `Docs/Codex/TOAST_NOTIFICATIONS.md` for endpoint, payload, severity variants, and quick test commands.
|
||||
|
||||
## Page Template Reference
|
||||
- Purpose: visual-only baseline for new pages; copy structure but wire your data in real pages.
|
||||
- Header: small Material icon left of the title, subtitle beneath, utility buttons on the top-right.
|
||||
|
||||
Reference in New Issue
Block a user