Skip to content

Commit e0a91fb

Browse files
committed
ux(first-touch): welcome email on org.created + Help link in nav
Two small but visible fixes for the brand-new-customer experience. Empty-state dashboard already had a 3-step welcome hero, but the two complementary gaps were: 1. Welcome email on org creation ────────────────────────────── The Clerk `organization.created` webhook was being processed but emitted nothing — no email, no inbox notification. First-touch silence ("did anything happen?" gap). New `welcome` notification kind: - Inbox setting key `welcome_notifications` (default ON) - Email setting key `email_welcome` (default ON, no UI today — operators flip via direct DB write to suppress on internal test orgs) - Audience `admin` — resolves to the org creator (who's auto-added as the first admin by Clerk) - Severity `info` (green bar in email layout) - Linked to `/dashboard` so the natural next click is the workspace itself Templates: - `welcome.subject.txt.j2` — friendly subject mentioning the three-step path so even unread inbox previews are useful - `welcome.body.txt.j2` — plain-text body with copy-pasteable install one-liner + dashboard URL - `welcome.body.html.j2` — branded layout, three-step rounded card, single primary CTA to /dashboard Webhook handler (`organization.created` branch in webhooks.py): wraps the create_notification call in try/except so a transient Clerk recipient-lookup outage doesn't cause Svix to retry the webhook and double-email on each retry. 2. Help link in the authenticated app header ──────────────────────────────────────── Layout.jsx had Dashboard / Settings / Admin / MCP / Sentinel / Pricing — `/docs` was reachable only by direct URL or via the landing page. Added "Help" between Sentinel and Pricing (matches the secondary-nav cluster other SaaS apps use). Skipped the support `mailto:` for now since support.sourceboxsentry.com isn't provisioned — adding a bounce-bound link would be worse than no link at all. Tests (6 new in tests/test_welcome_email.py): - inbox row written on org.created - EmailOutbox row enqueued with templates rendered + recipient resolved via the stubbed Clerk call - global EMAIL_ENABLED kill-switch off → inbox written, email skipped (matches every other kind) - per-org `email_welcome=false` → inbox written, email skipped (the operator-suppression path) - create_notification raising → webhook still returns 200 (Svix must not retry forever on a partial failure) - template content guard — three-step CloudNode + camera mention survives in both text + HTML bodies (catches a refactor that silently strips the actionable content) Total: 484 passed (was 478, +6), ruff clean, frontend builds.
1 parent 280c47c commit e0a91fb

7 files changed

Lines changed: 431 additions & 0 deletions

File tree

backend/app/api/notifications.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
"member_added": ("member_audit_notifications", True),
8080
"member_role_changed": ("member_audit_notifications", True),
8181
"member_removed": ("member_audit_notifications", True),
82+
# First-touch onboarding — fired once when an org is created.
83+
# Has its own setting key (no UI toggle today) so a future
84+
# "marketing email opt-out" change has somewhere to land
85+
# without conflating with the operational kinds above. Default
86+
# True because welcome is a one-time event, not a stream.
87+
"welcome": ("welcome_notifications", True),
8288
}
8389

8490

@@ -150,6 +156,12 @@
150156
# event volume.
151157
"motion": ("email_motion", False),
152158
"motion_digest": ("email_motion", False),
159+
# First-touch welcome on org creation. Own setting key with no UI
160+
# toggle today — operators can flip via direct DB write if they
161+
# need to suppress (e.g. an internal test org). Default True
162+
# because the whole point of the kind is to introduce the product;
163+
# an opt-in default would make the gap we're closing reappear.
164+
"welcome": ("email_welcome", True),
153165
}
154166
# Note: ``disk_critical`` is intentionally NOT in this map. Disk-full
155167
# is platform infrastructure state (our Fly volume) — irrelevant to

backend/app/api/webhooks.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,48 @@ async def clerk_webhook(request: Request, db: Session = Depends(get_db)):
323323
org_id,
324324
)
325325

