marvis docker container, ignore ueransim
This commit is contained in:
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.pytest_cache
|
||||||
|
.mypy_cache
|
||||||
|
.DS_Store
|
||||||
|
README.md
|
||||||
|
ai-slides.html
|
||||||
|
deploy.sh
|
||||||
19
Dockerfile
19
Dockerfile
@@ -1,18 +1,27 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
COPY app/ ./app/
|
|
||||||
|
|
||||||
EXPOSE 8095
|
COPY requirements.txt .
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
docker.io \
|
||||||
|
podman \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app/ ./app/
|
||||||
|
COPY config/ ./config/
|
||||||
|
|
||||||
|
EXPOSE 8100
|
||||||
|
|
||||||
# When running standalone, override these to point at your Prometheus/Alertmanager
|
# When running standalone, override these to point at your Prometheus/Alertmanager
|
||||||
ENV MARVIS_PROMETHEUS_URL=http://127.0.0.1:9090
|
ENV MARVIS_PROMETHEUS_URL=http://127.0.0.1:9090
|
||||||
ENV MARVIS_PROMETHEUS_PREFIX=/prometheus
|
ENV MARVIS_PROMETHEUS_PREFIX=/prometheus
|
||||||
ENV MARVIS_ALERTMANAGER_URL=http://127.0.0.1:9093
|
ENV MARVIS_ALERTMANAGER_URL=http://127.0.0.1:9093
|
||||||
ENV MARVIS_AI_MODE=rule
|
ENV MARVIS_AI_MODE=rule
|
||||||
|
ENV MARVIS_CONTAINER_RUNTIME=docker
|
||||||
|
ENV MARVIS_UERANSIM_ENV_FILE=/app/config/ueransim.env
|
||||||
# MARVIS_AI_MODE=openai → set MARVIS_OPENAI_API_KEY
|
# MARVIS_AI_MODE=openai → set MARVIS_OPENAI_API_KEY
|
||||||
# MARVIS_AI_MODE=ollama → set MARVIS_OLLAMA_URL + MARVIS_OLLAMA_MODEL
|
# MARVIS_AI_MODE=ollama → set MARVIS_OLLAMA_URL + MARVIS_OLLAMA_MODEL
|
||||||
|
|
||||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8095"]
|
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8100"]
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -5,6 +5,44 @@ This document describes the complete architecture and deployment procedure for t
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Deployment Model
|
||||||
|
|
||||||
|
The target environment for this project is a host where services are started by
|
||||||
|
`systemd`, including Docker-backed services. Marvis is intended to run the same
|
||||||
|
way:
|
||||||
|
|
||||||
|
- the FastAPI app runs on `127.0.0.1:8100`
|
||||||
|
- Traefik exposes it at `/core/marvis/*`
|
||||||
|
- `patch-ncm.py` injects the sidebar entries and iframe routes into the NCM UI
|
||||||
|
- the injected entries should only be added for services that are actually
|
||||||
|
reachable on the host when the patch is applied
|
||||||
|
|
||||||
|
For a containerised deployment without Compose, this repo includes an example
|
||||||
|
unit file at `config/p5g-marvis.service`.
|
||||||
|
|
||||||
|
### Build the image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t p5g-marvis:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install the systemd unit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config/p5g-marvis.service /usr/lib/systemd/system/p5g-marvis.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now p5g-marvis
|
||||||
|
```
|
||||||
|
|
||||||
|
Marvis will then be reachable on:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8100
|
||||||
|
http://127.0.0.1:8100/api/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -206,11 +244,17 @@ JS="/etc/athonet/ems-frontend/advanced/assets/index-Cw8Irsq8.js"
|
|||||||
cd /opt/p5g-marvis && python3 patch-ncm.py'
|
cd /opt/p5g-marvis && python3 patch-ncm.py'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`patch-ncm.py` injects only the services it detects as reachable on the host:
|
||||||
|
|
||||||
|
- `P5G_MARVIS_ENABLED=true|false` overrides Marvis detection
|
||||||
|
- `P5G_RADIO_ENABLED=true|false` overrides Radio detection
|
||||||
|
- by default it probes `127.0.0.1:8100` for Marvis and `127.0.0.1:4000` for Radio
|
||||||
|
|
||||||
Expected output:
|
Expected output:
|
||||||
```
|
```
|
||||||
Applied: sidebar entry
|
Applied: sidebar entry
|
||||||
Applied: marvis + radio routes with Or wrapper
|
Applied: injected iframe routes
|
||||||
Applied: /marvis permissions entry
|
Applied: permissions entry
|
||||||
Done — P5G Marvis: 10 occurrences, P5G Radio: 3 occurrences
|
Done — P5G Marvis: 10 occurrences, P5G Radio: 3 occurrences
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
BIN
__pycache__/patch-ncm.cpython-314.pyc
Normal file
BIN
__pycache__/patch-ncm.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-314.pyc
Normal file
BIN
app/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
@@ -14,6 +14,15 @@ OPENAI_BASE_URL = os.getenv("MARVIS_OPENAI_BASE_URL", "https://api.openai.co
|
|||||||
OLLAMA_URL = os.getenv("MARVIS_OLLAMA_URL", "http://localhost:11434")
|
OLLAMA_URL = os.getenv("MARVIS_OLLAMA_URL", "http://localhost:11434")
|
||||||
OLLAMA_MODEL = os.getenv("MARVIS_OLLAMA_MODEL", "llama3")
|
OLLAMA_MODEL = os.getenv("MARVIS_OLLAMA_MODEL", "llama3")
|
||||||
|
|
||||||
|
# Container/runtime integration
|
||||||
|
CONTAINER_RUNTIME = os.getenv("MARVIS_CONTAINER_RUNTIME", "docker")
|
||||||
|
CONTAINER_HOST = os.getenv("CONTAINER_HOST", "")
|
||||||
|
UERANSIM_IMAGE = os.getenv("MARVIS_UERANSIM_IMAGE", "p5g-marvis-ueransim")
|
||||||
|
UERANSIM_ENV_FILE = os.getenv("MARVIS_UERANSIM_ENV_FILE", "/app/config/ueransim.env")
|
||||||
|
UERANSIM_NETWORK_MODE = os.getenv("MARVIS_UERANSIM_NETWORK_MODE", "host")
|
||||||
|
UERANSIM_PRIVILEGED = os.getenv("MARVIS_UERANSIM_PRIVILEGED", "true").lower() in {"1", "true", "yes", "on"}
|
||||||
|
UERANSIM_CONTAINER_NAME = os.getenv("MARVIS_UERANSIM_CONTAINER_NAME", "marvis-ueransim-test")
|
||||||
|
|
||||||
# Maps Prometheus target_type label → display name
|
# Maps Prometheus target_type label → display name
|
||||||
TARGET_TYPE_MAP = {
|
TARGET_TYPE_MAP = {
|
||||||
"amf": "AMF", "smf": "SMF", "upf": "UPF",
|
"amf": "AMF", "smf": "SMF", "upf": "UPF",
|
||||||
|
|||||||
BIN
app/services/__pycache__/ai.cpython-314.pyc
Normal file
BIN
app/services/__pycache__/ai.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/log_analyzer.cpython-314.pyc
Normal file
BIN
app/services/__pycache__/log_analyzer.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/ueransim.cpython-314.pyc
Normal file
BIN
app/services/__pycache__/ueransim.cpython-314.pyc
Normal file
Binary file not shown.
@@ -6,7 +6,15 @@ Phase 2: swap MARVIS_AI_MODE=openai or MARVIS_AI_MODE=ollama to route through LL
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.config import AI_MODE, OPENAI_API_KEY, OPENAI_MODEL, OPENAI_BASE_URL, OLLAMA_URL, OLLAMA_MODEL
|
from app.config import (
|
||||||
|
AI_MODE,
|
||||||
|
CONTAINER_RUNTIME,
|
||||||
|
OPENAI_API_KEY,
|
||||||
|
OPENAI_MODEL,
|
||||||
|
OPENAI_BASE_URL,
|
||||||
|
OLLAMA_MODEL,
|
||||||
|
OLLAMA_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def answer(query: str, network_state: dict, alerts: list) -> str:
|
async def answer(query: str, network_state: dict, alerts: list) -> str:
|
||||||
@@ -69,7 +77,7 @@ def _health_summary(up: list, down: list, alerts: list) -> str:
|
|||||||
lines.append(f"✅ **{len(up)} UP**: {', '.join(n['name'] for n in up)}")
|
lines.append(f"✅ **{len(up)} UP**: {', '.join(n['name'] for n in up)}")
|
||||||
if down:
|
if down:
|
||||||
lines.append(f"🔴 **{len(down)} DOWN**: {', '.join(n['name'] for n in down)}")
|
lines.append(f"🔴 **{len(down)} DOWN**: {', '.join(n['name'] for n in down)}")
|
||||||
lines.append(f" ⚡ Action: check `podman logs <nf>` on the VM")
|
lines.append(f" ⚡ Action: check `{CONTAINER_RUNTIME} logs <nf>` in the runtime host")
|
||||||
|
|
||||||
if alerts:
|
if alerts:
|
||||||
lines.append(f"\n⚠️ **{len(alerts)} alert(s)** — {len(crit)} critical, {len(warn)} warning")
|
lines.append(f"\n⚠️ **{len(alerts)} alert(s)** — {len(crit)} critical, {len(warn)} warning")
|
||||||
@@ -91,7 +99,7 @@ def _nf_detail(nf_name: str, nfs: list, alerts: list) -> str:
|
|||||||
|
|
||||||
if not nf or nf["state"] == "unknown":
|
if not nf or nf["state"] == "unknown":
|
||||||
return (f"ℹ️ No Prometheus data found for **{nf_name}**.\n"
|
return (f"ℹ️ No Prometheus data found for **{nf_name}**.\n"
|
||||||
f"Check: `podman ps | grep {nf_name.lower()}`")
|
f"Check: `{CONTAINER_RUNTIME} ps | grep {nf_name.lower()}`")
|
||||||
|
|
||||||
icon = "✅" if nf["state"] == "up" else "🔴"
|
icon = "✅" if nf["state"] == "up" else "🔴"
|
||||||
lines = [f"{icon} **{nf_name}** is **{nf['state'].upper()}**",
|
lines = [f"{icon} **{nf_name}** is **{nf['state'].upper()}**",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import time
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.config import CONTAINER_HOST, CONTAINER_RUNTIME
|
||||||
|
|
||||||
# ── In-memory history (up to 96 snapshots ≈ 48 min at 30 s refresh) ────────
|
# ── In-memory history (up to 96 snapshots ≈ 48 min at 30 s refresh) ────────
|
||||||
_history: deque = deque(maxlen=96)
|
_history: deque = deque(maxlen=96)
|
||||||
|
|
||||||
@@ -137,14 +139,18 @@ _container_cache_ts: float = 0.0
|
|||||||
|
|
||||||
|
|
||||||
async def _discover_containers() -> dict[str, str]:
|
async def _discover_containers() -> dict[str, str]:
|
||||||
"""Run `podman ps` and map NF names to actual container names."""
|
"""Run the configured container runtime and map NF names to actual container names."""
|
||||||
global _container_cache, _container_cache_ts
|
global _container_cache, _container_cache_ts
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if _container_cache and now - _container_cache_ts < 60:
|
if _container_cache and now - _container_cache_ts < 60:
|
||||||
return _container_cache
|
return _container_cache
|
||||||
try:
|
try:
|
||||||
|
cmd = [CONTAINER_RUNTIME]
|
||||||
|
if CONTAINER_HOST:
|
||||||
|
cmd.extend(["--host", CONTAINER_HOST])
|
||||||
|
cmd.extend(["ps", "--format", "{{.Names}}"])
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"podman", "ps", "--format", "{{.Names}}",
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
@@ -167,10 +173,14 @@ async def _discover_containers() -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
async def _read_logs(container: str, tail: int = 400) -> str:
|
async def _read_logs(container: str, tail: int = 400) -> str:
|
||||||
"""Read recent logs from a podman container (stdout + stderr)."""
|
"""Read recent logs from a container (stdout + stderr)."""
|
||||||
try:
|
try:
|
||||||
|
cmd = [CONTAINER_RUNTIME]
|
||||||
|
if CONTAINER_HOST:
|
||||||
|
cmd.extend(["--host", CONTAINER_HOST])
|
||||||
|
cmd.extend(["logs", "--tail", str(tail), container])
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"podman", "logs", "--tail", str(tail), container,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
@@ -277,7 +287,7 @@ async def analyze_logs() -> dict:
|
|||||||
"severity": "critical",
|
"severity": "critical",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"description": f"{nf_st['name']} is unreachable",
|
"description": f"{nf_st['name']} is unreachable",
|
||||||
"remediation": (f"Run `podman ps` on the VM and check if {nf_st['name']} "
|
"remediation": (f"Run `{CONTAINER_RUNTIME} ps` and check if {nf_st['name']} "
|
||||||
f"container is running; inspect its logs."),
|
f"container is running; inspect its logs."),
|
||||||
"source": "prometheus",
|
"source": "prometheus",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import uuid
|
|||||||
import time
|
import time
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from app.config import (
|
||||||
|
CONTAINER_HOST,
|
||||||
|
CONTAINER_RUNTIME,
|
||||||
|
UERANSIM_CONTAINER_NAME,
|
||||||
|
UERANSIM_ENV_FILE,
|
||||||
|
UERANSIM_IMAGE,
|
||||||
|
UERANSIM_NETWORK_MODE,
|
||||||
|
UERANSIM_PRIVILEGED,
|
||||||
|
)
|
||||||
|
|
||||||
_tasks: Dict[str, dict] = {}
|
_tasks: Dict[str, dict] = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -28,31 +38,39 @@ async def run_test(task_id: str) -> None:
|
|||||||
def log(msg: str, type: str = "info") -> None:
|
def log(msg: str, type: str = "info") -> None:
|
||||||
task["logs"].append({"msg": msg, "type": type, "ts": time.strftime("%H:%M:%S")})
|
task["logs"].append({"msg": msg, "type": type, "ts": time.strftime("%H:%M:%S")})
|
||||||
|
|
||||||
log("▸ Checking UERANSIM Docker image…", "run")
|
runtime_cmd = [CONTAINER_RUNTIME]
|
||||||
|
if CONTAINER_HOST:
|
||||||
|
runtime_cmd.extend(["--host", CONTAINER_HOST])
|
||||||
|
|
||||||
|
log(f"▸ Checking UERANSIM image via {CONTAINER_RUNTIME}…", "run")
|
||||||
check = await asyncio.create_subprocess_exec(
|
check = await asyncio.create_subprocess_exec(
|
||||||
"docker", "images", "-q", "ueransim",
|
*runtime_cmd, "images", "-q", UERANSIM_IMAGE,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
out, _ = await check.communicate()
|
out, _ = await check.communicate()
|
||||||
if not out.strip():
|
if not out.strip():
|
||||||
log("✗ UERANSIM image not found.", "err")
|
log("✗ UERANSIM image not found.", "err")
|
||||||
log(" SSH to host and run: bash /opt/p5g-marvis/build-ueransim.sh", "err")
|
log(" Build it with `docker compose build ueransim` or set MARVIS_UERANSIM_IMAGE.", "err")
|
||||||
task["status"] = "error"
|
task["status"] = "error"
|
||||||
return
|
return
|
||||||
|
|
||||||
log(" UERANSIM image ready", "ok")
|
log(" UERANSIM image ready", "ok")
|
||||||
log("▸ Starting test container — allow up to 60s…", "run")
|
log("▸ Starting test container — allow up to 60s…", "run")
|
||||||
|
|
||||||
env_file = "/opt/p5g-marvis/config/ueransim.env"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
run_cmd = [*runtime_cmd, "run", "--rm", "--name", UERANSIM_CONTAINER_NAME]
|
||||||
|
if UERANSIM_NETWORK_MODE:
|
||||||
|
run_cmd.append(f"--network={UERANSIM_NETWORK_MODE}")
|
||||||
|
if UERANSIM_PRIVILEGED:
|
||||||
|
run_cmd.append("--privileged")
|
||||||
|
run_cmd.extend([
|
||||||
|
"--env-file", UERANSIM_ENV_FILE,
|
||||||
|
UERANSIM_IMAGE,
|
||||||
|
])
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"docker", "run", "--rm",
|
*run_cmd,
|
||||||
"--network=host",
|
|
||||||
"--privileged",
|
|
||||||
"--env-file", env_file,
|
|
||||||
"ueransim",
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.STDOUT,
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
)
|
)
|
||||||
@@ -81,7 +99,7 @@ async def run_test(task_id: str) -> None:
|
|||||||
log("✓ Emulated data session completed successfully", "ok")
|
log("✓ Emulated data session completed successfully", "ok")
|
||||||
task["status"] = "done"
|
task["status"] = "done"
|
||||||
elif proc.returncode == 2:
|
elif proc.returncode == 2:
|
||||||
log("⚠ Credentials not configured — edit /opt/p5g-marvis/config/ueransim.env", "warn")
|
log(f"⚠ Credentials not configured — edit {UERANSIM_ENV_FILE}", "warn")
|
||||||
task["status"] = "error"
|
task["status"] = "error"
|
||||||
else:
|
else:
|
||||||
log(f"✗ Test exited with code {proc.returncode}", "err")
|
log(f"✗ Test exited with code {proc.returncode}", "err")
|
||||||
|
|||||||
24
config/p5g-marvis.service
Normal file
24
config/p5g-marvis.service
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=P5G Marvis container
|
||||||
|
After=docker.service network-online.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
TimeoutStartSec=0
|
||||||
|
|
||||||
|
ExecStartPre=-/usr/bin/docker rm -f p5g-marvis
|
||||||
|
ExecStart=/usr/bin/docker run \
|
||||||
|
--name p5g-marvis \
|
||||||
|
--publish 127.0.0.1:8100:8100 \
|
||||||
|
--env MARVIS_PROMETHEUS_URL=http://127.0.0.1:9090 \
|
||||||
|
--env MARVIS_PROMETHEUS_PREFIX=/prometheus \
|
||||||
|
--env MARVIS_ALERTMANAGER_URL=http://127.0.0.1:9093 \
|
||||||
|
--env MARVIS_AI_MODE=rule \
|
||||||
|
p5g-marvis:latest
|
||||||
|
ExecStop=/usr/bin/docker stop p5g-marvis
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
111
patch-ncm.py
111
patch-ncm.py
@@ -6,6 +6,11 @@ Reads from the clean .bak, applies all patches, writes the result.
|
|||||||
Safe to re-run (idempotent).
|
Safe to re-run (idempotent).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
JS = "/etc/athonet/ems-frontend/advanced/assets/index-Cw8Irsq8.js"
|
JS = "/etc/athonet/ems-frontend/advanced/assets/index-Cw8Irsq8.js"
|
||||||
BAK = JS + ".bak"
|
BAK = JS + ".bak"
|
||||||
|
|
||||||
@@ -14,26 +19,60 @@ with open(BAK, "r", encoding="utf-8") as f:
|
|||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
|
|
||||||
|
def env_flag(name: str) -> str | None:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return value.strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled(name: str, default_host: str, default_port: int) -> bool:
|
||||||
|
flag = env_flag(name)
|
||||||
|
if flag in {"1", "true", "yes", "on"}:
|
||||||
|
return True
|
||||||
|
if flag in {"0", "false", "no", "off"}:
|
||||||
|
return False
|
||||||
|
host = os.getenv(f"{name}_HOST", default_host)
|
||||||
|
port = int(os.getenv(f"{name}_PORT", str(default_port)))
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.settimeout(0.5)
|
||||||
|
return sock.connect_ex((host, port)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
ENABLE_MARVIS = is_enabled("P5G_MARVIS_ENABLED", "127.0.0.1", 8100)
|
||||||
|
ENABLE_RADIO = is_enabled("P5G_RADIO_ENABLED", "127.0.0.1", 4000)
|
||||||
|
|
||||||
|
if not ENABLE_MARVIS and not ENABLE_RADIO:
|
||||||
|
print("ERROR: neither P5G Marvis nor P5G Radio appears reachable; refusing to patch")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# ── 1. Sidebar nav entry ───────────────────────────────────────────────────
|
# ── 1. Sidebar nav entry ───────────────────────────────────────────────────
|
||||||
SIDEBAR_OLD = '!_o(ue.UPF,t)}]}]}'
|
SIDEBAR_OLD = '!_o(ue.UPF,t)}]}]}'
|
||||||
SIDEBAR_NEW = ('!_o(ue.UPF,t)}]},' # UPF entry closes; start custom entries
|
sidebar_items = []
|
||||||
'{value:"/marvis",label:"P5G Marvis",'
|
if ENABLE_MARVIS:
|
||||||
'icon:a.jsx(ge.Magic,{}),disabled:false,'
|
sidebar_items.append(
|
||||||
'subItems:['
|
'{value:"/marvis",label:"P5G Marvis",'
|
||||||
'{value:"/marvis/overview",label:"P5G Marvis Insights",disabled:false},'
|
'icon:a.jsx(ge.Magic,{}),disabled:false,'
|
||||||
'{value:"/marvis/actions",label:"P5G Marvis Actions",disabled:false},'
|
'subItems:['
|
||||||
'{value:"/marvis/minis",label:"P5G Marvis Minis",disabled:false},'
|
'{value:"/marvis/overview",label:"P5G Marvis Insights",disabled:false},'
|
||||||
'{value:"/marvis/ai",label:"P5G Marvis AI",disabled:false}'
|
'{value:"/marvis/actions",label:"P5G Marvis Actions",disabled:false},'
|
||||||
']},' # close Marvis, comma before Radio
|
'{value:"/marvis/minis",label:"P5G Marvis Minis",disabled:false},'
|
||||||
'{value:"/radio",label:"P5G Radio",'
|
'{value:"/marvis/ai",label:"P5G Marvis AI",disabled:false}'
|
||||||
'icon:a.jsx(ge.Magic,{}),disabled:false}'
|
']}'
|
||||||
']}') # close outer items array + object
|
)
|
||||||
|
if ENABLE_RADIO:
|
||||||
|
sidebar_items.append(
|
||||||
|
'{value:"/radio",label:"P5G Radio",'
|
||||||
|
'icon:a.jsx(ge.Magic,{}),disabled:false}'
|
||||||
|
)
|
||||||
|
SIDEBAR_NEW = '!_o(ue.UPF,t)}]},' + ",".join(sidebar_items) + ']}'
|
||||||
|
|
||||||
if SIDEBAR_OLD in src:
|
if SIDEBAR_OLD in src:
|
||||||
src = src.replace(SIDEBAR_OLD, SIDEBAR_NEW, 1)
|
src = src.replace(SIDEBAR_OLD, SIDEBAR_NEW, 1)
|
||||||
print("Applied: sidebar entry")
|
print("Applied: sidebar entry")
|
||||||
changed = True
|
changed = True
|
||||||
elif SIDEBAR_NEW.split(',icon')[0] in src:
|
elif any(label in src for label in ["P5G Marvis", "P5G Radio"]):
|
||||||
print("Skipped: sidebar entry already present")
|
print("Skipped: sidebar entry already present")
|
||||||
else:
|
else:
|
||||||
print("ERROR: sidebar anchor not found"); exit(1)
|
print("ERROR: sidebar anchor not found"); exit(1)
|
||||||
@@ -69,12 +108,18 @@ ROUTE_MARKER = 'path:"marvis"'
|
|||||||
|
|
||||||
RADIO_MARKER = 'path:"radio"'
|
RADIO_MARKER = 'path:"radio"'
|
||||||
|
|
||||||
if ROUTE_ANCHOR in src and ROUTE_MARKER not in src:
|
route_parts = []
|
||||||
src = src.replace(ROUTE_ANCHOR, 'G4t,' + MARVIS_ROUTE + ',' + RADIO_ROUTE + ',rht', 1)
|
if ENABLE_MARVIS:
|
||||||
print("Applied: marvis + radio routes with Or wrapper")
|
route_parts.append(MARVIS_ROUTE)
|
||||||
|
if ENABLE_RADIO:
|
||||||
|
route_parts.append(RADIO_ROUTE)
|
||||||
|
|
||||||
|
if ROUTE_ANCHOR in src and ROUTE_MARKER not in src and RADIO_MARKER not in src:
|
||||||
|
src = src.replace(ROUTE_ANCHOR, 'G4t,' + ",".join(route_parts) + ',rht', 1)
|
||||||
|
print("Applied: injected iframe routes")
|
||||||
changed = True
|
changed = True
|
||||||
elif ROUTE_MARKER in src:
|
elif ROUTE_MARKER in src or RADIO_MARKER in src:
|
||||||
print("Skipped: marvis route already present")
|
print("Skipped: route entry already present")
|
||||||
else:
|
else:
|
||||||
print("WARNING: route anchor G4t,rht not found — iframe routes not injected")
|
print("WARNING: route anchor G4t,rht not found — iframe routes not injected")
|
||||||
|
|
||||||
@@ -84,19 +129,32 @@ else:
|
|||||||
# "Permissions for route /marvis are not managed". Tt=()=>!0 means no
|
# "Permissions for route /marvis are not managed". Tt=()=>!0 means no
|
||||||
# specific permission required (same as profile and siteLoader routes).
|
# specific permission required (same as profile and siteLoader routes).
|
||||||
PERMS_OLD = '...QUe}'
|
PERMS_OLD = '...QUe}'
|
||||||
PERMS_NEW = '...QUe,"/marvis":Tt,"/marvis/overview":Tt,"/marvis/actions":Tt,"/marvis/minis":Tt,"/marvis/ai":Tt,"/radio":Tt}'
|
perm_entries = []
|
||||||
|
if ENABLE_MARVIS:
|
||||||
|
perm_entries.extend([
|
||||||
|
'"/marvis":Tt',
|
||||||
|
'"/marvis/overview":Tt',
|
||||||
|
'"/marvis/actions":Tt',
|
||||||
|
'"/marvis/minis":Tt',
|
||||||
|
'"/marvis/ai":Tt',
|
||||||
|
])
|
||||||
|
if ENABLE_RADIO:
|
||||||
|
perm_entries.append('"/radio":Tt')
|
||||||
|
PERMS_NEW = '...QUe,' + ",".join(perm_entries) + '}'
|
||||||
|
|
||||||
if PERMS_OLD in src and PERMS_NEW not in src:
|
if PERMS_OLD in src and PERMS_NEW not in src:
|
||||||
src = src.replace(PERMS_OLD, PERMS_NEW, 1)
|
src = src.replace(PERMS_OLD, PERMS_NEW, 1)
|
||||||
print("Applied: /marvis permissions entry")
|
print("Applied: permissions entry")
|
||||||
changed = True
|
changed = True
|
||||||
elif PERMS_NEW in src:
|
elif all(entry in src for entry in perm_entries):
|
||||||
print("Skipped: /marvis permissions entry already present")
|
print("Skipped: permissions entry already present")
|
||||||
else:
|
else:
|
||||||
print("WARNING: BW permissions anchor not found — permissions not registered")
|
print("WARNING: BW permissions anchor not found — permissions not registered")
|
||||||
|
|
||||||
assert src.count('P5G Marvis') >= 1, "P5G Marvis not found after patch"
|
if ENABLE_MARVIS:
|
||||||
assert src.count('P5G Radio') >= 1, "P5G Radio not found after patch"
|
assert src.count('P5G Marvis') >= 1, "P5G Marvis not found after patch"
|
||||||
|
if ENABLE_RADIO:
|
||||||
|
assert src.count('P5G Radio') >= 1, "P5G Radio not found after patch"
|
||||||
|
|
||||||
if not changed:
|
if not changed:
|
||||||
print("Nothing changed — already fully patched")
|
print("Nothing changed — already fully patched")
|
||||||
@@ -105,4 +163,7 @@ if not changed:
|
|||||||
with open(JS, "w", encoding="utf-8") as f:
|
with open(JS, "w", encoding="utf-8") as f:
|
||||||
f.write(src)
|
f.write(src)
|
||||||
|
|
||||||
print(f"Done — P5G Marvis: {src.count('P5G Marvis')} occurrences, P5G Radio: {src.count('P5G Radio')} occurrences")
|
print(
|
||||||
|
f"Done — P5G Marvis: {src.count('P5G Marvis')} occurrences, "
|
||||||
|
f"P5G Radio: {src.count('P5G Radio')} occurrences"
|
||||||
|
)
|
||||||
|
|||||||
12
userfiles/label.txt
Normal file
12
userfiles/label.txt
Normal file
File diff suppressed because one or more lines are too long
2
userfiles/permforroute.txt
Normal file
2
userfiles/permforroute.txt
Normal file
File diff suppressed because one or more lines are too long
2
userfiles/subitems.txt
Normal file
2
userfiles/subitems.txt
Normal file
File diff suppressed because one or more lines are too long
16
userfiles/upf.txt
Normal file
16
userfiles/upf.txt
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user