{% trans "CyberPanel's plugin system allows developers to extend the control panel's functionality with custom features. Plugins integrate seamlessly with CyberPanel's Django-based architecture, providing access to the full power of the platform while maintaining security and consistency." %}
{% trans "What Can Plugins Do?" %}
@@ -347,8 +458,11 @@
{% trans "Create custom reporting tools" %}
{% trans "Integrate security features" %}
+
-
{% trans "Prerequisites" %}
+
+
{% trans "Prerequisites" %}
+
{% trans "Required Knowledge" %}
Python 3.6+: {% trans "Basic to intermediate Python knowledge" %}
@@ -357,8 +471,11 @@
Linux/Unix: {% trans "Basic command-line familiarity" %}
XML: {% trans "Understanding of XML structure for meta.xml" %}
+ {% trans "Required: Category (type)" %}: {% trans "The <type> field is required. See the Plugin Categories section below for valid options. Plugins without a valid category will not appear in the Plugin Store." %}
+
+
{% trans "Step 3: Create urls.py" %}
from django.urls import path
from . import views
@@ -458,8 +582,11 @@ def main_view(request):
{% trans "Important" %}: {% trans "Always use the @cyberpanel_login_required decorator for all views to ensure users are authenticated." %}
+
-
{% trans "Plugin Structure & Files" %}
+
+
{% trans "Plugin Structure & Files" %}
+
{% trans "Required Files" %}
__init__.py - {% trans "Required" %} - {% trans "Python package marker" %}
@@ -476,8 +603,11 @@ def main_view(request):
templates/ - {% trans "Optional" %} - {% trans "HTML templates" %}
static/ - {% trans "Optional" %} - {% trans "CSS, JS, images" %}
+
-
{% trans "Version Numbering (Semantic Versioning)" %}
+
+
{% trans "Version Numbering (Semantic Versioning)" %}
+
{% trans "CyberPanel plugins use semantic versioning (SemVer) with a three-number format (X.Y.Z) to help users understand the impact of each update:" %}
@@ -505,8 +635,107 @@ def main_view(request):
{% trans "Version Format in meta.xml" %}
<version>1.0.0</version>
{% trans "Never use formats like '1.0' or 'v1.0'. Always use the full semantic version: '1.0.0'" %}
+
-
{% trans "Core Components" %}
+
+
{% trans "Plugin Categories" %}
+
+
{% trans "The <type> field in meta.xml determines how your plugin is grouped in the Plugin Store. Use exactly one of these values (case-sensitive):" %}
+
+
+
+
{% trans "Category" %}
+
{% trans "Purpose" %}
+
+
+
+
Utility
{% trans "General-purpose tools, helpers, and utilities" %}
+
Security
{% trans "Security features: firewalls, fail2ban, SSL, etc." %}
+
Backup
{% trans "Backup, snapshot, and restore functionality" %}
+
Performance
{% trans "Caching, optimization, and performance tuning" %}
+
Monitoring
{% trans "Monitoring, alerts, and health checks" %}
+
Integration
{% trans "Third-party integrations: Discord, Slack, webhooks, APIs" %}
+
Email
{% trans "Email marketing, SMTP, mail management" %}
+
Development
{% trans "Developer tools: PM2, Node.js, deployment" %}
+
Analytics
{% trans "Analytics, tracking, and reporting" %}
+
+
+
+
+
+
{% trans "Freshness Badges" %}
+
+
{% trans "The Plugin Store and Installed Plugins views display freshness badges based on the last update date (modify_date from GitHub commit or meta.xml file mtime). These help users quickly see how actively maintained a plugin is:" %}
+
+
+
+
{% trans "Badge" %}
+
{% trans "Condition" %}
+
{% trans "Meaning" %}
+
+
+
+
NEW
{% trans "Updated within last 90 days" %}
{% trans "Recently released or actively maintained" %}
+
Stable
{% trans "Updated within last 365 days" %}
{% trans "Updated within the past year" %}
+
Unstable
{% trans "1–2 years since last update" %}
{% trans "May need maintenance; consider forking or updating" %}
+
STALE
{% trans "Over 2 years since last update" %}
{% trans "Not updated recently; use with caution" %}
+
+
+
{% trans "Badges are calculated automatically from the plugin's modify_date. For plugins in the Plugin Store, this comes from the GitHub repository's last commit. For installed plugins, it uses the meta.xml file modification time." %}
+
+
+
+
{% trans "Premium/Paid Plugin Creation" %}
+
+
{% trans "You can create premium (paid) plugins and implement your own verification system. This includes optional encryption between the plugin and your verification site to prevent unauthorized bypass." %}
+
+
{% trans "1. Mark Your Plugin as Paid in meta.xml" %}
{% trans "Set <paid>true</paid> to display the Premium badge and subscription prompts in the Plugin Store." %}
+
+
{% trans "2. Build Your Own Verification System" %}
+
{% trans "Premium plugins typically verify access via a remote API. You can host this on your own site. Common verification methods:" %}
+
+
{% trans "Patreon" %}: {% trans "Verify membership via Patreon OAuth/API" %}
+
{% trans "PayPal" %}: {% trans "Verify one-time or recurring payments" %}
+
{% trans "Plugin Grants" %}: {% trans "Admin panel where you manually grant access by email, IP, or domain" %}
+
{% trans "Activation Keys" %}: {% trans "Generate unique keys when granting access; users enter the key in the plugin" %}
+
+
+
{% trans "3. Optional: Encrypt Plugin–API Communication" %}
+
{% trans "To protect against users modifying your plugin to bypass verification, you can encrypt the communication between the plugin and your verification API using AES-256-CBC. This ensures:" %}
+
+
{% trans "Verification requests cannot be easily intercepted or forged" %}
+
{% trans "Responses cannot be tampered with" %}
+
{% trans "Your verification logic remains on your server; the plugin only encrypts/decrypts with a shared key" %}
+
+
{% trans "Implementation outline:" %}
+
+
{% trans "Generate a 32-byte secret key and store it in your API config (e.g. config.php)" %}
+
{% trans "In your plugin (Python), encrypt outgoing requests and decrypt responses using the same key" %}
+
{% trans "On your API (PHP/Python), decrypt incoming requests and encrypt responses" %}
+
{% trans "Use the X-Encrypted: 1 header to indicate encrypted payloads" %}
+
+
{% trans "Both sides must use the same AES-256-CBC key. Keep the key secret and never commit it to public repositories. Store it in a protected config file outside the web root or in environment variables." %}
+
+
{% trans "4. Typical Premium Plugin Flow" %}
+
+
{% trans "User installs your premium plugin" %}
+
{% trans "Plugin shows an activation screen (Patreon/PayPal links, or activation key input)" %}
+
{% trans "Plugin calls your verification API with user identifier (email, domain, IP) or activation key" %}
+
{% trans "Your API checks: Plugin Grants, activation key, Patreon, or PayPal — in your preferred order" %}
+
{% trans "If access is granted, API returns success; plugin unlocks features and optionally stores the key locally" %}
+
+
+ {% trans "Tip" %}: {% trans "Provide multiple verification paths: Patreon for subscribers, PayPal for one-time purchasers, Plugin Grants for beta testers or sponsors, and activation keys for manual grants. Encryption is optional but recommended for paid plugins to deter bypass attempts." %}
+
+
+
+
+
{% trans "Core Components" %}
+
{% trans "1. Authentication & Security" %}
{% trans "Always use the cyberpanel_login_required decorator:" %}
cd /home/cyberpanel/plugins/myPlugin
zip -r myPlugin-v1.0.0.zip . \
-x "*.pyc" \
-x "__pycache__/*" \
-x "*.log"
+
-
{% trans "Troubleshooting" %}
+
+
{% trans "Troubleshooting" %}
+
{% trans "Installation Issues" %}
{% trans "Check meta.xml format and validity" %}
@@ -626,8 +873,11 @@ zip -r myPlugin-v1.0.0.zip . \
{% trans "Review template paths" %}
{% trans "Check for JavaScript errors" %}
+
-
{% trans "Examples & References" %}
+
+
{% trans "Examples & References" %}
+
{% trans "Reference Plugins" %}
examplePlugin: {% trans "Basic plugin structure" %}
@@ -663,28 +913,40 @@ zip -r myPlugin-v1.0.0.zip . \
{% trans "Ready to Start?" %} {% trans "Begin with a simple plugin and gradually add more features as you become familiar with the system. Check the examplePlugin and testPlugin directories for complete working examples." %}
+
{% trans "Author" %}: master3395 |
- {% trans "Version" %}: 2.0.0 |
- {% trans "Last Updated" %}: 2026-01-04
+ {% trans "Version" %}: 2.1.0 |
+ {% trans "Last Updated" %}: 2026-02-02
{% endblock %}
\ No newline at end of file
diff --git a/pluginHolder/views.py b/pluginHolder/views.py
index 7ff70153a..1dec3c48e 100644
--- a/pluginHolder/views.py
+++ b/pluginHolder/views.py
@@ -53,6 +53,33 @@ def _is_plugin_enabled(plugin_name):
return True # Default to enabled if file read fails
return True # Default to enabled if state file doesn't exist
+
+def _get_freshness_badge(modify_date):
+ """
+ Return freshness badge (NEW/Stable/STALE) based on modify_date.
+ modify_date format: 'YYYY-MM-DD HH:MM:SS' or 'N/A'
+ - 0-90 days: NEW (yellow)
+ - 90-365 days: Stable (green)
+ - 730+ days: STALE (red)
+ - 365-730 days: no badge
+ """
+ if not modify_date or modify_date == 'N/A' or not isinstance(modify_date, str):
+ return None
+ try:
+ dt = datetime.strptime(modify_date[:19], '%Y-%m-%d %H:%M:%S')
+ days_ago = (datetime.now() - dt).days
+ if days_ago <= 90:
+ return {'badge': 'NEW', 'class': 'freshness-badge-new', 'title': 'This plugin was released/updated within the last 3 months'}
+ elif days_ago <= 365:
+ return {'badge': 'Stable', 'class': 'freshness-badge-stable', 'title': 'This plugin was updated within the last year'}
+ elif days_ago < 730:
+ return {'badge': 'Unstable', 'class': 'freshness-badge-unstable', 'title': 'This plugin has not been updated in over 1 year'}
+ else:
+ return {'badge': 'STALE', 'class': 'freshness-badge-stale', 'title': 'This plugin has not been updated in over 2 years'}
+ except (ValueError, TypeError):
+ pass
+ return None
+
def _set_plugin_state(plugin_name, enabled):
"""Set plugin enabled/disabled state"""
state_file = _get_plugin_state_file(plugin_name)
@@ -115,20 +142,27 @@ def installed(request):
desc_elem = root.find('description')
version_elem = root.find('version')
- # Type field is optional (testPlugin doesn't have it)
- if name_elem is None or desc_elem is None or version_elem is None:
- errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, description, or version)'})
+ # All fields required including type (category) - no default
+ if name_elem is None or type_elem is None or desc_elem is None or version_elem is None:
+ errorPlugins.append({'name': plugin, 'error': 'Missing required metadata fields (name, type/category, description, or version)'})
logging.writeToFile(f"Plugin {plugin}: Missing required metadata fields in meta.xml")
continue
- # Check if text is None (empty elements)
- if name_elem.text is None or desc_elem.text is None or version_elem.text is None:
- errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields'})
+ # Check if text is None or empty (all required)
+ type_text = type_elem.text.strip() if type_elem.text else ''
+ if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text:
+ errorPlugins.append({'name': plugin, 'error': 'Empty metadata fields (name, type/category, description, or version required)'})
logging.writeToFile(f"Plugin {plugin}: Empty metadata fields in meta.xml")
continue
+ # Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
+ if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'):
+ errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'})
+ logging.writeToFile(f"Plugin {plugin}: Invalid category '{type_text}'")
+ continue
+
data['name'] = name_elem.text
- data['type'] = type_elem.text if type_elem is not None and type_elem.text is not None else 'Plugin'
+ data['type'] = type_text
data['desc'] = desc_elem.text
data['version'] = version_elem.text
data['plugin_dir'] = plugin # Plugin directory name
@@ -158,6 +192,7 @@ def installed(request):
modify_date = 'N/A'
data['modify_date'] = modify_date
+ data['freshness_badge'] = _get_freshness_badge(modify_date)
# Extract settings URL or main URL for "Manage" button
settings_url_elem = root.find('settings_url')
@@ -252,20 +287,28 @@ def installed(request):
pluginMetaData = ElementTree.parse(metaXmlPath)
root = pluginMetaData.getroot()
- # Validate required fields
+ # Validate required fields (including type/category - no default)
name_elem = root.find('name')
type_elem = root.find('type')
desc_elem = root.find('description')
version_elem = root.find('version')
- if name_elem is None or desc_elem is None or version_elem is None:
+ if name_elem is None or type_elem is None or desc_elem is None or version_elem is None:
+ errorPlugins.append({'name': plugin, 'error': 'Missing required metadata (name, type/category, description, or version)'})
continue
- if name_elem.text is None or desc_elem.text is None or version_elem.text is None:
+ type_text = type_elem.text.strip() if type_elem.text else ''
+ if name_elem.text is None or desc_elem.text is None or version_elem.text is None or not type_text:
+ errorPlugins.append({'name': plugin, 'error': 'Empty metadata (type/category required)'})
+ continue
+
+ # Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
+ if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'):
+ errorPlugins.append({'name': plugin, 'error': f'Invalid category "{type_text}". Use: Utility, Security, Backup, or Performance.'})
continue
data['name'] = name_elem.text
- data['type'] = type_elem.text if type_elem is not None and type_elem.text is not None else 'Plugin'
+ data['type'] = type_text
data['desc'] = desc_elem.text
data['version'] = version_elem.text
data['plugin_dir'] = plugin
@@ -287,6 +330,7 @@ def installed(request):
modify_date = 'N/A'
data['modify_date'] = modify_date
+ data['freshness_badge'] = _get_freshness_badge(modify_date)
# Extract settings URL or main URL
settings_url_elem = root.find('settings_url')
@@ -943,6 +987,7 @@ def _enrich_store_plugins(plugins):
elif 'is_paid' not in plugin or plugin.get('is_paid') is None:
# Try to check from local meta.xml if available
meta_path = None
+ source_path = os.path.join(plugin_source_dir, plugin_dir)
if os.path.exists(installed_path):
meta_path = os.path.join(installed_path, 'meta.xml')
elif os.path.exists(source_path):
@@ -1050,10 +1095,21 @@ def _fetch_plugins_from_github():
patreon_url_elem = root.find('patreon_url')
patreon_url = patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
+ # Category (type) is required - valid: Utility, Security, Backup, Performance (Plugin removed)
+ type_elem = root.find('type')
+ if type_elem is None or not type_elem.text or not type_elem.text.strip():
+ logging.writeToFile(f"Plugin {plugin_name}: Missing required type/category in meta.xml, skipping")
+ continue
+ type_text = type_elem.text.strip().lower()
+ if type_text not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'):
+ logging.writeToFile(f"Plugin {plugin_name}: Invalid category '{type_elem.text}', skipping (use Utility, Security, Backup, or Performance)")
+ continue
+
+ freshness = _get_freshness_badge(modify_date)
plugin_data = {
'plugin_dir': plugin_name,
'name': root.find('name').text if root.find('name') is not None else plugin_name,
- 'type': root.find('type').text if root.find('type') is not None else 'Plugin',
+ 'type': type_elem.text.strip(),
'description': root.find('description').text if root.find('description') is not None else '',
'version': root.find('version').text if root.find('version') is not None else '1.0.0',
'url': root.find('url').text if root.find('url') is not None else f'/plugins/{plugin_name}/',
@@ -1062,6 +1118,7 @@ def _fetch_plugins_from_github():
'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
'modify_date': modify_date,
+ 'freshness_badge': freshness,
'is_paid': is_paid,
'patreon_tier': patreon_tier,
'patreon_url': patreon_url
@@ -1110,21 +1167,29 @@ def _fetch_plugins_from_github():
@require_http_methods(["GET"])
def fetch_plugin_store(request):
"""Fetch plugins from the plugin store with caching"""
- mailUtilities.checkHome()
-
- # Try to get from cache first
- cached_plugins = _get_cached_plugins()
- if cached_plugins is not None:
- # Enrich cached plugins with installed/enabled status
- enriched_plugins = _enrich_store_plugins(cached_plugins)
- return JsonResponse({
- 'success': True,
- 'plugins': enriched_plugins,
- 'cached': True
- })
-
- # Cache miss or expired - fetch from GitHub
try:
+ mailUtilities.checkHome()
+ except Exception as e:
+ logging.writeToFile(f"fetch_plugin_store: checkHome failed: {str(e)}")
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Authentication required. Please log in again.',
+ 'plugins': []
+ }, status=401)
+
+ try:
+ # Try to get from cache first
+ cached_plugins = _get_cached_plugins()
+ if cached_plugins is not None:
+ # Enrich cached plugins with installed/enabled status
+ enriched_plugins = _enrich_store_plugins(cached_plugins)
+ return JsonResponse({
+ 'success': True,
+ 'plugins': enriched_plugins,
+ 'cached': True
+ })
+
+ # Cache miss or expired - fetch from GitHub
plugins = _fetch_plugins_from_github()
# Enrich plugins with installed/enabled status
@@ -1139,7 +1204,7 @@ def fetch_plugin_store(request):
'plugins': enriched_plugins,
'cached': False
})
-
+
except Exception as e:
error_message = str(e)
diff --git a/rollback_phpmyadmin_redirect.sh b/rollback_phpmyadmin_redirect.sh
deleted file mode 100644
index 159ec00a3..000000000
--- a/rollback_phpmyadmin_redirect.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/bash
-
-# CyberPanel phpMyAdmin Access Control Rollback Script
-# This script reverts the phpMyAdmin access control changes
-
-echo "=== CyberPanel phpMyAdmin Access Control Rollback ==="
-
-# Check if running as root
-if [ "$EUID" -ne 0 ]; then
- echo "Please run this script as root"
- exit 1
-fi
-
-# Find the most recent backup
-LATEST_BACKUP=$(ls -t /usr/local/CyberCP/public/phpmyadmin/index.php.backup.* 2>/dev/null | head -n1)
-
-if [ -z "$LATEST_BACKUP" ]; then
- echo "No backup found. Cannot rollback changes."
- echo "You may need to reinstall phpMyAdmin or restore from your own backup."
- exit 1
-fi
-
-echo "Found backup: $LATEST_BACKUP"
-echo "Restoring original phpMyAdmin index.php..."
-
-# Restore the original index.php
-cp "$LATEST_BACKUP" /usr/local/CyberCP/public/phpmyadmin/index.php
-
-# Remove the .htaccess file if it exists
-if [ -f "/usr/local/CyberCP/public/phpmyadmin/.htaccess" ]; then
- echo "Removing .htaccess file..."
- rm /usr/local/CyberCP/public/phpmyadmin/.htaccess
-fi
-
-# Set proper permissions
-echo "Setting permissions..."
-chown lscpd:lscpd /usr/local/CyberCP/public/phpmyadmin/index.php
-chmod 644 /usr/local/CyberCP/public/phpmyadmin/index.php
-
-# Restart LiteSpeed to ensure changes take effect
-echo "Restarting LiteSpeed..."
-systemctl restart lscpd
-
-echo "=== Rollback Complete ==="
-echo ""
-echo "phpMyAdmin access control has been reverted!"
-echo "phpMyAdmin should now work as it did before the changes."
-echo ""
-echo "Backup file used: $LATEST_BACKUP"
diff --git a/simple_install.sh b/simple_install.sh
new file mode 100644
index 000000000..3d3cdb5b8
--- /dev/null
+++ b/simple_install.sh
@@ -0,0 +1,114 @@
+#!/bin/sh
+
+# Simplified CyberPanel Installation Script
+# Based on 2.4.4 approach with AlmaLinux 9 fixes
+
+OUTPUT=$(cat /etc/*release)
+
+# Detect OS and set appropriate variables
+if echo $OUTPUT | grep -q "AlmaLinux 9" ; then
+ echo -e "\nDetecting AlmaLinux 9...\n"
+ SERVER_OS="AlmaLinux9"
+ PKG_MGR="dnf"
+elif echo $OUTPUT | grep -q "AlmaLinux 8" ; then
+ echo -e "\nDetecting AlmaLinux 8...\n"
+ SERVER_OS="AlmaLinux8"
+ PKG_MGR="yum"
+elif echo $OUTPUT | grep -q "Ubuntu 22.04" ; then
+ echo -e "\nDetecting Ubuntu 22.04...\n"
+ SERVER_OS="Ubuntu2204"
+ PKG_MGR="apt"
+elif echo $OUTPUT | grep -q "Ubuntu 20.04" ; then
+ echo -e "\nDetecting Ubuntu 20.04...\n"
+ SERVER_OS="Ubuntu2004"
+ PKG_MGR="apt"
+elif echo $OUTPUT | grep -q "CentOS Linux 8" ; then
+ echo -e "\nDetecting CentOS 8...\n"
+ SERVER_OS="CentOS8"
+ PKG_MGR="yum"
+else
+ echo -e "\nUnsupported OS detected. This script supports:\n"
+ echo -e "AlmaLinux: 8, 9\n"
+ echo -e "Ubuntu: 20.04, 22.04\n"
+ echo -e "CentOS: 8\n"
+ exit 1
+fi
+
+echo "Installing basic dependencies..."
+
+# Install basic packages
+if [ "$PKG_MGR" = "dnf" ]; then
+ dnf update -y
+ dnf install -y epel-release
+ dnf install -y wget curl unzip zip rsync firewalld git python3 python3-pip
+ dnf install -y mariadb-server mariadb-client
+ dnf install -y ImageMagick gd libicu oniguruma aspell libc-client
+elif [ "$PKG_MGR" = "yum" ]; then
+ yum update -y
+ yum install -y epel-release
+ yum install -y wget curl unzip zip rsync firewalld git python3 python3-pip
+ yum install -y mariadb-server mariadb-client
+ yum install -y ImageMagick gd libicu oniguruma aspell libc-client
+elif [ "$PKG_MGR" = "apt" ]; then
+ apt update -y
+ apt install -y wget curl unzip zip rsync git python3 python3-pip
+ apt install -y mariadb-server mariadb-client
+ apt install -y imagemagick php-gd php-intl php-mbstring php-pspell
+fi
+
+# Start and enable MariaDB
+echo "Starting MariaDB..."
+systemctl enable mariadb
+systemctl start mariadb
+
+# Create MySQL password file
+echo "Setting up MySQL..."
+mkdir -p /etc/cyberpanel
+echo "cyberpanel123" > /etc/cyberpanel/mysqlPassword
+chmod 600 /etc/cyberpanel/mysqlPassword
+
+# Secure MySQL installation
+mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'cyberpanel123';" 2>/dev/null || true
+mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='';" 2>/dev/null || true
+mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" 2>/dev/null || true
+mysql -u root -pcyberpanel123 -e "DROP DATABASE IF EXISTS test;" 2>/dev/null || true
+mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" 2>/dev/null || true
+mysql -u root -pcyberpanel123 -e "FLUSH PRIVILEGES;" 2>/dev/null || true
+
+# Configure firewall
+echo "Configuring firewall..."
+if [ "$PKG_MGR" = "dnf" ] || [ "$PKG_MGR" = "yum" ]; then
+ systemctl enable firewalld
+ systemctl start firewalld
+ firewall-cmd --permanent --add-port=8090/tcp
+ firewall-cmd --permanent --add-port=7080/tcp
+ firewall-cmd --permanent --add-port=80/tcp
+ firewall-cmd --permanent --add-port=443/tcp
+ firewall-cmd --permanent --add-port=21/tcp
+ firewall-cmd --permanent --add-port=25/tcp
+ firewall-cmd --permanent --add-port=587/tcp
+ firewall-cmd --permanent --add-port=465/tcp
+ firewall-cmd --permanent --add-port=110/tcp
+ firewall-cmd --permanent --add-port=143/tcp
+ firewall-cmd --permanent --add-port=993/tcp
+ firewall-cmd --permanent --add-port=995/tcp
+ firewall-cmd --permanent --add-port=53/tcp
+ firewall-cmd --permanent --add-port=53/udp
+ firewall-cmd --reload
+fi
+
+# Download and install CyberPanel
+echo "Downloading CyberPanel..."
+rm -f cyberpanel.sh
+curl --silent -o cyberpanel.sh "https://cyberpanel.sh/?dl&$SERVER_OS" 2>/dev/null
+
+if [ -f "cyberpanel.sh" ]; then
+ echo "Installing CyberPanel..."
+ chmod +x cyberpanel.sh
+ ./cyberpanel.sh
+else
+ echo "Failed to download CyberPanel installer!"
+ exit 1
+fi
+
+echo "Installation completed!"
diff --git a/to-do/MARIADB_INSTALLATION_FIXES.md b/to-do/MARIADB_INSTALLATION_FIXES.md
new file mode 100644
index 000000000..f133868d3
--- /dev/null
+++ b/to-do/MARIADB_INSTALLATION_FIXES.md
@@ -0,0 +1,88 @@
+# MariaDB Installation Fixes
+
+## Issues Fixed
+
+### 1. MariaDB-server-compat Package Conflict
+**Problem**: `MariaDB-server-compat-12.1.2-1.el9.noarch` was conflicting with MariaDB 10.11 installation, causing transaction test errors.
+
+**Solution**:
+- Enhanced compat package removal with multiple aggressive removal attempts
+- Added `--allowerasing` flag to dnf remove commands
+- Added dnf exclude configuration to prevent compat package reinstallation
+- Verification step to ensure all compat packages are removed before installation
+
+**Files Modified**:
+- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function
+- `cyberpanel-repo/install/install.py` - `installMySQL()` function
+
+### 2. MySQL Command Not Found Error
+**Problem**: After MariaDB installation failed, the `changeMYSQLRootPassword()` function tried to use the `mysql` command which didn't exist, causing `FileNotFoundError`.
+
+**Solution**:
+- Added verification that MariaDB binaries exist before attempting password change
+- Added check for mysql/mariadb command availability
+- Added MariaDB service status verification before password change
+- Added wait time for MariaDB to be ready after service start
+
+**Files Modified**:
+- `cyberpanel-repo/install/install.py` - `changeMYSQLRootPassword()` function
+- `cyberpanel-repo/install/install.py` - `installMySQL()` function
+
+### 3. MariaDB Installation Verification
+**Problem**: Installation was proceeding even when MariaDB wasn't actually installed successfully.
+
+**Solution**:
+- Added binary existence check after installation
+- Added service status verification
+- Added proper error handling and return values
+- Installation now fails gracefully if MariaDB wasn't installed
+
+**Files Modified**:
+- `cyberpanel-repo/plogical/upgrade.py` - `fix_almalinux9_mariadb()` function
+- `cyberpanel-repo/install/install.py` - `installMySQL()` function
+
+## Changes Made
+
+### upgrade.py
+1. **Enhanced compat package removal**:
+ - Multiple removal attempts (dnf remove, rpm -e, individual package removal)
+ - Added `--allowerasing` flag
+ - Added dnf exclude configuration
+ - Verification step
+
+2. **Improved MariaDB installation**:
+ - Added `--exclude='MariaDB-server-compat*'` to dnf install command
+ - Added fallback with `--allowerasing` if conflicts occur
+ - Added binary existence verification after installation
+ - Proper error handling and return values
+
+### install.py
+1. **Enhanced compat package removal** (same as upgrade.py)
+
+2. **Improved installation verification**:
+ - Check for MariaDB binaries after installation
+ - Verify service is running before password change
+ - Added wait time for service to be ready
+ - Proper error handling
+
+3. **Improved password change function**:
+ - Verify mysql/mariadb command exists before attempting password change
+ - Better error messages
+ - Graceful failure handling
+
+## Testing Recommendations
+
+1. Test on clean AlmaLinux 9 system
+2. Test with existing MariaDB-server-compat package installed
+3. Test with MariaDB 10.x already installed
+4. Test with MariaDB 12.x already installed
+5. Verify MariaDB service starts correctly
+6. Verify mysql/mariadb commands are available
+7. Verify password change succeeds
+
+## Notes
+
+- The fixes maintain backward compatibility
+- All changes include proper error handling
+- Installation now fails gracefully with clear error messages
+- Compat package removal is more aggressive to handle edge cases
diff --git a/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md b/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md
new file mode 100644
index 000000000..39371c4fe
--- /dev/null
+++ b/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md
@@ -0,0 +1,132 @@
+# What Was in the Old cyberpanel-fix Repo – Pre-Removal Checklist
+
+Before removing `/home/cyberpanel-fix-backup-20260202`, verify the merged repo has everything you need.
+
+---
+
+## 1. Files ONLY in cyberpanel-repo (not in old fix) ✅
+
+These are in the merged repo and were not in the old fix:
+
+| File | Purpose |
+|------|---------|
+| `commit_and_push.sh`, `commit_changes.py`, `push_fix.py`, `push_fix.sh` | Dev/utility scripts |
+| `fix_todo_git.py`, `remove_todo.py`, `remove_todo_from_git.sh` | Git helpers |
+| `olves issue -1654: Hostname SSL setup...` | Patch file (typo in filename) |
+| `pluginHolder/patreon_verifier.py.bak`, `plugin_access.py.bak` | Backups |
+| `pluginHolder/templates/pluginHolder/plugins.html.backup` | Template backup |
+| `static/userManagment/modifyUser.html` | UI change |
+| `to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md` | Notes |
+| `to-do/REPO-MERGE-2026-02-02.md` | Merge notes |
+
+**Action:** None. These are already in the merged repo.
+
+---
+
+## 2. Files COPIED from old fix into repo ✅
+
+These were only in the old fix and were copied into repo during the merge:
+
+| File | Purpose |
+|------|---------|
+| `cyberpanel_clean.sh` | Clean install script |
+| `cyberpanel_complete.sh` | Complete install script |
+| `cyberpanel_simple.sh` | Simple install script |
+| `cyberpanel_standalone.sh` | Standalone install script |
+| `fix_installation_issues.sh` | Installation fixes |
+| `install_phpmyadmin.sh` | phpMyAdmin installer |
+| `simple_install.sh` | Simple installer |
+| `INSTALLER_SUMMARY.md` | Installer docs |
+| `UNIVERSAL_OS_COMPATIBILITY.md` | OS compatibility docs |
+| `to-do/MARIADB_INSTALLATION_FIXES.md` | MariaDB fixes |
+
+**Action:** Confirm these exist in `/home/cyberpanel-repo/`.
+
+---
+
+## 3. Files that DIFFER – repo is the intended version
+
+The merged repo keeps the **cyberpanel-repo** versions. Old fix had older or different logic.
+
+### CyberCP/settings.py
+- **Repo:** `emailMarketing` is commented out (install via Plugin Store)
+- **Old fix:** `emailMarketing` was in `INSTALLED_APPS`
+
+**Check:** Plugin Store for emailMarketing works; no need for it in core install.
+
+### CyberCP/urls.py
+- **Repo:** `path('emailMarketing/', ...)` is commented out
+- **Old fix:** `path('emailMarketing/', ...)` was active
+
+**Check:** Same as above; emailMarketing via Plugin Store.
+
+### plogical/mailUtilities.py
+- **Repo:** DNS fallback logic – falls back to **local DNS** when external API fails
+- **Old fix:** Returns empty `[]` when external API fails; no local fallback
+
+**Check:** Hostname SSL / rDNS works when cyberpanel.net API is down or unreachable.
+
+### emailMarketing/meta.xml
+- **Repo:** version `1.0.1`, category `Email`
+- **Old fix:** version `1.0.0`
+
+### examplePlugin/meta.xml
+- **Repo:** version `1.0.1`, category `Utility`
+- **Old fix:** version `1.0.0`
+
+**Check:** Plugin Store shows correct versions and categories.
+
+---
+
+## 4. PluginHolder / Plugin Store (in repo)
+
+The merged repo has:
+
+- Collapsible help sections
+- Freshness badges (NEW/Stable/Unstable/STALE)
+- Activate All / Deactivate All
+- Updated categories and premium docs
+- Version 2.1.0 in the help footer
+
+**Check:** `/plugins/help/` and `/plugins/installed` behave as expected.
+
+---
+
+## 5. Quick verification commands
+
+```bash
+# Copied files exist
+ls -la /home/cyberpanel-repo/cyberpanel_clean.sh \
+ /home/cyberpanel-repo/fix_installation_issues.sh \
+ /home/cyberpanel-repo/install_phpmyadmin.sh
+
+# Symlink works
+ls -la /home/cyberpanel-fix
+# Should show: cyberpanel-fix -> cyberpanel-repo
+
+# Live deployment
+ls -la /usr/local/CyberCP/pluginHolder/templates/pluginHolder/help.html
+# Should have collapsible sections and version 2.1.0
+```
+
+---
+
+## 6. Safe to remove when
+
+- [ ] Plugin Store loads and filters work
+- [ ] Plugin Development Guide (help) shows collapsible sections and 2.1.0
+- [ ] Hostname SSL / rDNS works (or you accept no local DNS fallback)
+- [ ] emailMarketing is installed via Plugin Store, not core (if used)
+- [ ] Install scripts (`cyberpanel_clean.sh`, etc.) are present and used as needed
+
+---
+
+## Remove backup
+
+```bash
+rm -rf /home/cyberpanel-fix-backup-20260202
+```
+
+---
+
+**Created:** 2026-02-02
diff --git a/to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md b/to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md
new file mode 100644
index 000000000..302845ded
--- /dev/null
+++ b/to-do/PLUGIN-DEFAULT-REMOVAL-2026-02-01.md
@@ -0,0 +1,13 @@
+# Plugin Default Removal - 2026-02-01
+
+## Summary
+CyberPanel repository no longer requires any plugins by default. Plugins are installed by users from the [Plugin Store](https://github.com/master3395/cyberpanel-plugins) via the CyberPanel Plugin Manager.
+
+## Changes
+- **settings.py**: Removed `emailMarketing` from `INSTALLED_APPS`
+- **urls.py**: Commented out `emailMarketing` route (plugin installer adds it when plugin is installed)
+
+## Plugin Installation
+Users install plugins from: https://github.com/master3395/cyberpanel-plugins
+
+The plugin installer adds apps to `INSTALLED_APPS` and URL routes when plugins are installed via the Plugin Store UI.
diff --git a/to-do/REPO-MERGE-2026-02-02.md b/to-do/REPO-MERGE-2026-02-02.md
new file mode 100644
index 000000000..79b96232e
--- /dev/null
+++ b/to-do/REPO-MERGE-2026-02-02.md
@@ -0,0 +1,38 @@
+# CyberPanel Repo Merge – 2026-02-02
+
+## Summary
+
+`cyberpanel-repo` and `cyberpanel-fix` have been merged into a single working directory.
+
+## What Was Done
+
+1. **Unique files copied from cyberpanel-fix into cyberpanel-repo:**
+ - `cyberpanel_clean.sh`
+ - `cyberpanel_complete.sh`
+ - `cyberpanel_simple.sh`
+ - `cyberpanel_standalone.sh`
+ - `fix_installation_issues.sh`
+ - `install_phpmyadmin.sh`
+ - `simple_install.sh`
+ - `INSTALLER_SUMMARY.md`
+ - `UNIVERSAL_OS_COMPATIBILITY.md`
+ - `to-do/MARIADB_INSTALLATION_FIXES.md`
+
+2. **cyberpanel-fix backup:** Renamed to `cyberpanel-fix-backup-20260202`
+
+3. **Symlink created:** `cyberpanel-fix` → `cyberpanel-repo`
+ - Paths like `/home/cyberpanel-fix/` now resolve to `/home/cyberpanel-repo/`
+
+## Single Source of Truth
+
+Use **`/home/cyberpanel-repo`** (or `/home/cyberpanel-fix` via symlink) for all CyberPanel development and deployment.
+
+## Backup Location
+
+The previous cyberpanel-fix tree is preserved at:
+`/home/cyberpanel-fix-backup-20260202`
+
+You can remove it after confirming everything works:
+```bash
+rm -rf /home/cyberpanel-fix-backup-20260202
+```