v2.5.5-dev: FTP Create Account fix, ftp_quotas table, CP_VERSION, mailUtilities indent, deploy scripts and docs

- baseTemplate: CP_VERSION from CYBERPANEL_FULL_VERSION (2.5.5.dev) for cache busting
- FTP Create Account: inline script + polling + scope sync so details form shows after website select
- ftp.js: showFTPDetails, select2/change handlers (ftp/static, static, public/static)
- sql/create_ftp_quotas.sql + deploy-ftp-quotas-table.sh for /ftp/quotaManagement
- plogical/mailUtilities.py: indentation fix in DNS query try/except block
- deploy-ftp-create-account-fix.sh, to-do docs (FTP-QUOTAS-TABLE-FIX, V2.5.5-DEV-FIXES-AND-DEPLOY, RUNTIME-VS-REPO)
This commit is contained in:
master3395
2026-02-04 02:33:32 +01:00
parent 27b664c3a9
commit ac6f4f6992
12 changed files with 508 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% with CP_VERSION="2.4.4.1" %}
{% with CP_VERSION=CYBERPANEL_FULL_VERSION|default:"2.5.5.dev" %}
<!DOCTYPE html>
<html lang="en" ng-app="CyberCP">
<head>

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Deploy FTP Create Account template fix to a CyberPanel installation.
# Copies the updated createFTPAccount.html and optionally restarts lscpd.
#
# Usage (run from anywhere):
# sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
# sudo bash deploy-ftp-create-account-fix.sh [REPO_DIR] [CP_DIR]
#
# Or from repo root: cd /home/cyberpanel-repo && sudo bash deploy-ftp-create-account-fix.sh
set -e
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
err() { log "ERROR: $*" >&2; }
# Resolve REPO_DIR
if [[ -n "$1" && -d "$1/ftp" ]]; then
REPO_DIR="$1"
shift
elif [[ -d "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/ftp" ]]; then
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
elif [[ -d "/home/cyberpanel-repo/ftp" ]]; then
REPO_DIR="/home/cyberpanel-repo"
elif [[ -d "./ftp" ]]; then
REPO_DIR="$(pwd)"
else
err "Repo not found. Use: sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh"
exit 1
fi
CP_DIR="${1:-/usr/local/CyberCP}"
RESTART_LSCPD="${RESTART_LSCPD:-1}"
if [[ ! -d "$CP_DIR" ]]; then
err "CyberPanel directory not found: $CP_DIR"
exit 1
fi
if [[ ! -f "$REPO_DIR/ftp/templates/ftp/createFTPAccount.html" ]]; then
err "Source template not found in: $REPO_DIR"
exit 1
fi
log "REPO_DIR=$REPO_DIR"
log "CP_DIR=$CP_DIR"
SRC="$REPO_DIR/ftp/templates/ftp/createFTPAccount.html"
DST="$CP_DIR/ftp/templates/ftp/createFTPAccount.html"
mkdir -p "$(dirname "$DST")"
cp -f "$SRC" "$DST"
log "Copied: ftp/templates/ftp/createFTPAccount.html"
if [[ "$RESTART_LSCPD" =~ ^(1|yes|true)$ ]]; then
if systemctl is-active --quiet lscpd 2>/dev/null; then
log "Restarting lscpd..."
systemctl restart lscpd || { err "lscpd restart failed"; exit 1; }
log "lscpd restarted."
else
log "lscpd not running or not a systemd service; skip restart."
fi
else
log "Skipping restart (set RESTART_LSCPD=1 to restart lscpd)."
fi
log "Deploy complete. Hard-refresh /ftp/createFTPAccount in the browser (Ctrl+Shift+R)."

