Improve PMG connection testing to validate metrics endpoints

Related to #551

Enhanced the PMG connection test to actually validate the metrics
endpoints that Pulse uses for monitoring, rather than only checking
the version endpoint. This provides users with immediate feedback if
their PMG credentials lack the necessary permissions to collect metrics.

Backend changes:
- Test mail statistics, cluster status, and quarantine endpoints during
  connection test (internal/api/config_handlers.go:1695-1714)
- Return warnings array in test response when endpoints are unavailable
- Increased timeout from 10s to 15s to accommodate multiple endpoint checks
- Added warning logs for failed endpoint checks

Frontend changes:
- Added showWarning() toast function for warning messages
- Enhanced NodeModal to display warning status with amber styling
- Added warnings list display in test results UI
- Updated Settings.tsx to show warnings from connection tests

This change helps users identify permission issues immediately rather
than discovering later that metrics aren't being collected despite a
"successful" connection.
This commit is contained in:
rcourtman 2025-11-05 18:40:39 +00:00
parent c93581e1aa
commit 449d77504f
4 changed files with 69 additions and 6 deletions

View file

@ -57,6 +57,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
status: string;
message: string;
isCluster?: boolean;
warnings?: string[];
} | null>(null);
const [isTesting, setIsTesting] = createSignal(false);
@ -362,9 +363,10 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
try {
const result = await NodesAPI.testConnection(testData as NodeConfig);
setTestResult({
status: 'success',
status: result.warnings && result.warnings.length > 0 ? 'warning' : 'success',
message: result.message || 'Connection successful',
isCluster: result.isCluster,
warnings: result.warnings,
});
} catch (error) {
logger.error('Test connection error:', error);
@ -1863,7 +1865,9 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
class={`mx-6 p-3 rounded-lg text-sm ${
testResult()?.status === 'success'
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200'
: testResult()?.status === 'warning'
? 'bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 text-amber-800 dark:text-amber-200'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200'
}`}
>
<div class="flex items-start gap-2">
@ -1881,6 +1885,19 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
<circle cx="12" cy="12" r="10"></circle>
</svg>
</Show>
<Show when={testResult()?.status === 'warning'}>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
class="flex-shrink-0 mt-0.5"
>
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</Show>
<Show when={testResult()?.status === 'error'}>
<svg
width="16"
@ -1896,13 +1913,23 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
</Show>
<div>
<div class="flex-1">
<p>{testResult()?.message}</p>
<Show when={testResult()?.isCluster}>
<p class="mt-1 text-xs opacity-80">
Cluster detected! All cluster nodes will be automatically added.
</p>
</Show>
<Show when={testResult()?.warnings && testResult()!.warnings!.length > 0}>
<div class="mt-2 space-y-1">
<p class="text-xs font-semibold opacity-90">Warnings:</p>
<ul class="text-xs space-y-0.5 opacity-80">
<For each={testResult()?.warnings}>
{(warning) => <li> {warning}</li>}
</For>
</ul>
</div>
</Show>
</div>
</div>
</div>

View file

@ -12,7 +12,7 @@ import {
import type { JSX } from 'solid-js';
import { useNavigate, useLocation } from '@solidjs/router';
import { useWebSocket } from '@/App';
import { showSuccess, showError } from '@/utils/toast';
import { showSuccess, showError, showWarning } from '@/utils/toast';
import { copyToClipboard } from '@/utils/clipboard';
import { getPulsePort, getPulseWebSocketUrl } from '@/utils/url';
import { logger } from '@/utils/logger';
@ -1843,7 +1843,13 @@ const Settings: Component<SettingsProps> = (props) => {
// Use the existing node test endpoint which uses stored credentials
const result = await NodesAPI.testExistingNode(nodeId);
if (result.status === 'success') {
showSuccess(result.message || 'Connection successful');
// Check for warnings in the response
if (result.warnings && Array.isArray(result.warnings) && result.warnings.length > 0) {
const warningMessage = result.message + '\n\nWarnings:\n' + result.warnings.map((w: string) => '• ' + w).join('\n');
showWarning(warningMessage);
} else {
showSuccess(result.message || 'Connection successful');
}
} else {
throw new Error(result.message || 'Connection failed');
}

View file

@ -23,3 +23,5 @@ export const showSuccess = (title: string, message?: string, duration?: number)
showToast('success', title, message, duration);
export const showError = (title: string, message?: string, duration?: number) =>
showToast('error', title, message, duration);
export const showWarning = (title: string, message?: string, duration?: number) =>
showToast('warning', title, message, duration);

View file

@ -1675,7 +1675,7 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
version, err := tempClient.GetVersion(ctx)
@ -1692,10 +1692,34 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
}
}
// Test actual metrics endpoints to ensure monitoring will work
warnings := []string{}
// Test mail statistics endpoint (core PMG functionality)
if _, err := tempClient.GetMailStatistics(ctx, "day"); err != nil {
warnings = append(warnings, "Mail statistics endpoint unavailable - check user permissions")
log.Warn().Err(err).Msg("PMG connection test: mail statistics check failed")
}
// Test cluster status endpoint
if _, err := tempClient.GetClusterStatus(ctx, true); err != nil {
warnings = append(warnings, "Cluster status endpoint unavailable")
log.Warn().Err(err).Msg("PMG connection test: cluster status check failed")
}
// Test quarantine endpoint
if _, err := tempClient.GetQuarantineStatus(ctx, "spam"); err != nil {
warnings = append(warnings, "Quarantine endpoint unavailable")
log.Warn().Err(err).Msg("PMG connection test: quarantine check failed")
}
message := "Connected to PMG instance"
if versionLabel != "" {
message = fmt.Sprintf("Connected to PMG instance (version %s)", versionLabel)
}
if len(warnings) > 0 {
message += " (some metrics may be unavailable - check logs for details)"
}
response := map[string]interface{}{
"status": "success",
@ -1711,6 +1735,10 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
}
}
if len(warnings) > 0 {
response["warnings"] = warnings
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}