Add info tooltips to form fields across remotes, settings, and restore pages

Adds ⓘ tooltip icons with contextual help text to technical fields:
- remotes.cgi: SSH key, S3 endpoint, GDrive service account/folder ID,
  base dir, bandwidth limit, rsync options, retention count
- settings.cgi: working dir, log retention, include/exclude accounts,
  lock file, SSH timeout/retries, rsync options
- restore.cgi: restore mode, restore strategy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-04 19:28:57 +02:00
parent 5b19d5d29e
commit 8dcd3aaca7
3 changed files with 24 additions and 22 deletions

View File

@@ -567,7 +567,7 @@ sub render_remote_form {
# Key field # Key field
print qq{<div id="auth-key-field"$key_hidden>\n}; print qq{<div id="auth-key-field"$key_hidden>\n};
_field($conf, 'REMOTE_KEY', 'SSH Private Key', 'Absolute path'); _field($conf, 'REMOTE_KEY', 'SSH Private Key', 'Absolute path', 'Path to the private key file used for passwordless SSH authentication');
print qq{</div>\n}; print qq{</div>\n};
# Password field # Password field
@@ -592,7 +592,7 @@ sub render_remote_form {
_field($conf, 'S3_ACCESS_KEY_ID', 'Access Key ID', 'Required'); _field($conf, 'S3_ACCESS_KEY_ID', 'Access Key ID', 'Required');
_password_field($conf, 'S3_SECRET_ACCESS_KEY', 'Secret Access Key', 'Required'); _password_field($conf, 'S3_SECRET_ACCESS_KEY', 'Secret Access Key', 'Required');
_field($conf, 'S3_REGION', 'Region', 'Default: us-east-1'); _field($conf, 'S3_REGION', 'Region', 'Default: us-east-1');
_field($conf, 'S3_ENDPOINT', 'Custom Endpoint', 'For MinIO, Wasabi, etc.'); _field($conf, 'S3_ENDPOINT', 'Custom Endpoint', 'For MinIO, Wasabi, etc.', 'Only needed for S3-compatible services, leave empty for AWS');
_field($conf, 'S3_BUCKET', 'Bucket Name', 'Required'); _field($conf, 'S3_BUCKET', 'Bucket Name', 'Required');
print qq{<p class="text-xs text-base-content/60 mt-2">Requires <code>rclone</code> installed on this server.</p>\n}; print qq{<p class="text-xs text-base-content/60 mt-2">Requires <code>rclone</code> installed on this server.</p>\n};
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
@@ -604,8 +604,8 @@ sub render_remote_form {
print qq{<div id="type-gdrive-fields"$gdrive_hidden>\n}; print qq{<div id="type-gdrive-fields"$gdrive_hidden>\n};
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n}; print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Google Drive</h2>\n}; print qq{<h2 class="card-title text-sm">Google Drive</h2>\n};
_field($conf, 'GDRIVE_SERVICE_ACCOUNT_FILE', 'Service Account JSON', 'Absolute path, required'); _field($conf, 'GDRIVE_SERVICE_ACCOUNT_FILE', 'Service Account JSON', 'Absolute path, required', 'Google Cloud service account key file for API access');
_field($conf, 'GDRIVE_ROOT_FOLDER_ID', 'Root Folder ID', 'Optional'); _field($conf, 'GDRIVE_ROOT_FOLDER_ID', 'Root Folder ID', 'Optional', 'Google Drive folder ID to use as the root for backups');
print qq{<p class="text-xs text-base-content/60 mt-2">Requires <code>rclone</code> installed on this server.</p>\n}; print qq{<p class="text-xs text-base-content/60 mt-2">Requires <code>rclone</code> installed on this server.</p>\n};
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
print qq{</div>\n}; print qq{</div>\n};
@@ -613,22 +613,22 @@ sub render_remote_form {
# ── Common fields ───────────────────────────────────────── # ── Common fields ─────────────────────────────────────────
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n}; print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Storage Path</h2>\n}; print qq{<h2 class="card-title text-sm">Storage Path</h2>\n};
_field($conf, 'REMOTE_BASE', 'Remote Base Dir', 'Default: /backups'); _field($conf, 'REMOTE_BASE', 'Remote Base Dir', 'Default: /backups', 'Root directory on the remote where all backup snapshots are stored');
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
# Transfer # Transfer
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n}; print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Transfer Settings</h2>\n}; print qq{<h2 class="card-title text-sm">Transfer Settings</h2>\n};
_field($conf, 'BWLIMIT', 'Bandwidth Limit', 'KB/s, 0 = unlimited'); _field($conf, 'BWLIMIT', 'Bandwidth Limit', 'KB/s, 0 = unlimited', 'Throttle transfer speed to avoid saturating the network');
print qq{<div id="rsync-opts-field"$ssh_hidden>\n}; print qq{<div id="rsync-opts-field"$ssh_hidden>\n};
_field($conf, 'RSYNC_EXTRA_OPTS', 'Extra rsync Options', 'SSH only'); _field($conf, 'RSYNC_EXTRA_OPTS', 'Extra rsync Options', 'SSH only', 'Additional flags appended to rsync commands, e.g. --compress');
print qq{</div>\n}; print qq{</div>\n};
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
# Retention # Retention
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n}; print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Retention</h2>\n}; print qq{<h2 class="card-title text-sm">Retention</h2>\n};
_field($conf, 'RETENTION_COUNT', 'Snapshots to Keep', 'Default: 30'); _field($conf, 'RETENTION_COUNT', 'Snapshots to Keep', 'Default: 30', 'Number of backup snapshots to retain per account before pruning old ones');
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
# Submit # Submit
@@ -768,11 +768,12 @@ JS
} }
sub _field { sub _field {
my ($conf, $key, $label, $hint) = @_; my ($conf, $key, $label, $hint, $tip) = @_;
my $val = GnizaWHM::UI::esc($conf->{$key} // ''); my $val = GnizaWHM::UI::esc($conf->{$key} // '');
my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : ''; my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : '';
my $tip_html = $tip ? qq{ <span class="tooltip tooltip-top" data-tip="$tip">&#9432;</span>} : '';
print qq{<div class="flex items-center gap-3 mb-2.5">\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="$key">$label</label>\n}; print qq{ <label class="w-44 font-medium text-sm whitespace-nowrap" for="$key">$label$tip_html</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{ <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{ $hint_html\n} if $hint;
print qq{</div>\n}; print qq{</div>\n};

View File

@@ -240,7 +240,7 @@ sub handle_step2 {
# Restore mode toggle: Full Account vs Selective # Restore mode toggle: Full Account vs Selective
print qq{<div class="flex items-center gap-3 mb-2.5">\n}; print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-44 font-medium text-sm">Restore Mode</label>\n}; print qq{ <label class="w-44 font-medium text-sm whitespace-nowrap">Restore Mode <span class="tooltip tooltip-top" data-tip="Full Account restores everything; Selective lets you pick specific items like files, databases, or mailboxes">&#9432;</span></label>\n};
print qq{ <div class="join inline-flex items-stretch">\n}; print qq{ <div class="join inline-flex items-stretch">\n};
print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Full Account" value="full" checked onchange="gnizaModeChanged()">\n}; print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Full Account" value="full" checked onchange="gnizaModeChanged()">\n};
print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Selective" value="selective" onchange="gnizaModeChanged()">\n}; print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Selective" value="selective" onchange="gnizaModeChanged()">\n};
@@ -249,7 +249,7 @@ sub handle_step2 {
# Restore strategy (visible only for Full Account mode) # Restore strategy (visible only for Full Account mode)
print qq{<div id="strategy-panel" class="flex items-center gap-3 mb-2.5">\n}; print qq{<div id="strategy-panel" class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-44 font-medium text-sm">Restore Strategy</label>\n}; print qq{ <label class="w-44 font-medium text-sm whitespace-nowrap">Restore Strategy <span class="tooltip tooltip-top" data-tip="Overwrite merges into the existing account; Terminate deletes the account first and re-creates it from the backup">&#9432;</span></label>\n};
print qq{ <div class="join inline-flex items-stretch">\n}; print qq{ <div class="join inline-flex items-stretch">\n};
print qq{ <input type="radio" name="strategy" class="join-item btn btn-sm m-0" aria-label="Overwrite (merge)" value="merge" checked>\n}; print qq{ <input type="radio" name="strategy" class="join-item btn btn-sm m-0" aria-label="Overwrite (merge)" value="merge" checked>\n};
print qq{ <input type="radio" name="strategy" class="join-item btn btn-sm m-0" aria-label="Terminate & re-create" value="terminate">\n}; print qq{ <input type="radio" name="strategy" class="join-item btn btn-sm m-0" aria-label="Terminate & re-create" value="terminate">\n};

View File

@@ -123,12 +123,13 @@ if (@errors && $method eq 'POST') {
# Helper to output a text field row # Helper to output a text field row
sub field_text { sub field_text {
my ($key, $label, $hint, $extra) = @_; my ($key, $label, $hint, $extra, $tip) = @_;
$extra //= ''; $extra //= '';
my $val = GnizaWHM::UI::esc($conf->{$key} // ''); my $val = GnizaWHM::UI::esc($conf->{$key} // '');
my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : ''; my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : '';
my $tip_html = $tip ? qq{ <span class="tooltip tooltip-top" data-tip="$tip">&#9432;</span>} : '';
print qq{<div class="flex items-center gap-3 mb-2.5">\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="$key">$label</label>\n}; print qq{ <label class="w-44 font-medium text-sm whitespace-nowrap" for="$key">$label$tip_html</label>\n};
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="$key" name="$key" value="$val" $extra>\n}; print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="$key" name="$key" value="$val" $extra>\n};
print qq{ $hint_html\n} if $hint; print qq{ $hint_html\n} if $hint;
print qq{</div>\n}; print qq{</div>\n};
@@ -158,7 +159,7 @@ print GnizaWHM::UI::csrf_hidden_field();
# Section: Local Settings # Section: Local Settings
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n}; print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Local Settings</h2>\n}; print qq{<h2 class="card-title text-sm">Local Settings</h2>\n};
field_text('TEMP_DIR', 'Working Directory', 'Default: /usr/local/gniza/workdir'); field_text('TEMP_DIR', 'Working Directory', 'Default: /usr/local/gniza/workdir', '', 'Temporary directory used for pkgacct output before transfer');
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
# Section: Account Filtering # Section: Account Filtering
@@ -167,11 +168,11 @@ print qq{<h2 class="card-title text-sm">Account Filtering</h2>\n};
my $inc_val = GnizaWHM::UI::esc($conf->{INCLUDE_ACCOUNTS} // ''); my $inc_val = GnizaWHM::UI::esc($conf->{INCLUDE_ACCOUNTS} // '');
my $exc_val = GnizaWHM::UI::esc($conf->{EXCLUDE_ACCOUNTS} // ''); my $exc_val = GnizaWHM::UI::esc($conf->{EXCLUDE_ACCOUNTS} // '');
print qq{<div class="flex items-center gap-3 mb-2.5">\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="INCLUDE_ACCOUNTS">Include Accounts</label>\n}; print qq{ <label class="w-44 font-medium text-sm whitespace-nowrap" for="INCLUDE_ACCOUNTS">Include Accounts <span class="tooltip tooltip-top" data-tip="Only back up these accounts. Leave empty to back up all accounts.">&#9432;</span></label>\n};
print qq{ <textarea class="textarea textarea-bordered textarea-sm w-full max-w-xs" id="INCLUDE_ACCOUNTS" name="INCLUDE_ACCOUNTS" placeholder="Comma-separated, empty = all">$inc_val</textarea>\n}; print qq{ <textarea class="textarea textarea-bordered textarea-sm w-full max-w-xs" id="INCLUDE_ACCOUNTS" name="INCLUDE_ACCOUNTS" placeholder="Comma-separated, empty = all">$inc_val</textarea>\n};
print qq{</div>\n}; print qq{</div>\n};
print qq{<div class="flex items-center gap-3 mb-2.5">\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="EXCLUDE_ACCOUNTS">Exclude Accounts</label>\n}; print qq{ <label class="w-44 font-medium text-sm whitespace-nowrap" for="EXCLUDE_ACCOUNTS">Exclude Accounts <span class="tooltip tooltip-top" data-tip="These accounts will never be backed up">&#9432;</span></label>\n};
print qq{ <textarea class="textarea textarea-bordered textarea-sm w-full max-w-xs" id="EXCLUDE_ACCOUNTS" name="EXCLUDE_ACCOUNTS" placeholder="Comma-separated">$exc_val</textarea>\n}; print qq{ <textarea class="textarea textarea-bordered textarea-sm w-full max-w-xs" id="EXCLUDE_ACCOUNTS" name="EXCLUDE_ACCOUNTS" placeholder="Comma-separated">$exc_val</textarea>\n};
print qq{</div>\n}; print qq{</div>\n};
@@ -188,7 +189,7 @@ print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div
print qq{<h2 class="card-title text-sm">Logging</h2>\n}; print qq{<h2 class="card-title text-sm">Logging</h2>\n};
field_text('LOG_DIR', 'Log Directory', 'Default: /var/log/gniza'); field_text('LOG_DIR', 'Log Directory', 'Default: /var/log/gniza');
field_select('LOG_LEVEL', 'Log Level', ['debug', 'info', 'warn', 'error']); field_select('LOG_LEVEL', 'Log Level', ['debug', 'info', 'warn', 'error']);
field_text('LOG_RETAIN', 'Log Retention (days)', 'Default: 90'); field_text('LOG_RETAIN', 'Log Retention (days)', 'Default: 90', '', 'Log files older than this are automatically deleted');
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
# Section: Notifications # Section: Notifications
@@ -225,10 +226,10 @@ print qq{</div>\n</div>\n};
# Section: Advanced # Section: Advanced
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n}; print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Advanced</h2>\n}; print qq{<h2 class="card-title text-sm">Advanced</h2>\n};
field_text('LOCK_FILE', 'Lock File', 'Default: /var/run/gniza.lock'); field_text('LOCK_FILE', 'Lock File', 'Default: /var/run/gniza.lock', '', 'Prevents multiple backup processes from running simultaneously');
field_text('SSH_TIMEOUT', 'SSH Timeout (seconds)', 'Default: 30'); field_text('SSH_TIMEOUT', 'SSH Timeout (seconds)', 'Default: 30', '', 'How long to wait for an SSH connection before giving up');
field_text('SSH_RETRIES', 'SSH Retries', 'Default: 3'); field_text('SSH_RETRIES', 'SSH Retries', 'Default: 3', '', 'Number of rsync retry attempts with exponential backoff on failure');
field_text('RSYNC_EXTRA_OPTS', 'Extra rsync Options', 'Additional flags for rsync'); field_text('RSYNC_EXTRA_OPTS', 'Extra rsync Options', 'Additional flags for rsync', '', 'Global extra flags appended to all rsync commands, e.g. --compress');
print qq{</div>\n</div>\n}; print qq{</div>\n</div>\n};
# Submit # Submit