From d8ee83e30d4710dc0229e8aa5555c71810d958d9 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 2 Feb 2026 20:39:56 +0100 Subject: [PATCH] =?UTF-8?q?Plugin=20Store=20&=20Installed=20Plugins:=20sea?= =?UTF-8?q?rch=20bar,=20A-=C3=85=20sort,=20sort=20toggle,=20Store=20A-?= =?UTF-8?q?=C3=85=20label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Installed plugins: search box in header (same row as Activate/Deactivate All) - Grid/Table: default sort A-Å by name; sort bar with Name (toggle A-Å/Å-A), Type, Date (toggle newest/oldest) - Apply sort on load so list shows A-Å when Name A-Å is selected - Store view: letter filter label 'A-Å Filter' (not A-Z); add Æ, Ø, Å to letter buttons - views.py: sort pluginList by name (case-insensitive) before template - Add deploy-installed-plugins-search.sh for template deployment --- .../deploy-installed-plugins-search.sh | 29 ++ .../templates/pluginHolder/plugins.html | 265 +++++++++++++++++- pluginHolder/views.py | 100 ++++++- 3 files changed, 379 insertions(+), 15 deletions(-) create mode 100644 pluginHolder/deploy-installed-plugins-search.sh 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 %} +
+ +
{% for plugin in plugins %} -
+
{% if plugin.type|lower == "security" %} @@ -1275,6 +1355,10 @@
{% endfor %} +
@@ -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 @@
- +
@@ -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}")