@aglyn/plugins-marketing 1.0.0-beta.205 → 1.0.0-beta.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -15
- package/src/lib/components/staff-org-email-card.component.d.ts +8 -0
- package/src/lib/components/staff-org-email-card.component.js +379 -0
- package/src/lib/components/staff-org-email-card.component.js.map +1 -0
- package/src/lib/constants/operator-alerts.d.ts +22 -0
- package/src/lib/constants/operator-alerts.js +84 -0
- package/src/lib/constants/operator-alerts.js.map +1 -0
- package/src/lib/declarations.server.d.ts +22 -0
- package/src/lib/declarations.server.js +31 -0
- package/src/lib/declarations.server.js.map +1 -0
- package/src/lib/plugin.js +9 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/campaign-send.js +13 -0
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-events.js +27 -1
- package/src/lib/server/email-events.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-marketing",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.207",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.207",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.207",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.207",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.207",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.207",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.207",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.207",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.207",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.207",
|
|
37
|
+
"@aglyn/shared-util-http": "1.0.0-beta.207",
|
|
38
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.207",
|
|
39
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.207",
|
|
40
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.207",
|
|
41
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.207",
|
|
42
42
|
"@swc/helpers": "0.5.23"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The staff organization page's email card (`adminOrgDetail` zone): every
|
|
3
|
+
* campaign send with its state and numbers, and the campaign containers
|
|
4
|
+
* with the sites each runs on.
|
|
5
|
+
*/
|
|
6
|
+
export declare function StaffOrgEmailCard(props: {
|
|
7
|
+
orgId: string;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
|
|
19
|
+
import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
|
|
20
|
+
import { mdiEyeOutline } from "@aglyn/shared-data-mdi";
|
|
21
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
22
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
23
|
+
import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
24
|
+
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
25
|
+
import { useFirestore, usePagedCollection } from "@aglyn/tenant-feature-instance";
|
|
26
|
+
import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Stack, Tab, Tabs, Typography } from "@mui/material";
|
|
27
|
+
import { collection, documentId, limit, orderBy, query } from "firebase/firestore";
|
|
28
|
+
import { useMemo, useState } from "react";
|
|
29
|
+
const whenOf = (value)=>{
|
|
30
|
+
if (typeof (value == null ? void 0 : value.toDate) === 'function') return value.toDate().toLocaleString();
|
|
31
|
+
if (typeof (value == null ? void 0 : value.seconds) === 'number') return new Date(value.seconds * 1000).toLocaleString();
|
|
32
|
+
if (typeof value === 'number') return new Date(value).toLocaleString();
|
|
33
|
+
return '—';
|
|
34
|
+
};
|
|
35
|
+
/** When a send went, or is due: the sent stamp, else the schedule, else its creation. */ const sendWhen = (row)=>row['sentAt'] ? whenOf(row['sentAt']) : row['sendAtMs'] ? `due ${whenOf(row['sendAtMs'])}` : whenOf(row['createdAtMs']);
|
|
36
|
+
/** The staff preview of the design a send was composed from, if it names one. */ const designPreviewHref = (row)=>typeof row['templateScreenId'] === 'string' && row['templateScreenId'] && typeof row['hostId'] === 'string' && row['hostId'] ? buildRoute(Route.ADMIN_SITE_PREVIEW, {
|
|
37
|
+
hostId: row['hostId'],
|
|
38
|
+
kind: 'screen',
|
|
39
|
+
docId: row['templateScreenId']
|
|
40
|
+
}) : undefined;
|
|
41
|
+
/**
|
|
42
|
+
* A stored message body, in a frame with `sandbox` and no permissions: a
|
|
43
|
+
* customer's HTML is only ever pixels on the staff page.
|
|
44
|
+
*/ function MessageDialog(props) {
|
|
45
|
+
var _ref;
|
|
46
|
+
const { row, onClose } = props;
|
|
47
|
+
return /*#__PURE__*/ _jsxs(Dialog, {
|
|
48
|
+
open: Boolean(row),
|
|
49
|
+
onClose: onClose,
|
|
50
|
+
maxWidth: "md",
|
|
51
|
+
fullWidth: true,
|
|
52
|
+
children: [
|
|
53
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
54
|
+
children: row ? String(row['subject'] || row.$id) : ''
|
|
55
|
+
}),
|
|
56
|
+
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
57
|
+
children: [
|
|
58
|
+
(row == null ? void 0 : row['preheader']) ? /*#__PURE__*/ _jsx(Typography, {
|
|
59
|
+
variant: "caption",
|
|
60
|
+
color: "text.secondary",
|
|
61
|
+
children: String(row['preheader'])
|
|
62
|
+
}) : null,
|
|
63
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
64
|
+
component: "iframe",
|
|
65
|
+
title: "Message",
|
|
66
|
+
sandbox: "",
|
|
67
|
+
srcDoc: String((_ref = row == null ? void 0 : row['body']) != null ? _ref : ''),
|
|
68
|
+
sx: {
|
|
69
|
+
mt: 1,
|
|
70
|
+
width: '100%',
|
|
71
|
+
height: 560,
|
|
72
|
+
border: 1,
|
|
73
|
+
borderColor: 'divider',
|
|
74
|
+
bgcolor: '#fff'
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
}),
|
|
79
|
+
/*#__PURE__*/ _jsx(DialogActions, {
|
|
80
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
81
|
+
onClick: onClose,
|
|
82
|
+
children: 'Close'
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** One paged table of an organization subcollection. */ function OrgCollectionTable(props) {
|
|
89
|
+
const { orgId, collectionId, columns, emptyLabel, onOpen } = props;
|
|
90
|
+
const firestore = useFirestore();
|
|
91
|
+
const paged = usePagedCollection((pageLimit)=>orgId ? query(collection(firestore, 'orgs', orgId, collectionId), orderBy(documentId()), limit(pageLimit)) : null, [
|
|
92
|
+
firestore,
|
|
93
|
+
orgId,
|
|
94
|
+
collectionId
|
|
95
|
+
], {
|
|
96
|
+
idField: '$id'
|
|
97
|
+
});
|
|
98
|
+
const byId = useMemo(()=>new Map(paged.rows.map((row)=>[
|
|
99
|
+
row.$id,
|
|
100
|
+
row
|
|
101
|
+
])), [
|
|
102
|
+
paged.rows
|
|
103
|
+
]);
|
|
104
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
105
|
+
spacing: 1,
|
|
106
|
+
children: [
|
|
107
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
108
|
+
rows: paged.rows,
|
|
109
|
+
columns: columns,
|
|
110
|
+
loading: paged.status === 'loading',
|
|
111
|
+
disableColumnSorting: true,
|
|
112
|
+
noRowsLabel: emptyLabel,
|
|
113
|
+
onOpen: onOpen ? (id)=>{
|
|
114
|
+
const row = byId.get(String(id));
|
|
115
|
+
if (row) onOpen(row);
|
|
116
|
+
} : undefined,
|
|
117
|
+
hideFooter: true,
|
|
118
|
+
rowHeight: TABLE_ROW_HEIGHT
|
|
119
|
+
}),
|
|
120
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
121
|
+
page: paged.page,
|
|
122
|
+
pageSize: paged.pageSize,
|
|
123
|
+
rowCount: paged.rows.length,
|
|
124
|
+
hasMore: paged.hasMore,
|
|
125
|
+
onPageChange: paged.setPage,
|
|
126
|
+
onPageSizeChange: paged.setPageSize
|
|
127
|
+
})
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const chips = (items)=>/*#__PURE__*/ _jsx(Stack, {
|
|
132
|
+
direction: "row",
|
|
133
|
+
spacing: 0.5,
|
|
134
|
+
useFlexGap: true,
|
|
135
|
+
sx: {
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
height: '100%',
|
|
138
|
+
flexWrap: 'wrap'
|
|
139
|
+
},
|
|
140
|
+
children: items.map((item)=>{
|
|
141
|
+
var _item_color;
|
|
142
|
+
return /*#__PURE__*/ _jsx(Chip, {
|
|
143
|
+
size: "small",
|
|
144
|
+
label: item.label,
|
|
145
|
+
color: (_item_color = item.color) != null ? _item_color : 'default',
|
|
146
|
+
variant: item.color ? 'filled' : 'outlined'
|
|
147
|
+
}, item.label);
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* The staff organization page's email card (`adminOrgDetail` zone): every
|
|
152
|
+
* campaign send with its state and numbers, and the campaign containers
|
|
153
|
+
* with the sites each runs on.
|
|
154
|
+
*/ export function StaffOrgEmailCard(props) {
|
|
155
|
+
const { orgId } = props;
|
|
156
|
+
const [tab, setTab] = useState('sends');
|
|
157
|
+
const [message, setMessage] = useState(null);
|
|
158
|
+
const sendColumns = useMemo(()=>[
|
|
159
|
+
{
|
|
160
|
+
field: 'subject',
|
|
161
|
+
headerName: 'Subject',
|
|
162
|
+
flex: 1.4,
|
|
163
|
+
minWidth: 200,
|
|
164
|
+
valueGetter: (_value, row)=>String(row['subject'] || '(no subject)'),
|
|
165
|
+
renderCell: ({ row })=>{
|
|
166
|
+
var _row_hostId;
|
|
167
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
168
|
+
sx: {
|
|
169
|
+
justifyContent: 'center',
|
|
170
|
+
height: '100%',
|
|
171
|
+
lineHeight: 1.25
|
|
172
|
+
},
|
|
173
|
+
children: [
|
|
174
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
175
|
+
variant: "body2",
|
|
176
|
+
noWrap: true,
|
|
177
|
+
sx: {
|
|
178
|
+
lineHeight: 1.25
|
|
179
|
+
},
|
|
180
|
+
children: String(row['subject'] || '(no subject)')
|
|
181
|
+
}),
|
|
182
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
183
|
+
variant: "caption",
|
|
184
|
+
color: "text.secondary",
|
|
185
|
+
noWrap: true,
|
|
186
|
+
sx: {
|
|
187
|
+
fontFamily: 'monospace',
|
|
188
|
+
lineHeight: 1.25
|
|
189
|
+
},
|
|
190
|
+
children: String((_row_hostId = row['hostId']) != null ? _row_hostId : 'no site')
|
|
191
|
+
})
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
field: 'status',
|
|
198
|
+
headerName: 'Status',
|
|
199
|
+
flex: 1.4,
|
|
200
|
+
minWidth: 220,
|
|
201
|
+
valueGetter: (_value, row)=>{
|
|
202
|
+
var _row_status;
|
|
203
|
+
return String((_row_status = row['status']) != null ? _row_status : 'draft');
|
|
204
|
+
},
|
|
205
|
+
renderCell: ({ row })=>{
|
|
206
|
+
var _row_stats, _row_status;
|
|
207
|
+
var _row_staffReview;
|
|
208
|
+
const stats = (_row_stats = row['stats']) != null ? _row_stats : {};
|
|
209
|
+
return chips([
|
|
210
|
+
{
|
|
211
|
+
label: String((_row_status = row['status']) != null ? _row_status : 'draft'),
|
|
212
|
+
color: row['status'] === 'sent' ? 'success' : row['status'] === 'failed' ? 'error' : undefined
|
|
213
|
+
},
|
|
214
|
+
...((_row_staffReview = row['staffReview']) == null ? void 0 : _row_staffReview.state) === 'held' ? [
|
|
215
|
+
{
|
|
216
|
+
label: 'held for review',
|
|
217
|
+
color: 'warning'
|
|
218
|
+
}
|
|
219
|
+
] : [],
|
|
220
|
+
...typeof stats.sent === 'number' ? [
|
|
221
|
+
{
|
|
222
|
+
label: `${stats.sent} sent`
|
|
223
|
+
}
|
|
224
|
+
] : [],
|
|
225
|
+
...typeof stats.uniqueOpens === 'number' ? [
|
|
226
|
+
{
|
|
227
|
+
label: `${stats.uniqueOpens} opened`
|
|
228
|
+
}
|
|
229
|
+
] : [],
|
|
230
|
+
...typeof stats.uniqueClicks === 'number' ? [
|
|
231
|
+
{
|
|
232
|
+
label: `${stats.uniqueClicks} clicked`
|
|
233
|
+
}
|
|
234
|
+
] : [],
|
|
235
|
+
...stats.bounced ? [
|
|
236
|
+
{
|
|
237
|
+
label: `${stats.bounced} bounced`,
|
|
238
|
+
color: 'error'
|
|
239
|
+
}
|
|
240
|
+
] : []
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
field: 'audience',
|
|
246
|
+
headerName: 'Audience',
|
|
247
|
+
flex: 0.6,
|
|
248
|
+
minWidth: 110,
|
|
249
|
+
valueGetter: (_value, row)=>{
|
|
250
|
+
var _row_audience;
|
|
251
|
+
return String((_row_audience = row['audience']) != null ? _row_audience : '—');
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
field: 'when',
|
|
256
|
+
headerName: 'When',
|
|
257
|
+
flex: 0.9,
|
|
258
|
+
minWidth: 160,
|
|
259
|
+
valueGetter: (_value, row)=>sendWhen(row)
|
|
260
|
+
},
|
|
261
|
+
listActionsColumn((row)=>{
|
|
262
|
+
const design = designPreviewHref(row);
|
|
263
|
+
return /*#__PURE__*/ _jsx(ListRowActions, {
|
|
264
|
+
label: String(row['subject'] || row.$id),
|
|
265
|
+
quick: {
|
|
266
|
+
icon: mdiEyeOutline.path,
|
|
267
|
+
label: 'View message',
|
|
268
|
+
onClick: ()=>setMessage(row),
|
|
269
|
+
unavailableReason: row['body'] ? undefined : 'No message body was stored'
|
|
270
|
+
},
|
|
271
|
+
items: [
|
|
272
|
+
{
|
|
273
|
+
key: 'design',
|
|
274
|
+
label: 'Open design preview',
|
|
275
|
+
href: design,
|
|
276
|
+
external: true,
|
|
277
|
+
disabled: !design,
|
|
278
|
+
disabledReason: design ? undefined : 'This send was not composed from a design'
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
key: 'site',
|
|
282
|
+
label: 'Open site',
|
|
283
|
+
href: row['hostId'] ? buildRoute(Route.ADMIN_SITE_DETAIL, {
|
|
284
|
+
hostId: String(row['hostId'])
|
|
285
|
+
}) : undefined,
|
|
286
|
+
disabled: !row['hostId'],
|
|
287
|
+
disabledReason: row['hostId'] ? undefined : 'This send names no site'
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
});
|
|
291
|
+
}, {
|
|
292
|
+
width: 110
|
|
293
|
+
})
|
|
294
|
+
], []);
|
|
295
|
+
const campaignColumns = useMemo(()=>[
|
|
296
|
+
{
|
|
297
|
+
field: 'name',
|
|
298
|
+
headerName: 'Campaign',
|
|
299
|
+
flex: 1.4,
|
|
300
|
+
minWidth: 200,
|
|
301
|
+
valueGetter: (_value, row)=>String(row['name'] || row.$id)
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
field: 'visibleTo',
|
|
305
|
+
headerName: 'Runs on',
|
|
306
|
+
flex: 1.2,
|
|
307
|
+
minWidth: 180,
|
|
308
|
+
valueGetter: (_value, row)=>(Array.isArray(row['visibleTo']) ? row['visibleTo'] : []).join(', '),
|
|
309
|
+
renderCell: ({ row })=>chips([
|
|
310
|
+
...(Array.isArray(row['visibleTo']) ? row['visibleTo'] : []).map((token)=>({
|
|
311
|
+
label: token === 'org' ? 'every site' : token.replace(/^host:/, '')
|
|
312
|
+
})),
|
|
313
|
+
...row['deletedAt'] ? [
|
|
314
|
+
{
|
|
315
|
+
label: 'deleted',
|
|
316
|
+
color: 'error'
|
|
317
|
+
}
|
|
318
|
+
] : []
|
|
319
|
+
])
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
field: 'window',
|
|
323
|
+
headerName: 'Window',
|
|
324
|
+
flex: 1,
|
|
325
|
+
minWidth: 180,
|
|
326
|
+
valueGetter: (_value, row)=>row['startAtMs'] || row['endAtMs'] ? `${whenOf(row['startAtMs'])} – ${row['endAtMs'] ? whenOf(row['endAtMs']) : 'open'}` : '—'
|
|
327
|
+
}
|
|
328
|
+
], []);
|
|
329
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
330
|
+
children: [
|
|
331
|
+
/*#__PURE__*/ _jsx(CardDisplay, {
|
|
332
|
+
header: 'Email campaigns',
|
|
333
|
+
help: pluginDocsHelp('staffConsole', {
|
|
334
|
+
anchor: '#staff-org-email',
|
|
335
|
+
excerpt: "The organization's campaign sends and campaigns. A send opens its stored message or its design preview."
|
|
336
|
+
}),
|
|
337
|
+
contentGutterX: true,
|
|
338
|
+
contentGutterY: true,
|
|
339
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
340
|
+
spacing: 2,
|
|
341
|
+
children: [
|
|
342
|
+
/*#__PURE__*/ _jsxs(Tabs, {
|
|
343
|
+
value: tab,
|
|
344
|
+
onChange: (_event, next)=>setTab(next),
|
|
345
|
+
children: [
|
|
346
|
+
/*#__PURE__*/ _jsx(Tab, {
|
|
347
|
+
value: "sends",
|
|
348
|
+
label: "Sends"
|
|
349
|
+
}),
|
|
350
|
+
/*#__PURE__*/ _jsx(Tab, {
|
|
351
|
+
value: "campaigns",
|
|
352
|
+
label: "Campaigns"
|
|
353
|
+
})
|
|
354
|
+
]
|
|
355
|
+
}),
|
|
356
|
+
tab === 'sends' ? /*#__PURE__*/ _jsx(OrgCollectionTable, {
|
|
357
|
+
orgId: orgId,
|
|
358
|
+
collectionId: "campaigns",
|
|
359
|
+
columns: sendColumns,
|
|
360
|
+
emptyLabel: "This organization has sent no campaigns",
|
|
361
|
+
onOpen: (row)=>row['body'] ? setMessage(row) : undefined
|
|
362
|
+
}, "sends") : /*#__PURE__*/ _jsx(OrgCollectionTable, {
|
|
363
|
+
orgId: orgId,
|
|
364
|
+
collectionId: "emailCampaigns",
|
|
365
|
+
columns: campaignColumns,
|
|
366
|
+
emptyLabel: "This organization has no email campaigns"
|
|
367
|
+
}, "campaigns")
|
|
368
|
+
]
|
|
369
|
+
})
|
|
370
|
+
}),
|
|
371
|
+
/*#__PURE__*/ _jsx(MessageDialog, {
|
|
372
|
+
row: message,
|
|
373
|
+
onClose: ()=>setMessage(null)
|
|
374
|
+
})
|
|
375
|
+
]
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
//# sourceMappingURL=staff-org-email-card.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/staff-org-email-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport { mdiEyeOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListRowActions,\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useFirestore, usePagedCollection } from '@aglyn/tenant-feature-instance'\nimport {\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n Tab,\n Tabs,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, documentId, limit, orderBy, query } from 'firebase/firestore'\nimport { type ReactNode, useMemo, useState } from 'react'\n\n/*\n * AN ORGANIZATION'S EMAIL WORK, AS STAFF SEE IT (AGL-3380).\n *\n * The staff organization page is about a customer the staffer is not a\n * member of, so nothing here sends, schedules or edits. Each tab is a paged\n * walk of one collection in document-id order. A send opens the message it\n * stored, drawn in a frame that runs nothing, or the design it was composed\n * from in the staff preview.\n */\n\ntype Row = { $id: string } & Record<string, any>\n\nconst whenOf = (value: any): string => {\n if (typeof value?.toDate === 'function') return value.toDate().toLocaleString()\n if (typeof value?.seconds === 'number') return new Date(value.seconds * 1000).toLocaleString()\n if (typeof value === 'number') return new Date(value).toLocaleString()\n return '—'\n}\n\n/** When a send went, or is due: the sent stamp, else the schedule, else its creation. */\nconst sendWhen = (row: Row): string =>\n row['sentAt']\n ? whenOf(row['sentAt'])\n : row['sendAtMs']\n ? `due ${whenOf(row['sendAtMs'])}`\n : whenOf(row['createdAtMs'])\n\n/** The staff preview of the design a send was composed from, if it names one. */\nconst designPreviewHref = (row: Row): string | undefined =>\n typeof row['templateScreenId'] === 'string' &&\n row['templateScreenId'] &&\n typeof row['hostId'] === 'string' &&\n row['hostId']\n ? buildRoute(Route.ADMIN_SITE_PREVIEW, {\n hostId: row['hostId'],\n kind: 'screen',\n docId: row['templateScreenId'],\n })\n : undefined\n\n/**\n * A stored message body, in a frame with `sandbox` and no permissions: a\n * customer's HTML is only ever pixels on the staff page.\n */\nfunction MessageDialog(props: { row: Row | null; onClose: () => void }) {\n const { row, onClose } = props\n return (\n <Dialog open={Boolean(row)} onClose={onClose} maxWidth=\"md\" fullWidth>\n <DialogTitle>{row ? String(row['subject'] || row.$id) : ''}</DialogTitle>\n <DialogContent>\n {row?.['preheader'] ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {String(row['preheader'])}\n </Typography>\n ) : null}\n <Box\n component=\"iframe\"\n title=\"Message\"\n sandbox=\"\"\n srcDoc={String(row?.['body'] ?? '')}\n sx={{ mt: 1, width: '100%', height: 560, border: 1, borderColor: 'divider', bgcolor: '#fff' }}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Close'}</Button>\n </DialogActions>\n </Dialog>\n )\n}\n\n/** One paged table of an organization subcollection. */\nfunction OrgCollectionTable(props: {\n orgId: string\n collectionId: string\n columns: GridColDef[]\n emptyLabel: string\n onOpen?: (row: Row) => void\n}) {\n const { orgId, collectionId, columns, emptyLabel, onOpen } = props\n const firestore = useFirestore()\n const paged = usePagedCollection<Row>(\n (pageLimit) =>\n orgId\n ? query(\n collection(firestore, 'orgs', orgId, collectionId),\n orderBy(documentId()),\n limit(pageLimit),\n )\n : null,\n [firestore, orgId, collectionId],\n { idField: '$id' },\n )\n const byId = useMemo(() => new Map(paged.rows.map((row) => [row.$id, row])), [paged.rows])\n return (\n <Stack spacing={1}>\n <ListTable\n rows={paged.rows}\n columns={columns}\n loading={paged.status === 'loading'}\n disableColumnSorting\n noRowsLabel={emptyLabel}\n onOpen={\n onOpen\n ? (id) => {\n const row = byId.get(String(id))\n if (row) onOpen(row)\n }\n : undefined\n }\n hideFooter\n rowHeight={TABLE_ROW_HEIGHT}\n />\n <ListPagination\n page={paged.page}\n pageSize={paged.pageSize}\n rowCount={paged.rows.length}\n hasMore={paged.hasMore}\n onPageChange={paged.setPage}\n onPageSizeChange={paged.setPageSize}\n />\n </Stack>\n )\n}\n\nconst chips = (items: Array<{ label: string; color?: 'success' | 'warning' | 'error' }>): ReactNode => (\n <Stack direction=\"row\" spacing={0.5} useFlexGap sx={{ alignItems: 'center', height: '100%', flexWrap: 'wrap' }}>\n {items.map((item) => (\n <Chip\n key={item.label}\n size=\"small\"\n label={item.label}\n color={item.color ?? 'default'}\n variant={item.color ? 'filled' : 'outlined'}\n />\n ))}\n </Stack>\n)\n\ntype EmailTab = 'sends' | 'campaigns'\n\n/**\n * The staff organization page's email card (`adminOrgDetail` zone): every\n * campaign send with its state and numbers, and the campaign containers\n * with the sites each runs on.\n */\nexport function StaffOrgEmailCard(props: { orgId: string }) {\n const { orgId } = props\n const [tab, setTab] = useState<EmailTab>('sends')\n const [message, setMessage] = useState<Row | null>(null)\n\n const sendColumns: GridColDef[] = useMemo(\n () => [\n {\n field: 'subject',\n headerName: 'Subject',\n flex: 1.4,\n minWidth: 200,\n valueGetter: (_value, row: Row) => String(row['subject'] || '(no subject)'),\n renderCell: ({ row }: { row: Row }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}>\n <Typography variant=\"body2\" noWrap sx={{ lineHeight: 1.25 }}>\n {String(row['subject'] || '(no subject)')}\n </Typography>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n noWrap\n sx={{ fontFamily: 'monospace', lineHeight: 1.25 }}\n >\n {String(row['hostId'] ?? 'no site')}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n flex: 1.4,\n minWidth: 220,\n valueGetter: (_value, row: Row) => String(row['status'] ?? 'draft'),\n renderCell: ({ row }: { row: Row }) => {\n const stats = row['stats'] ?? {}\n return chips([\n {\n label: String(row['status'] ?? 'draft'),\n color: row['status'] === 'sent' ? 'success' : row['status'] === 'failed' ? 'error' : undefined,\n },\n ...(row['staffReview']?.state === 'held'\n ? [{ label: 'held for review', color: 'warning' as const }]\n : []),\n ...(typeof stats.sent === 'number' ? [{ label: `${stats.sent} sent` }] : []),\n ...(typeof stats.uniqueOpens === 'number' ? [{ label: `${stats.uniqueOpens} opened` }] : []),\n ...(typeof stats.uniqueClicks === 'number' ? [{ label: `${stats.uniqueClicks} clicked` }] : []),\n ...(stats.bounced ? [{ label: `${stats.bounced} bounced`, color: 'error' as const }] : []),\n ])\n },\n },\n {\n field: 'audience',\n headerName: 'Audience',\n flex: 0.6,\n minWidth: 110,\n valueGetter: (_value, row: Row) => String(row['audience'] ?? '—'),\n },\n {\n field: 'when',\n headerName: 'When',\n flex: 0.9,\n minWidth: 160,\n valueGetter: (_value, row: Row) => sendWhen(row),\n },\n listActionsColumn(\n (row: Row) => {\n const design = designPreviewHref(row)\n return (\n <ListRowActions\n label={String(row['subject'] || row.$id)}\n quick={{\n icon: mdiEyeOutline.path,\n label: 'View message',\n onClick: () => setMessage(row),\n unavailableReason: row['body'] ? undefined : 'No message body was stored',\n }}\n items={[\n {\n key: 'design',\n label: 'Open design preview',\n href: design,\n external: true,\n disabled: !design,\n disabledReason: design ? undefined : 'This send was not composed from a design',\n },\n {\n key: 'site',\n label: 'Open site',\n href: row['hostId']\n ? buildRoute(Route.ADMIN_SITE_DETAIL, { hostId: String(row['hostId']) })\n : undefined,\n disabled: !row['hostId'],\n disabledReason: row['hostId'] ? undefined : 'This send names no site',\n },\n ]}\n />\n )\n },\n { width: 110 },\n ),\n ],\n [],\n )\n\n const campaignColumns: GridColDef[] = useMemo(\n () => [\n {\n field: 'name',\n headerName: 'Campaign',\n flex: 1.4,\n minWidth: 200,\n valueGetter: (_value, row: Row) => String(row['name'] || row.$id),\n },\n {\n field: 'visibleTo',\n headerName: 'Runs on',\n flex: 1.2,\n minWidth: 180,\n valueGetter: (_value, row: Row) =>\n (Array.isArray(row['visibleTo']) ? row['visibleTo'] : []).join(', '),\n renderCell: ({ row }: { row: Row }) =>\n chips([\n ...(Array.isArray(row['visibleTo']) ? (row['visibleTo'] as string[]) : []).map((token) => ({\n label: token === 'org' ? 'every site' : token.replace(/^host:/, ''),\n })),\n ...(row['deletedAt'] ? [{ label: 'deleted', color: 'error' as const }] : []),\n ]),\n },\n {\n field: 'window',\n headerName: 'Window',\n flex: 1,\n minWidth: 180,\n valueGetter: (_value, row: Row) =>\n row['startAtMs'] || row['endAtMs']\n ? `${whenOf(row['startAtMs'])} – ${row['endAtMs'] ? whenOf(row['endAtMs']) : 'open'}`\n : '—',\n },\n ],\n [],\n )\n\n return (\n <>\n <CardDisplay\n header={'Email campaigns'}\n help={pluginDocsHelp('staffConsole', {\n anchor: '#staff-org-email',\n excerpt:\n \"The organization's campaign sends and campaigns. A send opens its stored message or its design preview.\",\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Tabs value={tab} onChange={(_event, next: EmailTab) => setTab(next)}>\n <Tab value=\"sends\" label=\"Sends\" />\n <Tab value=\"campaigns\" label=\"Campaigns\" />\n </Tabs>\n {tab === 'sends' ? (\n <OrgCollectionTable\n key=\"sends\"\n orgId={orgId}\n collectionId=\"campaigns\"\n columns={sendColumns}\n emptyLabel=\"This organization has sent no campaigns\"\n onOpen={(row) => (row['body'] ? setMessage(row) : undefined)}\n />\n ) : (\n <OrgCollectionTable\n key=\"campaigns\"\n orgId={orgId}\n collectionId=\"emailCampaigns\"\n columns={campaignColumns}\n emptyLabel=\"This organization has no email campaigns\"\n />\n )}\n </Stack>\n </CardDisplay>\n <MessageDialog row={message} onClose={() => setMessage(null)} />\n </>\n )\n}\n"],"names":["buildRoute","Route","pluginDocsHelp","mdiEyeOutline","CardDisplay","ListPagination","ListRowActions","ListTable","listActionsColumn","TABLE_ROW_HEIGHT","useFirestore","usePagedCollection","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Stack","Tab","Tabs","Typography","collection","documentId","limit","orderBy","query","useMemo","useState","whenOf","value","toDate","toLocaleString","seconds","Date","sendWhen","row","designPreviewHref","ADMIN_SITE_PREVIEW","hostId","kind","docId","undefined","MessageDialog","props","onClose","open","Boolean","maxWidth","fullWidth","String","$id","variant","color","component","title","sandbox","srcDoc","sx","mt","width","height","border","borderColor","bgcolor","onClick","OrgCollectionTable","orgId","collectionId","columns","emptyLabel","onOpen","firestore","paged","pageLimit","idField","byId","Map","rows","map","spacing","loading","status","disableColumnSorting","noRowsLabel","id","get","hideFooter","rowHeight","page","pageSize","rowCount","length","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","chips","items","direction","useFlexGap","alignItems","flexWrap","item","size","label","StaffOrgEmailCard","tab","setTab","message","setMessage","sendColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","justifyContent","lineHeight","noWrap","fontFamily","stats","state","sent","uniqueOpens","uniqueClicks","bounced","design","quick","icon","path","unavailableReason","key","href","external","disabled","disabledReason","ADMIN_SITE_DETAIL","campaignColumns","Array","isArray","join","token","replace","header","help","anchor","excerpt","contentGutterX","contentGutterY","onChange","_event","next"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts';
|
|
18
|
+
export declare const MARKETING_EMAIL_EVENTS_FAILED: OperatorAlertDefinition;
|
|
19
|
+
export declare const MARKETING_EMAIL_EVENTS_UNCONFIGURED: OperatorAlertDefinition;
|
|
20
|
+
export declare const MARKETING_EMAIL_EVENTS_SIGNATURE_REJECTED: OperatorAlertDefinition;
|
|
21
|
+
export declare const MARKETING_REPUTATION_BREAKER: OperatorAlertDefinition;
|
|
22
|
+
export declare const MARKETING_OPERATOR_ALERTS: readonly OperatorAlertDefinition[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { BUNDLE_ID } from "./bundle-common.js";
|
|
17
|
+
/**
|
|
18
|
+
* The marketing plugin's operator alerts (AGL-3377), registered from
|
|
19
|
+
* `declarations.server`. Its events webhook is where every provider bounce
|
|
20
|
+
* and complaint becomes a suppression, so a webhook that is failing is
|
|
21
|
+
* mail the platform keeps sending to addresses that already refused it.
|
|
22
|
+
*/ const DAY = 24 * 60;
|
|
23
|
+
export const MARKETING_EMAIL_EVENTS_FAILED = {
|
|
24
|
+
type: 'marketing.emailEventsFailed',
|
|
25
|
+
pluginId: BUNDLE_ID,
|
|
26
|
+
label: 'Email events webhook failed to apply an event',
|
|
27
|
+
description: 'The email provider’s event webhook threw while applying an event and answered 200 so the provider would not retry. A bounce or complaint in it was not suppressed, so the address keeps being mailed.',
|
|
28
|
+
tier: 'must',
|
|
29
|
+
category: 'deliverability',
|
|
30
|
+
title: 'A {{eventType}} email event was not applied',
|
|
31
|
+
body: 'The email events webhook failed on a {{eventType}} event ({{error}}). The provider will not retry it, so a bounce or complaint it carried was not suppressed.',
|
|
32
|
+
delivery: 'immediate',
|
|
33
|
+
dedupeWindowMinutes: 6 * 60,
|
|
34
|
+
defaultEnabled: true
|
|
35
|
+
};
|
|
36
|
+
export const MARKETING_EMAIL_EVENTS_UNCONFIGURED = {
|
|
37
|
+
type: 'marketing.emailEventsUnconfigured',
|
|
38
|
+
pluginId: BUNDLE_ID,
|
|
39
|
+
label: 'Email events webhook has no signing secret',
|
|
40
|
+
description: 'The email provider is posting events and the install has no RESEND_WEBHOOK_SECRET, so every bounce and complaint is refused and none is suppressed.',
|
|
41
|
+
tier: 'must',
|
|
42
|
+
category: 'deliverability',
|
|
43
|
+
title: 'Email events are being refused: no signing secret',
|
|
44
|
+
body: 'The email provider posted an event and RESEND_WEBHOOK_SECRET is not set, so it was refused. Bounces and complaints are not being suppressed until it is.',
|
|
45
|
+
delivery: 'immediate',
|
|
46
|
+
dedupeWindowMinutes: DAY,
|
|
47
|
+
defaultEnabled: true
|
|
48
|
+
};
|
|
49
|
+
export const MARKETING_EMAIL_EVENTS_SIGNATURE_REJECTED = {
|
|
50
|
+
type: 'marketing.emailEventsSignatureRejected',
|
|
51
|
+
pluginId: BUNDLE_ID,
|
|
52
|
+
label: 'Email events webhook failing its signature check',
|
|
53
|
+
description: 'Repeated signed deliveries to the email events webhook matched no secret. Usually a rotated signing secret: until it is fixed, no bounce or complaint is suppressed.',
|
|
54
|
+
tier: 'must',
|
|
55
|
+
category: 'deliverability',
|
|
56
|
+
title: 'Email events webhook is refusing deliveries',
|
|
57
|
+
body: 'The email events webhook refused several signed deliveries within six hours because their signature did not match RESEND_WEBHOOK_SECRET. Bounces and complaints are not being suppressed.',
|
|
58
|
+
delivery: 'immediate',
|
|
59
|
+
dedupeWindowMinutes: 6 * 60,
|
|
60
|
+
minOccurrences: 3,
|
|
61
|
+
defaultEnabled: true
|
|
62
|
+
};
|
|
63
|
+
export const MARKETING_REPUTATION_BREAKER = {
|
|
64
|
+
type: 'marketing.reputationBreakerTripped',
|
|
65
|
+
pluginId: BUNDLE_ID,
|
|
66
|
+
label: 'Campaign sending blocked by the reputation breaker',
|
|
67
|
+
description: 'A workspace’s bounce or complaint rate tripped the circuit breaker, so its campaigns are refused. Check the list before reinstating it.',
|
|
68
|
+
tier: 'should',
|
|
69
|
+
category: 'deliverability',
|
|
70
|
+
title: 'Campaigns blocked on workspace {{orgId}}',
|
|
71
|
+
body: 'Workspace {{orgId}}’s campaigns are blocked by the reputation breaker: {{reason}}',
|
|
72
|
+
link: '/admin/orgs/{{orgId}}',
|
|
73
|
+
delivery: 'immediate',
|
|
74
|
+
dedupeWindowMinutes: DAY,
|
|
75
|
+
defaultEnabled: true
|
|
76
|
+
};
|
|
77
|
+
export const MARKETING_OPERATOR_ALERTS = [
|
|
78
|
+
MARKETING_EMAIL_EVENTS_FAILED,
|
|
79
|
+
MARKETING_EMAIL_EVENTS_UNCONFIGURED,
|
|
80
|
+
MARKETING_EMAIL_EVENTS_SIGNATURE_REJECTED,
|
|
81
|
+
MARKETING_REPUTATION_BREAKER
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=operator-alerts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/constants/operator-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { BUNDLE_ID } from './bundle-common'\n\n/**\n * The marketing plugin's operator alerts (AGL-3377), registered from\n * `declarations.server`. Its events webhook is where every provider bounce\n * and complaint becomes a suppression, so a webhook that is failing is\n * mail the platform keeps sending to addresses that already refused it.\n */\nconst DAY = 24 * 60\n\nexport const MARKETING_EMAIL_EVENTS_FAILED: OperatorAlertDefinition = {\n type: 'marketing.emailEventsFailed',\n pluginId: BUNDLE_ID,\n label: 'Email events webhook failed to apply an event',\n description:\n 'The email provider’s event webhook threw while applying an event and answered 200 so the provider would not retry. A bounce or complaint in it was not suppressed, so the address keeps being mailed.',\n tier: 'must',\n category: 'deliverability',\n title: 'A {{eventType}} email event was not applied',\n body:\n 'The email events webhook failed on a {{eventType}} event ({{error}}). The provider will not retry it, so a bounce or complaint it carried was not suppressed.',\n delivery: 'immediate',\n dedupeWindowMinutes: 6 * 60,\n defaultEnabled: true,\n}\n\nexport const MARKETING_EMAIL_EVENTS_UNCONFIGURED: OperatorAlertDefinition = {\n type: 'marketing.emailEventsUnconfigured',\n pluginId: BUNDLE_ID,\n label: 'Email events webhook has no signing secret',\n description:\n 'The email provider is posting events and the install has no RESEND_WEBHOOK_SECRET, so every bounce and complaint is refused and none is suppressed.',\n tier: 'must',\n category: 'deliverability',\n title: 'Email events are being refused: no signing secret',\n body:\n 'The email provider posted an event and RESEND_WEBHOOK_SECRET is not set, so it was refused. Bounces and complaints are not being suppressed until it is.',\n delivery: 'immediate',\n dedupeWindowMinutes: DAY,\n defaultEnabled: true,\n}\n\nexport const MARKETING_EMAIL_EVENTS_SIGNATURE_REJECTED: OperatorAlertDefinition = {\n type: 'marketing.emailEventsSignatureRejected',\n pluginId: BUNDLE_ID,\n label: 'Email events webhook failing its signature check',\n description:\n 'Repeated signed deliveries to the email events webhook matched no secret. Usually a rotated signing secret: until it is fixed, no bounce or complaint is suppressed.',\n tier: 'must',\n category: 'deliverability',\n title: 'Email events webhook is refusing deliveries',\n body:\n 'The email events webhook refused several signed deliveries within six hours because their signature did not match RESEND_WEBHOOK_SECRET. Bounces and complaints are not being suppressed.',\n delivery: 'immediate',\n dedupeWindowMinutes: 6 * 60,\n minOccurrences: 3,\n defaultEnabled: true,\n}\n\nexport const MARKETING_REPUTATION_BREAKER: OperatorAlertDefinition = {\n type: 'marketing.reputationBreakerTripped',\n pluginId: BUNDLE_ID,\n label: 'Campaign sending blocked by the reputation breaker',\n description:\n 'A workspace’s bounce or complaint rate tripped the circuit breaker, so its campaigns are refused. Check the list before reinstating it.',\n tier: 'should',\n category: 'deliverability',\n title: 'Campaigns blocked on workspace {{orgId}}',\n body: 'Workspace {{orgId}}’s campaigns are blocked by the reputation breaker: {{reason}}',\n link: '/admin/orgs/{{orgId}}',\n delivery: 'immediate',\n dedupeWindowMinutes: DAY,\n defaultEnabled: true,\n}\n\nexport const MARKETING_OPERATOR_ALERTS: readonly OperatorAlertDefinition[] = [\n MARKETING_EMAIL_EVENTS_FAILED,\n MARKETING_EMAIL_EVENTS_UNCONFIGURED,\n MARKETING_EMAIL_EVENTS_SIGNATURE_REJECTED,\n MARKETING_REPUTATION_BREAKER,\n]\n"],"names":["BUNDLE_ID","DAY","MARKETING_EMAIL_EVENTS_FAILED","type","pluginId","label","description","tier","category","title","body","delivery","dedupeWindowMinutes","defaultEnabled","MARKETING_EMAIL_EVENTS_UNCONFIGURED","MARKETING_EMAIL_EVENTS_SIGNATURE_REJECTED","minOccurrences","MARKETING_REPUTATION_BREAKER","link","MARKETING_OPERATOR_ALERTS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,SAAS,QAAQ,qBAAiB;AAE3C;;;;;CAKC,GACD,MAAMC,MAAM,KAAK;AAEjB,OAAO,MAAMC,gCAAyD;IACpEC,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAI;IACzBC,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMC,sCAA+D;IAC1EX,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqBX;IACrBY,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAME,4CAAqE;IAChFZ,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAI;IACzBI,gBAAgB;IAChBH,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMI,+BAAwD;IACnEd,MAAM;IACNC,UAAUJ;IACVK,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MAAM;IACNQ,MAAM;IACNP,UAAU;IACVC,qBAAqBX;IACrBY,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMM,4BAAgE;IAC3EjB;IACAY;IACAC;IACAE;CACD,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The marketing plugin's server declarations: the light registrations core
|
|
19
|
+
* reads at boot, before any surface loads — its operator alerts (AGL-3377),
|
|
20
|
+
* so Staff → Operator alerts lists them before the first one is raised.
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerMarketingServerDeclarations(): void;
|