mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-06 17:56:30 +02:00
@@ -2559,7 +2559,7 @@
|
||||
<script src="{% static 'managePHP/managePHP.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'serverLogs/serverLogs.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'serverStatus/serverStatus.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=6" data-cfasync="false"></script>
|
||||
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=11" data-cfasync="false"></script>
|
||||
<script src="{% static 'emailPremium/emailPremium.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}&msModal=20260402d" data-cfasync="false"></script>
|
||||
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
|
||||
@@ -210,6 +210,9 @@ class ContainerManager(multi.Thread):
|
||||
data['memoryUsage'] = 0
|
||||
data['cpuUsage'] = 0
|
||||
|
||||
# JSON for Angular ng-init (flat map container_port -> host_port from DB)
|
||||
data['ports_json'] = json.dumps(data.get('ports', {}))
|
||||
|
||||
template = 'dockerManager/viewContainer.html'
|
||||
proc = httpProc(request, template, data, 'admin')
|
||||
return proc.render()
|
||||
@@ -588,14 +591,21 @@ class ContainerManager(multi.Thread):
|
||||
|
||||
image = data['image']
|
||||
tag = data['tag']
|
||||
force_update = bool(data.get('force_update'))
|
||||
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'installImageStatus': 0, 'error_message': "Image already installed"}
|
||||
if not self._validate_image_name(image):
|
||||
data_ret = {'installImageStatus': 0, 'error_message': 'Invalid image name format'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
|
||||
if not force_update:
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'installImageStatus': 0, 'error_message': "Image already installed"}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
|
||||
try:
|
||||
image = client.images.pull(image, tag=tag)
|
||||
@@ -636,14 +646,17 @@ class ContainerManager(multi.Thread):
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Check if image already exists
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'pullImageStatus': 0, 'error_message': "Image already exists locally"}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
force_update = bool(data.get('force_update'))
|
||||
|
||||
# Check if image already exists (skip when refreshing from registry)
|
||||
if not force_update:
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'pullImageStatus': 0, 'error_message': "Image already exists locally"}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
|
||||
# Pull the image
|
||||
try:
|
||||
@@ -1306,6 +1319,36 @@ class ContainerManager(multi.Thread):
|
||||
secure_log_error(e, 'container_operation')
|
||||
return 'Operation failed'
|
||||
|
||||
@staticmethod
|
||||
def _normalize_ports_for_save(ports_raw):
|
||||
"""
|
||||
Parse client-submitted port map for saveContainerSettings recreate.
|
||||
Returns (True, dict) with container_port -> host_port (str),
|
||||
(True, None) if ports_raw is None (caller should use DB),
|
||||
or (False, error_message).
|
||||
"""
|
||||
if ports_raw is None:
|
||||
return True, None
|
||||
if not isinstance(ports_raw, dict):
|
||||
return False, 'Invalid ports payload'
|
||||
out = {}
|
||||
for ck, cv in ports_raw.items():
|
||||
if not ck:
|
||||
continue
|
||||
ckey = str(ck).strip()
|
||||
if not ckey:
|
||||
continue
|
||||
if cv in (None, '', 'null'):
|
||||
continue
|
||||
try:
|
||||
hp = int(cv)
|
||||
except (ValueError, TypeError):
|
||||
return False, 'Invalid host port for %s' % ckey
|
||||
if hp < 1024 or hp > 65535:
|
||||
return False, 'Choose host port between 1024 and 65535'
|
||||
out[ckey] = str(hp)
|
||||
return True, out
|
||||
|
||||
def saveContainerSettings(self, userID=None, data=None):
|
||||
try:
|
||||
name = data['name']
|
||||
@@ -1375,6 +1418,15 @@ class ContainerManager(multi.Thread):
|
||||
continue
|
||||
volumes[volume['src']] = {'bind': volume['dest'],
|
||||
'mode': 'rw'}
|
||||
ports_for_recreate = json.loads(con.ports)
|
||||
if 'ports' in data and data.get('ports') is not None:
|
||||
ok_ports, norm_ports = self._normalize_ports_for_save(data.get('ports'))
|
||||
if not ok_ports:
|
||||
data_ret = {'saveSettingsStatus': 0, 'error_message': norm_ports}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
if norm_ports is not None:
|
||||
ports_for_recreate = norm_ports
|
||||
# Prepare data for recreate function
|
||||
data = {
|
||||
'name': name,
|
||||
@@ -1382,7 +1434,7 @@ class ContainerManager(multi.Thread):
|
||||
'image': con.image,
|
||||
'tag': con.tag,
|
||||
'env': envDict,
|
||||
'ports': json.loads(con.ports),
|
||||
'ports': ports_for_recreate,
|
||||
'volumes': volumes,
|
||||
'memory': con.memory
|
||||
}
|
||||
@@ -1395,6 +1447,7 @@ class ContainerManager(multi.Thread):
|
||||
|
||||
con.env = json.dumps(envDict)
|
||||
con.volumes = json.dumps(volumes)
|
||||
con.ports = json.dumps(ports_for_recreate)
|
||||
con.save()
|
||||
|
||||
data_ret = {'saveSettingsStatus': 1, 'error_message': 'None'}
|
||||
|
||||
@@ -1389,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.cName = "";
|
||||
$scope.status = "";
|
||||
$scope.savingSettings = false;
|
||||
$scope.currentPorts = {};
|
||||
$scope.initialPortsForSettings = {};
|
||||
$scope.loadingTop = false;
|
||||
$scope.statusInterval = null;
|
||||
$scope.statsInterval = null;
|
||||
@@ -1398,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.advancedEnvText = '';
|
||||
$scope.advancedEnvCount = 0;
|
||||
$scope.parsedEnvVars = {};
|
||||
|
||||
/** Flat map from DB (container_port -> host_port), set from template ng-init */
|
||||
$scope.serverPortMap = {};
|
||||
|
||||
$scope.initAngularPortsFromServer = function (obj) {
|
||||
if (obj === undefined || obj === null) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
if (angular.isString(obj)) {
|
||||
try {
|
||||
obj = angular.fromJson(obj);
|
||||
} catch (e1) {
|
||||
try {
|
||||
obj = JSON.parse(obj);
|
||||
} catch (e2) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!angular.isObject(obj)) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
$scope.serverPortMap = obj;
|
||||
$scope.ports = obj;
|
||||
};
|
||||
|
||||
// Auto-refresh status every 5 seconds
|
||||
$scope.startStatusMonitoring = function() {
|
||||
$scope.statusInterval = $interval(function() {
|
||||
@@ -2028,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
|
||||
$scope.refreshContainerInfo = function () {
|
||||
$timeout(function () {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$scope.addVolField = function () {
|
||||
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
|
||||
$scope.volListNumber = $scope.volListNumber + 1;
|
||||
@@ -2038,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
|
||||
new PNotify({
|
||||
title: 'Confirmation required',
|
||||
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#containerSettingLoading').show();
|
||||
url = "/docker/saveContainerSettings";
|
||||
$scope.savingSettings = true;
|
||||
@@ -2063,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
envConfirmation: $scope.envConfirmation,
|
||||
envList: finalEnvList,
|
||||
volList: $scope.volList,
|
||||
advancedEnvMode: $scope.advancedEnvMode
|
||||
advancedEnvMode: $scope.advancedEnvMode,
|
||||
ports: $scope.currentPorts || {}
|
||||
};
|
||||
|
||||
|
||||
@@ -2088,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
title: 'Settings Saved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -2213,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$("#commandModal").modal("show");
|
||||
};
|
||||
|
||||
// Port editing functionality
|
||||
$scope.showPortEditModal = function() {
|
||||
// Initialize current ports from container data
|
||||
// Port editing (in Container Settings modal)
|
||||
$scope.initSettingsPortsFromContainer = function () {
|
||||
$scope.currentPorts = {};
|
||||
if ($scope.ports) {
|
||||
for (var iport in $scope.ports) {
|
||||
var eport = $scope.ports[iport];
|
||||
if (eport && eport.length > 0) {
|
||||
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
|
||||
? $scope.serverPortMap
|
||||
: ($scope.ports || {});
|
||||
if (src && typeof src === 'object') {
|
||||
for (var iport in src) {
|
||||
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
|
||||
continue;
|
||||
}
|
||||
var eport = src[iport];
|
||||
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
|
||||
$scope.currentPorts[iport] = eport[0].HostPort;
|
||||
} else if (eport !== undefined && eport !== null && eport !== '') {
|
||||
$scope.currentPorts[iport] = String(eport);
|
||||
}
|
||||
}
|
||||
}
|
||||
$("#portEditModal").modal("show");
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function() {
|
||||
$scope.portsJsonSnapshot = function (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return '{}';
|
||||
}
|
||||
var keys = Object.keys(obj).sort();
|
||||
var flat = {};
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var v = obj[k];
|
||||
flat[k] = v === null || v === undefined ? '' : String(v);
|
||||
}
|
||||
return JSON.stringify(flat);
|
||||
};
|
||||
|
||||
$scope.portsDirty = function () {
|
||||
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
|
||||
};
|
||||
|
||||
$scope.showPortEditModal = function () {
|
||||
$scope.initSettingsPortsFromContainer();
|
||||
$("#settings").modal("show");
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function () {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
|
||||
if (containerPort) {
|
||||
$scope.currentPorts[containerPort] = '';
|
||||
@@ -2236,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removePortMapping = function(containerPort) {
|
||||
$scope.removePortMapping = function (containerPort) {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
if (confirm('Are you sure you want to remove this port mapping?')) {
|
||||
delete $scope.currentPorts[containerPort];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updatePortMappings = function() {
|
||||
$("#portEditLoading").show();
|
||||
$scope.updatePortMappings = function () {
|
||||
$scope.updatingPorts = true;
|
||||
|
||||
var url = "/docker/updateContainerPorts";
|
||||
@@ -2259,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$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();
|
||||
$("#settings").modal("hide");
|
||||
new PNotify({
|
||||
title: 'Success',
|
||||
text: 'Port mappings updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.refreshContainerInfo();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
@@ -2279,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Error updating port mappings: ' + error.data.error_message,
|
||||
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
@@ -2388,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
$scope.showingSearch = false;
|
||||
$("#searchResult").hide();
|
||||
|
||||
function dockerHistoryFormatSize(raw) {
|
||||
var n = parseInt(raw, 10);
|
||||
if (isNaN(n) || n < 0) {
|
||||
return (raw === undefined || raw === null) ? '' : String(raw);
|
||||
}
|
||||
if (n === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
||||
var v = n / Math.pow(1024, i);
|
||||
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
|
||||
function dockerHistoryFormatCreated(raw) {
|
||||
if (raw === undefined || raw === null) {
|
||||
return '';
|
||||
}
|
||||
var n = Number(raw);
|
||||
if (isNaN(n)) {
|
||||
return String(raw);
|
||||
}
|
||||
var sec = n;
|
||||
if (n > 1e14) {
|
||||
sec = Math.floor(n / 1e9);
|
||||
} else if (n > 1e12) {
|
||||
sec = Math.floor(n / 1e6);
|
||||
}
|
||||
var d = new Date(sec * 1000);
|
||||
if (isNaN(d.getTime())) {
|
||||
return String(raw);
|
||||
}
|
||||
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
||||
}
|
||||
|
||||
function dockerHistoryEnrichLayer(h) {
|
||||
var o = angular.extend({}, h);
|
||||
o.SizeHuman = dockerHistoryFormatSize(h.Size);
|
||||
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
|
||||
return o;
|
||||
}
|
||||
|
||||
$scope.pullImage = function (image, tag) {
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.installImageStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Image pulled successfully',
|
||||
text: 'Reloading...',
|
||||
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading…',
|
||||
type: 'success'
|
||||
});
|
||||
location.reload()
|
||||
@@ -2424,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
url = "/docker/installImage";
|
||||
var data = {
|
||||
image: image,
|
||||
tag: tag
|
||||
tag: tag,
|
||||
force_update: true
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
@@ -2445,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
}
|
||||
|
||||
$scope.refreshLocalImage = function (rowId, imageName) {
|
||||
var sel = document.getElementById(String(rowId));
|
||||
if (!sel || sel.selectedIndex < 0) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var raw = sel.options[sel.selectedIndex].text;
|
||||
if (!raw) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var li = raw.lastIndexOf(':');
|
||||
var tag = li > -1 ? raw.substring(li + 1) : raw;
|
||||
$scope.pullImage(imageName, tag);
|
||||
}
|
||||
|
||||
$scope.searchImages = function () {
|
||||
console.log($scope.searchString);
|
||||
if (!$scope.searchString) {
|
||||
@@ -2580,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
$scope.getHistory = function (counter) {
|
||||
$('#imageLoading').show();
|
||||
var name = $("#" + counter).val()
|
||||
var sel = $("#" + counter);
|
||||
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
|
||||
|
||||
url = "/docker/getImageHistory";
|
||||
|
||||
@@ -2600,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
if (response.data.imageHistoryStatus === 1) {
|
||||
$('#history').modal('show');
|
||||
$scope.historyList = response.data.history;
|
||||
var raw = response.data.history || [];
|
||||
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
|
||||
@@ -375,8 +375,20 @@
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.modal-dialog-history {
|
||||
width: 96vw;
|
||||
max-width: min(96vw, 1400px);
|
||||
margin: 1.75rem auto;
|
||||
}
|
||||
|
||||
.history-modal-scroll {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
background: var(--bg-hover, #f8f9ff);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@@ -390,13 +402,74 @@
|
||||
color: var(--text-primary, #1e293b);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.history-table th.history-col-size {
|
||||
text-align: right;
|
||||
width: 9%;
|
||||
}
|
||||
|
||||
.history-table th.history-col-created {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.history-table th.history-col-comment {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.history-table th.history-col-id {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.history-table td {
|
||||
padding: 0.75rem;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||
word-break: break-all;
|
||||
vertical-align: top;
|
||||
word-break: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.history-table td.history-col-id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
.history-table td.history-col-cmd {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.history-layer-cmd {
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
max-height: 14rem;
|
||||
max-width: 100%;
|
||||
background: var(--bg-primary, #fff);
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.6rem;
|
||||
}
|
||||
|
||||
.history-table td.history-col-created {
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.history-table td.history-col-comment {
|
||||
overflow-x: auto;
|
||||
max-width: 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.history-table td.history-col-size {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
@@ -583,6 +656,11 @@
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn-primary" title="{% trans 'Pull latest from registry for the selected tag. Running containers are unchanged until recreated.' %}"
|
||||
ng-click="refreshLocalImage({{ forloop.counter }}, '{{ image.name|escapejs }}')">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
{% trans "Refresh" %}
|
||||
</button>
|
||||
<button class="btn-info" title="{% trans 'View History' %}"
|
||||
ng-click="getHistory({{ forloop.counter }})">
|
||||
<i class="fas fa-history"></i>
|
||||
@@ -603,7 +681,7 @@
|
||||
|
||||
<!-- History Modal -->
|
||||
<div id="history" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-dialog modal-lg modal-dialog-history">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
@@ -614,26 +692,28 @@
|
||||
style="font-size: 1.5rem; background: transparent; border: none;">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="history-modal-scroll">
|
||||
<table class="history-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th class="history-col-id">{% trans "ID" %}</th>
|
||||
<th>{% trans "Created By" %}</th>
|
||||
<th>{% trans "Created" %}</th>
|
||||
<th>{% trans "Comment" %}</th>
|
||||
<th>{% trans "Size" %}</th>
|
||||
<th class="history-col-created">{% trans "Created" %}</th>
|
||||
<th class="history-col-comment">{% trans "Comment" %}</th>
|
||||
<th class="history-col-size">{% trans "Size" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="history in historyList track by $index">
|
||||
<td ng-bind="history.Id"></td>
|
||||
<td ng-bind="history.CreatedBy"></td>
|
||||
<td ng-bind="history.Created"></td>
|
||||
<td ng-bind="history.Comment"></td>
|
||||
<td ng-bind="history.Size"></td>
|
||||
<td class="history-col-id" ng-bind="history.Id"></td>
|
||||
<td class="history-col-cmd"><pre class="history-layer-cmd" ng-bind="history.CreatedBy"></pre></td>
|
||||
<td class="history-col-created" ng-bind="history.CreatedDisplay"></td>
|
||||
<td class="history-col-comment" ng-bind="history.Comment"></td>
|
||||
<td class="history-col-size"><span ng-bind="history.SizeHuman"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" data-dismiss="modal">
|
||||
|
||||
@@ -589,7 +589,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modern-container" ng-controller="viewContainer">
|
||||
<div class="modern-container" ng-controller="viewContainer" ng-init='initAngularPortsFromServer({{ ports_json|safe }})'>
|
||||
<div class="page-header">
|
||||
<div class="container-header">
|
||||
<div class="container-icon-large">
|
||||
@@ -738,7 +738,7 @@
|
||||
<div class="action-text">{% trans "Remove" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-btn" data-toggle="modal" data-target="#settings">
|
||||
<div class="action-btn" data-toggle="modal" data-target="#settings" ng-click="initSettingsPortsFromContainer()">
|
||||
<i class="fas fa-sliders-h action-icon" style="color: #6366f1;"></i>
|
||||
<div class="action-text">{% trans "Settings" %}</div>
|
||||
</div>
|
||||
@@ -762,11 +762,6 @@
|
||||
<i class="fas fa-code action-icon" style="color: #10b981;"></i>
|
||||
<div class="action-text">{% trans "Run Command" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-btn" ng-click="showPortEditModal()" ng-disabled="status!='running'">
|
||||
<i class="fas fa-edit action-icon" style="color: #8b5cf6;"></i>
|
||||
<div class="action-text">{% trans "Edit Ports" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -826,6 +821,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Port mappings" %}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>{% trans "Important:" %}</strong>
|
||||
{% trans "Changing host ports requires checking the confirmation box below; the container will be recreated." %}
|
||||
</div>
|
||||
<div class="port-mapping-container">
|
||||
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
|
||||
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
|
||||
</div>
|
||||
<div style="color: #007bff; font-size: 1.25rem;">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
|
||||
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
|
||||
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535" ng-disabled="!envConfirmation">
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; height: 38px;">
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
|
||||
ng-show="Object.keys(currentPorts).length > 0" title="{% trans 'Remove port mapping' %}" ng-disabled="!envConfirmation">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
|
||||
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
|
||||
<p>{% trans "No port mappings configured" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 0.5rem;">
|
||||
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()" ng-disabled="!envConfirmation">
|
||||
<i class="fas fa-plus"></i>
|
||||
{% trans "Add Port Mapping" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -834,7 +874,7 @@
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input ng-model="envConfirmation" type="checkbox">
|
||||
<strong>{% trans "I understand that editing ENV or Volumes will recreate the container" %}</strong>
|
||||
<strong>{% trans "I understand that editing ENV, Volumes, or Ports will recreate the container" %}</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -856,10 +896,10 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label class="switch" style="position: relative; display: inline-block; width: 60px; height: 34px;">
|
||||
<input type="checkbox" ng-model="advancedEnvMode" ng-change="toggleEnvMode()" style="opacity: 0; width: 0; height: 0;">
|
||||
<span class="slider" style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px;">
|
||||
<span class="slider-thumb" style="position: absolute; content: ''; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"></span>
|
||||
<label class="switch cp-adv-env-switch" style="position: relative; display: inline-block; width: 60px; height: 34px;">
|
||||
<input type="checkbox" ng-model="advancedEnvMode" ng-change="toggleEnvMode()">
|
||||
<span class="slider cp-adv-env-slider">
|
||||
<span class="slider-thumb cp-adv-env-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -1246,77 +1286,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port Editing Modal -->
|
||||
<div id="portEditModal" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<i class="fas fa-edit" style="margin-right: 0.5rem;"></i>
|
||||
{% trans "Edit Port Mappings" %}
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
style="font-size: 1.5rem; background: transparent; border: none;">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>{% trans "Important:" %}</strong> {% trans "Editing port mappings will temporarily stop and recreate the container. Any unsaved data in the container may be lost." %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">
|
||||
<i class="fas fa-plug" style="margin-right: 0.5rem;"></i>
|
||||
{% trans "Port Mappings" %}
|
||||
</label>
|
||||
<div class="port-mapping-container">
|
||||
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
|
||||
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
|
||||
</div>
|
||||
<div style="color: #007bff; font-size: 1.25rem;">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
|
||||
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
|
||||
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535">
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; height: 38px;">
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
|
||||
ng-show="Object.keys(currentPorts).length > 1" title="{% trans 'Remove port mapping' %}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
|
||||
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
|
||||
<p>{% trans "No port mappings configured" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()">
|
||||
<i class="fas fa-plus"></i>
|
||||
{% trans "Add Port Mapping" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<img id="portEditLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="updatingPorts" ng-click="updatePortMappings()">
|
||||
<i class="fas fa-save"></i> {% trans "Update Port Mappings" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
<i class="fas fa-times"></i> {% trans "Cancel" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -1439,13 +1408,79 @@
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.compose-benefits {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.compose-actions {
|
||||
margin-top: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid rgba(0, 123, 255, 0.15);
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Container Settings modal: checkbox alignment + advanced mode switch */
|
||||
#settings .checkbox label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
padding-left: 0;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#settings .checkbox label input[type="checkbox"] {
|
||||
position: static !important;
|
||||
margin-left: 0 !important;
|
||||
margin-top: 0.2rem;
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-thumb {
|
||||
position: absolute;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: #fff;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-switch input:checked + .cp-adv-env-slider {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-switch input:checked + .cp-adv-env-slider .cp-adv-env-thumb {
|
||||
left: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.compose-actions .btn {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
* Created by usman on 6/22/18.
|
||||
*/
|
||||
|
||||
/* Ensure CyberCP Angular module exists (system-status.js normally sets window.app in <head>) */
|
||||
(function () {
|
||||
if (typeof window === 'undefined' || typeof angular === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (typeof window.app !== 'undefined' && window.app) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.app = angular.module('CyberCP');
|
||||
} catch (e) {
|
||||
try {
|
||||
window.app = angular.module('CyberCP', []);
|
||||
} catch (e2) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
var app = window.app;
|
||||
|
||||
/* Java script code to list accounts */
|
||||
|
||||
app.controller('listDomains', function ($scope, $http) {
|
||||
|
||||
@@ -611,7 +611,7 @@
|
||||
<div ng-hide="RspamdNotifyBox">
|
||||
<div ng-hide="failedToStartInallation" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Failed to start installation. Error message:" %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Failed to start installation. Error message:" %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="couldNotConnect" class="alert alert-danger">
|
||||
@@ -621,7 +621,7 @@
|
||||
|
||||
<div ng-hide="installationFailed" class="alert alert-danger">
|
||||
<i class="fas fa-times-circle alert-icon"></i>
|
||||
<span>{% trans "Installation failed." %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Installation failed." %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="RspamdSuccessfullyInstalled" class="alert alert-success">
|
||||
@@ -637,7 +637,7 @@
|
||||
<h3 class="install-log-title">{% trans "Installation Progress" %}</h3>
|
||||
<span ng-hide="RspamdLoading" class="loading-spinner"></span>
|
||||
</div>
|
||||
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly>{{ requestData }}</textarea>
|
||||
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -663,20 +663,20 @@
|
||||
</button>
|
||||
</div>
|
||||
<p style="color: var(--text-secondary, #64748b); font-size: 14px; margin-top: 16px; line-height: 1.6;">
|
||||
{% trans "Opens the official Rspamd web interface in a new tab (path /emailPremium/Rspamd/ui/ — proxied through CyberPanel, admin session required)." %}
|
||||
{% trans "Opens the Rspamd controller UI over HTTPS using the same hostname and port as CyberPanel (path /emailPremium/Rspamd/ui/). Rspamd itself only speaks HTTP on 127.0.0.1:11334; the panel reverse-proxies it so you do not need https://YOUR_SERVER_IP:11334 (that URL is not served by Rspamd)." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="content-section">
|
||||
<h2 class="section-title">{% trans "Alternative: SSH tunnel" %}</h2>
|
||||
<p style="color: var(--text-secondary, #64748b); font-size: 14px; line-height: 1.6;">
|
||||
{% trans "If the proxied UI misbehaves, connect to port 11334 on the server via SSH and use your local browser." %}
|
||||
{% trans "If the proxied UI misbehaves, forward port 11334 to your machine and open the link below (HTTP on localhost only — not HTTPS)." %}
|
||||
</p>
|
||||
<pre style="background: var(--bg-secondary,#f8f9ff); padding: 12px; border-radius: 8px; font-size: 13px; overflow-x: auto; border: 1px solid var(--border-light, #e8e9ff);">ssh -N -L 11334:127.0.0.1:11334 root@{{ ipAddress }}</pre>
|
||||
<p style="margin-top: 12px;">
|
||||
<a href="http://127.0.0.1:11334/" target="_blank" rel="noopener noreferrer" class="action-btn secondary">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
{% trans "Open Rspamd UI (when tunnel is active)" %}
|
||||
{% trans "Open Rspamd UI (when tunnel is active — http://127.0.0.1:11334/)" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -772,7 +772,7 @@
|
||||
<div style="margin-top: 20px;">
|
||||
<div ng-hide="failedToSave" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Failed to save RSPAMD configurations. Error message:" %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Failed to save RSPAMD configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="successfullySaved" class="alert alert-success">
|
||||
@@ -848,7 +848,7 @@
|
||||
<div style="margin-top: 20px;">
|
||||
<div ng-hide="ClamAVfailedToSave" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Failed to save ClamAV configurations. Error message:" %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Failed to save ClamAV configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="ClamAVsuccessfullySaved" class="alert alert-success">
|
||||
@@ -906,7 +906,7 @@
|
||||
<div style="margin-top: 20px;">
|
||||
<div ng-hide="postfixfailedToSave" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Failed to save Postfix configurations. Error message:" %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Failed to save Postfix configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="postfixsuccessfullySaved" class="alert alert-success">
|
||||
@@ -964,7 +964,7 @@
|
||||
<div style="margin-top: 20px;">
|
||||
<div ng-hide="RedisfailedToSave" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Failed to save Redis configurations. Error message:" %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Failed to save Redis configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="RedissuccessfullySaved" class="alert alert-success">
|
||||
@@ -989,7 +989,7 @@
|
||||
<div ng-hide="uninstallRspamdNotifyBox">
|
||||
<div ng-hide="failedToStartInallation" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Failed to start uninstallation. Error message:" %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Failed to start uninstallation. Error message:" %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="couldNotConnect" class="alert alert-danger">
|
||||
@@ -999,7 +999,7 @@
|
||||
|
||||
<div ng-hide="installationFailed" class="alert alert-danger">
|
||||
<i class="fas fa-times-circle alert-icon"></i>
|
||||
<span>{% trans "Installation failed." %} {$ errorMessage $}</span>
|
||||
<span>{% trans "Installation failed." %} <span ng-bind="errorMessage"></span></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="RspamdSuccessfullyInstalled" class="alert alert-success">
|
||||
@@ -1015,7 +1015,7 @@
|
||||
<h3 class="install-log-title">{% trans "Uninstallation Progress" %}</h3>
|
||||
<span ng-hide="RspamdLoading" class="loading-spinner"></span>
|
||||
</div>
|
||||
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly>{{ requestData }}</textarea>
|
||||
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1052,7 +1052,7 @@
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<textarea ng-model="RspamdlogsData" class="log-textarea" rows="20" readonly>{{ Rspamdlogs }}</textarea>
|
||||
<textarea ng-model="RspamdlogsData" class="log-textarea" rows="20" readonly></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="modal-btn secondary" data-dismiss="modal">
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import time
|
||||
import http.client
|
||||
|
||||
from django.shortcuts import redirect
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from loginSystem.models import Administrator
|
||||
from mailServer.models import Domains, EUsers
|
||||
@@ -1244,18 +1246,177 @@ def Rspamd(request):
|
||||
|
||||
checkIfRspamdInstalled = 0
|
||||
|
||||
ipFile = "/etc/cyberpanel/machineIP"
|
||||
f = open(ipFile)
|
||||
ipData = f.read()
|
||||
ipAddress = ipData.split('\n', 1)[0]
|
||||
ipAddress = '127.0.0.1'
|
||||
try:
|
||||
ipFile = "/etc/cyberpanel/machineIP"
|
||||
with open(ipFile, 'r') as f:
|
||||
ipData = f.read()
|
||||
first_line = ipData.split('\n', 1)[0].strip()
|
||||
if first_line:
|
||||
ipAddress = first_line
|
||||
except (OSError, IOError, IndexError):
|
||||
pass
|
||||
|
||||
if mailUtilities.checkIfRspamdInstalled() == 1:
|
||||
checkIfRspamdInstalled = 1
|
||||
|
||||
rspamd_ui_url = request.build_absolute_uri('/emailPremium/Rspamd/ui/')
|
||||
|
||||
proc = httpProc(request, 'emailPremium/Rspamd.html',
|
||||
{'checkIfRspamdInstalled': checkIfRspamdInstalled, 'ipAddress': ipAddress}, 'admin')
|
||||
{
|
||||
'checkIfRspamdInstalled': checkIfRspamdInstalled,
|
||||
'ipAddress': ipAddress,
|
||||
'rspamd_ui_url': rspamd_ui_url,
|
||||
}, 'admin')
|
||||
return proc.render()
|
||||
|
||||
###Rspamd
|
||||
|
||||
_RSPAMD_UPSTREAM = ('127.0.0.1', 11334)
|
||||
_RSPAMD_HOP_RESPONSE = frozenset({
|
||||
'connection', 'transfer-encoding', 'keep-alive', 'proxy-authenticate',
|
||||
'proxy-authorization', 'te', 'trailers', 'upgrade', 'content-encoding',
|
||||
})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def rspamd_ui_proxy(request, subpath=None):
|
||||
"""Reverse-proxy Rspamd controller UI (localhost:11334) for logged-in admins only."""
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
if currentACL['admin'] != 1:
|
||||
return ACLManager.loadError()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
if mailUtilities.checkIfRspamdInstalled() != 1:
|
||||
return HttpResponse(
|
||||
'Rspamd is not installed.',
|
||||
status=503,
|
||||
content_type='text/plain; charset=utf-8',
|
||||
)
|
||||
|
||||
proxy_base = request.build_absolute_uri('/emailPremium/Rspamd/ui').rstrip('/')
|
||||
path = '/'
|
||||
if subpath:
|
||||
path = '/' + subpath.lstrip('/')
|
||||
q = request.META.get('QUERY_STRING', '')
|
||||
if q:
|
||||
full_path = path + '?' + q
|
||||
else:
|
||||
full_path = path
|
||||
|
||||
forward_method = request.method
|
||||
if forward_method == 'HEAD':
|
||||
forward_method = 'GET'
|
||||
|
||||
body = None
|
||||
if forward_method in ('POST', 'PUT', 'PATCH', 'DELETE'):
|
||||
body = request.body
|
||||
|
||||
headers = {}
|
||||
acc = request.META.get('HTTP_ACCEPT')
|
||||
if acc:
|
||||
headers['Accept'] = acc
|
||||
al = request.META.get('HTTP_ACCEPT_LANGUAGE')
|
||||
if al:
|
||||
headers['Accept-Language'] = al
|
||||
ua = request.META.get('HTTP_USER_AGENT')
|
||||
if ua:
|
||||
headers['User-Agent'] = ua
|
||||
auth = request.META.get('HTTP_AUTHORIZATION')
|
||||
if auth:
|
||||
headers['Authorization'] = auth
|
||||
ct = request.META.get('CONTENT_TYPE')
|
||||
if ct and forward_method in ('POST', 'PUT', 'PATCH'):
|
||||
headers['Content-Type'] = ct
|
||||
cookie = request.META.get('HTTP_COOKIE')
|
||||
if cookie:
|
||||
headers['Cookie'] = cookie
|
||||
xhr = request.META.get('HTTP_X_REQUESTED_WITH')
|
||||
if xhr:
|
||||
headers['X-Requested-With'] = xhr
|
||||
|
||||
# Rspamd 3.8+ controller uses custom headers (not only query params). The stock
|
||||
# proxy whitelist omitted these, which breaks getmap/savemap and can break saveactions.
|
||||
_rspamd_hdr_map = (
|
||||
('HTTP_PASSWORD', 'Password'),
|
||||
('HTTP_MAP', 'Map'),
|
||||
('HTTP_IP', 'IP'),
|
||||
('HTTP_USER', 'User'),
|
||||
('HTTP_FROM', 'From'),
|
||||
('HTTP_RCPT', 'Rcpt'),
|
||||
('HTTP_HELO', 'Helo'),
|
||||
('HTTP_HOSTNAME', 'Hostname'),
|
||||
('HTTP_PASS', 'Pass'),
|
||||
('HTTP_CLASSIFIER', 'classifier'),
|
||||
('HTTP_CLASS', 'class'),
|
||||
('HTTP_FLAG', 'flag'),
|
||||
('HTTP_WEIGHT', 'weight'),
|
||||
('HTTP_HASH', 'Hash'),
|
||||
)
|
||||
for meta_key, out_name in _rspamd_hdr_map:
|
||||
val = request.META.get(meta_key)
|
||||
if val:
|
||||
headers[out_name] = val
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = http.client.HTTPConnection(
|
||||
_RSPAMD_UPSTREAM[0], _RSPAMD_UPSTREAM[1], timeout=120,
|
||||
)
|
||||
conn.request(forward_method, full_path, body=body, headers=headers)
|
||||
upstream = conn.getresponse()
|
||||
data = upstream.read()
|
||||
status = upstream.status
|
||||
except (ConnectionRefusedError, OSError, http.client.HTTPException) as _e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(
|
||||
'rspamd_ui_proxy upstream error: %s' % (type(_e).__name__,),
|
||||
)
|
||||
return HttpResponse(
|
||||
'Could not reach Rspamd on 127.0.0.1:11334. Is rspamd running?',
|
||||
status=502,
|
||||
content_type='text/plain; charset=utf-8',
|
||||
)
|
||||
finally:
|
||||
if conn is not None:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if request.method == 'HEAD':
|
||||
out = HttpResponse(status=status)
|
||||
data = b''
|
||||
else:
|
||||
out = HttpResponse(data, status=status)
|
||||
|
||||
for hdr, val in upstream.getheaders():
|
||||
key = hdr.lower()
|
||||
if key in _RSPAMD_HOP_RESPONSE:
|
||||
continue
|
||||
if key == 'location':
|
||||
val = _rewrite_rspamd_location(val, proxy_base)
|
||||
if request.method == 'HEAD' and key == 'content-length':
|
||||
continue
|
||||
out[hdr] = val
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _rewrite_rspamd_location(location, proxy_base):
|
||||
if not location:
|
||||
return location
|
||||
if location.startswith('http://127.0.0.1:11334'):
|
||||
return proxy_base + location[len('http://127.0.0.1:11334'):]
|
||||
if location.startswith('http://[::1]:11334'):
|
||||
return proxy_base + location[len('http://[::1]:11334'):]
|
||||
if location.startswith('/') and not location.startswith('//'):
|
||||
return proxy_base + location
|
||||
return location
|
||||
|
||||
|
||||
def installRspamd(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
|
||||
@@ -1475,8 +1475,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Firewall Rules Pagination (show when there are rules or a total count) -->
|
||||
<div class="pagination-bar" ng-if="(rules && rules.length > 0) || (rulesTotalCount > 0)">
|
||||
<!-- Firewall Rules Pagination: use ng-show (not ng-if) so ng-model binds to firewallController; ng-if creates a child scope and breaks Per page / Go to page. -->
|
||||
<div class="pagination-bar" ng-show="(rules && rules.length > 0) || (rulesTotalCount > 0)">
|
||||
<div class="pagination-info">
|
||||
<span>{% trans "Showing" %} {$ rulesRangeStart() || 0 $} – {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $}</span>
|
||||
<span class="pagination-size">
|
||||
@@ -1680,8 +1680,8 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Banned IPs Pagination (show when there are rows or a total count) -->
|
||||
<div class="pagination-bar" ng-if="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
|
||||
<!-- Banned IPs Pagination: ng-show avoids child scope from ng-if (same ng-model issue as rules). -->
|
||||
<div class="pagination-bar" ng-show="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
|
||||
<div class="pagination-info">
|
||||
<span>{% trans "Showing" %} {$ bannedRangeStart() || 0 $} – {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $}</span>
|
||||
<span class="pagination-size banned-ips-per-page">
|
||||
|
||||
@@ -4839,6 +4839,13 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h
|
||||
command = f'/usr/local/lsws/lsphp80/bin/php /usr/local/CyberCP/snappymail_cyberpanel.php'
|
||||
preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
|
||||
try:
|
||||
from plogical.snappymail_plugin_utilities import install_and_enable_list_unsubscribe_header_plugin
|
||||
if install_and_enable_list_unsubscribe_header_plugin():
|
||||
logging.InstallLog.writeToFile("SnappyMail list-unsubscribe-header plugin installed and enabled", 0)
|
||||
except BaseException as plug_msg:
|
||||
logging.InstallLog.writeToFile("Warning: list-unsubscribe SnappyMail plugin: " + str(plug_msg), 0)
|
||||
|
||||
|
||||
except BaseException as msg:
|
||||
logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [downoad_and_install_snappymail]")
|
||||
|
||||
83
install/snappymail/plugins/list-unsubscribe-header/index.php
Normal file
83
install/snappymail/plugins/list-unsubscribe-header/index.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Bundled with CyberPanel (see plogical/snappymail_plugin_utilities.py).
|
||||
* Upstream: https://github.com/master3395/snappymail-list-unsubscribe-header
|
||||
*
|
||||
* Adds List-Unsubscribe and List-Unsubscribe-Post on outbound mail (RFC 2369 / RFC 8058).
|
||||
*/
|
||||
class ListUnsubscribeHeaderPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = 'List-Unsubscribe headers',
|
||||
AUTHOR = 'Master3395',
|
||||
URL = 'https://newstargeted.com/',
|
||||
VERSION = '1.1.0',
|
||||
RELEASE = '2026-04-11',
|
||||
REQUIRED = '2.0.0',
|
||||
DESCRIPTION = 'Adds List-Unsubscribe and List-Unsubscribe-Post for bulk/mailing-list best practices.';
|
||||
|
||||
public function Init() : void
|
||||
{
|
||||
$this->addHook('filter.send-message', 'FilterSendMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function configMapping() : array
|
||||
{
|
||||
return array(
|
||||
\RainLoop\Plugins\Property::NewInstance('unsubscribe_https_url')
|
||||
->SetLabel('HTTPS unsubscribe URL')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::URL)
|
||||
->SetDescription('Must be https:// — used in List-Unsubscribe (RFC 8058 one-click target).')
|
||||
->SetDefaultValue('https://newstargeted.com/list-unsubscribe-endpoint.php'),
|
||||
\RainLoop\Plugins\Property::NewInstance('mailto_unsubscribe')
|
||||
->SetLabel('Unsubscribe mailto address')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
|
||||
->SetDescription('Email only (e.g. postmaster@example.com) or full mailto:user@example.com')
|
||||
->SetDefaultValue('postmaster@newstargeted.com'),
|
||||
\RainLoop\Plugins\Property::NewInstance('one_click_post')
|
||||
->SetLabel('Send List-Unsubscribe-Post (one-click)')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||
->SetDescription('RFC 8058 — disable if your HTTPS URL does not accept POST.')
|
||||
->SetDefaultValue(true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \MailSo\Mime\Message $oMessage
|
||||
*/
|
||||
public function FilterSendMessage(&$oMessage) : void
|
||||
{
|
||||
if ($oMessage instanceof \MailSo\Mime\Message) {
|
||||
$sHttps = \trim((string) $this->Config()->Get('plugin', 'unsubscribe_https_url', 'https://newstargeted.com/list-unsubscribe-endpoint.php'));
|
||||
$sMailRaw = \trim((string) $this->Config()->Get('plugin', 'mailto_unsubscribe', 'postmaster@newstargeted.com'));
|
||||
$bOneClick = (bool) $this->Config()->Get('plugin', 'one_click_post', true);
|
||||
|
||||
if ($sHttps === '' || !\preg_match('#^https://#i', $sHttps)) {
|
||||
$sHttps = 'https://newstargeted.com/list-unsubscribe-endpoint.php';
|
||||
}
|
||||
if ($sMailRaw === '') {
|
||||
$sMailto = 'mailto:postmaster@newstargeted.com';
|
||||
} elseif (\stripos($sMailRaw, 'mailto:') === 0) {
|
||||
$sMailto = $sMailRaw;
|
||||
} else {
|
||||
$sMailto = 'mailto:' . $sMailRaw;
|
||||
}
|
||||
if (!\preg_match('#^mailto:[^<>\s]+$#i', $sMailto)) {
|
||||
$sMailto = 'mailto:postmaster@newstargeted.com';
|
||||
}
|
||||
|
||||
$sListUnsub = '<' . $sHttps . '>, <' . $sMailto . '>';
|
||||
$oMessage->SetCustomHeader(
|
||||
\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE,
|
||||
$sListUnsub
|
||||
);
|
||||
if ($bOneClick) {
|
||||
$oMessage->SetCustomHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
plogical/snappymail_plugin_utilities.py
Normal file
136
plogical/snappymail_plugin_utilities.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Bundle and enable the SnappyMail list-unsubscribe-header plugin (CyberPanel).
|
||||
|
||||
Data may live under snappymail/ or legacy rainloop/ until migration completes.
|
||||
Upstream plugin: https://github.com/master3395/snappymail-list-unsubscribe-header
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
PLUGIN_ID_LIST_UNSUBSCRIBE = 'list-unsubscribe-header'
|
||||
BUNDLE_REL = 'install/snappymail/plugins/list-unsubscribe-header/index.php'
|
||||
CYBERCP_ROOT = '/usr/local/CyberCP'
|
||||
|
||||
DATA_ROOT_CANDIDATES = (
|
||||
'/usr/local/lscp/cyberpanel/snappymail/data',
|
||||
'/usr/local/lscp/cyberpanel/rainloop/data',
|
||||
)
|
||||
|
||||
|
||||
def bundled_list_unsubscribe_src():
|
||||
return os.path.join(CYBERCP_ROOT, BUNDLE_REL)
|
||||
|
||||
|
||||
def _merge_enabled_list_line(line, plugin_id):
|
||||
"""Merge plugin_id into SnappyMail enabled_list = \"...\" line."""
|
||||
m = re.match(r'^(\s*enabled_list\s*=\s*")([^"]*)("\s*)\r?$', line)
|
||||
if not m:
|
||||
return line
|
||||
inner = m.group(2)
|
||||
parts = [p.strip() for p in inner.split(',') if p.strip()]
|
||||
if plugin_id not in parts:
|
||||
parts.append(plugin_id)
|
||||
inner_new = ','.join(parts)
|
||||
return m.group(1) + inner_new + m.group(3) + ('\n' if line.endswith('\n') else '')
|
||||
|
||||
|
||||
def _force_plugins_enable_on_line(line):
|
||||
"""Under [plugins], ensure enable = On."""
|
||||
if not re.match(r'^\s*enable\s*=', line):
|
||||
return line
|
||||
if re.search(r'(?i)=\s*On\b', line):
|
||||
return line
|
||||
return re.sub(r'(?i)=\s*\S+', '= On', line, count=1)
|
||||
|
||||
|
||||
def merge_plugin_into_application_ini(application_ini_path, plugin_id):
|
||||
"""
|
||||
Merge plugin_id into [plugins] enabled_list; set enable = On when that key is present.
|
||||
"""
|
||||
if not os.path.isfile(application_ini_path):
|
||||
return False
|
||||
with open(application_ini_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
out = []
|
||||
in_plugins = False
|
||||
for line in lines:
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith('[plugins]'):
|
||||
in_plugins = True
|
||||
out.append(line)
|
||||
continue
|
||||
if in_plugins and stripped.startswith('[') and not stripped.startswith('[plugins]'):
|
||||
in_plugins = False
|
||||
if in_plugins and re.match(r'^\s*enabled_list\s*=', line):
|
||||
out.append(_merge_enabled_list_line(line, plugin_id))
|
||||
continue
|
||||
if in_plugins and re.match(r'^\s*enable\s*=', line):
|
||||
out.append(_force_plugins_enable_on_line(line))
|
||||
continue
|
||||
out.append(line)
|
||||
with open(application_ini_path, 'w') as f:
|
||||
f.writelines(out)
|
||||
return True
|
||||
|
||||
|
||||
def _chown_lscpd(path):
|
||||
try:
|
||||
subprocess.check_call(['chown', 'lscpd:lscpd', path], stderr=subprocess.DEVNULL)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
|
||||
|
||||
def _copy_bundled_plugin_to_data_root(data_root):
|
||||
"""Copy bundled index.php into .../plugins/list-unsubscribe-header/."""
|
||||
src = bundled_list_unsubscribe_src()
|
||||
if not os.path.isfile(src):
|
||||
return False
|
||||
dest_dir = os.path.join(
|
||||
data_root, '_data_', '_default_', 'plugins', PLUGIN_ID_LIST_UNSUBSCRIBE
|
||||
)
|
||||
try:
|
||||
os.makedirs(dest_dir, mode=0o700, exist_ok=True)
|
||||
except OSError:
|
||||
return False
|
||||
dest_file = os.path.join(dest_dir, 'index.php')
|
||||
try:
|
||||
shutil.copy2(src, dest_file)
|
||||
except (OSError, IOError):
|
||||
return False
|
||||
try:
|
||||
os.chmod(dest_file, 0o644)
|
||||
except OSError:
|
||||
pass
|
||||
_chown_lscpd(dest_file)
|
||||
_chown_lscpd(dest_dir)
|
||||
return True
|
||||
|
||||
|
||||
def install_and_enable_list_unsubscribe_header_plugin():
|
||||
"""
|
||||
Copy bundled plugin into each existing SnappyMail data root and merge into enabled_list.
|
||||
Idempotent. Returns 1 if at least one data root was updated, else 0.
|
||||
"""
|
||||
if not os.path.isfile(bundled_list_unsubscribe_src()):
|
||||
return 0
|
||||
ok = 0
|
||||
for root in DATA_ROOT_CANDIDATES:
|
||||
default_path = os.path.join(root, '_data_', '_default_')
|
||||
if not os.path.isdir(default_path):
|
||||
continue
|
||||
if not _copy_bundled_plugin_to_data_root(root):
|
||||
continue
|
||||
app_ini = os.path.join(default_path, 'configs', 'application.ini')
|
||||
if os.path.isfile(app_ini):
|
||||
merge_plugin_into_application_ini(app_ini, PLUGIN_ID_LIST_UNSUBSCRIBE)
|
||||
try:
|
||||
os.chmod(app_ini, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
_chown_lscpd(app_ini)
|
||||
ok = 1
|
||||
return ok
|
||||
@@ -1587,6 +1587,13 @@ $cfg['Servers'][$i]['port'] = '3306';
|
||||
command = f'/usr/local/lsws/lsphp83/bin/php /usr/local/CyberCP/snappymail_cyberpanel.php'
|
||||
Upgrade.executioner_silent(command, 'verify certificate', 0)
|
||||
|
||||
try:
|
||||
from plogical.snappymail_plugin_utilities import install_and_enable_list_unsubscribe_header_plugin
|
||||
if install_and_enable_list_unsubscribe_header_plugin():
|
||||
Upgrade.stdOut("SnappyMail list-unsubscribe-header plugin installed and enabled", 0)
|
||||
except BaseException as plug_msg:
|
||||
Upgrade.stdOut("Warning: list-unsubscribe SnappyMail plugin: " + str(plug_msg), 0)
|
||||
|
||||
# labsPath = '/usr/local/lscp/cyberpanel/rainloop/data/_data_/_default_/configs/application.ini'
|
||||
|
||||
# labsData = """[labs]
|
||||
|
||||
@@ -974,6 +974,15 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.dockerUpdateInProgress) {
|
||||
new PNotify({
|
||||
title: 'Update in progress',
|
||||
text: 'Wait until the current update finishes before starting another.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no new image specified, use current image
|
||||
if (!$scope.newImage) {
|
||||
$scope.newImage = $scope.currentImage;
|
||||
@@ -1000,9 +1009,18 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
history: false
|
||||
}
|
||||
})).get().on('pnotify.confirm', function () {
|
||||
var dockerUpdateNotificationId = null;
|
||||
$scope.dockerUpdateInProgress = true;
|
||||
$('#imageLoading').show();
|
||||
$("#updateContainer").modal("hide");
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyStart === 'function') {
|
||||
dockerUpdateNotificationId = window.cpDockerUpdateNotifyStart(
|
||||
$scope.updateContainerName,
|
||||
$scope.newImage + ':' + $scope.newTag
|
||||
);
|
||||
}
|
||||
|
||||
url = "/docker/updateContainer";
|
||||
var data = {
|
||||
name: $scope.updateContainerName,
|
||||
@@ -1020,15 +1038,27 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
|
||||
function ListInitialData(response) {
|
||||
console.log(response);
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
|
||||
if (response.data.updateContainerStatus === 1) {
|
||||
var ok = response.data && response.data.updateContainerStatus === 1;
|
||||
var imgLabel = ok
|
||||
? (response.data.new_image || response.data.message || 'Updated')
|
||||
: (response.data && response.data.error_message ? response.data.error_message : 'Update failed');
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, ok, imgLabel);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
new PNotify({
|
||||
title: 'Container Updated Successfully',
|
||||
text: `Container updated to ${response.data.new_image}`,
|
||||
text: 'Container updated to ' + (response.data.new_image || response.data.message || 'new image'),
|
||||
type: 'success'
|
||||
});
|
||||
location.reload();
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 2200);
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
@@ -1039,7 +1069,11 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, false, 'Could not connect to server');
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: 'Could not connect to server',
|
||||
@@ -1355,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.cName = "";
|
||||
$scope.status = "";
|
||||
$scope.savingSettings = false;
|
||||
$scope.currentPorts = {};
|
||||
$scope.initialPortsForSettings = {};
|
||||
$scope.loadingTop = false;
|
||||
$scope.statusInterval = null;
|
||||
$scope.statsInterval = null;
|
||||
@@ -1364,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.advancedEnvText = '';
|
||||
$scope.advancedEnvCount = 0;
|
||||
$scope.parsedEnvVars = {};
|
||||
|
||||
/** Flat map from DB (container_port -> host_port), set from template ng-init */
|
||||
$scope.serverPortMap = {};
|
||||
|
||||
$scope.initAngularPortsFromServer = function (obj) {
|
||||
if (obj === undefined || obj === null) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
if (angular.isString(obj)) {
|
||||
try {
|
||||
obj = angular.fromJson(obj);
|
||||
} catch (e1) {
|
||||
try {
|
||||
obj = JSON.parse(obj);
|
||||
} catch (e2) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!angular.isObject(obj)) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
$scope.serverPortMap = obj;
|
||||
$scope.ports = obj;
|
||||
};
|
||||
|
||||
// Auto-refresh status every 5 seconds
|
||||
$scope.startStatusMonitoring = function() {
|
||||
$scope.statusInterval = $interval(function() {
|
||||
@@ -1994,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
|
||||
$scope.refreshContainerInfo = function () {
|
||||
$timeout(function () {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$scope.addVolField = function () {
|
||||
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
|
||||
$scope.volListNumber = $scope.volListNumber + 1;
|
||||
@@ -2004,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
|
||||
new PNotify({
|
||||
title: 'Confirmation required',
|
||||
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#containerSettingLoading').show();
|
||||
url = "/docker/saveContainerSettings";
|
||||
$scope.savingSettings = true;
|
||||
@@ -2029,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
envConfirmation: $scope.envConfirmation,
|
||||
envList: finalEnvList,
|
||||
volList: $scope.volList,
|
||||
advancedEnvMode: $scope.advancedEnvMode
|
||||
advancedEnvMode: $scope.advancedEnvMode,
|
||||
ports: $scope.currentPorts || {}
|
||||
};
|
||||
|
||||
|
||||
@@ -2054,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
title: 'Settings Saved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -2179,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$("#commandModal").modal("show");
|
||||
};
|
||||
|
||||
// Port editing functionality
|
||||
$scope.showPortEditModal = function() {
|
||||
// Initialize current ports from container data
|
||||
// Port editing (in Container Settings modal)
|
||||
$scope.initSettingsPortsFromContainer = function () {
|
||||
$scope.currentPorts = {};
|
||||
if ($scope.ports) {
|
||||
for (var iport in $scope.ports) {
|
||||
var eport = $scope.ports[iport];
|
||||
if (eport && eport.length > 0) {
|
||||
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
|
||||
? $scope.serverPortMap
|
||||
: ($scope.ports || {});
|
||||
if (src && typeof src === 'object') {
|
||||
for (var iport in src) {
|
||||
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
|
||||
continue;
|
||||
}
|
||||
var eport = src[iport];
|
||||
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
|
||||
$scope.currentPorts[iport] = eport[0].HostPort;
|
||||
} else if (eport !== undefined && eport !== null && eport !== '') {
|
||||
$scope.currentPorts[iport] = String(eport);
|
||||
}
|
||||
}
|
||||
}
|
||||
$("#portEditModal").modal("show");
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function() {
|
||||
$scope.portsJsonSnapshot = function (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return '{}';
|
||||
}
|
||||
var keys = Object.keys(obj).sort();
|
||||
var flat = {};
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var v = obj[k];
|
||||
flat[k] = v === null || v === undefined ? '' : String(v);
|
||||
}
|
||||
return JSON.stringify(flat);
|
||||
};
|
||||
|
||||
$scope.portsDirty = function () {
|
||||
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
|
||||
};
|
||||
|
||||
$scope.showPortEditModal = function () {
|
||||
$scope.initSettingsPortsFromContainer();
|
||||
$("#settings").modal("show");
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function () {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
|
||||
if (containerPort) {
|
||||
$scope.currentPorts[containerPort] = '';
|
||||
@@ -2202,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removePortMapping = function(containerPort) {
|
||||
$scope.removePortMapping = function (containerPort) {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
if (confirm('Are you sure you want to remove this port mapping?')) {
|
||||
delete $scope.currentPorts[containerPort];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updatePortMappings = function() {
|
||||
$("#portEditLoading").show();
|
||||
$scope.updatePortMappings = function () {
|
||||
$scope.updatingPorts = true;
|
||||
|
||||
var url = "/docker/updateContainerPorts";
|
||||
@@ -2225,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$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();
|
||||
$("#settings").modal("hide");
|
||||
new PNotify({
|
||||
title: 'Success',
|
||||
text: 'Port mappings updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.refreshContainerInfo();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
@@ -2245,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Error updating port mappings: ' + error.data.error_message,
|
||||
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
@@ -2354,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
$scope.showingSearch = false;
|
||||
$("#searchResult").hide();
|
||||
|
||||
function dockerHistoryFormatSize(raw) {
|
||||
var n = parseInt(raw, 10);
|
||||
if (isNaN(n) || n < 0) {
|
||||
return (raw === undefined || raw === null) ? '' : String(raw);
|
||||
}
|
||||
if (n === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
||||
var v = n / Math.pow(1024, i);
|
||||
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
|
||||
function dockerHistoryFormatCreated(raw) {
|
||||
if (raw === undefined || raw === null) {
|
||||
return '';
|
||||
}
|
||||
var n = Number(raw);
|
||||
if (isNaN(n)) {
|
||||
return String(raw);
|
||||
}
|
||||
var sec = n;
|
||||
if (n > 1e14) {
|
||||
sec = Math.floor(n / 1e9);
|
||||
} else if (n > 1e12) {
|
||||
sec = Math.floor(n / 1e6);
|
||||
}
|
||||
var d = new Date(sec * 1000);
|
||||
if (isNaN(d.getTime())) {
|
||||
return String(raw);
|
||||
}
|
||||
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
||||
}
|
||||
|
||||
function dockerHistoryEnrichLayer(h) {
|
||||
var o = angular.extend({}, h);
|
||||
o.SizeHuman = dockerHistoryFormatSize(h.Size);
|
||||
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
|
||||
return o;
|
||||
}
|
||||
|
||||
$scope.pullImage = function (image, tag) {
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.installImageStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Image pulled successfully',
|
||||
text: 'Reloading...',
|
||||
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading…',
|
||||
type: 'success'
|
||||
});
|
||||
location.reload()
|
||||
@@ -2390,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
url = "/docker/installImage";
|
||||
var data = {
|
||||
image: image,
|
||||
tag: tag
|
||||
tag: tag,
|
||||
force_update: true
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
@@ -2411,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
}
|
||||
|
||||
$scope.refreshLocalImage = function (rowId, imageName) {
|
||||
var sel = document.getElementById(String(rowId));
|
||||
if (!sel || sel.selectedIndex < 0) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var raw = sel.options[sel.selectedIndex].text;
|
||||
if (!raw) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var li = raw.lastIndexOf(':');
|
||||
var tag = li > -1 ? raw.substring(li + 1) : raw;
|
||||
$scope.pullImage(imageName, tag);
|
||||
}
|
||||
|
||||
$scope.searchImages = function () {
|
||||
console.log($scope.searchString);
|
||||
if (!$scope.searchString) {
|
||||
@@ -2546,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
$scope.getHistory = function (counter) {
|
||||
$('#imageLoading').show();
|
||||
var name = $("#" + counter).val()
|
||||
var sel = $("#" + counter);
|
||||
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
|
||||
|
||||
url = "/docker/getImageHistory";
|
||||
|
||||
@@ -2566,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
if (response.data.imageHistoryStatus === 1) {
|
||||
$('#history').modal('show');
|
||||
$scope.historyList = response.data.history;
|
||||
var raw = response.data.history || [];
|
||||
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
@@ -2713,4 +2899,3 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1389,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.cName = "";
|
||||
$scope.status = "";
|
||||
$scope.savingSettings = false;
|
||||
$scope.currentPorts = {};
|
||||
$scope.initialPortsForSettings = {};
|
||||
$scope.loadingTop = false;
|
||||
$scope.statusInterval = null;
|
||||
$scope.statsInterval = null;
|
||||
@@ -1398,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.advancedEnvText = '';
|
||||
$scope.advancedEnvCount = 0;
|
||||
$scope.parsedEnvVars = {};
|
||||
|
||||
/** Flat map from DB (container_port -> host_port), set from template ng-init */
|
||||
$scope.serverPortMap = {};
|
||||
|
||||
$scope.initAngularPortsFromServer = function (obj) {
|
||||
if (obj === undefined || obj === null) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
if (angular.isString(obj)) {
|
||||
try {
|
||||
obj = angular.fromJson(obj);
|
||||
} catch (e1) {
|
||||
try {
|
||||
obj = JSON.parse(obj);
|
||||
} catch (e2) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!angular.isObject(obj)) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
$scope.serverPortMap = obj;
|
||||
$scope.ports = obj;
|
||||
};
|
||||
|
||||
// Auto-refresh status every 5 seconds
|
||||
$scope.startStatusMonitoring = function() {
|
||||
$scope.statusInterval = $interval(function() {
|
||||
@@ -2028,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
|
||||
$scope.refreshContainerInfo = function () {
|
||||
$timeout(function () {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$scope.addVolField = function () {
|
||||
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
|
||||
$scope.volListNumber = $scope.volListNumber + 1;
|
||||
@@ -2038,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
|
||||
new PNotify({
|
||||
title: 'Confirmation required',
|
||||
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#containerSettingLoading').show();
|
||||
url = "/docker/saveContainerSettings";
|
||||
$scope.savingSettings = true;
|
||||
@@ -2063,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
envConfirmation: $scope.envConfirmation,
|
||||
envList: finalEnvList,
|
||||
volList: $scope.volList,
|
||||
advancedEnvMode: $scope.advancedEnvMode
|
||||
advancedEnvMode: $scope.advancedEnvMode,
|
||||
ports: $scope.currentPorts || {}
|
||||
};
|
||||
|
||||
|
||||
@@ -2088,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
title: 'Settings Saved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -2213,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$("#commandModal").modal("show");
|
||||
};
|
||||
|
||||
// Port editing functionality
|
||||
$scope.showPortEditModal = function() {
|
||||
// Initialize current ports from container data
|
||||
// Port editing (in Container Settings modal)
|
||||
$scope.initSettingsPortsFromContainer = function () {
|
||||
$scope.currentPorts = {};
|
||||
if ($scope.ports) {
|
||||
for (var iport in $scope.ports) {
|
||||
var eport = $scope.ports[iport];
|
||||
if (eport && eport.length > 0) {
|
||||
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
|
||||
? $scope.serverPortMap
|
||||
: ($scope.ports || {});
|
||||
if (src && typeof src === 'object') {
|
||||
for (var iport in src) {
|
||||
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
|
||||
continue;
|
||||
}
|
||||
var eport = src[iport];
|
||||
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
|
||||
$scope.currentPorts[iport] = eport[0].HostPort;
|
||||
} else if (eport !== undefined && eport !== null && eport !== '') {
|
||||
$scope.currentPorts[iport] = String(eport);
|
||||
}
|
||||
}
|
||||
}
|
||||
$("#portEditModal").modal("show");
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function() {
|
||||
$scope.portsJsonSnapshot = function (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return '{}';
|
||||
}
|
||||
var keys = Object.keys(obj).sort();
|
||||
var flat = {};
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var v = obj[k];
|
||||
flat[k] = v === null || v === undefined ? '' : String(v);
|
||||
}
|
||||
return JSON.stringify(flat);
|
||||
};
|
||||
|
||||
$scope.portsDirty = function () {
|
||||
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
|
||||
};
|
||||
|
||||
$scope.showPortEditModal = function () {
|
||||
$scope.initSettingsPortsFromContainer();
|
||||
$("#settings").modal("show");
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function () {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
|
||||
if (containerPort) {
|
||||
$scope.currentPorts[containerPort] = '';
|
||||
@@ -2236,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removePortMapping = function(containerPort) {
|
||||
$scope.removePortMapping = function (containerPort) {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
if (confirm('Are you sure you want to remove this port mapping?')) {
|
||||
delete $scope.currentPorts[containerPort];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updatePortMappings = function() {
|
||||
$("#portEditLoading").show();
|
||||
$scope.updatePortMappings = function () {
|
||||
$scope.updatingPorts = true;
|
||||
|
||||
var url = "/docker/updateContainerPorts";
|
||||
@@ -2259,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$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();
|
||||
$("#settings").modal("hide");
|
||||
new PNotify({
|
||||
title: 'Success',
|
||||
text: 'Port mappings updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.refreshContainerInfo();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
@@ -2279,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Error updating port mappings: ' + error.data.error_message,
|
||||
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
@@ -2388,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
$scope.showingSearch = false;
|
||||
$("#searchResult").hide();
|
||||
|
||||
function dockerHistoryFormatSize(raw) {
|
||||
var n = parseInt(raw, 10);
|
||||
if (isNaN(n) || n < 0) {
|
||||
return (raw === undefined || raw === null) ? '' : String(raw);
|
||||
}
|
||||
if (n === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
||||
var v = n / Math.pow(1024, i);
|
||||
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
|
||||
function dockerHistoryFormatCreated(raw) {
|
||||
if (raw === undefined || raw === null) {
|
||||
return '';
|
||||
}
|
||||
var n = Number(raw);
|
||||
if (isNaN(n)) {
|
||||
return String(raw);
|
||||
}
|
||||
var sec = n;
|
||||
if (n > 1e14) {
|
||||
sec = Math.floor(n / 1e9);
|
||||
} else if (n > 1e12) {
|
||||
sec = Math.floor(n / 1e6);
|
||||
}
|
||||
var d = new Date(sec * 1000);
|
||||
if (isNaN(d.getTime())) {
|
||||
return String(raw);
|
||||
}
|
||||
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
||||
}
|
||||
|
||||
function dockerHistoryEnrichLayer(h) {
|
||||
var o = angular.extend({}, h);
|
||||
o.SizeHuman = dockerHistoryFormatSize(h.Size);
|
||||
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
|
||||
return o;
|
||||
}
|
||||
|
||||
$scope.pullImage = function (image, tag) {
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.installImageStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Image pulled successfully',
|
||||
text: 'Reloading...',
|
||||
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading…',
|
||||
type: 'success'
|
||||
});
|
||||
location.reload()
|
||||
@@ -2424,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
url = "/docker/installImage";
|
||||
var data = {
|
||||
image: image,
|
||||
tag: tag
|
||||
tag: tag,
|
||||
force_update: true
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
@@ -2445,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
}
|
||||
|
||||
$scope.refreshLocalImage = function (rowId, imageName) {
|
||||
var sel = document.getElementById(String(rowId));
|
||||
if (!sel || sel.selectedIndex < 0) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var raw = sel.options[sel.selectedIndex].text;
|
||||
if (!raw) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var li = raw.lastIndexOf(':');
|
||||
var tag = li > -1 ? raw.substring(li + 1) : raw;
|
||||
$scope.pullImage(imageName, tag);
|
||||
}
|
||||
|
||||
$scope.searchImages = function () {
|
||||
console.log($scope.searchString);
|
||||
if (!$scope.searchString) {
|
||||
@@ -2580,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
$scope.getHistory = function (counter) {
|
||||
$('#imageLoading').show();
|
||||
var name = $("#" + counter).val()
|
||||
var sel = $("#" + counter);
|
||||
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
|
||||
|
||||
url = "/docker/getImageHistory";
|
||||
|
||||
@@ -2600,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
if (response.data.imageHistoryStatus === 1) {
|
||||
$('#history').modal('show');
|
||||
$scope.historyList = response.data.history;
|
||||
var raw = response.data.history || [];
|
||||
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
* Created by usman on 6/22/18.
|
||||
*/
|
||||
|
||||
/* Ensure CyberCP Angular module exists (system-status.js normally sets window.app in <head>) */
|
||||
(function () {
|
||||
if (typeof window === 'undefined' || typeof angular === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (typeof window.app !== 'undefined' && window.app) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.app = angular.module('CyberCP');
|
||||
} catch (e) {
|
||||
try {
|
||||
window.app = angular.module('CyberCP', []);
|
||||
} catch (e2) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
var app = window.app;
|
||||
|
||||
/* Java script code to list accounts */
|
||||
|
||||
app.controller('listDomains', function ($scope, $http) {
|
||||
|
||||
Reference in New Issue
Block a user