diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 287b9196..71a4b6bf 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react" 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Switch } from "./ui/switch" @@ -46,6 +46,8 @@ interface ProxMenuxTool { key: string name: string enabled: boolean + version?: string + has_source?: boolean } interface RemoteStorage { @@ -79,6 +81,18 @@ export function Settings() { const [loadingTools, setLoadingTools] = useState(true) const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes") 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 const [suppressionCategories, setSuppressionCategories] = useState([]) @@ -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 networkUnit = unit as "Bytes" | "Bits" localStorage.setItem("proxmenux-network-unit", networkUnit) @@ -843,10 +877,22 @@ export function Settings() { {proxmenuxTools.map((tool) => (
-
- {tool.name} +
+
+ {tool.name} + v{tool.version || '1.0'} +
+ {tool.has_source && ( + + )}
))}
@@ -854,6 +900,65 @@ export function Settings() { )} + + {/* Code Viewer Modal */} + {codeModal.open && ( +
setCodeModal(prev => ({ ...prev, open: false }))}> +
+
e.stopPropagation()} + > + {/* Header */} +
+
+ +
+

{codeModal.toolName}

+

+ {codeModal.functionName && {codeModal.functionName}()} + {codeModal.script && — {codeModal.script}} + {codeModal.version && v{codeModal.version}} +

+
+
+
+ {codeModal.source && ( + + )} + +
+
+ {/* Body */} +
+ {codeModal.loading ? ( +
+
+
+ ) : codeModal.error ? ( +
+ +

{codeModal.error}

+
+ ) : ( +
{codeModal.source}
+ )} +
+
+
+ )}
) } diff --git a/AppImage/scripts/flask_proxmenux_routes.py b/AppImage/scripts/flask_proxmenux_routes.py index d8725b43..b06252b6 100644 --- a/AppImage/scripts/flask_proxmenux_routes.py +++ b/AppImage/scripts/flask_proxmenux_routes.py @@ -1,33 +1,97 @@ -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, request import json import os +import re proxmenux_bp = Blueprint('proxmenux', __name__) -# Tool descriptions mapping -TOOL_DESCRIPTIONS = { - 'lvm_repair': 'LVM PV Headers Repair', - 'repo_cleanup': 'Repository Cleanup', - 'subscription_banner': 'Subscription Banner Removal', - 'time_sync': 'Time Synchronization', - 'apt_languages': 'APT Language Skip', - 'journald': 'Journald Optimization', - 'logrotate': 'Logrotate Optimization', - 'system_limits': 'System Limits Increase', - 'entropy': 'Entropy Generation (haveged)', - 'memory_settings': 'Memory Settings Optimization', - 'kernel_panic': 'Kernel Panic Configuration', - 'apt_ipv4': 'APT IPv4 Force', - 'kexec': 'kexec for quick reboots', - 'network_optimization': 'Network Optimizations', - 'bashrc_custom': 'Bashrc Customization', - 'figurine': 'Figurine', - 'fastfetch': 'Fastfetch', - 'log2ram': 'Log2ram (SSD Protection)', - 'amd_fixes': 'AMD CPU (Ryzen/EPYC) fixes', - 'persistent_network': 'Setting persistent network interfaces' +# Tool metadata: description, function name in bash script, and version +# version: current version of the optimization function +# function: the bash function name that implements this optimization +TOOL_METADATA = { + 'subscription_banner': {'name': 'Subscription Banner Removal', 'function': 'remove_subscription_banner', 'version': '1.0'}, + 'time_sync': {'name': 'Time Synchronization', 'function': 'configure_time_sync', 'version': '1.0'}, + 'apt_languages': {'name': 'APT Language Skip', 'function': 'skip_apt_languages', 'version': '1.0'}, + 'journald': {'name': 'Journald Optimization', 'function': 'optimize_journald', 'version': '1.0'}, + 'logrotate': {'name': 'Logrotate Optimization', 'function': 'optimize_logrotate', 'version': '1.0'}, + 'system_limits': {'name': 'System Limits Increase', 'function': 'increase_system_limits', 'version': '1.0'}, + 'entropy': {'name': 'Entropy Generation (haveged)', 'function': 'configure_entropy', 'version': '1.0'}, + 'memory_settings': {'name': 'Memory Settings Optimization', 'function': 'optimize_memory_settings', 'version': '1.0'}, + 'kernel_panic': {'name': 'Kernel Panic Configuration', 'function': 'configure_kernel_panic', 'version': '1.0'}, + 'apt_ipv4': {'name': 'APT IPv4 Force', 'function': 'force_apt_ipv4', 'version': '1.0'}, + 'kexec': {'name': 'kexec for quick reboots', 'function': 'enable_kexec', 'version': '1.0'}, + 'network_optimization': {'name': 'Network Optimizations', 'function': 'apply_network_optimizations', 'version': '1.0'}, + 'bashrc_custom': {'name': 'Bashrc Customization', 'function': 'customize_bashrc', 'version': '1.0'}, + 'figurine': {'name': 'Figurine', 'function': 'configure_figurine', 'version': '1.0'}, + 'fastfetch': {'name': 'Fastfetch', 'function': 'configure_fastfetch', 'version': '1.0'}, + 'log2ram': {'name': 'Log2ram (SSD Protection)', 'function': 'configure_log2ram', 'version': '1.0'}, + 'amd_fixes': {'name': 'AMD CPU (Ryzen/EPYC) fixes', 'function': 'apply_amd_fixes', 'version': '1.0'}, + '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']) def get_update_status(): """Get ProxMenux update availability status from config.json""" @@ -83,14 +147,17 @@ def get_installed_tools(): with open(installed_tools_path, 'r') as f: data = json.load(f) - # Convert to list format with descriptions + # Convert to list format with descriptions and version tools = [] for tool_key, enabled in data.items(): if enabled: # Only include enabled tools + meta = TOOL_METADATA.get(tool_key, {}) tools.append({ 'key': tool_key, - 'name': TOOL_DESCRIPTIONS.get(tool_key, tool_key.replace('_', ' ').title()), - 'enabled': enabled + 'name': meta.get('name', tool_key.replace('_', ' ').title()), + 'enabled': enabled, + 'version': meta.get('version', '1.0'), + 'has_source': bool(meta.get('function')), }) # Sort alphabetically by name @@ -112,3 +179,54 @@ def get_installed_tools(): 'success': False, 'error': str(e) }), 500 + + +@proxmenux_bp.route('/api/proxmenux/tool-source/', 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