View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Create the missing ftp_quotas table in the CyberPanel database.
# Fixes: (1146, "Table 'cyberpanel.ftp_quotas' doesn't exist") on /ftp/quotaManagement
#
# Usage:
# sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
# sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
set -e
log() { echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*"; }
err() { log "ERROR: $*" >&2; }
if [[ -n "$1" && -f "$1/sql/create_ftp_quotas.sql" ]]; then
REPO_DIR="$1"
shift
elif [[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/sql/create_ftp_quotas.sql" ]]; then
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
elif [[ -f "/home/cyberpanel-repo/sql/create_ftp_quotas.sql" ]]; then
REPO_DIR="/home/cyberpanel-repo"
else
err "sql/create_ftp_quotas.sql not found."
exit 1
fi
CP_DIR="${1:-/usr/local/CyberCP}"
SQL_FILE="$REPO_DIR/sql/create_ftp_quotas.sql"
if [[ ! -d "$CP_DIR" ]]; then
err "CyberPanel directory not found: $CP_DIR"
exit 1
fi
log "REPO_DIR=$REPO_DIR"
log "CP_DIR=$CP_DIR"
mkdir -p "$CP_DIR/sql"
cp -f "$SQL_FILE" "$CP_DIR/sql/create_ftp_quotas.sql"
log "Copied create_ftp_quotas.sql to $CP_DIR/sql/"
# Run SQL using Django DB connection (no password on command line)
log "Creating ftp_quotas table..."
export CP_DIR
python3 << 'PYEOF'
import os
import sys
cp_dir = os.environ.get('CP_DIR', '/usr/local/CyberCP')
sys.path.insert(0, cp_dir)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CyberCP.settings')
import django
django.setup()
from django.db import connection
with open(os.path.join(cp_dir, 'sql', 'create_ftp_quotas.sql'), 'r') as f:
sql = f.read()
with connection.cursor() as cursor:
cursor.execute(sql)
print('Executed CREATE TABLE IF NOT EXISTS ftp_quotas.')
PYEOF
log "Done. Reload https://207.180.193.210:2087/ftp/quotaManagement"

View File

@@ -15,7 +15,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.generatedPasswordView = true;
$(document).ready(function () {
$( ".ftpDetails" ).hide();
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
@@ -26,9 +26,11 @@ app.controller('createFTPAccount', function ($scope, $http) {
$sel.select2();
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.$apply();
$(".ftpDetails").show();
$scope.$evalAsync(function () {
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
});
$(".ftpDetails, .account-details").show();
});
} else {
initNativeSelect();
@@ -41,20 +43,23 @@ app.controller('createFTPAccount', function ($scope, $http) {
}
function initNativeSelect() {
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.$apply();
$(".ftpDetails").show();
var val = $(this).val();
$scope.$evalAsync(function () {
$scope.ftpDomain = val;
$scope.ftpDetails = (val && val !== '') ? false : true;
});
$(".ftpDetails, .account-details").show();
});
}
});
$scope.showFTPDetails = function() {
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
$(".ftpDetails").show();
$scope.ftpDetails = false;
$(".ftpDetails, .account-details").show();
} else {
$(".ftpDetails").hide();
$scope.ftpDetails = true;
$(".ftpDetails, .account-details").hide();
}
};

View File

@@ -452,18 +452,65 @@
<div class="col-md-8">
<div class="form-group">
<label class="form-label">{% trans "Select Website" %}</label>
<select ng-model="ftpDomain" ng-change="showFTPDetails()" class="form-control create-ftp-acct-select">
<select id="create-ftp-website-select" ng-model="ftpDomain" ng-change="showFTPDetails()" class="form-control create-ftp-acct-select">
<option value="">{% trans "Choose a website..." %}</option>
{% for items in websiteList %}
<option value="{{ items }}">{{ items }}</option>
{% endfor %}
</select>
<script data-cfasync="false">
(function() {
function hasWebsiteSelected() {
var sel = document.getElementById('create-ftp-website-select');
if (!sel) return false;
if (sel.value && sel.value !== '') return true;
var rendered = document.querySelector('.select2-selection__rendered');
var text = rendered ? (rendered.title || rendered.textContent || '').trim() : '';
return text !== '' && text.indexOf('Choose') === -1;
}
function showFTPFormSection() {
var sel = document.getElementById('create-ftp-website-select');
var section = document.querySelector('.account-details.ftpDetails');
var container = document.querySelector('.modern-container[ng-controller="createFTPAccount"]');
if (section && hasWebsiteSelected()) {
section.classList.remove('ng-hide');
section.style.setProperty('display', 'block', 'important');
try { section.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) {}
if (typeof angular !== 'undefined' && container) {
try {
var scope = angular.element(container).scope();
if (scope) {
scope.ftpDetails = false;
scope.$evalAsync(function() { scope.ftpDetails = false; });
}
} catch (e) {}
}
}
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.getElementById('create-ftp-website-select');
if (!sel) return;
sel.addEventListener('change', showFTPFormSection);
sel.addEventListener('input', showFTPFormSection);
if (sel.value) showFTPFormSection();
var delays = [50, 150, 350, 600, 1000, 1500, 2200, 3000];
for (var i = 0; i < delays.length; i++) {
setTimeout(showFTPFormSection, delays[i]);
}
var pollCount = 0;
var pollId = setInterval(function() {
showFTPFormSection();
if (++pollCount >= 20) clearInterval(pollId);
}, 200);
});
})();
</script>
</div>
</div>
</div>
</div>
<div ng-hide="ftpDetails" class="account-details">
<div ng-hide="ftpDetails" class="account-details ftpDetails">
<h3><i class="fas fa-cog"></i> {% trans "FTP Account Details" %}</h3>
<div class="row">

