diff --git a/backend/src/app.ts b/backend/src/app.ts
index af55def..1ded93f 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -468,13 +468,11 @@ const normalizeWorkspace = (row: WorkspaceRow) => ({
const normalizeAdminRescueWorkspace = (
row: WorkspaceRow & {
- owner_email: string | null;
bird_count: number;
member_count: number;
},
) => ({
workspace: normalizeWorkspace(row),
- ownerEmail: row.owner_email,
birdCount: Number(row.bird_count ?? 0),
memberCount: Number(row.member_count ?? 0),
});
@@ -1267,10 +1265,12 @@ const sendRescueStatusNotification = async ({
workspace,
ownerEmail,
event,
+ note,
}: {
workspace: WorkspaceRow;
ownerEmail: string | null;
event: 'created' | 'converted' | 'status_changed' | 'canceled';
+ note?: string | null;
}) => {
const statusLabel = workspace.rescue_verification_status.replace(/_/g, ' ');
const eventLabel =
@@ -1294,6 +1294,11 @@ const sendRescueStatusNotification = async ({
`Billing email: ${workspace.billing_email ?? 'not set'}`,
`Flock ID: ${workspace.id}`,
];
+ const escapedNote = note ? escapeHtml(note) : null;
+
+ if (note) {
+ lines.push(`Note: ${note}`);
+ }
if (!mailTransport) {
console.log(`Rescue status notification for ${rescueStatusNotificationEmail}:\n${lines.join('\n')}`);
@@ -1314,6 +1319,7 @@ const sendRescueStatusNotification = async ({
Billing email: ${escapedBillingEmail}
Flock ID: ${workspace.id}
+ ${escapedNote ? `Note: ${escapedNote}
` : ''}
`,
});
@@ -1368,6 +1374,17 @@ const sendRescueOnboardingWebhook = async ({
}
};
+const trySendRescueOnboardingWebhook = async (payload: Parameters[0]) => {
+ try {
+ await sendRescueOnboardingWebhook(payload);
+ return null;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown rescue onboarding webhook error.';
+ console.error(`Rescue onboarding webhook failed for workspace ${payload.workspaceId}:`, error);
+ return `The rescue onboarding webhook failed and this rescue requires manual review. ${errorMessage}`;
+ }
+};
+
const issueMagicLinkInvite = async ({
email,
name,
@@ -2553,15 +2570,6 @@ app.post('/api/workspaces', requireAuth, requireSessionAuth, async (req: Request
res.status(400).json({ error: 'Rescue onboarding details are required.' });
return;
}
-
- await sendRescueOnboardingWebhook({
- action: 'created',
- workspaceId,
- flockName: parsed.data.name,
- ownerEmail: req.auth!.user.email,
- requestedByUserId: req.auth!.user.id,
- rescueOnboarding: parsed.data.rescueOnboarding,
- });
}
const workspace = await createWorkspace({
@@ -2575,10 +2583,20 @@ app.post('/api/workspaces', requireAuth, requireSessionAuth, async (req: Request
});
if (workspace?.workspace_type === 'rescue') {
+ const onboardingWebhookError = await trySendRescueOnboardingWebhook({
+ action: 'created',
+ workspaceId: workspace.id,
+ flockName: workspace.name,
+ ownerEmail: req.auth!.user.email,
+ requestedByUserId: req.auth!.user.id,
+ rescueOnboarding: parsed.data.rescueOnboarding!,
+ });
+
await sendRescueStatusNotification({
workspace,
ownerEmail: req.auth!.user.email,
event: 'created',
+ note: onboardingWebhookError,
});
}
@@ -2626,15 +2644,6 @@ app.put('/api/workspace', requireAuth, requireSessionAuth, requireWorkspaceRole(
res.status(400).json({ error: 'Rescue onboarding details are required.' });
return;
}
-
- await sendRescueOnboardingWebhook({
- action: 'converted',
- workspaceId: currentWorkspace.id,
- flockName: parsed.data.name,
- ownerEmail: req.auth!.user.email,
- requestedByUserId: req.auth!.user.id,
- rescueOnboarding: parsed.data.rescueOnboarding,
- });
}
const workspace = await updateWorkspace({
@@ -2647,10 +2656,20 @@ app.put('/api/workspace', requireAuth, requireSessionAuth, requireWorkspaceRole(
});
if (workspace?.workspace_type === 'rescue' && isConvertingToRescue) {
+ const onboardingWebhookError = await trySendRescueOnboardingWebhook({
+ action: 'converted',
+ workspaceId: workspace.id,
+ flockName: workspace.name,
+ ownerEmail: req.auth!.user.email,
+ requestedByUserId: req.auth!.user.id,
+ rescueOnboarding: parsed.data.rescueOnboarding!,
+ });
+
await sendRescueStatusNotification({
workspace,
ownerEmail: req.auth!.user.email,
event: 'converted',
+ note: onboardingWebhookError,
});
}
diff --git a/backend/src/repositories/workspaceRepository.ts b/backend/src/repositories/workspaceRepository.ts
index c3d9ff1..44d9171 100644
--- a/backend/src/repositories/workspaceRepository.ts
+++ b/backend/src/repositories/workspaceRepository.ts
@@ -380,7 +380,6 @@ export const deleteWorkspaceMember = async (memberId: string, workspaceId: numbe
export const listRescueWorkspacesForAdmin = async () => {
const result = await db.query<
WorkspaceRow & {
- owner_email: string | null;
bird_count: number;
member_count: number;
}
@@ -398,17 +397,13 @@ export const listRescueWorkspacesForAdmin = async () => {
workspaces.rescue_verification_status,
workspaces.created_at,
workspaces.updated_at,
- owner.invite_email AS owner_email,
COUNT(DISTINCT birds.id)::int AS bird_count,
COUNT(DISTINCT workspace_members.id)::int AS member_count
FROM workspaces
- LEFT JOIN workspace_members owner
- ON owner.workspace_id = workspaces.id
- AND owner.role = 'owner'
LEFT JOIN birds ON birds.workspace_id = workspaces.id
LEFT JOIN workspace_members ON workspace_members.workspace_id = workspaces.id
WHERE workspaces.workspace_type = 'rescue'
- GROUP BY workspaces.id, owner.invite_email
+ GROUP BY workspaces.id
ORDER BY
CASE workspaces.rescue_verification_status
WHEN 'pending' THEN 0
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2275a36..b58ff34 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -158,7 +158,6 @@ type AdminSummary = {
type AdminRescueWorkspace = {
workspace: Workspace;
- ownerEmail: string | null;
birdCount: number;
memberCount: number;
};
@@ -1003,6 +1002,16 @@ const formatBillingPlanBirdLimit = (billingPlan: BillingPlan) => {
return null;
};
+const formatBillingBirdUsage = (billingPlan: BillingPlan, birdCount: number) => {
+ const birdLimit = formatBillingPlanBirdLimit(billingPlan);
+
+ if (!birdLimit) {
+ return `${birdCount} bird${birdCount === 1 ? '' : 's'}`;
+ }
+
+ return `${birdCount} of ${birdLimit} birds used`;
+};
+
const formatSubscriptionStatus = (status: SubscriptionStatus) => {
if (status === 'trialing') {
return 'Trialing';
@@ -4554,7 +4563,7 @@ function App() {
{formatRescueVerificationStatus(entry.workspace.rescueVerificationStatus)} • {entry.birdCount} birds • {entry.memberCount} members
- Owner {entry.ownerEmail ?? 'unknown'} • Billing {entry.workspace.billingEmail ?? 'not set'}
+ Billing {entry.workspace.billingEmail ?? 'not set'}