iitian Cursor commited on
Commit
81fe24b
·
1 Parent(s): 8b3905d

Serve Next.js SOC dashboard at /ui with FastAPI redirect from /.

Browse files
.dockerignore CHANGED
@@ -11,7 +11,6 @@ chroma_data
11
  .env
12
  frontend/node_modules
13
  frontend/.next
 
14
  *.log
15
- import_profile*.txt
16
- import_p3.txt
17
  .tmp_import.log
 
11
  .env
12
  frontend/node_modules
13
  frontend/.next
14
+ frontend/out
15
  *.log
 
 
16
  .tmp_import.log
.gitignore CHANGED
@@ -1,5 +1,4 @@
1
  .venv/
2
- .venv_py314/
3
  __pycache__/
4
  *.py[cod]
5
  .pytest_cache/
@@ -8,7 +7,7 @@ __pycache__/
8
  chroma_data/
9
  .env
10
  frontend/.next/
 
11
  frontend/node_modules/
12
  dist/
13
  *.log
14
- !demo_logs/auth_demo.log
 
1
  .venv/
 
2
  __pycache__/
3
  *.py[cod]
4
  .pytest_cache/
 
7
  chroma_data/
8
  .env
9
  frontend/.next/
10
+ frontend/out/
11
  frontend/node_modules/
12
  dist/
13
  *.log
 
Dockerfile CHANGED
@@ -1,5 +1,15 @@
1
- # Hugging Face Spaces (Docker SDK) app must listen on 7860.
2
- # Space has no bundled Postgres/Redis; backend degrades with SKIP_DB / no REDIS_URL.
 
 
 
 
 
 
 
 
 
 
3
  FROM python:3.12-slim
4
 
5
  ENV PYTHONDONTWRITEBYTECODE=1 \
@@ -22,6 +32,7 @@ COPY requirements.txt /app/requirements.txt
22
  RUN pip install --no-cache-dir -r /app/requirements.txt
23
 
24
  COPY . /app
 
25
 
26
  RUN mkdir -p /app/demo_logs
27
 
 
1
+ # Hugging Face Spaces (Docker): API + static Next.js dashboard at /ui/
2
+ FROM node:22-alpine AS frontend_build
3
+ WORKDIR /build/frontend
4
+ COPY frontend/package.json frontend/package-lock.json ./
5
+ RUN npm ci
6
+ COPY frontend ./
7
+ ENV NEXT_TELEMETRY_DISABLED=1 \
8
+ NEXT_STATIC_EXPORT=1 \
9
+ NEXT_PUBLIC_BASE_PATH=/ui \
10
+ NEXT_PUBLIC_API_URL=
11
+ RUN npm run build
12
+
13
  FROM python:3.12-slim
14
 
15
  ENV PYTHONDONTWRITEBYTECODE=1 \
 
32
  RUN pip install --no-cache-dir -r /app/requirements.txt
33
 
34
  COPY . /app
35
+ COPY --from=frontend_build /build/frontend/out /app/frontend/out
36
 
37
  RUN mkdir -p /app/demo_logs
38
 
README.md CHANGED
@@ -14,6 +14,22 @@ short_description: SentinelAI — Autonomous Multi-Agent AI SOC
14
 
15
  ---
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  # SentinelAI — Autonomous Multi-Agent AI SOC
18
 
19
  SentinelAI is a hackathon-grade, production-shaped **autonomous Security Operations Center**. It continuously ingests telemetry through collector agents, normalizes and enriches events, runs multi-modal detection (rules + heuristics + optional LLM reasoning on AMD ROCm), correlates attack chains, scores risk, drafts analyst narratives, emits remediation, and fans out alerts—while a **Next.js 15** command deck visualizes live operations.
 
14
 
15
  ---
16
 
17
+ ---
18
+ title: SentinelAI
19
+ emoji: 🏃
20
+ colorFrom: red
21
+ colorTo: pink
22
+ sdk: docker
23
+ app_port: 7860
24
+ pinned: false
25
+ license: apache-2.0
26
+ short_description: SentinelAI — Autonomous Multi-Agent AI SOC
27
+ ---
28
+
29
+ **This Hugging Face Space** serves the SOC dashboard at **`/ui/`** (static Next.js behind FastAPI). Open the Space URL in a browser and you are redirected from `/` to the deck; API docs stay at **`/docs`**. The container uses `SKIP_DB=1` (no bundled PostgreSQL/Redis).
30
+
31
+ ---
32
+
33
  # SentinelAI — Autonomous Multi-Agent AI SOC
34
 
35
  SentinelAI is a hackathon-grade, production-shaped **autonomous Security Operations Center**. It continuously ingests telemetry through collector agents, normalizes and enriches events, runs multi-modal detection (rules + heuristics + optional LLM reasoning on AMD ROCm), correlates attack chains, scores risk, drafts analyst narratives, emits remediation, and fans out alerts—while a **Next.js 15** command deck visualizes live operations.
backend/app/main.py CHANGED
@@ -12,8 +12,10 @@ import threading
12
  from pathlib import Path
13
  from typing import Annotated, Any
14
 
15
- from fastapi import Depends, FastAPI, WebSocket, WebSocketDisconnect
16
  from fastapi.middleware.cors import CORSMiddleware
 
 
17
 
18
  ROOT = Path(__file__).resolve().parents[2]
