Files
CyberPanel/testPlugin/templates/testPlugin/plugin_docs.html
Master3395 97fd4e055a Enhance security by adding rel="noopener" to external links
- Updated multiple HTML templates to include rel="noopener" on links that open in a new tab, improving security by preventing potential reverse tabnabbing attacks.
- This change affects various templates across the backup, base, file manager, mail server, and website functions sections.
2025-09-13 17:44:37 +02:00

1625 lines
58 KiB
HTML

{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Plugin Development Guide - CyberPanel" %}{% endblock %}
{% block header_scripts %}
<style>
.docs-wrapper {
background: transparent;
padding: 20px;
}
.docs-container {
max-width: 1200px;
margin: 0 auto;
}
.docs-header {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.docs-content {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.docs-nav {
display: flex;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.nav-button {
background: var(--bg-secondary, #f8f9ff);
color: var(--text-primary, #2f3640);
border: 1px solid var(--border-primary, #e8e9ff);
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
}
.nav-button:hover {
background: #5856d6;
color: white;
text-decoration: none;
transform: translateY(-2px);
}
.nav-button.active {
background: #5856d6;
color: white;
}
.docs-section {
display: none;
}
.docs-section.active {
display: block;
}
.section-title {
font-size: 24px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #5856d6;
}
.section-content {
line-height: 1.6;
color: var(--text-secondary, #64748b);
}
.section-content h1,
.section-content h2,
.section-content h3 {
color: var(--text-primary, #2f3640);
margin-top: 30px;
margin-bottom: 15px;
}
.section-content h1 {
font-size: 28px;
border-bottom: 2px solid #5856d6;
padding-bottom: 10px;
}
.section-content h2 {
font-size: 22px;
color: #5856d6;
}
.section-content h3 {
font-size: 18px;
}
.section-content code {
background: var(--bg-secondary, #f8f9ff);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #e83e8c;
}
.section-content pre {
background: var(--bg-secondary, #f8f9ff);
padding: 20px;
border-radius: 8px;
overflow-x: auto;
border: 1px solid var(--border-primary, #e8e9ff);
margin: 15px 0;
}
.section-content pre code {
background: none;
padding: 0;
color: var(--text-primary, #2f3640);
}
.section-content ul,
.section-content ol {
margin: 15px 0;
padding-left: 25px;
}
.section-content li {
margin: 8px 0;
}
.section-content blockquote {
border-left: 4px solid #5856d6;
padding-left: 20px;
margin: 20px 0;
font-style: italic;
color: var(--text-secondary, #64748b);
}
.feature-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-item {
background: var(--bg-secondary, #f8f9ff);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--border-primary, #e8e9ff);
}
.feature-item h4 {
color: #5856d6;
margin-bottom: 10px;
font-size: 16px;
}
.back-button {
background: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
.back-button:hover {
background: #5a6268;
color: white;
text-decoration: none;
}
.toc {
background: var(--bg-secondary, #f8f9ff);
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px solid var(--border-primary, #e8e9ff);
}
.toc h3 {
color: #5856d6;
margin-bottom: 15px;
}
.toc ul {
list-style: none;
padding: 0;
margin: 0;
}
.toc li {
margin: 8px 0;
}
.toc a {
color: var(--text-secondary, #64748b);
text-decoration: none;
transition: color 0.3s ease;
}
.toc a:hover {
color: #5856d6;
}
@media (max-width: 768px) {
.docs-wrapper {
padding: 15px;
}
.docs-nav {
flex-direction: column;
}
.nav-button {
justify-content: center;
}
.feature-list {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="docs-wrapper">
<div class="docs-container">
<!-- Docs Header -->
<div class="docs-header">
<h1>
<i class="fas fa-book" style="margin-right: 12px; color: #5856d6;"></i>
{% trans "Plugin Development Documentation" %}
</h1>
<p>{% trans "Complete guide for developing, installing, and managing CyberPanel plugins" %}</p>
</div>
<!-- Navigation -->
<div class="docs-nav">
<a href="#" class="nav-button active" data-section="quick-guide">
<i class="fas fa-rocket"></i>
{% trans "Quick Start" %}
</a>
<a href="#" class="nav-button" data-section="official-guide">
<i class="fas fa-graduation-cap"></i>
{% trans "Official Guide" %}
</a>
<a href="#" class="nav-button" data-section="full-guide">
<i class="fas fa-book-open"></i>
{% trans "Advanced Guide" %}
</a>
<a href="#" class="nav-button" data-section="os-compatibility">
<i class="fas fa-desktop"></i>
{% trans "OS Compatibility" %}
</a>
<a href="{% url 'testPlugin:plugin_home' %}" class="nav-button">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Plugin" %}
</a>
</div>
<!-- Quick Installation Guide -->
<div class="docs-section active" id="quick-guide">
<div class="docs-content">
<h1 class="section-title">Quick Installation Guide - CyberPanel Test Plugin</h1>
<div class="feature-list">
<div class="feature-item">
<h4>🚀 One-Command Installation</h4>
<p>Install the test plugin with a single command using curl or wget.</p>
</div>
<div class="feature-item">
<h4>📦 Manual Installation</h4>
<p>Download and install manually with step-by-step instructions.</p>
</div>
<div class="feature-item">
<h4>⚙️ Easy Management</h4>
<p>Simple install/uninstall process with proper cleanup.</p>
</div>
</div>
<h2>One-Command Installation</h2>
<pre><code># Install the test plugin with a single command
curl -sSL https://raw.githubusercontent.com/cyberpanel/testPlugin/main/install.sh | bash</code></pre>
<h2>Manual Installation</h2>
<ol>
<li><strong>Download the plugin</strong>
<pre><code>git clone https://github.com/cyberpanel/testPlugin.git
cd testPlugin</code></pre>
</li>
<li><strong>Run the installation script</strong>
<pre><code>chmod +x install.sh
./install.sh</code></pre>
</li>
<li><strong>Access the plugin</strong>
<ul>
<li>URL: <code>https://your-domain:8090/testPlugin/</code></li>
<li>Login with your CyberPanel admin credentials</li>
</ul>
</li>
</ol>
<h2>Features Included</h2>
<div class="feature-list">
<div class="feature-item">
<h4>✅ Enable/Disable Toggle</h4>
<p>Toggle the plugin on/off with a beautiful switch</p>
</div>
<div class="feature-item">
<h4>✅ Test Button</h4>
<p>Click to show popup messages from the side</p>
</div>
<div class="feature-item">
<h4>✅ Settings Page</h4>
<p>Configure custom messages and preferences</p>
</div>
<div class="feature-item">
<h4>✅ Activity Logs</h4>
<p>View all plugin activities with filtering</p>
</div>
<div class="feature-item">
<h4>✅ Inline Integration</h4>
<p>Loads within CyberPanel interface</p>
</div>
<div class="feature-item">
<h4>✅ Responsive Design</h4>
<p>Works perfectly on all devices</p>
</div>
</div>
<h2>Uninstallation</h2>
<pre><code># Uninstall the plugin
./install.sh --uninstall</code></pre>
<h2>Troubleshooting</h2>
<p>If you encounter any issues:</p>
<ol>
<li><strong>Check CyberPanel logs</strong>
<pre><code>tail -f /home/cyberpanel/logs/cyberpanel.log</code></pre>
</li>
<li><strong>Restart CyberPanel services</strong>
<pre><code>systemctl restart lscpd
systemctl restart cyberpanel</code></pre>
</li>
<li><strong>Verify installation</strong>
<pre><code>ls -la /home/cyberpanel/plugins/testPlugin
ls -la /usr/local/CyberCP/testPlugin</code></pre>
</li>
</ol>
<blockquote>
<strong>Note:</strong> This plugin is designed for testing and development purposes. Always backup your system before installing any plugins.
</blockquote>
</div>
</div>
<!-- Official CyberPanel Plugin Development Guide -->
<div class="docs-section" id="official-guide">
<div class="docs-content">
<h1 class="section-title">Getting Started with CyberPanel Plugin Development</h1>
<div class="feature-list">
<div class="feature-item">
<h4>🎯 Official Documentation</h4>
<p>Based on the official CyberPanel plugin development guide from the CyberPanel team.</p>
</div>
<div class="feature-item">
<h4>📚 Step-by-Step Tutorial</h4>
<p>Complete walkthrough from development environment setup to plugin installation.</p>
</div>
<div class="feature-item">
<h4>🔧 Signal Integration</h4>
<p>Learn how to hook into CyberPanel events and respond to core functionality.</p>
</div>
</div>
<blockquote>
<strong>Source:</strong> This guide is based on the official CyberPanel documentation and the <a href="https://github.com/usmannasir/beautiful_names" target="_blank" rel="noopener">beautiful_names plugin repository</a>.
</blockquote>
<h2>Prerequisites</h2>
<ul>
<li><strong>Python</strong> - Clear understanding of Python Programming Language</li>
<li><strong>Django</strong> - Experience with Django framework</li>
<li><strong>HTML (Basic)</strong> - Basic HTML knowledge</li>
<li><strong>CSS (Basic)</strong> - Basic CSS knowledge</li>
</ul>
<p><strong>Note:</strong> You can use plain JavaScript in your plugins or any JavaScript framework. You just have to follow the norms of Django framework, because CyberPanel plugin is just another Django app.</p>
<h2>Step 1: Set up your Development Environment</h2>
<h3>Clone CyberPanel Repository</h3>
<pre><code>git clone https://github.com/usmannasir/cyberpanel/ --single-branch v1.7.2-plugin</code></pre>
<h3>Create a Django App</h3>
<pre><code>cd v1.7.2-plugin
django-admin startapp pluginName</code></pre>
<p>Choose your plugin name wisely as it's of great importance. Once the Django app is created, you need to define a meta file for your plugin so that CyberPanel can read information about your plugin.</p>
<h3>Create Meta File</h3>
<pre><code>cd pluginName
nano meta.xml</code></pre>
<p>Paste the following content in the meta.xml file:</p>
<pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;cyberpanelPluginConfig&gt;
&lt;name&gt;customplugin&lt;/name&gt;
&lt;type&gt;plugin&lt;/type&gt;
&lt;description&gt;Plugin to make custom changes&lt;/description&gt;
&lt;version&gt;0&lt;/version&gt;
&lt;/cyberpanelPluginConfig&gt;</code></pre>
<h2>Step 2: Creating a Signal File and Adjusting Settings</h2>
<h3>Create Signals File</h3>
<p>Create a signals.py file (you can name it anything, but signals.py is recommended). You can leave this file empty for now.</p>
<h3>Configure apps.py</h3>
<p>In your apps.py file, you need to import the signals file inside the ready function:</p>
<pre><code>def ready(self):
import signals</code></pre>
<h3>Configure __init__.py</h3>
<p>You need to specify a default_app_config variable in this file:</p>
<pre><code>default_app_config = 'examplePlugin.apps.ExamplepluginConfig'</code></pre>
<h3>Create urls.py</h3>
<p>Inside your app root directory, create urls.py and paste this content:</p>
<pre><code>from django.conf.urls import url
import views
urlpatterns = [
url(r'^$', views.examplePlugin, name='examplePlugin'),
]</code></pre>
<p><strong>Important:</strong> Replace <code>examplePlugin</code> with your plugin name. This URL definition is very important for CyberPanel to register your plugin page.</p>
<h3>Optional Files</h3>
<p>You can create these optional files for database model management:</p>
<ul>
<li><strong>pre_install</strong> - Executed before installation of plugin</li>
<li><strong>post_install</strong> - Executed after installation of plugin</li>
</ul>
<p>If your file is Python code, don't forget to include this line at the top:</p>
<pre><code>#!/usr/local/CyberCP/bin/python2</code></pre>
<h2>Step 3: Responding to Events</h2>
<p>To plug into events fired by CyberPanel core, you can respond to various events happening in the core. Visit the <a href="http://cyberpanel.net/docs/2-list-of-signals-events-files/" target="_blank" rel="noopener">signal file documentation</a> for a complete list of events.</p>
<h3>Example Events</h3>
<ul>
<li><strong>preWebsiteCreation</strong> - Fired before CyberPanel starts the creation of website</li>
<li><strong>postWebsiteDeletion</strong> - Fired after core finished the deletion of website</li>
</ul>
<h3>Responding to Events</h3>
<p>Here's how you can respond to the <code>postWebsiteDeletion</code> event:</p>
<pre><code>from django.dispatch import receiver
from django.http import HttpResponse
from websiteFunctions.signals import postWebsiteDeletion
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
@receiver(postWebsiteDeletion)
def rcvr(sender, **kwargs):
request = kwargs['request']
logging.writeToFile('Hello World from Example Plugin.')
return HttpResponse('Hello World from Example Plugin.')</code></pre>
<h3>Return Values</h3>
<ul>
<li><strong>HttpResponse object</strong> - CyberPanel core will stop further processing and return your response to browser</li>
<li><strong>int 200</strong> - CyberPanel core will continue processing, assuming the event was successfully executed</li>
</ul>
<h2>Step 4: Packing, Shipping and Installing Plugin</h2>
<h3>Package Your Plugin</h3>
<p>After completing your plugin, zip your Django app. The zip file name should be your plugin name (e.g., <code>examplePlugin.zip</code>), otherwise installation will fail.</p>
<h3>Installation</h3>
<p>First, upload your plugin to <code>/usr/local/CyberCP/pluginInstaller</code>:</p>
<pre><code>cd /usr/local/CyberCP/pluginInstaller
python pluginInstaller.py install --pluginName examplePlugin</code></pre>
<h3>Uninstall</h3>
<pre><code>cd /usr/local/CyberCP/pluginInstaller
python pluginInstaller.py remove --pluginName examplePlugin</code></pre>
<h2>Beautiful Names Plugin Example</h2>
<p>CyberPanel has released an official plugin called <a href="https://github.com/usmannasir/beautiful_names" target="_blank" rel="noopener">Beautiful Names</a> that removes the <code>admin_</code> prefix from Package and FTP account names. This plugin serves as a great example of how to create CyberPanel plugins.</p>
<h3>Installation of Beautiful Names</h3>
<pre><code>cd /usr/local/CyberCP/pluginInstaller
wget https://cyberpanel.net/beautifulNames.zip
python pluginInstaller.py install --pluginName beautifulNames</code></pre>
<h3>Uninstall Beautiful Names</h3>
<pre><code>cd /usr/local/CyberCP/pluginInstaller
python pluginInstaller.py remove --pluginName beautifulNames</code></pre>
<h2>Plugin Installation Facility</h2>
<p>The plugin installation facility is in beta and not available with the official install yet. To install plugins, you need to install CyberPanel via the test version:</p>
<pre><code>sh &lt;(curl https://mirror.cyberpanel.net/install-test.sh || wget -O - https://mirror.cyberpanel.net/install-test.sh)</code></pre>
<h2>Additional Resources</h2>
<ul>
<li><a href="http://cyberpanel.net/docs/2-list-of-signals-events-files/" target="_blank" rel="noopener">Complete List of Signals and Events</a></li>
<li><a href="https://github.com/usmannasir/beautiful_names" target="_blank" rel="noopener">Beautiful Names Plugin Repository</a></li>
<li><a href="https://github.com/usmannasir/cyberpanel" target="_blank" rel="noopener">CyberPanel GitHub Repository</a></li>
</ul>
<blockquote>
<strong>Note:</strong> This guide is based on the official CyberPanel documentation. For the most up-to-date information, always refer to the official sources.
</blockquote>
</div>
</div>
<!-- Full Development Guide -->
<div class="docs-section" id="full-guide">
<div class="docs-content">
<h1 class="section-title">CyberPanel Plugin Development Guide</h1>
<div class="toc">
<h3>Table of Contents</h3>
<ul>
<li><a href="#install-plugins">How to Install Plugins</a></li>
<li><a href="#uninstall-plugins">How to Uninstall Plugins</a></li>
<li><a href="#meta-xml">How to Add meta.xml</a></li>
<li><a href="#add-buttons">How to Add Buttons for Pages</a></li>
<li><a href="#add-toggles">How to Add Toggles</a></li>
<li><a href="#install-uninstall-buttons">How to Add Install/Uninstall Buttons</a></li>
<li><a href="#enable-disable-buttons">How to Add Enable/Disable Buttons</a></li>
<li><a href="#avoid-sidebar-breaking">How to Avoid Breaking the Sidebar</a></li>
<li><a href="#inline-loading">How to Make Plugins Load Inline</a></li>
<li><a href="#plugin-structure">Plugin Structure Overview</a></li>
<li><a href="#best-practices">Best Practices</a></li>
<li><a href="#troubleshooting">Troubleshooting</a></li>
</ul>
</div>
<div id="install-plugins">
<h2>How to Install Plugins</h2>
<h3>Method 1: Using the Installation Script (Recommended)</h3>
<pre><code># Download and run the installation script
curl -sSL https://raw.githubusercontent.com/cyberpanel/testPlugin/main/install.sh | bash
# Or download first, then run
wget https://raw.githubusercontent.com/cyberpanel/testPlugin/main/install.sh
chmod +x install.sh
./install.sh</code></pre>
<h3>Method 2: Manual Installation</h3>
<ol>
<li><strong>Create Plugin Directory Structure</strong>
<pre><code>mkdir -p /home/cyberpanel/plugins/yourPlugin
mkdir -p /usr/local/CyberCP/yourPlugin</code></pre>
</li>
<li><strong>Copy Plugin Files</strong>
<pre><code>cp -r yourPlugin/* /usr/local/CyberCP/yourPlugin/
chown -R cyberpanel:cyberpanel /usr/local/CyberCP/yourPlugin
chmod -R 755 /usr/local/CyberCP/yourPlugin</code></pre>
</li>
<li><strong>Create Symlink</strong>
<pre><code>ln -sf /usr/local/CyberCP/yourPlugin /home/cyberpanel/plugins/yourPlugin</code></pre>
</li>
<li><strong>Update Django Settings</strong>
<p>Add your plugin to <code>INSTALLED_APPS</code> in <code>/usr/local/CyberCP/cyberpanel/settings.py</code>:</p>
<pre><code>INSTALLED_APPS = [
# ... existing apps ...
'yourPlugin',
]</code></pre>
</li>
<li><strong>Update URL Configuration</strong>
<p>Add your plugin URLs in <code>/usr/local/CyberCP/cyberpanel/urls.py</code>:</p>
<pre><code>urlpatterns = [
# ... existing patterns ...
path("yourPlugin/", include("yourPlugin.urls")),
]</code></pre>
</li>
<li><strong>Run Migrations</strong>
<pre><code>cd /usr/local/CyberCP
python3 manage.py makemigrations yourPlugin
python3 manage.py migrate yourPlugin</code></pre>
</li>
<li><strong>Collect Static Files</strong>
<pre><code>python3 manage.py collectstatic --noinput</code></pre>
</li>
<li><strong>Restart Services</strong>
<pre><code>systemctl restart lscpd
systemctl restart cyberpanel</code></pre>
</li>
</ol>
</div>
<div id="uninstall-plugins">
<h2>How to Uninstall Plugins</h2>
<h3>Method 1: Using the Installation Script</h3>
<pre><code># Run with uninstall flag
./install.sh --uninstall</code></pre>
<h3>Method 2: Manual Uninstallation</h3>
<ol>
<li><strong>Remove Plugin Files</strong>
<pre><code>rm -rf /usr/local/CyberCP/yourPlugin
rm -f /home/cyberpanel/plugins/yourPlugin</code></pre>
</li>
<li><strong>Remove from Django Settings</strong>
<pre><code>sed -i '/yourPlugin/d' /usr/local/CyberCP/cyberpanel/settings.py</code></pre>
</li>
<li><strong>Remove from URLs</strong>
<pre><code>sed -i '/yourPlugin/d' /usr/local/CyberCP/cyberpanel/urls.py</code></pre>
</li>
<li><strong>Restart Services</strong>
<pre><code>systemctl restart lscpd
systemctl restart cyberpanel</code></pre>
</li>
</ol>
</div>
<div id="meta-xml">
<h2>How to Add meta.xml</h2>
<p>Create a <code>meta.xml</code> file in your plugin root directory:</p>
<pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;plugin&gt;
&lt;name&gt;Your Plugin Name&lt;/name&gt;
&lt;type&gt;Utility&lt;/type&gt;
&lt;description&gt;Your plugin description&lt;/description&gt;
&lt;version&gt;1.0.0&lt;/version&gt;
&lt;author&gt;Your Name&lt;/author&gt;
&lt;website&gt;https://your-website.com&lt;/website&gt;
&lt;license&gt;MIT&lt;/license&gt;
&lt;dependencies&gt;
&lt;python&gt;3.6+&lt;/python&gt;
&lt;django&gt;2.2+&lt;/django&gt;
&lt;/dependencies&gt;
&lt;permissions&gt;
&lt;admin&gt;true&lt;/admin&gt;
&lt;user&gt;false&lt;/user&gt;
&lt;/permissions&gt;
&lt;settings&gt;
&lt;enable_toggle&gt;true&lt;/enable_toggle&gt;
&lt;test_button&gt;true&lt;/test_button&gt;
&lt;popup_messages&gt;true&lt;/popup_messages&gt;
&lt;inline_integration&gt;true&lt;/inline_integration&gt;
&lt;/settings&gt;
&lt;/plugin&gt;</code></pre>
<h3>Required Fields:</h3>
<ul>
<li><code>name</code>: Plugin display name</li>
<li><code>type</code>: Plugin category (Utility, Security, Performance, etc.)</li>
<li><code>description</code>: Plugin description</li>
<li><code>version</code>: Plugin version</li>
</ul>
<h3>Optional Fields:</h3>
<ul>
<li><code>author</code>: Plugin author</li>
<li><code>website</code>: Plugin website</li>
<li><code>license</code>: License type</li>
<li><code>dependencies</code>: Required dependencies</li>
<li><code>permissions</code>: Access permissions</li>
<li><code>settings</code>: Plugin-specific settings</li>
</ul>
</div>
<div id="add-buttons">
<h2>How to Add Buttons for Pages</h2>
<h3>1. In Your Template</h3>
<pre><code>&lt;!-- Primary Action Button --&gt;
&lt;button class="btn-test" id="your-button"&gt;
&lt;i class="fas fa-icon"&gt;&lt;/i&gt;
Button Text
&lt;/button&gt;
&lt;!-- Secondary Button --&gt;
&lt;a href="{% url 'yourPlugin:your_view' %}" class="btn-secondary"&gt;
&lt;i class="fas fa-icon"&gt;&lt;/i&gt;
Button Text
&lt;/a&gt;
&lt;!-- Danger Button --&gt;
&lt;button class="btn-danger" id="danger-button"&gt;
&lt;i class="fas fa-trash"&gt;&lt;/i&gt;
Delete
&lt;/button&gt;</code></pre>
<h3>2. CSS Styles</h3>
<pre><code>.btn-test {
background: linear-gradient(135deg, #5856d6, #4a90e2);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-test:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(88,86,214,0.3);
}
.btn-test:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}</code></pre>
<h3>3. JavaScript Event Handling</h3>
<pre><code>document.getElementById('your-button').addEventListener('click', function() {
// Your button logic here
fetch('/yourPlugin/your-endpoint/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
},
body: JSON.stringify({data: 'value'})
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
showNotification('success', 'Success', data.message);
} else {
showNotification('error', 'Error', data.error_message);
}
});
});</code></pre>
</div>
<div id="add-toggles">
<h2>How to Add Toggles</h2>
<h3>1. HTML Structure</h3>
<pre><code>&lt;div class="control-group"&gt;
&lt;label for="plugin-toggle" class="toggle-label"&gt;
Enable Feature
&lt;/label&gt;
&lt;label class="toggle-switch"&gt;
&lt;input type="checkbox" id="plugin-toggle" {% if feature_enabled %}checked{% endif %}&gt;
&lt;span class="slider"&gt;&lt;/span&gt;
&lt;/label&gt;
&lt;/div&gt;</code></pre>
<h3>2. CSS Styles</h3>
<pre><code>.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
input:checked + .slider {
background-color: #5856d6;
}
input:checked + .slider:before {
transform: translateX(26px);
}</code></pre>
<h3>3. JavaScript Handling</h3>
<pre><code>document.getElementById('plugin-toggle').addEventListener('change', function() {
fetch('/yourPlugin/toggle/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
}
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
showNotification('success', 'Toggle Updated', data.message);
} else {
showNotification('error', 'Error', data.error_message);
// Revert toggle state
this.checked = !this.checked;
}
});
});</code></pre>
</div>
<div id="install-uninstall-buttons">
<h2>How to Add Install/Uninstall Buttons</h2>
<h3>1. In Your Plugin Template</h3>
<pre><code>&lt;div class="plugin-actions"&gt;
&lt;button class="btn-install" id="install-plugin"&gt;
&lt;i class="fas fa-download"&gt;&lt;/i&gt;
Install Plugin
&lt;/button&gt;
&lt;button class="btn-uninstall" id="uninstall-plugin"&gt;
&lt;i class="fas fa-trash"&gt;&lt;/i&gt;
Uninstall Plugin
&lt;/button&gt;
&lt;/div&gt;</code></pre>
<h3>2. CSS Styles</h3>
<pre><code>.plugin-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn-install {
background: #10b981;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-uninstall {
background: #ef4444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-install:hover {
background: #059669;
transform: translateY(-2px);
}
.btn-uninstall:hover {
background: #dc2626;
transform: translateY(-2px);
}</code></pre>
<h3>3. JavaScript Implementation</h3>
<pre><code>// Install button
document.getElementById('install-plugin').addEventListener('click', function() {
if (confirm('Are you sure you want to install this plugin?')) {
this.disabled = true;
this.innerHTML = '&lt;i class="fas fa-spinner fa-spin"&gt;&lt;/i&gt; Installing...';
fetch('/yourPlugin/install/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
}
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
showNotification('success', 'Installation Complete', data.message);
location.reload();
} else {
showNotification('error', 'Installation Failed', data.error_message);
}
})
.finally(() => {
this.disabled = false;
this.innerHTML = '&lt;i class="fas fa-download"&gt;&lt;/i&gt; Install Plugin';
});
}
});
// Uninstall button
document.getElementById('uninstall-plugin').addEventListener('click', function() {
if (confirm('Are you sure you want to uninstall this plugin? This action cannot be undone.')) {
this.disabled = true;
this.innerHTML = '&lt;i class="fas fa-spinner fa-spin"&gt;&lt;/i&gt; Uninstalling...';
fetch('/yourPlugin/uninstall/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
}
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
showNotification('success', 'Uninstallation Complete', data.message);
setTimeout(() => location.reload(), 2000);
} else {
showNotification('error', 'Uninstallation Failed', data.error_message);
}
})
.finally(() => {
this.disabled = false;
this.innerHTML = '&lt;i class="fas fa-trash"&gt;&lt;/i&gt; Uninstall Plugin';
});
}
});</code></pre>
</div>
<div id="enable-disable-buttons">
<h2>How to Add Enable/Disable Plugin Buttons</h2>
<h3>1. Model for Plugin State</h3>
<pre><code># models.py
from django.db import models
from django.contrib.auth.models import User
class PluginSettings(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
plugin_enabled = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['user']</code></pre>
<h3>2. View for Toggle</h3>
<pre><code># views.py
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from .models import PluginSettings
@require_http_methods(["POST"])
def toggle_plugin(request):
try:
settings, created = PluginSettings.objects.get_or_create(
user=request.user,
defaults={'plugin_enabled': True}
)
settings.plugin_enabled = not settings.plugin_enabled
settings.save()
return JsonResponse({
'status': 1,
'enabled': settings.plugin_enabled,
'message': f'Plugin {"enabled" if settings.plugin_enabled else "disabled"} successfully'
})
except Exception as e:
return JsonResponse({'status': 0, 'error_message': str(e)})</code></pre>
<h3>3. Template Implementation</h3>
<pre><code>&lt;div class="plugin-controls"&gt;
&lt;label for="plugin-toggle" class="toggle-label"&gt;
Enable Plugin
&lt;/label&gt;
&lt;label class="toggle-switch"&gt;
&lt;input type="checkbox" id="plugin-toggle" {% if plugin_enabled %}checked{% endif %}&gt;
&lt;span class="slider"&gt;&lt;/span&gt;
&lt;/label&gt;
&lt;/div&gt;</code></pre>
<h3>4. JavaScript Handling</h3>
<pre><code>document.getElementById('plugin-toggle').addEventListener('change', function() {
fetch('/yourPlugin/toggle/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
}
})
.then(response => response.json())
.then(data => {
if (data.status === 1) {
showNotification('success', 'Plugin Toggle', data.message);
// Update UI elements based on enabled state
updatePluginUI(data.enabled);
} else {
showNotification('error', 'Error', data.error_message);
this.checked = !this.checked; // Revert toggle
}
});
});
function updatePluginUI(enabled) {
const buttons = document.querySelectorAll('.plugin-button');
buttons.forEach(button => {
button.disabled = !enabled;
});
const statusIndicator = document.querySelector('.status-indicator');
if (statusIndicator) {
statusIndicator.textContent = enabled ? 'Enabled' : 'Disabled';
statusIndicator.className = `status-indicator ${enabled ? 'enabled' : 'disabled'}`;
}
}</code></pre>
</div>
<div id="avoid-sidebar-breaking">
<h2>How to Avoid Breaking the CyberPanel Sidebar</h2>
<h3>1. Use CyberPanel's Base Template</h3>
<p>Always extend the base template:</p>
<pre><code>{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Your Plugin - CyberPanel" %}{% endblock %}
{% block content %}
&lt;!-- Your plugin content here --&gt;
{% endblock %}</code></pre>
<h3>2. Don't Modify the Sidebar HTML</h3>
<p>Never directly modify the sidebar HTML. Instead, use CyberPanel's built-in navigation system.</p>
<h3>3. Use Proper CSS Scoping</h3>
<pre><code>/* Good: Scoped to your plugin */
.your-plugin-wrapper {
/* Your styles here */
}
/* Bad: Global styles that might affect sidebar */
.sidebar {
/* Don't do this */
}</code></pre>
<h3>4. Use CyberPanel's CSS Variables</h3>
<pre><code>.your-plugin-element {
background: var(--bg-primary, white);
color: var(--text-primary, #2f3640);
border: 1px solid var(--border-primary, #e8e9ff);
}</code></pre>
<h3>5. Test Responsive Design</h3>
<p>Ensure your plugin works on all screen sizes without breaking the sidebar:</p>
<pre><code>@media (max-width: 768px) {
.your-plugin-wrapper {
padding: 15px;
}
/* Don't modify sidebar behavior */
}</code></pre>
</div>
<div id="inline-loading">
<h2>How to Make Plugins Load Inline</h2>
<h3>1. Use CyberPanel's httpProc</h3>
<pre><code># views.py
from plogical.httpProc import httpProc
def your_view(request):
context = {
'data': 'your_data',
'plugin_enabled': True
}
proc = httpProc(request, 'yourPlugin/your_template.html', context, 'admin')
return proc.render()</code></pre>
<h3>2. Template Structure</h3>
<pre><code>{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Your Plugin - CyberPanel" %}{% endblock %}
{% block header_scripts %}
&lt;style&gt;
/* Your plugin-specific styles */
.your-plugin-wrapper {
background: transparent;
padding: 20px;
}
.your-plugin-container {
max-width: 1200px;
margin: 0 auto;
}
&lt;/style&gt;
{% endblock %}
{% block content %}
&lt;div class="your-plugin-wrapper"&gt;
&lt;div class="your-plugin-container"&gt;
&lt;!-- Your plugin content here --&gt;
&lt;/div&gt;
&lt;/div&gt;
{% endblock %}</code></pre>
<h3>3. URL Configuration</h3>
<pre><code># urls.py
from django.urls import path
from . import views
app_name = 'yourPlugin'
urlpatterns = [
path('', views.your_view, name='your_view'),
path('settings/', views.settings_view, name='settings'),
# ... other URLs
]</code></pre>
<h3>4. Main URLs Integration</h3>
<pre><code># In /usr/local/CyberCP/cyberpanel/urls.py
urlpatterns = [
# ... existing patterns ...
path("yourPlugin/", include("yourPlugin.urls")),
]</code></pre>
</div>
<div id="plugin-structure">
<h2>Plugin Structure Overview</h2>
<pre><code>yourPlugin/
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── views.py
├── urls.py
├── signals.py
├── meta.xml
├── install.sh
├── templates/
│ └── yourPlugin/
│ ├── plugin_home.html
│ ├── plugin_settings.html
│ └── plugin_logs.html
├── static/
│ └── yourPlugin/
│ ├── css/
│ │ └── yourPlugin.css
│ └── js/
│ └── yourPlugin.js
└── migrations/
└── __init__.py</code></pre>
</div>
<div id="best-practices">
<h2>Best Practices</h2>
<h3>1. Security</h3>
<ul>
<li>Always validate user input</li>
<li>Use CSRF protection</li>
<li>Sanitize data before displaying</li>
<li>Use proper authentication decorators</li>
</ul>
<h3>2. Performance</h3>
<ul>
<li>Use database indexes for frequently queried fields</li>
<li>Implement caching where appropriate</li>
<li>Optimize database queries</li>
<li>Minimize JavaScript and CSS</li>
</ul>
<h3>3. User Experience</h3>
<ul>
<li>Provide clear feedback for all actions</li>
<li>Use loading states for long operations</li>
<li>Implement proper error handling</li>
<li>Make the interface responsive</li>
</ul>
<h3>4. Code Quality</h3>
<ul>
<li>Follow Django best practices</li>
<li>Use meaningful variable names</li>
<li>Add proper documentation</li>
<li>Write unit tests</li>
</ul>
<h3>5. Integration</h3>
<ul>
<li>Use CyberPanel's existing components</li>
<li>Follow the established design patterns</li>
<li>Maintain consistency with the UI</li>
<li>Test thoroughly before release</li>
</ul>
</div>
<div id="troubleshooting">
<h2>Troubleshooting</h2>
<h3>Common Issues</h3>
<h4>1. Plugin not showing in installed plugins</h4>
<ul>
<li>Check if meta.xml exists and is valid</li>
<li>Verify the plugin is in INSTALLED_APPS</li>
<li>Ensure proper file permissions</li>
</ul>
<h4>2. Template not found errors</h4>
<ul>
<li>Check template path in views.py</li>
<li>Verify template files exist</li>
<li>Ensure proper directory structure</li>
</ul>
<h4>3. Static files not loading</h4>
<ul>
<li>Run <code>python3 manage.py collectstatic</code></li>
<li>Check STATIC_URL configuration</li>
<li>Verify file permissions</li>
</ul>
<h4>4. Database migration errors</h4>
<ul>
<li>Check model definitions</li>
<li>Run <code>python3 manage.py makemigrations</code></li>
<li>Verify database connectivity</li>
</ul>
<h4>5. Permission denied errors</h4>
<ul>
<li>Check file ownership (cyberpanel:cyberpanel)</li>
<li>Verify file permissions (755 for directories, 644 for files)</li>
<li>Ensure proper SELinux context if applicable</li>
</ul>
<h3>Debug Steps</h3>
<h4>1. Check CyberPanel logs</h4>
<pre><code>tail -f /home/cyberpanel/logs/cyberpanel.log</code></pre>
<h4>2. Check Django logs</h4>
<pre><code>tail -f /home/cyberpanel/logs/django.log</code></pre>
<h4>3. Verify plugin installation</h4>
<pre><code>ls -la /home/cyberpanel/plugins/
ls -la /usr/local/CyberCP/yourPlugin/</code></pre>
<h4>4. Test database connectivity</h4>
<pre><code>cd /usr/local/CyberCP
python3 manage.py shell</code></pre>
<h4>5. Check service status</h4>
<pre><code>systemctl status lscpd
systemctl status cyberpanel</code></pre>
</div>
<blockquote>
<strong>Conclusion:</strong> This guide provides comprehensive instructions for developing CyberPanel plugins. Follow the best practices and troubleshooting steps to ensure your plugins integrate seamlessly with CyberPanel while maintaining security and performance standards.
</blockquote>
</div>
</div>
</div>
</div>
<!-- OS Compatibility Guide -->
<div class="docs-section" id="os-compatibility">
<div class="docs-content">
<h1 class="section-title">Operating System Compatibility</h1>
<div class="feature-list">
<div class="feature-item">
<h4>🌐 Multi-OS Support</h4>
<p>Comprehensive support for all CyberPanel-supported operating systems.</p>
</div>
<div class="feature-item">
<h4>🔍 Automatic Detection</h4>
<p>Intelligent OS detection and configuration for seamless installation.</p>
</div>
<div class="feature-item">
<h4>🧪 Compatibility Testing</h4>
<p>Built-in compatibility testing to verify system requirements.</p>
</div>
</div>
<h2>Supported Operating Systems</h2>
<div class="compatibility-grid">
<div class="os-card">
<h3>Ubuntu</h3>
<ul>
<li>✅ Ubuntu 22.04 (Full Support)</li>
<li>✅ Ubuntu 20.04 (Full Support)</li>
<li>✅ Debian 11+ (Compatible)</li>
</ul>
<p><strong>Package Manager:</strong> apt-get</p>
<p><strong>Web Server:</strong> apache2</p>
</div>
<div class="os-card">
<h3>RHEL-based</h3>
<ul>
<li>✅ AlmaLinux 8, 9, 10</li>
<li>✅ RockyLinux 8, 9</li>
<li>✅ RHEL 8, 9</li>
<li>✅ CentOS 9</li>
</ul>
<p><strong>Package Manager:</strong> dnf/yum</p>
<p><strong>Web Server:</strong> httpd</p>
</div>
<div class="os-card">
<h3>CloudLinux</h3>
<ul>
<li>✅ CloudLinux 8</li>
<li>✅ CloudLinux 7 (Limited)</li>
</ul>
<p><strong>Package Manager:</strong> yum</p>
<p><strong>Web Server:</strong> httpd</p>
</div>
</div>
<h2>Python Compatibility</h2>
<p>The plugin requires Python 3.6+ and automatically detects the correct Python executable:</p>
<div class="code-block">
<pre><code># Detection order:
1. python3.12
2. python3.11
3. python3.10
4. python3.9
5. python3.8
6. python3.7
7. python3.6
8. python3
9. python (fallback)</code></pre>
</div>
<h2>Installation Compatibility</h2>
<p>The installation script automatically detects your operating system and configures the plugin accordingly:</p>
<div class="code-block">
<pre><code># Automatic detection includes:
- OS name and version
- Python executable path
- Package manager (apt-get, dnf, yum)
- Service manager (systemctl, service)
- Web server (apache2, httpd)
- User and group permissions</code></pre>
</div>
<h2>Compatibility Testing</h2>
<p>Run the built-in compatibility test to verify your system:</p>
<div class="code-block">
<pre><code># Navigate to plugin directory
cd /usr/local/CyberCP/testPlugin
# Run compatibility test
python3 test_os_compatibility.py
# Or make it executable and run
chmod +x test_os_compatibility.py
./test_os_compatibility.py</code></pre>
</div>
<h2>Test Results</h2>
<p>The compatibility test checks:</p>
<ul>
<li>✅ OS detection and version</li>
<li>✅ Python installation and version</li>
<li>✅ Package manager availability</li>
<li>✅ Service manager functionality</li>
<li>✅ Web server configuration</li>
<li>✅ File permissions and ownership</li>
<li>✅ Network connectivity</li>
<li>✅ CyberPanel integration</li>
</ul>
<h2>OS-Specific Configurations</h2>
<h3>Ubuntu/Debian Systems</h3>
<div class="code-block">
<pre><code># Package Manager: apt-get
# Python: python3
# Pip: pip3
# Service Manager: systemctl
# Web Server: apache2
# User/Group: cyberpanel:cyberpanel
# Installation commands
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv git curl
sudo apt-get install -y build-essential python3-dev</code></pre>
</div>
<h3>RHEL-based Systems</h3>
<div class="code-block">
<pre><code># Package Manager: dnf (RHEL 8+) / yum (RHEL 7)
# Python: python3
# Pip: pip3
# Service Manager: systemctl
# Web Server: httpd
# User/Group: cyberpanel:cyberpanel
# Installation commands (RHEL 8+)
sudo dnf install -y python3 python3-pip python3-devel git curl
sudo dnf install -y gcc gcc-c++ make
# Installation commands (RHEL 7)
sudo yum install -y python3 python3-pip python3-devel git curl
sudo yum install -y gcc gcc-c++ make</code></pre>
</div>
<h3>CloudLinux</h3>
<div class="code-block">
<pre><code># Package Manager: yum
# Python: python3
# Pip: pip3
# Service Manager: systemctl
# Web Server: httpd
# User/Group: cyberpanel:cyberpanel
# Installation commands
sudo yum install -y python3 python3-pip python3-devel git curl
sudo yum install -y gcc gcc-c++ make
# CageFS configuration
cagefsctl --enable cyberpanel
cagefsctl --update</code></pre>
</div>
<h2>Security Compatibility</h2>
<h3>SELinux (RHEL-based systems)</h3>
<div class="code-block">
<pre><code># Check SELinux status
sestatus
# Set proper context for plugin files
setsebool -P httpd_can_network_connect 1
chcon -R -t httpd_exec_t /usr/local/CyberCP/testPlugin/</code></pre>
</div>
<h3>AppArmor (Ubuntu/Debian)</h3>
<div class="code-block">
<pre><code># Check AppArmor status
aa-status
# Allow Apache to access plugin files
aa-complain apache2</code></pre>
</div>
<h3>Firewall Configuration</h3>
<div class="code-block">
<pre><code># Ubuntu/Debian (ufw)
sudo ufw allow 8090/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# RHEL-based (firewalld)
sudo firewall-cmd --permanent --add-port=8090/tcp
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --reload</code></pre>
</div>
<h2>Troubleshooting</h2>
<h3>Common Issues</h3>
<div class="troubleshooting-section">
<h4>Python not found</h4>
<div class="code-block">
<pre><code># Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y python3 python3-pip
# RHEL-based
sudo dnf install -y python3 python3-pip
# or
sudo yum install -y python3 python3-pip</code></pre>
</div>
<h4>Permission denied</h4>
<div class="code-block">
<pre><code>sudo chown -R cyberpanel:cyberpanel /home/cyberpanel/plugins
sudo chown -R cyberpanel:cyberpanel /usr/local/CyberCP/testPlugin</code></pre>
</div>
<h4>Service not starting</h4>
<div class="code-block">
<pre><code>sudo systemctl daemon-reload
sudo systemctl restart lscpd
sudo systemctl restart apache2 # Ubuntu/Debian
sudo systemctl restart httpd # RHEL-based</code></pre>
</div>
</div>
<h2>Debug Commands</h2>
<div class="code-block">
<pre><code># Check OS information
cat /etc/os-release
uname -a
# Check Python installation
python3 --version
which python3
which pip3
# Check services
systemctl status lscpd
systemctl status apache2 # Ubuntu/Debian
systemctl status httpd # RHEL-based
# Check file permissions
ls -la /home/cyberpanel/plugins/
ls -la /usr/local/CyberCP/testPlugin/
# Check CyberPanel logs
tail -f /home/cyberpanel/logs/cyberpanel.log
tail -f /home/cyberpanel/logs/django.log</code></pre>
</div>
<blockquote>
<strong>Note:</strong> The plugin is designed to work seamlessly across all supported operating systems. If you encounter any compatibility issues, please run the compatibility test and check the troubleshooting section above.
</blockquote>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const navButtons = document.querySelectorAll('.nav-button[data-section]');
const sections = document.querySelectorAll('.docs-section');
navButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
// Remove active class from all buttons and sections
navButtons.forEach(btn => btn.classList.remove('active'));
sections.forEach(section => section.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Show corresponding section
const targetSection = document.getElementById(this.dataset.section);
if (targetSection) {
targetSection.classList.add('active');
}
});
});
// Smooth scrolling for anchor links
const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Initialize with quick-guide active
const quickGuideButton = document.querySelector('[data-section="quick-guide"]');
if (quickGuideButton) {
quickGuideButton.classList.add('active');
}
const quickGuideSection = document.getElementById('quick-guide');
if (quickGuideSection) {
quickGuideSection.classList.add('active');
}
});
</script>
{% endblock %}