From b4a9a0741f6c937b6f7f2cb10e5b167210cf68d4 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 25 Jan 2026 03:55:50 +0100 Subject: [PATCH 01/40] =?UTF-8?q?fix(docker):=20listContainers=20HTML=20pa?= =?UTF-8?q?ge=20=E2=80=93=20avoid=20JSON/cache=20mix-up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GET /docker/containers for HTML page; GET /docker/listContainers redirects there - POST /docker/listContainers returns 405 (page uses getContainerList for data) - Remove duplicate listContainers Angular controller; fix pagination (getContainerList) - Extend getContainerList API: totalCount, totalPages, currentPage, itemsPerPage - Add ACTIVITY BOARD-style pagination: Prev/Next, Go to page, Showing X–Y of Z - Update menu/templates/JS redirects to /docker/containers - Sync dockerManager.js across app static, STATIC_ROOT, public/static - Cache-Control on HTML response; cache-bust script ?v=4 Fixes raw JSON instead of UI when loading /docker/listContainers (cache/proxy serving stored JSON for GET). Use /docker/containers for the page. --- .../templates/baseTemplate/index.html | 102 +- dockerManager/container.py | 38 +- .../static/dockerManager/dockerManager.js | 385 +--- .../templates/dockerManager/images.html | 2 +- .../templates/dockerManager/index.html | 2 +- .../dockerManager/listContainers.html | 41 +- dockerManager/urls.py | 2 +- dockerManager/views.py | 51 +- public/static/dockerManager/dockerManager.js | 1699 ++++++++++++++++- static/dockerManager/dockerManager.js | 385 +--- 10 files changed, 1923 insertions(+), 784 deletions(-) diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index 4f7b020c7..e818e9bd9 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -1,8 +1,8 @@ {% load i18n %} {% get_current_language as LANGUAGE_CODE %} -{% with CP_VERSION="2.4.4.1" %} +{% with CP_VERSION="2.5.5-dev-fix" %} - + @@ -17,30 +17,39 @@ - - - - - - - + + + + - + - + @@ -1566,7 +1575,7 @@ {% block header_scripts %}{% endblock %} - + @@ -1803,9 +1812,6 @@ List Sub/Addon Domains - - Fix Subdomain Logs - {% if admin or modifyWebsite %} Modify Website @@ -2157,7 +2163,7 @@ Manage Images - + Manage Containers @@ -2287,9 +2293,6 @@ AI Scanner - - Security Management - @@ -2343,11 +2346,6 @@ Manage FTP - {% if admin %} - - Bandwidth Management - - {% endif %} @@ -2452,6 +2450,12 @@ + + + {% endblock %} diff --git a/dockerManager/urls.py b/dockerManager/urls.py index 4652109c2..0292b1afd 100644 --- a/dockerManager/urls.py +++ b/dockerManager/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ re_path(r'^getTags$', views.getTags, name='getTags'), re_path(r'^runContainer', views.runContainer, name='runContainer'), re_path(r'^submitContainerCreation$', views.submitContainerCreation, name='submitContainerCreation'), + re_path(r'^containers$', views.listContainersPage, name='listContainersPage'), re_path(r'^listContainers$', views.listContainers, name='listContainers'), re_path(r'^getContainerList$', views.getContainerList, name='getContainerList'), re_path(r'^getContainerLogs$', views.getContainerLogs, name='getContainerLogs'), @@ -33,7 +34,6 @@ urlpatterns = [ re_path(r'^updateContainerPorts$', views.updateContainerPorts, name='updateContainerPorts'), re_path(r'^manageNetworks$', views.manageNetworks, name='manageNetworks'), re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'), - re_path(r'^listContainers$', views.listContainers, name='listContainers'), re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'), re_path(r'^deleteContainerKeepData$', views.deleteContainerKeepData, name='deleteContainerKeepData'), re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'), diff --git a/dockerManager/views.py b/dockerManager/views.py index 8f256f1c6..356fa353c 100644 --- a/dockerManager/views.py +++ b/dockerManager/views.py @@ -179,14 +179,40 @@ def runContainer(request): return redirect(loadLoginPage) @preDockerRun -def listContainers(request): +def listContainersPage(request): + """ + GET /docker/containers: Render HTML page only. Separate URL avoids + cache/proxy ever serving JSON (listContainers used to return JSON). + """ try: userID = request.session['userID'] cm = ContainerManager() - return cm.listContainers(request, userID) + resp = cm.listContainers(request, userID) + resp['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' + resp['Pragma'] = 'no-cache' + resp['Expires'] = '0' + return resp except KeyError: return redirect(loadLoginPage) + +@preDockerRun +def listContainers(request): + """ + GET: Redirect to /docker/containers (HTML). POST: 405. + listContainers URL historically returned JSON; caches may serve stale JSON. + Use /docker/containers for the page to avoid that. + """ + try: + request.session['userID'] # ensure logged in + except KeyError: + return redirect(loadLoginPage) + + if request.method != 'GET': + return HttpResponse('Method Not Allowed', status=405) + from django.urls import reverse + return redirect(reverse('listContainersPage')) + @preDockerRun def getContainerLogs(request): try: @@ -746,27 +772,6 @@ def getContainerEnv(request): except KeyError: return redirect(loadLoginPage) -@preDockerRun -def listContainers(request): - """ - Get list of all Docker containers - """ - try: - userID = request.session['userID'] - currentACL = ACLManager.loadedACL(userID) - - if currentACL['admin'] == 1: - pass - else: - return ACLManager.loadErrorJson() - - cm = ContainerManager() - coreResult = cm.listContainers(userID) - - return coreResult - except KeyError: - return redirect(loadLoginPage) - @preDockerRun def getDockerNetworks(request): """ diff --git a/public/static/dockerManager/dockerManager.js b/public/static/dockerManager/dockerManager.js index 4ae67f3f1..ed03f297d 100644 --- a/public/static/dockerManager/dockerManager.js +++ b/public/static/dockerManager/dockerManager.js @@ -110,7 +110,7 @@ app.controller('dockerImages', function ($scope) { /* Java script code to install Container */ app.controller('runContainer', function ($scope, $http) { - $scope.containerCreationLoading = true; + $scope.containerCreationLoading = false; $scope.installationDetailsForm = false; $scope.installationProgress = true; $scope.errorMessageBox = true; @@ -120,6 +120,91 @@ app.controller('runContainer', function ($scope, $http) { $scope.volList = {}; $scope.volListNumber = 0; + $scope.eport = {}; + $scope.iport = {}; + $scope.portType = {}; + $scope.envList = {}; + + // Advanced Environment Variable Mode + $scope.advancedEnvMode = false; + + // Network configuration + $scope.selectedNetwork = 'bridge'; + $scope.networkMode = 'bridge'; + $scope.extraHosts = ''; + $scope.availableNetworks = []; + + // Load available Docker networks + $scope.loadAvailableNetworks = function() { + var url = "/docker/getDockerNetworks"; + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, {}, config).then(function(response) { + if (response.data.status === 1) { + $scope.availableNetworks = response.data.networks; + } + }, function(error) { + console.error('Failed to load networks:', error); + }); + }; + + // Initialize networks on page load + $scope.loadAvailableNetworks(); + + // Helper function to generate Docker Compose YAML + $scope.generateDockerComposeYml = function(containerInfo) { + var yml = 'version: \'3.8\'\n\n'; + yml += 'services:\n'; + yml += ' ' + containerInfo.name + ':\n'; + yml += ' image: ' + containerInfo.image + '\n'; + yml += ' container_name: ' + containerInfo.name + '\n'; + + // Add ports + var ports = Object.keys(containerInfo.ports); + if (ports.length > 0) { + yml += ' ports:\n'; + for (var i = 0; i < ports.length; i++) { + var port = ports[i]; + if (containerInfo.ports[port]) { + yml += ' - "' + containerInfo.ports[port] + ':' + port + '"\n'; + } + } + } + + // Add volumes + var volumes = Object.keys(containerInfo.volumes); + if (volumes.length > 0) { + yml += ' volumes:\n'; + for (var i = 0; i < volumes.length; i++) { + var volume = volumes[i]; + if (containerInfo.volumes[volume]) { + yml += ' - ' + containerInfo.volumes[volume] + ':' + volume + '\n'; + } + } + } + + // Add environment variables + var envVars = Object.keys(containerInfo.environment); + if (envVars.length > 0) { + yml += ' environment:\n'; + for (var i = 0; i < envVars.length; i++) { + var envVar = envVars[i]; + yml += ' - ' + envVar + '=' + containerInfo.environment[envVar] + '\n'; + } + } + + // Add restart policy + yml += ' restart: unless-stopped\n'; + + return yml; + }; + $scope.advancedEnvText = ''; + $scope.advancedEnvCount = 0; + $scope.parsedEnvVars = {}; $scope.addVolField = function () { $scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''}; $scope.volListNumber = $scope.volListNumber + 1; @@ -135,8 +220,392 @@ app.controller('runContainer', function ($scope, $http) { $scope.envList[countEnv + 1] = {'name': '', 'value': ''}; }; + // Advanced Environment Variable Functions + $scope.toggleEnvMode = function() { + if ($scope.advancedEnvMode) { + // Switching to advanced mode - convert existing envList to text format + $scope.convertToAdvancedFormat(); + } else { + // Switching to simple mode - convert advanced text to envList + $scope.convertToSimpleFormat(); + } + }; + + $scope.convertToAdvancedFormat = function() { + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + $scope.advancedEnvText = envLines.join('\n'); + $scope.parseAdvancedEnv(); + }; + + $scope.convertToSimpleFormat = function() { + $scope.parseAdvancedEnv(); + var newEnvList = {}; + var index = 0; + for (var key in $scope.parsedEnvVars) { + newEnvList[index] = {'name': key, 'value': $scope.parsedEnvVars[key]}; + index++; + } + $scope.envList = newEnvList; + }; + + $scope.parseAdvancedEnv = function() { + $scope.parsedEnvVars = {}; + $scope.advancedEnvCount = 0; + + if (!$scope.advancedEnvText) { + return; + } + + var lines = $scope.advancedEnvText.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + + // Skip empty lines and comments + if (!line || line.startsWith('#')) { + continue; + } + + // Parse KEY=VALUE format + var equalIndex = line.indexOf('='); + if (equalIndex > 0) { + var key = line.substring(0, equalIndex).trim(); + var value = line.substring(equalIndex + 1).trim(); + + // Remove quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + if (key && key.match(/^[A-Za-z_][A-Za-z0-9_]*$/)) { + $scope.parsedEnvVars[key] = value; + $scope.advancedEnvCount++; + } + } + } + }; + + $scope.loadEnvTemplate = function() { + var templates = { + 'web-app': 'NODE_ENV=production\nPORT=3000\nDATABASE_URL=postgresql://user:pass@localhost/db\nREDIS_URL=redis://localhost:6379\nJWT_SECRET=your-jwt-secret\nAPI_KEY=your-api-key', + 'database': 'POSTGRES_DB=myapp\nPOSTGRES_USER=user\nPOSTGRES_PASSWORD=password\nPOSTGRES_HOST=localhost\nPOSTGRES_PORT=5432', + 'api': 'API_HOST=0.0.0.0\nAPI_PORT=8080\nLOG_LEVEL=info\nCORS_ORIGIN=*\nRATE_LIMIT=1000\nAPI_KEY=your-secret-key', + 'monitoring': 'PROMETHEUS_PORT=9090\nGRAFANA_PORT=3000\nALERTMANAGER_PORT=9093\nRETENTION_TIME=15d\nSCRAPE_INTERVAL=15s' + }; + + var templateNames = Object.keys(templates); + var templateChoice = prompt('Choose a template:\n' + templateNames.map((name, i) => (i + 1) + '. ' + name).join('\n') + '\n\nEnter number or template name:'); + + if (templateChoice) { + var templateIndex = parseInt(templateChoice) - 1; + var selectedTemplate = null; + + if (templateIndex >= 0 && templateIndex < templateNames.length) { + selectedTemplate = templates[templateNames[templateIndex]]; + } else { + // Try to find by name + var templateName = templateChoice.toLowerCase().replace(/\s+/g, '-'); + if (templates[templateName]) { + selectedTemplate = templates[templateName]; + } + } + + if (selectedTemplate) { + if ($scope.advancedEnvMode) { + $scope.advancedEnvText = selectedTemplate; + $scope.parseAdvancedEnv(); + } else { + // Convert template to simple format + var lines = selectedTemplate.split('\n'); + $scope.envList = {}; + var index = 0; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line && !line.startsWith('#')) { + var equalIndex = line.indexOf('='); + if (equalIndex > 0) { + $scope.envList[index] = { + 'name': line.substring(0, equalIndex).trim(), + 'value': line.substring(equalIndex + 1).trim() + }; + index++; + } + } + } + } + + new PNotify({ + title: 'Template Loaded', + text: 'Environment variable template has been loaded successfully', + type: 'success' + }); + } + } + }; + + + // Docker Compose Functions for runContainer + $scope.generateDockerCompose = function() { + // Get container information from form + var containerInfo = { + name: $scope.name || 'my-container', + image: $scope.image || 'nginx:latest', + ports: $scope.eport || {}, + volumes: $scope.volList || {}, + environment: {} + }; + + // Collect environment variables + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + containerInfo.environment = $scope.parsedEnvVars; + } else { + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + containerInfo.environment[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + + // Generate docker-compose.yml content + var composeContent = $scope.generateDockerComposeYml(containerInfo); + + // Create and download file + var blob = new Blob([composeContent], { type: 'text/yaml' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'docker-compose.yml'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: 'Docker Compose Generated', + text: 'docker-compose.yml file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.generateEnvFile = function() { + var envText = ''; + + if ($scope.advancedEnvMode && $scope.advancedEnvText) { + envText = $scope.advancedEnvText; + } else { + // Convert simple mode to .env format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envText += $scope.envList[key].name + '=' + $scope.envList[key].value + '\n'; + } + } + } + + if (!envText.trim()) { + new PNotify({ + title: 'Nothing to Generate', + text: 'No environment variables to generate .env file', + type: 'warning' + }); + return; + } + + // Create and download file + var blob = new Blob([envText], { type: 'text/plain' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '.env'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: '.env File Generated', + text: '.env file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.showComposeHelp = function() { + var helpContent = ` +
+

How to use Docker Compose with Environment Variables

+
+
Step 1: Download Files
+

Click "Generate docker-compose.yml" and "Generate .env file" to download both files.

+ +
Step 2: Place Files
+

Place both files in the same directory on your server.

+ +
Step 3: Run Docker Compose
+

Run the following commands in your terminal:

+
docker compose up -d
+ +
Step 4: Update Environment Variables
+

To update environment variables:

+
    +
  1. Edit the .env file
  2. +
  3. Run: docker compose up -d
  4. +
  5. Only the environment variables will be reloaded (no container rebuild needed!)
  6. +
+ +
Benefits:
+
    +
  • No need to recreate containers
  • +
  • Faster environment variable updates
  • +
  • Version control friendly
  • +
  • Easy to share configurations
  • +
