#!/usr/bin/env python3 """ patch-ncm.py — Inject P5G Marvis + P5G Radio pages into the NCM React bundle. Reads from the clean .bak, applies all patches, writes the result. Safe to re-run (idempotent). """ import os import socket import sys JS = "/etc/athonet/ems-frontend/advanced/assets/index-Cw8Irsq8.js" BAK = JS + ".bak" with open(BAK, "r", encoding="utf-8") as f: src = f.read() 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 ─────────────────────────────────────────────────── SIDEBAR_OLD = '!_o(ue.UPF,t)}]}]}' sidebar_items = [] if ENABLE_MARVIS: sidebar_items.append( '{value:"/marvis",label:"P5G Marvis",' 'icon:a.jsx(ge.Magic,{}),disabled:false,' 'subItems:[' '{value:"/marvis/overview",label:"P5G Marvis Insights",disabled:false},' '{value:"/marvis/actions",label:"P5G Marvis Actions",disabled:false},' '{value:"/marvis/minis",label:"P5G Marvis Minis",disabled:false},' '{value:"/marvis/ai",label:"P5G Marvis AI",disabled:false}' ']}' ) 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: src = src.replace(SIDEBAR_OLD, SIDEBAR_NEW, 1) print("Applied: sidebar entry") changed = True elif any(label in src for label in ["P5G Marvis", "P5G Radio"]): print("Skipped: sidebar entry already present") else: print("ERROR: sidebar anchor not found"); exit(1) # ── 2. React Router route with iframe element ────────────────────────────── # Insert between G4t (UPF route object) and rht (Platform route object) # in the router children array — this is the unique anchor in the route config ROUTE_ANCHOR = 'G4t,rht' # Wrap in Or({fullHeight:!0}) exactly like every other NF route — this gives the # NCM AppBar + left-navigation sidebar via G0, and a content area that properly # fills the remaining height. The iframe then uses height:"100%" to fill it. MARVIS_ROUTE = ('{path:"marvis",' 'element:a.jsx(Or,{fullHeight:!0}),' 'handle:vr({labelIntl:"P5G Marvis",icon:a.jsx(ge.Magic,{})}),' 'children:[' '{index:!0,element:a.jsx(ur,{to:"overview",replace:!0})},' '{path:"overview",element:a.jsx("iframe",{src:"/core/marvis/overview",style:{display:"block",width:"100%",height:"100%",border:"none"},title:"P5G Marvis Insights"})},' '{path:"actions",element:a.jsx("iframe",{src:"/core/marvis/actions",style:{display:"block",width:"100%",height:"100%",border:"none"},title:"P5G Marvis Actions"})},' '{path:"minis",element:a.jsx("iframe",{src:"/core/marvis/minis",style:{display:"block",width:"100%",height:"100%",border:"none"},title:"P5G Marvis Minis"})},' '{path:"ai",element:a.jsx("iframe",{src:"/core/marvis/",style:{display:"block",width:"100%",height:"100%",border:"none"},title:"P5G Marvis AI"})}' ']}') # closed marvis route RADIO_ROUTE = ('{path:"radio",' 'element:a.jsx(Or,{fullHeight:!0}),' 'handle:vr({labelIntl:"P5G Radio",icon:a.jsx(ge.Magic,{})}),' 'children:[' '{index:!0,element:a.jsx("iframe",{src:"/core/radio/",' 'style:{display:"block",width:"100%",height:"100%",border:"none"},' 'title:"P5G Radio"})}' ']}') ROUTE_MARKER = 'path:"marvis"' RADIO_MARKER = 'path:"radio"' route_parts = [] if ENABLE_MARVIS: 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 elif ROUTE_MARKER in src or RADIO_MARKER in src: print("Skipped: route entry already present") else: print("WARNING: route anchor G4t,rht not found — iframe routes not injected") # ── Validate ─────────────────────────────────────────────────────────────── # ── 3. Register /marvis in the BW permissions registry ──────────────────── # BW is the route→permissions map. If a path is missing, the app throws # "Permissions for route /marvis are not managed". Tt=()=>!0 means no # specific permission required (same as profile and siteLoader routes). PERMS_OLD = '...QUe}' 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: src = src.replace(PERMS_OLD, PERMS_NEW, 1) print("Applied: permissions entry") changed = True elif all(entry in src for entry in perm_entries): print("Skipped: permissions entry already present") else: print("WARNING: BW permissions anchor not found — permissions not registered") if ENABLE_MARVIS: 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: print("Nothing changed — already fully patched") exit(0) with open(JS, "w", encoding="utf-8") as f: f.write(src) print( f"Done — P5G Marvis: {src.count('P5G Marvis')} occurrences, " f"P5G Radio: {src.count('P5G Radio')} occurrences" )