From 99accbffec0c0619fde6d8450e0da2a657c42c56 Mon Sep 17 00:00:00 2001 From: Debasis Ghosh <65386308+debdevops@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:10:50 +0530 Subject: [PATCH] Add new screenshots and update documentation - Added multiple new screenshots to the archive for various features and UI elements. - Created package.json and package-lock.json for Playwright dependency. - Removed outdated DOCUMENTATION_INDEX.md file. - Updated README.md to reflect changes in documentation structure and removed references to the deleted index. --- README.md | 776 ++++++------------ REMOTE_ACCESS.md | 174 ---- docs/PERMISSIONS.md | 86 -- docs/SCREENSHOTS.md | 331 -------- docs/screenshots/01-connect-page.png | Bin 0 -> 504732 bytes docs/screenshots/02-messages-queue-active.png | Bin 0 -> 293348 bytes .../03-message-detail-properties.png | Bin 0 -> 477582 bytes docs/screenshots/04-message-detail-body.png | Bin 0 -> 326990 bytes .../05-message-detail-ai-insights.png | Bin 0 -> 334905 bytes .../06-message-detail-forensic.png | Bin 0 -> 341284 bytes .../screenshots/07-message-detail-headers.png | Bin 0 -> 318103 bytes .../08-messages-deadletter-queue.png | Bin 0 -> 275027 bytes docs/screenshots/09-dlq-message-detail.png | Bin 0 -> 315727 bytes .../10-dlq-message-ai-insights.png | Bin 0 -> 391656 bytes .../11-messages-topic-subscription.png | Bin 0 -> 220101 bytes .../screenshots/12-fab-quick-actions-open.png | Bin 0 -> 355164 bytes docs/screenshots/13-send-message-dialog.png | Bin 0 -> 315376 bytes .../14-generate-messages-dialog.png | Bin 0 -> 428661 bytes docs/screenshots/15-test-dlq-dialog.png | Bin 0 -> 292931 bytes docs/screenshots/16-dlq-history-overview.png | Bin 0 -> 362132 bytes docs/screenshots/17-dlq-history-detail.png | Bin 0 -> 493556 bytes docs/screenshots/18-dlq-history-full.png | Bin 0 -> 493557 bytes docs/screenshots/19-rules-page.png | Bin 0 -> 336394 bytes .../screenshots/20-rules-template-gallery.png | Bin 0 -> 395322 bytes docs/screenshots/21-rules-create-dialog.png | Bin 0 -> 344546 bytes docs/screenshots/22-health-page.png | Bin 0 -> 348321 bytes docs/screenshots/23-help-page.png | Bin 0 -> 367914 bytes docs/screenshots/24-help-page-full.png | Bin 0 -> 367914 bytes docs/screenshots/25-sidebar-navigation.png | Bin 0 -> 71351 bytes docs/screenshots/26-scalar-api-docs.png | Bin 0 -> 191520 bytes docs/screenshots/27-full-app-overview.png | Bin 0 -> 326685 bytes docs/screenshots/28-message-filter.png | Bin 0 -> 329842 bytes .../{ => archive}/01-Start-The-App.png | Bin .../archive/01-connect-page-full.png | Bin 0 -> 425273 bytes docs/screenshots/archive/01-connect-page.png | Bin 0 -> 488655 bytes ...onnect-Service-Bus-With-Manage-ConnStr.png | Bin .../02-connect-page-namespace-connected.png | Bin 0 -> 385763 bytes .../archive/02-messages-overview.png | Bin 0 -> 149306 bytes .../{ => archive}/03-Connected-ServiceBus.png | Bin .../archive/03-messages-page-overview.png | Bin 0 -> 206264 bytes .../archive/03-messages-queue-browser.png | Bin 0 -> 292598 bytes .../04-feature-message-browser-empty.png | Bin .../archive/04-message-detail-properties.png | Bin 0 -> 476268 bytes .../archive/04-messages-queue-browser.png | Bin 0 -> 165227 bytes .../05-main-message-display1.png | Bin .../archive/05-message-detail-body.png | Bin 0 -> 325952 bytes .../06-main-message-display2.png | Bin .../archive/06-message-detail-ai-insights.png | Bin 0 -> 345566 bytes ...message-generator-basic-single-message.png | Bin .../archive/07-message-detail-forensic.png | Bin 0 -> 339187 bytes ...08-feature-message-generator-scenarios.png | Bin .../archive/08-message-detail-headers.png | Bin 0 -> 318213 bytes .../archive/09-dead-letter-queue.png | Bin 0 -> 204973 bytes ...9-feature-message-generator-scenarios1.png | Bin .../archive/10-dead-letter-queue.png | Bin 0 -> 163885 bytes .../archive/10-dlq-message-detail-replay.png | Bin 0 -> 308029 bytes .../{ => archive}/10-message-display.png | Bin .../11-Generate-Single-Message-Topic.png | Bin .../11-messages-topic-subscription.png | Bin 0 -> 160850 bytes .../11-topic-subscription-messages.png | Bin 0 -> 197332 bytes .../archive/12-fab-quick-actions-menu.png | Bin 0 -> 354072 bytes .../archive/12-fab-quick-actions-open.png | Bin 0 -> 163896 bytes .../12-showing-message-topic.png | Bin .../archive/13-fab-send-message-dialog.png | Bin 0 -> 160257 bytes ...3-feature-message-details-custom-props.png | Bin .../archive/13-send-message-dialog.png | Bin 0 -> 311197 bytes .../14-fab-generate-messages-dialog.png | Bin 0 -> 160850 bytes ...-feature-message-details-custom-props1.png | Bin .../archive/14-generate-messages-dialog.png | Bin 0 -> 422170 bytes .../archive/15-dlq-history-page.png | Bin 0 -> 285113 bytes .../archive/15-fab-test-dlq-dialog.png | Bin 0 -> 160850 bytes .../15-feature-message-details-JSON-prop.png | Bin .../archive/15-test-dlq-dialog.png | Bin 0 -> 421473 bytes .../archive/16-dlq-history-detail.png | Bin 0 -> 286137 bytes .../archive/16-dlq-history-page.png | Bin 0 -> 276721 bytes .../16-feature-message-details-AI-Insight.png | Bin .../archive/17-dlq-history-detail.png | Bin 0 -> 456895 bytes ...17-feature-message-details-AI-Insight1.png | Bin docs/screenshots/archive/17-rules-page.png | Bin 0 -> 302034 bytes ...18-feature-message-details-AI-Insight2.png | Bin docs/screenshots/archive/18-rules-page.png | Bin 0 -> 327698 bytes .../archive/18-rules-template-gallery.png | Bin 0 -> 301087 bytes .../19-feature-ai-findings-1.png | Bin docs/screenshots/archive/19-health-page.png | Bin 0 -> 105686 bytes .../archive/19-rules-template-gallery.png | Bin 0 -> 393986 bytes .../20-feature-ai-findings-2.png | Bin docs/screenshots/archive/20-health-page.png | Bin 0 -> 264646 bytes docs/screenshots/archive/20-help-page.png | Bin 0 -> 307129 bytes .../screenshots/archive/21-help-page-full.png | Bin 0 -> 368087 bytes docs/screenshots/archive/21-help-page.png | Bin 0 -> 368087 bytes .../21-workflow-dlq-investigation-step1.png | Bin .../archive/22-scalar-api-docs.png | Bin 0 -> 190795 bytes .../22-workflow-dlq-investigation-step2.png | Bin .../23-workflow-dlq-AI-Insight.png | Bin .../24-workflow-dlq-replay-step4.png | Bin .../{ => archive}/25-feature-find-feature.png | Bin .../archive/25-scalar-api-docs.png | Bin 0 -> 190795 bytes .../{ => archive}/26-row-ui-new-feature.png | Bin .../{ => archive}/27-dlq-enhancement.png | Bin .../{ => archive}/28-dlq-intelligence.png | Bin .../29-dlq-history-post-replay-message.png | Bin .../{ => archive}/30-auto-replay-feature.png | Bin .../31-auto-relay-test-feature.png | Bin .../{ => archive}/32-replay-all-messages.png | Bin .../{ => archive}/33-replay-all-process.png | Bin .../34-post-replay-all-messages.png | Bin ...q-intelligence-history-post-replay-all.png | Bin docs/screenshots/servicehub-demo.gif | Bin 0 -> 1483901 bytes package-lock.json | 59 ++ package.json | 5 + services/api/DOCUMENTATION_INDEX.md | 450 ---------- services/api/README.md | 7 +- 112 files changed, 330 insertions(+), 1558 deletions(-) delete mode 100644 REMOTE_ACCESS.md delete mode 100644 docs/PERMISSIONS.md delete mode 100644 docs/SCREENSHOTS.md create mode 100644 docs/screenshots/01-connect-page.png create mode 100644 docs/screenshots/02-messages-queue-active.png create mode 100644 docs/screenshots/03-message-detail-properties.png create mode 100644 docs/screenshots/04-message-detail-body.png create mode 100644 docs/screenshots/05-message-detail-ai-insights.png create mode 100644 docs/screenshots/06-message-detail-forensic.png create mode 100644 docs/screenshots/07-message-detail-headers.png create mode 100644 docs/screenshots/08-messages-deadletter-queue.png create mode 100644 docs/screenshots/09-dlq-message-detail.png create mode 100644 docs/screenshots/10-dlq-message-ai-insights.png create mode 100644 docs/screenshots/11-messages-topic-subscription.png create mode 100644 docs/screenshots/12-fab-quick-actions-open.png create mode 100644 docs/screenshots/13-send-message-dialog.png create mode 100644 docs/screenshots/14-generate-messages-dialog.png create mode 100644 docs/screenshots/15-test-dlq-dialog.png create mode 100644 docs/screenshots/16-dlq-history-overview.png create mode 100644 docs/screenshots/17-dlq-history-detail.png create mode 100644 docs/screenshots/18-dlq-history-full.png create mode 100644 docs/screenshots/19-rules-page.png create mode 100644 docs/screenshots/20-rules-template-gallery.png create mode 100644 docs/screenshots/21-rules-create-dialog.png create mode 100644 docs/screenshots/22-health-page.png create mode 100644 docs/screenshots/23-help-page.png create mode 100644 docs/screenshots/24-help-page-full.png create mode 100644 docs/screenshots/25-sidebar-navigation.png create mode 100644 docs/screenshots/26-scalar-api-docs.png create mode 100644 docs/screenshots/27-full-app-overview.png create mode 100644 docs/screenshots/28-message-filter.png rename docs/screenshots/{ => archive}/01-Start-The-App.png (100%) create mode 100644 docs/screenshots/archive/01-connect-page-full.png create mode 100644 docs/screenshots/archive/01-connect-page.png rename docs/screenshots/{ => archive}/02-Connect-Service-Bus-With-Manage-ConnStr.png (100%) create mode 100644 docs/screenshots/archive/02-connect-page-namespace-connected.png create mode 100644 docs/screenshots/archive/02-messages-overview.png rename docs/screenshots/{ => archive}/03-Connected-ServiceBus.png (100%) create mode 100644 docs/screenshots/archive/03-messages-page-overview.png create mode 100644 docs/screenshots/archive/03-messages-queue-browser.png rename docs/screenshots/{ => archive}/04-feature-message-browser-empty.png (100%) create mode 100644 docs/screenshots/archive/04-message-detail-properties.png create mode 100644 docs/screenshots/archive/04-messages-queue-browser.png rename docs/screenshots/{ => archive}/05-main-message-display1.png (100%) create mode 100644 docs/screenshots/archive/05-message-detail-body.png rename docs/screenshots/{ => archive}/06-main-message-display2.png (100%) create mode 100644 docs/screenshots/archive/06-message-detail-ai-insights.png rename docs/screenshots/{ => archive}/07-feature-message-generator-basic-single-message.png (100%) create mode 100644 docs/screenshots/archive/07-message-detail-forensic.png rename docs/screenshots/{ => archive}/08-feature-message-generator-scenarios.png (100%) create mode 100644 docs/screenshots/archive/08-message-detail-headers.png create mode 100644 docs/screenshots/archive/09-dead-letter-queue.png rename docs/screenshots/{ => archive}/09-feature-message-generator-scenarios1.png (100%) create mode 100644 docs/screenshots/archive/10-dead-letter-queue.png create mode 100644 docs/screenshots/archive/10-dlq-message-detail-replay.png rename docs/screenshots/{ => archive}/10-message-display.png (100%) rename docs/screenshots/{ => archive}/11-Generate-Single-Message-Topic.png (100%) create mode 100644 docs/screenshots/archive/11-messages-topic-subscription.png create mode 100644 docs/screenshots/archive/11-topic-subscription-messages.png create mode 100644 docs/screenshots/archive/12-fab-quick-actions-menu.png create mode 100644 docs/screenshots/archive/12-fab-quick-actions-open.png rename docs/screenshots/{ => archive}/12-showing-message-topic.png (100%) create mode 100644 docs/screenshots/archive/13-fab-send-message-dialog.png rename docs/screenshots/{ => archive}/13-feature-message-details-custom-props.png (100%) create mode 100644 docs/screenshots/archive/13-send-message-dialog.png create mode 100644 docs/screenshots/archive/14-fab-generate-messages-dialog.png rename docs/screenshots/{ => archive}/14-feature-message-details-custom-props1.png (100%) create mode 100644 docs/screenshots/archive/14-generate-messages-dialog.png create mode 100644 docs/screenshots/archive/15-dlq-history-page.png create mode 100644 docs/screenshots/archive/15-fab-test-dlq-dialog.png rename docs/screenshots/{ => archive}/15-feature-message-details-JSON-prop.png (100%) create mode 100644 docs/screenshots/archive/15-test-dlq-dialog.png create mode 100644 docs/screenshots/archive/16-dlq-history-detail.png create mode 100644 docs/screenshots/archive/16-dlq-history-page.png rename docs/screenshots/{ => archive}/16-feature-message-details-AI-Insight.png (100%) create mode 100644 docs/screenshots/archive/17-dlq-history-detail.png rename docs/screenshots/{ => archive}/17-feature-message-details-AI-Insight1.png (100%) create mode 100644 docs/screenshots/archive/17-rules-page.png rename docs/screenshots/{ => archive}/18-feature-message-details-AI-Insight2.png (100%) create mode 100644 docs/screenshots/archive/18-rules-page.png create mode 100644 docs/screenshots/archive/18-rules-template-gallery.png rename docs/screenshots/{ => archive}/19-feature-ai-findings-1.png (100%) create mode 100644 docs/screenshots/archive/19-health-page.png create mode 100644 docs/screenshots/archive/19-rules-template-gallery.png rename docs/screenshots/{ => archive}/20-feature-ai-findings-2.png (100%) create mode 100644 docs/screenshots/archive/20-health-page.png create mode 100644 docs/screenshots/archive/20-help-page.png create mode 100644 docs/screenshots/archive/21-help-page-full.png create mode 100644 docs/screenshots/archive/21-help-page.png rename docs/screenshots/{ => archive}/21-workflow-dlq-investigation-step1.png (100%) create mode 100644 docs/screenshots/archive/22-scalar-api-docs.png rename docs/screenshots/{ => archive}/22-workflow-dlq-investigation-step2.png (100%) rename docs/screenshots/{ => archive}/23-workflow-dlq-AI-Insight.png (100%) rename docs/screenshots/{ => archive}/24-workflow-dlq-replay-step4.png (100%) rename docs/screenshots/{ => archive}/25-feature-find-feature.png (100%) create mode 100644 docs/screenshots/archive/25-scalar-api-docs.png rename docs/screenshots/{ => archive}/26-row-ui-new-feature.png (100%) rename docs/screenshots/{ => archive}/27-dlq-enhancement.png (100%) rename docs/screenshots/{ => archive}/28-dlq-intelligence.png (100%) rename docs/screenshots/{ => archive}/29-dlq-history-post-replay-message.png (100%) rename docs/screenshots/{ => archive}/30-auto-replay-feature.png (100%) rename docs/screenshots/{ => archive}/31-auto-relay-test-feature.png (100%) rename docs/screenshots/{ => archive}/32-replay-all-messages.png (100%) rename docs/screenshots/{ => archive}/33-replay-all-process.png (100%) rename docs/screenshots/{ => archive}/34-post-replay-all-messages.png (100%) rename docs/screenshots/{ => archive}/35-rdlq-intelligence-history-post-replay-all.png (100%) create mode 100644 docs/screenshots/servicehub-demo.gif create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 services/api/DOCUMENTATION_INDEX.md diff --git a/README.md b/README.md index bf5bed1..e9fff17 100644 --- a/README.md +++ b/README.md @@ -1,645 +1,426 @@ +
+ # ServiceHub -**The Forensic Debugger for Azure Service Bus Incidents** +### The Forensic Debugger for Azure Service Bus -When production breaks at 2 AM and you need to see **what's inside your queues** โ€” not just message counts. +**See what's inside your queues โ€” not just message counts.** -![ServiceHub Message Browser](docs/screenshots/05-main-message-display1.png) +![ServiceHub Demo](docs/screenshots/servicehub-demo.gif) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![.NET 10.0](https://img.shields.io/badge/.NET-10.0-purple.svg)](https://dotnet.microsoft.com/download/dotnet/10.0) -[![React](https://img.shields.io/badge/React-18-blue.svg)](https://react.dev/) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/) - ---- - -## โœจ What's New in v2.0 +[![.NET 10](https://img.shields.io/badge/.NET-10-purple.svg)](https://dotnet.microsoft.com/) +[![React 18](https://img.shields.io/badge/React-18-61dafb.svg)](https://react.dev/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6.svg)](https://www.typescriptlang.org/) -๐ŸŽฏ **DLQ Intelligence System** โ€” Persistent tracking of dead-letter messages with categorization and timeline -โšก **Auto-Replay Rules** โ€” Define conditional replay rules with live statistics (Pending/Replayed/Success) -๐Ÿ”„ **Batch Replay All** โ€” Replay multiple DLQ messages with one click (optimized for O(N) performance) -๐Ÿ“Š **Instant Scanning** โ€” "Scan Now" button for immediate DLQ polling (bypasses background schedule) -๐Ÿ“ **Export Capabilities** โ€” Download DLQ data as JSON/CSV for reporting -๐ŸŽจ **Enhanced UI** โ€” Improved message row display with better visual hierarchy +[Get Started](#-quick-start) ยท [Features](#-features) ยท [Screenshots](#-screenshots) ยท [API Docs](#-api-documentation) ยท [Contributing](#-contributing) -[See screenshots 26-35](#-dlq-intelligence--auto-replay-system) | [Read changelog](#roadmap) +
--- -## The Problem: Blind Incident Response +## Why ServiceHub? -You're troubleshooting a Service Bus incident. Azure Portal shows **5,000 messages in Dead-Letter Queue**, but: +Production breaks at 2 AM. Azure Portal shows **5,000 messages in Dead-Letter Queue** but you can't read them โ€” only counts and metadata. You manually sample messages one by one, spending hours on what should take minutes. -โŒ **Can't read message content** โ€” Portal only shows counts and basic metadata -โŒ **No search capability** โ€” Can't find messages by correlation ID or customer ID -โŒ **No pattern analysis** โ€” Can't identify which error types are dominating -โŒ **Manual investigation** โ€” Sample one message at a time with no bulk insight -โŒ **Limited DLQ context** โ€” Can't see failure reasons across all dead-lettered messages +**ServiceHub is a self-hosted web application that lets you browse, search, and analyze Azure Service Bus messages in real time** โ€” like email for your message queues. -**Result:** 2-hour incidents become 6-hour marathons of blind debugging. +| Capability | Azure Portal | ServiceHub | +|---|---|---| +| View message content | Count only | Full body + properties | +| Search messages | Not available | Full-text search | +| DLQ investigation | One at a time | Batch analysis + AI | +| Pattern detection | Not available | AI-powered clustering | +| Replay from DLQ | Not available | One-click replay | +| Access | Cloud portal | Any browser | --- -## ServiceHub vs. Azure Tools - -| **Capability** | **Azure Portal** | **Service Bus Explorer** | **ServiceHub** | -|----------------|------------------|--------------------------|----------------| -| **View Message Content** | โŒ Count only | โœ… Desktop only | โœ… Web-based | -| **Search Messages** | โŒ | โš ๏ธ Basic filtering | โœ… Full-text + properties | -| **DLQ Investigation** | โš ๏ธ One at a time | โš ๏ธ Manual inspection | โœ… Batch analysis + AI insights | -| **Pattern Detection** | โŒ | โŒ | โœ… AI-powered clustering | -| **Correlation Tracking** | โŒ | โš ๏ธ Manual | โœ… Search by any property | -| **Read-Only Safety** | โœ… | โš ๏ธ Can delete | โœ… Peek-only mode | -| **Multi-Message View** | โŒ | โš ๏ธ Limited | โœ… Browse 100s at once | -| **DLQ Replay** | โŒ | โŒ | โœ… One-click replay | -| **Deployment** | โ˜๏ธ Cloud | ๐Ÿ’ป Desktop app | ๐Ÿš€ Self-hosted web app | -| **Access During Incidents** | โœ… Always | โš ๏ธ Need .NET desktop | โœ… Browser only | - -### Why ServiceHub? - -โœ… **Built for incidents** โ€” Fast, web-based, read-only by design -โœ… **AI-powered insights** โ€” Automatically detect error patterns across thousands of messages -โœ… **Search at scale** โ€” Find needle-in-haystack messages by any property -โœ… **DLQ forensics** โ€” Understand failure patterns, not just individual messages -โœ… **Safe operations** โ€” Listen-only permissions, messages never leave the queue - ---- +## ๐Ÿ“ธ Screenshots -## Real-World Scenarios +### Connect to Azure Service Bus -### Scenario 1: Dead-Letter Queue Incident -**Problem:** 5,000 orders stuck in DLQ. Customer escalation at 2 AM. -**ServiceHub Solution:** -1. Browse all 5,000 DLQ messages in seconds -2. AI detects **3 error patterns**: "Payment Timeout" (40%), "Invalid Address" (35%), "Duplicate Order" (25%) -3. Search for specific customer orders by correlation ID -4. After fixing payment gateway, replay all "Payment Timeout" messages with one click - -**Time Saved:** 6 hours โ†’ 45 minutes - -### Scenario 2: Message Correlation Tracking -**Problem:** Customer reports "Order #12345 never processed." Find the message across 10,000 active messages. -**ServiceHub Solution:** -1. Search across message body, properties, and custom headers -2. Find order in 3 seconds -3. Review full message properties, delivery attempts, and processing timestamps - -**Time Saved:** 30 minutes โ†’ 30 seconds - -### Scenario 3: Integration Testing -**Problem:** Need to test error handling with 100 realistic failure scenarios. -**ServiceHub Solution:** -1. Use Message Generator with "Payment Gateway" scenario -2. Generate 100 messages with 30% anomaly rate -3. Verify error handling and DLQ behavior -4. Clean up test messages with one filter (tagged `ServiceHub-Generated`) +Enter your connection string and you're in. Supports Listen-only (read-only) or Manage (full access) policies. -**Time Saved:** Manual testing โ†’ Automated in 2 minutes +![Connect Page](docs/screenshots/01-connect-page.png) ---- +### Message Browser -## Feature Showcase +Browse active messages across queues and topic subscriptions. See message previews, status badges, and metadata at a glance. -### ๐ŸŽจ Enhanced Message Browser UI +![Messages Queue Active](docs/screenshots/02-messages-queue-active.png) -Improved message list with better visual hierarchy and information density: +### Message Detail โ€” Properties -![Row UI Enhancement](docs/screenshots/26-row-ui-new-feature.png) +Click any message to inspect every detail. System properties show Message ID, enqueue time, TTL, sequence number, delivery count, and content type. -**Improvements:** -- Clearer message metadata display -- Enhanced property visibility -- Better spacing and readability -- Optimized for long debugging sessions +![Message Properties](docs/screenshots/03-message-detail-properties.png) ---- +### Message Detail โ€” Body -### ๐Ÿ” Message Browser with Search +Full JSON body with syntax highlighting and copy button. Supports JSON, XML, and plain text formats. -Browse messages across queues and topic subscriptions with powerful filtering: +![Message Body](docs/screenshots/04-message-detail-body.png) -![Empty Queue](docs/screenshots/04-feature-message-browser-empty.png) +### Message Detail โ€” AI Insights -![Messages Loaded](docs/screenshots/05-main-message-display1.png) +AI-powered analysis detects patterns, anomalies, and error clusters across your messages. All processing happens in your browser โ€” no data leaves your environment. -**Features:** -- View 100+ messages at once (Azure Portal: count only) -- Real-time filtering by content, properties, message ID -- Event type badges, timestamps, delivery counts -- Active and Dead-Letter queue tabs in one view +![AI Insights](docs/screenshots/05-message-detail-ai-insights.png) ---- +### Message Detail โ€” Forensic View -### ๐Ÿ“Š Complete Message Details +Deep forensic analysis showing message lifecycle, delivery attempts, and processing timeline. -Click any message to inspect every detail: +![Forensic View](docs/screenshots/06-message-detail-forensic.png) -**System Properties** โ€” Message ID, enqueue time, TTL, sequence number: -![Message Properties](docs/screenshots/10-message-display.png) +### Message Detail โ€” Headers -**Custom Properties** โ€” Application headers, correlation IDs, business metadata: -![Custom Properties](docs/screenshots/13-feature-message-details-custom-props.png) +View all custom application properties and correlation headers. -**Message Body** โ€” Full JSON with syntax highlighting and copy button: -![Message Body](docs/screenshots/15-feature-message-details-JSON-prop.png) +![Headers View](docs/screenshots/07-message-detail-headers.png) ---- +### Dead-Letter Queue -### ๐Ÿค– AI-Powered Pattern Detection +Investigate failed messages with DLQ reason, error description, and AI-powered remediation guidance. Replay messages back to the original queue with one click. -ServiceHub analyzes **all messages** and automatically identifies error clusters: +![Dead Letter Queue](docs/screenshots/08-messages-deadletter-queue.png) -![AI Findings](docs/screenshots/19-feature-ai-findings-1.png) +### DLQ Message Detail with Replay -**What it detects:** -- Error message patterns (e.g., "Payment Timeout" across 500 messages) -- Anomaly rates and confidence scores -- Message groups by failure type -- Suggested remediation actions +See why messages failed and replay them after fixing the root cause. -![AI Insights](docs/screenshots/16-feature-message-details-AI-Insight.png) +![DLQ Message Detail](docs/screenshots/09-dlq-message-detail.png) -**Real-world example:** From 5,000 DLQ messages, AI instantly identifies: -- 40% failed due to "Payment Gateway Timeout" -- 35% failed due to "Invalid Shipping Address" -- 25% failed due to "Duplicate Order ID" +### DLQ AI Analysis ---- +AI automatically categorizes DLQ failures and suggests remediation steps. -### โš ๏ธ Dead-Letter Queue Forensics +![DLQ AI Insights](docs/screenshots/10-dlq-message-ai-insights.png) -Investigate DLQ failures with complete context: +### Topic Subscriptions -![DLQ Investigation](docs/screenshots/21-workflow-dlq-investigation-step1.png) +Browse messages from topic subscriptions with the same powerful inspection tools. -**See instantly:** -- Dead-letter reason (e.g., "MaxDeliveryCountExceeded") -- Error description from Azure Service Bus -- Delivery attempt history -- Original message timestamp vs. DLQ timestamp +![Topic Subscription](docs/screenshots/11-messages-topic-subscription.png) -![DLQ Guidance](docs/screenshots/22-workflow-dlq-investigation-step2.png) +### Quick Actions (FAB) -**ServiceHub provides actionable guidance:** -- Why the message failed -- Recommended fix based on failure type -- Whether to replay or discard -- Related messages with same failure pattern +Floating action button provides quick access to Send Message, Generate Messages, Test DLQ, and Refresh All. ---- +![FAB Quick Actions](docs/screenshots/12-fab-quick-actions-open.png) -### ๐Ÿ”„ DLQ Message Replay +### Send Message -After fixing the root cause, replay messages back to the original queue: +Send single messages to queues or topics for ad-hoc testing. Supports custom properties, content types, and advanced options. -![DLQ AI Insights](docs/screenshots/23-workflow-dlq-AI-Insight.png) +![Send Message](docs/screenshots/13-send-message-dialog.png) -![Replay Dialog](docs/screenshots/24-workflow-dlq-replay-step4.png) +### Generate Test Messages -**Use cases:** -- Payment gateway was down โ†’ Now restored โ†’ Replay all payment messages -- Bug in message processor โ†’ Bug fixed โ†’ Replay all failed messages -- Invalid configuration โ†’ Config corrected โ†’ Reprocess orders +Generate realistic test messages with built-in scenarios (Order Processing, Payment Gateway, Notification Service, and more). Configure volume and anomaly rate. ---- +![Generate Messages](docs/screenshots/14-generate-messages-dialog.png) -### ๐Ÿ”Ž Advanced Search +### DLQ Intelligence Dashboard -Find messages by any property โ€” something Azure Portal **cannot do**: +Persistent tracking and monitoring of dead-letter queue messages with trend chart, status breakdown, and category classification. -![Search](docs/screenshots/25-feature-find-feature.png) +![DLQ Intelligence](docs/screenshots/16-dlq-history-overview.png) -**Search across:** -- Message body content (JSON, XML, plain text) -- Custom properties (correlation ID, customer ID, order number) -- System properties (message ID, session ID) -- Event types and categories +### DLQ History Detail -**Example:** Find all messages for customer "C-12345" across 10,000 messages in 2 seconds. +Drill into individual DLQ records with forensic timeline, replay history, and status tracking. ---- +![DLQ History Detail](docs/screenshots/17-dlq-history-detail.png) -## Testing & Development Tools +### Auto-Replay Rules -### Message Generator +Define conditional replay rules with live statistics. Match messages by dead-letter reason, error description, entity name, content type, or body text. -Generate realistic test messages for integration testing: +![Auto-Replay Rules](docs/screenshots/19-rules-page.png) -![Generator](docs/screenshots/07-feature-message-generator-basic-single-message.png) +### Rule Template Gallery -![Generator Scenarios](docs/screenshots/08-feature-message-generator-scenarios.png) +Browse pre-built rule templates for common failure scenarios โ€” transient errors, max delivery exceeded, expired messages, and more. -**6 Built-in Scenarios:** -1. **Order Processing** โ€” E-commerce order flow -2. **Payment Gateway** โ€” Payment transactions with retries -3. **Notification Service** โ€” Email/SMS delivery events -4. **Inventory Management** โ€” Stock updates and alerts -5. **User Activity** โ€” Login, registration, profile updates -6. **Error Events** โ€” Simulated failures for testing +![Template Gallery](docs/screenshots/20-rules-template-gallery.png) -**Configuration:** -- Volume: 30, 50, 100, 150, 200 messages -- Anomaly rate: 0% to 50% (inject failures for testing) -- Auto-tagged with `ServiceHub-Generated` for easy cleanup +### Create Auto-Replay Rule -### Send Custom Messages +Build custom rules with field conditions, operators, actions, rate limiting, and target entity configuration. -Send single messages to queues or topics for ad-hoc testing: +![Create Rule](docs/screenshots/21-rules-create-dialog.png) -![Send Message](docs/screenshots/11-Generate-Single-Message-Topic.png) +### System Health -![Topic Messages](docs/screenshots/12-showing-message-topic.png) +Monitor API health, uptime, memory usage, thread count, GC collections, and server information. ---- +![System Health](docs/screenshots/22-health-page.png) -## Quick Start (Zero Configuration!) +### Help & Quick Reference -### ๐Ÿš€ Automated Setup (Recommended) +Searchable help guide covering every feature, Azure Service Bus concepts, and a guided tour. -**No manual prerequisites needed!** The setup script automatically installs everything: +![Help Page](docs/screenshots/23-help-page.png) -```bash -git clone https://github.com/debdevops/servicehub.git -cd servicehub -./run.sh -``` +### Message Search & Filter -The script will: -โœ… Auto-install .NET 8 SDK (if not present) -โœ… Auto-install Node.js 18+ (if not present) -โœ… Install all dependencies -โœ… Start both API and UI servers +Find messages by any property โ€” message ID, correlation ID, body content, custom headers. -**First run:** 5-10 minutes (includes installations) -**Subsequent runs:** 30-60 seconds +![Message Filter](docs/screenshots/28-message-filter.png) -Open **http://localhost:3000** +### Sidebar Navigation -> ๐Ÿ“˜ **Detailed Setup Guide:** See [SETUP.md](SETUP.md) for platform-specific details, troubleshooting, and manual installation options. +Namespace browser with live message counts, queue/topic tree, and Quick Access panel. -### Prerequisites (Auto-Installed) -The following are **automatically installed** by `run.sh`: -- โœ… .NET 8.0 SDK -- โœ… Node.js 18+ -- โœ… npm package manager -- โœ… Required utilities (curl, lsof) +![Sidebar Navigation](docs/screenshots/25-sidebar-navigation.png) -**Supports all major platforms:** -- macOS (Intel & Apple Silicon), Ubuntu/Debian, RHEL/CentOS/Fedora, Arch Linux, openSUSE, Alpine, WSL -- See [CROSS-PLATFORM-COMPATIBILITY.md](CROSS-PLATFORM-COMPATIBILITY.md) for complete list +### API Documentation (Scalar) -**You only need:** -- โš ๏ธ Azure Service Bus connection string (Listen permission only) +Interactive API documentation with Scalar โ€” test endpoints directly from the browser. -![Application Start](docs/screenshots/01-Start-The-App.png) +![API Docs](docs/screenshots/26-scalar-api-docs.png) -### Connect to Your Namespace +--- -1. Enter a display name for your connection -2. Paste your Service Bus connection string -3. Click "Connect" +## ๐Ÿš€ Features -![Connection Form](docs/screenshots/02-Connect-Service-Bus-With-Manage-ConnStr.png) +### Message Browser +- Browse **active** and **dead-letter** queue messages side by side +- View full message body with JSON syntax highlighting +- Inspect system properties, custom headers, and application properties +- Real-time search across message content and properties +- Auto-refresh with configurable polling intervals -Your queues and topics appear instantly: +### AI-Powered Analysis +- **Pattern detection** โ€” Identify error clusters across thousands of messages +- **Anomaly identification** โ€” Flag unusual messages automatically +- **Remediation suggestions** โ€” Actionable guidance for each failure type +- **Client-side processing** โ€” All analysis runs in your browser; no data leaves your environment -![Connected Namespace](docs/screenshots/03-Connected-ServiceBus.png) +### DLQ Intelligence System +- **Persistent tracking** โ€” DLQ messages stored in local SQLite database +- **Category classification** โ€” Auto-categorizes: Transient, MaxDelivery, Expired, DataQuality, Authorization +- **Trend chart** โ€” 30-day DLQ trend visualization (New vs. Resolved) +- **Instant scanning** โ€” "Scan Now" for immediate DLQ polling +- **Export** โ€” Download DLQ data as JSON or CSV +- **Status tracking** โ€” Active โ†’ Replayed โ†’ ReplayFailed โ†’ Resolved -**That's it!** Start browsing messages, investigating DLQs, and using AI insights. +### Auto-Replay Rules Engine +- **Conditional matching** โ€” Match messages by reason, error description, entity, delivery count, body text +- **Operators** โ€” Contains, Equals, StartsWith, EndsWith, Regex, GreaterThan, LessThan, In +- **Live statistics** โ€” Pending/Replayed/Success counts updated in real time +- **Rate limiting** โ€” Max replays per hour to prevent overwhelming downstream services +- **Batch replay** โ€” Replay all matching messages with one click +- **Template gallery** โ€” Pre-built rules for common failure scenarios +- **Circuit breaker** โ€” Auto-disables rules if success rate drops below threshold + +### Testing & Development Tools +- **Send Message** โ€” Send single messages to queues or topics with custom properties +- **Generate Messages** โ€” 6 built-in scenarios with configurable volume (30โ€“200) and anomaly rate (0โ€“50%) +- **Test DLQ** โ€” Move test messages to dead-letter queue for testing DLQ workflows +- **Tagged messages** โ€” All generated messages tagged with `ServiceHub-Generated` for easy cleanup + +### Security & Safety +- **Read-only by default** โ€” Uses Azure SDK PeekMessagesAsync; messages are never removed +- **Listen-only supported** โ€” Works with Listen permission for browse-only access +- **Encrypted at rest** โ€” Connection strings encrypted with AES-GCM +- **No external API calls** โ€” AI analysis runs entirely in the browser +- **No data persistence** โ€” Messages displayed in-memory only +- **Safe for production** โ€” Will not interfere with your consumers --- -## Architecture +## โšก Quick Start -``` -servicehub/ -โ”œโ”€โ”€ apps/web/ # React + TypeScript frontend -โ”‚ โ””โ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ components/ # UI components -โ”‚ โ”œโ”€โ”€ hooks/ # React Query hooks -โ”‚ โ””โ”€โ”€ lib/ # API client, utilities -โ”‚ -โ”œโ”€โ”€ services/api/ # ASP.NET Core backend -โ”‚ โ””โ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ ServiceHub.Api/ # REST API controllers -โ”‚ โ”œโ”€โ”€ ServiceHub.Core/ # Domain entities & DTOs -โ”‚ โ””โ”€โ”€ ServiceHub.Infrastructure/ # Azure SDK integration -โ”‚ -โ””โ”€โ”€ run.sh # One-command startup script -``` +### Automated Setup (Recommended) -**Tech Stack:** -- **Frontend:** React 18, TypeScript, Tailwind CSS, TanStack Query (React Query) -- **Backend:** ASP.NET Core 8, Azure.Messaging.ServiceBus SDK -- **AI Analysis:** Client-side pattern detection (no external API calls) -- **Database:** None (stateless, in-memory only) - ---- +```bash +git clone https://github.com/debdevops/servicehub.git +cd servicehub +./run.sh +``` -## Security & Compliance +The script automatically: +- Installs .NET 10 SDK (if not present) +- Installs Node.js 18+ (if not present) +- Builds and starts both API and UI servers -### Read-Only by Design +Open **http://localhost:3000** and connect with your Azure Service Bus connection string. -ServiceHub uses Azure SDK's **`PeekMessagesAsync`** โ€” messages are **never removed** from queues: +### Prerequisites -```csharp -// ServiceHub only reads โ€” never receives or deletes -await receiver.PeekMessagesAsync(maxMessages: 100); -``` +Auto-installed by `run.sh`: +- .NET 10.0 SDK +- Node.js 18+ -โœ… **Safe for production** โ€” Messages remain in queue for your actual consumers -โœ… **Audit-friendly** โ€” No message deletion or modification -โœ… **Incident-safe** โ€” Won't interfere with recovery processes +You provide: +- Azure Service Bus connection string (Listen permission minimum) -### Minimal Permissions Required - -Create a dedicated SAS policy with **Listen permission only**: +### Create a Service Bus Policy +For read-only browsing: ```bash -# Azure CLI az servicebus namespace authorization-rule create \ --namespace-name \ - --name servicehub-readonly \ + --resource-group \ + --name servicehub \ --rights Listen ``` -โŒ ServiceHub does NOT require: -- `Manage` permissions -- `Send` permissions (except for testing tools, optional) -- `Receive` permissions (never removes messages) - -### Data Privacy - -- **No external API calls** โ€” AI analysis runs in your browser -- **No data persistence** โ€” Messages displayed in-memory only -- **No logging of message content** โ€” API logs only metadata (message count, queue names) -- **Self-hosted** โ€” Deploy in your VPC/VNET, no SaaS dependencies - ---- - -## Use Cases by Role - -### DevOps Engineers -โœ… Investigate DLQ spikes during incidents -โœ… Validate message processing after deployments -โœ… Search for specific customer transactions -โœ… Monitor message flow across queues and topics - -### QA Engineers -โœ… Generate realistic test messages for integration testing -โœ… Verify message routing to correct topic subscriptions -โœ… Test error handling with controlled anomaly injection -โœ… Validate message transformations and enrichments - -### Support Teams -โœ… Find customer orders by correlation ID during escalations -โœ… Verify if customer message was received or dead-lettered -โœ… Provide timestamps and failure reasons to customers -โœ… Replay messages after incident resolution - -### Platform Engineers -โœ… Debug namespace-level message routing issues -โœ… Analyze message patterns for capacity planning -โœ… Investigate intermittent failures across multiple queues -โœ… Document incident timelines with actual message evidence - ---- - -## Key Differentiators - -| Feature | Impact | -|---------|--------| -| **Web-Based** | No desktop installation needed โ€” accessible from any browser during incidents | -| **Bulk Message View** | Browse 100s of messages at once vs. one-by-one in Azure Portal | -| **AI Pattern Detection** | Automatically identify root causes across thousands of DLQ messages | -| **Full-Text Search** | Find messages by correlation ID, customer ID, or any property | -| **DLQ Replay** | One-click message reprocessing after fixing issues | -| **Testing Tools** | Generate realistic scenarios for integration testing | -| **Read-Only Safety** | Listen-only permissions โ€” never removes messages from production | -| **Self-Hosted** | Deploy in your environment, no SaaS vendor dependency | - ---- - -## ๐ŸŽฏ DLQ Intelligence & Auto-Replay System - -ServiceHub includes a **production-ready DLQ management system** that goes far beyond simple message inspection. - -### ๐Ÿ“Š DLQ Intelligence Dashboard - -Persistent tracking and monitoring of dead-letter queue messages: - -![DLQ Enhancement](docs/screenshots/27-dlq-enhancement.png) - -![DLQ Intelligence](docs/screenshots/28-dlq-intelligence.png) - -**Features:** -- **Persistent history** โ€” All DLQ messages tracked in SQLite database -- **Instant scanning** โ€” "Scan Now" button triggers immediate DLQ poll (bypasses 10-15s background schedule) -- **Category classification** โ€” Auto-categorizes failures: Transient, MaxDelivery, Expired, DataQuality, Authorization, etc. -- **Export capabilities** โ€” Download DLQ data as JSON or CSV for reporting -- **Timeline view** โ€” Replay history with timestamps, outcomes, and error details -- **Status tracking** โ€” Active โ†’ Replayed โ†’ ReplayFailed โ†’ Resolved +For full access (send, generate, replay): +```bash +az servicebus namespace authorization-rule create \ + --namespace-name \ + --resource-group \ + --name servicehub \ + --rights Listen Send Manage +``` -![DLQ History Post-Replay](docs/screenshots/29-dlq-history-post-replay-message.png) +### URLs -**Background monitoring:** DLQ messages are automatically scanned every 10-15 seconds and stored for forensic analysis. +| Service | URL | +|---|---| +| UI | http://localhost:3000 | +| API | http://localhost:5153 | +| API Docs | http://localhost:5153/scalar/v1 | --- -### โšก Auto-Replay Rules Engine - -Define **conditional replay rules** that match DLQ messages and enable batch operations: - -![Auto-Replay Feature](docs/screenshots/30-auto-replay-feature.png) - -**Rule Components:** -- **Conditions** โ€” Match messages by reason, error description, entity name, delivery count, content type, body text, or custom properties -- **Operators** โ€” Contains, Equals, StartsWith, EndsWith, Regex, GreaterThan, LessThan, In -- **Target Entity** โ€” Optionally replay to a different queue/topic (not just original) -- **Rate Limiting** โ€” Max replays per hour to prevent overwhelming downstream services +## ๐Ÿ—๏ธ Architecture -**Rule Statistics (Live Evaluation):** -- **Pending** โ€” How many Active DLQ messages currently match this rule (evaluated in real-time) -- **Replayed** โ€” How many messages have been replayed using this rule -- **Success** โ€” Replay success count and percentage +``` +servicehub/ +โ”œโ”€โ”€ apps/web/ # React 18 + TypeScript + Vite frontend +โ”‚ โ””โ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ components/ # UI components (messages, DLQ, rules, FAB) +โ”‚ โ”œโ”€โ”€ hooks/ # React Query hooks for API communication +โ”‚ โ”œโ”€โ”€ lib/ # API client, utilities, help content +โ”‚ โ””โ”€โ”€ pages/ # Page components (Messages, Connect, Rules, Health, Help) +โ”‚ +โ”œโ”€โ”€ services/api/ # ASP.NET Core 10 backend +โ”‚ โ””โ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ ServiceHub.Api/ # REST controllers, middleware, auth +โ”‚ โ”œโ”€โ”€ ServiceHub.Core/ # Domain entities, DTOs, interfaces +โ”‚ โ”œโ”€โ”€ ServiceHub.Infrastructure/ # Azure SDK integration, SQLite +โ”‚ โ””โ”€โ”€ ServiceHub.Shared/ # Common types and utilities +โ”‚ +โ”œโ”€โ”€ scripts/ # Setup and utility scripts +โ””โ”€โ”€ run.sh # One-command startup +``` -![Auto-Relay Test Feature](docs/screenshots/31-auto-relay-test-feature.png) +**Tech Stack:** +| Layer | Technology | +|---|---| +| Frontend | React 18, TypeScript, Tailwind CSS, TanStack Query | +| Backend | ASP.NET Core 10, Azure.Messaging.ServiceBus SDK | +| AI Analysis | Client-side pattern detection (no external API) | +| Database | SQLite (DLQ Intelligence), in-memory cache | +| API Docs | Scalar (OpenAPI) | -**Test Before Replay:** Click "Test" to preview how many Active DLQ messages match your conditions before executing any replay. +For detailed backend architecture, see [services/api/ARCHITECTURE.md](services/api/ARCHITECTURE.md). --- -### ๐Ÿ”„ Batch Replay All System - -Replay **multiple matching messages** with a single click: - -![Replay All Messages](docs/screenshots/32-replay-all-messages.png) - -**Safety Features:** -- **Confirmation dialog with warnings** โ€” Red danger header, 3 safety warnings before executing -- **Real-time statistics** โ€” Shows how many messages will be affected -- **Cancel auto-focused** โ€” Safer default to prevent accidental clicks -- **Test first workflow** โ€” Encourages testing rules before bulk replay - -![Replay All Process](docs/screenshots/33-replay-all-process.png) - -**Performance:** Optimized batch replay using **single DLQ receiver per entity** (not per message). Tested with 7 messages across 2 subscriptions โ†’ **9 seconds** (vs. 30s+ timeout before optimization). - -![Post-Replay All Messages](docs/screenshots/34-post-replay-all-messages.png) - -**Outcome Tracking:** -- Messages matched: 7 -- Successfully replayed: 7 -- Failed: 0 -- Skipped: 0 - -![DLQ Intelligence History Post-Replay All](docs/screenshots/35-rdlq-intelligence-history-post-replay-all.png) +## ๐Ÿ’ก Real-World Scenarios -**History Audit:** Every replay attempt is recorded in the DLQ Intelligence history with: -- Timestamp -- Rule name or "Manual Replay" -- Outcome (Success/Failed) -- Target entity -- Error details (if failed) - ---- +### Scenario 1: Dead-Letter Queue Incident +**Problem:** 5,000 orders stuck in DLQ at 2 AM. -### Real-World DLQ Scenario +**With ServiceHub:** +1. Browse all 5,000 DLQ messages instantly +2. AI detects 3 error patterns: Payment Timeout (40%), Invalid Address (35%), Duplicate Order (25%) +3. Search for specific customer orders by correlation ID +4. Create auto-replay rule for Payment Timeout โ€” replay all 2,000 messages -**Situation:** 1,500 order messages dead-lettered due to payment gateway outage. +**Time saved:** 6 hours โ†’ 45 minutes -**ServiceHub Workflow:** -1. Open **DLQ Intelligence** โ†’ See 1,500 Active messages categorized as "Transient" failures -2. Create **Auto-Replay Rule**: - - Condition: `Reason contains "Payment Gateway Timeout"` - - Target: Original subscription - - Rate limit: 500/hour -3. Click **Test** โ†’ Confirms 1,500 messages match -4. Click **Replay All** โ†’ Review warnings โ†’ Confirm -5. Result: **All 1,500 messages replayed in ~3 minutes** -6. Check **DLQ Intelligence History** โ†’ See replay audit trail with success/failure counts +### Scenario 2: Message Correlation +**Problem:** Customer reports order never processed. -**Why it's safe:** -- โœ… Messages removed from DLQ only after successful send to target -- โœ… Failed replays remain in DLQ with error description -- โœ… Rate limiting prevents overwhelming downstream services -- โœ… Full audit trail for compliance/debugging +**With ServiceHub:** +1. Search across message body and properties +2. Find order in 3 seconds across 10,000 messages +3. Review full message properties and delivery history ---- +**Time saved:** 30 minutes โ†’ 30 seconds -## Roadmap - -**Completed Features** โœ… +### Scenario 3: Integration Testing +**Problem:** Need to test error handling with 100 realistic failure scenarios. -- โœ… **Bulk DLQ Replay** โ€” Replay multiple messages with rule-based filters -- โœ… **DLQ Intelligence** โ€” Persistent tracking, categorization, and history -- โœ… **Auto-Replay Rules** โ€” Conditional replay with rate limiting -- โœ… **Batch Optimization** โ€” O(N) performance with single receiver per entity +**With ServiceHub:** +1. Select Payment Gateway scenario in Message Generator +2. Generate 100 messages with 30% anomaly rate +3. Verify error handling and DLQ behavior +4. Clean up test messages filtered by ServiceHub-Generated tag -**Community Requests:** [Open an issue](https://github.com/debdevops/servicehub/issues) to suggest features! +**Time saved:** Manual testing โ†’ Automated in 2 minutes --- -## Comparison: ServiceHub vs. Alternatives +## ๐Ÿ” Permissions Guide -### vs. Azure Portal +| Permission Level | Capabilities | +|---|---| +| **Listen only** | Browse messages, inspect DLQ, search, AI insights, view health | +| **Listen + Send** | All above + replay from DLQ + send test messages | +| **Manage** | All above + generate messages, test DLQ, full management | -**Azure Portal Strengths:** -- Always available, no installation -- Basic metrics and alerting -- Namespace management - -**Azure Portal Limitations:** -- โŒ Cannot view message **content** (only metadata) -- โŒ No search or filtering capabilities -- โŒ DLQ investigation requires manual sampling -- โŒ No pattern detection or bulk analysis - -**When to use ServiceHub:** When you need to see **what's inside messages** during incidents. +> **Tip:** Create a dedicated `servicehub` policy instead of using `RootManageSharedAccessKey`. --- -### vs. Azure Service Bus Explorer (Desktop) - -**Service Bus Explorer Strengths:** -- Rich feature set for power users -- Message management (send, receive, delete) -- Detailed property inspection - -**Service Bus Explorer Limitations:** -- โš ๏ธ Desktop application (Windows .NET required) -- โš ๏ธ Limited multi-message view -- โŒ No AI pattern detection -- โŒ No bulk DLQ analysis -- โš ๏ธ Can accidentally delete production messages +## ๐Ÿ“– API Documentation -**When to use ServiceHub:** -- Web-based access from any device -- Read-only safety for production -- AI-powered pattern detection -- Bulk message analysis +ServiceHub exposes a REST API documented with Scalar (OpenAPI). Access interactive docs at: ---- - -### vs. Custom Scripts +**http://localhost:5153/scalar/v1** -**Custom Scripts (PowerShell/CLI) Strengths:** -- Automation potential -- Scriptable workflows +![API Documentation](docs/screenshots/26-scalar-api-docs.png) -**Custom Scripts Limitations:** -- โš ๏ธ Requires scripting expertise -- โŒ No UI for visual exploration -- โŒ Limited to technical users -- โš ๏ธ Time-consuming to build and maintain - -**When to use ServiceHub:** -- Ready-to-use web UI -- Accessible to non-developers (support teams, QA) -- Zero maintenance required +Key endpoints: +- `GET /api/v1/namespaces` โ€” List connected namespaces +- `GET /api/v1/namespaces/{id}/queues` โ€” List queues with message counts +- `GET /api/v1/namespaces/{id}/topics` โ€” List topics with subscription counts +- `GET /api/v1/namespaces/{id}/queues/{name}/messages` โ€” Browse messages +- `POST /api/v1/namespaces/{id}/queues/{name}/messages` โ€” Send a message +- `GET /api/v1/dlq-history` โ€” DLQ Intelligence records +- `GET /api/v1/replay-rules` โ€” Auto-replay rules --- -## FAQ - -**Q: Does ServiceHub remove messages from queues?** -A: No. ServiceHub uses Azure SDK's **`PeekMessagesAsync`** which only reads messages without removing them. Your consumers continue processing normally. +## โ“ FAQ -**Q: Can I use ServiceHub in production?** -A: Yes. ServiceHub only requires **Listen** permission and operates in read-only mode (except for optional testing tools). Messages remain in queues for your actual consumers. +**Q: Does ServiceHub remove messages from queues?** +No. ServiceHub uses PeekMessagesAsync which reads messages without removing them. Your consumers continue processing normally. -**Q: How does AI pattern detection work?** -A: ServiceHub analyzes message content, properties, and error descriptions using heuristic algorithms **in your browser**. No data leaves your environment โ€” no external AI API calls. +**Q: Is it safe for production?** +Yes. ServiceHub only requires Listen permission and operates in read-only mode. Messages remain in queues for your actual consumers. -**Q: What about sensitive data in messages?** -A: ServiceHub displays messages only in your browser session (not persisted). Deploy ServiceHub in your private network and restrict access via your identity provider. API logs do not capture message content. +**Q: How does AI analysis work?** +ServiceHub analyzes message content using heuristic algorithms entirely in your browser. No data leaves your environment. -**Q: Can I deploy ServiceHub to Azure App Service / Kubernetes?** -A: Yes. ServiceHub is a standard ASP.NET Core + React app. You can containerize it with Docker and deploy to any platform supporting .NET 8 and Node.js. +**Q: What about sensitive data?** +Messages are displayed only in your browser session โ€” not persisted. Deploy ServiceHub in your private network and restrict access. -**Q: Does ServiceHub support topics with subscriptions?** -A: Yes. ServiceHub browses messages from both queues and topic subscriptions. You can view messages for each subscription independently. +**Q: Can I deploy to Azure App Service / Kubernetes?** +Yes. ServiceHub is a standard ASP.NET Core + React SPA. Containerize with Docker and deploy anywhere supporting .NET 10. -**Q: What about message sessions?** -A: Session-aware browsing is on the roadmap. Currently, you can view session-enabled messages but not group them by session ID. - -**Q: Can I contribute?** -A: Absolutely! ServiceHub is open source (MIT license). Fork the repository, make your changes, and submit a pull request. +**Q: Does it support topics with subscriptions?** +Yes. Browse messages from both queues and topic subscriptions independently. --- -## Contributing - -We welcome contributions! Whether it's bug fixes, new features, or documentation improvements. +## ๐Ÿค Contributing -**How to contribute:** +We welcome contributions! Bug fixes, features, and documentation improvements. 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Make your changes with tests -4. Commit your changes (`git commit -m 'Add amazing feature'`) -5. Push to the branch (`git push origin feature/amazing-feature`) -6. Open a Pull Request +4. Commit and push +5. Open a Pull Request --- @@ -647,43 +428,14 @@ We welcome contributions! Whether it's bug fixes, new features, or documentation MIT License โ€” see [LICENSE](LICENSE) for details. -**TL;DR:** Use ServiceHub freely in your organization, modify as needed, no attribution required in your deployment. - ---- - -## Links & Resources - -- **๐Ÿ  Repository:** https://github.com/debdevops/servicehub -- **๐Ÿ› Report Issues:** https://github.com/debdevops/servicehub/issues -- **๐Ÿ“– Full Documentation:** [docs/COMPREHENSIVE-GUIDE.md](docs/COMPREHENSIVE-GUIDE.md) -- **๏ฟฝ Screenshots Guide:** [docs/SCREENSHOTS.md](docs/SCREENSHOTS.md) -- **๐Ÿ’ฌ Discussions:** https://github.com/debdevops/servicehub/discussions - ---- - -## Support - -**Need help?** -- ๐Ÿ“– Check the [documentation](docs/) -- ๐Ÿ’ฌ Start a [discussion](https://github.com/debdevops/servicehub/discussions) -- ๐Ÿ› Report a [bug](https://github.com/debdevops/servicehub/issues) - -**Enterprise support:** For production deployment assistance, custom features, or training โ€” open an issue with `[Enterprise]` tag. - ---- - -## Star History - -If ServiceHub saved you time during an incident, give it a โญ on GitHub! - ---
-**ServiceHub** โ€” Because your Service Bus messages shouldn't be invisible during incidents. +**ServiceHub** โ€” Because your Service Bus messages should not be invisible during incidents. -Built with โค๏ธ for DevOps and Platform Engineers +Built for DevOps, Platform, and SRE Engineers. -[Get Started](#quick-start-5-minutes) โ€ข [View Features](#feature-showcase) โ€ข [Report Issue](https://github.com/debdevops/servicehub/issues) +[Get Started](#-quick-start) ยท [View Features](#-features) ยท [Report Issue](https://github.com/debdevops/servicehub/issues)
diff --git a/REMOTE_ACCESS.md b/REMOTE_ACCESS.md deleted file mode 100644 index 257a483..0000000 --- a/REMOTE_ACCESS.md +++ /dev/null @@ -1,174 +0,0 @@ -# ServiceHub โ€” Remote Server Access Guide - -This guide explains how to run ServiceHub on a Linux server and access it -from a different machine (laptop, another server, etc.). - ---- - -## How It Works - -ServiceHub uses a Vite development proxy to route browser API calls: - -``` -Your Browser โ†’ http://linuxhost:3000/api/v1/... - โ†“ (Vite proxy, same server) - http://localhost:5153/api/v1/... (ASP.NET Core API) -``` - -By keeping the proxy on the server, the browser never needs to reach port 5153 -directly โ€” it just calls port 3000 (the UI port) for everything. This means: - -- **No CORS issues** โ€” the browser sees all requests as same-origin (port 3000). -- **Firewall simplicity** โ€” you only need to open one port (3000) to the outside world. -- **No hardcoded IP addresses** in client code. - ---- - -## Quick Start (Remote Server) - -### 1. Start ServiceHub - -```bash -cd /path/to/servicehub -bash run.sh -``` - -The startup banner will show all access URLs automatically: - -``` -๐ŸŒ Web UI: - โ€ข http://localhost:3000 โ† from this machine - โ€ข http://192.168.1.50:3000 โ† from remote machines (by IP) - โ€ข http://linuxhost:3000 โ† from remote machines (by hostname) - -๐Ÿ“ API Endpoints: - โ€ข HTTP: http://localhost:5153 - โ€ข Remote: http://192.168.1.50:5153 - โ€ข API Docs: http://localhost:5153/scalar/v1 -``` - -### 2. Access from Your Laptop - -Open a browser on your laptop and navigate to: - -``` -http://linuxhost:3000 -``` - -Replace `linuxhost` with the server's hostname or IP address. - ---- - -## Firewall Configuration - -If the connection is refused, open the UI port on the server's firewall. -You only need port **3000** (API calls are proxied through it). - -**Ubuntu / Debian (ufw):** -```bash -sudo ufw allow 3000/tcp -sudo ufw reload -``` - -**RHEL / CentOS / Fedora (firewalld):** -```bash -sudo firewall-cmd --add-port=3000/tcp --permanent -sudo firewall-cmd --reload -``` - -**Quick test** (from your laptop): -```bash -curl http://linuxhost:3000/health -``` - ---- - -## Supporting Additional CORS Origins (Advanced) - -The Vite proxy eliminates CORS for browser calls. However, if you are making -**direct API calls** from another backend service or tool (e.g. curl, Postman, -another server-side app), you need to tell the .NET API which origins to allow. - -Use the `SERVICEHUB_ALLOWED_ORIGINS` environment variable โ€” no config-file edits needed: - -```bash -export SERVICEHUB_ALLOWED_ORIGINS="http://192.168.1.50:3000,http://linuxhost:3000" -bash run.sh -``` - -Multiple origins are comma-separated. This variable is read at startup by -`CorsConfiguration.cs` and merged into the allowed origins list alongside any -values already in `appsettings.json`. - ---- - -## Environment Variable Reference - -| Variable | Purpose | Example | -|---|---|---| -| `SERVICEHUB_ALLOWED_ORIGINS` | Extra CORS origins (comma-separated) | `http://192.168.1.50:3000` | -| `VITE_API_BASE_URL` | Override API base URL in the browser client | `http://192.168.1.50:5153/api/v1` | - -> **Note:** `VITE_API_BASE_URL` is only needed if you want the browser to call the API -> **directly** (bypassing the proxy). With the default proxy configuration you do -> not need to set this. - ---- - -## Troubleshooting - -### "Connection Refused" on port 3000 - -1. Confirm Vite started with `--host 0.0.0.0`: - ```bash - grep "host" /tmp/servicehub_ui_startup.log - ``` -2. Check the firewall rules (see above). -3. Verify the process is listening: - ```bash - ss -tlnp | grep 3000 - ``` - -### "Network Error" / API Can't Be Reached - -The browser is talking to the Vite proxy on port 3000, which forwards to the API -on port 5153 (loopback). If you see API errors: - -1. Check the API is running: - ```bash - curl http://localhost:5153/health - ``` -2. Check API logs: - ```bash - tail -50 /tmp/servicehub_api_startup.log - ``` - -### CORS Errors in Browser Dev Tools - -If you see `Access-Control-Allow-Origin` errors, it means requests are bypassing -the Vite proxy and hitting the API directly. Ensure: - -- `VITE_API_BASE_URL` is **not** set to an absolute URL pointing at a different - host. Clear it or leave it unset to use the proxy. -- The browser is loading the UI from the same origin it's making API calls to. - ---- - -## Production Deployment - -For a production Linux deployment (no Vite dev server), build the React app and -serve it from the .NET API's wwwroot folder: - -```bash -# Build the React app (outputs to services/api/src/ServiceHub.Api/wwwroot) -cd apps/web -npm run build - -# Run the .NET API (it serves both the SPA and the API) -cd services/api -dotnet run --project src/ServiceHub.Api/ServiceHub.Api.csproj \ - --urls "http://0.0.0.0:5153" \ - --environment Production -``` - -Then open `http://linuxhost:5153` โ€” only one port needed in production. diff --git a/docs/PERMISSIONS.md b/docs/PERMISSIONS.md deleted file mode 100644 index a99126f..0000000 --- a/docs/PERMISSIONS.md +++ /dev/null @@ -1,86 +0,0 @@ -# ServiceHub โ€” Azure Permissions Guide - -ServiceHub requires appropriate permissions to access your Azure Service Bus resources. This guide explains the permissions needed for different features. - ---- - -## Recommended Setup: Shared Access Policy - -**For full ServiceHub functionality, create a dedicated Shared Access Policy with:** -- โœ… **Manage** permission -- โœ… **Send** permission -- โœ… **Listen** permission - -**How to create:** -1. Go to Azure Portal โ†’ Your Service Bus Namespace -2. Navigate to **Shared Access Policies** -3. Click **+ Add** -4. Name: `ServiceHub-FullAccess` (or your preferred name) -5. Check: โœ… **Manage**, โœ… **Send**, โœ… **Listen** -6. Click **Create** -7. Copy the **Primary Connection String** - -โš ๏ธ **Do NOT use RootManageSharedAccessKey** โ€” Always create a dedicated policy for ServiceHub. - ---- - -## Alternative: Limited Permissions (Read-Only) - -If you only need to browse messages without replay or testing capabilities: - -**Required Role (using Azure RBAC):** -- `Azure Service Bus Data Receiver` - -**Or create a Shared Access Policy with:** -- โœ… **Listen** permission only - -**Permissions Granted:** -- โœ… Peek/browse messages from queues and subscriptions -- โœ… View message metadata, properties, and bodies -- โœ… View queue and topic metrics -- โŒ Cannot replay messages from DLQ -- โŒ Cannot create test DLQ messages - ---- - -## Feature-Specific Requirements - -### ๐Ÿ” Read-Only Investigation - -**Permissions Required:** -- Listen (peek messages) - -**What You Can Do:** -- Browse active and dead-letter queue messages -- View message details and properties -- Search and filter messages -- View queue/topic metrics - -### ๐Ÿ”„ Replay Messages from DLQ - -**Permissions Required:** -- Listen (read from DLQ) -- Send (write to active queue) - -**What You Can Do:** -- All read-only features -- Move messages from DLQ back to main queue - -### ๐Ÿงช Create Test DLQ Messages - -**Permissions Required:** -- Listen (read from queue) -- Send (move messages to DLQ) - -**What You Can Do:** -- All read-only features -- Manually dead-letter messages for testing - -### ๐Ÿ› ๏ธ Full Management - -**Permissions Required:** -- Manage (full control) - -**What You Can Do:** -- All features above -- Future management operations \ No newline at end of file diff --git a/docs/SCREENSHOTS.md b/docs/SCREENSHOTS.md deleted file mode 100644 index 2695438..0000000 --- a/docs/SCREENSHOTS.md +++ /dev/null @@ -1,331 +0,0 @@ -# ServiceHub Screenshots Guide - -This guide explains the 35 screenshots used in the README and how they demonstrate ServiceHub capabilities. - ---- - -## ๐Ÿ“ธ Screenshot Organization - -All screenshots are located in `docs/screenshots/` with descriptive filenames that indicate their purpose and placement in the documentation. - -### Screenshot Mapping - -| # | Filename | Shows | Used In Section | -|---|----------|-------|-----------------| -| 1 | 01-Start-The-App.png | Application startup | Quick Start | -| 2 | 02-Connect-Service-Bus-With-Manage-ConnStr.png | Connection form with connection string | Quick Start | -| 3 | 03-Connected-ServiceBus.png | Connected namespace view | Quick Start | -| 4 | 04-feature-message-browser-empty.png | Message browser (empty) | Features | -| 5 | 05-main-message-display1.png | Message browser with loaded messages | Features - Message Browser | -| 6 | 06-feature-message-generator.png | Message generator basic form | Testing Tools | -| 7 | 07-feature-message-generator-basic-single-message.png | Single message generator | Testing Tools | -| 8 | 08-feature-message-generator-scenarios.png | Generator scenarios selection | Testing Tools | -| 9 | 09-feature-send-message.png | Send message form | Testing Tools | -| 10 | 10-message-display.png | Message details view | Features - Message Details | -| 11 | 11-Generate-Single-Message-Topic.png | Generate message to topic | Testing Tools | -| 12 | 12-showing-message-topic.png | Topic messages display | Features | -| 13 | 13-feature-message-details-custom-props.png | Custom properties tab | Features - Message Details | -| 14 | 14-feature-ai-findings.png | AI findings indicator | Features - AI Insights | -| 15 | 15-feature-message-details-JSON-prop.png | Message body JSON | Features - Message Details | -| 16 | 16-feature-message-details-AI-Insight.png | AI insights in message details | Features - AI Insights | -| 17 | 17-feature-ai-patterns-popup.png | AI patterns summary popup | Features - AI Insights | -| 18 | 18-feature-dlq-tab-with-ai.png | DLQ tab with AI indicators | Features - DLQ | -| 19 | 19-feature-ai-findings-1.png | AI findings detection | Features - AI Insights | -| 20 | 20-workflow-dlq-investigation-step1.png | DLQ investigation step 1 | Workflows - DLQ | -| 21 | 21-workflow-dlq-investigation-step1.png | DLQ investigation detailed | Workflows - DLQ | -| 22 | 22-workflow-dlq-investigation-step2.png | DLQ AI insights view | Workflows - DLQ | -| 23 | 23-workflow-dlq-AI-Insight.png | DLQ AI guidance | Workflows - DLQ | -| 24 | 24-workflow-dlq-replay-step4.png | Replay confirmation dialog | Workflows - DLQ | -| 25 | 25-feature-find-feature.png | Advanced search functionality | Features - Search | -| 26 | 26-row-ui-new-feature.png | Enhanced message row UI | Features - Message Browser | -| 27 | 27-dlq-enhancement.png | DLQ enhancements | Features - DLQ Intelligence | -| 28 | 28-dlq-intelligence.png | DLQ Intelligence dashboard | Features - DLQ Intelligence | -| 29 | 29-dlq-history-post-replay-message.png | DLQ history after single replay | Features - DLQ Intelligence | -| 30 | 30-auto-replay-feature.png | Auto-Replay Rules page with rule cards | Features - Auto-Replay System | -| 31 | 31-auto-relay-test-feature.png | Rule test dialog with matched messages | Features - Auto-Replay System | -| 32 | 32-replay-all-messages.png | Replay All confirmation with warnings | Features - Batch Replay | -| 33 | 33-replay-all-process.png | Batch replay in progress | Features - Batch Replay | -| 34 | 34-post-replay-all-messages.png | Results after batch replay | Features - Batch Replay | -| 35 | 35-rdlq-intelligence-history-post-replay-all.png | DLQ history audit trail after bulk replay | Features - DLQ Intelligence | - -**Total screenshots: 35 files** - ---- - -## ๐ŸŽฏ Key Screenshots Explained - -### DLQ Intelligence System (Screenshots 26-35) - -**New Feature Area:** DLQ Intelligence & Auto-Replay System - -#### Enhanced Message UI (Screenshot 26) -**File:** `26-row-ui-new-feature.png` -- Improved message list visual hierarchy -- Better property visibility -- Enhanced spacing and readability - -#### DLQ Intelligence Dashboard (Screenshots 27-28) -**Files:** `27-dlq-enhancement.png`, `28-dlq-intelligence.png` -- Persistent DLQ message tracking in SQLite database -- Category classification (Transient, MaxDelivery, Expired, DataQuality, etc.) -- "Scan Now" button for instant DLQ polling -- Export capabilities (JSON/CSV) -- Timeline view with replay history - -#### Replay History Tracking (Screenshot 29) -**File:** `29-dlq-history-post-replay-message.png` -- Shows DLQ message status after replay -- Timestamps and outcome tracking -- Audit trail for compliance - -#### Auto-Replay Rules System (Screenshots 30-31) -**Files:** `30-auto-replay-feature.png`, `31-auto-relay-test-feature.png` -- Rule cards with live statistics (Pending/Replayed/Success) -- Conditions builder (field, operator, value) -- "Test" button to preview matched messages -- Real-time evaluation against Active DLQ messages - -#### Batch Replay All (Screenshots 32-35) -**Files:** `32-replay-all-messages.png`, `33-replay-all-process.png`, `34-post-replay-all-messages.png`, `35-rdlq-intelligence-history-post-replay-all.png` -- Confirmation dialog with red danger header and 3 safety warnings -- Shows matched message count before execution -- Real-time progress indicator -- Post-replay statistics (matched/replayed/failed/skipped) -- Complete audit trail in DLQ Intelligence history - ---- - -### Hero Image (Most Important) -**File:** `05-main-message-display1.png` - -**Shows:** -- ServiceHub message browser with loaded messages -- Active (50) and Dead-Letter (0) tabs -- AI Findings: 2 indicator -- Auto-refresh toggle (ON, 7s ago) -- Filter, Search, and Refresh buttons -- Message list with preview text and timestamps - -**Why it's the hero:** This single screenshot demonstrates ServiceHub's complete value proposition - browsing real messages with AI insights during an incident. - -**Used in 7+ sections:** -- Hero (top of README) -- Problem section (after state) -- Quick Start Step 4 -- Features - Message Browser -- Roadmap current capabilities -- Get Started CTA -- Comparison sections - ---- - -### Critical Workflow Screenshots - -#### DLQ Investigation (4-step workflow) -1. **Step 1:** `18-feature-dlq-tab-with-ai.png` - Shows 3 messages in DLQ with AI badges -2. **Step 2:** `19-workflow-dlq-investigation-step1.png` - DLQ message Properties showing TestingDLQ reason -3. **Step 3:** `20-workflow-dlq-investigation-step2.png` - AI Insights showing 88% confidence pattern -4. **Step 4:** `21-workflow-dlq-replay-step3.png` - Replay confirmation dialog -5. **Step 5:** `22-workflow-dlq-replay-step4.png` - After replay (message counts increased) - -#### Message Inspection (3-tab view) -1. **Properties:** `11-feature-message-details-properties.png` - Message ID, timestamps, delivery count -2. **Custom Props:** `12-feature-message-details-custom-props.png` - Application headers -3. **Body:** `13-feature-message-details-body.png` - JSON message content - ---- - -## ๐Ÿ“ README Structure & Screenshot Usage - -### Hero Section (Line 1-15) -- **1 screenshot:** `08-hero-message-browser-loaded.png` -- **Purpose:** Immediate visual impact showing full capabilities - -### The Problem (Line 32-75) -- **2 screenshots:** - - `01-problem-empty-state.png` (Azure Portal limitations) - - `08-hero-message-browser-loaded.png` (ServiceHub solution) -- **Purpose:** Visual before/after comparison - -### Quick Start (Line 77-125) -- **5 screenshots:** - - `02-quickstart-connection-form.png` (Step 3) - - `03-quickstart-connected-namespace.png` (Step 3) - - `08-hero-message-browser-loaded.png` (Step 4) - - `07-feature-message-generator-scenarios.png` (Step 4) -- **Purpose:** Visual step-by-step setup guide - -### Key Features (Line 127-550) -- **15+ screenshots** demonstrating: - - Message browser (`08-hero...`) - - Message details (3 tabs: `11`, `12`, `13`) - - Search (`23-feature-search...`) - - DLQ forensics (`18`, `24`) - - AI insights (4 screenshots: `14`, `15`, `16`, `17`) - - Message generator (`06`, `07`) - - Send message (`09`) - - Replay (`21`, `22`) - -### Investigation Workflows (Line 552-650) -- **8 screenshots** showing: - - DLQ investigation (5 screenshots) - - Topic delivery (`10-workflow...`) - - Search functionality (`23-feature...`) - -### Comparison (Line 652-750) -- **2 screenshots:** - - `01-problem-empty-state.png` (Portal) - - `08-hero-message-browser-loaded.png` (ServiceHub) - -### Security & Trust (Line 752-850) -- **1 screenshot:** - - `02-quickstart-connection-form.png` (Security guidance) - -### Roadmap (Line 900-950) -- **1 screenshot:** - - `08-hero-message-browser-loaded.png` (Current capabilities) - -### Get Started (Line 1000-1100) -- **1 screenshot:** - - `08-hero-message-browser-loaded.png` (CTA) - ---- - -## โœ… Verification Checklist - -Use this to verify screenshots display correctly on GitHub: - -- [ ] **Hero image** loads immediately at top -- [ ] **Problem section** shows before/after comparison -- [ ] **Quick Start** shows connection flow (4 steps) -- [ ] **Features** demonstrates each capability with real screenshot -- [ ] **DLQ workflow** shows clear 4-step progression -- [ ] **AI insights** displays pattern detection screenshots -- [ ] **Message details** shows all 3 tabs (Properties, Custom, Body) -- [ ] **Comparison** shows Portal vs ServiceHub side-by-side -- [ ] All images load in <2 seconds -- [ ] No broken image icons (๐Ÿ–ผ๏ธโŒ) - ---- - -## ๐Ÿ”ง Maintenance - -### Adding New Screenshots - -1. Take screenshot of new feature -2. Save to `docs/screenshots/` with naming convention: - - Format: `XX-category-description.png` - - Categories: `feature`, `workflow`, `comparison`, `security` - - Example: `25-feature-bulk-replay.png` - -3. Update README.md with new image reference: - ```markdown - ![Descriptive alt text](docs/screenshots/25-feature-bulk-replay.png) - *Caption explaining what screenshot shows* - ``` - -4. Commit and push: - ```bash - git add docs/screenshots/25-feature-bulk-replay.png README.md - git commit -m "docs: Add bulk replay feature screenshot" - git push origin main - ``` - -### Updating Existing Screenshots - -1. Replace file in `docs/screenshots/` (keep same filename) -2. Commit and push: - ```bash - git add docs/screenshots/08-hero-message-browser-loaded.png - git commit -m "docs: Update hero image with latest UI" - git push origin main - ``` - -3. GitHub will automatically display updated image - ---- - -## ๐Ÿ“Š Screenshot Statistics - -| Metric | Value | -|--------|-------| -| **Total screenshots** | 35 files | -| **Most used screenshot** | `05-main-message-display1.png` (message browser) | -| **Total size** | ~6.8 MB (estimated) | -| **Average file size** | ~195 KB | -| **Largest file** | ~350 KB | -| **Smallest file** | ~85 KB | -| **Recommended max size** | 500 KB per file | - ---- - -## ๐ŸŽจ Screenshot Best Practices - -### When Taking Screenshots - -1. **Full window** (not partial crops) -2. **Realistic data** (show 10-50 messages, not 1000s) -3. **No sensitive data** (use test namespaces, generated messages) -4. **Consistent resolution** (1200-1600px width) -5. **Clean UI** (close unnecessary panels) - -### File Format - -- **Format:** PNG (for crisp UI text) -- **Compression:** Use pngquant (65-80% quality) -- **Max size:** 500 KB per file -- **Naming:** Descriptive (`feature-name.png`, not `Screenshot 1.png`) - -### Alt Text - -Write descriptive alt text for accessibility: - -```markdown - -![ServiceHub message browser showing 50 active messages with AI findings indicator](...) - - -![Screenshot](...) -![Message browser](...) -``` - ---- - -## ๐Ÿš€ Quick Commands - -```bash -# View all screenshots -ls -lh docs/screenshots/ - -# Count screenshots -ls -1 docs/screenshots/ | wc -l - -# Check file sizes -ls -lh docs/screenshots/*.png | sort -k5 -hr - -# Find large files (>500KB) -find docs/screenshots -name "*.png" -size +500k - -# Optimize all screenshots -cd docs/screenshots -for img in *.png; do - pngquant --quality=65-80 --ext .png --force "$img" -done -``` - ---- - -## ๐ŸŒ View on GitHub - -**Repository:** https://github.com/debdevops/servicehub - -**Branch:** dglocal-110126 - -**README:** https://github.com/debdevops/servicehub/blob/dglocal-110126/README.md - -All 24 screenshots should display inline without broken image icons. - ---- - -**Last updated:** February 15, 2026 -**Screenshots version:** v2.0 (DLQ Intelligence & Auto-Replay System added) diff --git a/docs/screenshots/01-connect-page.png b/docs/screenshots/01-connect-page.png new file mode 100644 index 0000000000000000000000000000000000000000..2be589c0588f1df1f72fb9cab888f1d81e4f1b43 GIT binary patch literal 504732 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�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