Setup wizard step 1 (SSH key) stays as-is, then redirects to remotes.cgi?action=add&wizard=1 and schedules.cgi?action=add&wizard=1 instead of reimplementing its own forms. - Remove ~200 lines of duplicated form/handler code from setup.cgi - Wizard gets full feature parity (S3/GDrive, password auth, etc.) - key_path pre-fills REMOTE_KEY, remote_name pre-fills REMOTES - wizard=1 flag chains the flow: remote add → schedule add → index Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
523 lines
20 KiB
Perl
523 lines
20 KiB
Perl
#!/usr/local/cpanel/3rdparty/bin/perl
|
|
# gniza WHM Plugin — Schedule CRUD
|
|
use strict;
|
|
use warnings;
|
|
|
|
use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/lib';
|
|
|
|
use Whostmgr::HTMLInterface ();
|
|
use Cpanel::Form ();
|
|
use File::Copy ();
|
|
use GnizaWHM::Config;
|
|
use GnizaWHM::Validator;
|
|
use GnizaWHM::Cron;
|
|
use GnizaWHM::UI;
|
|
|
|
my $form = Cpanel::Form::parseform();
|
|
my $method = $ENV{'REQUEST_METHOD'} // 'GET';
|
|
my $action = $form->{'action'} // 'list';
|
|
|
|
# Route to handler
|
|
if ($action eq 'add') { handle_add() }
|
|
elsif ($action eq 'edit') { handle_edit() }
|
|
elsif ($action eq 'delete') { handle_delete() }
|
|
elsif ($action eq 'toggle_cron') { handle_toggle_cron() }
|
|
else { handle_list() }
|
|
|
|
exit;
|
|
|
|
# ── List ─────────────────────────────────────────────────────
|
|
|
|
sub handle_list {
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Schedules', '', '/cgi/gniza-whm/schedules.cgi');
|
|
|
|
print GnizaWHM::UI::page_header('Schedule Management');
|
|
print GnizaWHM::UI::render_nav('schedules.cgi');
|
|
print GnizaWHM::UI::render_flash();
|
|
|
|
# Configured schedules
|
|
my @schedules = GnizaWHM::UI::list_schedules();
|
|
my $cron_schedules = GnizaWHM::Cron::get_current_schedules();
|
|
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Configured Schedules</h2>\n};
|
|
|
|
if (@schedules) {
|
|
print qq{<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100"><table class="table">\n};
|
|
print qq{<thead><tr><th>Name</th><th>Type</th><th>Time</th><th>Day</th><th>Remotes</th><th>Active</th><th>Actions</th></tr></thead>\n};
|
|
print qq{<tbody>\n};
|
|
for my $name (@schedules) {
|
|
my $conf = GnizaWHM::Config::parse(GnizaWHM::UI::schedule_conf_path($name), 'schedule');
|
|
my $esc_name = GnizaWHM::UI::esc($name);
|
|
my $esc_sched = GnizaWHM::UI::esc($conf->{SCHEDULE} // '');
|
|
my $esc_time = GnizaWHM::UI::esc($conf->{SCHEDULE_TIME} // '02:00');
|
|
my $esc_day = GnizaWHM::UI::esc($conf->{SCHEDULE_DAY} // '-');
|
|
my $esc_remotes = GnizaWHM::UI::esc($conf->{REMOTES} // '(all)');
|
|
$esc_remotes = '(all)' if $esc_remotes eq '';
|
|
|
|
my $in_cron = exists $cron_schedules->{$name};
|
|
my $checked = $in_cron ? ' checked' : '';
|
|
|
|
print qq{<tr class="hover">};
|
|
print qq{<td><strong>$esc_name</strong></td>};
|
|
print qq{<td>$esc_sched</td><td>$esc_time</td><td>$esc_day</td><td>$esc_remotes</td>};
|
|
print qq{<td>};
|
|
print qq{<input type="checkbox" class="toggle toggle-sm toggle-success" data-schedule="$esc_name" onchange="gnizaToggleCron(this)"$checked>};
|
|
print qq{</td>};
|
|
print qq{<td>};
|
|
print qq{<a href="schedules.cgi?action=edit&name=$esc_name" class="btn btn-ghost btn-sm">Edit</a> };
|
|
print qq{<form method="POST" action="schedules.cgi" style="display:inline">};
|
|
print qq{<input type="hidden" name="action" value="delete">};
|
|
print qq{<input type="hidden" name="name" value="$esc_name">};
|
|
print GnizaWHM::UI::csrf_hidden_field();
|
|
print qq{<button type="submit" class="btn btn-error btn-sm" onclick="return confirm('Delete schedule $esc_name?')">Delete</button>};
|
|
print qq{</form>};
|
|
print qq{</td>};
|
|
print qq{</tr>\n};
|
|
}
|
|
print qq{</tbody>\n</table></div>\n};
|
|
} else {
|
|
print qq{<p>No schedules configured. Add a schedule to define when backups run.</p>\n};
|
|
}
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# CSRF token + AJAX toggle script
|
|
my $csrf_token = GnizaWHM::UI::generate_csrf_token();
|
|
print qq{<script>
|
|
var gnizaCsrf = '} . GnizaWHM::UI::esc($csrf_token) . qq{';
|
|
function gnizaToggleCron(el) {
|
|
var name = el.getAttribute('data-schedule');
|
|
el.disabled = true;
|
|
var fd = new FormData();
|
|
fd.append('action', 'toggle_cron');
|
|
fd.append('name', name);
|
|
fd.append('gniza_csrf', gnizaCsrf);
|
|
fetch('schedules.cgi', { method: 'POST', body: fd })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
gnizaCsrf = d.csrf;
|
|
el.checked = d.active;
|
|
el.disabled = false;
|
|
})
|
|
.catch(function() {
|
|
el.checked = !el.checked;
|
|
el.disabled = false;
|
|
});
|
|
}
|
|
</script>\n};
|
|
|
|
# Action buttons
|
|
print qq{<div class="flex gap-2 mb-6">\n};
|
|
print qq{ <a href="schedules.cgi?action=add" class="btn btn-primary btn-sm">Add Schedule</a>\n};
|
|
print qq{</div>\n};
|
|
|
|
print GnizaWHM::UI::page_footer();
|
|
Whostmgr::HTMLInterface::footer();
|
|
}
|
|
|
|
# ── Add ──────────────────────────────────────────────────────
|
|
|
|
sub handle_add {
|
|
my @errors;
|
|
|
|
if ($method eq 'POST') {
|
|
unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) {
|
|
push @errors, 'Invalid or expired form token. Please try again.';
|
|
}
|
|
|
|
my $name = $form->{'schedule_name'} // '';
|
|
my $name_err = GnizaWHM::Validator::validate_schedule_name($name);
|
|
push @errors, $name_err if $name_err;
|
|
|
|
if (!@errors && -f GnizaWHM::UI::schedule_conf_path($name)) {
|
|
push @errors, "A schedule named '$name' already exists.";
|
|
}
|
|
|
|
my %data;
|
|
for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) {
|
|
$data{$key} = $form->{$key} // '';
|
|
}
|
|
|
|
if (!@errors) {
|
|
my $validation_errors = GnizaWHM::Validator::validate_schedule_config(\%data);
|
|
push @errors, @$validation_errors;
|
|
}
|
|
|
|
if (!@errors) {
|
|
my $dest = GnizaWHM::UI::schedule_conf_path($name);
|
|
my $example = GnizaWHM::UI::schedule_example_path();
|
|
if (-f $example) {
|
|
File::Copy::copy($example, $dest)
|
|
or do { push @errors, "Failed to create schedule file: $!"; goto RENDER_ADD; };
|
|
}
|
|
my ($ok, $err) = GnizaWHM::Config::write($dest, \%data, \@GnizaWHM::Config::SCHEDULE_KEYS);
|
|
if ($ok) {
|
|
my ($cron_ok, $cron_err) = GnizaWHM::Cron::install_schedule($name);
|
|
if ($form->{'wizard'}) {
|
|
GnizaWHM::UI::set_flash('success', 'Setup complete! Your remote and schedule are configured.');
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: index.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
if ($cron_ok) {
|
|
GnizaWHM::UI::set_flash('success', "Schedule '$name' created and activated.");
|
|
} else {
|
|
GnizaWHM::UI::set_flash('warning', "Schedule '$name' created but cron activation failed: $cron_err");
|
|
}
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
} else {
|
|
push @errors, "Failed to save schedule config: $err";
|
|
}
|
|
}
|
|
}
|
|
|
|
RENDER_ADD:
|
|
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Add Schedule', '', '/cgi/gniza-whm/schedules.cgi');
|
|
|
|
print GnizaWHM::UI::page_header('Add Schedule');
|
|
print GnizaWHM::UI::render_nav('schedules.cgi');
|
|
|
|
if (@errors) {
|
|
print GnizaWHM::UI::render_errors(\@errors);
|
|
}
|
|
|
|
my $conf = {};
|
|
if ($method eq 'POST') {
|
|
for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) {
|
|
$conf->{$key} = $form->{$key} // '';
|
|
}
|
|
} elsif ($form->{'remote_name'}) {
|
|
$conf->{REMOTES} = $form->{'remote_name'};
|
|
}
|
|
my $name_val = GnizaWHM::UI::esc($form->{'schedule_name'} // '');
|
|
|
|
render_schedule_form($conf, $name_val, 0, $form->{'wizard'} ? 1 : 0);
|
|
|
|
print GnizaWHM::UI::page_footer();
|
|
Whostmgr::HTMLInterface::footer();
|
|
}
|
|
|
|
# ── Edit ─────────────────────────────────────────────────────
|
|
|
|
sub handle_edit {
|
|
my $name = $form->{'name'} // '';
|
|
my @errors;
|
|
|
|
my $name_err = GnizaWHM::Validator::validate_schedule_name($name);
|
|
if ($name_err) {
|
|
GnizaWHM::UI::set_flash('error', "Invalid schedule name.");
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
my $conf_path = GnizaWHM::UI::schedule_conf_path($name);
|
|
unless (-f $conf_path) {
|
|
GnizaWHM::UI::set_flash('error', "Schedule '$name' not found.");
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
if ($method eq 'POST') {
|
|
unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) {
|
|
push @errors, 'Invalid or expired form token. Please try again.';
|
|
}
|
|
|
|
my %data;
|
|
for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) {
|
|
$data{$key} = $form->{$key} // '';
|
|
}
|
|
|
|
if (!@errors) {
|
|
my $validation_errors = GnizaWHM::Validator::validate_schedule_config(\%data);
|
|
push @errors, @$validation_errors;
|
|
}
|
|
|
|
if (!@errors) {
|
|
my ($ok, $err) = GnizaWHM::Config::write($conf_path, \%data, \@GnizaWHM::Config::SCHEDULE_KEYS);
|
|
if ($ok) {
|
|
# Re-install cron if it was active, so timing/remote changes take effect
|
|
my $cron_schedules = GnizaWHM::Cron::get_current_schedules();
|
|
if (exists $cron_schedules->{$name}) {
|
|
GnizaWHM::Cron::install_schedule($name);
|
|
}
|
|
GnizaWHM::UI::set_flash('success', "Schedule '$name' updated successfully.");
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
} else {
|
|
push @errors, "Failed to save schedule config: $err";
|
|
}
|
|
}
|
|
}
|
|
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Edit Schedule', '', '/cgi/gniza-whm/schedules.cgi');
|
|
|
|
print GnizaWHM::UI::page_header("Edit Schedule: " . GnizaWHM::UI::esc($name));
|
|
print GnizaWHM::UI::render_nav('schedules.cgi');
|
|
|
|
if (@errors) {
|
|
print GnizaWHM::UI::render_errors(\@errors);
|
|
}
|
|
|
|
my $conf;
|
|
if (@errors && $method eq 'POST') {
|
|
$conf = {};
|
|
for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) {
|
|
$conf->{$key} = $form->{$key} // '';
|
|
}
|
|
} else {
|
|
$conf = GnizaWHM::Config::parse($conf_path, 'schedule');
|
|
}
|
|
|
|
render_schedule_form($conf, GnizaWHM::UI::esc($name), 1);
|
|
|
|
print GnizaWHM::UI::page_footer();
|
|
Whostmgr::HTMLInterface::footer();
|
|
}
|
|
|
|
# ── Delete ───────────────────────────────────────────────────
|
|
|
|
sub handle_delete {
|
|
if ($method ne 'POST') {
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) {
|
|
GnizaWHM::UI::set_flash('error', 'Invalid or expired form token.');
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
my $name = $form->{'name'} // '';
|
|
my $name_err = GnizaWHM::Validator::validate_schedule_name($name);
|
|
if ($name_err) {
|
|
GnizaWHM::UI::set_flash('error', 'Invalid schedule name.');
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
my $conf_path = GnizaWHM::UI::schedule_conf_path($name);
|
|
if (-f $conf_path) {
|
|
GnizaWHM::Cron::remove_schedule($name);
|
|
unlink $conf_path;
|
|
GnizaWHM::UI::set_flash('success', "Schedule '$name' deleted.");
|
|
} else {
|
|
GnizaWHM::UI::set_flash('error', "Schedule '$name' not found.");
|
|
}
|
|
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
# ── Toggle Cron ──────────────────────────────────────────────
|
|
|
|
sub handle_toggle_cron {
|
|
if ($method ne 'POST') {
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: schedules.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) {
|
|
my $new_csrf = GnizaWHM::UI::generate_csrf_token();
|
|
_json_response(0, 0, 'Invalid or expired form token.', $new_csrf);
|
|
}
|
|
|
|
my $new_csrf = GnizaWHM::UI::generate_csrf_token();
|
|
|
|
my $name = $form->{'name'} // '';
|
|
my $name_err = GnizaWHM::Validator::validate_schedule_name($name);
|
|
if ($name_err) {
|
|
_json_response(0, 0, 'Invalid schedule name.', $new_csrf);
|
|
}
|
|
|
|
my $cron_schedules = GnizaWHM::Cron::get_current_schedules();
|
|
my $is_active = exists $cron_schedules->{$name};
|
|
|
|
if ($is_active) {
|
|
my ($ok, $err) = GnizaWHM::Cron::remove_schedule($name);
|
|
if ($ok) {
|
|
_json_response(1, 0, "Cron disabled for '$name'.", $new_csrf);
|
|
} else {
|
|
_json_response(0, 1, "Failed to remove cron: $err", $new_csrf);
|
|
}
|
|
} else {
|
|
my ($ok, $err) = GnizaWHM::Cron::install_schedule($name);
|
|
if ($ok) {
|
|
_json_response(1, 1, "Cron enabled for '$name'.", $new_csrf);
|
|
} else {
|
|
_json_response(0, 0, "Failed to install cron: $err", $new_csrf);
|
|
}
|
|
}
|
|
}
|
|
|
|
sub _json_response {
|
|
my ($ok, $active, $message, $csrf) = @_;
|
|
# Escape for JSON
|
|
$message =~ s/\\/\\\\/g;
|
|
$message =~ s/"/\\"/g;
|
|
my $active_str = $active ? 'true' : 'false';
|
|
my $ok_str = $ok ? 'true' : 'false';
|
|
print "Content-Type: application/json\r\n\r\n";
|
|
print qq({"ok":$ok_str,"active":$active_str,"message":"$message","csrf":"$csrf"});
|
|
exit;
|
|
}
|
|
|
|
# ── Shared Form Renderer ────────────────────────────────────
|
|
|
|
sub render_schedule_form {
|
|
my ($conf, $name_val, $is_edit, $wizard) = @_;
|
|
|
|
my $action_val = $is_edit ? 'edit' : 'add';
|
|
print qq{<form method="POST" action="schedules.cgi">\n};
|
|
print qq{<input type="hidden" name="action" value="$action_val">\n};
|
|
if ($wizard) {
|
|
print qq{<input type="hidden" name="wizard" value="1">\n};
|
|
}
|
|
print GnizaWHM::UI::csrf_hidden_field();
|
|
|
|
if ($is_edit) {
|
|
print qq{<input type="hidden" name="name" value="$name_val">\n};
|
|
}
|
|
|
|
# Schedule name
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Schedule Identity</h2>\n};
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm" for="schedule_name">Schedule Name</label>\n};
|
|
if ($is_edit) {
|
|
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" value="$name_val" disabled>\n};
|
|
} else {
|
|
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="schedule_name" name="schedule_name" value="$name_val" required>\n};
|
|
print qq{ <span class="text-xs text-base-content/60 ml-2">Letters, digits, hyphens, underscores</span>\n};
|
|
}
|
|
print qq{</div>\n};
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# Schedule settings
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Schedule Settings</h2>\n};
|
|
|
|
my $sched = $conf->{SCHEDULE} // '';
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm" for="SCHEDULE">Schedule Type</label>\n};
|
|
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="SCHEDULE" name="SCHEDULE" onchange="gnizaScheduleChange()">\n};
|
|
for my $opt ('hourly', 'daily', 'weekly', 'monthly', 'custom') {
|
|
my $sel = ($sched eq $opt) ? ' selected' : '';
|
|
print qq{ <option value="} . GnizaWHM::UI::esc($opt) . qq{"$sel>$opt</option>\n};
|
|
}
|
|
print qq{ </select>\n};
|
|
print qq{</div>\n};
|
|
|
|
_sched_field($conf, 'SCHEDULE_TIME', 'Time (HH:MM)', 'Default: 02:00');
|
|
|
|
print qq{<div id="gniza-schedule-day">\n};
|
|
_sched_field($conf, 'SCHEDULE_DAY', 'Day', 'Day-of-week 0-6 (weekly) or day-of-month 1-28 (monthly)');
|
|
print qq{</div>\n};
|
|
|
|
print qq{<div id="gniza-schedule-cron">\n};
|
|
_sched_field($conf, 'SCHEDULE_CRON', 'Cron Expression', '5-field cron (for custom only)');
|
|
print qq{</div>\n};
|
|
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# Target remotes
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Target Remotes</h2>\n};
|
|
print qq{<p class="text-xs text-base-content/60 mb-3">Select which remotes this schedule targets. Leave all unchecked to target all remotes.</p>\n};
|
|
|
|
my @remotes = GnizaWHM::UI::list_remotes();
|
|
my %selected_remotes;
|
|
my $remotes_str = $conf->{REMOTES} // '';
|
|
for my $r (split /,/, $remotes_str) {
|
|
$r =~ s/^\s+|\s+$//g;
|
|
$selected_remotes{$r} = 1 if $r ne '';
|
|
}
|
|
|
|
if (@remotes) {
|
|
for my $rname (@remotes) {
|
|
my $esc_name = GnizaWHM::UI::esc($rname);
|
|
my $checked = $selected_remotes{$rname} ? ' checked' : '';
|
|
print qq{<label class="flex items-center gap-2 mb-1 cursor-pointer">\n};
|
|
print qq{ <input type="checkbox" class="checkbox checkbox-sm" name="remote_$esc_name" value="1"$checked>\n};
|
|
print qq{ <span class="text-sm">$esc_name</span>\n};
|
|
print qq{</label>\n};
|
|
}
|
|
# Hidden field to collect selected remotes via JS
|
|
print qq{<input type="hidden" name="REMOTES" id="remotes_hidden" value="} . GnizaWHM::UI::esc($remotes_str) . qq{">\n};
|
|
} else {
|
|
print qq{<p class="text-sm">No remotes configured. <a href="remotes.cgi?action=add" class="link">Add a remote</a> first.</p>\n};
|
|
}
|
|
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# Submit
|
|
print qq{<div class="flex gap-2 mt-4">\n};
|
|
my $btn_label = $is_edit ? 'Save Changes' : 'Create Schedule';
|
|
print qq{ <button type="submit" class="btn btn-primary btn-sm" onclick="return gnizaCollectRemotes()">$btn_label</button>\n};
|
|
print qq{ <a href="schedules.cgi" class="btn btn-ghost btn-sm">Cancel</a>\n};
|
|
print qq{</div>\n};
|
|
|
|
print qq{</form>\n};
|
|
|
|
# JS for schedule field visibility and remote collection
|
|
print <<'JS';
|
|
<script>
|
|
function gnizaScheduleChange() {
|
|
var sel = document.getElementById('SCHEDULE').value;
|
|
var dayDiv = document.getElementById('gniza-schedule-day');
|
|
var cronDiv = document.getElementById('gniza-schedule-cron');
|
|
dayDiv.style.display = (sel === 'hourly' || sel === 'weekly' || sel === 'monthly') ? '' : 'none';
|
|
cronDiv.style.display = (sel === 'custom') ? '' : 'none';
|
|
var dayLabel = dayDiv.querySelector('label');
|
|
var dayInput = dayDiv.querySelector('input');
|
|
var dayHint = dayDiv.querySelector('.text-xs');
|
|
if (sel === 'hourly') {
|
|
if (dayLabel) dayLabel.textContent = 'Every N hours';
|
|
if (dayHint) dayHint.textContent = '1-23 (default: 1 = every hour)';
|
|
} else {
|
|
if (dayLabel) dayLabel.textContent = 'Day';
|
|
if (dayHint) dayHint.textContent = 'Day-of-week 0-6 (weekly) or day-of-month 1-28 (monthly)';
|
|
}
|
|
}
|
|
function gnizaCollectRemotes() {
|
|
var checks = document.querySelectorAll('input[type="checkbox"][name^="remote_"]');
|
|
var selected = [];
|
|
for (var i = 0; i < checks.length; i++) {
|
|
if (checks[i].checked) {
|
|
selected.push(checks[i].name.replace('remote_', ''));
|
|
}
|
|
}
|
|
var hidden = document.getElementById('remotes_hidden');
|
|
if (hidden) { hidden.value = selected.join(','); }
|
|
return true;
|
|
}
|
|
gnizaScheduleChange();
|
|
</script>
|
|
JS
|
|
}
|
|
|
|
sub _sched_field {
|
|
my ($conf, $key, $label, $hint) = @_;
|
|
my $val = GnizaWHM::UI::esc($conf->{$key} // '');
|
|
my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : '';
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm" for="$key">$label</label>\n};
|
|
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="$key" name="$key" value="$val">\n};
|
|
print qq{ $hint_html\n} if $hint;
|
|
print qq{</div>\n};
|
|
}
|