Plugins: fix installed/active counts, 404s, metadata sync, install fallback

- pluginHolder/views: use dir+meta.xml for installed count; exclude core apps;
  repair pass to restore meta.xml from source or GitHub; ensure_plugin_meta_xml
  falls back to GitHub when source missing; cap active <= installed
- pluginHolder/urls: include plugin routes for all on-disk plugins (not only
  INSTALLED_APPS) so /plugins/<name>/settings/ works after install
- pluginHolder/plugins.html: Install button tries local then store (GitHub)
- CyberCP/settings: sync INSTALLED_APPS with plugin dirs on disk (meta.xml+urls.py)
Author: master3395
This commit is contained in:
master3395
2026-02-15 23:03:01 +01:00
parent 87502cfcbc
commit 3a73682561
4 changed files with 223 additions and 89 deletions

View File

@@ -430,13 +430,20 @@
.installed-sort-filter-bar {
flex-basis: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
flex-direction: column;
align-items: flex-start;
gap: 10px;
margin-top: 12px;
padding: 12px 0;
border-top: 1px solid var(--border-primary, #e2e8f0);
}
.installed-sort-filter-bar .installed-filter-row,
.installed-sort-filter-bar .installed-sort-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.installed-sort-filter-bar .sort-label {
font-size: 13px;
font-weight: 600;
@@ -1224,10 +1231,25 @@
{% trans "Plugin Development Guide" %}
</a>
</div>
<!-- Sort bar for Grid and Table view (visible when grid or table is active) -->
<!-- Filter + Sort bar for Grid and Table view (visible when grid or table is active) -->
<div id="installedSortFilterBar" class="installed-sort-filter-bar" style="display: none;"
data-trans-name-asc="{% trans 'Name A-Å' %}" data-trans-name-desc="{% trans 'Name Å-A' %}"
data-trans-date-newest="{% trans 'Date (newest)' %}" data-trans-date-oldest="{% trans 'Date (oldest)' %}">
data-trans-date-newest="{% trans 'Date (newest)' %}" data-trans-date-oldest="{% trans 'Date (oldest)' }}">
<div class="installed-filter-row">
<span class="sort-label">{% trans "Show:" %}</span>
<div class="filter-btns sort-btns">
<button type="button" class="sort-btn filter-btn active" data-filter="all" id="installedFilterBtnAll" onclick="setInstalledFilter('all')" title="{% trans 'Show all plugins' %}">
<i class="fas fa-th-list"></i> <span class="filter-btn-label">{% trans "All" %}</span>
</button>
<button type="button" class="sort-btn filter-btn" data-filter="installed" id="installedFilterBtnInstalled" onclick="setInstalledFilter('installed')" title="{% trans 'Show only installed plugins' %}">
<i class="fas fa-check-circle"></i> <span class="filter-btn-label">{% trans "Installed only" %}</span>
</button>
<button type="button" class="sort-btn filter-btn" data-filter="active" id="installedFilterBtnActive" onclick="setInstalledFilter('active')" title="{% trans 'Show only active (enabled) plugins' %}">
<i class="fas fa-power-off"></i> <span class="filter-btn-label">{% trans "Active only" %}</span>
</button>
</div>
</div>
<div class="installed-sort-row">
<span class="sort-label">{% trans "Sort by:" %}</span>
<div class="sort-btns">
<button type="button" class="sort-btn active" data-sort-field="name" id="installedSortBtnName" onclick="toggleInstalledSort('name')" title="{% trans 'Click to toggle A-Å / Å-A' %}">
@@ -1240,13 +1262,14 @@
<i class="fas fa-calendar-alt"></i> <span class="sort-btn-label">{% trans "Date (newest)" %}</span>
</button>
</div>
</div>
</div>
</div>
<!-- Grid View -->
<div id="gridView" class="plugins-grid">
{% for plugin in plugins %}
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}">
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
<div class="plugin-header">
<div class="plugin-icon">
{% if plugin.type|lower == "security" %}
@@ -1388,7 +1411,7 @@
</thead>
<tbody>
{% for plugin in plugins %}
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}">
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
<td>
<strong>{{ plugin.name }}</strong>
{% if plugin.freshness_badge %}
@@ -1658,6 +1681,7 @@ 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
let currentInstalledFilter = 'all'; // all, installed, active
// Get CSRF cookie helper function
function getCookie(name) {
@@ -2035,14 +2059,37 @@ function clearPluginSearch() {
}
}
function setInstalledFilter(filter) {
currentInstalledFilter = filter;
var bar = document.getElementById('installedSortFilterBar');
if (bar) {
try {
bar.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.classList.toggle('active', (btn.getAttribute('data-filter') || '') === filter);
});
} catch (e) { console.warn('setInstalledFilter: filter buttons', e); }
}
try {
filterInstalledPlugins();
} catch (e) { console.warn('setInstalledFilter: filterInstalledPlugins', e); }
}
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 filter = currentInstalledFilter || 'all';
const gridView = document.getElementById('gridView');
const tableView = document.getElementById('tableView');
const noResultsGrid = document.getElementById('installedPluginsNoResultsGrid');
const noResultsTable = document.getElementById('installedPluginsNoResultsTable');
if (!gridView && !tableView) return;
var visibleCount = 0;
function matchesFilter(installed, enabled) {
if (filter === 'all') return true;
if (filter === 'installed') return installed === 'true';
if (filter === 'active') return installed === 'true' && enabled === 'true';
return true;
}
if (gridView) {
var cards = gridView.querySelectorAll('.plugin-card');
cards.forEach(function(card) {
@@ -2050,7 +2097,11 @@ function filterInstalledPlugins() {
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; });
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
var installed = card.getAttribute('data-installed') || 'false';
var enabled = card.getAttribute('data-enabled') || 'false';
var filterMatch = matchesFilter(installed, enabled);
var show = searchMatch && filterMatch;
card.style.display = show ? '' : 'none';
if (show) visibleCount++;
});
@@ -2065,17 +2116,22 @@ function filterInstalledPlugins() {
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; });
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
var installed = row.getAttribute('data-installed') || 'false';
var enabled = row.getAttribute('data-enabled') || 'false';
var filterMatch = matchesFilter(installed, enabled);
var show = searchMatch && filterMatch;
row.style.display = show ? '' : 'none';
if (show) visibleCount++;
});
}
}
var hasFilterOrSearch = terms.length > 0 || (filter !== 'all');
if (noResultsGrid) {
noResultsGrid.style.display = (terms.length > 0 && visibleCount === 0) ? 'block' : 'none';
noResultsGrid.style.display = (hasFilterOrSearch && visibleCount === 0) ? 'block' : 'none';
}
if (noResultsTable) {
noResultsTable.style.display = (terms.length > 0 && visibleCount === 0) ? 'table-row' : 'none';
noResultsTable.style.display = (hasFilterOrSearch && visibleCount === 0) ? 'table-row' : 'none';
}
}
@@ -2386,43 +2442,54 @@ function installPlugin(pluginName) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing...';
fetch(`/plugins/api/install/${pluginName}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Installation Failed!',
text: data.error || 'Failed to install plugin',
type: 'error'
});
} else {
alert('Error: ' + (data.error || 'Failed to install plugin'));
}
btn.disabled = false;
btn.innerHTML = originalText;
}
})
.catch(error => {
const headers = {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
};
function showError(msg) {
if (typeof PNotify !== 'undefined') {
new PNotify({
title: 'Error!',
text: 'Failed to install plugin: ' + error.message,
type: 'error'
});
new PNotify({ title: 'Installation Failed!', text: msg || 'Failed to install plugin', type: 'error' });
} else {
alert('Error: Failed to install plugin - ' + error.message);
alert('Error: ' + (msg || 'Failed to install plugin'));
}
btn.disabled = false;
btn.innerHTML = originalText;
}
function tryLocalInstall() {
return fetch(`/plugins/api/install/${pluginName}/`, { method: 'POST', headers: headers })
.then(function(r) { return r.json().then(function(data) { return { response: r, data: data }; }); });
}
function tryStoreInstall() {
return fetch(`/plugins/api/store/install/${pluginName}/`, { method: 'POST', headers: headers })
.then(function(r) { return r.json().then(function(data) { return { response: r, data: data }; }); });
}
tryLocalInstall()
.then(function(result) {
if (result.data.success) {
location.reload();
return;
}
var err = result.data.error || '';
if (result.response.status === 404 || (err && err.indexOf('Plugin source not found') !== -1)) {
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installing from store...';
return tryStoreInstall();
}
showError(err);
})
.then(function(result) {
if (!result || !result.data) return;
if (result.data.success) {
location.reload();
} else {
showError(result.data.error || 'Failed to install plugin');
}
})
.catch(function(error) {
showError(error && error.message ? error.message : 'Failed to install plugin');
});
}
@@ -2978,7 +3045,14 @@ document.addEventListener('DOMContentLoaded', function() {
// Set initial view without updating hash (only update hash if there was already one)
const hadHash = hash.length > 0;
toggleView(initialView, hadHash);
try {
toggleView(initialView, hadHash);
} catch (e) {
console.warn('plugins: toggleView on load failed', e);
if (storeView) storeView.style.display = 'block';
if (gridView) gridView.style.display = 'none';
if (tableView) tableView.style.display = 'none';
}
} else {
// Elements don't exist (no plugins installed), just show store view directly
if (storeView) {