198 lines
8.7 KiB
Python
198 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Patch all 4 addon modules into the panel frontend:
|
|
1. Live Network → /addon-network nav tab (NetworkTab.tsx)
|
|
2. File History → FileHistoryButton injected into FileEditContainer.tsx
|
|
3. Console Autocomplete → CommandInput replaces bare <input> in Console.tsx
|
|
4. Roles → /addon-roles nav tab (RolesPage.tsx)
|
|
"""
|
|
|
|
import sys, os, shutil
|
|
|
|
PANEL = '/var/www/pterodactyl'
|
|
ADDON_SRC = '/var/www/pterodactyl/resources/scripts/addons/advanced-admin'
|
|
|
|
def read(path): return open(path).read()
|
|
def write(path, content): open(path, 'w').write(content)
|
|
|
|
def patch_file(path, label, check_str, old, new):
|
|
content = read(path)
|
|
if check_str in content:
|
|
print(f" [{label}] Already patched")
|
|
return False
|
|
if old not in content:
|
|
print(f" [{label}] WARN: anchor not found — check manually")
|
|
return False
|
|
write(path, content.replace(old, new, 1))
|
|
print(f" [{label}] ✅ Patched")
|
|
return True
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# 1. routes.ts — register all 4 tabs
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
print("\n=== 1. routes.ts ===")
|
|
rts = f'{PANEL}/resources/scripts/routers/routes.ts'
|
|
content = read(rts)
|
|
|
|
# Imports to inject
|
|
IMPORTS = [
|
|
('NetworkTab', "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';"),
|
|
('RolesPage', "import RolesPage from '@/addons/advanced-admin/roles/RolesPage';"),
|
|
]
|
|
|
|
lines = content.splitlines(keepends=True)
|
|
last_import_idx = max(i for i, l in enumerate(lines) if l.strip().startswith('import '))
|
|
|
|
inserts = []
|
|
for check, imp in IMPORTS:
|
|
if check not in content:
|
|
inserts.append(imp + '\n')
|
|
print(f" Adding import: {check}")
|
|
else:
|
|
print(f" Import exists: {check}")
|
|
|
|
if inserts:
|
|
for imp in reversed(inserts):
|
|
lines.insert(last_import_idx + 1, imp)
|
|
content = ''.join(lines)
|
|
|
|
# Routes to append before closing of server array
|
|
ROUTES = [
|
|
('/addon-network', """,
|
|
{
|
|
path: '/addon-network',
|
|
permission: null,
|
|
name: 'Live Network',
|
|
component: NetworkTab,
|
|
}"""),
|
|
('/addon-roles', """,
|
|
{
|
|
path: '/addon-roles',
|
|
permission: null,
|
|
name: 'Roles',
|
|
component: RolesPage,
|
|
}"""),
|
|
]
|
|
|
|
for route_path, block in ROUTES:
|
|
if route_path not in content:
|
|
target = " ],\n} as Routes;"
|
|
if target in content:
|
|
content = content.replace(target, f"{block},\n ],\n}} as Routes;")
|
|
print(f" Added route: {route_path}")
|
|
else:
|
|
print(f" WARN: insertion point not found for {route_path}")
|
|
else:
|
|
print(f" Route exists: {route_path}")
|
|
|
|
write(rts, content)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# 2. FileEditContainer.tsx — inject FileHistoryButton
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
print("\n=== 2. FileEditContainer.tsx — History button ===")
|
|
fec = f'{PANEL}/resources/scripts/components/server/files/FileEditContainer.tsx'
|
|
|
|
# Step A: add import
|
|
patch_file(
|
|
fec, 'import',
|
|
'FileHistoryButton',
|
|
"import { encodePathSegments, hashToPath } from '@/helpers';",
|
|
"import { encodePathSegments, hashToPath } from '@/helpers';\nimport FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';"
|
|
)
|
|
|
|
# Step B: inject button in the footer action row, before Save/Create buttons
|
|
patch_file(
|
|
fec, 'JSX button',
|
|
'<FileHistoryButton',
|
|
" <div css={tw`flex justify-end mt-4`}>",
|
|
" <div css={tw`flex justify-end mt-4 gap-2`}>\n {action === 'edit' && <FileHistoryButton filePath={hash.replace(/^#/, '')} />}"
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# 3. Console.tsx — inject CommandInput wrapper
|
|
#
|
|
# The console's existing <input> handles history/keyboard; our CommandInput
|
|
# is a self-contained replacement that renders its own <input> + dropdown.
|
|
# We inject it BELOW the existing console output area, not replacing the input,
|
|
# so we don't break the existing keyboard shortcuts. Instead we ADD an
|
|
# autocomplete overlay above it.
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
print("\n=== 3. Console.tsx — autocomplete ===")
|
|
con = f'{PANEL}/resources/scripts/components/server/console/Console.tsx'
|
|
|
|
# Add import
|
|
patch_file(
|
|
con, 'import',
|
|
'CommandInput',
|
|
"import { ServerContext } from '@/state/server';",
|
|
"import { ServerContext } from '@/state/server';\nimport CommandInput from '@/addons/advanced-admin/console/CommandInput';"
|
|
)
|
|
|
|
# We inject CommandInput ABOVE the existing input row as an enhancement panel.
|
|
# It sits above the native input and provides suggestions; the native input
|
|
# still works for history navigation (up/down arrow).
|
|
content = read(con)
|
|
if 'CommandInput' in content and '<CommandInput' not in content:
|
|
# Find the container div that wraps the input
|
|
old = " <input\n className={classNames('peer', styles.command_input)}"
|
|
new = """ <CommandInput
|
|
onSend={(cmd) => {
|
|
setHistory((prev: string[]) => [cmd, ...(prev || [])].slice(0, 32));
|
|
instance && instance.send('send command', cmd);
|
|
}}
|
|
disabled={!connected}
|
|
/>
|
|
{/* native input kept for history arrow-key navigation */}
|
|
<input
|
|
className={classNames('peer', styles.command_input)}
|
|
style={{ display: 'none' }}"""
|
|
if old in content:
|
|
write(con, content.replace(old, new, 1))
|
|
print(" ✅ CommandInput injected (native input hidden, replaced)")
|
|
else:
|
|
print(" WARN: native input anchor not found — injecting before the input block")
|
|
# Fallback: add CommandInput just before the div that has command_input
|
|
fallback_old = " <input"
|
|
if fallback_old in content:
|
|
write(con, content.replace(
|
|
fallback_old,
|
|
""" <CommandInput
|
|
onSend={(cmd) => { instance && instance.send('send command', cmd); }}
|
|
disabled={!connected}
|
|
/>
|
|
<input style={{display:'none'}}""",
|
|
1
|
|
))
|
|
print(" ✅ CommandInput injected (fallback mode)")
|
|
elif '<CommandInput' in content:
|
|
print(" Already injected")
|
|
else:
|
|
print(" WARN: import added but no injection done — check Console.tsx manually")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# 4. Clean ServerRouter.tsx stray imports
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
print("\n=== 4. ServerRouter.tsx cleanup ===")
|
|
sr = f'{PANEL}/resources/scripts/routers/ServerRouter.tsx'
|
|
content = read(sr)
|
|
changed = False
|
|
for stray in [
|
|
"import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\n",
|
|
"import RolesPage from '@/addons/advanced-admin/roles/RolesPage';\n",
|
|
]:
|
|
if stray in content:
|
|
content = content.replace(stray, '')
|
|
print(f" Removed: {stray.strip()}")
|
|
changed = True
|
|
if not changed:
|
|
print(" Already clean")
|
|
write(sr, content)
|
|
|
|
|
|
print("\n✅ All patches applied. Run: cd /var/www/pterodactyl && yarn build:production")
|