View File

@@ -1682,69 +1682,69 @@ LogFile /var/log/clamav/clamav.log
# Query each DNS server
for url in urls:
try:
response = requests.get(f'{url}/index.php?ip={ip_address}', timeout=5)
try:
response = requests.get(f'{url}/index.php?ip={ip_address}', timeout=5)
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'url to call {ip_address} is {url}')
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'url to call {ip_address} is {url}')
if response.status_code == 200:
try:
data = response.json()
if response.status_code == 200:
try:
data = response.json()
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'response from dns system {str(data)}')
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'response from dns system {str(data)}')
# Validate response structure
if not isinstance(data, dict):
logging.CyberCPLogFileWriter.writeToFile(f'Invalid response format from {url}: not a dictionary')
continue
# Validate response structure
if not isinstance(data, dict):
logging.CyberCPLogFileWriter.writeToFile(f'Invalid response format from {url}: not a dictionary')
continue
if 'status' not in data:
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing "status" key')
continue
if 'status' not in data:
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing "status" key')
continue
if data['status'] == 1:
# Validate results structure
if 'results' not in data or not isinstance(data['results'], dict):
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing or invalid "results" key')
if data['status'] == 1:
# Validate results structure
if 'results' not in data or not isinstance(data['results'], dict):
logging.CyberCPLogFileWriter.writeToFile(f'Response from {url} missing or invalid "results" key')
continue
results_dict = data['results']
# Safely extract results from different DNS servers
dns_servers = ['8.8.8.8', '1.1.1.1', '9.9.9.9']
for dns_server in dns_servers:
if dns_server in results_dict:
result_value = results_dict[dns_server]
if result_value and result_value not in results:
results.append(result_value)
successful_queries += 1
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned status != 1: {data.get("status", "unknown")}')
except ValueError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to parse JSON response from {url}: {str(e)}')
continue
except KeyError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Missing key in response from {url}: {str(e)}')
continue
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned HTTP {response.status_code}')
except Timeout as e:
logging.CyberCPLogFileWriter.writeToFile(f'Timeout while querying DNS server {url}: {str(e)}')
continue
except ConnectionError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Connection error while querying DNS server {url}: {str(e)}')
continue
except RequestException as e:
logging.CyberCPLogFileWriter.writeToFile(f'Request error while querying DNS server {url}: {str(e)}')
continue
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Unexpected error while querying DNS server {url}: {str(e)}')
continue
results_dict = data['results']
# Safely extract results from different DNS servers
dns_servers = ['8.8.8.8', '1.1.1.1', '9.9.9.9']
for dns_server in dns_servers:
if dns_server in results_dict:
result_value = results_dict[dns_server]
if result_value and result_value not in results:
results.append(result_value)
successful_queries += 1
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned status != 1: {data.get("status", "unknown")}')
except ValueError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Failed to parse JSON response from {url}: {str(e)}')
continue
except KeyError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Missing key in response from {url}: {str(e)}')
continue
else:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'DNS server {url} returned HTTP {response.status_code}')
except Timeout as e:
logging.CyberCPLogFileWriter.writeToFile(f'Timeout while querying DNS server {url}: {str(e)}')
continue
except ConnectionError as e:
logging.CyberCPLogFileWriter.writeToFile(f'Connection error while querying DNS server {url}: {str(e)}')
continue
except RequestException as e:
logging.CyberCPLogFileWriter.writeToFile(f'Request error while querying DNS server {url}: {str(e)}')
continue
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f'Unexpected error while querying DNS server {url}: {str(e)}')
continue
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'rDNS result of {ip_address} is {str(results)} (successful queries: {successful_queries}/{len(urls)})')

View File

