Merge pull request #22 from rucko24/feature/auto-refresh
Feature/auto-refresh Enhancements
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea
|
||||
@@ -39,6 +39,41 @@ require_once 'auth.inc';
|
||||
require_once 'guiconfig.inc';
|
||||
require_once 'bastille_manager-lib.inc';
|
||||
|
||||
$img_path = [
|
||||
'add' => 'images/add.png',
|
||||
'mod' => 'images/edit.png',
|
||||
'del' => 'images/delete.png',
|
||||
'loc' => 'images/locked.png',
|
||||
'unl' => 'images/unlocked.png',
|
||||
'mai' => 'images/maintain.png',
|
||||
'inf' => 'images/info.png',
|
||||
'ena' => 'images/status_enabled.png',
|
||||
'dis' => 'images/status_disabled.png',
|
||||
'mup' => 'images/up.png',
|
||||
'mdn' => 'images/down.png'
|
||||
];
|
||||
|
||||
// --- START AUTO-REFRESH LOGIC ---
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'refresh_table') {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
ob_start();
|
||||
|
||||
// Fetch fresh data
|
||||
$jls_list = [];
|
||||
if (function_exists('get_jail_infos')) {
|
||||
$jls_list = get_jail_infos();
|
||||
}
|
||||
|
||||
// Return JSON
|
||||
ob_clean();
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-cache');
|
||||
echo json_encode(['success' => true, 'jails' => $jls_list ?: []]);
|
||||
exit;
|
||||
}
|
||||
// --- END AUTO-REFRESH LOGIC ---
|
||||
|
||||
function mwexec_parallel($commands) {
|
||||
$processes = [];
|
||||
$results = [];
|
||||
@@ -53,7 +88,6 @@ function mwexec_parallel($commands) {
|
||||
$process = proc_open($command, $descriptors, $pipes);
|
||||
|
||||
if (is_resource($process)) {
|
||||
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
|
||||
@@ -71,7 +105,6 @@ function mwexec_parallel($commands) {
|
||||
foreach ($processes as $key => $proc) {
|
||||
$elapsed = time() - $start_time;
|
||||
if ($elapsed < $timeout) {
|
||||
|
||||
$stdout = stream_get_contents($proc['pipes'][1]);
|
||||
$stderr = stream_get_contents($proc['pipes'][2]);
|
||||
|
||||
@@ -102,8 +135,8 @@ function mwexec_parallel($commands) {
|
||||
}
|
||||
|
||||
function mwexec_background($command) {
|
||||
$command = $command . ' > /dev/null 2>&1 &';
|
||||
exec($command);
|
||||
$command = $command . ' > /dev/null 2>&1 &';
|
||||
exec($command);
|
||||
}
|
||||
|
||||
$sphere_scriptname = basename(__FILE__);
|
||||
@@ -127,19 +160,6 @@ $gt_selection_start_confirm = gtext('Do you really want to start selected jail(s
|
||||
$gt_selection_stop_confirm = gtext('Do you want to stop the selected jail(s)?');
|
||||
$gt_selection_restart_confirm = gtext('Do you want to restart the selected jail(s)?');
|
||||
$gt_selection_autoboot_confirm = gtext('Do you want to set auto-boot on selected jail(s)?');
|
||||
$img_path = [
|
||||
'add' => 'images/add.png',
|
||||
'mod' => 'images/edit.png',
|
||||
'del' => 'images/delete.png',
|
||||
'loc' => 'images/locked.png',
|
||||
'unl' => 'images/unlocked.png',
|
||||
'mai' => 'images/maintain.png',
|
||||
'inf' => 'images/info.png',
|
||||
'ena' => 'images/status_enabled.png',
|
||||
'dis' => 'images/status_disabled.png',
|
||||
'mup' => 'images/up.png',
|
||||
'mdn' => 'images/down.png'
|
||||
];
|
||||
|
||||
$jls_list = get_jail_infos();
|
||||
$sphere_array = $jls_list;
|
||||
@@ -184,19 +204,16 @@ if($_POST):
|
||||
if(isset($_POST['start_selected_jail']) && $_POST['start_selected_jail']):
|
||||
$checkbox_member_array = isset($_POST[$checkbox_member_name]) ? $_POST[$checkbox_member_name] : [];
|
||||
$commands = [];
|
||||
$jail_names = [];
|
||||
|
||||
foreach($checkbox_member_array as $checkbox_member_record):
|
||||
if(false !== ($index = array_search_ex($checkbox_member_record, $sphere_array, 'jailname'))):
|
||||
if(!isset($sphere_array[$index]['protected'])):
|
||||
$commands[] = "/usr/local/bin/bastille start {$checkbox_member_record}";
|
||||
$jail_names[] = $checkbox_member_record;
|
||||
endif;
|
||||
endif;
|
||||
endforeach;
|
||||
|
||||
if (!empty($commands)):
|
||||
|
||||
$results = mwexec_parallel($commands);
|
||||
|
||||
$success_count = 0;
|
||||
@@ -345,36 +362,116 @@ if($_POST):
|
||||
endif;
|
||||
endif;
|
||||
|
||||
$pgtitle = [gtext("Extensions"), gtext('Bastille')];
|
||||
$pgtitle = [gtext("Extensions"), gtext('Bastille'), gtext('Manager')];
|
||||
include 'fbegin.inc';
|
||||
?>
|
||||
<style>
|
||||
/* Refresh button style */
|
||||
#refresh-now {
|
||||
appearance: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: bold;
|
||||
color: var(--txc-input-rw);
|
||||
background-color: var(--bgc-area-data);
|
||||
border: 1px solid var(--boc-button);
|
||||
border-radius: var(--bor);
|
||||
padding: 0.125rem 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
#refresh-now:hover {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
/* --- SIMPLE RESIZE STYLES --- */
|
||||
table.area_data_selection {
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.area_data_selection th {
|
||||
position: relative;
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* The visible handle */
|
||||
.resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.resizer:hover, .resizing {
|
||||
background-color: #007bff; /* Azul */
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
$(window).on("load", function() {
|
||||
// Init action buttons
|
||||
$("#start_selected_jail").click(function () {
|
||||
stopAutoRefresh(); // Pause for safety
|
||||
return confirm('<?=$gt_selection_start_confirm;?>');
|
||||
});
|
||||
$("#stop_selected_jail").click(function () {
|
||||
stopAutoRefresh();
|
||||
return confirm('<?=$gt_selection_stop_confirm;?>');
|
||||
});
|
||||
$("#restart_selected_jail").click(function () {
|
||||
stopAutoRefresh();
|
||||
return confirm('<?=$gt_selection_restart_confirm;?>');
|
||||
});
|
||||
$("#autoboot_selected_jail").click(function () {
|
||||
return confirm('<?=$gt_selection_restart_confirm;?>');
|
||||
stopAutoRefresh();
|
||||
return confirm('<?=$gt_selection_autoboot_confirm;?>');
|
||||
});
|
||||
// Disable action buttons.
|
||||
disableactionbuttons(true);
|
||||
|
||||
// Init member checkboxes
|
||||
$("input[name='<?=$checkbox_member_name;?>[]']").click(function() {
|
||||
controlactionbuttons(this, '<?=$checkbox_member_name;?>[]');
|
||||
});
|
||||
// Init spinner onsubmit()
|
||||
$("#iform").submit(function() { spinner(); });
|
||||
$(".spin").click(function() { spinner(); });
|
||||
|
||||
// Attempt to load the previously saved interval
|
||||
var savedInterval = localStorage.getItem('bastille_refresh_interval');
|
||||
if (savedInterval !== null) {
|
||||
$("#refresh-interval").val(savedInterval);
|
||||
autoRefresh.interval = parseInt(savedInterval);
|
||||
}
|
||||
// --- REFRESH INIT ---
|
||||
// Only start if the button is visible (enabled in settings)
|
||||
if (localStorage.getItem('bastille_show_refresh_button') === 'true') {
|
||||
$("#refresh-controls").show();
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
$("#refresh-now").click(function() {
|
||||
updateJailTable();
|
||||
});
|
||||
|
||||
// save interval value in local storage
|
||||
$("#refresh-interval").change(function() {
|
||||
var val = parseInt($(this).val());
|
||||
localStorage.setItem('bastille_refresh_interval', val);
|
||||
stopAutoRefresh();
|
||||
if (val > 0) {
|
||||
autoRefresh.interval = val;
|
||||
startAutoRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
initSimpleResize();
|
||||
});
|
||||
|
||||
function disableactionbuttons(ab_disable) {
|
||||
$("#start_selected_jail").prop("disabled", ab_disable);
|
||||
$("#stop_selected_jail").prop("disabled", ab_disable);
|
||||
@@ -397,6 +494,149 @@ function controlactionbuttons(ego, triggerbyname) {
|
||||
}
|
||||
disableactionbuttons(ab_disable);
|
||||
}
|
||||
|
||||
// --- AUTO-REFRESH JS ---
|
||||
var autoRefresh = {
|
||||
enabled: true,
|
||||
interval: 30000,
|
||||
timerId: null,
|
||||
lastUpdate: Date.now(),
|
||||
isUpdating: false,
|
||||
selectedJails: []
|
||||
};
|
||||
|
||||
function updateJailTable() {
|
||||
if (autoRefresh.isUpdating) return;
|
||||
autoRefresh.isUpdating = true;
|
||||
$("#refresh-status").text('Updating...');
|
||||
|
||||
// Backup of checked checkboxes for persistence
|
||||
autoRefresh.selectedJails = [];
|
||||
$("input[name='<?=$checkbox_member_name;?>[]']:checked").each(function() {
|
||||
autoRefresh.selectedJails.push($(this).val());
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: 'bastille_manager_gui.php?action=refresh_table',
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data.success) {
|
||||
var tbody = $(".area_data_selection tbody");
|
||||
tbody.empty();
|
||||
data.jails.forEach(function(jail) {
|
||||
var row = $('<tr>');
|
||||
var checkCell = $('<td class="lcelc">');
|
||||
var cb = $('<input type="checkbox">')
|
||||
.attr('name', '<?=$checkbox_member_name;?>[]')
|
||||
.attr('value', jail.jailname)
|
||||
.attr('id', jail.jailname)
|
||||
.prop('checked', autoRefresh.selectedJails.includes(jail.jailname))
|
||||
.click(function() { controlactionbuttons(this, '<?=$checkbox_member_name;?>[]'); });
|
||||
checkCell.append(cb);
|
||||
row.append(checkCell);
|
||||
|
||||
// 2. Data Columns
|
||||
row.append($('<td class="lcell">').text(jail.id || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.name || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.boot || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.prio || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.state || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.type || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.ip || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.ports || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.rel || '-'));
|
||||
row.append($('<td class="lcell">').text(jail.tags || '-'));
|
||||
|
||||
var statImg = (jail.state === "Up") ? '<?=$img_path['ena'];?>' : '<?=$img_path['dis'];?>';
|
||||
row.append($('<td class="lcell">').append($('<img>').attr('src', statImg)));
|
||||
row.append($('<td class="lcell">').append($('<img>').attr('src', jail.logo)));
|
||||
|
||||
var tools = $('<td class="lcebld">').html('<table class="area_data_selection_toolbox"><tbody><tr>' +
|
||||
'<td><a href="<?=$sphere_scriptname_child;?>?jailname=' + encodeURIComponent(jail.jailname) + '"><img src="<?=$img_path['mai'];?>" class="spin oneemhigh"></a></td>' +
|
||||
'<td><a href="bastille_manager_jconf.php?jailname=' + encodeURIComponent(jail.jailname) + '"><img src="<?=$g_img['mod'];?>"></a></td>' +
|
||||
'<td><a href="bastille_manager_info.php?uuid=' + encodeURIComponent(jail.jailname) + '"><img src="<?=$g_img['inf'];?>"></a></td>' +
|
||||
'</tr></tbody></table>');
|
||||
row.append(tools);
|
||||
|
||||
tbody.append(row);
|
||||
});
|
||||
autoRefresh.lastUpdate = Date.now();
|
||||
$("#refresh-status").text('Last update: just now');
|
||||
controlactionbuttons(null, '<?=$checkbox_member_name;?>[]');
|
||||
}
|
||||
},
|
||||
complete: function() { autoRefresh.isUpdating = false; }
|
||||
});
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
if (autoRefresh.interval > 0) {
|
||||
autoRefresh.timerId = setInterval(updateJailTable, autoRefresh.interval);
|
||||
}
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefresh.timerId) clearInterval(autoRefresh.timerId);
|
||||
}
|
||||
|
||||
// --- STABLE REDIMENSIONING FUNCTION (without %) ---
|
||||
function initSimpleResize() {
|
||||
var $table = $("table.area_data_selection");
|
||||
var $cols = $table.find('colgroup col');
|
||||
var $headers = $table.find('thead th');
|
||||
|
||||
$headers.each(function(i) {
|
||||
if (i >= $headers.length - 1) return; // Ignore the last column
|
||||
var $resizer = $('<div class="resizer"></div>');
|
||||
$(this).append($resizer);
|
||||
});
|
||||
|
||||
var isResizing = false;
|
||||
var startX = 0;
|
||||
var $currentCol = null;
|
||||
var startWidth = 0;
|
||||
|
||||
$table.on('mousedown', '.resizer', function(e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
stopAutoRefresh();
|
||||
|
||||
// Convert all columns to fixed pixels when starting to drag
|
||||
$cols.each(function() {
|
||||
var w = $(this).width();
|
||||
$(this).css('width', w + 'px');
|
||||
});
|
||||
|
||||
var idx = $(this).parent().index();
|
||||
$currentCol = $cols.eq(idx);
|
||||
|
||||
isResizing = true;
|
||||
startX = e.pageX;
|
||||
startWidth = $currentCol.width();
|
||||
$(this).addClass('resizing');
|
||||
|
||||
$(document).on('mousemove.rsz', function(e) {
|
||||
if (!isResizing) return;
|
||||
var diff = e.pageX - startX;
|
||||
var newW = startWidth + diff;
|
||||
|
||||
if (newW > 30) {
|
||||
$currentCol.css('width', newW + 'px');
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('mouseup.rsz', function() {
|
||||
if (!isResizing) {
|
||||
return;
|
||||
}
|
||||
isResizing = false;
|
||||
$('.resizer').removeClass('resizing');
|
||||
$(document).off('mousemove.rsz mouseup.rsz');
|
||||
setTimeout(function() {
|
||||
startAutoRefresh();
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
<?php
|
||||
@@ -438,24 +678,37 @@ $document->render();
|
||||
<tbody>
|
||||
<?php
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="area_data_selection">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="refresh-controls" style="text-align: right; display: none;">
|
||||
<span id="refresh-status" style="font-style: italic; margin-right: 15px; color: #666;">Last update: just now</span>
|
||||
<button type="button" id="refresh-now" class="formbtn">Refresh</button>
|
||||
<select id="refresh-interval" class="formfld">
|
||||
<option value="5000">5s</option>
|
||||
<option value="10000">10s</option>
|
||||
<option value="30000" selected>30s</option>
|
||||
<option value="60000">60s</option>
|
||||
<option value="0">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<table class="area_data_selection" style="width: 100%; table-layout: fixed; border-collapse: collapse;">
|
||||
<colgroup>
|
||||
<col style="width:2%">
|
||||
<col style="width:2%">
|
||||
<col style="width:3%">
|
||||
<col style="width:12%">
|
||||
<col style="width:4%">
|
||||
<col style="width:4%">
|
||||
<col style="width:4%">
|
||||
<col style="width:4%">
|
||||
<col style="width:12%">
|
||||
<col style="width:12%">
|
||||
<col style="width:7%">
|
||||
<col style="width:12%">
|
||||
<col style="width:4%">
|
||||
<col style="width:4%">
|
||||
<col style="width:10%">
|
||||
<col style="width:3%">
|
||||
<col style="width:3%">
|
||||
<col style="width:3%">
|
||||
<col style="width:3%">
|
||||
<col style="width:10%">
|
||||
<col style="width:10%">
|
||||
<col style="width:5%">
|
||||
<col style="width:10%">
|
||||
<col style="width:3%">
|
||||
<col style="width:3%">
|
||||
<col style="width:5%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<?php
|
||||
@@ -533,6 +786,7 @@ $document->render();
|
||||
endif;
|
||||
endif;
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="bastille_manager_jconf.php?jailname=<?=urlencode($sphere_record['jailname']);?>"><img src="<?=$g_img['mod'];?>" title="<?=$gt_record_conf?>" alt="<?=$gt_record_conf?>"/></a>
|
||||
</td>
|
||||
@@ -562,8 +816,9 @@ $document->render();
|
||||
<input name="autoboot_selected_jail" id="autoboot_selected_jail" type="submit" class="formbtn" value="<?=$gt_selection_autoboot;?>"/>
|
||||
</div>
|
||||
<?php
|
||||
include 'formend.inc';
|
||||
include 'formend.inc';
|
||||
?>
|
||||
</td></tr></tbody></table></form>
|
||||
<?php
|
||||
include 'fend.inc';
|
||||
?>
|
||||
@@ -306,6 +306,23 @@ $(document).ready(function(){
|
||||
$('#getinfo_bastille').html(data.bastille);
|
||||
$('#getinfo_ext').html(data.ext);
|
||||
});
|
||||
|
||||
// --- LOGICA DEL CHECKBOX REFRESH (LocalStorage) ---
|
||||
var $chk = $("#show_refresh_button");
|
||||
|
||||
// 1. Leer estado inicial desde LocalStorage
|
||||
var savedState = localStorage.getItem('bastille_show_refresh_button');
|
||||
if (savedState === 'true') {
|
||||
$chk.prop('checked', true);
|
||||
} else {
|
||||
$chk.prop('checked', false); // Por defecto deshabilitado
|
||||
}
|
||||
|
||||
// 2. Guardar cambios al hacer click
|
||||
$chk.change(function() {
|
||||
var isChecked = $(this).is(':checked');
|
||||
localStorage.setItem('bastille_show_refresh_button', isChecked);
|
||||
});
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@@ -373,6 +390,9 @@ $(document).ready(function(){
|
||||
<?php html_titleline(gtext("Uninstall"));?>
|
||||
<?php html_checkbox2('delete_confirm',gtext('Uninstall confirm'),'' ? true : false,gtext('Check to confirm extension uninstall. Note: Jail related content will be preserved by default.'),'',false);?>
|
||||
<?php html_separator();?>
|
||||
<?php html_titleline(gtext("Refresh"));?>
|
||||
<?php html_checkbox2('show_refresh_button',gtext('Show refresh button'),'' ? true : false,gtext('This will display a refresh button in the containers tab.'),'',false);?>
|
||||
<?php html_separator();?>
|
||||
</table>
|
||||
<div id="submit1">
|
||||
<input name="uninstall" type="submit" class="formbtn" title="<?=gtext("Uninstall Extension");?>" value="<?=gtext("Uninstall");?>" onclick="return confirm('<?=gtext("Bastille Extension and packages will be completely removed, Bastille containers and child directories will not be touched, really to proceed?");?>')" />
|
||||
@@ -384,4 +404,4 @@ $(document).ready(function(){
|
||||
<script type="text/javascript">
|
||||
<!--enable_change(false);-->
|
||||
</script>
|
||||
<?php include("fend.inc");?>
|
||||
<?php include("fend.inc");?>
|
||||
58
gui/css/bastille-header-refresh.css
Normal file
58
gui/css/bastille-header-refresh.css
Normal file
@@ -0,0 +1,58 @@
|
||||
/* bastille_manager.css
|
||||
Estilo NATIVO (Minimalista)
|
||||
*/
|
||||
|
||||
#refresh-controls {
|
||||
/* Fondo transparente y sin bordes para integrarse con el tema */
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 10px 0;
|
||||
margin-bottom: 5px;
|
||||
|
||||
/* Alineación a la derecha */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
/* Fuente estándar del sistema */
|
||||
font-size: 13px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#refresh-status {
|
||||
/* Color de texto por defecto del tema (negro/gris) */
|
||||
color: inherit;
|
||||
margin-right: 5px;
|
||||
|
||||
/* Coloca el texto a la izquierda de los botones */
|
||||
order: -1;
|
||||
}
|
||||
|
||||
/* Pequeño spinner azul discreto solo cuando actualiza */
|
||||
#refresh-status.updating .refresh-spinner {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid #ccc;
|
||||
border-top-color: #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Animación del spinner */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Asegurar que los iconos de la tabla estén centrados verticalmente */
|
||||
.area_data_selection tbody td img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Centrado perfecto para los checkboxes */
|
||||
.lcelc {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
Reference in New Issue
Block a user