326+
# ── Organization created ──────────────────────────────────────
327+
# First-touch welcome — fires once when a user creates an org via
328+
# Clerk's CreateOrganization modal (on signup or later). The
329+
# creator is automatically the org's first admin, so the
330+
# ``audience="admin"`` recipient resolution lands the email on
331+
# them. Wrapped in try/except — a recipient-lookup race or a
332+
# template-render bug must NOT cause Svix to retry the webhook
333+
# forever (the org IS created either way; we'd just keep
334+
# double-emailing on each retry).
335+
elif event_type == "organization.created":
336+
org_id = data.get("id")
337+
org_name = data.get("name") or "your organization"
338+
if org_id:
339+
try:
340+
from app.api.notifications import create_notification
341+
create_notification(
342+
org_id=org_id,
343+
kind="welcome",
344+
title=f"Welcome to SourceBox Sentry, {org_name}",
345+
body=(
346+
"Your SourceBox Sentry workspace is ready. "
347+
"Three steps to your first live feed: install "
348+
"CloudNode on the host where your cameras live, "
349+
"wait ~30 seconds for it to register, then add "
350+
"your first camera from Settings → Cameras. "
351+
"Full docs at /docs."
352+
),
353+
severity="info",
354+
audience="admin",
355+
link="/dashboard",
356+
meta={
357+
"org_name": org_name,
358+
"created_by": data.get("created_by"),
359+
},
360+
db=db,
361+
)
362+
except Exception:
363+
logger.exception(
364+
"[ClerkWebhook] welcome notification failed for org=%s",
365+
org_id,
366+
)
367+
326368
# ── Organization deleted ───────────────────────────────────────
327369
elif event_type == "organization.deleted":
328370
org_id = data.get("id")
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<h2 style="margin:0 0 16px;font-size:22px;font-weight:600;color:#111;line-height:1.3;">
2+
Welcome to SourceBox Sentry
3+
</h2>
4+
5+
<p style="margin:0 0 20px;font-size:15px;line-height:1.6;color:#374151;">
6+
Your workspace is ready. This is the command center — a
7+
self-hosted-camera-feeds dashboard, AI-aware via the MCP
8+
integration, with motion detection + incident reports baked in.
9+
</p>
10+
11+
<p style="margin:0 0 12px;font-size:14px;color:#6b7280;font-weight:600;letter-spacing:0.04em;text-transform:uppercase;">
12+
Three steps to your first live feed
13+
</p>
14+
15+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px;width:100%;">
16+
<tr>
17+
<td style="padding:14px 16px;background:#f9fafb;border-radius:6px 6px 0 0;border-bottom:1px solid #e5e5e9;font-size:14px;line-height:1.6;color:#374151;">
18+
<strong style="color:#111;">1. Install CloudNode</strong> on the machine where
19+
your cameras live. One-liner for Linux/macOS:
20+
<pre style="margin:8px 0 0;padding:10px 12px;background:#0a0a0f;color:#22c55e;border-radius:4px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;overflow-x:auto;">curl -fsSL {{ dashboard_url }}/install.sh | bash</pre>
21+
</td>
22+
</tr>
23+
<tr>
24+
<td style="padding:14px 16px;background:#f9fafb;border-bottom:1px solid #e5e5e9;font-size:14px;line-height:1.6;color:#374151;">
25+
<strong style="color:#111;">2. Wait ~30 seconds.</strong> CloudNode auto-registers with
26+
your org and shows up under Settings → CloudNode panel.
27+
</td>
28+
</tr>
29+
<tr>
30+
<td style="padding:14px 16px;background:#f9fafb;border-radius:0 0 6px 6px;font-size:14px;line-height:1.6;color:#374151;">
31+
<strong style="color:#111;">3. Add your first camera</strong> (RTSP / ONVIF / HTTP
32+
MJPEG) from Settings → Cameras. The live grid populates
33+
immediately.
34+
</td>
35+
</tr>
36+
</table>
37+
38+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 20px;">
39+
<tr>
40+
<td style="background:#22c55e;border-radius:6px;">
41+
<a href="{{ dashboard_url }}/dashboard"
42+
style="display:inline-block;padding:12px 22px;font-size:15px;font-weight:600;color:#0a0a0f;text-decoration:none;">
43+
Open dashboard →
44+
</a>
45+
</td>
46+
</tr>
47+
</table>
48+
49+
<p style="margin:0 0 8px;font-size:14px;color:#6b7280;font-weight:600;">
50+
Need a hand?
51+
</p>
52+
<p style="margin:0;font-size:14px;line-height:1.6;color:#374151;">
53+
The full documentation lives at
54+
<a href="{{ dashboard_url }}/docs" style="color:#22c55e;text-decoration:none;">{{ dashboard_url }}/docs</a>.
55+
Start with <em>Getting Started</em> or jump straight to <em>CloudNode setup</em>
56+
if you've already got the install running.
57+
</p>
58+
59+
<p style="margin:24px 0 0;font-size:13px;color:#9ca3af;line-height:1.5;">
60+
This is a one-time welcome. No recurring marketing email — only
61+
operational alerts you've opted into.
62+
</p>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{{ notification.title }}
2+
3+
Your SourceBox Sentry workspace is ready. This is the command
4+
center — a self-hosted-camera-feeds dashboard, AI-aware via the
5+
MCP integration, with motion detection + incident reports baked in.
6+
7+
Three steps to your first live feed:
8+
9+
1. Install CloudNode on the machine where your cameras live.
10+
One-liner for Linux/macOS:
11+
curl -fsSL {{ dashboard_url }}/install.sh | bash
12+
Windows: download the installer from
13+
{{ dashboard_url }}/downloads/windows/x64
14+
15+
2. The CloudNode auto-registers with your org and shows up under
16+
Settings → CloudNode panel within ~30 seconds of starting.
17+
18+
3. Add your first camera (RTSP / ONVIF / HTTP MJPEG) from
19+
Settings → Cameras. The live grid populates immediately.
20+
21+
Open the dashboard:
22+
{{ dashboard_url }}/dashboard
23+
24+
Need a hand? The full documentation lives at
25+
{{ dashboard_url }}/docs. Start with "Getting Started" or jump
26+
straight to "CloudNode setup" if you've already got the install
27+
running.
28+
29+
——
30+
You're receiving this because you just created an organization at
31+
SourceBox Sentry. This is a one-time welcome — there's no
32+
recurring marketing email.
33+
Manage email alerts: {{ dashboard_url }}/settings#settings-notifications
34+
Unsubscribe from welcome emails (won't affect operational alerts):
35+
{{ unsubscribe_url }}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Welcome to SourceBox Sentry — let's get your first camera online

0 commit comments

Comments
 (0)