feat: improve screenshots and mobile UI responsiveness

- Update screenshot tool to use MacBook Air resolution (2560x1600)
- Remove empty side borders from screenshots
- Use mock data for all screenshots for privacy
- Fix mobile alert buttons overflowing viewport
- Exempt localhost from API rate limiting for better dev experience
- Update documentation to showcase all features with screenshots
- Reorganize README visual tour into feature sections
This commit is contained in:
Pulse Monitor
2025-08-30 12:25:53 +00:00
parent 88cf835d75
commit 0a86333374
13 changed files with 133 additions and 85 deletions
+20 -4
View File
@@ -539,10 +539,26 @@ make dev
See Pulse in action with our [complete screenshot gallery →](docs/SCREENSHOTS.md)
| Dashboard | Alert History | Mobile View |
|-----------|---------------|-------------|
| ![Dashboard](docs/images/01-dashboard.png) | ![Alerts](docs/images/05-alert-history.png) | ![Mobile](docs/images/08-mobile.png) |
| *Monitor all nodes, VMs, and containers* | *Track and analyze alert patterns* | *Full mobile responsiveness* |
### Core Features
| Dashboard | Storage | Backups |
|-----------|---------|---------|
| ![Dashboard](docs/images/01-dashboard.png) | ![Storage](docs/images/02-storage.png) | ![Backups](docs/images/03-backups.png) |
| *Real-time monitoring of nodes, VMs & containers* | *Storage pool usage across all nodes* | *Unified backup management & PBS integration* |
### Alerts & Configuration
| Alert Configuration | Alert History | Settings |
|---------------------|---------------|----------|
| ![Alerts](docs/images/04-alerts.png) | ![Alert History](docs/images/05-alert-history.png) | ![Settings](docs/images/06-settings.png) |
| *Configure thresholds & notifications* | *Track patterns with visual timeline* | *Manage nodes & authentication* |
### Mobile Experience
| Mobile Dashboard |
|------------------|
| ![Mobile](docs/images/08-mobile.png) |
| *Fully responsive interface for monitoring on the go* |
## Links
+5 -9
View File
@@ -1,8 +1,8 @@
# Pulse Screenshots
## Dashboard Overview
## Dashboard Overview (Dark Mode)
![Dashboard Overview](images/01-dashboard.png)
*Real-time monitoring dashboard showing 7 Proxmox nodes with 35 VMs and 56 containers. Color-coded resource usage (CPU, RAM, storage) with quick status indicators for running/stopped guests. Automatic layout adapts to cluster size - compact cards for 5-9 nodes.*
*Real-time monitoring dashboard showing 7 Proxmox nodes with 35 VMs and 56 containers. Color-coded resource usage (CPU, RAM, storage) with quick status indicators for running/stopped guests. Automatic layout adapts to cluster size - compact cards for 5-9 nodes. Professional dark theme optimized for 24/7 monitoring setups.*
## Storage Management
![Storage Management](images/02-storage.png)
@@ -12,9 +12,9 @@
![Unified Backup View](images/03-backups.png)
*Centralized backup management showing PBS backups, PVE backup tasks, and VM snapshots in one place. Track backup status, sizes, retention, and quickly identify failed or missing backups across your entire infrastructure.*
## Alert Configuration
![Alert Configuration](images/04-alerts.png)
*Configure monitoring thresholds and notification settings. Set global defaults or per-resource overrides for CPU, memory, and disk alerts. Supports quiet hours, cooldown periods, and alert grouping to prevent notification spam.*
## Alerts & Configuration
![Alerts and Configuration](images/04-alerts.png)
*Unified alerts view showing active alerts and configuration settings. Monitor current system alerts with severity indicators, affected resources, and acknowledgment status. Configure thresholds, notification settings, quiet hours, and alert grouping. Bulk actions allow managing multiple alerts simultaneously.*
## Alert History & Analytics
![Alert History](images/05-alert-history.png)
@@ -24,10 +24,6 @@
![Node Configuration](images/06-settings.png)
*Manage Proxmox nodes and PBS instances through the UI. Add/remove nodes, configure credentials securely (encrypted at rest), set polling intervals, and manage authentication settings. Quick health check shows connection status for all nodes.*
## Dark Mode Theme
![Dark Theme](images/07-dark-mode.png)
*Professional dark theme optimized for 24/7 monitoring setups. High contrast design reduces eye strain during extended viewing sessions while maintaining excellent readability of metrics and status indicators.*
## Mobile Responsive Design
![Mobile View](images/08-mobile.png)
*Fully responsive mobile interface for monitoring on the go. Touch-optimized controls, collapsible navigation, and adaptive layouts ensure full functionality on smartphones and tablets without compromising usability.*
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 827 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 112 KiB