19
  if str(ROOT) not in sys.path:
@@ -174,6 +176,10 @@ app.add_middleware(
174
  allow_headers=["*"],
175
  )
176
 
 
 
 
 
177
 
178
  async def get_session():
179
  if os.getenv("SKIP_DB", "").lower() in {"1", "true", "yes"}:
@@ -280,10 +286,14 @@ async def replay_buffer():
280
 
281
 
282
  @app.get("/")
283
- async def root():
284
- """Landing hint FastAPI did not define `/` before; browsers hitting only the host saw 404."""
 
 
 
285
  return {
286
  "service": "SentinelAI SOC API",
 
287
  "docs": "/docs",
288
  "health": "/health",
289
  "openapi_json": "/openapi.json",
 
12
  from pathlib import Path
13
  from typing import Annotated, Any
14
 
15
+ from fastapi import Depends, FastAPI, Request, WebSocket, WebSocketDisconnect
16
  from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import RedirectResponse
18
+ from fastapi.staticfiles import StaticFiles
19
 
20
  ROOT = Path(__file__).resolve().parents[2]
21
  if str(ROOT) not in sys.path:
 
176
  allow_headers=["*"],
177
  )
178
 
179
+ _UI_STATIC = ROOT / "frontend" / "out"
180
+ if _UI_STATIC.is_dir():
181
+ app.mount("/ui", StaticFiles(directory=str(_UI_STATIC), html=True), name="ui")
182
+
183
 
184
  async def get_session():
185
  if os.getenv("SKIP_DB", "").lower() in {"1", "true", "yes"}:
 
286
 
287
 
288
  @app.get("/")
289
+ async def root(request: Request):
290
+ """Browsers get the Next dashboard at `/ui` when static export is baked in; API clients keep JSON."""
291
+ accept = request.headers.get("accept") or ""
292
+ if _UI_STATIC.is_dir() and accept.startswith("text/html"):
293
+ return RedirectResponse(url="/ui/", status_code=302)
294
  return {
295
  "service": "SentinelAI SOC API",
296
+ "dashboard": "/ui/",
297
  "docs": "/docs",
298
  "health": "/health",
299
  "openapi_json": "/openapi.json",
frontend/next.config.ts CHANGED
@@ -1,6 +1,10 @@
1
  import type { NextConfig } from "next";
2
  import path from "path";
3
 
 
 
 
 
4
  const nextConfig: NextConfig = {
5
  // Keeps output file tracing anchored to this app when multiple lockfiles exist on the machine.
6
  outputFileTracingRoot: path.join(process.cwd()),
@@ -8,6 +12,13 @@ const nextConfig: NextConfig = {
8
  eslint: {
9
  ignoreDuringBuilds: true,
10
  },
 
 
 
 
 
 
 
11
  };
12
 
13
  export default nextConfig;
 
1
  import type { NextConfig } from "next";
2
  import path from "path";
3
 
4
+ /** When `NEXT_STATIC_EXPORT=1`, emit `out/` for embedding behind FastAPI (e.g. Hugging Face Docker Space). */
5
+ const staticExport = process.env.NEXT_STATIC_EXPORT === "1";
6
+ const basePath = process.env.NEXT_PUBLIC_BASE_PATH?.trim() ?? "";
7
+
8
  const nextConfig: NextConfig = {
9
  // Keeps output file tracing anchored to this app when multiple lockfiles exist on the machine.
10
  outputFileTracingRoot: path.join(process.cwd()),
 
12
  eslint: {
13
  ignoreDuringBuilds: true,
14
  },
15
+ ...(staticExport
16
+ ? {
17
+ output: "export" as const,
18
+ images: { unoptimized: true },
19
+ }
20
+ : {}),
21
+ ...(basePath ? { basePath } : {}),
22
  };
23
 
24
  export default nextConfig;
frontend/src/app/page.tsx CHANGED
@@ -71,13 +71,27 @@ function normalizeApiOrigin(raw: string): string {
71
  }
72
  }
73
 
74
- function apiBase() {
75
- const raw = process.env.NEXT_PUBLIC_API_URL ?? "http://127.0.0.1:8000";
 
 
 
 
 
 
76
  return normalizeApiOrigin(raw);
77
  }
78
 
79
- function wsUrl() {
80
- const base = apiBase();
 
 
 
 
 
 
 
 
81
  return base.replace(/^http/, "ws") + "/live-events";
82
  }
83
 
 
71
  }
72
  }
73
 
74
+ function apiBase(): string {
75
+ const raw = process.env.NEXT_PUBLIC_API_URL;
76
+ if (raw === undefined || raw === "") {
77
+ if (typeof window !== "undefined") {
78
+ return window.location.origin;
79
+ }
80
+ return "";
81
+ }
82
  return normalizeApiOrigin(raw);
83
  }
84
 
85
+ function wsUrl(): string {
86
+ const raw = process.env.NEXT_PUBLIC_API_URL;
87
+ if (raw === undefined || raw === "") {
88
+ if (typeof window !== "undefined") {
89
+ const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
90
+ return `${proto}//${window.location.host}/live-events`;
91
+ }
92
+ return "ws://127.0.0.1:8000/live-events";
93
+ }
94
+ const base = normalizeApiOrigin(raw);
95
  return base.replace(/^http/, "ws") + "/live-events";
96
  }
97