chore: UI improvements and branding updates

Frontend Updates:
- Add AI Intelligence page routing in App.tsx
- Add PulsePatrolLogo brand component
- Update Settings with Pulse Assistant branding
- Update FeaturesStep wizard text
- Minor fixes across Dashboard, Backups, Storage, Kubernetes components

Backend:
- Update license feature handling
- Improve model converters

Tooling:
- Add pulse-check diagnostic script
This commit is contained in:
rcourtman
2026-01-24 22:44:45 +00:00
parent d1ab2c913e
commit 0844f2b976
22 changed files with 164 additions and 74 deletions

View File

@@ -46,6 +46,7 @@ import SettingsIcon from 'lucide-solid/icons/settings';
import NetworkIcon from 'lucide-solid/icons/network';
import Maximize2Icon from 'lucide-solid/icons/maximize-2';
import Minimize2Icon from 'lucide-solid/icons/minimize-2';
import { PulsePatrolLogo } from '@/components/Brand/PulsePatrolLogo';
import { TokenRevealDialog } from './components/TokenRevealDialog';
import { useAlertsActivation } from './stores/alertsActivation';
import { UpdateProgressModal } from './components/UpdateProgressModal';
@@ -53,6 +54,7 @@ import type { UpdateStatus } from './api/updates';
import { AIChat } from './components/AI/Chat';
import { AIStatusIndicator } from './components/AI/AIStatusIndicator';
import { aiChatStore } from './stores/aiChat';
import { getPatrolStatus } from './api/patrol';
import { useResourcesAsLegacy } from './hooks/useResources';
import { updateSystemSettingsFromResponse, markSystemSettingsLoadedWithDefaults } from './stores/systemSettings';
import { initKioskMode, isKioskMode, setKioskMode, subscribeToKioskMode } from './utils/url';
@@ -83,6 +85,9 @@ const HostsOverview = lazy(() =>
default: module.HostsOverview,
})),
);
const AIIntelligencePage = lazy(() =>
import('./pages/AIIntelligence').then((module) => ({ default: module.AIIntelligence })),
);
// Enhanced store type with proper typing
@@ -962,6 +967,7 @@ function App() {
<Route path="/servers" component={() => <Navigate href="/hosts" />} />
<Route path="/alerts/*" component={AlertsPage} />
<Route path="/ai/*" component={AIIntelligencePage} />
<Route path="/settings/*" component={SettingsRoute} />
</Router>
);
@@ -1037,6 +1043,17 @@ function AppLayout(props: {
const navigate = useNavigate();
const location = useLocation();
// Track patrol license status for Pro badge
const [patrolLicenseRequired, setPatrolLicenseRequired] = createSignal(false);
onMount(async () => {
try {
const status = await getPatrolStatus();
setPatrolLicenseRequired(status.license_required ?? false);
} catch {
// Ignore errors - default to not showing badge
}
});
const readSeenPlatforms = (): Record<string, boolean> => {
if (typeof window === 'undefined') return {};
try {
@@ -1101,6 +1118,7 @@ function AppLayout(props: {
if (path.startsWith('/hosts')) return 'hosts';
if (path.startsWith('/servers')) return 'hosts'; // Legacy redirect
if (path.startsWith('/alerts')) return 'alerts';
if (path.startsWith('/ai')) return 'ai';
if (path.startsWith('/settings')) return 'settings';
return 'proxmox';
};
@@ -1223,11 +1241,11 @@ function AppLayout(props: {
scopes.includes('*') || scopes.includes('settings:read');
const tabs: Array<{
id: 'alerts' | 'settings';
id: 'alerts' | 'ai' | 'settings';
label: string;
route: string;
tooltip: string;
badge: 'update' | null;
badge: 'update' | 'pro' | null;
count: number | undefined;
breakdown: { warning: number; critical: number } | undefined;
icon: JSX.Element;
@@ -1242,6 +1260,16 @@ function AppLayout(props: {
breakdown,
icon: <BellIcon class="w-4 h-4 shrink-0" />,
},
{
id: 'ai',
label: 'Patrol',
route: '/ai',
tooltip: 'Pulse Patrol monitoring and analysis',
badge: patrolLicenseRequired() ? 'pro' : null,
count: undefined,
breakdown: undefined,
icon: <PulsePatrolLogo class="w-4 h-4 shrink-0" />,
},
];
// Only show settings tab if user has access
@@ -1484,6 +1512,11 @@ function AppLayout(props: {
<span aria-hidden="true" class="block h-2 w-2 rounded-full bg-red-500 animate-pulse"></span>
</span>
</Show>
<Show when={tab.badge === 'pro'}>
<span class="ml-1.5 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/50 rounded">
Pro
</span>
</Show>
</div>
);
}}

View File

@@ -231,7 +231,7 @@ export class AgentProfilesAPI {
if (!response.ok) {
const text = await response.text();
if (response.status === 503) {
throw new Error('AI service is not available. Please check AI settings.');
throw new Error('Pulse Assistant service is not available. Please check Pulse Assistant settings.');
}
throw new Error(text || `Failed to get suggestion: ${response.status}`);
}

View File

@@ -1163,7 +1163,7 @@ const UnifiedBackups: Component = () => {
</svg>
}
title="No backup sources configured"
description="Add a Proxmox VE or PBS node in the Settings tab to start monitoring backups."
description="Install the Pulse agent for extra capabilities (temperature monitoring and Pulse Patrol automation), or add a node via API token in Settings → Proxmox."
actions={
<button
type="button"

View File

@@ -0,0 +1,29 @@
import type { Component } from 'solid-js';
interface PulsePatrolLogoProps {
class?: string;
title?: string;
}
export const PulsePatrolLogo: Component<PulsePatrolLogoProps> = (props) => {
const title = () => props.title ?? 'Pulse Patrol';
return (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-label={title()}
class={props.class}
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<title>{title()}</title>
{/* Shield with check */}
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<path d="m9 12 2 2 4-4" />
</svg>
);
};

View File

@@ -1006,7 +1006,7 @@ export function Dashboard(props: DashboardProps) {
</svg>
}
title="No Proxmox VE nodes configured"
description="Add a Proxmox VE node in the Settings tab to start monitoring your infrastructure."
description="Install the Pulse agent for extra capabilities (temperature monitoring and Pulse Patrol automation), or add a node via API token in Settings → Proxmox."
actions={
<button
type="button"

View File

@@ -135,7 +135,7 @@ Start with the most critical problems first.`;
hover:shadow-lg hover:shadow-purple-500/30
transition-all duration-150 active:scale-95
ring-1 ring-purple-400/50"
title={`Ask AI to help investigate and resolve ${props.problemGuests.length} problem${props.problemGuests.length !== 1 ? 's' : ''}`}
title={`Ask Pulse Assistant to help investigate and resolve ${props.problemGuests.length} problem${props.problemGuests.length !== 1 ? 's' : ''}`}
>
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
@@ -144,7 +144,7 @@ Start with the most critical problems first.`;
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
<span>Investigate {props.problemGuests.length} with AI</span>
<span>Investigate {props.problemGuests.length} with Pulse Assistant</span>
<svg class="w-3 h-3 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>

View File

@@ -279,11 +279,11 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
return;
}
if (!aiConfigured()) {
setAnalysisError('AI is not configured. Configure it in Settings -> AI.');
setAnalysisError('Pulse Assistant is not configured. Configure it in Settings → Pulse Assistant.');
return;
}
if (!kubernetesAiEnabled()) {
setAnalysisError('Pulse Pro is required for Kubernetes AI analysis.');
setAnalysisError('Pulse Pro is required for Kubernetes analysis.');
return;
}
@@ -560,12 +560,12 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
<div class="flex-1 min-w-[300px]">
<div class="flex items-center gap-2">
<div class="text-sm font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
Kubernetes AI Analysis
Kubernetes Analysis
{/* Badge removed - feature soft-locked instead */}
</div>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 leading-relaxed">
Generate deep health insights and actionable remediation for your clusters using Pulse's advanced AI engine.
Generate deep health insights and actionable remediation for your clusters using Pulse's advanced analysis engine.
</div>
</div>
<Show when={!licenseLoading() && !kubernetesAiEnabled()}>
@@ -584,7 +584,7 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
<Show when={licenseLoading() || aiLoading()}>
<div class="flex items-center gap-3 p-4 bg-blue-50/50 dark:bg-blue-900/20 rounded-xl border border-blue-100 dark:border-blue-800 animate-pulse">
<div class="h-4 w-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span class="text-xs font-medium text-blue-700 dark:text-blue-300">Synchronizing AI & License...</span>
<span class="text-xs font-medium text-blue-700 dark:text-blue-300">Synchronizing Pulse Assistant & License...</span>
</div>
</Show>
@@ -599,7 +599,7 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
</div>
<h4 class="text-lg font-bold text-gray-900 dark:text-white mb-2">Power up your Kubernetes Fleet</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
Pulse Pro brings advanced AI-driven diagnostics to your Kubernetes clusters. Identify bottlenecks, security risks, and configuration drift in seconds.
Pulse Pro brings advanced diagnostics to your Kubernetes clusters. Identify bottlenecks, security risks, and configuration drift in seconds.
</p>
<a
href={upgradeUrl()}
@@ -607,7 +607,7 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
rel="noreferrer"
class="inline-flex items-center gap-2.5 px-6 py-2.5 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transform hover:scale-105 active:scale-95 transition-all shadow-lg font-bold text-sm"
>
Unlock Kubernetes AI
Unlock Kubernetes Insights
<ExternalLink class="w-4 h-4" />
</a>
</div>
@@ -646,7 +646,7 @@ export const KubernetesClusters: Component<KubernetesClustersProps> = (props) =>
<Show when={!aiLoading() && !aiConfigured()}>
<div class="text-xs text-amber-600 dark:text-amber-400">
AI is not configured. Configure it in Settings → AI.
Pulse Assistant is not configured. Configure it in Settings → Pulse Assistant.
</div>
</Show>

View File

@@ -404,7 +404,7 @@ export const AISettings: Component = () => {
}
} catch (error) {
logger.error('[AISettings] Failed to load settings:', error);
notificationStore.error('Failed to load AI settings');
notificationStore.error('Failed to load Pulse Assistant settings');
setSettings(null);
resetForm(null);
} finally {
@@ -587,12 +587,12 @@ export const AISettings: Component = () => {
const updated = await AIAPI.updateSettings(payload);
setSettings(updated);
resetForm(updated);
notificationStore.success('AI settings saved');
notificationStore.success('Pulse Assistant settings saved');
// Notify other components (like AIChat) that settings changed so they can refresh models
aiChatStore.notifySettingsChanged();
} catch (error) {
logger.error('[AISettings] Failed to save settings:', error);
const message = error instanceof Error ? error.message : 'Failed to save AI settings';
const message = error instanceof Error ? error.message : 'Failed to save Pulse Assistant settings';
notificationStore.error(message);
} finally {
setSaving(false);
@@ -650,7 +650,7 @@ export const AISettings: Component = () => {
let confirmMessage = `Clear ${PROVIDER_DISPLAY_NAMES[provider] || provider} credentials?`;
if (isLastProvider) {
confirmMessage = `⚠️ This is your only configured provider! Clearing it will disable AI until you configure another provider. Continue?`;
confirmMessage = `⚠️ This is your only configured provider! Clearing it will disable Pulse Assistant until you configure another provider. Continue?`;
} else if (modelUsesProvider) {
confirmMessage = `Your current model uses ${PROVIDER_DISPLAY_NAMES[provider] || provider}. Clearing this will require selecting a different model. Continue?`;
} else {
@@ -726,7 +726,7 @@ export const AISettings: Component = () => {
</div>
<SectionHeader
title="Pulse Assistant"
description="Configure AI-powered infrastructure analysis"
description="Configure Pulse Assistant and Patrol analysis"
size="sm"
class="flex-1"
/>
@@ -757,7 +757,7 @@ export const AISettings: Component = () => {
// Revert on failure
setForm('enabled', !newValue);
logger.error('[AISettings] Failed to toggle AI:', error);
const message = error instanceof Error ? error.message : 'Failed to update AI setting';
const message = error instanceof Error ? error.message : 'Failed to update Pulse Assistant setting';
notificationStore.error(message);
}
}}
@@ -778,7 +778,7 @@ export const AISettings: Component = () => {
<Show when={loading()}>
<div class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-300">
<span class="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
Loading AI settings...
Loading Pulse Assistant settings...
</div>
</Show>
@@ -966,17 +966,17 @@ export const AISettings: Component = () => {
</Show>
</div>
{/* AI Provider Configuration - Configure API keys for all providers */}
{/* Provider Configuration - Configure API keys for all providers */}
<div class={`${formField} p-5 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/40`}>
<div class="mb-3">
<h4 class="font-medium text-gray-900 dark:text-white flex items-center gap-2">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
AI Provider Configuration
Provider Configuration
</h4>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Configure API keys for each AI provider you want to use. Models from all configured providers will appear in the model selectors.
Configure API keys for each provider you want to use. Models from all configured providers will appear in the model selectors.
</p>
</div>
@@ -1469,7 +1469,7 @@ export const AISettings: Component = () => {
</Show>
<Show when={!autoFixLocked() && !form.patrolAutoFix && !autoFixAcknowledged()}>
<p class="text-[10px] text-amber-600 dark:text-amber-400 mt-1">
AI will execute fixes without approval. Enable with caution.
Pulse Patrol will execute fixes without approval. Enable with caution.
</p>
</Show>
<Show when={!autoFixLocked() && form.patrolAutoFix}>
@@ -1477,7 +1477,7 @@ export const AISettings: Component = () => {
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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" />
</svg>
Auto-Fix is ON. AI will attempt automatic remediation.
Auto-Fix is ON. Pulse Patrol will attempt automatic remediation.
</p>
</Show>
</div>
@@ -1524,7 +1524,7 @@ export const AISettings: Component = () => {
</Show>
</div>
{/* AI Cost Controls - Compact */}
{/* Usage Cost Controls - Compact */}
<div class="flex items-center gap-3 p-3 rounded-lg border border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20">
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
@@ -1610,19 +1610,19 @@ export const AISettings: Component = () => {
class="flex-1 px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
disabled={saving()}
>
<option value="read_only">Read Only - AI can only observe</option>
<option value="controlled">Controlled - AI executes with your approval</option>
<option value="autonomous">Autonomous - AI executes without approval (Pro)</option>
<option value="read_only">Read Only - Pulse Assistant can only observe</option>
<option value="controlled">Controlled - Pulse Assistant executes with your approval</option>
<option value="autonomous">Autonomous - Pulse Assistant executes without approval (Pro)</option>
</select>
</div>
<p class="text-[10px] text-gray-500 dark:text-gray-400 ml-[7.5rem]">
{form.controlLevel === 'read_only' && '🔒 AI can only query and observe - no commands or control actions'}
{form.controlLevel === 'controlled' && '✅ AI can execute commands and control VMs/containers with your approval'}
{form.controlLevel === 'autonomous' && '⚠️ AI executes all commands and control actions without asking'}
{form.controlLevel === 'read_only' && '🔒 Pulse Assistant can only query and observe - no commands or control actions'}
{form.controlLevel === 'controlled' && '✅ Pulse Assistant can execute commands and control VMs/containers with your approval'}
{form.controlLevel === 'autonomous' && '⚠️ Pulse Assistant executes all commands and control actions without asking'}
</p>
<Show when={form.controlLevel === 'autonomous'}>
<div class="p-2 bg-amber-100/50 dark:bg-amber-900/30 rounded border border-amber-200 dark:border-amber-800 text-[10px] text-amber-800 dark:text-amber-200">
<strong>Legal Disclaimer:</strong> AI models can hallucinate. You are responsible for any damage caused by autonomous actions. See <a href="https://github.com/rcourtman/Pulse/blob/main/TERMS.md" target="_blank" class="underline">Terms of Service</a>.
<strong>Legal Disclaimer:</strong> Model-driven systems can hallucinate. You are responsible for any damage caused by autonomous actions. See <a href="https://github.com/rcourtman/Pulse/blob/main/TERMS.md" target="_blank" class="underline">Terms of Service</a>.
</div>
</Show>
<Show when={form.controlLevel === 'autonomous' && autoFixLocked()}>
@@ -1653,7 +1653,7 @@ export const AISettings: Component = () => {
disabled={saving()}
/>
<p class="text-[10px] text-gray-500 dark:text-gray-400 mt-1">
Comma-separated VMIDs or names that AI cannot control
Comma-separated VMIDs or names that Pulse Assistant cannot control
</p>
</div>
</div>
@@ -1686,7 +1686,7 @@ export const AISettings: Component = () => {
<Show when={showChatMaintenance()}>
<div class="px-3 py-3 bg-white dark:bg-gray-800/50 border-t border-gray-200 dark:border-gray-700 space-y-3">
<p class="text-xs text-gray-500 dark:text-gray-400">
Use this panel to summarize, inspect, or revert a specific chat session. It does not change your default AI settings.
Use this panel to summarize, inspect, or revert a specific chat session. It does not change your default Pulse Assistant settings.
</p>
<div class="flex items-center justify-between">
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">Session</label>
@@ -1790,7 +1790,7 @@ export const AISettings: Component = () => {
<span class="text-xs font-medium">
{settings()?.configured
? `Ready • ${settings()?.configured_providers?.length || 0} provider${(settings()?.configured_providers?.length || 0) !== 1 ? 's' : ''}${availableModels().length} models`
: 'Configure at least one AI provider above to enable AI features'}
: 'Configure at least one provider above to enable Pulse Assistant features'}
</span>
<Show when={settings()?.configured && settings()?.model}>
<span class="text-xs opacity-75 ml-2">
@@ -2089,7 +2089,7 @@ export const AISettings: Component = () => {
disabled={setupSaving() || (setupProvider() !== 'ollama' && !setupApiKey().trim()) || (setupProvider() === 'ollama' && !setupOllamaUrl().trim())}
>
{setupSaving() && <span class="h-4 w-4 border-2 border-white border-t-transparent rounded-full animate-spin" />}
Enable AI
Enable Pulse Assistant
</button>
</div>
</div>

View File

@@ -684,7 +684,7 @@ export const DiagnosticsPanel: Component = () => {
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Pulse Assistant</h4>
<p class="text-xs text-gray-500 dark:text-gray-400">Pulse AI Service</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Pulse Assistant Service</p>
</div>
<div class="ml-auto">
<StatusBadge

View File

@@ -724,7 +724,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
<li>Creates monitoring user and API token automatically</li>
<li>Registers the node with Pulse</li>
<li>Enables temperature monitoring (no SSH required)</li>
<li>Enables AI features for managing VMs/containers</li>
<li>Enables Pulse Patrol automation for managing VMs/containers</li>
</ul>
<p class="text-blue-800 dark:text-blue-200 font-medium">
Run this command on your Proxmox VE node:
@@ -797,7 +797,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
<Show when={formData().setupMode === 'auto'}>
<div class="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 mb-3 dark:border-amber-700 dark:bg-amber-900/20">
<p class="text-xs text-amber-800 dark:text-amber-200">
<strong>Limited functionality:</strong> API-only mode does not include temperature monitoring or AI features.
<strong>Limited functionality:</strong> API-only mode does not include temperature monitoring or Pulse Patrol automation.
For full functionality, use the Agent Install tab instead.
</p>
</div>
@@ -1307,7 +1307,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
<ul class="text-xs text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
<li>One-command setup (creates API user and token automatically)</li>
<li>Built-in temperature monitoring (no SSH required)</li>
<li>Pulse features (execute commands via Pulse AI)</li>
<li>Pulse features (execute commands via Pulse Assistant)</li>
<li>Automatic reconnection on network issues</li>
</ul>
<p class="text-blue-800 dark:text-blue-200 text-xs mt-3">

View File

@@ -19,8 +19,8 @@ const TIER_LABELS: Record<string, string> = {
const FEATURE_LABELS: Record<string, string> = {
ai_patrol: 'Pulse Patrol',
ai_alerts: 'Pulse Alert Analysis',
ai_autofix: 'AI Auto-Fix',
kubernetes_ai: 'Kubernetes AI',
ai_autofix: 'Patrol Auto-Fix',
kubernetes_ai: 'Kubernetes Insights',
update_alerts: 'Update Alerts',
multi_user: 'Multi-user / RBAC',
white_label: 'White-label Branding',
@@ -153,7 +153,7 @@ export const ProLicensePanel: Component = () => {
<div class="space-y-6">
<SettingsPanel
title="Pulse Pro License"
description="Activate your Pulse Pro license to unlock AI automation features."
description="Activate your Pulse Pro license to unlock Pulse Patrol automation features."
action={
<button
class="inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-60"

View File

@@ -303,7 +303,7 @@ const SETTINGS_HEADER_META: Record<SettingsTab, { title: string; description: st
},
'system-ai': {
title: 'Pulse Assistant',
description: 'Configure AI-powered infrastructure analysis and remediation suggestions.',
description: 'Configure Pulse Assistant and Patrol analysis and remediation suggestions.',
},
'system-pro': {
title: 'Pulse Pro',
@@ -2440,14 +2440,17 @@ const Settings: Component<SettingsProps> = (props) => {
</svg>
<div class="flex-1">
<p class="text-sm text-blue-800 dark:text-blue-200">
<strong>Recommended:</strong> Install the Pulse agent on your Proxmox nodes for automatic setup, temperature monitoring, and AI features.
<strong>Recommended:</strong> Install the Pulse agent on your Proxmox nodes for extra capabilities like temperature monitoring and Pulse Patrol automation (it also auto-creates the API token and links the node).
</p>
<p class="mt-1 text-xs text-blue-700 dark:text-blue-300">
Prefer not to run an agent on PVE? Use the manual API token setup below.
</p>
<button
type="button"
onClick={() => navigate('/settings/agents')}
class="mt-2 text-sm font-medium text-blue-700 hover:text-blue-800 dark:text-blue-300 dark:hover:text-blue-200 underline"
>
Go to Agents tab
Install agent
</button>
</div>
</div>

View File

@@ -247,10 +247,10 @@ export const SuggestProfileModal: Component<SuggestProfileModalProps> = (props)
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
AI Profile Suggestion
Pulse Assistant Profile Suggestion
</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">
Describe what you need, and AI will draft a profile
Describe what you need, and Pulse Assistant will draft a profile
</p>
</div>
</div>

View File

@@ -1,6 +1,8 @@
import { Component, createSignal, Show, For, onMount, createEffect, createMemo } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { useWebSocket } from '@/App';
import { Card } from '@/components/shared/Card';
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
import { formatRelativeTime, formatAbsoluteTime } from '@/utils/format';
import { MonitoringAPI } from '@/api/monitoring';
import { AgentProfilesAPI, type AgentProfile, type AgentProfileAssignment } from '@/api/agentProfiles';
@@ -144,6 +146,7 @@ const buildCommandsByPlatform = (url: string): Record<
export const UnifiedAgents: Component = () => {
const { state } = useWebSocket();
const navigate = useNavigate();
let hasLoggedSecurityStatusError = false;
@@ -476,7 +479,7 @@ export const UnifiedAgents: Component = () => {
profile.description?.toLowerCase().includes('pulse ai') ||
name.toLowerCase().startsWith('ai scope');
return isAIManaged
? { label: 'AI-managed', detail: name, category: 'ai-managed' as const }
? { label: 'Patrol-managed', detail: name, category: 'ai-managed' as const }
: { label: name, detail: 'Assigned profile', category: 'profile' as const };
};
@@ -756,7 +759,7 @@ export const UnifiedAgents: Component = () => {
try {
await MonitoringAPI.updateHostAgentConfig(hostId, { commandsEnabled: enabled });
notificationStore.success(`AI command execution ${enabled ? 'enabled' : 'disabled'}. Syncing with agent...`);
notificationStore.success(`Pulse command execution ${enabled ? 'enabled' : 'disabled'}. Syncing with agent...`);
} catch (err) {
// On error, clear the pending state so toggle reverts
setPendingCommandConfig(prev => {
@@ -816,6 +819,24 @@ export const UnifiedAgents: Component = () => {
</p>
</div>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-100">
<div class="flex items-start gap-3">
<ProxmoxIcon class="w-5 h-5 text-orange-500 mt-0.5 shrink-0" />
<div class="flex-1">
<p class="text-sm">
Proxmox nodes can be added here with the unified agent for extra capabilities like temperature monitoring and Pulse Patrol automation (auto-creates the API token and links the node).
</p>
<button
type="button"
onClick={() => navigate('/settings/proxmox')}
class="mt-2 text-xs font-medium text-emerald-800 hover:text-emerald-900 dark:text-emerald-200 dark:hover:text-emerald-100 underline"
>
Prefer API-only? Use manual setup
</button>
</div>
</div>
</div>
<div class="space-y-5">
<Show when={requiresToken()}>
<div class="space-y-3">
@@ -962,11 +983,11 @@ export const UnifiedAgents: Component = () => {
onChange={(e) => setEnableCommands(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Enable Pulse command execution (for AI auto-fix)
Enable Pulse command execution (for Patrol auto-fix)
</label>
<Show when={enableCommands()}>
<div class="rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-sm text-blue-800 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-200">
<span class="font-medium">Pulse commands enabled</span> The agent will accept diagnostic and fix commands from Pulse AI features.
<span class="font-medium">Pulse commands enabled</span> The agent will accept diagnostic and fix commands from Pulse Patrol features.
</div>
</Show>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm text-emerald-900 dark:border-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-100">
@@ -1249,7 +1270,7 @@ export const UnifiedAgents: Component = () => {
<option value="all">All scopes</option>
<option value="default">Default</option>
<option value="profile">Profile assigned</option>
<option value="ai-managed">AI-managed</option>
<option value="ai-managed">Patrol-managed</option>
</select>
</div>
<div class="min-w-[220px] flex-1 space-y-1">
@@ -1446,8 +1467,8 @@ export const UnifiedAgents: Component = () => {
title={isPending
? 'Syncing with agent...'
: effectiveEnabled
? 'AI command execution enabled'
: 'AI command execution disabled'
? 'Pulse command execution enabled'
: 'Pulse command execution disabled'
}
>
<span

View File

@@ -18,9 +18,9 @@ export const FeaturesStep: Component<FeaturesStepProps> = (props) => {
const features = [
{
id: 'ai',
name: 'Pulse AI',
name: 'Pulse Assistant',
icon: '🤖',
desc: 'Intelligent monitoring assistant with auto-fix capabilities',
desc: 'Guided troubleshooting with Patrol automation and auto-fix capabilities',
enabled: aiEnabled,
setEnabled: setAiEnabled,
badge: 'New in 5.0',
@@ -97,12 +97,12 @@ export const FeaturesStep: Component<FeaturesStepProps> = (props) => {
</button>
))}
{/* AI info box */}
{/* Assistant info box */}
<div class="bg-gradient-to-r from-purple-500/10 to-blue-500/10 border border-purple-400/20 rounded-xl p-4">
<div class="flex items-start gap-3">
<div class="text-2xl"></div>
<div>
<p class="text-white font-medium">Pulse AI Features</p>
<p class="text-white font-medium">Pulse Assistant & Patrol Features</p>
<p class="text-white/60 text-sm mt-1">
Chat assistant for infrastructure questions<br />
Patrol mode for proactive monitoring<br />
@@ -110,7 +110,7 @@ export const FeaturesStep: Component<FeaturesStepProps> = (props) => {
Predictive failure detection
</p>
<p class="text-white/40 text-xs mt-2">
Requires API key configuration in Settings AI after setup
Requires API key configuration in Settings Pulse Assistant after setup
</p>
</div>
</div>

View File

@@ -754,7 +754,7 @@ const Storage: Component = () => {
</svg>
}
title="No storage configured"
description="Add a Proxmox VE or PBS node in the Settings tab to start monitoring storage."
description="Install the Pulse agent for extra capabilities (temperature monitoring and Pulse Patrol automation), or add a node via API token in Settings → Proxmox."
actions={
<button
type="button"

View File

@@ -28,6 +28,7 @@ export interface UnifiedFinding {
resourceName: string;
resourceType: string;
alertId?: string;
alertType?: string;
isThreshold?: boolean;
category: string;
severity: 'critical' | 'warning' | 'info' | 'watch';

View File

@@ -167,13 +167,13 @@ func GetTierDisplayName(tier Tier) string {
func GetFeatureDisplayName(feature string) string {
switch feature {
case FeatureAIPatrol:
return "AI Patrol (Background Health Checks)"
return "Pulse Patrol (Background Health Checks)"
case FeatureAIAlerts:
return "AI Alert Analysis"
return "Alert Analysis"
case FeatureAIAutoFix:
return "AI Auto-Fix"
return "Pulse Patrol Auto-Fix"
case FeatureKubernetesAI:
return "Kubernetes AI Analysis"
return "Kubernetes Analysis"
case FeatureUpdateAlerts:
return "Update Alerts (Container/Package Updates)"
case FeatureRBAC:

View File

@@ -62,7 +62,7 @@ func TestLicenseHasFeature(t *testing.T) {
// Should have tier features
if !license.HasFeature(FeatureAIPatrol) {
t.Error("Pro license should have AI Patrol")
t.Error("Pro license should have Pulse Patrol")
}
// Should have explicit features
@@ -211,7 +211,7 @@ func TestServiceFeatureGating(t *testing.T) {
// Should now have Pro features
if !service.HasFeature(FeatureAIPatrol) {
t.Error("Should have AI Patrol with Pro license")
t.Error("Should have Pulse Patrol with Pro license")
}
if !service.IsValid() {
t.Error("Should be valid with active license")
@@ -356,8 +356,8 @@ func TestGetTierDisplayName(t *testing.T) {
}
func TestGetFeatureDisplayName(t *testing.T) {
if GetFeatureDisplayName(FeatureAIPatrol) != "AI Patrol (Background Health Checks)" {
t.Error("Wrong display name for AI Patrol")
if GetFeatureDisplayName(FeatureAIPatrol) != "Pulse Patrol (Background Health Checks)" {
t.Error("Wrong display name for Pulse Patrol")
}
}

View File

@@ -1,6 +1,7 @@
package models
import (
"encoding/json"
"strings"
"time"
)
@@ -948,7 +949,7 @@ type ResourceConvertInput struct {
LastSeenUnix int64
Alerts []ResourceAlertInput
Identity *ResourceIdentityInput
PlatformData map[string]any
PlatformData json.RawMessage
}
// ResourceMetricInput represents a metric value for resource conversion.

View File

@@ -1,5 +1,7 @@
package models
import "encoding/json"
// Frontend-friendly type aliases with proper JSON tags
// These extend the base types with additional computed fields
@@ -612,7 +614,7 @@ type ResourceFrontend struct {
Identity *ResourceIdentityFrontend `json:"identity,omitempty"`
// Platform-specific data (JSON blob)
PlatformData map[string]any `json:"platformData,omitempty"`
PlatformData json.RawMessage `json:"platformData,omitempty"`
}
// ResourceMetricFrontend represents a metric value for the frontend.

BIN
pulse-check Executable file

Binary file not shown.