From 8e502dc7d827d80a40acd6ae08f6df33264e96ed Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 28 Sep 2025 18:46:52 +0000 Subject: [PATCH] feat: unify styling and improve cluster detection --- Makefile | 7 +- README.md | 12 +- docs/frontend-style-guide.md | 82 ++ frontend-modern/src/api/notifications.ts | 3 +- .../src/components/Alerts/CustomRulesTab.tsx | 50 +- .../components/Alerts/EmailProviderSelect.tsx | 352 +++---- .../src/components/Alerts/OverrideModal.tsx | 17 +- .../src/components/Alerts/ResourceTable.tsx | 10 +- .../src/components/Alerts/ThresholdsTable.tsx | 19 +- .../src/components/Alerts/WebhookConfig.tsx | 210 ++-- .../src/components/Backups/Backups.tsx | 49 +- .../src/components/Backups/BackupsFilter.tsx | 5 +- .../src/components/Backups/UnifiedBackups.tsx | 116 ++- .../components/Dashboard/CompactNodeCard.tsx | 33 +- .../src/components/Dashboard/Dashboard.tsx | 157 +-- .../components/Dashboard/DashboardFilter.tsx | 5 +- .../Dashboard/DiskHealthSummary.tsx | 17 +- .../src/components/Dashboard/NodeCard.tsx | 25 +- .../src/components/Dashboard/PBSCard.tsx | 14 +- .../src/components/ErrorBoundary.tsx | 24 +- .../src/components/FirstRunSetup.tsx | 28 +- .../src/components/SecurityWarning.tsx | 14 +- .../Settings/BatchCredentialModal.tsx | 38 +- .../Settings/ChangePasswordModal.tsx | 28 +- .../components/Settings/DiscoveryModal.tsx | 7 +- .../components/Settings/GenerateAPIToken.tsx | 40 +- .../src/components/Settings/GuestURLs.tsx | 14 +- .../src/components/Settings/NodeModal.tsx | 182 ++-- .../Settings/QuickSecuritySetup.tsx | 74 +- .../src/components/Settings/Settings.tsx | 283 +++-- .../src/components/Storage/DiskList.tsx | 17 +- .../src/components/Storage/Storage.tsx | 98 +- .../src/components/Storage/StorageFilter.tsx | 5 +- .../src/components/shared/Card.tsx | 47 + .../src/components/shared/EmptyState.tsx | 68 ++ frontend-modern/src/components/shared/Form.ts | 57 + .../components/shared/NodeSummaryTable.tsx | 7 +- .../src/components/shared/SectionHeader.tsx | 48 + .../src/components/shared/SettingsPanel.tsx | 42 + .../src/components/shared/Toggle.tsx | 56 + frontend-modern/src/pages/Alerts.tsx | 883 ++++++++-------- frontend-modern/src/stores/events.ts | 15 +- frontend-modern/src/stores/websocket.ts | 11 + internal/api/config_handlers.go | 975 +++++++++++------- internal/api/frontend_embed.go | 78 +- internal/config/client_helpers.go | 15 +- internal/config/config.go | 119 ++- internal/notifications/notifications.go | 4 + scripts/.go-version | 1 + scripts/dev-hot.sh | 44 + scripts/install-go-toolchain.sh | 49 + 51 files changed, 2724 insertions(+), 1830 deletions(-) create mode 100644 docs/frontend-style-guide.md create mode 100644 frontend-modern/src/components/shared/Card.tsx create mode 100644 frontend-modern/src/components/shared/EmptyState.tsx create mode 100644 frontend-modern/src/components/shared/Form.ts create mode 100644 frontend-modern/src/components/shared/SectionHeader.tsx create mode 100644 frontend-modern/src/components/shared/SettingsPanel.tsx create mode 100644 frontend-modern/src/components/shared/Toggle.tsx create mode 100644 scripts/.go-version create mode 100755 scripts/dev-hot.sh create mode 100755 scripts/install-go-toolchain.sh diff --git a/Makefile b/Makefile index 6b2525056..2b088ab09 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Pulse Makefile for development -.PHONY: build run dev frontend backend all clean +.PHONY: build run dev frontend backend all clean dev-hot # Build everything all: frontend backend @@ -32,6 +32,9 @@ run: build dev: frontend backend sudo systemctl restart pulse-backend +dev-hot: + ./scripts/dev-hot.sh + # Clean build artifacts clean: rm -f pulse @@ -39,4 +42,4 @@ clean: # Quick rebuild and restart for development restart: frontend backend - sudo systemctl restart pulse-backend \ No newline at end of file + sudo systemctl restart pulse-backend diff --git a/README.md b/README.md index 1abad8ff7..537cdfa2e 100644 --- a/README.md +++ b/README.md @@ -504,12 +504,14 @@ journalctl -u pulse -f ### Quick Start - Hot Reload (Recommended) ```bash -# Best development experience with instant frontend updates -./scripts/hot-dev.sh -# Frontend: http://localhost:5173 (hot reload) -# Backend: http://localhost:7655 +# Launch Vite + Go with automatic frontend proxying +make dev-hot +# Frontend HMR: http://127.0.0.1:5173 +# Backend API: http://127.0.0.1:7655 (served via the Go app) ``` +The backend now detects `FRONTEND_DEV_SERVER` and proxies requests straight to the Vite dev server. Edit files under `frontend-modern/src/` and the browser refreshes instantly—no manual rebuilds or service restarts required. Use `CTRL+C` to stop both processes. + ### Production-like Development ```bash # Watches files and rebuilds/embeds frontend into Go binary @@ -565,4 +567,4 @@ See Pulse in action with our [complete screenshot gallery →](docs/SCREENSHOTS. ## License -MIT - See [LICENSE](LICENSE) \ No newline at end of file +MIT - See [LICENSE](LICENSE) diff --git a/docs/frontend-style-guide.md b/docs/frontend-style-guide.md new file mode 100644 index 000000000..ff585105b --- /dev/null +++ b/docs/frontend-style-guide.md @@ -0,0 +1,82 @@ +# Frontend UI Style Guide + +This project now ships a handful of shared primitives to keep typography and form layouts consistent. The snippets below show the preferred usage. + +## Section headers + +Use `SectionHeader` for any inline card titles, modal headings, or sub-section titles instead of ad-hoc `