+
+
+ `; + + // Create modal for help + var modal = document.createElement('div'); + modal.className = 'modal fade'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + $(modal).modal('show'); + + // Remove modal when closed + $(modal).on('hidden.bs.modal', function() { + document.body.removeChild(modal); + }); + }; + + $scope.loadEnvFromFile = function() { + var input = document.createElement('input'); + input.type = 'file'; + input.accept = '.env,text/plain'; + input.onchange = function(event) { + var file = event.target.files[0]; + if (file) { + var reader = new FileReader(); + reader.onload = function(e) { + $scope.advancedEnvText = e.target.result; + $scope.parseAdvancedEnv(); + $scope.$apply(); + + new PNotify({ + title: 'File Loaded', + text: 'Environment variables loaded from file successfully', + type: 'success' + }); + }; + reader.readAsText(file); + } + }; + input.click(); + }; + + $scope.copyEnvToClipboard = function() { + var textToCopy = ''; + + if ($scope.advancedEnvMode) { + textToCopy = $scope.advancedEnvText; + } else { + // Convert simple format to text + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + textToCopy = envLines.join('\n'); + } + + if (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(function() { + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }).catch(function(err) { + // Fallback for older browsers + var textArea = document.createElement('textarea'); + textArea.value = textToCopy; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }); + } + }; + + $scope.clearAdvancedEnv = function() { + $scope.advancedEnvText = ''; + $scope.parsedEnvVars = {}; + $scope.advancedEnvCount = 0; + }; + var statusFile; + // Watch for changes to validate ports + $scope.$watchGroup(['name', 'dockerOwner', 'memory'], function() { + $scope.updateFormValidity(); + }); + + $scope.$watch('eport', function() { + $scope.updateFormValidity(); + }, true); + + $scope.formIsValid = false; + + $scope.updateFormValidity = function() { + // Basic required fields + if (!$scope.name || !$scope.dockerOwner || !$scope.memory) { + $scope.formIsValid = false; + return; + } + + // Check if all port mappings are filled (if they exist) + if ($scope.portType && Object.keys($scope.portType).length > 0) { + for (var port in $scope.portType) { + if (!$scope.eport || !$scope.eport[port]) { + $scope.formIsValid = false; + return; + } + } + } + + $scope.formIsValid = true; + }; + $scope.createContainer = function () { $scope.containerCreationLoading = true; @@ -158,15 +627,34 @@ app.controller('runContainer', function ($scope, $http) { var image = $scope.image; var numberOfEnv = Object.keys($scope.envList).length; + // Prepare environment variables based on mode + var finalEnvList = {}; + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + // Use parsed environment variables from advanced mode + finalEnvList = $scope.parsedEnvVars; + } else { + // Convert simple envList to proper format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + finalEnvList[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + var data = { name: name, tag: tag, memory: memory, dockerOwner: dockerOwner, image: image, - envList: $scope.envList, - volList: $scope.volList - + envList: finalEnvList, + volList: $scope.volList, + advancedEnvMode: $scope.advancedEnvMode, + network: $scope.selectedNetwork, + network_mode: $scope.networkMode, + extraOptions: { + add_host: $scope.extraHosts + } }; try { @@ -239,13 +727,96 @@ app.controller('runContainer', function ($scope, $http) { app.controller('listContainers', function ($scope, $http) { $scope.activeLog = ""; $scope.assignActive = ""; + $scope.dockerOwner = ""; + + /* Pagination (ACTIVITY BOARD-style) */ + var CONTAINERS_PER_PAGE = 10; + $scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } }; + $scope.gotoPageInput = { containers: 1 }; + $scope.totalCount = 0; + $scope.totalPages = 1; + $scope.Math = Math; + + $scope.getTotalPages = function(section) { + if (section === 'containers') return Math.max(1, $scope.totalPages || 1); + return 1; + }; + + $scope.goToPage = function(section, page) { + if (section !== 'containers') return; + var totalPages = $scope.getTotalPages(section); + var p = parseInt(page, 10); + if (isNaN(p) || p < 1) p = 1; + if (p > totalPages) p = totalPages; + $scope.getFurtherContainersFromDB(p); + }; + + $scope.nextPage = function(section) { + if (section !== 'containers') return; + if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) { + $scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1); + } + }; + + $scope.prevPage = function(section) { + if (section !== 'containers') return; + if ($scope.pagination.containers.currentPage > 1) { + $scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1); + } + }; + + $scope.getPageNumbers = function(section) { + if (section !== 'containers') return []; + var totalPages = $scope.getTotalPages(section); + var current = $scope.pagination.containers.currentPage; + var maxVisible = 5; + var pages = []; + if (totalPages <= maxVisible) { + for (var i = 1; i <= totalPages; i++) pages.push(i); + } else { + var start = Math.max(1, current - 2); + var end = Math.min(totalPages, start + maxVisible - 1); + if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1); + for (var j = start; j <= end; j++) pages.push(j); + } + return pages; + }; $scope.assignContainer = function (name) { - $("#assign").modal("show"); + console.log('assignContainer called with:', name); $scope.assignActive = name; + console.log('assignActive set to:', $scope.assignActive); + $("#assign").modal("show"); + }; + + // Test function to verify scope is working + $scope.testScope = function() { + alert('Scope is working! assignActive: ' + $scope.assignActive + ', dockerOwner: ' + $scope.dockerOwner); }; $scope.submitAssignContainer = function () { + console.log('submitAssignContainer called'); + console.log('dockerOwner:', $scope.dockerOwner); + console.log('assignActive:', $scope.assignActive); + + if (!$scope.dockerOwner) { + new PNotify({ + title: 'Error', + text: 'Please select an owner', + type: 'error' + }); + return; + } + + if (!$scope.assignActive) { + new PNotify({ + title: 'Error', + text: 'No container selected', + type: 'error' + }); + return; + } + url = "/docker/assignContainer"; var data = {name: $scope.assignActive, admin: $scope.dockerOwner}; @@ -265,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) { title: 'Container assigned successfully', type: 'success' }); - window.location.href = '/docker/listContainers'; + window.location.href = '/docker/containers'; } else { new PNotify({ @@ -383,8 +954,234 @@ app.controller('listContainers', function ($scope, $http) { }) } + // Update Container Functions + $scope.showUpdateModal = function (name, currentImage, currentTag) { + $scope.updateContainerName = name; + $scope.currentImage = currentImage; + $scope.currentTag = currentTag; + $scope.newImage = ''; + $scope.newTag = 'latest'; + $("#updateContainer").modal("show"); + }; + + $scope.performUpdate = function () { + if (!$scope.updateContainerName) { + new PNotify({ + title: 'Error', + text: 'No container selected', + type: 'error' + }); + return; + } + + // If no new image specified, use current image + if (!$scope.newImage) { + $scope.newImage = $scope.currentImage; + } + + // If no new tag specified, use latest + if (!$scope.newTag) { + $scope.newTag = 'latest'; + } + + (new PNotify({ + title: 'Update Confirmation', + text: `Are you sure you want to update container "${$scope.updateContainerName}" to ${$scope.newImage}:${$scope.newTag}? This will preserve all data.`, + icon: 'fa fa-question-circle', + hide: false, + confirm: { + confirm: true + }, + buttons: { + closer: false, + sticker: false + }, + history: { + history: false + } + })).get().on('pnotify.confirm', function () { + $('#imageLoading').show(); + $("#updateContainer").modal("hide"); + + url = "/docker/updateContainer"; + var data = { + name: $scope.updateContainerName, + newImage: $scope.newImage, + newTag: $scope.newTag + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + function ListInitialData(response) { + console.log(response); + $('#imageLoading').hide(); + + if (response.data.updateContainerStatus === 1) { + new PNotify({ + title: 'Container Updated Successfully', + text: `Container updated to ${response.data.new_image}`, + type: 'success' + }); + location.reload(); + } else { + new PNotify({ + title: 'Update Failed', + text: response.data.error_message, + type: 'error' + }); + } + } + + function cantLoadInitialData(response) { + $('#imageLoading').hide(); + new PNotify({ + title: 'Update Failed', + text: 'Could not connect to server', + type: 'error' + }); + } + }); + }; + + // Delete Container with Data + $scope.deleteContainerWithData = function (name, unlisted = false) { + (new PNotify({ + title: 'Dangerous Operation', + text: `Are you sure you want to delete container "${name}" and ALL its data? This action cannot be undone!`, + icon: 'fa fa-exclamation-triangle', + hide: false, + confirm: { + confirm: true + }, + buttons: { + closer: false, + sticker: false + }, + history: { + history: false + } + })).get().on('pnotify.confirm', function () { + $('#imageLoading').show(); + url = "/docker/deleteContainerWithData"; + + var data = {name: name, unlisted: unlisted}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + function ListInitialData(response) { + console.log(response); + $('#imageLoading').hide(); + + if (response.data.deleteContainerWithDataStatus === 1) { + new PNotify({ + title: 'Container and Data Deleted', + text: 'Container and all associated data have been permanently deleted', + type: 'success' + }); + location.reload(); + } else { + new PNotify({ + title: 'Deletion Failed', + text: response.data.error_message, + type: 'error' + }); + } + } + + function cantLoadInitialData(response) { + $('#imageLoading').hide(); + new PNotify({ + title: 'Deletion Failed', + text: 'Could not connect to server', + type: 'error' + }); + } + }); + }; + + // Delete Container Keep Data + $scope.deleteContainerKeepData = function (name, unlisted = false) { + (new PNotify({ + title: 'Delete Container (Keep Data)', + text: `Are you sure you want to delete container "${name}" but keep all data? The data will be preserved in Docker volumes.`, + icon: 'fa fa-save', + hide: false, + confirm: { + confirm: true + }, + buttons: { + closer: false, + sticker: false + }, + history: { + history: false + } + })).get().on('pnotify.confirm', function () { + $('#imageLoading').show(); + url = "/docker/deleteContainerKeepData"; + + var data = {name: name, unlisted: unlisted}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + function ListInitialData(response) { + console.log(response); + $('#imageLoading').hide(); + + if (response.data.deleteContainerKeepDataStatus === 1) { + var message = 'Container deleted successfully, data preserved'; + if (response.data.preserved_volumes && response.data.preserved_volumes.length > 0) { + message += '\nPreserved volumes: ' + response.data.preserved_volumes.join(', '); + } + new PNotify({ + title: 'Container Deleted (Data Preserved)', + text: message, + type: 'success' + }); + location.reload(); + } else { + new PNotify({ + title: 'Deletion Failed', + text: response.data.error_message, + type: 'error' + }); + } + } + + function cantLoadInitialData(response) { + $('#imageLoading').hide(); + new PNotify({ + title: 'Deletion Failed', + text: 'Could not connect to server', + type: 'error' + }); + } + }); + }; + $scope.showLog = function (name, refresh = false) { $scope.logs = ""; + $scope.logInfo = null; + $scope.formattedLogs = ""; + if (refresh === false) { $('#logs').modal('show'); $scope.activeLog = name; @@ -412,18 +1209,37 @@ app.controller('listContainers', function ($scope, $http) { if (response.data.containerLogStatus === 1) { $scope.logs = response.data.containerLog; + $scope.logInfo = { + container_status: response.data.container_status, + log_count: response.data.log_count + }; + + // Format logs for better display + $scope.formatLogs(); + + // Auto-scroll to bottom + setTimeout(function() { + $scope.scrollToBottom(); + }, 100); } else { + $scope.logs = response.data.error_message; + $scope.logInfo = null; + $scope.formattedLogs = ""; + new PNotify({ title: 'Unable to complete request', text: response.data.error_message, type: 'error' }); - } } function cantLoadInitialData(response) { + $scope.logs = "Error loading logs"; + $scope.logInfo = null; + $scope.formattedLogs = ""; + new PNotify({ title: 'Unable to complete request', type: 'error' @@ -431,87 +1247,145 @@ app.controller('listContainers', function ($scope, $http) { } }; - url = "/docker/getContainerList"; + // Format logs with syntax highlighting and better readability + $scope.formatLogs = function() { + if (!$scope.logs || $scope.logs === 'Loading...') { + $scope.formattedLogs = $scope.logs; + return; + } - var data = {page: 1}; + var lines = $scope.logs.split('\n'); + var formattedLines = []; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + var formattedLine = line; + + // Escape HTML characters + formattedLine = formattedLine.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + + // Add syntax highlighting for common log patterns + if (line.match(/^\[.*?\]/)) { + // Timestamp lines + formattedLine = '' + formattedLine + ''; + } else if (line.match(/ERROR|FATAL|CRITICAL/i)) { + // Error lines + formattedLine = '' + formattedLine + ''; + } else if (line.match(/WARN|WARNING/i)) { + // Warning lines + formattedLine = '' + formattedLine + ''; + } else if (line.match(/INFO/i)) { + // Info lines + formattedLine = '' + formattedLine + ''; + } else if (line.match(/DEBUG/i)) { + // Debug lines + formattedLine = '' + formattedLine + ''; + } else if (line.match(/SUCCESS|OK|COMPLETED/i)) { + // Success lines + formattedLine = '' + formattedLine + ''; + } + + formattedLines.push(formattedLine); + } + + $scope.formattedLogs = formattedLines.join('\n'); + }; + + // Scroll functions + $scope.scrollToTop = function() { + var container = document.getElementById('logContainer'); + if (container) { + container.scrollTop = 0; } }; - $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + $scope.scrollToBottom = function() { + var container = document.getElementById('logContainer'); + if (container) { + container.scrollTop = container.scrollHeight; + } + }; + // Clear logs function + $scope.clearLogs = function() { + $scope.logs = ""; + $scope.formattedLogs = ""; + $scope.logInfo = null; + }; - function ListInitialData(response) { - console.log(response); - + function handleContainerListResponse(response) { if (response.data.listContainerStatus === 1) { - var finalData = JSON.parse(response.data.data); $scope.ContainerList = finalData; - console.log($scope.ContainerList); + $scope.totalCount = response.data.totalCount || 0; + $scope.totalPages = Math.max(1, response.data.totalPages || 1); + var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1); + $scope.pagination.containers.currentPage = cp; + $scope.gotoPageInput.containers = cp; $("#listFail").hide(); - } - else { + } else { $("#listFail").fadeIn(); - $scope.errorMessage = response.data.error_message; - + $scope.errorMessage = response.data.error_message || 'Failed to load containers'; } } function cantLoadInitialData(response) { - console.log("not good"); + $("#listFail").fadeIn(); + $scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server'; } + var config = { + headers: { 'X-CSRFToken': getCookie('csrftoken') } + }; + $http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData); $scope.getFurtherContainersFromDB = function (pageNumber) { - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - var data = {page: pageNumber}; - - - dataurl = "/docker/getContainerList"; - - $http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData); - - - function ListInitialData(response) { - if (response.data.listContainerStatus === 1) { - - var finalData = JSON.parse(response.data.data); - $scope.ContainerList = finalData; - $("#listFail").hide(); - } - else { - $("#listFail").fadeIn(); - $scope.errorMessage = response.data.error_message; - console.log(response.data); - - } - } - - function cantLoadInitialData(response) { - console.log("not good"); - } - - + var p = Math.max(1, parseInt(pageNumber, 10) || 1); + $http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData); }; }); /* Java script code for containerr home page */ -app.controller('viewContainer', function ($scope, $http) { +app.controller('viewContainer', function ($scope, $http, $interval, $timeout) { $scope.cName = ""; $scope.status = ""; $scope.savingSettings = false; $scope.loadingTop = false; + $scope.statusInterval = null; + $scope.statsInterval = null; + + // Advanced Environment Variable Functions for viewContainer + $scope.advancedEnvMode = false; + $scope.advancedEnvText = ''; + $scope.advancedEnvCount = 0; + $scope.parsedEnvVars = {}; + + // Auto-refresh status every 5 seconds + $scope.startStatusMonitoring = function() { + $scope.statusInterval = $interval(function() { + $scope.refreshStatus(true); // Silent refresh + }, 5000); + }; + + // Stop monitoring on scope destroy + $scope.$on('$destroy', function() { + if ($scope.statusInterval) { + $interval.cancel($scope.statusInterval); + } + if ($scope.statsInterval) { + $interval.cancel($scope.statsInterval); + } + }); + + // Start monitoring when controller loads + $timeout(function() { + $scope.startStatusMonitoring(); + }, 1000); $scope.recreate = function () { (new PNotify({ @@ -576,6 +1450,397 @@ app.controller('viewContainer', function ($scope, $http) { $scope.envList[countEnv + 1] = {'name': '', 'value': ''}; }; + // Advanced Environment Variable Functions for viewContainer + $scope.toggleEnvMode = function() { + if ($scope.advancedEnvMode) { + // Switching to advanced mode - convert existing envList to text format + $scope.convertToAdvancedFormat(); + } else { + // Switching to simple mode - convert advanced text to envList + $scope.convertToSimpleFormat(); + } + }; + + $scope.convertToAdvancedFormat = function() { + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + $scope.advancedEnvText = envLines.join('\n'); + $scope.parseAdvancedEnv(); + }; + + $scope.convertToSimpleFormat = function() { + $scope.parseAdvancedEnv(); + var newEnvList = {}; + var index = 0; + for (var key in $scope.parsedEnvVars) { + newEnvList[index] = {'name': key, 'value': $scope.parsedEnvVars[key]}; + index++; + } + $scope.envList = newEnvList; + }; + + $scope.parseAdvancedEnv = function() { + $scope.parsedEnvVars = {}; + $scope.advancedEnvCount = 0; + + if (!$scope.advancedEnvText) { + return; + } + + var lines = $scope.advancedEnvText.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + + // Skip empty lines and comments + if (!line || line.startsWith('#')) { + continue; + } + + // Parse KEY=VALUE format + var equalIndex = line.indexOf('='); + if (equalIndex > 0) { + var key = line.substring(0, equalIndex).trim(); + var value = line.substring(equalIndex + 1).trim(); + + // Remove quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + if (key && key.match(/^[A-Za-z_][A-Za-z0-9_]*$/)) { + $scope.parsedEnvVars[key] = value; + $scope.advancedEnvCount++; + } + } + } + }; + + $scope.copyEnvToClipboard = function() { + var textToCopy = ''; + + if ($scope.advancedEnvMode) { + textToCopy = $scope.advancedEnvText; + } else { + // Convert simple format to text + var envLines = []; + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envLines.push($scope.envList[key].name + '=' + $scope.envList[key].value); + } + } + textToCopy = envLines.join('\n'); + } + + if (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(function() { + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }).catch(function(err) { + // Fallback for older browsers + var textArea = document.createElement('textarea'); + textArea.value = textToCopy; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + new PNotify({ + title: 'Copied to Clipboard', + text: 'Environment variables copied to clipboard', + type: 'success' + }); + }); + } + }; + + // Import/Export Functions + $scope.importEnvFromContainer = function() { + // Show modal to select container to import from + $scope.showContainerImportModal = true; + $scope.loadContainersForImport(); + }; + + $scope.loadContainersForImport = function() { + $scope.importLoading = true; + $scope.importContainers = []; + + $http.get('/dockerManager/loadContainersForImport/', { + params: { + currentContainer: $scope.cName + } + }).then(function(response) { + $scope.importContainers = response.data.containers || []; + $scope.importLoading = false; + }).catch(function(error) { + new PNotify({ + title: 'Import Failed', + text: 'Failed to load containers for import', + type: 'error' + }); + $scope.importLoading = false; + }); + }; + + $scope.selectContainerForImport = function(container) { + $scope.selectedImportContainer = container; + $scope.loadEnvFromContainer(container.name); + }; + + $scope.loadEnvFromContainer = function(containerName) { + $scope.importEnvLoading = true; + + $http.get('/dockerManager/getContainerEnv/', { + params: { + containerName: containerName + } + }).then(function(response) { + if (response.data.success) { + var envVars = response.data.envVars || {}; + + if ($scope.advancedEnvMode) { + // Convert to .env format + var envText = ''; + for (var key in envVars) { + envText += key + '=' + envVars[key] + '\n'; + } + $scope.advancedEnvText = envText; + $scope.parseAdvancedEnv(); + } else { + // Convert to simple mode + $scope.envList = {}; + var index = 0; + for (var key in envVars) { + $scope.envList[index] = {'name': key, 'value': envVars[key]}; + index++; + } + } + + $scope.showContainerImportModal = false; + new PNotify({ + title: 'Import Successful', + text: 'Environment variables imported from ' + containerName, + type: 'success' + }); + } else { + new PNotify({ + title: 'Import Failed', + text: response.data.message || 'Failed to import environment variables', + type: 'error' + }); + } + $scope.importEnvLoading = false; + }).catch(function(error) { + new PNotify({ + title: 'Import Failed', + text: 'Failed to import environment variables', + type: 'error' + }); + $scope.importEnvLoading = false; + }); + }; + + $scope.exportEnvToFile = function() { + var envText = ''; + + if ($scope.advancedEnvMode && $scope.advancedEnvText) { + envText = $scope.advancedEnvText; + } else { + // Convert simple mode to .env format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envText += $scope.envList[key].name + '=' + $scope.envList[key].value + '\n'; + } + } + } + + if (!envText.trim()) { + new PNotify({ + title: 'Nothing to Export', + text: 'No environment variables to export', + type: 'warning' + }); + return; + } + + // Create and download file + var blob = new Blob([envText], { type: 'text/plain' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = $scope.cName + '_environment.env'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: 'Export Successful', + text: 'Environment variables exported to file', + type: 'success' + }); + }; + + // Docker Compose Functions + $scope.generateDockerCompose = function() { + // Get container information + var containerInfo = { + name: $scope.cName, + image: $scope.image || 'nginx:latest', + ports: $scope.ports || {}, + volumes: $scope.volList || {}, + environment: {} + }; + + // Collect environment variables + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + containerInfo.environment = $scope.parsedEnvVars; + } else { + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + containerInfo.environment[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + + // Generate docker-compose.yml content + var composeContent = $scope.generateDockerComposeYml(containerInfo); + + // Create and download file + var blob = new Blob([composeContent], { type: 'text/yaml' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'docker-compose.yml'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: 'Docker Compose Generated', + text: 'docker-compose.yml file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.generateEnvFile = function() { + var envText = ''; + + if ($scope.advancedEnvMode && $scope.advancedEnvText) { + envText = $scope.advancedEnvText; + } else { + // Convert simple mode to .env format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + envText += $scope.envList[key].name + '=' + $scope.envList[key].value + '\n'; + } + } + } + + if (!envText.trim()) { + new PNotify({ + title: 'Nothing to Generate', + text: 'No environment variables to generate .env file', + type: 'warning' + }); + return; + } + + // Create and download file + var blob = new Blob([envText], { type: 'text/plain' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = '.env'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + new PNotify({ + title: '.env File Generated', + text: '.env file has been generated and downloaded', + type: 'success' + }); + }; + + $scope.showComposeHelp = function() { + var helpContent = ` +
+

