Skip to content

Commit 7f7a37e

Browse files
committed
chore: close OSS-launch gaps — docs accuracy, issue templates, neutral copy, message deep links
- CHANGELOG: correct the stale [Unreleased] entry claiming DLQ monitoring is Azure-only; DlqMonitorWorker scans all providers and DlqMonitorService skips only unregistered ones. - README: surface the security posture (read-only default, AES-GCM at rest, zero external calls, prod safety rails) as a callout at the top; replace stale test counts with resilient rounded figures. - Add .github/ISSUE_TEMPLATE (bug report, feature request, config) so the templates CONTRIBUTING already references actually exist; security reports routed to SECURITY.md via contact link. - ConnectPage: provider-neutral marketing copy (hero, empty state, how-it-works, comparison table) — Azure-specific text stays only in the Azure form and SAS instructions. No functional changes. - MessagesPage: message-level deep links — selection is initialized from and mirrored to a ?message=<id> URL parameter so a specific message can be shared; cleared on tab/entity switch with a first-render guard so deep links survive page load. 4 new tests. Frontend: 1,106 tests green, tsc clean. Backend untouched.
1 parent 271b655 commit 7f7a37e

8 files changed

Lines changed: 205 additions & 18 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Bug report
2+
description: Something in ServiceHub isn't working as expected
3+
title: "[Bug]: "
4+
labels: ["bug", "triage"]
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
Thanks for taking the time to report a bug. Before submitting, please search
10+
[existing issues](https://github.com/debdevops/servicehub/issues) to avoid duplicates.
11+
12+
⚠️ **Security vulnerabilities:** do NOT report them here — see [SECURITY.md](https://github.com/debdevops/servicehub/blob/main/SECURITY.md) for responsible disclosure.
13+
- type: textarea
14+
id: what-happened
15+
attributes:
16+
label: What happened?
17+
description: A clear description of the bug, and what you expected to happen instead.
18+
placeholder: "When I click Replay on a DLQ message, the request fails with..."
19+
validations:
20+
required: true
21+
- type: textarea
22+
id: steps
23+
attributes:
24+
label: Steps to reproduce
25+
placeholder: |
26+
1. Connect a namespace with provider '...'
27+
2. Open the Dead-Letter tab on queue '...'
28+
3. Click '...'
29+
4. See error
30+
validations:
31+
required: true
32+
- type: dropdown
33+
id: provider
34+
attributes:
35+
label: Cloud provider
36+
description: Which provider does this affect?
37+
options:
38+
- Azure Service Bus
39+
- AWS SQS/SNS
40+
- GCP Pub/Sub
41+
- Simulator mode
42+
- Not provider-specific
43+
validations:
44+
required: true
45+
- type: dropdown
46+
id: mode
47+
attributes:
48+
label: Run mode
49+
options:
50+
- "./run.sh (development, real credentials)"
51+
- "./run.sh --simulator"
52+
- Self-hosted deployment
53+
- Other
54+
validations:
55+
required: true
56+
- type: input
57+
id: version
58+
attributes:
59+
label: ServiceHub version
60+
description: From the `.version` file or the version badge in the UI footer.
61+
placeholder: "3.3.0"
62+
validations:
63+
required: true
64+
- type: textarea
65+
id: environment
66+
attributes:
67+
label: Environment
68+
description: OS, .NET SDK version (`dotnet --version`), Node version (`node --version`), browser.
69+
placeholder: |
70+
OS: macOS 15.5
71+
.NET: 10.0.100
72+
Node: 22.11.0
73+
Browser: Chrome 138
74+
validations:
75+
required: true
76+
- type: textarea
77+
id: logs
78+
attributes:
79+
label: Relevant logs
80+
description: |
81+
API console output or browser console errors.
82+
⚠️ **Redact any connection strings, keys, or secrets before pasting.**
83+
render: shell

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
blank_issues_enabled: true
2+
contact_links:
3+
- name: 🔒 Report a security vulnerability
4+
url: https://github.com/debdevops/servicehub/blob/main/SECURITY.md
5+
about: Do NOT open a public issue for security vulnerabilities — follow the responsible disclosure process instead.
6+
- name: 💬 Questions & help
7+
url: https://github.com/debdevops/servicehub/discussions
8+
about: Ask questions or discuss ideas before opening an issue.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Feature request
2+
description: Suggest an idea or improvement for ServiceHub
3+
title: "[Feature]: "
4+
labels: ["enhancement", "triage"]
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
Thanks for the suggestion! Please describe the **use-case, not just the solution** —
10+
understanding the problem you're solving helps us design the right feature.
11+
- type: textarea
12+
id: problem
13+
attributes:
14+
label: What problem are you trying to solve?
15+
description: Describe the situation where you hit this limitation. What were you doing? What slowed you down?
16+
placeholder: "During an incident I needed to replay ~500 DLQ messages matching a specific error, but..."
17+
validations:
18+
required: true
19+
- type: textarea
20+
id: proposal
21+
attributes:
22+
label: Proposed solution
23+
description: How would you like ServiceHub to solve it? Rough ideas are fine.
24+
validations:
25+
required: true
26+
- type: dropdown
27+
id: provider
28+
attributes:
29+
label: Which provider(s) does this involve?
30+
multiple: true
31+
options:
32+
- Azure Service Bus
33+
- AWS SQS/SNS
34+
- GCP Pub/Sub
35+
- Simulator mode
36+
- All / not provider-specific
37+
- type: textarea
38+
id: alternatives
39+
attributes:
40+
label: Alternatives you've considered
41+
description: Workarounds you use today (scripts, portal, CLI, other tools).

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
- **AWS and GCP now have resilience pipelines matching Azure's retry pattern**`AwsResiliencePipeline` and `GcpResiliencePipeline` (Polly-based, 3 retries, exponential backoff with jitter, 1s base / 30s cap) wrap the AWS SQS/SNS and GCP Pub/Sub message receivers/senders, retrying only transient errors (AWS: SDK-marked retryable or 5xx/429; GCP: `Unavailable`/`DeadlineExceeded`/`Internal`/`ResourceExhausted`/`Aborted` gRPC codes).
1717
- **GCP Pub/Sub message count normalized**`GcpMessageReceiver.GetMessageCountAsync` previously could surface as a failure; Pub/Sub has no direct count API, so it now returns a neutral `Success(0)`, consistent with the existing "unsupported read" convention used elsewhere (e.g. `GetScheduledMessagesAsync`).
18-
- **DLQ background monitoring now skips non-Azure namespaces explicitly**`DlqMonitorWorker` filters to Azure namespaces before scanning (DLQ monitoring is Azure-only today) and logs how many namespaces were skipped, instead of attempting and failing an Azure client build against AWS/GCP namespaces every poll cycle.
18+
- **DLQ background monitoring is provider-aware**`DlqMonitorWorker` scans all active namespaces regardless of provider; `DlqMonitorService` resolves each namespace's provider through `CloudProviderRouter`, skipping only namespaces whose provider is not registered on this server (with an explanatory log entry) instead of attempting an Azure client build against AWS/GCP namespaces. Provider conventions are handled per cloud: GCP `-dlq` dead-letter topic subscriptions, Azure `DeadLetterCount` short-circuiting.
1919

2020
### Changed
2121

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Production breaks at 2 AM. Your cloud portal shows **5,000 messages in the Dead-
2525

2626
**ServiceHub is an ultra-fast, self-hosted web application that gives engineers full forensic visibility into their cloud message queues** — like a debugger, but for Azure Service Bus, AWS SQS/SNS, and GCP Pub/Sub.
2727

28+
> [!IMPORTANT]
29+
> **Built for strict environments.** Read-only by default (`Peek`, never consume) · connection strings AES-GCM-256 encrypted at rest · **zero external calls** — pattern analysis runs entirely in your browser, no message data ever leaves your network · destructive actions (replay, send) blocked on production namespaces. Details in [Security](#security).
30+
2831
> [!TIP]
2932
> **No credentials?** Try the built-in [Simulator Mode](#simulator-mode) — runs 3 synthetic namespaces (Azure + AWS + GCP) with 50 seeded messages each. No cloud account needed.
3033
@@ -267,10 +270,10 @@ ServiceHub uses client-side heuristic pattern detection — pure JavaScript in y
267270
Bug fixes, features, and documentation improvements are welcome!
268271

269272
```bash
270-
# Unit tests (Vitest — 1,045 tests, ≥60% coverage required)
273+
# Unit tests (Vitest — 1,100+ tests, ≥60% coverage required)
271274
cd apps/web && npm run test:coverage
272275

273-
# Backend tests (xUnit — 1,327 tests)
276+
# Backend tests (xUnit — 1,500+ unit + integration tests)
274277
cd services/api && dotnet test
275278

276279
# E2E tests (Playwright — requires ./run.sh --simulator)

apps/web/src/__tests__/pages/MessagesPage.test.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ vi.mock('@/hooks/useNamespaces', () => ({
2424

2525
// Mock heavy components
2626
vi.mock('@/components/messages', () => ({
27-
MessageList: ({ messages, onQueueTabChange, activeCounts }: any) => (
27+
MessageList: ({ messages, onQueueTabChange, activeCounts, onSelectMessage, selectedId }: any) => (
2828
<div data-testid="message-list">
2929
<span>{messages.length} messages</span>
30+
<span data-testid="selected-id">{selectedId ?? 'none'}</span>
31+
<button onClick={() => onSelectMessage('msg-2')}>Select msg-2</button>
3032
<button onClick={() => onQueueTabChange('active')}>Active ({activeCounts.active})</button>
3133
<button onClick={() => onQueueTabChange('deadletter')}>Dead-Letter ({activeCounts.deadletter})</button>
3234
</div>
@@ -265,4 +267,33 @@ describe('MessagesPage', () => {
265267
render(<Wrapper><MessagesPage /></Wrapper>);
266268
expect(screen.getByTestId('message-list')).toBeInTheDocument();
267269
});
270+
271+
describe('Message deep links', () => {
272+
it('restores selection from the ?message= URL parameter on load', () => {
273+
const Wrapper = createWrapper('/messages?namespace=ns1&queue=test-queue&message=msg-1');
274+
render(<Wrapper><MessagesPage /></Wrapper>);
275+
expect(screen.getByTestId('selected-id')).toHaveTextContent('msg-1');
276+
});
277+
278+
it('shows no selection when the URL has no message parameter', () => {
279+
const Wrapper = createWrapper();
280+
render(<Wrapper><MessagesPage /></Wrapper>);
281+
expect(screen.getByTestId('selected-id')).toHaveTextContent('none');
282+
});
283+
284+
it('updates selection when a message is chosen', () => {
285+
const Wrapper = createWrapper();
286+
render(<Wrapper><MessagesPage /></Wrapper>);
287+
fireEvent.click(screen.getByText('Select msg-2'));
288+
expect(screen.getByTestId('selected-id')).toHaveTextContent('msg-2');
289+
});
290+
291+
it('clears the selection when switching queue tabs', () => {
292+
const Wrapper = createWrapper('/messages?namespace=ns1&queue=test-queue&message=msg-1');
293+
render(<Wrapper><MessagesPage /></Wrapper>);
294+
expect(screen.getByTestId('selected-id')).toHaveTextContent('msg-1');
295+
fireEvent.click(screen.getByText(/Dead-Letter \(/));
296+
expect(screen.getByTestId('selected-id')).toHaveTextContent('none');
297+
});
298+
});
268299
});

apps/web/src/pages/ConnectPage.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import toast from 'react-hot-toast';
1212
/**
1313
* Connection Setup Page
1414
*
15-
* Onboarding-style screen for connecting to Azure Service Bus namespaces.
15+
* Onboarding-style screen for connecting cloud messaging namespaces (Azure Service Bus, AWS SQS/SNS, GCP Pub/Sub).
1616
*/
1717
export function ConnectPage() {
1818
const navigate = useNavigate();
@@ -244,7 +244,7 @@ export function ConnectPage() {
244244
<span className="text-xs font-semibold text-sky-700">Free · Open Source · No installation</span>
245245
</div>
246246
<h1 className="text-2xl font-bold text-gray-900 leading-tight">
247-
Debug Azure Service Bus{' '}
247+
Debug your cloud message queues{' '}
248248
<span className="text-primary-600">in seconds.</span>
249249
</h1>
250250
<p className="text-sm text-gray-500 mt-1">
@@ -816,7 +816,7 @@ export function ConnectPage() {
816816
<span className="text-2xl">📭</span>
817817
</div>
818818
<h3 className="font-medium text-gray-900 mb-1">No saved connections yet</h3>
819-
<p className="text-sm text-gray-500">Add your first Service Bus to get started</p>
819+
<p className="text-sm text-gray-500">Connect your first namespace to get started</p>
820820
</div>
821821
)}
822822
</div>
@@ -885,7 +885,7 @@ export function ConnectPage() {
885885
<span className="text-sm font-bold text-primary-600">1</span>
886886
</div>
887887
<p className="text-sm font-semibold text-gray-900">60 seconds to your first message view</p>
888-
<p className="text-xs text-gray-500 mt-1.5">Paste a Listen-only connection string — no admin rights, no Azure Portal clutter, no SDK to install</p>
888+
<p className="text-xs text-gray-500 mt-1.5">Paste read-only credentials — no admin rights, no cloud console clutter, no SDK to install</p>
889889
</div>
890890
<div className="text-center p-5 bg-white rounded-xl border border-gray-200 shadow-sm">
891891
<div className="w-9 h-9 bg-primary-50 border border-primary-100 rounded-full flex items-center justify-center mx-auto mb-3">
@@ -918,7 +918,7 @@ export function ConnectPage() {
918918
<h4 className="font-semibold text-gray-900 mb-2">847 messages failing? Find the pattern in 30 seconds.</h4>
919919
<p className="text-xs text-gray-600">
920920
Group by error, spot the duplicate root cause (null ref, version mismatch, timeout), apply a fix, 1-click bulk replay.
921-
Azure Portal: 30 min. ServiceHub: 2 min.
921+
Cloud console: 30 min. ServiceHub: 2 min.
922922
</p>
923923
</div>
924924

@@ -953,7 +953,7 @@ export function ConnectPage() {
953953
<div className="bg-gradient-to-br from-gray-50 to-white rounded-xl border border-gray-200 p-6">
954954
<h4 className="text-sm font-bold text-gray-800 mb-4 flex items-center gap-2">
955955
<span className="text-base"></span>
956-
ServiceHub vs Azure Portal
956+
ServiceHub vs Cloud Consoles
957957
</h4>
958958
<div className="space-y-3 text-sm">
959959
{[
@@ -971,7 +971,7 @@ export function ConnectPage() {
971971
))}
972972
<div className="flex items-center text-[10px] text-gray-400 pt-1 border-t border-gray-100">
973973
<span className="flex-1" />
974-
<span className="w-24 text-center">Azure Portal</span>
974+
<span className="w-24 text-center">Cloud Console</span>
975975
<span className="w-24 text-center text-primary-600 font-semibold">ServiceHub</span>
976976
</div>
977977
</div>

apps/web/src/pages/MessagesPage.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo, useEffect, useDeferredValue } from 'react';
1+
import { useState, useMemo, useEffect, useRef, useDeferredValue } from 'react';
22
import { useSearchParams, useNavigate } from 'react-router-dom';
33
import { useQueryClient } from '@tanstack/react-query';
44
import { Search, Filter, RefreshCw, Sparkles, X, AlertCircle, Play, Pause } from 'lucide-react';
@@ -143,8 +143,21 @@ export function MessagesPage() {
143143
}
144144
}, [namespaces, namespaceId, searchParams, setSearchParams]);
145145

146-
// Selected message for detail panel
147-
const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null);
146+
// Selected message for detail panel — initialized from the URL so message
147+
// links ("?message=<id>") can be shared and restore the selection on load
148+
const [selectedMessageId, setSelectedMessageId] = useState<string | null>(
149+
() => searchParams.get('message')
150+
);
151+
152+
const syncMessageParam = (id: string | null) => {
153+
const newParams = new URLSearchParams(searchParams);
154+
if (id) {
155+
newParams.set('message', id);
156+
} else {
157+
newParams.delete('message');
158+
}
159+
setSearchParams(newParams, { replace: true });
160+
};
148161

149162
// Queue tab: active or deadletter (sync with URL parameter)
150163
const [queueTab, setQueueTab] = useState<QueueTab>('active');
@@ -166,8 +179,14 @@ export function MessagesPage() {
166179
}
167180
}, [queueTypeParam]);
168181

169-
// Clear selection when switching queues/topics to prevent stale detail panel
182+
// Clear selection when switching queues/topics to prevent stale detail panel.
183+
// Skipped on first render so a deep-linked "?message=<id>" selection survives page load.
184+
const isFirstEntityRender = useRef(true);
170185
useEffect(() => {
186+
if (isFirstEntityRender.current) {
187+
isFirstEntityRender.current = false;
188+
return;
189+
}
171190
setSelectedMessageId(null);
172191
}, [namespaceId, queueName, topicName, subscriptionName]);
173192

@@ -337,19 +356,21 @@ export function MessagesPage() {
337356
setPaginationState(prev => ({ ...prev, skip: prev.skip + BATCH_SIZE }));
338357
};
339358

340-
// Handle message selection
359+
// Handle message selection — mirrored into the URL so the link is shareable
341360
const handleSelectMessage = (id: string) => {
342361
setSelectedMessageId(id);
362+
syncMessageParam(id);
343363
};
344364

345365
// Handle queue tab change
346366
const handleQueueTabChange = (tab: QueueTab) => {
347367
setQueueTab(tab);
348368
setSelectedMessageId(null); // Clear selection when switching tabs
349-
350-
// Update URL to keep it in sync with tab state
369+
370+
// Update URL to keep it in sync with tab state (and drop the stale message link)
351371
const newParams = new URLSearchParams(searchParams);
352372
newParams.set('queueType', tab);
373+
newParams.delete('message');
353374
setSearchParams(newParams, { replace: true });
354375

355376
// Refresh counts when switching between active/dlq tabs

0 commit comments

Comments
 (0)