`/`

` elements. + +```tsx +import { SectionHeader } from '@/components/shared/SectionHeader'; + + +``` + +Pass `titleClass`/`descriptionClass` when you need to tweak color or emphasis without rebuilding the layout. + +## Empty states + +Whenever a panel needs to show a loading, error, or "no data" treatment, render `EmptyState` inside a `Card`. + +```tsx +import { Card } from '@/components/shared/Card'; +import { EmptyState } from '@/components/shared/EmptyState'; + + + } + title="No backups yet" + description="Run your first job or adjust the filters to see activity." + actions={( + + )} + /> + +``` + +Icons and actions are optional; omit them when not needed. + +## Form helpers + +Shared form styles live in `@/components/shared/Form`. Import the helpers and apply them to each field container, label, and control for a uniform look. + +```tsx +import { formField, labelClass, controlClass, formHelpText, formCheckbox } from '@/components/shared/Form'; + +
+ + +

+ Use HTTPS on port 8006 for Proxmox VE and 8007 for PBS. +

+
+ + +``` + +Helper summary: + +- `formField`: wraps a label + control stack. +- `labelClass(extra?)`: base typography for labels, with optional extra classes. +- `controlClass(extra?)`: base input styling; append sizing tweaks (`px-2 py-1.5`) as needed. +- `formHelpText`: small secondary text (validation notes, hints). +- `formCheckbox`: shared checkbox styling for toggles inside copy-heavy forms. + +Stick to these helpers when building new settings panels, modals, or detail cards. If a component needs a variant that the helpers do not cover, extend them in `Form.ts` so the convention remains centralized. diff --git a/frontend-modern/src/api/notifications.ts b/frontend-modern/src/api/notifications.ts index 6e59f0e5d..cb1cc3ea3 100644 --- a/frontend-modern/src/api/notifications.ts +++ b/frontend-modern/src/api/notifications.ts @@ -107,7 +107,8 @@ export class NotificationsAPI { // Webhook management static async getWebhooks(): Promise { - return apiFetchJSON(`${this.baseUrl}/webhooks`); + const data = await apiFetchJSON(`${this.baseUrl}/webhooks`); + return Array.isArray(data) ? data : []; } static async createWebhook(webhook: Omit): Promise { diff --git a/frontend-modern/src/components/Alerts/CustomRulesTab.tsx b/frontend-modern/src/components/Alerts/CustomRulesTab.tsx index 9035fdf56..135223c34 100644 --- a/frontend-modern/src/components/Alerts/CustomRulesTab.tsx +++ b/frontend-modern/src/components/Alerts/CustomRulesTab.tsx @@ -1,5 +1,8 @@ import { For, Show } from 'solid-js'; import type { CustomAlertRule } from '@/types/alerts'; +import { Card } from '@/components/shared/Card'; +import { SectionHeader } from '@/components/shared/SectionHeader'; +import { EmptyState } from '@/components/shared/EmptyState'; interface CustomRulesTabProps { rules: CustomAlertRule[]; @@ -46,13 +49,15 @@ export function CustomRulesTab(props: CustomRulesTabProps) { return (
{/* Header */} -
-

Custom Alert Rules

-

- Custom rules apply specific thresholds to guests matching filter conditions. - Rules are evaluated in priority order (higher number = higher priority). -

-
+ + + {/* Priority Order Explanation */}
@@ -73,21 +78,23 @@ export function CustomRulesTab(props: CustomRulesTabProps) { {/* Rules List */} 0} fallback={ -
- - - -

- No custom alert rules defined yet. Create rules from the Dashboard by applying filters and clicking "Create Alert". -

-
+ + + + + )} + title="No custom alert rules" + description="Create rules from the Dashboard by applying filters and choosing Create Alert." + /> + }>
b.priority - a.priority)}> {(rule) => ( -
-
-
+ +

{rule.name}

@@ -132,7 +139,7 @@ export function CustomRulesTab(props: CustomRulesTabProps) {
-
+
Filters:
@@ -167,12 +174,11 @@ export function CustomRulesTab(props: CustomRulesTabProps) {
-
-
+ )}
); -} \ No newline at end of file +} diff --git a/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx b/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx index a03315599..b32d23501 100644 --- a/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx +++ b/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx @@ -1,5 +1,6 @@ import { createSignal, createEffect, Show, For } from 'solid-js'; import { NotificationsAPI } from '@/api/notifications'; +import { formField, labelClass, controlClass, formHelpText, formCheckbox } from '@/components/shared/Form'; interface EmailProvider { name: string; @@ -14,8 +15,8 @@ interface EmailProvider { interface EmailConfig { enabled: boolean; provider: string; - server: string; // Fixed: use 'server' not 'smtpHost' - port: number; // Fixed: use 'port' not 'smtpPort' + server: string; + port: number; from: string; username: string; password: string; @@ -37,10 +38,10 @@ interface EmailProviderSelectProps { export function EmailProviderSelect(props: EmailProviderSelectProps) { const [providers, setProviders] = createSignal([]); - const [showProviders, setShowProviders] = createSignal(false); const [showAdvanced, setShowAdvanced] = createSignal(false); - - // Load email providers + const [showInstructions, setShowInstructions] = createSignal(false); + + // Load email providers once createEffect(async () => { try { const data = await NotificationsAPI.getEmailProviders(); @@ -49,284 +50,261 @@ export function EmailProviderSelect(props: EmailProviderSelectProps) { console.error('Failed to load email providers:', err); } }); - - const selectProvider = (provider: EmailProvider) => { + + const applyProvider = (provider: EmailProvider | undefined) => { + if (!provider) { + props.onChange({ ...props.config, provider: '' }); + setShowInstructions(false); + return; + } + props.onChange({ ...props.config, provider: provider.name, - server: provider.smtpHost, // Fixed: use 'server' not 'smtpHost' - port: provider.smtpPort, // Fixed: use 'port' not 'smtpPort' + server: provider.smtpHost, + port: provider.smtpPort, tls: provider.tls, startTLS: provider.startTLS, username: provider.name === 'SendGrid' ? 'apikey' : props.config.username, }); - setShowProviders(false); + setShowInstructions(true); }; - - const currentProvider = () => providers().find(p => p.name === props.config.provider); - + + const handleProviderChange = (value: string) => { + if (!value) { + applyProvider(undefined); + return; + } + const provider = providers().find((p) => p.name === value); + applyProvider(provider); + }; + + const currentProvider = () => providers().find((p) => p.name === props.config.provider); + return ( -
- {/* Provider Selection */} -
-
- - + +
- - {/* Basic Configuration */} -
-
- + + +
+ + +
+ {currentProvider()!.instructions} +
+
+
+ +
+ +
+
+ props.onChange({ ...props.config, server: e.currentTarget.value })} placeholder="smtp.example.com" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" + class={controlClass('px-2 py-1.5')} />
- -
- + +
+ props.onChange({ ...props.config, port: parseInt(e.currentTarget.value) || 587 })} placeholder="587" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" + class={controlClass('px-2 py-1.5')} />
- -
- + +
+ props.onChange({ ...props.config, from: e.currentTarget.value })} placeholder="noreply@example.com" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" + class={controlClass('px-2 py-1.5')} />
- -
- + +
+ props.onChange({ ...props.config, replyTo: e.currentTarget.value })} - placeholder="admin@example.com (optional)" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" + placeholder="admin@example.com" + class={controlClass('px-2 py-1.5')} />
- -
- + +
+ props.onChange({ ...props.config, username: e.currentTarget.value })} placeholder={props.config.provider === 'SendGrid' ? 'apikey' : 'username@example.com'} - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" + class={controlClass('px-2 py-1.5')} />
- -
- + +
+ props.onChange({ ...props.config, password: e.currentTarget.value })} placeholder="••••••••" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" + class={controlClass('px-2 py-1.5')} />
- - {/* Recipients */} -
- + +
+