diff --git a/pluginHolder/deploy-installed-plugins-search.sh b/pluginHolder/deploy-installed-plugins-search.sh
new file mode 100644
index 000000000..6bdde6e3b
--- /dev/null
+++ b/pluginHolder/deploy-installed-plugins-search.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# Deploy updated plugins.html (installed-plugins search bar) to CyberPanel
+# Run on the server where CyberPanel is installed (e.g. 207.180.193.210)
+# Usage: sudo bash deploy-installed-plugins-search.sh
+# Or from repo root: sudo bash pluginHolder/deploy-installed-plugins-search.sh
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+SRC="$SCRIPT_DIR/templates/pluginHolder/plugins.html"
+DEST="/usr/local/CyberCP/pluginHolder/templates/pluginHolder/plugins.html"
+
+if [ ! -f "$SRC" ]; then
+ echo "Error: Source not found: $SRC"
+ exit 1
+fi
+
+if [ ! -f "$DEST" ]; then
+ echo "Error: CyberPanel template not found: $DEST"
+ exit 1
+fi
+
+echo "Backing up current template..."
+cp "$DEST" "${DEST}.bak.$(date +%Y%m%d)"
+echo "Copying updated plugins.html..."
+cp "$SRC" "$DEST"
+echo "Restarting lscpd..."
+systemctl restart lscpd
+echo "Done. Hard-refresh the browser (Ctrl+Shift+R) and open /plugins/installed#grid"
diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html
index 78e158354..c8c35de44 100644
--- a/pluginHolder/templates/pluginHolder/plugins.html
+++ b/pluginHolder/templates/pluginHolder/plugins.html
@@ -426,6 +426,52 @@
display: flex;
gap: 10px;
}
+ /* Sort/Filter bar for Grid and Table view */
+ .installed-sort-filter-bar {
+ flex-basis: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 12px;
+ margin-top: 12px;
+ padding: 12px 0;
+ border-top: 1px solid var(--border-primary, #e2e8f0);
+ }
+ .installed-sort-filter-bar .sort-label {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-secondary, #64748b);
+ margin-right: 4px;
+ }
+ .installed-sort-filter-bar .sort-btns {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+ .installed-sort-filter-bar .sort-btn {
+ padding: 6px 14px;
+ border: 1px solid var(--border-primary, #e8e9ff);
+ background: var(--bg-primary, white);
+ border-radius: 8px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ color: var(--text-secondary, #64748b);
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ }
+ .installed-sort-filter-bar .sort-btn:hover {
+ background: var(--bg-hover, #f0f1ff);
+ border-color: #5856d6;
+ color: #5856d6;
+ }
+ .installed-sort-filter-bar .sort-btn.active {
+ background: #5856d6;
+ color: white;
+ border-color: #5856d6;
+ }
.bulk-actions-header {
display: flex !important;
visibility: visible !important;
@@ -661,6 +707,16 @@
max-width: 480px;
}
+ /* Search bar in page header (same row as Activate/Deactivate All) */
+ .header-search-bar {
+ margin-bottom: 0;
+ min-width: 220px;
+ max-width: 320px;
+ margin-left: 8px;
+ padding-left: 16px;
+ border-left: 2px solid #e2e8f0;
+ }
+
.store-search-icon {
position: absolute;
left: 16px;
@@ -1123,6 +1179,13 @@
{% if plugins %}
+
+
+
+
{% trans "Sort by:" %}
+
+
+
+
+
+
{% for plugin in plugins %}
-
+
{% endfor %}
+
+
+
{% trans "No plugins match your search." %}
+
@@ -1295,7 +1379,7 @@
{% for plugin in plugins %}
-
+
|
{{ plugin.name }}
{% if plugin.freshness_badge %}
@@ -1375,6 +1459,12 @@
|
{% endfor %}
+
+ |
+
+ {% trans "No plugins match your search." %}
+ |
+
@@ -1503,17 +1593,20 @@
-
+
{% for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
{% endfor %}
+
+
+
@@ -1555,6 +1648,7 @@ let currentFilter = 'all';
let currentCategory = 'all';
let currentSearchQuery = '';
let isSettingHash = false; // Flag to prevent infinite loops
+let currentInstalledSort = 'name-asc'; // name-asc, name-desc, type, date-desc, date-asc
// Get CSRF cookie helper function
function getCookie(name) {
@@ -1597,17 +1691,31 @@ function toggleView(view, updateHash = true) {
setTimeout(() => { isSettingHash = false; }, 100);
}
+ const installedSearchWrapper = document.getElementById('installedPluginsSearchWrapper');
+ const installedSortFilterBar = document.getElementById('installedSortFilterBar');
if (view === 'grid') {
gridView.style.display = 'grid';
tableView.style.display = 'none';
storeView.style.display = 'none';
viewBtns[0].classList.add('active');
+ if (installedSearchWrapper) installedSearchWrapper.style.display = 'block';
+ if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex';
+ if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons();
+ if (typeof doApplyInstalledSort === 'function') doApplyInstalledSort();
+ filterInstalledPlugins();
} else if (view === 'table') {
gridView.style.display = 'none';
tableView.style.display = 'block';
storeView.style.display = 'none';
viewBtns[1].classList.add('active');
+ if (installedSearchWrapper) installedSearchWrapper.style.display = 'block';
+ if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex';
+ if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons();
+ if (typeof doApplyInstalledSort === 'function') doApplyInstalledSort();
+ filterInstalledPlugins();
} else if (view === 'store') {
+ if (installedSearchWrapper) installedSearchWrapper.style.display = 'none';
+ if (installedSortFilterBar) installedSortFilterBar.style.display = 'none';
gridView.style.display = 'none';
tableView.style.display = 'none';
storeView.style.display = 'block';
@@ -1911,6 +2019,137 @@ function clearPluginSearch() {
}
}
+function filterInstalledPlugins() {
+ const query = (document.getElementById('installedPluginSearchInput') && document.getElementById('installedPluginSearchInput').value) || '';
+ const terms = query.trim().toLowerCase().split(/\s+/).filter(function(t) { return t.length > 0; });
+ const gridView = document.getElementById('gridView');
+ const tableView = document.getElementById('tableView');
+ const noResultsGrid = document.getElementById('installedPluginsNoResultsGrid');
+ const noResultsTable = document.getElementById('installedPluginsNoResultsTable');
+ var visibleCount = 0;
+ if (gridView) {
+ var cards = gridView.querySelectorAll('.plugin-card');
+ cards.forEach(function(card) {
+ var name = (card.getAttribute('data-plugin-name') || '').toLowerCase();
+ var desc = (card.getAttribute('data-plugin-desc') || '').toLowerCase();
+ var type = (card.getAttribute('data-plugin-type') || '').toLowerCase();
+ var combined = name + ' ' + desc + ' ' + type;
+ var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
+ card.style.display = show ? '' : 'none';
+ if (show) visibleCount++;
+ });
+ }
+ if (tableView) {
+ var tbody = tableView.querySelector('.plugins-table-wrapper tbody');
+ if (tbody) {
+ var rows = tbody.querySelectorAll('tr');
+ rows.forEach(function(row) {
+ if (row.id === 'installedPluginsNoResultsTable') return;
+ var name = (row.getAttribute('data-plugin-name') || '').toLowerCase();
+ var desc = (row.getAttribute('data-plugin-desc') || '').toLowerCase();
+ var type = (row.getAttribute('data-plugin-type') || '').toLowerCase();
+ var combined = name + ' ' + desc + ' ' + type;
+ var show = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
+ row.style.display = show ? '' : 'none';
+ if (show) visibleCount++;
+ });
+ }
+ }
+ if (noResultsGrid) {
+ noResultsGrid.style.display = (terms.length > 0 && visibleCount === 0) ? 'block' : 'none';
+ }
+ if (noResultsTable) {
+ noResultsTable.style.display = (terms.length > 0 && visibleCount === 0) ? 'table-row' : 'none';
+ }
+}
+
+function clearInstalledPluginSearch() {
+ var input = document.getElementById('installedPluginSearchInput');
+ var clearBtn = document.getElementById('installedPluginSearchClear');
+ if (input) {
+ input.value = '';
+ if (clearBtn) clearBtn.style.display = 'none';
+ filterInstalledPlugins();
+ input.focus();
+ }
+}
+
+function toggleInstalledSort(field) {
+ if (field === 'name') {
+ currentInstalledSort = currentInstalledSort === 'name-asc' ? 'name-desc' : 'name-asc';
+ } else if (field === 'date') {
+ currentInstalledSort = currentInstalledSort === 'date-desc' ? 'date-asc' : 'date-desc';
+ } else {
+ currentInstalledSort = 'type';
+ }
+ updateInstalledSortButtons();
+ doApplyInstalledSort();
+}
+
+function updateInstalledSortButtons() {
+ var bar = document.getElementById('installedSortFilterBar');
+ var transNameAsc = bar && bar.getAttribute('data-trans-name-asc') || 'Name A-Å';
+ var transNameDesc = bar && bar.getAttribute('data-trans-name-desc') || 'Name Å-A';
+ var transDateNewest = bar && bar.getAttribute('data-trans-date-newest') || 'Date (newest)';
+ var transDateOldest = bar && bar.getAttribute('data-trans-date-oldest') || 'Date (oldest)';
+ var nameBtn = document.getElementById('installedSortBtnName');
+ var typeBtn = document.getElementById('installedSortBtnType');
+ var dateBtn = document.getElementById('installedSortBtnDate');
+ if (nameBtn) {
+ nameBtn.classList.toggle('active', currentInstalledSort === 'name-asc' || currentInstalledSort === 'name-desc');
+ var nameLabel = nameBtn.querySelector('.sort-btn-label');
+ var nameIcon = nameBtn.querySelector('i');
+ if (nameLabel) nameLabel.textContent = currentInstalledSort === 'name-desc' ? transNameDesc : transNameAsc;
+ if (nameIcon) nameIcon.className = currentInstalledSort === 'name-desc' ? 'fas fa-sort-alpha-down-alt' : 'fas fa-sort-alpha-down';
+ }
+ if (typeBtn) typeBtn.classList.toggle('active', currentInstalledSort === 'type');
+ if (dateBtn) {
+ dateBtn.classList.toggle('active', currentInstalledSort === 'date-asc' || currentInstalledSort === 'date-desc');
+ var dateLabel = dateBtn.querySelector('.sort-btn-label');
+ var dateIcon = dateBtn.querySelector('i');
+ if (dateLabel) dateLabel.textContent = currentInstalledSort === 'date-asc' ? transDateOldest : transDateNewest;
+ if (dateIcon) dateIcon.className = currentInstalledSort === 'date-asc' ? 'fas fa-calendar' : 'fas fa-calendar-alt';
+ }
+}
+
+function doApplyInstalledSort() {
+ var sortKey = currentInstalledSort;
+ var gridView = document.getElementById('gridView');
+ var tableView = document.getElementById('tableView');
+ var noResultsGrid = document.getElementById('installedPluginsNoResultsGrid');
+ var noResultsTable = document.getElementById('installedPluginsNoResultsTable');
+ function compareCards(a, b) {
+ var nameA = (a.getAttribute('data-plugin-name') || '').toLowerCase();
+ var nameB = (b.getAttribute('data-plugin-name') || '').toLowerCase();
+ var typeA = (a.getAttribute('data-plugin-type') || '').toLowerCase();
+ var typeB = (b.getAttribute('data-plugin-type') || '').toLowerCase();
+ var dateA = a.getAttribute('data-modify-date') || '0000-00-00 00:00:00';
+ var dateB = b.getAttribute('data-modify-date') || '0000-00-00 00:00:00';
+ if (sortKey === 'name-asc') return nameA.localeCompare(nameB);
+ if (sortKey === 'name-desc') return nameB.localeCompare(nameA);
+ if (sortKey === 'type') { var c = typeA.localeCompare(typeB); return c !== 0 ? c : nameA.localeCompare(nameB); }
+ if (sortKey === 'date-desc') return dateB.localeCompare(dateA);
+ if (sortKey === 'date-asc') return dateA.localeCompare(dateB);
+ return 0;
+ }
+ if (gridView) {
+ var cards = Array.prototype.slice.call(gridView.querySelectorAll('.plugin-card'));
+ cards.sort(compareCards);
+ cards.forEach(function(card) { gridView.appendChild(card); });
+ if (noResultsGrid) gridView.appendChild(noResultsGrid);
+ }
+ if (tableView) {
+ var tbody = tableView.querySelector('.plugins-table-wrapper tbody');
+ if (tbody) {
+ var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
+ var dataRows = rows.filter(function(r) { return r.id !== 'installedPluginsNoResultsTable'; });
+ dataRows.sort(compareCards);
+ dataRows.forEach(function(row) { tbody.appendChild(row); });
+ if (noResultsTable) tbody.appendChild(noResultsTable);
+ }
+ }
+}
+
function toggleAlphabetFilter() {
const filter = document.getElementById('alphabetFilter');
const toggleBtn = document.getElementById('alphabetToggleBtn');
@@ -2680,6 +2919,24 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
+ // Installed plugins search (Grid View and Table View)
+ const installedSearchInput = document.getElementById('installedPluginSearchInput');
+ const installedSearchClearBtn = document.getElementById('installedPluginSearchClear');
+ if (installedSearchInput) {
+ installedSearchInput.addEventListener('input', function() {
+ if (installedSearchClearBtn) {
+ installedSearchClearBtn.style.display = this.value.trim() ? 'block' : 'none';
+ }
+ filterInstalledPlugins();
+ });
+ installedSearchInput.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') {
+ clearInstalledPluginSearch();
+ e.preventDefault();
+ }
+ });
+ }
+
// Check URL hash for view preference
const hash = window.location.hash.substring(1); // Remove #
const validViews = ['grid', 'table', 'store'];
diff --git a/pluginHolder/views.py b/pluginHolder/views.py
index 1dec3c48e..d617ef2b5 100644
--- a/pluginHolder/views.py
+++ b/pluginHolder/views.py
@@ -35,6 +35,18 @@ GITHUB_COMMITS_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins
# Plugin backup configuration
PLUGIN_BACKUP_DIR = '/home/cyberpanel/plugin_backups'
+# Plugin source paths (checked in order; first match wins for install)
+PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins']
+
+def _get_plugin_source_path(plugin_name):
+ """Return the full path to a plugin's source directory, or None if not found."""
+ for base in PLUGIN_SOURCE_PATHS:
+ path = os.path.join(base, plugin_name)
+ meta_path = os.path.join(path, 'meta.xml')
+ if os.path.isdir(path) and os.path.exists(meta_path):
+ return path
+ return None
+
def _get_plugin_state_file(plugin_name):
"""Get the path to the plugin state file"""
if not os.path.exists(PLUGIN_STATE_DIR):
@@ -100,15 +112,23 @@ def help_page(request):
def installed(request):
mailUtilities.checkHome()
- pluginPath = '/home/cyberpanel/plugins'
installedPath = '/usr/local/CyberCP'
pluginList = []
errorPlugins = []
processed_plugins = set() # Track which plugins we've already processed
- # First, process plugins from source directory
- if os.path.exists(pluginPath):
+ # First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins)
+ for pluginPath in PLUGIN_SOURCE_PATHS:
+ if not os.path.exists(pluginPath):
+ continue
+ try:
+ dirs_in_path = [p for p in os.listdir(pluginPath) if os.path.isdir(os.path.join(pluginPath, p))]
+ logging.writeToFile(f"Plugin source path {pluginPath}: directories {sorted(dirs_in_path)}")
+ except Exception as e:
+ logging.writeToFile(f"Plugin source path {pluginPath}: listdir error {e}")
for plugin in os.listdir(pluginPath):
+ if plugin in processed_plugins:
+ continue
# Skip files (like .zip files) - only process directories
pluginDir = os.path.join(pluginPath, plugin)
if not os.path.isdir(pluginDir):
@@ -130,7 +150,8 @@ def installed(request):
# Add error handling to prevent 500 errors
try:
if metaXmlPath is None:
- # No meta.xml found in either location - skip silently
+ # No meta.xml found in either location - skip (log for diagnostics)
+ logging.writeToFile(f"Plugin {plugin}: skipped (no meta.xml in source or installed)")
continue
pluginMetaData = ElementTree.parse(metaXmlPath)
@@ -383,6 +404,60 @@ def installed(request):
logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}")
continue
+ # Ensure redisManager and memcacheManager load when present (fallback if missed by listdir)
+ for plugin_name in ('redisManager', 'memcacheManager'):
+ if plugin_name in processed_plugins:
+ continue
+ source_path = _get_plugin_source_path(plugin_name)
+ installed_meta = os.path.join(installedPath, plugin_name, 'meta.xml')
+ meta_xml_path = installed_meta if os.path.exists(installed_meta) else (os.path.join(source_path, 'meta.xml') if source_path else None)
+ if not meta_xml_path or not os.path.exists(meta_xml_path):
+ continue
+ try:
+ root = ElementTree.parse(meta_xml_path).getroot()
+ 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 type_elem is None or desc_elem is None or version_elem is None:
+ continue
+ type_text = (type_elem.text or '').strip()
+ if not type_text or name_elem.text is None or desc_elem.text is None or version_elem.text is None:
+ continue
+ if type_text.lower() not in ('utility', 'security', 'backup', 'performance', 'monitoring', 'integration', 'email', 'development', 'analytics'):
+ continue
+ complete_path = os.path.join(installedPath, plugin_name, 'meta.xml')
+ data = {
+ 'name': name_elem.text,
+ 'type': type_text,
+ 'desc': desc_elem.text,
+ 'version': version_elem.text,
+ 'plugin_dir': plugin_name,
+ 'installed': os.path.exists(complete_path),
+ 'enabled': _is_plugin_enabled(plugin_name) if os.path.exists(complete_path) else False,
+ 'is_paid': False,
+ 'patreon_tier': None,
+ 'patreon_url': None,
+ 'manage_url': f'/plugins/{plugin_name}/',
+ 'author': root.find('author').text if root.find('author') is not None and root.find('author').text else 'Unknown',
+ }
+ try:
+ modify_time = os.path.getmtime(meta_xml_path)
+ data['modify_date'] = datetime.fromtimestamp(modify_time).strftime('%Y-%m-%d %H:%M:%S')
+ except Exception:
+ data['modify_date'] = 'N/A'
+ data['freshness_badge'] = _get_freshness_badge(data['modify_date'])
+ paid_elem = root.find('paid')
+ if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
+ data['is_paid'] = True
+ data['patreon_tier'] = 'CyberPanel Paid Plugin'
+ data['patreon_url'] = root.find('patreon_url').text if root.find('patreon_url') is not None else 'https://www.patreon.com/membership/27789984'
+ pluginList.append(data)
+ processed_plugins.add(plugin_name)
+ logging.writeToFile(f"Plugin {plugin_name}: added via fallback (source or installed)")
+ except Exception as e:
+ logging.writeToFile(f"Plugin {plugin_name} fallback load error: {str(e)}")
+
# Calculate installed and active counts
# Double-check by also counting plugins that actually exist in /usr/local/CyberCP/
installed_plugins_in_filesystem = set()
@@ -415,6 +490,9 @@ def installed(request):
# Get cache expiry timestamp for display (will be converted to local time in browser)
cache_expiry_timestamp, _ = _get_cache_expiry_time()
+ # Sort plugins A-Å by name (case-insensitive) for Grid and Table view
+ pluginList.sort(key=lambda p: (p.get('name') or '').lower())
+
proc = httpProc(request, 'pluginHolder/plugins.html',
{'plugins': pluginList, 'error_plugins': errorPlugins,
'installed_count': installed_count, 'active_count': active_count,
@@ -426,12 +504,12 @@ def installed(request):
def install_plugin(request, plugin_name):
"""Install a plugin"""
try:
- # Check if plugin source exists
- pluginSource = '/home/cyberpanel/plugins/' + plugin_name
- if not os.path.exists(pluginSource):
+ # Check if plugin source exists (in any configured source path)
+ pluginSource = _get_plugin_source_path(plugin_name)
+ if not pluginSource:
return JsonResponse({
'success': False,
- 'error': f'Plugin source not found: {plugin_name}'
+ 'error': f'Plugin source not found: {plugin_name} (checked: {", ".join(PLUGIN_SOURCE_PATHS)})'
}, status=404)
# Check if already installed
@@ -1546,9 +1624,9 @@ def install_from_store(request, plugin_name):
# Fallback to local source if GitHub download failed
if use_local_fallback:
- pluginSource = '/home/cyberpanel/plugins/' + plugin_name
- if not os.path.exists(pluginSource):
- raise Exception(f'Plugin {plugin_name} not found in GitHub repository and local source not found at {pluginSource}')
+ pluginSource = _get_plugin_source_path(plugin_name)
+ if not pluginSource:
+ raise Exception(f'Plugin {plugin_name} not found in GitHub repository and local source not found (checked: {", ".join(PLUGIN_SOURCE_PATHS)})')
logging.writeToFile(f"Using local source for {plugin_name} from {pluginSource}")