Plugin Store: badges (NEW/Stable/Unstable/STALE), Activate/Deactivate All, categories & premium docs in help

This commit is contained in:
master3395
2026-02-02 02:18:05 +01:00
parent 86b5ed6e0e
commit e32219edde
4 changed files with 411 additions and 54 deletions

View File

@@ -129,6 +129,11 @@ def getAdminStatus(request):
def getSystemStatus(request):
default_fallback = {
'cpuUsage': 0, 'ramUsage': 0, 'diskUsage': 0,
'cpuCores': 2, 'ramTotalMB': 4096, 'diskTotalGB': 100,
'diskFreeGB': 100, 'uptime': 'N/A'
}
try:
val = request.session['userID']
currentACL = ACLManager.loadedACL(val)
@@ -215,31 +220,13 @@ def getSystemStatus(request):
except KeyError as e:
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] KeyError - No session userID: {str(e)}')
# Return default values on error
default_data = {
'cpuUsage': 0,
'ramUsage': 0,
'diskUsage': 0,
'cpuCores': 2,
'ramTotalMB': 4096,
'diskTotalGB': 100,
'diskFreeGB': 100,
'uptime': 'N/A'
}
return HttpResponse(json.dumps(default_data))
return HttpResponse(json.dumps(default_fallback))
except Exception as e:
# Return default values on error
default_data = {
'cpuUsage': 0,
'ramUsage': 0,
'diskUsage': 0,
'cpuCores': 2,
'ramTotalMB': 4096,
'diskTotalGB': 100,
'diskFreeGB': 100,
'uptime': 'N/A'
}
return HttpResponse(json.dumps(default_data))
logging.CyberCPLogFileWriter.writeToFile(f'[getSystemStatus] Exception: {str(e)}')
try:
return HttpResponse(json.dumps(default_fallback))
except Exception:
return HttpResponse('{"cpuUsage":0,"ramUsage":0,"diskUsage":0,"cpuCores":2,"ramTotalMB":4096,"diskTotalGB":100,"diskFreeGB":100,"uptime":"N/A"}', content_type='application/json')
def getLoadAverage(request):

View File

