mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 08:56:21 +00:00
Update flask_proxmenux_routes.py
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||||
import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff } from "lucide-react"
|
import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff, Code, X, Copy } from "lucide-react"
|
||||||
import { NotificationSettings } from "./notification-settings"
|
import { NotificationSettings } from "./notification-settings"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
import { Switch } from "./ui/switch"
|
import { Switch } from "./ui/switch"
|
||||||
@@ -46,6 +46,8 @@ interface ProxMenuxTool {
|
|||||||
key: string
|
key: string
|
||||||
name: string
|
name: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
version?: string
|
||||||
|
has_source?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RemoteStorage {
|
interface RemoteStorage {
|
||||||
@@ -79,6 +81,18 @@ export function Settings() {
|
|||||||
const [loadingTools, setLoadingTools] = useState(true)
|
const [loadingTools, setLoadingTools] = useState(true)
|
||||||
const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes")
|
const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes")
|
||||||
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true)
|
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true)
|
||||||
|
// Code viewer modal state
|
||||||
|
const [codeModal, setCodeModal] = useState<{
|
||||||
|
open: boolean
|
||||||
|
loading: boolean
|
||||||
|
toolName: string
|
||||||
|
version: string
|
||||||
|
functionName: string
|
||||||
|
source: string
|
||||||
|
script: string
|
||||||
|
error: string
|
||||||
|
}>({ open: false, loading: false, toolName: '', version: '', functionName: '', source: '', script: '', error: '' })
|
||||||
|
const [codeCopied, setCodeCopied] = useState(false)
|
||||||
|
|
||||||
// Health Monitor suppression settings
|
// Health Monitor suppression settings
|
||||||
const [suppressionCategories, setSuppressionCategories] = useState<SuppressionCategory[]>([])
|
const [suppressionCategories, setSuppressionCategories] = useState<SuppressionCategory[]>([])
|
||||||
@@ -120,6 +134,26 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewToolSource = async (tool: ProxMenuxTool) => {
|
||||||
|
setCodeModal({ open: true, loading: true, toolName: tool.name, version: tool.version || '1.0', functionName: '', source: '', script: '', error: '' })
|
||||||
|
try {
|
||||||
|
const data = await fetchApi(`/api/proxmenux/tool-source/${tool.key}`)
|
||||||
|
if (data.success) {
|
||||||
|
setCodeModal(prev => ({ ...prev, loading: false, functionName: data.function, source: data.source, script: data.script }))
|
||||||
|
} else {
|
||||||
|
setCodeModal(prev => ({ ...prev, loading: false, error: data.error || 'Source code not available' }))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setCodeModal(prev => ({ ...prev, loading: false, error: 'Failed to load source code' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copySourceCode = () => {
|
||||||
|
navigator.clipboard.writeText(codeModal.source)
|
||||||
|
setCodeCopied(true)
|
||||||
|
setTimeout(() => setCodeCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
const changeNetworkUnit = (unit: string) => {
|
const changeNetworkUnit = (unit: string) => {
|
||||||
const networkUnit = unit as "Bytes" | "Bits"
|
const networkUnit = unit as "Bytes" | "Bits"
|
||||||
localStorage.setItem("proxmenux-network-unit", networkUnit)
|
localStorage.setItem("proxmenux-network-unit", networkUnit)
|
||||||
@@ -843,10 +877,22 @@ export function Settings() {
|
|||||||
{proxmenuxTools.map((tool) => (
|
{proxmenuxTools.map((tool) => (
|
||||||
<div
|
<div
|
||||||
key={tool.key}
|
key={tool.key}
|
||||||
className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg border border-border hover:bg-muted transition-colors"
|
className="flex items-center justify-between gap-2 p-3 bg-muted/50 rounded-lg border border-border hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<span className="text-sm font-medium">{tool.name}</span>
|
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
|
||||||
|
<span className="text-sm font-medium truncate">{tool.name}</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded font-mono flex-shrink-0">v{tool.version || '1.0'}</span>
|
||||||
|
</div>
|
||||||
|
{tool.has_source && (
|
||||||
|
<button
|
||||||
|
onClick={() => viewToolSource(tool)}
|
||||||
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
|
||||||
|
title="View source code"
|
||||||
|
>
|
||||||
|
<Code className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -854,6 +900,65 @@ export function Settings() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Code Viewer Modal */}
|
||||||
|
{codeModal.open && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={() => setCodeModal(prev => ({ ...prev, open: false }))}>
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
|
<div
|
||||||
|
className="relative bg-card border border-border rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<Code className="h-5 w-5 text-orange-500 flex-shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-sm font-semibold truncate">{codeModal.toolName}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{codeModal.functionName && <span className="font-mono">{codeModal.functionName}()</span>}
|
||||||
|
{codeModal.script && <span> — {codeModal.script}</span>}
|
||||||
|
{codeModal.version && <span className="ml-2 bg-muted px-1.5 py-0.5 rounded font-mono">v{codeModal.version}</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{codeModal.source && (
|
||||||
|
<button
|
||||||
|
onClick={copySourceCode}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-muted hover:bg-muted/80 transition-colors"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
{codeCopied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
||||||
|
{codeCopied ? 'Copied' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setCodeModal(prev => ({ ...prev, open: false }))}
|
||||||
|
className="p-1.5 rounded-md hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Body */}
|
||||||
|
<div className="flex-1 overflow-auto p-0">
|
||||||
|
{codeModal.loading ? (
|
||||||
|
<div className="flex items-center justify-center py-16">
|
||||||
|
<div className="animate-spin h-8 w-8 border-4 border-orange-500 border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
) : codeModal.error ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
|
||||||
|
<Code className="h-10 w-10 mb-3 opacity-40" />
|
||||||
|
<p className="text-sm">{codeModal.error}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre className="text-xs leading-relaxed font-mono p-4 overflow-x-auto whitespace-pre text-foreground bg-muted/30"><code>{codeModal.source}</code></pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,97 @@
|
|||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify, request
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
proxmenux_bp = Blueprint('proxmenux', __name__)
|
proxmenux_bp = Blueprint('proxmenux', __name__)
|
||||||
|
|
||||||
# Tool descriptions mapping
|
# Tool metadata: description, function name in bash script, and version
|
||||||
TOOL_DESCRIPTIONS = {
|
# version: current version of the optimization function
|
||||||
'lvm_repair': 'LVM PV Headers Repair',
|
# function: the bash function name that implements this optimization
|
||||||
'repo_cleanup': 'Repository Cleanup',
|
TOOL_METADATA = {
|
||||||
'subscription_banner': 'Subscription Banner Removal',
|
'subscription_banner': {'name': 'Subscription Banner Removal', 'function': 'remove_subscription_banner', 'version': '1.0'},
|
||||||
'time_sync': 'Time Synchronization',
|
'time_sync': {'name': 'Time Synchronization', 'function': 'configure_time_sync', 'version': '1.0'},
|
||||||
'apt_languages': 'APT Language Skip',
|
'apt_languages': {'name': 'APT Language Skip', 'function': 'skip_apt_languages', 'version': '1.0'},
|
||||||
'journald': 'Journald Optimization',
|
'journald': {'name': 'Journald Optimization', 'function': 'optimize_journald', 'version': '1.0'},
|
||||||
'logrotate': 'Logrotate Optimization',
|
'logrotate': {'name': 'Logrotate Optimization', 'function': 'optimize_logrotate', 'version': '1.0'},
|
||||||
'system_limits': 'System Limits Increase',
|
'system_limits': {'name': 'System Limits Increase', 'function': 'increase_system_limits', 'version': '1.0'},
|
||||||
'entropy': 'Entropy Generation (haveged)',
|
'entropy': {'name': 'Entropy Generation (haveged)', 'function': 'configure_entropy', 'version': '1.0'},
|
||||||
'memory_settings': 'Memory Settings Optimization',
|
'memory_settings': {'name': 'Memory Settings Optimization', 'function': 'optimize_memory_settings', 'version': '1.0'},
|
||||||
'kernel_panic': 'Kernel Panic Configuration',
|
'kernel_panic': {'name': 'Kernel Panic Configuration', 'function': 'configure_kernel_panic', 'version': '1.0'},
|
||||||
'apt_ipv4': 'APT IPv4 Force',
|
'apt_ipv4': {'name': 'APT IPv4 Force', 'function': 'force_apt_ipv4', 'version': '1.0'},
|
||||||
'kexec': 'kexec for quick reboots',
|
'kexec': {'name': 'kexec for quick reboots', 'function': 'enable_kexec', 'version': '1.0'},
|
||||||
'network_optimization': 'Network Optimizations',
|
'network_optimization': {'name': 'Network Optimizations', 'function': 'apply_network_optimizations', 'version': '1.0'},
|
||||||
'bashrc_custom': 'Bashrc Customization',
|
'bashrc_custom': {'name': 'Bashrc Customization', 'function': 'customize_bashrc', 'version': '1.0'},
|
||||||
'figurine': 'Figurine',
|
'figurine': {'name': 'Figurine', 'function': 'configure_figurine', 'version': '1.0'},
|
||||||
'fastfetch': 'Fastfetch',
|
'fastfetch': {'name': 'Fastfetch', 'function': 'configure_fastfetch', 'version': '1.0'},
|
||||||
'log2ram': 'Log2ram (SSD Protection)',
|
'log2ram': {'name': 'Log2ram (SSD Protection)', 'function': 'configure_log2ram', 'version': '1.0'},
|
||||||
'amd_fixes': 'AMD CPU (Ryzen/EPYC) fixes',
|
'amd_fixes': {'name': 'AMD CPU (Ryzen/EPYC) fixes', 'function': 'apply_amd_fixes', 'version': '1.0'},
|
||||||
'persistent_network': 'Setting persistent network interfaces'
|
'persistent_network': {'name': 'Setting persistent network interfaces', 'function': 'setup_persistent_network', 'version': '1.0'},
|
||||||
|
'vfio_iommu': {'name': 'VFIO/IOMMU Passthrough', 'function': 'enable_vfio_iommu', 'version': '1.0'},
|
||||||
|
'lvm_repair': {'name': 'LVM PV Headers Repair', 'function': 'repair_lvm_headers', 'version': '1.0'},
|
||||||
|
'repo_cleanup': {'name': 'Repository Cleanup', 'function': 'cleanup_repos', 'version': '1.0'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Backward-compatible description mapping (used by get_installed_tools)
|
||||||
|
TOOL_DESCRIPTIONS = {k: v['name'] for k, v in TOOL_METADATA.items()}
|
||||||
|
|
||||||
|
# Scripts to search for function source code (in order of preference)
|
||||||
|
_SCRIPT_PATHS = [
|
||||||
|
'/usr/local/share/proxmenux/scripts/post_install/customizable_post_install.sh',
|
||||||
|
'/usr/local/share/proxmenux/scripts/post_install/auto_post_install.sh',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_bash_function(function_name: str) -> dict:
|
||||||
|
"""Extract a bash function's source code from the post-install scripts.
|
||||||
|
|
||||||
|
Searches each script for `function_name() {` and captures everything
|
||||||
|
until the matching closing `}` at column 0, respecting brace nesting.
|
||||||
|
|
||||||
|
Returns {'source': str, 'script': str, 'line_start': int, 'line_end': int}
|
||||||
|
or {'source': '', 'error': '...'} on failure.
|
||||||
|
"""
|
||||||
|
for script_path in _SCRIPT_PATHS:
|
||||||
|
if not os.path.isfile(script_path):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with open(script_path, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# Find function start: "function_name() {" or "function_name () {"
|
||||||
|
pattern = re.compile(rf'^{re.escape(function_name)}\s*\(\)\s*\{{')
|
||||||
|
start_idx = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if pattern.match(line):
|
||||||
|
start_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if start_idx is None:
|
||||||
|
continue # Try next script
|
||||||
|
|
||||||
|
# Capture until the closing } at indent level 0
|
||||||
|
brace_depth = 0
|
||||||
|
end_idx = start_idx
|
||||||
|
for i in range(start_idx, len(lines)):
|
||||||
|
brace_depth += lines[i].count('{') - lines[i].count('}')
|
||||||
|
if brace_depth <= 0:
|
||||||
|
end_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
source = ''.join(lines[start_idx:end_idx + 1])
|
||||||
|
script_name = os.path.basename(script_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'source': source,
|
||||||
|
'script': script_name,
|
||||||
|
'line_start': start_idx + 1,
|
||||||
|
'line_end': end_idx + 1,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return {'source': '', 'error': 'Function not found in available scripts'}
|
||||||
|
|
||||||
@proxmenux_bp.route('/api/proxmenux/update-status', methods=['GET'])
|
@proxmenux_bp.route('/api/proxmenux/update-status', methods=['GET'])
|
||||||
def get_update_status():
|
def get_update_status():
|
||||||
"""Get ProxMenux update availability status from config.json"""
|
"""Get ProxMenux update availability status from config.json"""
|
||||||
@@ -83,14 +147,17 @@ def get_installed_tools():
|
|||||||
with open(installed_tools_path, 'r') as f:
|
with open(installed_tools_path, 'r') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# Convert to list format with descriptions
|
# Convert to list format with descriptions and version
|
||||||
tools = []
|
tools = []
|
||||||
for tool_key, enabled in data.items():
|
for tool_key, enabled in data.items():
|
||||||
if enabled: # Only include enabled tools
|
if enabled: # Only include enabled tools
|
||||||
|
meta = TOOL_METADATA.get(tool_key, {})
|
||||||
tools.append({
|
tools.append({
|
||||||
'key': tool_key,
|
'key': tool_key,
|
||||||
'name': TOOL_DESCRIPTIONS.get(tool_key, tool_key.replace('_', ' ').title()),
|
'name': meta.get('name', tool_key.replace('_', ' ').title()),
|
||||||
'enabled': enabled
|
'enabled': enabled,
|
||||||
|
'version': meta.get('version', '1.0'),
|
||||||
|
'has_source': bool(meta.get('function')),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort alphabetically by name
|
# Sort alphabetically by name
|
||||||
@@ -112,3 +179,54 @@ def get_installed_tools():
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@proxmenux_bp.route('/api/proxmenux/tool-source/<tool_key>', methods=['GET'])
|
||||||
|
def get_tool_source(tool_key):
|
||||||
|
"""Get the bash source code of a specific optimization function.
|
||||||
|
|
||||||
|
Returns the function body extracted from the post-install scripts,
|
||||||
|
so users can see exactly what code was executed on their server.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
meta = TOOL_METADATA.get(tool_key)
|
||||||
|
if not meta:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Unknown tool: {tool_key}'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
func_name = meta.get('function')
|
||||||
|
if not func_name:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'No function mapping for {tool_key}'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
result = _extract_bash_function(func_name)
|
||||||
|
|
||||||
|
if not result.get('source'):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': result.get('error', 'Source code not available'),
|
||||||
|
'tool': tool_key,
|
||||||
|
'function': func_name,
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'tool': tool_key,
|
||||||
|
'name': meta['name'],
|
||||||
|
'version': meta.get('version', '1.0'),
|
||||||
|
'function': func_name,
|
||||||
|
'source': result['source'],
|
||||||
|
'script': result['script'],
|
||||||
|
'line_start': result['line_start'],
|
||||||
|
'line_end': result['line_end'],
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|||||||
Reference in New Issue
Block a user