+55 -51
View File
@@ -897,29 +897,31 @@ function OverviewTab(props: {
{/* Recent Alerts */}
<div>
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Active Alerts</h3>
<Show when={Object.keys(props.activeAlerts).length > 0}>
<div class="flex items-center gap-2">
<Show when={selectedAlerts().size > 0}>
<span class="text-xs text-gray-600 dark:text-gray-400">
{selectedAlerts().size} selected
</span>
<button
class="px-3 py-1 text-xs bg-yellow-600 text-white rounded hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={processingBulk()}
onClick={bulkAcknowledge}
>
{processingBulk() ? 'Processing...' : 'Acknowledge Selected'}
</button>
<button
class="px-3 py-1 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={processingBulk()}
onClick={bulkClear}
>
{processingBulk() ? 'Processing...' : 'Clear Selected'}
</button>
</Show>
<div class="flex flex-col gap-2 mb-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Active Alerts</h3>
<Show when={Object.keys(props.activeAlerts).length > 0 && selectedAlerts().size > 0}>
<span class="text-xs text-gray-600 dark:text-gray-400">
{selectedAlerts().size} selected
</span>
</Show>
</div>
<Show when={Object.keys(props.activeAlerts).length > 0 && selectedAlerts().size > 0}>
<div class="flex gap-1 justify-start sm:justify-end max-w-full">
<button
class="flex-shrink px-2 py-1 text-xs bg-yellow-600 text-white rounded hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={processingBulk()}
onClick={bulkAcknowledge}
>
Ack
</button>
<button
class="flex-shrink px-2 py-1 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={processingBulk()}
onClick={bulkClear}
>
Clear
</button>
</div>
</Show>
</div>
@@ -954,37 +956,39 @@ function OverviewTab(props: {
} ${
selectedAlerts().has(alert.id) ? 'ring-2 ring-blue-500' : ''
}`}>
<div class="flex items-start">
<input
type="checkbox"
class="mt-1 mr-3 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700"
checked={selectedAlerts().has(alert.id)}
onChange={() => toggleAlertSelection(alert.id)}
/>
<div class="flex-1">
<div class="flex items-center gap-2">
<span class={`text-sm font-medium ${
alert.level === 'critical' ? 'text-red-700 dark:text-red-400' : 'text-yellow-700 dark:text-yellow-400'
}`}>
{alert.resourceName}
</span>
<span class="text-xs text-gray-600 dark:text-gray-400">
({alert.type})
</span>
<Show when={alert.acknowledged}>
<span class="px-2 py-0.5 text-xs bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 rounded">
Acknowledged
<div class="flex flex-col sm:flex-row sm:items-start">
<div class="flex items-start flex-1">
<input
type="checkbox"
class="mt-1 mr-3 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700"
checked={selectedAlerts().has(alert.id)}
onChange={() => toggleAlertSelection(alert.id)}
/>
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span class={`text-sm font-medium truncate ${
alert.level === 'critical' ? 'text-red-700 dark:text-red-400' : 'text-yellow-700 dark:text-yellow-400'
}`}>
{alert.resourceName}
</span>
</Show>
<span class="text-xs text-gray-600 dark:text-gray-400">
({alert.type})
</span>
<Show when={alert.acknowledged}>
<span class="px-2 py-0.5 text-xs bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 rounded">
Acknowledged
</span>
</Show>
</div>
<p class="text-sm text-gray-700 dark:text-gray-300 mt-1 break-words">
{alert.message}
</p>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Started: {new Date(alert.startTime).toLocaleString()}
</p>
</div>
<p class="text-sm text-gray-700 dark:text-gray-300 mt-1">
{alert.message}
</p>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Started: {new Date(alert.startTime).toLocaleString()}
</p>
</div>
<div class="flex gap-2 ml-4">
<div class="flex gap-2 mt-3 sm:mt-0 sm:ml-4 self-end sm:self-start">
<Show when={!alert.acknowledged}>
<button
class="px-3 py-1 text-xs bg-yellow-600 text-white rounded hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+9 -3
View File
@@ -127,12 +127,18 @@ func UniversalRateLimitMiddleware(next http.Handler) http.Handler {
return
}
// Get appropriate rate limiter for this endpoint
limiter := GetRateLimiterForEndpoint(r.URL.Path, r.Method)
// Extract client IP
ip := GetClientIP(r)
// Skip rate limiting for localhost/development
if ip == "127.0.0.1" || ip == "::1" || ip == "localhost" {
next.ServeHTTP(w, r)
return
}
// Get appropriate rate limiter for this endpoint
limiter := GetRateLimiterForEndpoint(r.URL.Path, r.Method)
// Check rate limit
if !limiter.Allow(ip) {
// Add retry-after header
+44 -18
View File
@@ -32,13 +32,13 @@ async function main() {
console.log('Starting simple Puppeteer screenshot capture...\n');
// Launch browser with high-quality rendering options
// Launch browser with MacBook Air-like display settings
const browser = await puppeteer.launch({
headless: 'new', // Use new headless mode
defaultViewport: null, // Don't override viewport
args: [
'--window-size=1920,1080',
'--force-device-scale-factor=3', // Force 3x DPI rendering at browser level
'--window-size=1280,800', // MacBook Air effective resolution
'--force-device-scale-factor=2', // Retina display (2x)
'--disable-dev-shm-usage', // Better memory handling
'--enable-font-antialiasing', // Smooth fonts
'--font-render-hinting=none' // Disable hinting for sharper text
@@ -49,10 +49,11 @@ async function main() {
// CRITICAL: Set viewport BEFORE navigating for sharp rendering!
// This must happen before page.goto() or text will be blurry
// MacBook Air 13" display dimensions
await page.setViewport({
width: 1920,
height: 1080,
deviceScaleFactor: 3 // Use 3x for ultra-sharp text (better than 2x)
width: 1280,
height: 800,
deviceScaleFactor: 2 // Retina display
});
try {
@@ -115,7 +116,7 @@ async function main() {
}
}
// Take screenshot
// Take screenshot - MacBook Air resolution should fit content perfectly
await page.screenshot({
path: path.join(OUTPUT_DIR, `${view.file}.png`),
type: 'png'
@@ -124,6 +125,9 @@ async function main() {
console.log(`Saved ${view.file}.png`);
}
// Note: Active alerts are shown on the main Alerts tab (04-alerts.png)
// The tab has Config and History sub-tabs, no separate Active tab
// Take alert history screenshot - need to click on History sub-tab
console.log('Capturing alert history...');
@@ -142,31 +146,53 @@ async function main() {
if (historyTab) historyTab.click();
});
// Wait for history to load naturally from the mock API
await wait(5000);
// Wait for alert history to load with retry logic
console.log('Waiting for alert history to load...');
let retries = 0;
const maxRetries = 15; // 15 seconds max
// Take the screenshot
while (retries < maxRetries) {
await wait(1000);
// Check if data has loaded by looking for alert rows in the DOM
const hasData = await page.evaluate(() => {
// Look for alert history rows or the "No alerts" message
const rows = document.querySelectorAll('tbody tr, [data-testid="alert-row"], .alert-history-row');
const noDataMsg = Array.from(document.querySelectorAll('*')).find(
el => el.textContent && el.textContent.includes('No alerts in selected time range')
);
return rows.length > 0 || noDataMsg;
});
if (hasData) {
console.log('Alert history loaded after', retries + 1, 'seconds');
break;
}
retries++;
}
// Extra wait for any animations
await wait(1000);
// Take the screenshot - MacBook Air resolution
await page.screenshot({
path: path.join(OUTPUT_DIR, '05-alert-history.png'),
type: 'png'
});
console.log('Saved 05-alert-history.png');
// Copy dashboard as dark mode showcase
await fs.copyFile(
path.join(OUTPUT_DIR, '01-dashboard.png'),
path.join(OUTPUT_DIR, '07-dark-mode.png')
);
// Note: Dashboard is already in dark mode, no need for duplicate or light mode
// Mobile view
console.log('\nCapturing mobile view...');
// Set mobile viewport BEFORE reload for sharp rendering
await page.setViewport({
width: 390,
height: 844,
width: 375, // iPhone standard width
height: 667, // iPhone standard height
isMobile: true,
deviceScaleFactor: 3 // 3x for ultra-sharp mobile screenshots
deviceScaleFactor: 2 // 2x for standard retina
});
// Now reload with the new viewport already set