@@ -8,20 +8,44 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.ftpLoading = false;
$scope.ftpDetails = true;
$(document).ready(function () {
$( ".ftpDetails" ).hide();
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
$('.create-ftp-acct-select').select2();
if (typeof $ !== 'undefined' && $ && typeof $.fn !== 'undefined' && typeof $.fn.select2 === 'function') {
try {
var $sel = $('.create-ftp-acct-select');
if ($sel.length) {
$sel.select2();
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
$scope.$apply();
$(".ftpDetails, .account-details").show();
});
}
} catch (err) {}
}
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true;
$scope.$apply();
$(".ftpDetails, .account-details").show();
});
});
$('.create-ftp-acct-select').on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$( ".ftpDetails" ).show();
});
$scope.ftpLoading = true;
$scope.showFTPDetails = function() {
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
$scope.ftpDetails = false;
$(".ftpDetails, .account-details").show();
} else {
$scope.ftpDetails = true;
$(".ftpDetails, .account-details").hide();
}
};
$scope.createFTPAccount = function () {

21
sql/create_ftp_quotas.sql Normal file
View File

@@ -0,0 +1,21 @@
-- Create ftp_quotas table for FTP Quota Management (websiteFunctions.models.FTPQuota)
-- Run once per CyberPanel database. Safe to run: uses IF NOT EXISTS.
CREATE TABLE IF NOT EXISTS `ftp_quotas` (
`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
`user_id` INT NOT NULL,
`ftp_user` VARCHAR(255) NOT NULL,
`domain_id` INT NULL,
`quota_size_mb` INT NOT NULL DEFAULT 0,
`quota_used_mb` INT NOT NULL DEFAULT 0,
`quota_files` INT NOT NULL DEFAULT 0,
`quota_files_used` INT NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
UNIQUE KEY `ftp_quotas_ftp_user_unique` (`ftp_user`),
KEY `ftp_quotas_user_id` (`user_id`),
KEY `ftp_quotas_domain_id` (`domain_id`),
CONSTRAINT `ftp_quotas_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `loginSystem_administrator` (`id`) ON DELETE CASCADE,
CONSTRAINT `ftp_quotas_domain_id_fk` FOREIGN KEY (`domain_id`) REFERENCES `websiteFunctions_websites` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -15,7 +15,7 @@ app.controller('createFTPAccount', function ($scope, $http) {
$scope.generatedPasswordView = true;
$(document).ready(function () {
$( ".ftpDetails" ).hide();
$( ".ftpDetails, .account-details" ).hide();
$( ".ftpPasswordView" ).hide();
// Only use select2 if it's actually a function (avoids errors when Rocket Loader defers scripts)
@@ -27,8 +27,9 @@ app.controller('createFTPAccount', function ($scope, $http) {
$sel.on('select2:select', function (e) {
var data = e.params.data;
$scope.ftpDomain = data.text;
$scope.ftpDetails = false;
$scope.$apply();
$(".ftpDetails").show();
$(".ftpDetails, .account-details").show();
});
} else {
initNativeSelect();
@@ -42,19 +43,20 @@ app.controller('createFTPAccount', function ($scope, $http) {
function initNativeSelect() {
$('.create-ftp-acct-select').off('select2:select').on('change', function () {
$scope.ftpDomain = $(this).val();
$scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true;
$scope.$apply();
$(".ftpDetails").show();
$(".ftpDetails, .account-details").show();
});
}
});
$scope.showFTPDetails = function() {
if ($scope.ftpDomain && $scope.ftpDomain !== "") {
$(".ftpDetails").show();
$scope.ftpDetails = false;
$(".ftpDetails, .account-details").show();
} else {
$(".ftpDetails").hide();
$scope.ftpDetails = true;
$(".ftpDetails, .account-details").hide();
}
};

View File

@@ -0,0 +1,25 @@
# FTP Quotas Table Fix
## Problem
- **URL:** https://207.180.193.210:2087/ftp/quotaManagement
- **Error:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")`
The `FTPQuota` model in `websiteFunctions/models.py` uses `db_table = 'ftp_quotas'`, but the table had never been created in the database.
## Solution
1. **SQL:** `sql/create_ftp_quotas.sql` `CREATE TABLE IF NOT EXISTS ftp_quotas` with columns and FKs to `loginSystem_administrator` and `websiteFunctions_websites`.
2. **Deploy script:** `deploy-ftp-quotas-table.sh` Copies the SQL to `/usr/local/CyberCP/sql/` and runs it using Djangos DB connection (no password on command line).
## Deploy (already run)
```bash
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
```
## Manual run (if needed)
From repo root:
```bash
sudo bash deploy-ftp-quotas-table.sh [REPO_DIR] [CP_DIR]
```
Default `CP_DIR` is `/usr/local/CyberCP`.
After deployment, reload `/ftp/quotaManagement` in the browser.

View File

@@ -0,0 +1,83 @@
# Runtime vs Repo: What Belongs in cyberpanel-repo for 2.5.5-dev
## Goal
When users upgrade to **our** (master3395) 2.5.5-dev, the panel should look and behave the same. That means **default** look-and-feel and behavior must be defined in the repo, not only “generated” on the server.
---
## What is “runtime generated”?
On the live server, after install/upgrade you have:
1. **From the repo (clone/copy)**
All app code, templates, static sources, migrations, `version.txt`, default `settings.py`, etc.
→ This **should** be in the repo (and already is).
2. **Generated at install/upgrade**
- Python venv under `/usr/local/CyberCP/bin`, `lib`, `lib64`
- `collectstatic` output under `/usr/local/CyberCP/public/static`
- `version` table and `baseTemplate_cyberpanelcosmetic` row (if created by code/migrations)
- `lscpd` binary copy, symlinks, etc.
→ The **sources** that produce these (e.g. static sources, migrations) **should** be in the repo.
3. **Per-server / preserved**
- `CyberCP/settings.py` — upgrade **merges** only the `DATABASES` section from the old server; the rest (e.g. `INSTALLED_APPS`) comes from the **new** clone.
- `baseTemplate/static/baseTemplate/custom/` (custom CSS files)
- DB row `baseTemplate_cyberpanelcosmetic.MainDashboardCSS` (custom dashboard CSS)
- `.git/`, phpMyAdmin config, SnappyMail data, etc.
**Defaults** that define “how 2.5.5-dev looks” should be in the repo; **per-server overrides** stay on the server.
---
## What we need in the repo so 2.5.5-dev “looks the same”
- **Templates, static sources, JS/CSS**
Already in repo (e.g. `baseTemplate/`, `static/`). No change needed for “same look” unless you change the design.
- **Default `settings.py`**
Already in repo. Upgrade keeps DB credentials from the server and uses repo for everything else (e.g. `INSTALLED_APPS`).
So 2.5.5-dev behavior is driven by the repos `settings.py`.
- **Version**
`baseTemplate/views.py` has `VERSION = '2.5.5'`, `BUILD = 'dev'`. Repos `version.txt` is `{"version":"2.5.5","build":"dev"}`.
Upgrade also writes version into the DB. So version “same as 2.5.5-dev” is already defined in the repo.
- **Default “look” (cosmetic)**
- Code already creates a default `CyberPanelCosmetic` row with **empty** `MainDashboardCSS` if none exists (`baseTemplate/context_processors.py`, `plogical/httpProc.py`, `loginSystem/views.py`).
- If **your live server** has custom dashboard CSS (in DB or in `baseTemplate/static/baseTemplate/custom/`), that is **your** customization.
- To make “our 2.5.5-dev” ship with that same look as default, you have two options:
1. **Data migration**
Add a baseTemplate data migration that does:
- `CyberPanelCosmetic.objects.get_or_create(pk=1, defaults={'MainDashboardCSS': '<your default CSS>'})`
so every new/upgraded install gets that default look.
2. **Static default**
Put the CSS in a static file under `baseTemplate/static/` and include it in the base template so the default theme matches your live server.
- **Migrations**
All schema (and optional data) migrations must be in the repo so every 2.5.5-dev install/upgrade runs the same schema and, if you add it, the same default cosmetic data.
---
## What should **not** be in the repo
- **Secrets**: DB password, `SECRET_KEY`, API keys.
Keep in `settings.py` only placeholders or env reads; real values stay on the server (or in config.php / env per your rules).
- **User data**: sites, users, mail, backups.
These are per-server.
- **Generated artifacts**: venv, `collectstatic` output, compiled binaries.
Repo holds the **source**; install/upgrade generates these on the server.
---
## Summary
- **Yes:** “Runtime generated” **defaults** that define how 2.5.5-dev looks and behaves **should** be reflected in the repo (templates, static sources, migrations, default cosmetic logic or data).
- **Already in repo:** App code, default settings structure, version, static sources, migrations. So 2.5.5-dev upgrades already get the same **code** and **default look** (empty custom CSS).
- **Optional:** If your live server has a **specific** custom look (e.g. custom dashboard CSS), and you want that to be the **default** for everyone on 2.5.5-dev, add it to the repo via a data migration or default static CSS as above.
No change is **required** for “same look” unless you want to ship a non-empty default cosmetic (e.g. your current dashboard CSS) as part of 2.5.5-dev.

View File

@@ -0,0 +1,89 @@
# v2.5.5-dev: Fixes and Deploy Guide
This document lists fixes included in **v2.5.5-dev** and how to deploy them on a CyberPanel server.
---
## Version and cache busting
- **baseTemplate:** `CP_VERSION` in `baseTemplate/templates/baseTemplate/index.html` now uses `CYBERPANEL_FULL_VERSION` from context (from `baseTemplate/views.py`: `VERSION = '2.5.5'`, `BUILD = 'dev'`), so static URLs use `?v=2.5.5.dev` and cache busting matches the branch.
---
## 1. Email Limits page
- **Issue:** Raw Angular bindings (`{$ selectedEmail $}`) and `EmailLimitsNew` controller not registered.
- **Files:** `mailServer/mailserverManager.py`, `mailServer/templates/mailServer/EmailLimits.html`, `mailServer/static/mailServer/mailServer.js`, `mailServer/static/mailServer/emailLimitsController.js`, and mirrored under `static/mailServer/`.
- **Deploy:**
```bash
sudo bash /home/cyberpanel-repo/deploy-email-limits-fix.sh
```
- **Details:** See `to-do/EMAIL-LIMITS-DEPLOY-CHECKLIST.md`.
---
## 2. FTP Create Account page
- **Issue:** After selecting a website, the “FTP Account Details” form (username, password, path, quota) did not appear.
- **Files:** `ftp/templates/ftp/createFTPAccount.html` (inline script + polling + Angular scope sync), `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js`.
- **Deploy:**
```bash
sudo bash /home/cyberpanel-repo/deploy-ftp-create-account-fix.sh
```
- **After deploy:** Hard-refresh `/ftp/createFTPAccount` (Ctrl+Shift+R).
---
## 3. FTP Quota Management page
- **Issue:** `(1146, "Table 'cyberpanel.ftp_quotas' doesn't exist")` on `/ftp/quotaManagement`.
- **Files:** `sql/create_ftp_quotas.sql`, `websiteFunctions/models.py` (FTPQuota model already present).
- **Deploy:**
```bash
sudo bash /home/cyberpanel-repo/deploy-ftp-quotas-table.sh
```
- **Details:** See `to-do/FTP-QUOTAS-TABLE-FIX.md`.
---
## 4. mailUtilities indentation fix
- **File:** `plogical/mailUtilities.py` (indentation fix in DNS query try/except block).
- **Deploy:** Copy to `/usr/local/CyberCP/plogical/mailUtilities.py` and restart lscpd if needed.
---
## Deploy all fixes (in order)
Run on the server (e.g. from repo root):
```bash
sudo bash deploy-email-limits-fix.sh
sudo bash deploy-ftp-create-account-fix.sh
sudo bash deploy-ftp-quotas-table.sh
```
Then hard-refresh the FTP Create Account page in the browser. No need to restart lscpd after the FTP quotas table script (it only runs SQL).
---
## Files changed / added in v2.5.5-dev (fixes)
| Path | Description |
|------|-------------|
| `baseTemplate/templates/baseTemplate/index.html` | CP_VERSION from CYBERPANEL_FULL_VERSION |
| `mailServer/` (Email Limits) | Controller, template, getEmailsForDomain permission |
| `ftp/templates/ftp/createFTPAccount.html` | Inline fallback + polling for details form |
| `ftp/static/ftp/ftp.js`, `static/ftp/ftp.js`, `public/static/ftp/ftp.js` | showFTPDetails, select2/change handlers |
| `websiteFunctions/models.py` | FTPQuota model (table created via sql script) |
| `sql/create_ftp_quotas.sql` | CREATE TABLE ftp_quotas |
| `plogical/mailUtilities.py` | DNS block indentation fix |
| `deploy-email-limits-fix.sh` | Deploy Email Limits fix |
| `deploy-ftp-create-account-fix.sh` | Deploy FTP Create Account template |
| `deploy-ftp-quotas-table.sh` | Create ftp_quotas table |
---
## Optional (not committed by default)
- **CyberCP/urls.py:** If `emailMarketing` is commented out for local runserver, leave it uncommitted or revert before pushing so production keeps the route.