Add terminate-before-restore toggle, logo, and installer improvements
- Add "Terminate First" toggle to restore page (UI, Runner, CLI, lib) - When enabled, removes existing cPanel account before restoring - Add GNIZA Backup SVG logo to WHM plugin header (inline base64) - Copy uninstall.sh to /usr/local/gniza/ during installation - Update CLAUDE.md with new restore params and Runner options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
30
CLAUDE.md
30
CLAUDE.md
@@ -52,6 +52,7 @@ whm/
|
||||
├── restore.cgi # Restore workflow — 4-step form (account → snapshot → confirm → execute)
|
||||
├── assets/
|
||||
│ ├── gniza-whm.css # Built Tailwind/DaisyUI CSS (committed, ~58KB)
|
||||
│ ├── gniza-logo.svg # SVG logo (embedded as data URI in page header)
|
||||
│ └── src/
|
||||
│ ├── input.css # Tailwind v4 entry point with DaisyUI plugin
|
||||
│ ├── safelist.html # Class safelist for Tailwind content scanner
|
||||
@@ -384,7 +385,7 @@ All restore functions dispatch by `_is_rclone_mode` — using `rclone_from_remot
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `restore_full_account(user, ts)` | Full account restore from snapshot |
|
||||
| `restore_full_account(user, ts, terminate, exclude)` | Full account restore from snapshot. If `terminate=true`, removes existing account via `/scripts/removeacct` before restoring. Otherwise merges with `--force`. |
|
||||
| `restore_files(user, ts, path)` | Restore specific files/directories |
|
||||
| `restore_database(user, ts, dbname)` | Restore a MySQL database from snapshot |
|
||||
| `restore_mailbox(user, email, ts)` | Restore a mailbox (parses email → mail/domain/user path) |
|
||||
@@ -442,7 +443,7 @@ Pattern-based command runner for safe CLI execution from the WHM UI. Each allowe
|
||||
| `run($cmd, $subcmd, \@args, \%opts)` | Validate against allowlist and execute gniza CLI |
|
||||
|
||||
Allowed commands: `restore account/files/database/mailbox/list-databases/list-mailboxes`, `list`.
|
||||
Named option patterns: `--remote`, `--timestamp`, `--path`, `--account`.
|
||||
Named option patterns: `--remote`, `--timestamp`, `--path`, `--account`, `--terminate`, `--exclude`.
|
||||
|
||||
### GnizaWHM::Config
|
||||
|
||||
@@ -525,6 +526,31 @@ _restore_remote_globals
|
||||
|
||||
**NEVER write custom CSS.** Always use Tailwind utility classes and DaisyUI components exclusively. All styling must be done through class attributes in HTML — no custom CSS rules, no `<style>` blocks (except the auto-generated inline delivery in `page_header()`), no CSS files other than the Tailwind build output.
|
||||
|
||||
### WHM Theme & Color Palette
|
||||
|
||||
The WHM plugin uses a custom DaisyUI theme named `gniza` (defined in `assets/src/input.css`). Light-only, no dark mode.
|
||||
|
||||
| Role | OKLCH Value | Approx Color |
|
||||
|------|-------------|-------------|
|
||||
| **Primary** | `oklch(38.2% 0.145 259.4)` | Deep navy blue |
|
||||
| **Primary content** | `oklch(100% 0 0)` | White |
|
||||
| **Secondary** | `oklch(69.5% 0.169 47.8)` | Warm copper/orange |
|
||||
| **Secondary content** | `oklch(100% 0 0)` | White |
|
||||
| **Accent / Warning** | `oklch(86.4% 0.177 90.8)` | Soft gold/yellow |
|
||||
| **Accent content / Warning content** | `oklch(30.9% 0.116 258.9)` | Dark navy |
|
||||
| **Neutral / Base content** | `oklch(30.9% 0.116 258.9)` | Dark navy |
|
||||
| **Neutral content** | `oklch(100% 0 0)` | White |
|
||||
| **Base 100** | `transparent` | Transparent (inherits WHM background) |
|
||||
| **Base 200** | `oklch(97% 0 0)` | Near-white gray |
|
||||
| **Base 300** | `oklch(89.8% 0 0)` | Light gray |
|
||||
| **Info** | `oklch(69% 0.083 217.5)` | Muted blue |
|
||||
| **Success** | `oklch(65% 0.25 140)` | Vivid green |
|
||||
| **Error** | `oklch(57.7% 0.245 27.3)` | Red |
|
||||
|
||||
**Typography:** `'Helvetica Neue', Helvetica, Arial, sans-serif`
|
||||
**Border radius:** `0.5rem` (boxes/selectors), `0.25rem` (fields)
|
||||
**Base font size:** `1.6rem` (set on the `data-theme` wrapper to match WHM's sizing)
|
||||
|
||||
### WHM CSS Build System (Tailwind v4 + DaisyUI v5)
|
||||
|
||||
All WHM pages use Tailwind CSS v4 with DaisyUI v5 for styling. The CSS is built from source and committed.
|
||||
|
||||
@@ -293,7 +293,7 @@ cmd_restore() {
|
||||
account)
|
||||
local name="${1:-}"
|
||||
shift 2>/dev/null || true
|
||||
[[ -z "$name" ]] && die "Usage: gniza restore account <name> [--remote=NAME] [--timestamp=TS] [--force]"
|
||||
[[ -z "$name" ]] && die "Usage: gniza restore account <name> [--remote=NAME] [--timestamp=TS] [--terminate]"
|
||||
|
||||
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
||||
load_config "$config_file"
|
||||
@@ -305,9 +305,12 @@ cmd_restore() {
|
||||
|
||||
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
||||
local exclude; exclude=$(get_opt exclude "$@" 2>/dev/null) || exclude=""
|
||||
local terminate_val; terminate_val=$(get_opt terminate "$@" 2>/dev/null) || terminate_val=""
|
||||
local terminate=false
|
||||
[[ "$terminate_val" == "1" ]] && terminate=true
|
||||
|
||||
_test_connection
|
||||
restore_full_account "$name" "$timestamp" "" "$exclude"
|
||||
restore_full_account "$name" "$timestamp" "$terminate" "$exclude"
|
||||
;;
|
||||
files)
|
||||
local name="${1:-}"
|
||||
|
||||
@@ -57,7 +57,7 @@ _detect_pkgacct_base() {
|
||||
restore_full_account() {
|
||||
local user="$1"
|
||||
local timestamp="${2:-}"
|
||||
local _unused="${3:-}" # formerly strategy, kept for call-site compat
|
||||
local terminate="${3:-}"
|
||||
local exclude="${4:-}"
|
||||
local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}"
|
||||
|
||||
@@ -127,6 +127,16 @@ restore_full_account() {
|
||||
find "$mysql_dir" -name "*.sql.gz" -exec gunzip -f {} \;
|
||||
fi
|
||||
|
||||
# Terminate existing account if requested
|
||||
if [[ "$terminate" == "true" ]] && account_exists "$user"; then
|
||||
log_info "Terminating existing account $user before restore..."
|
||||
if ! /scripts/removeacct --skipbw --force "$user"; then
|
||||
log_error "Failed to terminate account $user"
|
||||
rm -rf "$restore_dir"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run restorepkg (--force to merge into existing account if present)
|
||||
log_info "Running restorepkg for $user..."
|
||||
local -a restorepkg_args=()
|
||||
@@ -1386,7 +1396,7 @@ restore_server() {
|
||||
((total++)) || true
|
||||
|
||||
log_info "--- Restoring account $user ($total) ---"
|
||||
if restore_full_account "$user" "$timestamp" "merge"; then
|
||||
if restore_full_account "$user" "$timestamp" "" ""; then
|
||||
((succeeded++)) || true
|
||||
else
|
||||
((failed++)) || true
|
||||
|
||||
@@ -40,6 +40,8 @@ mkdir -p "$INSTALL_DIR"
|
||||
cp -r "$SOURCE_DIR/bin" "$INSTALL_DIR/"
|
||||
cp -r "$SOURCE_DIR/lib" "$INSTALL_DIR/"
|
||||
cp -r "$SOURCE_DIR/etc" "$INSTALL_DIR/"
|
||||
cp "$SOURCE_DIR/scripts/uninstall.sh" "$INSTALL_DIR/uninstall.sh"
|
||||
chmod +x "$INSTALL_DIR/uninstall.sh"
|
||||
|
||||
# Make bin executable
|
||||
chmod +x "$INSTALL_DIR/bin/gniza"
|
||||
|
||||
25
whm/gniza-whm/assets/gniza-logo.svg
Normal file
25
whm/gniza-whm/assets/gniza-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 369 KiB |
@@ -48,6 +48,7 @@ my %OPT_PATTERNS = (
|
||||
path => qr/^[a-zA-Z0-9_.\/@ -]+$/,
|
||||
account => qr/^[a-z][a-z0-9_-]*$/,
|
||||
exclude => qr/^[a-zA-Z0-9_.,\/@ *?\[\]-]+$/,
|
||||
terminate => qr/^1$/,
|
||||
);
|
||||
|
||||
# _validate($cmd, $subcmd, \@args, \%opts)
|
||||
|
||||
@@ -19,6 +19,7 @@ my $REMOTE_EXAMPLE = '/usr/local/gniza/etc/remote.conf.example';
|
||||
my $SCHEDULE_EXAMPLE = '/usr/local/gniza/etc/schedule.conf.example';
|
||||
my $SSH_DIR = '/root/.ssh';
|
||||
my $CSS_FILE = '/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/assets/gniza-whm.css';
|
||||
my $LOGO_FILE = '/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/assets/gniza-logo.svg';
|
||||
|
||||
# ── HTML Escaping ─────────────────────────────────────────────
|
||||
|
||||
@@ -631,8 +632,19 @@ sub page_header {
|
||||
# Scope :root/:host to our container so DaisyUI base styles
|
||||
# (background, color, overflow, scrollbar) don't leak into WHM.
|
||||
$css = _scope_to_container($css);
|
||||
# Inline logo as base64 data URI
|
||||
my $logo_html = '';
|
||||
if (open my $lfh, '<', $LOGO_FILE) {
|
||||
local $/;
|
||||
my $svg_data = <$lfh>;
|
||||
close $lfh;
|
||||
require MIME::Base64;
|
||||
my $b64 = MIME::Base64::encode_base64($svg_data, '');
|
||||
$logo_html = qq{<div class="flex items-center justify-center gap-3 mb-4"><img src="data:image/svg+xml;base64,$b64" alt="GNIZA" style="height:48px;width:auto"></div>\n};
|
||||
}
|
||||
return qq{<style>$css</style>\n}
|
||||
. qq{<div data-theme="gniza" class="font-sans text-[1.6rem]" style="padding:30px 10px 10px 10px">\n};
|
||||
. qq{<div data-theme="gniza" class="font-sans text-[1.6rem]" style="padding:30px 10px 10px 10px">\n}
|
||||
. $logo_html;
|
||||
}
|
||||
|
||||
sub _unwrap_layers {
|
||||
|
||||
@@ -247,6 +247,12 @@ sub handle_step2 {
|
||||
print qq{ </div>\n};
|
||||
print qq{</div>\n};
|
||||
|
||||
# Terminate toggle (only visible in Full Account mode)
|
||||
print qq{<div id="terminate-panel" class="flex items-center gap-3 mb-2.5">\n};
|
||||
print qq{ <label class="w-52 font-medium text-sm whitespace-nowrap">Terminate First <span class="tooltip tooltip-top" data-tip="Remove the existing cPanel account before restoring. Results in a clean restore but causes brief downtime.">ⓘ</span></label>\n};
|
||||
print qq{ <input type="checkbox" class="toggle toggle-sm toggle-error" id="terminate" name="terminate" value="1">\n};
|
||||
print qq{</div>\n};
|
||||
|
||||
# Exclude paths (visible in both Full Account and Selective modes)
|
||||
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3 mt-3">\n};
|
||||
print qq{<div class="card-body py-3 px-4">\n};
|
||||
@@ -443,6 +449,7 @@ function gnizaModeChanged() {
|
||||
var mode = document.querySelector('input[name="restore_mode"]:checked').value;
|
||||
var selective = mode === 'selective';
|
||||
document.getElementById('selective-panel').hidden = !selective;
|
||||
document.getElementById('terminate-panel').hidden = selective;
|
||||
document.getElementById('type_account_hidden').disabled = selective;
|
||||
if (selective) {
|
||||
gnizaTypesChanged();
|
||||
@@ -851,6 +858,7 @@ sub handle_step3 {
|
||||
my $domain_names = $form->{'domain_names'} // '';
|
||||
my $ssl_names = $form->{'ssl_names'} // '';
|
||||
my $exclude_paths = $form->{'exclude_paths'} // '';
|
||||
my $terminate = $form->{'terminate'} // '';
|
||||
|
||||
# Collect selected types from type_* checkboxes
|
||||
my @all_type_keys = qw(account files database mailbox cron dbusers cpconfig domains ssl);
|
||||
@@ -912,6 +920,10 @@ sub handle_step3 {
|
||||
print qq{<tr><td class="font-medium">SSL</td><td>$ssl_display</td></tr>\n};
|
||||
}
|
||||
|
||||
if ($terminate eq '1' && grep { $_ eq 'account' } @selected_types) {
|
||||
print qq{<tr><td class="font-medium">Terminate First</td><td class="text-error font-medium">Yes — account will be removed before restore</td></tr>\n};
|
||||
}
|
||||
|
||||
if ($exclude_paths ne '') {
|
||||
my $exclude_display = GnizaWHM::UI::esc($exclude_paths);
|
||||
$exclude_display =~ s/,/, /g;
|
||||
@@ -936,6 +948,7 @@ sub handle_step3 {
|
||||
print qq{<input type="hidden" name="domain_names" value="} . GnizaWHM::UI::esc($domain_names) . qq{">\n};
|
||||
print qq{<input type="hidden" name="ssl_names" value="} . GnizaWHM::UI::esc($ssl_names) . qq{">\n};
|
||||
print qq{<input type="hidden" name="exclude_paths" value="} . GnizaWHM::UI::esc($exclude_paths) . qq{">\n};
|
||||
print qq{<input type="hidden" name="terminate" value="} . GnizaWHM::UI::esc($terminate) . qq{">\n};
|
||||
print GnizaWHM::UI::csrf_hidden_field();
|
||||
|
||||
print qq{<div class="flex items-center gap-2">\n};
|
||||
@@ -968,6 +981,7 @@ sub handle_step4 {
|
||||
my $domain_names = $form->{'domain_names'} // '';
|
||||
my $ssl_names = $form->{'ssl_names'} // '';
|
||||
my $exclude_paths = $form->{'exclude_paths'} // '';
|
||||
my $terminate = $form->{'terminate'} // '';
|
||||
|
||||
# Collect selected types
|
||||
my @all_type_keys = qw(account files database mailbox cron dbusers cpconfig domains ssl);
|
||||
@@ -990,7 +1004,10 @@ sub handle_step4 {
|
||||
$opts{timestamp} = $timestamp if $timestamp ne '';
|
||||
|
||||
if ($SIMPLE_TYPES{$type}) {
|
||||
$opts{exclude} = $exclude_paths if $exclude_paths ne '' && $type eq 'account';
|
||||
if ($type eq 'account') {
|
||||
$opts{exclude} = $exclude_paths if $exclude_paths ne '';
|
||||
$opts{terminate} = '1' if $terminate eq '1';
|
||||
}
|
||||
push @commands, ['restore', $type, [$account], {%opts}];
|
||||
} elsif ($type eq 'files') {
|
||||
$opts{path} = $path if $path ne '';
|
||||
|
||||
Reference in New Issue
Block a user