How to use Docker Compose with Environment Variables

+
+
Step 1: Download Files
+

Click "Generate docker-compose.yml" and "Generate .env file" to download both files.

+ +
Step 2: Place Files
+

Place both files in the same directory on your server.

+ +
Step 3: Run Docker Compose
+

Run the following commands in your terminal:

+
docker compose up -d
+ +
Step 4: Update Environment Variables
+

To update environment variables:

+
    +
  1. Edit the .env file
  2. +
  3. Run: docker compose up -d
  4. +
  5. Only the environment variables will be reloaded (no container rebuild needed!)
  6. +
+ +
Benefits:
+
    +
  • No need to recreate containers
  • +
  • Faster environment variable updates
  • +
  • Version control friendly
  • +
  • Easy to share configurations
  • +
+
+
+ `; + + // Create modal for help + var modal = document.createElement('div'); + modal.className = 'modal fade'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + $(modal).modal('show'); + + // Remove modal when closed + $(modal).on('hidden.bs.modal', function() { + document.body.removeChild(modal); + }); + }; + + $scope.showTop = function () { $scope.topHead = []; $scope.topProcesses = []; @@ -652,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http) { text: 'Redirecting...', type: 'success' }); - window.location.href = '/docker/listContainers'; + window.location.href = '/docker/containers'; } else { new PNotify({ @@ -674,7 +1939,7 @@ app.controller('viewContainer', function ($scope, $http) { }) }; - $scope.refreshStatus = function () { + $scope.refreshStatus = function (silent) { url = "/docker/getContainerStatus"; var data = {name: $scope.cName}; var config = { @@ -686,26 +1951,47 @@ app.controller('viewContainer', function ($scope, $http) { $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); function ListInitialData(response) { if (response.data.containerStatus === 1) { - console.log(response.data.status); + var oldStatus = $scope.status; $scope.status = response.data.status; + + // Animate status change + if (oldStatus !== $scope.status && !silent) { + // Add animation class + var statusBadge = document.querySelector('.status-badge'); + if (statusBadge) { + statusBadge.classList.add('status-changed'); + setTimeout(function() { + statusBadge.classList.remove('status-changed'); + }, 600); + } + + new PNotify({ + title: 'Status Updated', + text: 'Container is now ' + $scope.status, + type: 'info', + delay: 2000 + }); + } } else { - new PNotify({ - title: 'Unable to complete request', - text: response.data.error_message, - type: 'error' - }); - + if (!silent) { + new PNotify({ + title: 'Unable to complete request', + text: response.data.error_message, + type: 'error' + }); + } } } function cantLoadInitialData(response) { - PNotify.error({ - title: 'Unable to complete request', - text: "Problem in connecting to server" - }); + if (!silent) { + PNotify.error({ + title: 'Unable to complete request', + text: "Problem in connecting to server" + }); + } } - }; $scope.addVolField = function () { @@ -722,13 +2008,28 @@ app.controller('viewContainer', function ($scope, $http) { url = "/docker/saveContainerSettings"; $scope.savingSettings = true; + // Prepare environment variables based on mode + var finalEnvList = {}; + if ($scope.advancedEnvMode && $scope.parsedEnvVars) { + // Use parsed environment variables from advanced mode + finalEnvList = $scope.parsedEnvVars; + } else { + // Convert simple envList to proper format + for (var key in $scope.envList) { + if ($scope.envList[key].name && $scope.envList[key].value) { + finalEnvList[$scope.envList[key].name] = $scope.envList[key].value; + } + } + } + var data = { name: $scope.cName, memory: $scope.memory, startOnReboot: $scope.startOnReboot, envConfirmation: $scope.envConfirmation, - envList: $scope.envList, - volList: $scope.volList + envList: finalEnvList, + volList: $scope.volList, + advancedEnvMode: $scope.advancedEnvMode }; @@ -866,6 +2167,184 @@ app.controller('viewContainer', function ($scope, $http) { } }; + // Command execution functionality + $scope.commandToExecute = ''; + $scope.executingCommand = false; + $scope.commandOutput = null; + $scope.commandHistory = []; + + $scope.showCommandModal = function() { + $scope.commandToExecute = ''; + $scope.commandOutput = null; + $("#commandModal").modal("show"); + }; + + // Port editing functionality + $scope.showPortEditModal = function() { + // Initialize current ports from container data + $scope.currentPorts = {}; + if ($scope.ports) { + for (var iport in $scope.ports) { + var eport = $scope.ports[iport]; + if (eport && eport.length > 0) { + $scope.currentPorts[iport] = eport[0].HostPort; + } + } + } + $("#portEditModal").modal("show"); + }; + + $scope.addNewPortMapping = function() { + var containerPort = prompt('Enter container port (e.g., 80/tcp):'); + if (containerPort) { + $scope.currentPorts[containerPort] = ''; + $scope.$apply(); + } + }; + + $scope.removePortMapping = function(containerPort) { + if (confirm('Are you sure you want to remove this port mapping?')) { + delete $scope.currentPorts[containerPort]; + } + }; + + $scope.updatePortMappings = function() { + $("#portEditLoading").show(); + $scope.updatingPorts = true; + + var url = "/docker/updateContainerPorts"; + var data = { + name: $scope.cName, + ports: $scope.currentPorts + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $("#portEditLoading").hide(); + $scope.updatingPorts = false; + + if (response.data.status === 1) { + $("#portEditModal").modal("hide"); + // Refresh container status and ports + $scope.refreshContainerInfo(); + new PNotify({ + title: 'Success', + text: 'Port mappings updated successfully', + type: 'success' + }); + } else { + new PNotify({ + title: 'Error', + text: 'Failed to update port mappings: ' + response.data.error_message, + type: 'error' + }); + } + }, function(error) { + $("#portEditLoading").hide(); + $scope.updatingPorts = false; + new PNotify({ + title: 'Error', + text: 'Error updating port mappings: ' + error.data.error_message, + type: 'error' + }); + }); + }; + + $scope.executeCommand = function() { + if (!$scope.commandToExecute.trim()) { + new PNotify({ + title: 'Error', + text: 'Please enter a command to execute', + type: 'error' + }); + return; + } + + $scope.executingCommand = true; + $scope.commandOutput = null; + + url = "/docker/executeContainerCommand"; + var data = { + name: $scope.cName, + command: $scope.commandToExecute.trim() + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); + + function ListInitialData(response) { + console.log(response); + $scope.executingCommand = false; + + if (response.data.commandStatus === 1) { + $scope.commandOutput = { + command: response.data.command, + output: response.data.output, + exit_code: response.data.exit_code, + container_was_started: response.data.container_was_started + }; + + // Add to command history + $scope.commandHistory.unshift({ + command: response.data.command, + timestamp: new Date(), + container_was_started: response.data.container_was_started + }); + + // Keep only last 10 commands + if ($scope.commandHistory.length > 10) { + $scope.commandHistory = $scope.commandHistory.slice(0, 10); + } + + // Show success notification with container status info + var notificationText = 'Command completed with exit code: ' + response.data.exit_code; + if (response.data.container_was_started) { + notificationText += ' (Container was temporarily started and stopped)'; + } + + new PNotify({ + title: 'Command Executed', + text: notificationText, + type: response.data.exit_code === 0 ? 'success' : 'warning' + }); + } + else { + new PNotify({ + title: 'Command Execution Failed', + text: response.data.error_message, + type: 'error' + }); + } + } + + function cantLoadInitialData(response) { + $scope.executingCommand = false; + new PNotify({ + title: 'Command Execution Failed', + text: 'Could not connect to server', + type: 'error' + }); + } + }; + + $scope.selectCommand = function(command) { + $scope.commandToExecute = command; + }; + + $scope.clearOutput = function() { + $scope.commandOutput = null; + }; + }); @@ -1113,7 +2592,7 @@ app.controller('manageImages', function ($scope, $http) { (new PNotify({ title: 'Confirmation Needed', - text: 'Are you sure?', + text: 'Are you sure you want to remove this image?', icon: 'fa fa-question-circle', hide: false, confirm: { @@ -1131,14 +2610,16 @@ app.controller('manageImages', function ($scope, $http) { if (counter == '0') { var name = 0; + var force = false; } else { var name = $("#" + counter).val() + var force = false; } url = "/docker/removeImage"; - var data = {name: name}; + var data = {name: name, force: force}; var config = { headers: { @@ -1155,16 +2636,67 @@ app.controller('manageImages', function ($scope, $http) { if (response.data.removeImageStatus === 1) { new PNotify({ title: 'Image(s) removed', + text: 'Image has been successfully removed', type: 'success' }); window.location.href = "/docker/manageImages"; } else { - new PNotify({ - title: 'Unable to complete request', - text: response.data.error_message, - type: 'error' - }); + var errorMessage = response.data.error_message; + + // Check if it's a conflict error and offer force removal + if (errorMessage && errorMessage.includes("still being used by containers")) { + new PNotify({ + title: 'Image in Use', + text: errorMessage + ' Would you like to force remove it?', + icon: 'fa fa-exclamation-triangle', + hide: false, + confirm: { + confirm: true + }, + buttons: { + closer: false, + sticker: false + }, + history: { + history: false + } + }).get().on('pnotify.confirm', function () { + // Force remove the image + $('#imageLoading').show(); + var forceData = {name: name, force: true}; + $http.post(url, forceData, config).then(function(forceResponse) { + $('#imageLoading').hide(); + if (forceResponse.data.removeImageStatus === 1) { + new PNotify({ + title: 'Image Force Removed', + text: 'Image has been force removed successfully', + type: 'success' + }); + window.location.href = "/docker/manageImages"; + } else { + new PNotify({ + title: 'Force Removal Failed', + text: forceResponse.data.error_message, + type: 'error' + }); + } + }, function(forceError) { + $('#imageLoading').hide(); + new PNotify({ + title: 'Force Removal Failed', + text: 'Could not force remove the image', + type: 'error' + }); + }); + }); + } else { + new PNotify({ + title: 'Unable to complete request', + text: errorMessage, + type: 'error' + }); + } } $('#imageLoading').hide(); } @@ -1180,4 +2712,5 @@ app.controller('manageImages', function ($scope, $http) { }) } -}); \ No newline at end of file +}); + diff --git a/static/dockerManager/dockerManager.js b/static/dockerManager/dockerManager.js index 769d537df..ed03f297d 100644 --- a/static/dockerManager/dockerManager.js +++ b/static/dockerManager/dockerManager.js @@ -729,6 +729,59 @@ app.controller('listContainers', function ($scope, $http) { $scope.assignActive = ""; $scope.dockerOwner = ""; + /* Pagination (ACTIVITY BOARD-style) */ + var CONTAINERS_PER_PAGE = 10; + $scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } }; + $scope.gotoPageInput = { containers: 1 }; + $scope.totalCount = 0; + $scope.totalPages = 1; + $scope.Math = Math; + + $scope.getTotalPages = function(section) { + if (section === 'containers') return Math.max(1, $scope.totalPages || 1); + return 1; + }; + + $scope.goToPage = function(section, page) { + if (section !== 'containers') return; + var totalPages = $scope.getTotalPages(section); + var p = parseInt(page, 10); + if (isNaN(p) || p < 1) p = 1; + if (p > totalPages) p = totalPages; + $scope.getFurtherContainersFromDB(p); + }; + + $scope.nextPage = function(section) { + if (section !== 'containers') return; + if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) { + $scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1); + } + }; + + $scope.prevPage = function(section) { + if (section !== 'containers') return; + if ($scope.pagination.containers.currentPage > 1) { + $scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1); + } + }; + + $scope.getPageNumbers = function(section) { + if (section !== 'containers') return []; + var totalPages = $scope.getTotalPages(section); + var current = $scope.pagination.containers.currentPage; + var maxVisible = 5; + var pages = []; + if (totalPages <= maxVisible) { + for (var i = 1; i <= totalPages; i++) pages.push(i); + } else { + var start = Math.max(1, current - 2); + var end = Math.min(totalPages, start + maxVisible - 1); + if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1); + for (var j = start; j <= end; j++) pages.push(j); + } + return pages; + }; + $scope.assignContainer = function (name) { console.log('assignContainer called with:', name); $scope.assignActive = name; @@ -783,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) { title: 'Container assigned successfully', type: 'success' }); - window.location.href = '/docker/listContainers'; + window.location.href = '/docker/containers'; } else { new PNotify({ @@ -1264,77 +1317,35 @@ app.controller('listContainers', function ($scope, $http) { $scope.logInfo = null; }; - url = "/docker/getContainerList"; - - var data = {page: 1}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(ListInitialData, cantLoadInitialData); - - - function ListInitialData(response) { - console.log(response); - + function handleContainerListResponse(response) { if (response.data.listContainerStatus === 1) { - var finalData = JSON.parse(response.data.data); $scope.ContainerList = finalData; - console.log($scope.ContainerList); + $scope.totalCount = response.data.totalCount || 0; + $scope.totalPages = Math.max(1, response.data.totalPages || 1); + var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1); + $scope.pagination.containers.currentPage = cp; + $scope.gotoPageInput.containers = cp; $("#listFail").hide(); - } - else { + } else { $("#listFail").fadeIn(); - $scope.errorMessage = response.data.error_message; - + $scope.errorMessage = response.data.error_message || 'Failed to load containers'; } } function cantLoadInitialData(response) { - console.log("not good"); + $("#listFail").fadeIn(); + $scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server'; } + var config = { + headers: { 'X-CSRFToken': getCookie('csrftoken') } + }; + $http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData); $scope.getFurtherContainersFromDB = function (pageNumber) { - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - var data = {page: pageNumber}; - - - dataurl = "/docker/getContainerList"; - - $http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData); - - - function ListInitialData(response) { - if (response.data.listContainerStatus === 1) { - - var finalData = JSON.parse(response.data.data); - $scope.ContainerList = finalData; - $("#listFail").hide(); - } - else { - $("#listFail").fadeIn(); - $scope.errorMessage = response.data.error_message; - console.log(response.data); - - } - } - - function cantLoadInitialData(response) { - console.log("not good"); - } - - + var p = Math.max(1, parseInt(pageNumber, 10) || 1); + $http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData); }; }); @@ -1906,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) { text: 'Redirecting...', type: 'success' }); - window.location.href = '/docker/listContainers'; + window.location.href = '/docker/containers'; } else { new PNotify({ @@ -2703,257 +2714,3 @@ app.controller('manageImages', function ($scope, $http) { } }); -// Container List Controller -app.controller('listContainers', function ($scope, $http, $timeout, $window) { - $scope.containers = []; - $scope.loading = false; - $scope.updateContainerName = ''; - $scope.currentImage = ''; - $scope.newImage = ''; - $scope.newTag = 'latest'; - - // Load containers list - $scope.loadContainers = function() { - $scope.loading = true; - var url = '/docker/listContainers'; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, {}, config).then(function(response) { - $scope.loading = false; - if (response.data.status === 1) { - $scope.containers = response.data.containers || []; - } else { - new PNotify({ - title: 'Error Loading Containers', - text: response.data.error_message || 'Failed to load containers', - type: 'error' - }); - } - }, function(error) { - $scope.loading = false; - new PNotify({ - title: 'Connection Error', - text: 'Could not connect to server', - type: 'error' - }); - }); - }; - - // Initialize containers on page load - $scope.loadContainers(); - - // Open update container modal - $scope.openUpdateModal = function(container) { - $scope.updateContainerName = container.name; - $scope.currentImage = container.image; - $scope.newImage = ''; - $scope.newTag = 'latest'; - $('#updateContainer').modal('show'); - }; - - // Perform container update - $scope.performUpdate = function() { - if (!$scope.newImage && !$scope.newTag) { - new PNotify({ - title: 'Missing Information', - text: 'Please enter a new image name or tag', - type: 'error' - }); - return; - } - - // If no new image specified, use current image with new tag - var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0]; - var tagToUse = $scope.newTag || 'latest'; - - var data = { - containerName: $scope.updateContainerName, - newImage: imageToUse, - newTag: tagToUse - }; - - var url = '/docker/updateContainer'; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - // Show loading - $('#updateContainer').modal('hide'); - new PNotify({ - title: 'Updating Container', - text: 'Please wait while the container is being updated...', - type: 'info', - hide: false - }); - - $http.post(url, data, config).then(function(response) { - if (response.data.updateContainerStatus === 1) { - new PNotify({ - title: 'Container Updated Successfully', - text: response.data.message || 'Container has been updated successfully', - type: 'success' - }); - // Reload containers list - $scope.loadContainers(); - } else { - new PNotify({ - title: 'Update Failed', - text: response.data.error_message || 'Failed to update container', - type: 'error' - }); - } - }, function(error) { - new PNotify({ - title: 'Update Failed', - text: 'Could not connect to server', - type: 'error' - }); - }); - }; - - // Container actions - $scope.startContainer = function(containerName) { - var data = { containerName: containerName }; - var url = '/docker/startContainer'; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(function(response) { - if (response.data.startContainerStatus === 1) { - new PNotify({ - title: 'Container Started', - text: 'Container has been started successfully', - type: 'success' - }); - $scope.loadContainers(); - } else { - new PNotify({ - title: 'Start Failed', - text: response.data.error_message || 'Failed to start container', - type: 'error' - }); - } - }); - }; - - $scope.stopContainer = function(containerName) { - var data = { containerName: containerName }; - var url = '/docker/stopContainer'; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(function(response) { - if (response.data.stopContainerStatus === 1) { - new PNotify({ - title: 'Container Stopped', - text: 'Container has been stopped successfully', - type: 'success' - }); - $scope.loadContainers(); - } else { - new PNotify({ - title: 'Stop Failed', - text: response.data.error_message || 'Failed to stop container', - type: 'error' - }); - } - }); - }; - - $scope.restartContainer = function(containerName) { - var data = { containerName: containerName }; - var url = '/docker/restartContainer'; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(function(response) { - if (response.data.restartContainerStatus === 1) { - new PNotify({ - title: 'Container Restarted', - text: 'Container has been restarted successfully', - type: 'success' - }); - $scope.loadContainers(); - } else { - new PNotify({ - title: 'Restart Failed', - text: response.data.error_message || 'Failed to restart container', - type: 'error' - }); - } - }); - }; - - $scope.deleteContainerWithData = function(containerName) { - if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) { - var data = { containerName: containerName }; - var url = '/docker/deleteContainerWithData'; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(function(response) { - if (response.data.deleteContainerWithDataStatus === 1) { - new PNotify({ - title: 'Container Deleted', - text: 'Container and all data have been deleted successfully', - type: 'success' - }); - $scope.loadContainers(); - } else { - new PNotify({ - title: 'Delete Failed', - text: response.data.error_message || 'Failed to delete container', - type: 'error' - }); - } - }); - } - }; - - $scope.deleteContainerKeepData = function(containerName) { - if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) { - var data = { containerName: containerName }; - var url = '/docker/deleteContainerKeepData'; - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - - $http.post(url, data, config).then(function(response) { - if (response.data.deleteContainerKeepDataStatus === 1) { - new PNotify({ - title: 'Container Deleted', - text: 'Container has been deleted but data has been preserved', - type: 'success' - }); - $scope.loadContainers(); - } else { - new PNotify({ - title: 'Delete Failed', - text: response.data.error_message || 'Failed to delete container', - type: 'error' - }); - } - }); - } - }; -}); \ No newline at end of file From 56cb95faddc7f8d622957ee82e5fa6825ad10839 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sun, 25 Jan 2026 20:55:56 +0100 Subject: [PATCH 02/40] Update pluginHolder with Free/Paid badges and Plugin Information support - Added Free/Paid badges to Grid View, Table View, and Plugin Store - Fixed intermittent badge display issues with robust boolean handling - Updated plugin store to show plugin icons and proper pricing badges - Removed Deactivate/Uninstall from Plugin Store (only Install/Installed) - Fixed template syntax errors and duplicate navigation buttons - Enhanced cache handling for plugin metadata (is_paid, patreon_url, etc.) - Improved JavaScript cache-busting and isPaid normalization --- pluginHolder/discordWebhooks.zip | Bin 0 -> 21536 bytes pluginHolder/fail2ban.zip | Bin 0 -> 96619 bytes pluginHolder/patreon_verifier.py | 245 ++++++++++++ pluginHolder/plugin_access.py | 130 +++++++ .../templates/pluginHolder/plugin_help.html | 351 ++++++++++++++++++ .../pluginHolder/plugin_not_found.html | 93 +++++ .../templates/pluginHolder/plugins.html | 281 +++++++++----- pluginHolder/testPlugin.zip | Bin 0 -> 56070 bytes pluginHolder/views.py | 347 ++++++++++++++--- 9 files changed, 1319 insertions(+), 128 deletions(-) create mode 100644 pluginHolder/discordWebhooks.zip create mode 100644 pluginHolder/fail2ban.zip create mode 100644 pluginHolder/patreon_verifier.py create mode 100644 pluginHolder/plugin_access.py create mode 100644 pluginHolder/templates/pluginHolder/plugin_help.html create mode 100644 pluginHolder/templates/pluginHolder/plugin_not_found.html create mode 100644 pluginHolder/testPlugin.zip diff --git a/pluginHolder/discordWebhooks.zip b/pluginHolder/discordWebhooks.zip new file mode 100644 index 0000000000000000000000000000000000000000..f7cc151de96ab98866491bf763b762cb52345cb4 GIT binary patch literal 21536 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 literal 0 HcmV?d00001 diff --git a/pluginHolder/fail2ban.zip b/pluginHolder/fail2ban.zip new file mode 100644 index 0000000000000000000000000000000000000000..da2df290eda17735bc814742534223ce7c131f39 GIT binary patch literal 96619 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 k@*CBd5$ytX80 zH7ft5-9%r*`~OH!@@I9>C;cPLK{)>zng9PI@Ba^18`&5dTmQ2GUH_+p-X^Rc* zKY;n9>>pqz-MA-hES03MvEVT>bA zk*2Y%5L=$lx&MnMNrf3$I41ttsW*i4mt`rog+OLMx|*8RD{l&wxG6VsU#Dmc7i7*L z0W!;=0V^cOkx{T~Y>Yf)pP3u`z(r7m6%!wKsLv^BVMJ-wnVd0h$jNS26eI*lym_4b zoW+~M{DCy??HeR^{_z?Rcj(Rl2H%Kr?8gFsBi~?~e+y7Hh;!(GwzvZ+MM_4Qh~|Y; zh6aRw(IV)!{x5dGhnSAg(m+zh0VGzun7cG{&H+Wyg;5Zk7*s~mKbI=7l;iSvPJj+A zICL?*WDy@9Pt>Hr{)EkDeNVhg{Oyf|s|BMb@HSl~DeY}Y(K0=)2WD^7pb`-cTES|z z^jI=^NZ3?c*zU*t2Ez!^mEyGE!v-QhVcBpSZoXy3m@X-BFiF!UzbWZ})6mUV#95+X zWq8=Tytx=j6b6u`y=}pP2a9#P$r}MTkB5H;L2RjpqUj`0%&8LiLQ{IhaVn7kh%wh$ zjJR1EZrqZ`t0<9mJ?BR*AeA6e7A&*%xvVm3MsPX;G=LC#5zvDb@P`nD8qv%suqkq~ z4emUyTI!2ZJ|7Dm?pX1!Q1C9pr=sbL(DCFgW;v?}g_@-t>B$AilR^I}gE+{gNvU6^ zwYIeYGkOOKv0-a*1-YHNa_U{fHK zeOh3xOQ%8?T}2LSl|@SMR)YfgfW0W4W%nX0vXDWLQ-U;@GtnKS^qWR$qi;DOEor$=sh-t{kHr1u!gfo)fK>a?`i>`G5UR>~hu|J@#TxaG@|~@6Brs z*w1EK5nYoT($_V54LEZ@k7Ioc@TBlhaOL$(fShN-88zx=%*>&?Um-OwB;rnyq=P{P zF%cl;0;6gM?JT4V?+i^0VFAXiO@NZAzhG%Y8&ecYU!--2fHA`6iBKjY#g_04Fjgxo z^+VOPRdjiOM7;_rck9hIt97@s0Ta?seVzE0t+^~@Y167>WW15Ivh%>s>IDEVLz?=2bN3XEP82!5$52o#|^7&VM7^aMQ0)D-llk)etvAp(%DmkaaQjF-Z~ zwi}KVGLI*IIw(RE2-|14L8&8cAsJ6AzDvehKP6c+5>9(Iv>$`0CJ7+4%sBDUY6=A^ z$2{?%vL-gC?!cAUKDjo3aK9b`f9uxlqoN_ ztPHEPCp-j?NtZSrhA#i9m2AO_XYLa0-wq~8N)=v`aS%n6I*Ax)wFx!&V-6F_nRyrP zD6S8I4_q_Q$_vB8tTnBL=?A^)JQzQBhhC{6D2*+MmApo$zgRK^^ug#r<9~U}kVZtp zr1!ARY65_=;f-ZB0GGoEXV2>!7w5O;=?0P1b);Gc>^mN0ds@8;86Qw36VMGIq`JzM zcbU8u|Agw*@Zs25i(nYoE7~Fy9#W{suoUMoS+ zAy|)nf_PfUYZDQ8jo{T|GI2fPOpiKK3>#P+U%7s#>2nZ=1>V^vgE(49SkL+g9qJb~ zSMO`twy0v*y(O5X=F846i#N!dh++38FHuDWSoTYFCP=g7$}Low$_(`tI;E$PA1y6J zA2DxvXZ1!(S=b8AfAQ9>kXkiOZRw$kpb^o=DjXR9tX`715>La#|&Q&q6{TjE)N3grVY;HbKUTV6<(8eb6Pg&F#9n!$NF zSjy`_<*m%UvYOWJ0E+YCvrLo^M{CW*269%$Wp#I`q}{xgFlE}@{{XdXGW3bTZriKo zg^ZJi_bz>246U@Z?PW0Q;yg9SXQz(ZSp{34Bl8f9X@Z%JNhKFj70{|>ot_Phtc?bZoZ)+*0d_}LnnQk3BPX0HZ*rbTJKr?LsjnmC zfsv@;K7cf+dkts-vO$_0`4rs>;FM>bUT8sFUTmo>lW}d)W2n9$t74~Xn>0iRF6IU%OhZpn;?f z>q9(@5EX%$K`x1Fu=4B^bbi1X!J&=)k{mR;=C z-V4oTrGuPRaNB#t4%JHEv$EuiT9F6B4|Dx|`KTy8Ii%dnTrr*lU22b?dgGd{3Ci?= z=h-zUfA*NH3@?aU|J~TeMq{VbRNV!p&DwvyT4+AAR5|${tOgjYs)%&3>7pa?J)mo4dBxDu4i}WF^q%{ML3%5@J>YsIX)$09RzTu;b@DjG9H{D#z z@%ulbOh4=jwHq)10M|?a05tza?EJr==o#&Ar>#-MUwwJUvh08u$iLDlCQ>TcQ))*s zxZX4!|44LYTz?`DG>8H~$~4zU6LW4WbFSqkf7ZpnPEybh9?!rR#7_5cZkzyM%26pT zE1971uAQFmx0~G{zRuUTk%{*+`uqaV&cE^QaqHj@jttoB67vSYAl_r#5PIU!Nk+%B zjyVY~+aA7tR`@JW+JJfqjz>{RR}?WzU`|5>q<}I#WqMrr-p5g$!>9V25nwE(>ut#B zpC;SRok=QE%K12b-r;YcogJK)5rmH1lIqu5er|k1=9F+e*WrMIhATHbGCVBOdq$cd?cBD$<}OUTpz9=fo;Q{` zpLviVEHlZe6z>GWGj&-mQa`|~?kGL8H`mLV*ZZJ{C3O(Yc9pB9TwF4|(JOKTD02*D z&^G_Nnzen~v27Gf9veFhRmwB9<`lyQ(I(~4cio$1mbi2_Hmy_b<{U^|>LfE#MXnor z-8J?_)$o>=o4^uZFQSxOa>0Z40O&7Z%2{Gy#5nkSG5B(ffblOkSK=_p4uRGOXG@+d z!|#y8M@pAP#pfPMCOI-zh})4u~q(Nf$`*y%XTb#Ro+A>V(VLyX+ z*|zSe3F?7&QT7nIlhuca&MR+lhF~{APB;M$fYgQ>&-HKYQ-R5|518tINZ|<#xk@xk zdLwCcfiu}Vz$gH6%roc-{uac1waS<62Xnre4>kOiWz<`k@=*vAdqZAt8n|GlVJaqz z4}oGngZi8jvO*UD!!Bq8*-|2z!-a7V4}#YiG~4~`!R%ZWYio58v?<2@&|V+7SR-LD zg&G|*lcFrcY6cBf=EY?smeQ*0i`+OF+_{=!}%x}tZ=n#@wq zTk$hPDG(Nlsc79~Q8pSiZ#JWf=joenH8DjI@dZ z!+9<1GaoCToRVClk~_3jT`IOK)sQXlEO!klSr^%Cf0GBbUJ_$5T7D>JB_uMC_yja0 zg+G+aUEW(&5ao?-p{Ru+Bb<3;cvVm^86!~#Sf$W7p)RtZSOU2~ailO%)5X92N_BNm zQldjEt#1kO!?Zj=8i*{gFL+{9qscmGjqnlxdbaxq(9J?ZwzI}PxGNQQ6_~;#aYE0Z zHsFOLfGNuav6OnXU(u|BIyBYqMZ+_>Sa!q4vEI!8S3r)l5#OjR1-7dC)n?Tkn=osTsIBBw7t(=W3nAWUd07te(|f<< zPs~*opAI5qs*(L$mxj(b6D#BhgZ{IbRMiDF>s7RrEO;bitGMwYls`BwExO>1lI_VR zU2Jc22B%#EyD`}Y35_@EJinxIQs?8BU1gzD6R`)Lt4UW2@khUS^1SvyxOiOxG(uhs zHrUdnS}mJ&3s#f&*AMTOSU!aDMk`zGALQ0$w0p?M-FtR@-`Kofk@yn6d1st`Ow9l` z!F4@LK^GdkCenia3=&951+FnlsnY|bq0)x4e(omxq+%50u9q;W5nTUSR9+O%^dD=@`_Pt-${r;@s+ZP)N zey=W?0qB2d4Sy!nc&-KdrfpA2%%$Z;o|1AI&Peu41>(dgNIa{&mIUf$y zPA1JU4U%a#;nbX05!KrT)-ESQ4ETMU>O425H`CgwWi5gVlqsC3ag(;6o*Ho8<QPTmyhfmJ;-*{`Z0+F?SQd8vG0BbGGuBaB%Ak|g5>sbxHCy3VMu;D~FARmKdP z!Kl)eV#Xu2lPP}U==&?=J|X4#f^jr~s>5T<6?!DR*g6wt$_H!dabt?HF8H!>fwn)y$Z^xs>`<$hQ8C#Pk=!R%c zz_`r?zv8oS-akR*u%aj9onY>oOStxJa;ym1V*alEh~S6 z+`LY1Zb%xbA4%a{5<^dEv3Z*$ugs@iRl~Kocc&Rn8p>dyhK;`EfMHeEwzjiCdD%od z$K;|Dq3F+IStDLGQ$WV_E{oN5ON@s9Uq!+|Eb={qL-La8ve{g!LYN!vDMT-Ihi#UqWr0W4iuMZTta?yNgJf3#CZdUz5B7;&?9We&QdjQTQzoZN&*Q8;RJ z5K-x^dJ9s5zd-JBStM~o?W#`O^W<=S3(a};@W<(f79e<3X{RZ7V%FN0*LVKotZKxS zDqEBySO=8xvJ-d2tG#jPJY-c^#zntg#0a_E~Sg)`sV08Vj zC+&#Iq8_4hM-GXfMXC0KR+w(TQFWA%3UqfYiF8`C*$H5>Q%06=J=?7YKSM$in1P*0 zYT2BRq5Wy8j}IUfpP3M%%3}0n-(-d@fyYyQ9E#KY_{o%dAC)pXS>DybK=n1KsXB{(E0(k9vC#8{k#s?&8+2YDVgY*q zY72&trk#Vy2BJxwk)WT3qtp6cg$3NX%rA+Oz|dB{AH;MbBr1 z?TAYM7z9>c0I?98L3Zj{wi1_P?}(|Z;`P>C70pJGfNre+m1pvY=`VGzQK_XN`wufd zDfBiz9X7p7FoD*f4a2FprSRx4#A8>$DI!Y_8w|Q7^udvYVyYzg*car~a8;{k(7t#6 zhz7d6Dx^2nkOLuO?XuksPf|SwUbenhO1gsV)iSPDHKW$JvNGnkeHp(}mzN&|1wi;D z^$S(bl$GRq$r=XHM+>)~RP0FZDx?EpepM7_MdsQVnv8qvqy7CK%S{%F$ng;W)wQUQ z{a;zJv4x?Xxv7o4`F~}_J6g8RTkVKHdHR7%GqER_$tb2%jyMxb!}Pp+Bb;-CHM)}yF3E-+2bPy$6*^1L}=CgiS1QwT_d{nfdrK|xW@^w28AL*Tq`=vC(6u&h*1wpcTU5BAOeg0k!hfI3KlSCdj#wTy+f)a7r~x~&c2m7= z9!z`hAamY${2m4JUO-+i62K?SBTv@p^<2WPM!s7~)CXz#azuE_bhgnIM_urZK;M_a zyjR)7hC9Aa!RayU)bs5ZWmda`eZx^6NXApg$H#*|>V*T@?tTsRb6}e#L3#^yPuEXo zTwwZnGI{q{tNHC^nD-4UpV^%8)qyn-U82BPd{!SZQ#+80uq&3>E?S} z=F8}ISrY|kH8l);BqpNzcOlF~)LBV(iL({M-m4ld%H%vccHu~RWYQIEjzrN(vJXs~ zHLby(7&lUq%q#(Z7N@O!ao#K5z-u+@cTZZHAZ!~A-IPWzh_C}+*GhC*fMu7to zgMr?3E*ci9reeL4Y|>hm?L#pfKa+2Kg*N!I1$OpwYhPxKI4D8bxnDYN38Hrwi=~g5Xikh&R6UDj>o~_mm90r> zK-UDeqA0VuI9kO{x@oD~%*>iB-{ay*9~uHf_p4(X#L%6i^$OY;&?)0;>tS%?LfbSq z2GQx++_${gHp5Pf3U&SGDYo7tnPQ^l(wm{45f2-tDn3maCp$_L znE5m|M~5?TaM*fQ3I8nh_|l(^SdUHR-l}t)-*^A4#Mss4eOg8+bpqfPB+XM@43(`9 z6$zB_vf5fSlP_%`fOg_NU&ALXKq$iFnQ=Fa^~_K)P-lL~K*`f-<%PAUhDnE37iMa{ zh(!ZXxQU(woEwPz(thr^6lG} z#tRH347~0lfr#Lo*Ct*=Qu65qpax6eGbIBS*=WLh^dGh8Ekjoq{AcJmZ>Qo8v(XK~? znj%>yx3DX%3C_dvtn%s&Zk>$qg>s%qKsvQeauFq&o7PRYme6qra0auQj48+6XOPFo z2i^yO0H6jnIm*)8|0&DcenGR`Y`vW|nwlI^ab0?-cFrN7Qt@H|9g01APiNMOj}@cFDuUE&>qMV~FS{ ze)NQxn`th7p6cn9BAUm$LH}O2A49ZVm=3cL9x{CQFN{tuB91Mwv8{W>MS`euD=<2{ zzHwjCRN5}d3NuAhumVED0fJiY2r_Er8#l|T*|(ad zqnmlRm)$pTx3`|P{!9O7qRs@gB!IcXOT>((A=w5De2XvK1bkNBaa<~49Tr-D`+Q^W zI=aoWpvB9K-z8bjv%2GWWcBfjOW*%dO_QjYiyY;=`x$Pld{ALHB|i{t{K&2<8&bNHj)*4S}5K_7+@7m;SCjw1)bJ8WcDxIOLqc4-@JZx7PJC>?DL@Puc`4zdk1Y9 zM}VEwW{$Pr9tny9rcvv)bF)C@CycJPimsU0N@JGN4a(n<7CX)7Ly#{vzV?s5KTK`6 zxZ#q!1B#*1j_d$YqJFb$>!X6U$h!=!E%_Z{vY(elG7=7J>#$=Z;AbPE;lS3P`hTvW z46u~~9DD?_p;vT&sh8y#J_`x}*h!=dmw@$C8xnxa#l_s3tGeWN#`{{sE*q4RQB zOYX-1G&%|v#E=VrJebIPru#pZaLxn+p_l-bt?5hLvju3ZY9_s zCNWtSRipxP%u%Y8*u&kkj%a2+%G?A%h-ELQO64h$nw;LQbOJvPe@uyggdf3Ac=lx5 zY3^-i9(a=E*xd+eBmdg*%vmmkY(!AyXg1#m9smV9FK?+=9tK7X+0;yT*v3(3-xe-Xd#xw6X$AOiY6p$J~ zMZV#8eKP?(FNQ^U%_-qaZLA7{HFiKV(DRdB3V^#J-Mt{OWGJPScr6YN79KrM+}XXi z^n!$cy%;F`Rp6aAD@L4Rve@(Hz`?tGvCb09DT>ed{rk8BymGsA9#rTu_6hzs-|An7 zEbWCG$|$b?tAo5S8V%7t?u*0rT8K;qzCOG}{xzxnIN(=b6YOsZy71<$J>q-c?MoR} zf)alQ-~s#!jw@HeXkr%hyuf*HHX_(*amYKh7+{-n)J~+wi<_LO!j#2khd>C;KUDl-LliAgJlG zM8c{YUsmt5oOcd~CDfSH`w>tMIhC0bEYFkaREBJ=Sy)?hfB$HdUzF3bXH;-)zXmzW zD~<8mU>}x59+i&ytNuB1V5fq6N_*S$A0a$9_S|MPBaE3vm>cHEU?G(q!s{UJE%1%frCPdwaoYZsv0OvTM!?E5^i7F_NJLEv| zn{&2t_loU_6wcw^!r?g=QYJ%X-Qq(f8%;02^_@%W&<>8kcYT{lm@pC*^?_bc6$c2@ zRRO>=W?P;GWSGfg0s2D?t1@G+DIE3gbR&IQS$nKG*tW51;n6C%;GMSQla@Hf@=VPm zHR|mm)-4XXEQ%$OAKK^Yl!itdW3%o$c0EWyXw{kkY z-mb!hNj?PT`;4rUG41y_vkRn80T`iSUw5oxL)tm@Ve{84Q+gz-V@Q3PX~VYx@JKss z)^5biuh4?L>jbwLh^Nn0#k;+9RTo+FH7bm`wEZ-%*-X5%KP+~|nDHY4PLpDxnrHRk zW$5P&+_`%YPYt=$;Tru@*U(gdP(?KZeYOY{G@V7CqB_~cxI)rlV;)!FnduOY7kdt~ zT;O())U9J~57&ZGr(7 z;dd$ROlLEPB$mv^B{JbQ-J;?pmlQ-WW%}XnF*^jeZMk?A{$==fniMb!jZ5el8hyM9 zhN@PVuzImr=#)^FY1EnXtL-urA>btW&!xH5K!sHG--sw^_x`rWYDiK})zoY>W{aM3 zS1FyL^*+GpikLQ=ZlkokF%;6nyfOto9mjxbb3=J~fyWVI$RY{nH38N1dp@2z<=gWZ zgfk}=mLR=Bc)~~%?qpA!`W8ksvzi`Ra#c|T%Z9#o6ZWGNxdDSWhXK7`hqsZ7k#>(&6$ z<$^Xn+x8@mW3$Q9%RMV0Jto49*EemVnU&CO*zsPCF#4KJ>IL;Cci`$KD2p`QcB%8N zavbC0@Lag6pVuxX*^E*dLDw~SXnP)0CZET1{$H=Z?{)r!mplW-c+DHRNy*B;QDVfW z$fA%IJ|gt%D3LvI1V0j(u-u*urY2ZJTqx!3MN8x0X=Rd=WHKH_F|lTr33~~Q*GJ}* zz%G%326IL8CSsZ<)gsvoBQ-^-o%gkxLdD7UBg2tABLHR~*k_-!!KkXyXpmn=X&GV8 zKmCD~P(XDK&2myYY7F*?wg#vvZE|v+Ors+KzzLGE7XUg&NOP|C4hUjkdVk>`)0SBL z;dcMh7*5+Ka$toHoICv}dN)Z4sM|fVAfV+%)eo$m_CEHQ0Y07@a4rOp#e)aaSFSz$8#C6}<=WbRUZb|3%SkugTk!SHas<;pav+^%zR^+w~h z!ua>>_bq%x=O-7aWp)F`P`V*uqDY-KIkrqGP>VR(&_}VZrh5p8|8Xwb(;H8}Mp^K^ zQa++X{COI42d(}sK3bD!seg5%0*3QyLnZf86~%ZR%;u8dRZfQQa%S*yL>>K-YSo}e zQ6ZWr3_tE9VW-zBy=?rDnue7(FIM^YAB(^F>Xuv%?o)>DTqRY3*1>Sr7Oc+kUH_o#yD=|*q2o8 z-H%!ANK@{R@gkXL)>6U`g+hHMp(BgUOuXidp^TBaK5)p%$3r*_iZJk-PGkzx8(haX zeZSVpGZp1Xi=5u8eC93bXk3P<#(dZX;ENmUMmnqmagY_7y+hSa8)_UkeyVqnHXErI ziV!owv`Y8J$DtW(+W?=s=`fXB8%8x^;0s~w zNx{&?sgVQdBSk3C@}gFOV67MomX5Kl!u<=<}W$;hirE(P^$<+lnz@ z9Uc$}X${WnGG@sfA#e96S`L{cowf?HrIdRXY`8&rQE$C4YLjSN`U6(aRP=cnBvU}5 z(SH@5+FSMpha$)R*uF{b_0Rnqi{!Zzq7pbr&g5v=8dS}zurHxS+S@qA4KWf*eGpK5 z>--~!)CJz(i7+atFLiV7(tZyDc++=R)-@Lm)$T4?=;xyJ4~)=t1YGS)$y3FpPQRX71-Hf*_I;+Jv@yys*| zBiBnh|Mx*(wG**9irdtrdYS~aIvw6~g8o$C6#nyCYF|=Ys<>ssF*$Y*-n%IV{D8QA zWmh9M;Z)>y&>@h63{R@iecYR{BAGI?ugrtXXdD~DDzl?A=NqKF3}cc&P1C_x@kR>1 z-OG2m`rj#MjaUam3vc<}cdc5-b`8e8EutgMVdynmR~w>vf`*U9G;k(|5tWfha+Y$l zuA~C1qhA2Wc{pwPLG@h7*sp|Ap~o4@@qPvH}z@o|X5MI5JzkZ7x1WtDi$7 zfl`L%(|TEB1V0USNlZAe!BH2qQz^z}@yEY0){i9P4pMTDJh1)O8e7U8N_8v>Dm8)` zwtVXzUex}5sAqdmC@JC+QihAY(g4uO8xSO6kJi*uH$F5;IPNMGt#*@T1`D?G`wuWm z)eSBB{`w{{{C-oFeYW3!y_N!?ot43~W^z@6#9*vK7kH|UV@Jw5MH^VkNr6$6mjW24 zq#qR7*3MVzdw{9uAb`2^T=ytc##VvglFX(Z+JU9_s=D(&VE$Qn@ly3~R2Kll>C2-E zQ9R+m%ji_b3m7@OvD^h0IfBlI%eAYb*>L7-9_hn`QKkIvW1KcAHVn3NSGMc*zFm#9&^wtRY8h ztSX;nda*!y5n=8ye^&qmyA=vjWbEKG!+=Efexw`7Y;u%T%Y~=$_{~JwsVMj78x&MV zNsZ-1)do;U3HN>C-ix3Paa)D;Q|K)Rp!bLXOeH>L53WrNV4WAuQP;1W7-)NbW4#gi z&l&ce=VHx|2=;2vYx;-^W|vy#*rlnE!D*)R|2u#8;j7{&j~qpSH0MU`g#&QBtT`E| zGciz&)$OcJ(_$<`>IiwN?M#rt>ftwzsr6mRT{P+u{2~H&&Z$z?efq zF{xvS7>Hx)Rb9| zTPUm0ZnRKUAKwelEh7uKy;Qhe^f!ysq|9GmD#UJVM&MKh_44^uG1ji+QCHzbWu-xO zWM!jc4;rzomO1aLa~kJ6w0cAetd%AI-4iW{9iIv)4FxB5TpDteG}V#LqGz9%#j37) zf7H+l*2=O&J=UrULCf)@^3o_fi(4;KRxX*Ok&eBmX=l`5duD2RunkV+al#?8%+jSw zodpQ=x{XNFPcP4BoTaP4Za{3%O0a`irrP8()M(vBGf7IJ|8>=r35F_SV=DZ~Rig`8 zYEJ523{$EB=Q<%*5+jVJdw}HR<0Jd#&bC5`JzAZJ*61H@INCQ#WAdLBE}_XUeM}E* zsEDo`J(U=irie=B^8kEQj8oS=5RdAMGR~!^$U6wNUoVPlnW`%%_6!33S8%T)FW8^1 zos$6_gb+e?z0clNH3a#2{V(o$owmw5`JEiUcNjs32h$Ep^5X;V*rS|!Q3>xS1(zhL zhtgYhn~;wzzu6<+KQbYmxodkhjO9XY{brY<25j%5pP~{D8ik!$R22PS>rG~@x3V(s zrIr_=j^(5#(zDx@OI~}ZRWbpQKfLDlZZnjF>+oe{bwGZosC=ztf@7R6de+Dz*w}rb zYB~>fl#J>4)?hS*jfD=p8#{q|7SWPen08M8(0*^}M~*ELNdXO$baG8E(J828=>0`W zWwGO|SzFLiOQG^)XQj3n0Na!$8gRVPW}*DQD0_!6QKBH-wrtzBjZ?O5+qP}nwr#sk z*|u%_-0GfnzrL@(n}a>fS$;brW5xOdwbe1Z1KTApKj<`1@a{igOUKx@NAxQn+^rZe zVmz-^fp_`jxeCPBp{Dso?@nC2xMA;2u;ie7C}|2Oz;quJ@aqv#I;y#9uuwGyU8=F5 zo_C}7d@uwG*!@67`}ArEi+?7KJL` zoLz<@TTdljnix=8m&NK|zSM!aZGzY-bw!l-VVlk3I< zl{u+(d~x4V`Mf8t%l)EObwe8abiqu{FC0h(Kd)wU=6P1s;z18Qi$4Y*s5=ll;V%}s z!TYik$YJO!=mlj&Zq<6PdH`B-^0cd~dhigXjGi4hAJ5zzUfM6F8Vc6k5xyhFWJIqr z`yA8BZ#CG?2t|d?7uE;9LYS>{pbP?jC>b8yk8zBAKwy17(mqhaQ>zNT6q=G z;GTASEMmD>0SmZs6X@mmJQijhzs0uedk;OXkCbbvZD>isB)g1TcVH-|(1X)IMPtqU}GPc+>WNsjal>@QaZy0Gu^yU3Ex=a zk5J#k&CN~GP2%ywOWm19(EDcjPSx{a$Sya;yVlt0>>7%+`NLfbRI3~UrO^;+w4$gJ zS&!i6V-WQS%q2zL@tkZu;_Y1}T+)8?wF8JB3X@f4{!1G6yj7d5Gh+s3Bud&u)i zhXgMg2vldf-@vHD#0JiH>b)~gvBr-(Qt-tK$G)1j-}71zg9i6t z;fS=p?EvFJpC|DpI1$d4O!*-`8fAJcS7iFk&6#Vu{e}ira}#A!+ph}ND9x< zvtzAekq(^ugri1wi!JbtBcZ<`lV-a;-Y*=vfzU!V*+a^rhifg#wV!CZ{deGBdI|`P z5gbT<_x!yRMNd0%|D8UG^-AA_{H0H#NdK?&iLrr`xuKnbqw)VvpI~j;ZjL?c=rJmE zQdld@Xzz2tZk>!K|KL~0DtFx z2fQGAo(M)3mv7e2hfUNA&rEgfQpVtMe|~*k{O({EbwC73*pTqa`xmXZ%NBNO6Rc$| zA#{rpB4TtnO(7GFKD(*WS1)?{QV$YhuGUd@L|~%395sr^L&WaL%T4{o>nNpbSdzh> z4~L2brgm`PAJy5EP=KTYV(RD-xd7VUj``y$VMlIAS;v)EAQeCi4^BfwG0Gfx1Q%m^ z)+9Uq!U*fBgrhNjF;^qF2dtDDCxk^4Ha6&hgFvl878q>vgtEuiJ#bUf=?6%@S?(ZV z8lCS}hh^hDP%2%|CTX9n4!OU?r^Epmt?ndf=ENwh{J5ro6JnlFRX@|WQGu)J(4Nf zU5u_A4p~4oZNcS~4~L^&teM*FY5PWuE@gjYt{0;i`oUG_P*4+vkVd?ZvL7RSwKpl* ziz$U*eZu&l>0r<$A^35l10=p8BS>x;7}Pj22mb&>J_4{=n7^8E#DLc7?Amk_Zar_l z2@n_bv`M6qmTV+2A<^5m@EjMoCResjq6oG*-%xrywCP*Kbcd2*LMQ?&6rm6EhXKqS z74_#0M$x1kM_`rv4ha4ZACN>GjNw$yCD~IzCqu?1LDCVCUD=|cPM9PrFUzuh<0CP1 z6$I}?ktTT#4DFpT^#posv~#*uRH1~;D&Cg}H-CEA(vRC?Jim)tQQB1YR!3{|SC4iu zWEauTDz0!gVvYgDKMWJp=}vYslV&)cqkZ=$fLi`4>Pwg-is}Q&8U~6em2yv($paFc z3I3i?7?%mcqsD3>x~WLCrYY)%JnC!xY?w^4;BuAKxR}j! zliV(lo z6!4t6suH#Ut~q@MQ7y1Mti#RSK;Fp6t?C%;(61uAEspz%tuT({cXs2H9h1sU_Kpq| z&H-I36-$EJCe1^QDV4gdS0b5*MNk>EYL+4q*&)apJF8Pk39x?WjhyZ{sE-(zoIb(b z;dHePTxC3hz<5=~ZzRh!UR~wF$aZt%u9v_gq5>RIMf1T&h%s88cupr7mD|z^8A?s+ zS_3wEdzJ05+82n?RC;f|SI1C?iInDW%U9egFf=yZQ(%ajN$7~M1)dPFxAK|H8MbT+ zwMJf$>tRpeE;XTqh%#NIBU&xZH|?5Fbd!3!KVfwM$|gGW462^LrE;ajV4BKZ^VQQl zDO&9|I$m;e*kt3x|6#b5{dtVL?G>Phv2m8z0!`b$0~2HT=djnN6&OJ-*KLK8!shWfRtA;hORVArLETy>wZ)4 z&;B;Q^=b3eHC^Cfb=M{V&_z4=7iB7j-kI+ddmBtWT^Ti-JJN$L8Qf*!IH~M zePYjyQu;1yt|C7BCxfljKaV4;SU`a`h8rfLI%E>`h8H%UjAlcwOATBn(sCq*o0!z~ zD>shO!HzlTN4;epH!I?hbk|lvHS#*NN=%~xG2(l=4CFJ4-m*62(Pd>+w1%hAQ-ed6 zRpItuBfv7Df$zelnr9{o&Xo%0CC7UuID~!gj5LOD$+w39Rm$zS=2|CByR&OzV0}U= z$s$_HjDCbg4#|fVEtHeXo%W^$-6Lk`L;~S$mPT;0xr-+aC2l_3+T!PuXhkgz_k4b z7CxZd-%haQ(bS;3XD`i#RSJx>h4zGs#AvpI!0LPdRXI;*eZCI?*0#wy!XB+zU1cJT z1wh-kU091n|K}DNEk&L(eOLW74V}Jr4IBw@EIQn2lvjSix*hU0MAH^E|E95v>oL8) z8n|Lq`lnFSdR&wlJWWR)pH*$|ys1Q0VoC%ilifuWb&@-pMH22=$7b4-^mh8C1YH|t zkm&dxF5qX2(-LXsH}aF1)D*B`S+wb1+c*EIAtJ3a`5~DQLw%@D^jh;I@REsA0EV&x zUCu%ncg!h%1Va7VBVTl*Y*J0PNrB>TOe!t@Zt?Uu#&P{IKAv%~vc^V~Z0&=Jl0x}H z9YF8TA0KWNqH^ldTZ6)lC`;AEN{ib1b-)NyhJ8DdYUB@H;pchFNKbmRjYu4&m)Z%E zqM&g6WWgnb2frl#ZtsgdFH%_gG`hL6JjN0i~$XntgkDy@{NTC0_CAMS>9A7>@h)4 zcvmEDVz*1}8uYz7%@;4xBGaM-r6O+|=QqPNfLJ-g>bJ=%_PP=u$IMAmZQ;P+WaaNe z2$uY7&&i2*%~stQ@4!`MKjl!m0=lxZywML;MNZ1d5Pejxhcp%qY9SDDqBOsf-bsui zb3}|9>VI+_OJT|o$*FkJ0X3o;tdtwVAHIl#vII9GO%ADU==j2xlC~589Ak+qcN%( z)nqwpSzngB#J6<1IZX%;kzXTYSS3suNNx|T#~^+jo5}NzCZWg;dkVRb?;;Sx4Hgb! zxR3fhMZR9odjFL=$#MH^WsTm|U&zm=zl;8z`_un|{F2=f zKa^!nemz7LK|K3gU50i0@~@eU3euBaB4;)tv&H=e!s5a6xUT1zJnH9gtJmuA5eNwL zB$~1u($(%Qkc4kBC*ITO*E{>v^wOo(gL53ap3@*mo!w`#w|mB$ssVCoUit{@Hi=Ax zzwwazB27n#Ev}OkMu_oV)WjrGOpHBa1;iZ=L*^|WO*j-0NQxf)*tbt&;HtUuWM&VX z_`|vN@bqLbu|x47r+fUQ_GR7DYp0c1^ma9|XI!1kp|e1GaIosXzO;pTt`_YllfIYo zVGQE;j0^Em)!Sa*HcbQ3aZa|ic3*GjWb5~rHC6YwUn=b`dnHla!#G+;UB7>8$6`Jg zMj@Uj<-8@r?B_K>@+F=JZ+ycR{tF`u7Ybn{h*l1%mE>Z>+Em;lY1L$OOC>`a5Wb}0K+^sna&C_y9m32mT-ctA`lhK=U8=Q@<>>hh|uE7loITbOf;J7j@oBri3LvX{wd`xgM3Xc$CD!@suKlSCHc+vHKtNj zXdK}NF-1aS&Rlm+5Q&`)(g#-IE=zlP0VubNm*Jall(ncv-I}OA8FgTUy7Xj6aj~kY zM!;(;a!P#zw4G@`ZIG|c#G?p^Opy(loriItupMFKljJA4M_qnK-&~KR$6h-RLKY0S zhGS4~)e0mnTfT^qtaQvhXX$J$;H>e0Szv+ce}C~T z|K6_NCx6d+^(rG3)J&R}HXJEzoH!WWCtfu(CIrLv7d{;+SkONV&V3jMr$2_3?4B0hGojxByqaY=RA zFjy1|v9+dpV_!P$De{iPH1E2RXV*{m56uxqCr!Yd`h3}mW{=u~>7JjT`e-N|$imJ# zADwWM;$RV+reV|&m^R?lfMQuJCAGDpUEE|CL7gdf)+7`}f@PiQior!_9l6Fk9w7_$ zs7#*dOao3!QIhDvF4I%3zbdt|@DJL8CYE#%olztrRl|t%>3xJzZJfFK0Ls9U$$i~A z#h9;k!jphz6Le4)%Pl>u>}gzqr5kmOYCW0%%O4~qQN}^ zD2);VLj3^%g_8!`gv&piXMp3a@In$0@(DX%t1z2OK-Gi$vz3f%+#GhaOiFS_P4(*< zj&9MW)R)U&B41g9JJZ zo=*#5O7s@!FG;}Soku$6Ig#iDXW%^-h04yOe`;Q%ud(oja=kk2V6fvd*t2j8W(MXD zweU8fT{|9PdP3lWjAtfh)sxFnCwy-%x@zu;+BvKjJQ#h@C|Pgq_CVxj zqK?}Pqyo)k2BU|;N%9o<5dB8uWY)SI6$F}`&|H?XQp9)*X~`oM;urvWeu}{|akI$+ zWpil#6Y@DM8ecn+jT=NuY#?znBJgFPuZe_KTn&ZV0sRzmlWZafypM*j$Ma}iRUf>3 z?nuEhZbnX@3*1L)|D|5)W8R+_9iCS?ufgY;NwUWpU{QhCFeX$GK!@A~4;aIcX2L)d z6w!{-EM@Ibuf!;d!VodgnmMM9EgXodzN>WLme5L)A)dY$Ay?dU6w$H;EMM~`GP&5AUL*|ct50hsV8tiNS>O{Vb>)*kSVnN6?fU2nFc*GEV9`0(4Qcd0A zZ{47)7N?d$jXyk_bhx59h3inDLaf`i0X2$>Zbnd=yL3E6sUMaFqfa&oE!qa{ebdZ;AFI zYp`)k9L4GR35zeeF%5)+v3VggNfxA^i}**CjRCCovAKl9*$5cOupljTOBS; zlmne+>*)W6oSPJewEH0T&;EWuZ)%17YHK|;WLy0;ou*9f(}m|O`eSw>^D&Z^T06rY zLS$e2Y5#s6f@FP}Fn4`B@V2O> z%Bq~2zQnh{Awu7&w=^X#n$=Yzr`$-ocN1#1?%LHR@*u={?a>?;n9ek690l~~`P`oj zZ%|y1Q^5m|xyVqjG-QwEBvqwr0LUIakZX+Yr5$L5PI zWS+5}ltJc0o;)@+*ANX{b`s|GdmNdx%_K#@-Rf(+?7qk7ENYjNsH0MN)k|EZYzr^5+X3Y|y-qM>!YCnWt zaqo#ZS)yb;>p+GShk0kt{ng(nkKF0=$zgCzrJ3Md(5EBcK$W6gSPb)e-r_GxuSqX; z7%RgMR_|hSX_;jCNi5?+M;bzPuz_?!|%Vpee?=y&X+>vhc zA0v+U>cIGyDFPe1nMoVvyg);8k44U37;@UKAE;DmZa#Up&5j1)kth|_;hMw51~(BGhwXiU~2^P0>P z)?LfIl_EV8wuVvg7rLXlRj1#VziUZUPRrsQI|5=?XPW?4T&}z$&K#no<(>Q0Ay^H% zUtC5W)L7K@r$VC$Fivw-gL^oFTN*l@;is)tr~^c^QXRw8F9egu2wkG5HHj-p9ESk& z`9lA}v4FYY!dSuMv#t7$j2DEyW%@0KAO_WQ40 z*?xy>|JH9;Hp&J7K=J?6mHodcdwQ^S><&loxBCp`(u5Ta74cK8^K2@5W={gUNI1A` zf(rX<#!{NyP{tFfKewWGH1o}&QN$k>@q-JUlC=g)}L<;l?{dFDmro?!=j_{n0(tHMm=r~}rmx=(fpSNs9 zCG8RG?>F0odE=Kas*6IGoT+N>IX|zw#b-caeXxWHi5YVk1DUzA6X3bLZjvAJ`3ee> zY6Es81(oz?G8RDLYvsa)4mtE?J#(O1*T~@k{yjTg2-QeNjF^WpO`ndD{!I3u4d+OYN-HaR#47&{J+@9>fimkW}IwLRM zjoCdLDP)(O@S_$ZS2$=I4k%dcS>DNw!WIq)5WX}P*e$;XEc~{WH55p~gqIUfn=1*g zWYV7Q&|kN71Wh*007@|kh~a!LqAm`)N&x(kFh$_LQseSueJv8saGw^72~C08*!uNzK+z9@nOjmqkiE(at=V3?z9jEs zq433BaPVGCB%PuCZ&nAMhV?N+6KaD0>^@W;7@+lxfFy>@`m~G$)jbC@T1`R{Cm!$S zPvwxZn{)GA1_4>Uu?H4u{r&@EexT2~BfpnTmL0%j3R|_dP~GNkm<{I^`FGF&Z{#N3 z-KC=3os_E;jF~OslQJ8e$tXlT9lCvbf3x|oIm0>CUXL0h7iZ2Z8tG7SQUt;)zCK-H zUQq8snll*9{$$TXYvC}kezAC)&9z+bVd(w5;n8S5c*L2OeDgw4JmADh_=zH zxqKuE8?Z=LTt7njbN0f;CYU94APxl+fvFoaT;lQ%ePgO?Z#s zye$*VTL<0Ku(-zHWBL}2An!udQYN^Wc-^tkUFff=5|Le4V%{~?u(h}z+2L~$$gyOm zo6okd=1e&o-8zsRJ40?=LqosR5n+frs6>-bkI=2T`;na{y;z-|0|$mI+$Y$?!lQcT z2LI>ZCv%3a_FTbQy_&zVYrp%`dqNpTVNX3l`zIqPw%?jN4tgh_5VuMKjH;lm3@*?! z9hr2yWAMaLiCkrDLp$?j)?=5JI1;21QiPYrGrFRk^0LJ^;w5*pRBT< zcRGsh#2z|M>Pb#dP7LD2!>p+dL`{XA0NKR&XgR8Q5>Ml_N-iB~T)aXb0(71>!3>fi z1}Qh*^`ti|6`kF*CWFU=qk^&vg53m#<;k>_)K%s(a9yzE!O;8=imjY$7d9S!bRbB3Y4bqmr~l;%~f3sa@&zEeJW405ApD&uKI zS^nM(Dl$uj_qPODw18=1lerE;k~$>{5{0dhQG6jnIg!DTcDkfC_oaNv?L>Yn;~RRo z%V}I$!auCuZ(oV-fHI2;Lx#p0R@43yT6~_eGrJBr4-E$!4$%(4a*!qj68;``U%P(Z zpRDg7D`c}4;%3%>*ihOplJWO%1}L)c))@>yR|D%J`iYFNn1n~7ds1W67!m{e$agi1 zlu~oNimCe0j69daU~ahjcHL^p2a7IMg|dk*>_s#(h~l|nNZ}ww#<}(9@9_y&6S4!}#qnKR^w>0Y>J8eu!(VimhkWx^?oS{Oesm)9`V z)bVHuJ7}|a^v1nIhg64J96vXDo1DG`6-i}zyI6fpZP4yiwxc({1B-80Tfc<)Ue;r< z_`4q9Cawy$A|I_Ou=%S|w!7)PR%XxKjy$vaejCQ9b*QnKo~)*?FkJPSir<$=LqPPh zAH96u{(R*5{WC#FrM}>Ds>1a?x0>JOTf!91Xp^ zTIye^u<>Q+4JRm?$|mt{10KaxSy#41xQW98Vk9**+M)#+#_IV7pu4TYyX>m3I2ex` zK3$CO2Gv$DKbeTBUhfz?QUt+bEr);7$jA@#~y+l%ZTHpU0p! zpPtYVlerCsmZc6bnYj<5V4CV9451JOQuwlcgIwh9mWSt(2h1ZfpkeN;D@(C`8fwlN zb+G^D4~C^VX7qXe`|5C3U4-CPnC{k%A~Q1iO~Bvzt_m^$UvS<;v@G`F#j~yXnH(xh zXRK$L6i~aVrsbWh@nX$U63_RObQIvasjyvdr*I~~`h!LLtNLO}E7rWuEFo>73HZ*5 zP?4o6Gy+lk9kA_b1|vHsw)CH|UHl&pX*q>L*W^j7&$5f_YG@*n16Ff)F0wq;kB3thd{Hz{#N4Rbif;_(k ze9;-JS^f!21{CKOoqo-OUDb<)guC-l$6f}RwsxmU{|UUeq-OZRg;j#91jTlPDz6gI z0e%qD4~-Fq@$a4+WFxAxh~=aoybBwCG+)dE-U3r_Hel$CARu{Z+`$l(eS zyR6%q*%p!)#$H#>1qW{dqY6AGtfaIRM>I2&qISYv*I;809}j;uy-oTfNp{thO{zn> z8f}h823;tkesn<_901)*Y{?Ji1%ZTTDH6n*UiH_cvfrdyfL8=h{?aS5TP6iNimFEzacvb6|KVR}Fa zU_&PrN(vjy^n#EDxSBfHnk*AqJi?J_DRYJ&6DX5XJ|p4B89~%aKf%n(7aOE-YYoCJ zZIv1+U1B#_XzK2Bjip7#iwaLY&7>Jsjm|#PbBme8nxm$+gvt;h7f6>V`m{L%q>!0Q zD8y;g0BeZm%u><-aQX$!mbNO%K3YRBrQy=H2Wld&Dp+V+$^@G28mvuuGR@vz>#jQg z2-T*D2=>_SB)6IGEek?Pb;W4GYI#{j*Dgo!io($f#=1{l%>HEX{HCplOe#yQdgdr_ z@0R!?NZTa0?O4X5*aQ{hz5@a z%~}cQgnGr(8mN+NI-^SKZ`1KaE0s23NI{lu!DPT00(uYW0<5u6`@@)S>A21ZfmCuL z%4O2asgUonYH5r3l{-V_f|`R2`4JrbTg@BHl=Q;*i_pvqkeIP=l7iJ6mI$<(oDrUR zMV3vQjCObA?)R=Dk1&6Yy#hA;Vn8Y3s&^0n+aZ%WSV8+m=R2|t zsKBPcJuTFSVfIbz#X8wAZk=ICJvl^#(XWq~rF{1JJo@wxZ={ zZIM(OX)73ckEKY+1XytYw0onui1wN0cr{Z0X2ehvJPd#rb322>ARg$JAQzM>J>FFi zqC2v|rFvRo;rEWr3EFZH@OzGrYy9H9qQQPo3LV1iX+Xj;_tZaDY9!wCRLd6>3Zp}g zi9q5URxYo{|0X78mc@!!Q^TY#AbZ)w>}_eUV+%L#_-Nm*qR{2=MWZlc)hH^dQ_?6M z!uXPc=T}DziuN1YeyZTA#zAb^jXgD18;bP2&ddL`yzu?k?(wK6*uN1R0H6gG;J0`D z9}V}vzM}tqq*tQ;AIKPH@2Zk?io3)Bnutb&wN&ax&BYbi*dPBxTP+k&CZe!~C1N;6 z=Eh=F=hdzrbsE=zo-3=TnhmitK5#`6Qn#e!SX|2!KHq;9qMOLVE0M(w!qb$gM*%cU zp{VC`EM%AZwP@85vaT`LkcK#a#i^>zi`AYMlJCzDDJwAoO0HLWZd~iZnXX+!(2^m! zJ^ty|?TEJ+7;j-f%%E#+mB0^X~54Ve5F5Y4TWW3)AuG55UXU6 ziLg(Vad_fD_RbguehLoJYbY^cr8VEkMegC|-)8X6+&qT)%P3<8?2Kob)f@>k3lV?t zR_+bv*c6P~^1<3I}b+0t^>ki@}@$>#Vh8a2d!3t_GtxJ})C1V2)2ePI>$xayE!e6}(t8 z$-a>Z?oE=cq>uwd80D9iBLbqWOJpXZ-!XHn&^kieOHn&$!Y$OS_mQosDB; zF$h}DT-S;JZ)6pBGGY&buaXTclDnZn^eY1(bLA4rf7DWUW|!Lqu4pM}O=89z#;3K+ zkBU%va6){b@lDW�U>Q=z$=UZ+oGqdsnus>znSonilSwvk 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 0: + return True + + # Fallback: Check by tier name (for compatibility) + if PATREON_MEMBERSHIP_TIER.lower() in tier_name.lower(): + # Check if membership is active + status = membership.get('attributes', {}).get('patron_status', '') + if status in ['active_patron', 'former_patron']: + # Check if currently entitled + entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0) + if entitled > 0: + return True + + return False + + except Exception as e: + import logging + logging.writeToFile(f"Error checking Patreon membership for {user_email}: {str(e)}") + return False + + def _get_user_identity(self, access_token): + """ + Get user identity from Patreon API + """ + url = f"{PATREON_API_BASE}/identity" + + try: + req = urllib.request.Request(url) + req.add_header('Authorization', f'Bearer {access_token}') + + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + return data.get('data', {}) + except Exception as e: + import logging + logging.writeToFile(f"Error getting Patreon identity: {str(e)}") + return None + + def _get_memberships(self, access_token, member_id): + """ + Get user's memberships from Patreon API + """ + url = f"{PATREON_API_BASE}/members/{member_id}?include=currently_entitled_tiers" + + try: + req = urllib.request.Request(url) + req.add_header('Authorization', f'Bearer {access_token}') + + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + + # Parse included tiers + memberships = [] + included = data.get('included', []) + + for item in included: + if item.get('type') == 'tier': + # Include tier ID in the membership data + tier_data = { + 'id': item.get('id', ''), + 'type': item.get('type', ''), + 'attributes': item.get('attributes', {}) + } + memberships.append(tier_data) + + return memberships + except Exception as e: + import logging + logging.writeToFile(f"Error getting Patreon memberships: {str(e)}") + return [] + + def _load_cache(self): + """Load cache from file""" + if os.path.exists(self.cache_file): + try: + with open(self.cache_file, 'r') as f: + return json.load(f) + except: + return {} + return {} + + def _save_cache(self, cache_data): + """Save cache to file""" + try: + os.makedirs(os.path.dirname(self.cache_file), exist_ok=True) + with open(self.cache_file, 'w') as f: + json.dump(cache_data, f) + except Exception as e: + import logging + logging.writeToFile(f"Error saving Patreon cache: {str(e)}") + + def verify_plugin_access(self, user_email, plugin_name): + """ + Verify if user can access a paid plugin + + Args: + user_email: User's email address + plugin_name: Name of the plugin to check + + Returns: + dict: { + 'has_access': bool, + 'is_paid': bool, + 'message': str + } + """ + # Check if plugin is paid (this will be checked from meta.xml) + # For now, we'll assume the plugin system will pass this info + + # Check membership + has_membership = self.check_membership_cached(user_email) + + return { + 'has_access': has_membership, + 'is_paid': True, # This will be determined by plugin metadata + 'message': 'Access granted' if has_membership else 'Patreon subscription required' + } diff --git a/pluginHolder/plugin_access.py b/pluginHolder/plugin_access.py new file mode 100644 index 000000000..f460e0768 --- /dev/null +++ b/pluginHolder/plugin_access.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +""" +Plugin Access Control +Checks if user has access to paid plugins +""" + +from .patreon_verifier import PatreonVerifier +import logging + +def check_plugin_access(request, plugin_name, plugin_meta=None): + """ + Check if user has access to a plugin + + Args: + request: Django request object + plugin_name: Name of the plugin + plugin_meta: Plugin metadata dict (optional, will be loaded if not provided) + + Returns: + dict: { + 'has_access': bool, + 'is_paid': bool, + 'message': str, + 'patreon_url': str or None + } + """ + # Default response for free plugins + default_response = { + 'has_access': True, + 'is_paid': False, + 'message': 'Access granted', + 'patreon_url': None + } + + # If plugin_meta not provided, try to load it + if plugin_meta is None: + plugin_meta = _load_plugin_meta(plugin_name) + + # Check if plugin is paid + if not plugin_meta or not plugin_meta.get('is_paid', False): + return default_response + + # Plugin is paid - check Patreon membership + if not request.user or not request.user.is_authenticated: + return { + 'has_access': False, + 'is_paid': True, + 'message': 'Please log in to access this plugin', + 'patreon_url': plugin_meta.get('patreon_url') + } + + # Get user email + user_email = getattr(request.user, 'email', None) + if not user_email: + # Try to get from username or other fields + user_email = getattr(request.user, 'username', '') + + if not user_email: + return { + 'has_access': False, + 'is_paid': True, + 'message': 'Unable to verify user identity', + 'patreon_url': plugin_meta.get('patreon_url') + } + + # Check Patreon membership + verifier = PatreonVerifier() + has_membership = verifier.check_membership_cached(user_email) + + if has_membership: + return { + 'has_access': True, + 'is_paid': True, + 'message': 'Access granted', + 'patreon_url': None + } + else: + return { + 'has_access': False, + 'is_paid': True, + 'message': f'This plugin requires a Patreon subscription to "{plugin_meta.get("patreon_tier", "CyberPanel Paid Plugin")}"', + 'patreon_url': plugin_meta.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership') + } + +def _load_plugin_meta(plugin_name): + """ + Load plugin metadata from meta.xml + + Args: + plugin_name: Name of the plugin + + Returns: + dict: Plugin metadata or None + """ + import os + from xml.etree import ElementTree + + installed_path = f'/usr/local/CyberCP/{plugin_name}/meta.xml' + source_path = f'/home/cyberpanel/plugins/{plugin_name}/meta.xml' + + meta_path = None + if os.path.exists(installed_path): + meta_path = installed_path + elif os.path.exists(source_path): + meta_path = source_path + + if not meta_path: + return None + + try: + tree = ElementTree.parse(meta_path) + root = tree.getroot() + + # Extract paid plugin information + paid_elem = root.find('paid') + patreon_tier_elem = root.find('patreon_tier') + patreon_url_elem = root.find('patreon_url') + + is_paid = False + if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true': + is_paid = True + + return { + 'is_paid': is_paid, + 'patreon_tier': patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin', + 'patreon_url': patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership' + } + except Exception as e: + logging.writeToFile(f"Error loading plugin meta for {plugin_name}: {str(e)}") + return None diff --git a/pluginHolder/templates/pluginHolder/plugin_help.html b/pluginHolder/templates/pluginHolder/plugin_help.html new file mode 100644 index 000000000..40b340c67 --- /dev/null +++ b/pluginHolder/templates/pluginHolder/plugin_help.html @@ -0,0 +1,351 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Plugin Help - " %}{{ plugin_name }} - CyberPanel{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +{% load static %} + + +{% endblock %} \ No newline at end of file diff --git a/pluginHolder/templates/pluginHolder/plugin_not_found.html b/pluginHolder/templates/pluginHolder/plugin_not_found.html new file mode 100644 index 000000000..9d0af1f7f --- /dev/null +++ b/pluginHolder/templates/pluginHolder/plugin_not_found.html @@ -0,0 +1,93 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Plugin Not Found - CyberPanel" %}{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+
+
+ +
+

{% trans "Plugin Not Found" %}

+

+ {% if plugin_name %} + {% trans "The plugin" %} "{{ plugin_name }}" {% trans "could not be found." %} + {% else %} + {% trans "The requested plugin could not be found." %} + {% endif %} + {% if error %} +
{{ error }} + {% endif %} +

+ +
+
+{% endblock %} \ No newline at end of file diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index f996b8329..303866854 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -182,6 +182,103 @@ color: var(--text-secondary, #64748b); } + .plugin-pricing-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: 6px; + vertical-align: middle; + } + + .plugin-pricing-badge.free { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + .plugin-pricing-badge.paid { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; + } + + .paid-badge { + display: inline-block; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 4px 10px; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: 8px; + vertical-align: middle; + } + + .paid-badge i { + margin-right: 4px; + } + + .subscription-warning { + background: #fff3cd; + border: 1px solid #ffc107; + border-radius: 8px; + padding: 12px; + margin-top: 10px; + font-size: 12px; + color: #856404; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + } + + .subscription-warning-content { + display: flex; + align-items: center; + flex: 1; + } + + .subscription-warning i { + margin-right: 6px; + color: #ffc107; + } + + .subscription-warning-button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: linear-gradient(135deg, #f96854 0%, #f96854 100%); + color: white; + text-decoration: none; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + transition: all 0.3s ease; + white-space: nowrap; + box-shadow: 0 2px 4px rgba(249, 104, 84, 0.3); + } + + .subscription-warning-button:hover { + background: linear-gradient(135deg, #e55a47 0%, #e55a47 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(249, 104, 84, 0.4); + color: white; + text-decoration: none; + } + + .subscription-warning-button i { + margin-right: 0; + color: white; + } + .plugin-description { font-size: 13px; color: var(--text-secondary, #64748b); @@ -816,20 +913,35 @@
@@ -943,7 +1069,6 @@ {% trans "Plugin Name" %} - {% trans "Author" %} {% trans "Version" %} {% trans "Modify Date" %} {% trans "Status" %} @@ -959,13 +1084,13 @@ {{ plugin.name }} - - - {{ plugin.author|default:"Unknown" }} - - {{ plugin.version }} + {% if plugin.is_paid|default:False|default_if_none:False %} + + {% else %} + {% trans "Free" %} + {% endif %} @@ -1097,13 +1222,12 @@ + - + - - @@ -1125,6 +1249,8 @@ From 666b1d40971cd0427383666f75b6186ad2adc361 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:37:48 +0100 Subject: [PATCH 32/40] fix(plugins): Fix plugin store showing uninstalled plugins as installed - Only check /usr/local/CyberCP/ for installed status, not /home/cyberpanel/plugins/ - Require both plugin directory AND meta.xml to exist for installed status - Plugins in source directory are not considered installed - Fixes issue where Discord Webhooks, Fail2ban, Premium Plugin Example, and Test Plugin showed as installed when they weren't Previously, the check used: os.path.exists(installed_path) or os.path.exists(source_path) This incorrectly marked plugins as installed if they existed in the source directory. Now uses: os.path.exists(installed_path) and os.path.exists(installed_meta) This correctly only marks plugins as installed if they're actually in /usr/local/CyberCP/ with meta.xml --- pluginHolder/views.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index d7f6527be..f76143446 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -846,10 +846,15 @@ def _enrich_store_plugins(plugins): continue # Check if plugin is installed locally + # IMPORTANT: Only check installed location, not source location + # Plugins in /home/cyberpanel/plugins/ are just source files, not installed plugins installed_path = os.path.join(plugin_install_dir, plugin_dir) - source_path = os.path.join(plugin_source_dir, plugin_dir) + installed_meta = os.path.join(installed_path, 'meta.xml') - plugin['installed'] = os.path.exists(installed_path) or os.path.exists(source_path) + # Plugin is only considered installed if: + # 1. The plugin directory exists in /usr/local/CyberCP/ + # 2. AND meta.xml exists (indicates actual installation, not just leftover files) + plugin['installed'] = os.path.exists(installed_path) and os.path.exists(installed_meta) # Check if plugin is enabled (only if installed) if plugin['installed']: @@ -860,11 +865,15 @@ def _enrich_store_plugins(plugins): # Ensure is_paid field exists and is properly set (default to False if not set or invalid) # CRITICAL FIX: Always check local meta.xml FIRST as source of truth # This ensures cache entries without is_paid are properly enriched + # Check installed location first, then fallback to source location for metadata meta_path = None - if os.path.exists(installed_path): - meta_path = os.path.join(installed_path, 'meta.xml') + source_path = os.path.join(plugin_source_dir, plugin_dir) + if os.path.exists(installed_meta): + meta_path = installed_meta elif os.path.exists(source_path): - meta_path = os.path.join(source_path, 'meta.xml') + source_meta = os.path.join(source_path, 'meta.xml') + if os.path.exists(source_meta): + meta_path = source_meta # If we have a local meta.xml, use it as the source of truth (most reliable) if meta_path and os.path.exists(meta_path): From 6b65cf12d8b16f7b1a969dbbd6b10e273357642a Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:41:14 +0100 Subject: [PATCH 33/40] fix(plugins): Fix 'local variable pluginInstaller referenced before assignment' error - Remove redundant local import of pluginInstaller inside install_from_store function - pluginInstaller is already imported at module level (line 20) - The local import was creating a variable shadowing issue - When the cleanup code path wasn't executed, pluginInstaller was referenced before assignment - Fixes installation from store failing with variable reference error --- pluginHolder/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index f76143446..dc5ffa65c 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -1303,15 +1303,14 @@ def install_from_store(request, plugin_name): else: # Directory exists but no meta.xml - likely incomplete/uninstalled # Try to clean it up first using pluginInstaller.removeFiles which handles permissions + # pluginInstaller is already imported at module level, no need to import again try: - from pluginInstaller.pluginInstaller import pluginInstaller pluginInstaller.removeFiles(plugin_name) logging.writeToFile(f'Cleaned up incomplete plugin directory: {plugin_name}') except Exception as e: logging.writeToFile(f'Warning: Could not clean up incomplete directory: {str(e)}') # Try fallback: use system rm -rf try: - import subprocess result = subprocess.run(['rm', '-rf', pluginInstalled], capture_output=True, text=True, timeout=30) if result.returncode == 0: logging.writeToFile(f'Cleaned up incomplete plugin directory using rm -rf: {plugin_name}') From 35167b46cf295517db9aa674feffb6bfc48c320e Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:43:49 +0100 Subject: [PATCH 34/40] fix(plugins): Add error handling to fetch_plugin_store to prevent 500 errors - Wrap mailUtilities.checkHome in try-except - Add try-except around _enrich_store_plugins calls - If enrichment fails, return plugins without enrichment instead of 500 error - Add error handling for _is_plugin_enabled calls - Prevents HTTP 500 errors when plugin store loading fails - Fixes issue where installing discordWebhooks caused plugin store to fail --- pluginHolder/views.py | 109 ++++++++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index dc5ffa65c..89e2d8c6c 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -858,7 +858,11 @@ def _enrich_store_plugins(plugins): # Check if plugin is enabled (only if installed) if plugin['installed']: - plugin['enabled'] = _is_plugin_enabled(plugin_dir) + try: + plugin['enabled'] = _is_plugin_enabled(plugin_dir) + except Exception as e: + logging.writeToFile(f"Error checking enabled status for {plugin_dir}: {str(e)}") + plugin['enabled'] = False # Default to disabled on error else: plugin['enabled'] = False @@ -1203,7 +1207,10 @@ def _fetch_plugins_from_github(): @require_http_methods(["GET"]) def fetch_plugin_store(request): """Fetch plugins from the plugin store with caching""" - mailUtilities.checkHome() + try: + mailUtilities.checkHome() + except Exception as e: + logging.writeToFile(f"Warning in mailUtilities.checkHome: {str(e)}") # Add cache-busting headers to prevent browser caching response_headers = { @@ -1212,47 +1219,67 @@ def fetch_plugin_store(request): 'Expires': '0' } - # Try to get from cache first - cached_plugins = _get_cached_plugins() - if cached_plugins is not None: - # Sort plugins deterministically by name to prevent order changes - cached_plugins.sort(key=lambda x: x.get('name', '').lower()) - - # Enrich cached plugins with installed/enabled status - enriched_plugins = _enrich_store_plugins(cached_plugins) - response = JsonResponse({ - 'success': True, - 'plugins': enriched_plugins, - 'cached': True - }, json_dumps_params={'ensure_ascii': False}) - # Add headers - for key, value in response_headers.items(): - response[key] = value - return response - - # Cache miss or expired - fetch from GitHub try: - plugins = _fetch_plugins_from_github() + # Try to get from cache first + cached_plugins = _get_cached_plugins() + if cached_plugins is not None: + # Sort plugins deterministically by name to prevent order changes + cached_plugins.sort(key=lambda x: x.get('name', '').lower()) + + # Enrich cached plugins with installed/enabled status + try: + enriched_plugins = _enrich_store_plugins(cached_plugins) + except Exception as enrich_error: + logging.writeToFile(f"Error enriching cached plugins: {str(enrich_error)}") + # Return cached plugins without enrichment if enrichment fails + enriched_plugins = cached_plugins + for plugin in enriched_plugins: + plugin.setdefault('installed', False) + plugin.setdefault('enabled', False) + plugin.setdefault('is_paid', False) + + response = JsonResponse({ + 'success': True, + 'plugins': enriched_plugins, + 'cached': True + }, json_dumps_params={'ensure_ascii': False}) + # Add headers + for key, value in response_headers.items(): + response[key] = value + return response - # Sort plugins deterministically by name to prevent order changes - plugins.sort(key=lambda x: x.get('name', '').lower()) - - # Enrich plugins with installed/enabled status - enriched_plugins = _enrich_store_plugins(plugins) - - # Save to cache (save original, not enriched, to keep cache clean) - if plugins: - _save_plugins_cache(plugins) - - response = JsonResponse({ - 'success': True, - 'plugins': enriched_plugins, - 'cached': False - }, json_dumps_params={'ensure_ascii': False}) - # Add cache-busting headers - for key, value in response_headers.items(): - response[key] = value - return response + # Cache miss or expired - fetch from GitHub + try: + plugins = _fetch_plugins_from_github() + + # Sort plugins deterministically by name to prevent order changes + plugins.sort(key=lambda x: x.get('name', '').lower()) + + # Enrich plugins with installed/enabled status + try: + enriched_plugins = _enrich_store_plugins(plugins) + except Exception as enrich_error: + logging.writeToFile(f"Error enriching plugins from GitHub: {str(enrich_error)}") + # Return plugins without enrichment if enrichment fails + enriched_plugins = plugins + for plugin in enriched_plugins: + plugin.setdefault('installed', False) + plugin.setdefault('enabled', False) + plugin.setdefault('is_paid', False) + + # Save to cache (save original, not enriched, to keep cache clean) + if plugins: + _save_plugins_cache(plugins) + + response = JsonResponse({ + 'success': True, + 'plugins': enriched_plugins, + 'cached': False + }, json_dumps_params={'ensure_ascii': False}) + # Add cache-busting headers + for key, value in response_headers.items(): + response[key] = value + return response except Exception as e: error_message = str(e) From 28b69eb2f0703500efcedc10d352f790ead1f783 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:44:34 +0100 Subject: [PATCH 35/40] fix(plugins): Fix indentation error in fetch_plugin_store exception handler - Fix except block indentation to match try block - Add error handling for enrichment in stale cache fallback - Ensures proper exception handling structure --- pluginHolder/views.py | 64 ++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 89e2d8c6c..738612eba 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -1280,35 +1280,43 @@ def fetch_plugin_store(request): for key, value in response_headers.items(): response[key] = value return response - - except Exception as e: - error_message = str(e) - # If rate limited, try to use stale cache as fallback - if '403' in error_message or 'rate limit' in error_message.lower(): - stale_cache = _get_cached_plugins(allow_expired=True) # Get cache even if expired - if stale_cache is not None: - logging.writeToFile("Using stale cache due to rate limit") - # Sort plugins deterministically by name to prevent order changes - stale_cache.sort(key=lambda x: x.get('name', '').lower()) - enriched_plugins = _enrich_store_plugins(stale_cache) - response = JsonResponse({ - 'success': True, - 'plugins': enriched_plugins, - 'cached': True, - 'warning': 'Using cached data due to GitHub rate limit. Data may be outdated.' - }, json_dumps_params={'ensure_ascii': False}) - # Add cache-busting headers - for key, value in response_headers.items(): - response[key] = value - return response - - # No cache available, return error - return JsonResponse({ - 'success': False, - 'error': error_message, - 'plugins': [] - }, status=500) + except Exception as e: + error_message = str(e) + + # If rate limited, try to use stale cache as fallback + if '403' in error_message or 'rate limit' in error_message.lower(): + stale_cache = _get_cached_plugins(allow_expired=True) # Get cache even if expired + if stale_cache is not None: + logging.writeToFile("Using stale cache due to rate limit") + # Sort plugins deterministically by name to prevent order changes + stale_cache.sort(key=lambda x: x.get('name', '').lower()) + try: + enriched_plugins = _enrich_store_plugins(stale_cache) + except Exception as enrich_error: + logging.writeToFile(f"Error enriching stale cache: {str(enrich_error)}") + enriched_plugins = stale_cache + for plugin in enriched_plugins: + plugin.setdefault('installed', False) + plugin.setdefault('enabled', False) + plugin.setdefault('is_paid', False) + response = JsonResponse({ + 'success': True, + 'plugins': enriched_plugins, + 'cached': True, + 'warning': 'Using cached data due to GitHub rate limit. Data may be outdated.' + }, json_dumps_params={'ensure_ascii': False}) + # Add cache-busting headers + for key, value in response_headers.items(): + response[key] = value + return response + + # No cache available, return error + return JsonResponse({ + 'success': False, + 'error': error_message, + 'plugins': [] + }, status=500) @csrf_exempt @require_http_methods(["POST"]) From 3026d50f35f249f985a4ac75cb7055d1cb3720b4 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:44:59 +0100 Subject: [PATCH 36/40] fix(plugins): Fix indentation in fetch_plugin_store - final fix --- pluginHolder/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 738612eba..690002234 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -1280,7 +1280,7 @@ def fetch_plugin_store(request): for key, value in response_headers.items(): response[key] = value return response - + except Exception as e: error_message = str(e) From 1f369b36b8d18797125aafc823ec50d8a80543b0 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:45:29 +0100 Subject: [PATCH 37/40] fix(plugins): Add missing outer except block in fetch_plugin_store --- pluginHolder/views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 690002234..fe06092aa 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -1317,6 +1317,15 @@ def fetch_plugin_store(request): 'error': error_message, 'plugins': [] }, status=500) + except Exception as outer_error: + # Catch any other unexpected errors + error_message = str(outer_error) + logging.writeToFile(f"Unexpected error in fetch_plugin_store: {error_message}") + return JsonResponse({ + 'success': False, + 'error': error_message, + 'plugins': [] + }, status=500) @csrf_exempt @require_http_methods(["POST"]) From 2c9500fe35376f4c379928521d7d95df91b55c9b Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:47:41 +0100 Subject: [PATCH 38/40] feat(plugins): Add installed and active plugin statistics to Installed Plugins page - Calculate total installed plugins count - Calculate total active/enabled plugins count - Display statistics in page header with icons - Shows 'Installed: X' and 'Active: Y' counts - Statistics only shown when plugins are installed - Improves visibility of plugin status at a glance --- pluginHolder/templates/pluginHolder/plugins.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index a83d839d2..7978e2f24 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -989,6 +989,20 @@ {% trans "Installed Plugins" %}

{% trans "List of installed plugins on your CyberPanel" %}

+ {% if plugins %} +
+
+ + {% trans "Installed:" %} + {{ total_installed }} +
+
+ + {% trans "Active:" %} + {{ total_active }} +
+
+ {% endif %} From e13c0d0756339af5a833a010267b04a697d2c723 Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 03:48:13 +0100 Subject: [PATCH 39/40] fix(plugins): Add total_installed and total_active to context --- pluginHolder/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pluginHolder/views.py b/pluginHolder/views.py index fe06092aa..354aea705 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -427,11 +427,17 @@ def installed(request): # Sort plugins deterministically by name to prevent order changes pluginList.sort(key=lambda x: x.get('name', '').lower()) + # Calculate statistics + total_installed = len(pluginList) + total_active = sum(1 for plugin in pluginList if plugin.get('enabled', False)) + # Add cache-busting timestamp to context to prevent browser caching import time context = { 'plugins': pluginList, 'error_plugins': errorPlugins, + 'total_installed': total_installed, + 'total_active': total_active, 'cache_buster': int(time.time()) # Add timestamp to force template reload } From 211571a4dbdcf9a4e1d0edf6df46ebeaf1e24efc Mon Sep 17 00:00:00 2001 From: master3395 Date: Mon, 26 Jan 2026 17:45:36 +0100 Subject: [PATCH 40/40] Fix plugin count discrepancy and remove duplicate view toggle - Fixed installed plugin count to correctly show all 10 installed plugins - Added filesystem verification to ensure accurate plugin counting - Fixed duplicate view-toggle navigation row (removed second row) - Added installed/active count display in page header - Improved plugin installed status detection logic - Enhanced debug logging for plugin count discrepancies --- .../templates/pluginHolder/plugins.html | 521 ++---------- pluginHolder/views.py | 797 ++++-------------- 2 files changed, 256 insertions(+), 1062 deletions(-) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index 7978e2f24..089569bc6 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -206,32 +206,6 @@ border: 1px solid #ffeaa7; } - /* NEW and Stale badges */ - .plugin-status-badge { - display: inline-block; - padding: 3px 8px; - border-radius: 4px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - margin-left: 8px; - vertical-align: middle; - cursor: help; - position: relative; - } - - .plugin-status-badge.new { - background: #ffc107; - color: #000; - box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.3); - } - - .plugin-status-badge.stale { - background: #dc3545; - color: white; - box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.3); - } - .paid-badge { display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); @@ -654,62 +628,6 @@ border-color: #5856d6; } - /* Store Search Styles */ - .store-search-container { - padding: 15px; - background: var(--bg-secondary, #f8f9ff); - border-radius: 8px; - margin-bottom: 20px; - } - - .store-search-wrapper { - position: relative; - } - - .store-search-input { - width: 100%; - padding: 12px 15px 12px 45px; - border: 2px solid #e8e9ff; - border-radius: 8px; - font-size: 14px; - transition: all 0.3s ease; - background: white; - color: var(--text-primary, #2f3640); - } - - .store-search-input:focus { - outline: none; - border-color: #5856d6; - box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1); - } - - .store-search-input::placeholder { - color: #94a3b8; - } - - .clear-search-btn { - display: none; - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - color: #64748b; - cursor: pointer; - padding: 5px; - font-size: 14px; - transition: color 0.2s; - } - - .clear-search-btn:hover { - color: #dc3545; - } - - .clear-search-btn.visible { - display: block; - } - .store-table-wrapper { overflow-x: auto; background: var(--bg-primary, white); @@ -982,36 +900,40 @@
{% trans "Icon" %} {% trans "Plugin Name" %}{% trans "Author" %} {% trans "Version" %}{% trans "Pricing" %} {% trans "Modify Date" %}{% trans "Status" %} {% trans "Action" %}{% trans "Active" %} {% trans "Help" %} {% trans "About" %}
- - @@ -1204,30 +1083,11 @@ {% for plugin in plugins %} - -
{% trans "Icon" %} {% trans "Plugin Name" %} {% trans "Version" %}{% trans "Pricing" %} {% trans "Modify Date" %} {% trans "Status" %} {% trans "Action" %}
-
- {% if plugin.type == "Security" %} - - {% elif plugin.type == "Performance" %} - - {% elif plugin.type == "Utility" %} - - {% elif plugin.type == "Backup" %} - - {% else %} - - {% endif %} -
-
{{ plugin.name }} - {% if plugin.is_new|default:False %} NEW{% endif %} - {% if plugin.is_stale|default:False %} STALE{% endif %} {{ plugin.version }} - {% if plugin.is_paid|default:False|default_if_none:False %} {% else %} @@ -1310,6 +1170,26 @@

{% trans "No Plugins Installed" %}

{% trans "You haven't installed any plugins yet. Plugins extend CyberPanel's functionality with additional features." %}

+ + +
+ + + + + + {% trans "Plugin Development Guide" %} + +
{% endif %} @@ -1349,27 +1229,16 @@ - - - - - -
+ +