@@ -323,6 +323,9 @@
<li><a href="#first-plugin">{% trans "Creating Your First Plugin" %}</a></li>
<li><a href="#structure">{% trans "Plugin Structure & Files" %}</a></li>
<li><a href="#versioning">{% trans "Version Numbering (Semantic Versioning)" %}</a></li>
<li><a href="#categories">{% trans "Plugin Categories" %}</a></li>
<li><a href="#badges">{% trans "Freshness Badges" %}</a></li>
<li><a href="#premium">{% trans "Premium/Paid Plugin Creation" %}</a></li>
<li><a href="#components">{% trans "Core Components" %}</a></li>
<li><a href="#advanced">{% trans "Advanced Features" %}</a></li>
<li><a href="#best-practices">{% trans "Best Practices" %}</a></li>
@@ -402,7 +405,7 @@ mkdir -p migrations</code></pre>
&lt;/cyberpanelPluginConfig&gt;</code></pre>
<div class="alert alert-warning">
<strong>{% trans "Required: Category (type)" %}:</strong> {% trans "The &lt;type&gt; field is required. Valid categories: Utility, Security, Backup, Performance. Plugins without a valid category will not appear in the Plugin Store." %}
<strong>{% trans "Required: Category (type)" %}:</strong> {% trans "The &lt;type&gt; field is required. See the Plugin Categories section below for valid options. Plugins without a valid category will not appear in the Plugin Store." %}
</div>
<h3>{% trans "Step 3: Create urls.py" %}</h3>
@@ -510,6 +513,93 @@ def main_view(request):
<pre><code>&lt;version&gt;1.0.0&lt;/version&gt;</code></pre>
<p>{% trans "Never use formats like '1.0' or 'v1.0'. Always use the full semantic version: '1.0.0'" %}</p>
<h2 id="categories">{% trans "Plugin Categories" %}</h2>
<p>{% trans "The &lt;type&gt; field in meta.xml determines how your plugin is grouped in the Plugin Store. Use exactly one of these values (case-sensitive):" %}</p>
<table>
<thead>
<tr>
<th>{% trans "Category" %}</th>
<th>{% trans "Purpose" %}</th>
</tr>
</thead>
<tbody>
<tr><td><code>Utility</code></td><td>{% trans "General-purpose tools, helpers, and utilities" %}</td></tr>
<tr><td><code>Security</code></td><td>{% trans "Security features: firewalls, fail2ban, SSL, etc." %}</td></tr>
<tr><td><code>Backup</code></td><td>{% trans "Backup, snapshot, and restore functionality" %}</td></tr>
<tr><td><code>Performance</code></td><td>{% trans "Caching, optimization, and performance tuning" %}</td></tr>
<tr><td><code>Monitoring</code></td><td>{% trans "Monitoring, alerts, and health checks" %}</td></tr>
<tr><td><code>Integration</code></td><td>{% trans "Third-party integrations: Discord, Slack, webhooks, APIs" %}</td></tr>
<tr><td><code>Email</code></td><td>{% trans "Email marketing, SMTP, mail management" %}</td></tr>
<tr><td><code>Development</code></td><td>{% trans "Developer tools: PM2, Node.js, deployment" %}</td></tr>
<tr><td><code>Analytics</code></td><td>{% trans "Analytics, tracking, and reporting" %}</td></tr>
</tbody>
</table>
<h2 id="badges">{% trans "Freshness Badges" %}</h2>
<p>{% 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:" %}</p>
<table>
<thead>
<tr>
<th>{% trans "Badge" %}</th>
<th>{% trans "Condition" %}</th>
<th>{% trans "Meaning" %}</th>
</tr>
</thead>
<tbody>
<tr><td><span style="background:#fef08a;color:#854d0e;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">NEW</span></td><td>{% trans "Updated within last 90 days" %}</td><td>{% trans "Recently released or actively maintained" %}</td></tr>
<tr><td><span style="background:#bbf7d0;color:#166534;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Stable</span></td><td>{% trans "Updated within last 365 days" %}</td><td>{% trans "Updated within the past year" %}</td></tr>
<tr><td><span style="background:#e5e7eb;color:#4b5563;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Unstable</span></td><td>{% trans "12 years since last update" %}</td><td>{% trans "May need maintenance; consider forking or updating" %}</td></tr>
<tr><td><span style="background:#fecaca;color:#991b1b;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">STALE</span></td><td>{% trans "Over 2 years since last update" %}</td><td>{% trans "Not updated recently; use with caution" %}</td></tr>
</tbody>
</table>
<p>{% 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." %}</p>
<h2 id="premium">{% trans "Premium/Paid Plugin Creation" %}</h2>
<p>{% 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." %}</p>
<h3>{% trans "1. Mark Your Plugin as Paid in meta.xml" %}</h3>
<pre><code>&lt;paid&gt;true&lt;/paid&gt;
&lt;patreon_tier&gt;Your Tier Name&lt;/patreon_tier&gt;
&lt;patreon_url&gt;https://www.patreon.com/your-page&lt;/patreon_url&gt;</code></pre>
<p>{% trans "Set &lt;paid&gt;true&lt;/paid&gt; to display the Premium badge and subscription prompts in the Plugin Store." %}</p>
<h3>{% trans "2. Build Your Own Verification System" %}</h3>
<p>{% trans "Premium plugins typically verify access via a remote API. You can host this on your own site. Common verification methods:" %}</p>
<ul>
<li><strong>{% trans "Patreon" %}</strong>: {% trans "Verify membership via Patreon OAuth/API" %}</li>
<li><strong>{% trans "PayPal" %}</strong>: {% trans "Verify one-time or recurring payments" %}</li>
<li><strong>{% trans "Plugin Grants" %}</strong>: {% trans "Admin panel where you manually grant access by email, IP, or domain" %}</li>
<li><strong>{% trans "Activation Keys" %}</strong>: {% trans "Generate unique keys when granting access; users enter the key in the plugin" %}</li>
</ul>
<h3>{% trans "3. Optional: Encrypt PluginAPI Communication" %}</h3>
<p>{% 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:" %}</p>
<ul>
<li>{% trans "Verification requests cannot be easily intercepted or forged" %}</li>
<li>{% trans "Responses cannot be tampered with" %}</li>
<li>{% trans "Your verification logic remains on your server; the plugin only encrypts/decrypts with a shared key" %}</li>
</ul>
<p>{% trans "Implementation outline:" %}</p>
<ol>
<li>{% trans "Generate a 32-byte secret key and store it in your API config (e.g. config.php)" %}</li>
<li>{% trans "In your plugin (Python), encrypt outgoing requests and decrypt responses using the same key" %}</li>
<li>{% trans "On your API (PHP/Python), decrypt incoming requests and encrypt responses" %}</li>
<li>{% trans "Use the X-Encrypted: 1 header to indicate encrypted payloads" %}</li>
</ol>
<p>{% 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." %}</p>
<h3>{% trans "4. Typical Premium Plugin Flow" %}</h3>
<ol>
<li>{% trans "User installs your premium plugin" %}</li>
<li>{% trans "Plugin shows an activation screen (Patreon/PayPal links, or activation key input)" %}</li>
<li>{% trans "Plugin calls your verification API with user identifier (email, domain, IP) or activation key" %}</li>
<li>{% trans "Your API checks: Plugin Grants, activation key, Patreon, or PayPal — in your preferred order" %}</li>
<li>{% trans "If access is granted, API returns success; plugin unlocks features and optionally stores the key locally" %}</li>
</ol>
<div class="alert alert-info">
<strong>{% trans "Tip" %}:</strong> {% 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." %}
</div>
<h2 id="components">{% trans "Core Components" %}</h2>
<h3>{% trans "1. Authentication & Security" %}</h3>
<p>{% trans "Always use the cyberpanel_login_required decorator:" %}</p>
@@ -672,7 +762,7 @@ zip -r myPlugin-v1.0.0.zip . \
<p style="text-align: center; color: var(--text-secondary, #64748b);">
<strong>{% trans "Author" %}:</strong> master3395 |
<strong>{% trans "Version" %}:</strong> 2.0.0 |
<strong>{% trans "Last Updated" %}:</strong> 2026-01-04
<strong>{% trans "Last Updated" %}:</strong> 2026-02-02
</p>
</div>
</div>

View File

@@ -200,6 +200,32 @@
border: 1px solid #c3e6cb;
}
.freshness-badge-new, .freshness-badge-stable, .freshness-badge-stale {
display: inline-block;
padding: 3px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
margin-top: 4px;
}
.freshness-badge-new {
background: #fef08a;
color: #854d0e;
}
.freshness-badge-stable {
background: #bbf7d0;
color: #166534;
}
.freshness-badge-stale {
background: #fecaca;
color: #991b1b;
}
.freshness-badge-unstable {
background: #e5e7eb;
color: #4b5563;
}
.plugin-pricing-badge.paid {
background: #fff3cd;
color: #856404;
@@ -443,11 +469,62 @@
margin: 0 auto;
}
/* View Toggle */
/* View Toggle and Bulk Actions */
.view-toggle-wrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.view-toggle {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.bulk-actions-header {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
}
.bulk-actions-header .btn-bulk {
margin: 0;
flex-shrink: 0;
}
.btn-bulk {
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-activate-all {
background: #28a745;
color: white;
border-color: #218838;
}
.btn-activate-all:hover:not(:disabled) {
background: #218838;
}
.btn-activate-all:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-deactivate-all {
background: #ffc107;
color: #212529;
border-color: #e0a800;
}
.btn-deactivate-all:hover:not(:disabled) {
background: #e0a800;
}
.btn-deactivate-all:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.view-btn {
@@ -1076,7 +1153,7 @@
<div class="plugins-container">
<!-- Page Header -->
<div class="page-header">
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%; flex-wrap: wrap; gap: 16px;">
<div>
<h1>
<div class="icon">
@@ -1086,7 +1163,7 @@
</h1>
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
</div>
<div style="display: flex; gap: 20px; align-items: center; margin-top: 10px;">
<div style="display: flex; gap: 20px; align-items: center; margin-top: 10px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-check-circle" style="color: #28a745; font-size: 18px;"></i>
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
@@ -1099,6 +1176,16 @@
{% trans "Active:" %} {{ active_count|default:0 }}
</span>
</div>
{% if plugins %}
<div class="bulk-actions-header" style="display: flex; gap: 10px; margin-left: 8px; padding-left: 16px; border-left: 2px solid #e2e8f0;">
<button type="button" class="btn-bulk btn-activate-all" onclick="activateAllPlugins()" title="{% trans 'Activate all installed plugins' %}">
<i class="fas fa-toggle-on"></i> {% trans "Activate All Plugins" %}
</button>
<button type="button" class="btn-bulk btn-deactivate-all" onclick="deactivateAllPlugins()" title="{% trans 'Deactivate all installed plugins' %}">
<i class="fas fa-toggle-off"></i> {% trans "Deactivate All Plugins" %}
</button>
</div>
{% endif %}
</div>
</div>
</div>
@@ -1109,23 +1196,25 @@
{% if plugins %}
<!-- View Toggle -->
<div class="view-toggle">
<button class="view-btn active" onclick="toggleView('grid', true)">
<i class="fas fa-th-large"></i>
{% trans "Grid View" %}
</button>
<button class="view-btn" onclick="toggleView('table', true)">
<i class="fas fa-list"></i>
{% trans "Table View" %}
</button>
<button class="view-btn" onclick="toggleView('store', true)">
<i class="fas fa-store"></i>
{% trans "CyberPanel Plugin Store" %}
</button>
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
<i class="fas fa-book"></i>
{% trans "Plugin Development Guide" %}
</a>
<div class="view-toggle-wrapper">
<div class="view-toggle">
<button class="view-btn active" onclick="toggleView('grid', true)">
<i class="fas fa-th-large"></i>
{% trans "Grid View" %}
</button>
<button class="view-btn" onclick="toggleView('table', true)">
<i class="fas fa-list"></i>
{% trans "Table View" %}
</button>
<button class="view-btn" onclick="toggleView('store', true)">
<i class="fas fa-store"></i>
{% trans "CyberPanel Plugin Store" %}
</button>
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
<i class="fas fa-book"></i>
{% trans "Plugin Development Guide" %}
</a>
</div>
</div>
<!-- Grid View -->
@@ -1142,6 +1231,16 @@
<i class="fas fa-tools"></i>
{% elif plugin.type|lower == "backup" %}
<i class="fas fa-save"></i>
{% elif plugin.type|lower == "monitoring" %}
<i class="fas fa-heartbeat"></i>
{% elif plugin.type|lower == "integration" %}
<i class="fas fa-plug"></i>
{% elif plugin.type|lower == "email" %}
<i class="fas fa-envelope"></i>
{% elif plugin.type|lower == "development" %}
<i class="fas fa-code"></i>
{% elif plugin.type|lower == "analytics" %}
<i class="fas fa-chart-bar"></i>
{% else %}
<i class="fas fa-puzzle-piece"></i>
{% endif %}
@@ -1149,6 +1248,9 @@
<div class="plugin-info">
<h3 class="plugin-name">{{ plugin.name }}{% if plugin.is_paid|default:False|default_if_none:False %} <span class="paid-badge" title="{% trans 'Paid Plugin - Patreon Subscription Required' %}"><i class="fas fa-crown"></i> {% trans "Premium" %}</span>{% endif %}</h3>
<div class="plugin-meta">
{% if plugin.freshness_badge %}
<span class="{{ plugin.freshness_badge.class }}" title="{{ plugin.freshness_badge.title }}">{{ plugin.freshness_badge.badge }}</span>
{% endif %}
<span class="plugin-type">{{ plugin.type }}</span>
<span class="plugin-version-number">v{{ plugin.version }}</span>
{% if plugin.is_paid|default:False|default_if_none:False %}
@@ -1210,11 +1312,11 @@
</a>
{% endif %}
{% if plugin.enabled %}
<button class="btn-action btn-deactivate btn-small" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
<button class="btn-action btn-deactivate btn-small" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
<i class="fas fa-toggle-on"></i> {% trans "Deactivate" %}
</button>
{% else %}
<button class="btn-action btn-activate btn-small" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
<button class="btn-action btn-activate btn-small" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
</button>
{% endif %}
@@ -1264,6 +1366,9 @@
<tr>
<td>
<strong>{{ plugin.name }}</strong>
{% if plugin.freshness_badge %}
<br><span class="{{ plugin.freshness_badge.class }}" title="{{ plugin.freshness_badge.title }}">{{ plugin.freshness_badge.badge }}</span>
{% endif %}
</td>
<td>
<span class="plugin-version-number">{{ plugin.version }}</span>
@@ -1294,11 +1399,11 @@
</a>
{% endif %}
{% if plugin.enabled %}
<button class="btn-action btn-deactivate" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
<button class="btn-action btn-deactivate" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="deactivatePlugin('{{ plugin.plugin_dir }}')">
<i class="fas fa-toggle-on"></i> {% trans "Deactivate" %}
</button>
{% else %}
<button class="btn-action btn-activate" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
<button class="btn-action btn-activate" data-plugin-dir="{{ plugin.plugin_dir }}" onclick="activatePlugin('{{ plugin.plugin_dir }}')">
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
</button>
{% endif %}
@@ -1444,6 +1549,26 @@
<i class="fas fa-rocket"></i>
{% trans "Performance" %}
</button>
<button class="category-btn" onclick="filterByCategory('Monitoring', event)" data-category="Monitoring">
<i class="fas fa-heartbeat"></i>
{% trans "Monitoring" %}
</button>
<button class="category-btn" onclick="filterByCategory('Integration', event)" data-category="Integration">
<i class="fas fa-plug"></i>
{% trans "Integration" %}
</button>
<button class="category-btn" onclick="filterByCategory('Email', event)" data-category="Email">
<i class="fas fa-envelope"></i>
{% trans "Email" %}
</button>
<button class="category-btn" onclick="filterByCategory('Development', event)" data-category="Development">
<i class="fas fa-code"></i>
{% trans "Development" %}
</button>
<button class="category-btn" onclick="filterByCategory('Analytics', event)" data-category="Analytics">
<i class="fas fa-chart-bar"></i>
{% trans "Analytics" %}
</button>
</div>
<!-- Alphabetical Filter (collapsible, hidden by default) -->
@@ -1628,6 +1753,31 @@ function escapeHtml(text) {
return div.innerHTML;
}
function getFreshnessBadgeHtml(freshnessFromApi, modifyDate) {
// Use API data if available
if (freshnessFromApi && freshnessFromApi.badge && freshnessFromApi.class) {
return `<br><span class="${escapeHtml(freshnessFromApi.class)}" title="${escapeHtml(freshnessFromApi.title || '')}">${escapeHtml(freshnessFromApi.badge)}</span>`;
}
// Compute from modify_date (for cached data without freshness_badge)
if (!modifyDate || modifyDate === 'N/A') return '';
try {
const m = modifyDate.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/);
if (!m) return '';
const d = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]), parseInt(m[4]), parseInt(m[5]), parseInt(m[6]));
const daysAgo = Math.floor((Date.now() - d.getTime()) / (24 * 60 * 60 * 1000));
if (daysAgo <= 90) {
return '<br><span class="freshness-badge-new" title="This plugin was released/updated within the last 3 months">NEW</span>';
} else if (daysAgo <= 365) {
return '<br><span class="freshness-badge-stable" title="This plugin was updated within the last year">Stable</span>';
} else if (daysAgo < 730) {
return '<br><span class="freshness-badge-unstable" title="This plugin has not been updated in over 1 year">Unstable</span>';
} else {
return '<br><span class="freshness-badge-stale" title="This plugin has not been updated in over 2 years">STALE</span>';
}
} catch (e) {}
return '';
}
function displayStorePlugins() {
// Version: 2026-01-27-v1 - Added category filtering support
// CRITICAL: This function MUST create exactly 8 columns (Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
@@ -1692,6 +1842,16 @@ function displayStorePlugins() {
iconClass = 'fas fa-tools';
} else if (pluginType.includes('backup')) {
iconClass = 'fas fa-save';
} else if (pluginType.includes('monitoring')) {
iconClass = 'fas fa-heartbeat';
} else if (pluginType.includes('integration')) {
iconClass = 'fas fa-plug';
} else if (pluginType.includes('email')) {
iconClass = 'fas fa-envelope';
} else if (pluginType.includes('development')) {
iconClass = 'fas fa-code';
} else if (pluginType.includes('analytics')) {
iconClass = 'fas fa-chart-bar';
}
const iconHtml = `<div class="plugin-icon" style="width: 40px; height: 40px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
<i class="${iconClass}"></i>
@@ -1733,6 +1893,9 @@ function displayStorePlugins() {
// Modify Date column - show N/A for store plugins (they're from GitHub, not local)
const modifyDateHtml = plugin.modify_date ? `<small style="color: var(--text-secondary, #64748b);">${escapeHtml(plugin.modify_date)}</small>` : '<small style="color: var(--text-secondary, #64748b);">N/A</small>';
// Freshness badge (NEW/Stable/STALE) - use API data or compute from modify_date
const freshnessBadgeHtml = getFreshnessBadgeHtml(plugin.freshness_badge || null, plugin.modify_date);
// Pricing badge - ALWAYS show a badge (default to Free if is_paid is missing/undefined/null)
// Version: 2026-01-25-v4 - Normalize is_paid to handle all possible values
let isPaid = false;
@@ -1754,6 +1917,7 @@ function displayStorePlugins() {
<td style="text-align: center;">${iconHtml}</td>
<td>
<strong>${escapeHtml(plugin.name)}</strong>
${freshnessBadgeHtml}
</td>
<td>${categoryBadge}</td>
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
@@ -2400,6 +2564,91 @@ function deactivatePlugin(pluginName) {
});
}
function activateAllPlugins() {
const btns = document.querySelectorAll('#gridView .btn-activate[data-plugin-dir], #tableView .btn-activate[data-plugin-dir]');
const plugins = Array.from(btns).map(b => b.getAttribute('data-plugin-dir')).filter(Boolean);
if (plugins.length === 0) {
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Info', text: 'No inactive plugins to activate.', type: 'info' });
} else {
alert('No inactive plugins to activate.');
}
return;
}
const activateAllBtn = document.querySelector('.btn-activate-all');
if (activateAllBtn) activateAllBtn.disabled = true;
let done = 0;
const run = () => {
if (done >= plugins.length) {
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Success!', text: `Activated ${plugins.length} plugin(s).`, type: 'success' });
} else {
alert(`Activated ${plugins.length} plugin(s).`);
}
setTimeout(() => location.reload(), 800);
return;
}
const name = plugins[done];
fetch(`/plugins/api/enable/${name}/`, {
method: 'POST',
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (!data.success && typeof PNotify !== 'undefined') {
new PNotify({ title: 'Warning', text: `${name}: ${data.error || 'failed'}`, type: 'error' });
}
done++;
run();
})
.catch(() => { done++; run(); });
};
run();
}
function deactivateAllPlugins() {
const btns = document.querySelectorAll('#gridView .btn-deactivate[data-plugin-dir], #tableView .btn-deactivate[data-plugin-dir]');
const plugins = Array.from(btns).map(b => b.getAttribute('data-plugin-dir')).filter(Boolean);
if (plugins.length === 0) {
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Info', text: 'No active plugins to deactivate.', type: 'info' });
} else {
alert('No active plugins to deactivate.');
}
return;
}
if (!confirm(`Deactivate ${plugins.length} plugin(s)? They will be disabled but remain installed.`)) return;
const deactivateAllBtn = document.querySelector('.btn-deactivate-all');
if (deactivateAllBtn) deactivateAllBtn.disabled = true;
let done = 0;
const run = () => {
if (done >= plugins.length) {
if (typeof PNotify !== 'undefined') {
new PNotify({ title: 'Success!', text: `Deactivated ${plugins.length} plugin(s).`, type: 'success' });
} else {
alert(`Deactivated ${plugins.length} plugin(s).`);
}
setTimeout(() => location.reload(), 800);
return;
}
const name = plugins[done];
fetch(`/plugins/api/disable/${name}/`, {
method: 'POST',
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (!data.success && typeof PNotify !== 'undefined') {
new PNotify({ title: 'Warning', text: `${name}: ${data.error || 'failed'}`, type: 'error' });
}
done++;
run();
})
.catch(() => { done++; run(); });
};
run();
}
function dismissNotice() {
const notice = document.getElementById('pluginStoreNotice');
if (notice) {

View File

@@ -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)
@@ -129,7 +156,7 @@ def installed(request):
continue
# Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
if type_text.lower() not in ('utility', 'security', 'backup', 'performance'):
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
@@ -165,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')
@@ -275,7 +303,7 @@ def installed(request):
continue
# Valid categories only: Utility, Security, Backup, Performance (Plugin category removed)
if type_text.lower() not in ('utility', 'security', 'backup', 'performance'):
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
@@ -302,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')
@@ -1072,10 +1101,11 @@ def _fetch_plugins_from_github():
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'):
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,
@@ -1088,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