@aglyn/tenant-data-admin 1.0.0-beta.262 → 1.0.0-beta.263
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 +7 -7
- package/src/lib/server/keep-alive.d.ts +18 -0
- package/src/lib/server/keep-alive.js +59 -0
- package/src/lib/server/keep-alive.js.map +1 -0
- package/src/lib/server/notifications.d.ts +16 -5
- package/src/lib/server/notifications.js +26 -5
- package/src/lib/server/notifications.js.map +1 -1
- package/src/lib/server/push-apns.js +3 -1
- package/src/lib/server/push-apns.js.map +1 -1
- package/src/lib/server/push-delivery.d.ts +9 -0
- package/src/lib/server/push-delivery.js +10 -0
- package/src/lib/server/push-delivery.js.map +1 -1
- package/src/lib/server/render-system-email.d.ts +7 -1
- package/src/lib/server/render-system-email.js +1 -1
- package/src/lib/server/render-system-email.js.map +1 -1
- package/src/lib/server/sso-domain-policy.d.ts +43 -7
- package/src/lib/server/sso-domain-policy.js +61 -9
- package/src/lib/server/sso-domain-policy.js.map +1 -1
- package/src/lib/server/weekly-summary.d.ts +41 -25
- package/src/lib/server/weekly-summary.js +206 -117
- package/src/lib/server/weekly-summary.js.map +1 -1
|
@@ -16,20 +16,30 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types';
|
|
18
18
|
import { type ReleaseFlagKey } from '@aglyn/aglyn/app-utils/release-flags';
|
|
19
|
+
import { type WeeklySummaryColors, type WeeklySummaryEmail } from '@aglyn/aglyn/app-utils/weekly-summary';
|
|
19
20
|
import { type ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
|
|
20
21
|
import { type FigureReaderContext } from './figure-readers';
|
|
21
22
|
/**
|
|
22
23
|
* THE WEEKLY SUMMARY'S SENDS (AGL-3867): `POST /api/admin/weekly-summary`,
|
|
23
24
|
* Mondays at 13:00 UTC (08:00 Central) from `scheduled-crons.yml`.
|
|
24
25
|
*
|
|
25
|
-
* ##
|
|
26
|
+
* ## What is sent, and to whom
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* One email for each of a workspace's sites that had something to report,
|
|
29
|
+
* and a workspace overview that adds them up: totals across the sites, a line
|
|
30
|
+
* for each, and the workspace's own figures, such as its deals.
|
|
31
|
+
*
|
|
32
|
+
* - The workspace's owners and admins get the overview and every site's
|
|
33
|
+
* email. In a workspace where only one site had anything to report, the
|
|
34
|
+
* overview would repeat it, so that site's email carries the workspace's
|
|
35
|
+
* own figures instead and no overview is sent.
|
|
36
|
+
* - A member who is an admin of a site, without being one of the
|
|
37
|
+
* workspace's, gets that site's email: they run it.
|
|
38
|
+
*
|
|
39
|
+
* The **Weekly summary** switch on each person's account
|
|
40
|
+
* (`users/{uid}.digestPrefs.weeklySummary`, on until turned off) governs all
|
|
41
|
+
* of it. A locked or disabled account, a suppressed address and a suspended
|
|
42
|
+
* member are not mailed.
|
|
33
43
|
*
|
|
34
44
|
* ## What it reads
|
|
35
45
|
*
|
|
@@ -38,32 +48,38 @@ import { type FigureReaderContext } from './figure-readers';
|
|
|
38
48
|
* `weeklySummary`, and the workspace through every workspace reader that
|
|
39
49
|
* does. Each reader is offered only where its plan feature is included and
|
|
40
50
|
* its plugin runs (`eligibleFigureReaders`), read over the seven whole days
|
|
41
|
-
* before the run, and
|
|
42
|
-
* the rest are still read.
|
|
51
|
+
* before the run, and summarized by its owner. A reader that fails is left
|
|
52
|
+
* out; the rest are still read. Every figure is read once per workspace, and
|
|
53
|
+
* every recipient is sent the same figures.
|
|
43
54
|
*
|
|
44
55
|
* ## Once a week, and resumable
|
|
45
56
|
*
|
|
46
|
-
* `orgs/{orgId}/weeklySummaries/{week}` records each
|
|
47
|
-
*
|
|
48
|
-
* never
|
|
49
|
-
* are swept in id order behind a cursor,
|
|
50
|
-
* already runs for the CRM digest.
|
|
57
|
+
* `orgs/{orgId}/weeklySummaries/{week}.sent.{uid}.{email}` records each
|
|
58
|
+
* email sent — `overview`, or a site's id — stamped after each send, so a run
|
|
59
|
+
* that dies mid-workspace sends only what never went, and a second run in the
|
|
60
|
+
* same week sends nothing. Workspaces are swept in id order behind a cursor,
|
|
61
|
+
* the `done: false` loop the workflow already runs for the CRM digest.
|
|
51
62
|
*
|
|
52
63
|
* The email is a `bulk` send under the platform send-rate governor: a refusal
|
|
53
|
-
* stops the run and answers `done: true`, and
|
|
54
|
-
*
|
|
55
|
-
*
|
|
64
|
+
* stops the run and answers `done: true`, and a manual re-run in the same week
|
|
65
|
+
* picks up the rest. Metered as PLATFORM mail, not the workspace's: it is the
|
|
66
|
+
* platform reporting to the people who run it.
|
|
56
67
|
*/
|
|
57
68
|
type Firestore = FirebaseFirestore.Firestore;
|
|
58
69
|
/** The beat `/api/health/crons` reads, once this job joins its inventory. */
|
|
59
70
|
export declare const WEEKLY_SUMMARY_JOB_ID = "weekly-summary";
|
|
60
71
|
/** `orgs/{orgId}/weeklySummaries/{week}` — server-written, matched by no rule. */
|
|
61
72
|
export declare const WEEKLY_SUMMARY_MARKERS = "weeklySummaries";
|
|
73
|
+
/** The marker key of the overview; a site's email is keyed by the site's id. */
|
|
74
|
+
export declare const WEEKLY_SUMMARY_OVERVIEW = "overview";
|
|
75
|
+
/** The system emails: the overview, and one site's. */
|
|
76
|
+
export declare const WEEKLY_SUMMARY_TEMPLATE = "weekly-summary";
|
|
77
|
+
export declare const WEEKLY_SUMMARY_SITE_TEMPLATE = "weekly-summary-site";
|
|
62
78
|
/** Workspaces per invocation; a chunk that times out never advances its cursor. */
|
|
63
79
|
export declare const WEEKLY_SUMMARY_ORG_CHUNK = 20;
|
|
64
80
|
/** Sites a workspace's sweep ranks, by document id. */
|
|
65
81
|
export declare const WEEKLY_SUMMARY_HOST_READ_LIMIT = 50;
|
|
66
|
-
/** Sites
|
|
82
|
+
/** Sites a week's summary reports on: the busiest by last week's page views. */
|
|
67
83
|
export declare const WEEKLY_SUMMARY_MAX_SITES = 5;
|
|
68
84
|
/** Members per workspace whose address is chased through the auth pools when their member document has none. */
|
|
69
85
|
export declare const WEEKLY_SUMMARY_MAX_UID_LOOKUPS = 5;
|
|
@@ -75,13 +91,10 @@ export interface WeeklySummarySendResult {
|
|
|
75
91
|
rateLimited: boolean;
|
|
76
92
|
reason?: string;
|
|
77
93
|
}
|
|
78
|
-
export interface
|
|
94
|
+
export interface WeeklySummaryMessage {
|
|
79
95
|
to: string;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
body: string;
|
|
83
|
-
/** The plain fallback, closing line included. */
|
|
84
|
-
text: string;
|
|
96
|
+
template: typeof WEEKLY_SUMMARY_TEMPLATE | typeof WEEKLY_SUMMARY_SITE_TEMPLATE;
|
|
97
|
+
email: WeeklySummaryEmail;
|
|
85
98
|
org: Record<string, unknown>;
|
|
86
99
|
merge: Record<string, string>;
|
|
87
100
|
}
|
|
@@ -99,11 +112,13 @@ export interface WeeklySummaryDeps {
|
|
|
99
112
|
reachable: (uids: string[]) => Promise<string[]>;
|
|
100
113
|
/** The readers that declare a `weeklySummary` and pass the gates here. */
|
|
101
114
|
readers: (context: Omit<FigureReaderContext, 'accept'>) => Promise<ResolvedPluginFigureReader[]>;
|
|
115
|
+
/** The org's email colors, from its theme. */
|
|
116
|
+
colors: (org: Record<string, unknown>) => WeeklySummaryColors;
|
|
102
117
|
/** The member's deliverable address, or `null` when there is none or it is suppressed. */
|
|
103
118
|
address: (member: AglynOrgMember, lookups: {
|
|
104
119
|
left: number;
|
|
105
120
|
}) => Promise<string | null>;
|
|
106
|
-
send: (
|
|
121
|
+
send: (message: WeeklySummaryMessage) => Promise<WeeklySummarySendResult>;
|
|
107
122
|
budgetMs?: number;
|
|
108
123
|
}
|
|
109
124
|
export interface WeeklySummaryOrgReport {
|
|
@@ -131,6 +146,7 @@ export declare function runWeeklySummarySweep(deps: WeeklySummaryDeps, options?:
|
|
|
131
146
|
limit?: number;
|
|
132
147
|
}): Promise<WeeklySummaryReport>;
|
|
133
148
|
/** The sweep's reach into the real platform: Firestore, the flags, the roster, the mail. */
|
|
149
|
+
/** The sweep's reach into the real platform: Firestore, the flags, the roster, the mail. */
|
|
134
150
|
export declare function weeklySummaryDeps(options: {
|
|
135
151
|
runAt: Date;
|
|
136
152
|
dryRun: boolean;
|
|
@@ -14,16 +14,17 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
-
import { brandSupportLine } from "@aglyn/aglyn/app-utils/brand-support-line";
|
|
18
17
|
import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
|
|
19
18
|
import { writeCronBeat } from "@aglyn/aglyn/app-utils/health-report";
|
|
20
19
|
import { DIGEST_PREFS_FIELD, digestEnabled, INSIGHT_DIGESTS_FIELD, insightDigestSubscribed } from "@aglyn/aglyn/app-utils/notifications";
|
|
20
|
+
import { liveCustomDomain, TENANT_APEX } from "@aglyn/aglyn/app-utils/host-naming";
|
|
21
|
+
import { hostRoleFor } from "@aglyn/aglyn/app-utils/organizations";
|
|
21
22
|
import { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
22
23
|
import { platformConsoleOrigin } from "@aglyn/aglyn/app-utils/platform-brand";
|
|
23
24
|
import { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides } from "@aglyn/aglyn/app-utils/release-flags";
|
|
24
|
-
import {
|
|
25
|
+
import { composeWeeklySummaryOverviewEmail, composeWeeklySummarySiteEmail, WEEKLY_SUMMARY_DAYS, WEEKLY_SUMMARY_DIGEST_KEY, weeklySummaryHasNews, weeklySummaryPeriod } from "@aglyn/aglyn/app-utils/weekly-summary";
|
|
25
26
|
import { normalizePluginFigureTable, pluginFigureSummarySection } from "@aglyn/aglyn/plugin-manager/plugin-figures";
|
|
26
|
-
import { rateLimitedRetryAtMs, sendEmail } from "@aglyn/shared-util-email";
|
|
27
|
+
import { emailAccent, PLATFORM_EMAIL_PALETTE, rateLimitedRetryAtMs, resolveEmailColor, sendEmail } from "@aglyn/shared-util-email";
|
|
27
28
|
import { withoutMailWithheldAccounts } from "./account-mail.js";
|
|
28
29
|
import { findUserByUidAcrossPools } from "./auth-pools.js";
|
|
29
30
|
import { isCronAuthorized, isCronDryRun } from "./cron-auth.js";
|
|
@@ -33,14 +34,27 @@ import { eligibleFigureReaders } from "./figure-readers.js";
|
|
|
33
34
|
import firebaseAdmin from "./firebase-admin.js";
|
|
34
35
|
import { listOrgMembers } from "./organizations.js";
|
|
35
36
|
import { getServerReleaseFlagValues } from "./release-flags.js";
|
|
36
|
-
import { loadSystemEmail, renderLoadedSystemEmail, systemEmailBrand, systemEmailContent } from "./render-system-email.js";
|
|
37
|
+
import { loadSystemEmail, renderLoadedSystemEmail, systemEmailBrand, systemEmailContent, systemEmailTheme } from "./render-system-email.js";
|
|
37
38
|
/** The beat `/api/health/crons` reads, once this job joins its inventory. */ export const WEEKLY_SUMMARY_JOB_ID = 'weekly-summary';
|
|
38
39
|
/** `orgs/{orgId}/weeklySummaries/{week}` — server-written, matched by no rule. */ export const WEEKLY_SUMMARY_MARKERS = 'weeklySummaries';
|
|
40
|
+
/** The marker key of the overview; a site's email is keyed by the site's id. */ export const WEEKLY_SUMMARY_OVERVIEW = 'overview';
|
|
41
|
+
/** The system emails: the overview, and one site's. */ export const WEEKLY_SUMMARY_TEMPLATE = 'weekly-summary';
|
|
42
|
+
export const WEEKLY_SUMMARY_SITE_TEMPLATE = 'weekly-summary-site';
|
|
39
43
|
/** Workspaces per invocation; a chunk that times out never advances its cursor. */ export const WEEKLY_SUMMARY_ORG_CHUNK = 20;
|
|
40
44
|
/** Sites a workspace's sweep ranks, by document id. */ export const WEEKLY_SUMMARY_HOST_READ_LIMIT = 50;
|
|
41
|
-
/** Sites
|
|
45
|
+
/** Sites a week's summary reports on: the busiest by last week's page views. */ export const WEEKLY_SUMMARY_MAX_SITES = 5;
|
|
42
46
|
/** Members per workspace whose address is chased through the auth pools when their member document has none. */ export const WEEKLY_SUMMARY_MAX_UID_LOOKUPS = 5;
|
|
43
47
|
/** How long one call works before it stops and leaves the rest to the next. */ export const WEEKLY_SUMMARY_RUN_BUDGET_MS = 45000;
|
|
48
|
+
/** The theme tokens each color of the layout is drawn from. */ const COLOR_TOKENS = {
|
|
49
|
+
text: 'text.primary',
|
|
50
|
+
muted: 'text.secondary',
|
|
51
|
+
border: 'divider',
|
|
52
|
+
surface: 'grey.50',
|
|
53
|
+
accent: 'primary.main',
|
|
54
|
+
link: 'primary.main',
|
|
55
|
+
up: 'success.dark',
|
|
56
|
+
down: 'error.dark'
|
|
57
|
+
};
|
|
44
58
|
const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
45
59
|
function siteName(data, id) {
|
|
46
60
|
return str(data['displayName']) || str(data['name']) || str(data['subdomain']) || id;
|
|
@@ -65,23 +79,23 @@ function weekDays(from) {
|
|
|
65
79
|
}, (_, index)=>new Date(start + index * 86400000).toISOString().slice(0, 10));
|
|
66
80
|
}
|
|
67
81
|
/**
|
|
68
|
-
* Each reader's
|
|
69
|
-
* by its owner. A reader that fails, answers no table
|
|
70
|
-
* seven days is left out.
|
|
71
|
-
*/ async function
|
|
82
|
+
* Each reader's part, in its declared order: read over the week, then
|
|
83
|
+
* summarized by its owner. A reader that fails, answers no table, has nothing
|
|
84
|
+
* to say or cannot be read over seven days is left out.
|
|
85
|
+
*/ async function readParts(readers, base) {
|
|
72
86
|
const ordered = readers.filter(({ reader })=>reader.weeklySummary).sort((a, b)=>{
|
|
73
87
|
var _ref, _ref1;
|
|
74
88
|
var _a_reader_weeklySummary, _b_reader_weeklySummary;
|
|
75
89
|
return ((_ref = (_a_reader_weeklySummary = a.reader.weeklySummary) == null ? void 0 : _a_reader_weeklySummary.order) != null ? _ref : 0) - ((_ref1 = (_b_reader_weeklySummary = b.reader.weeklySummary) == null ? void 0 : _b_reader_weeklySummary.order) != null ? _ref1 : 0) || a.reader.id.localeCompare(b.reader.id);
|
|
76
90
|
});
|
|
77
|
-
const
|
|
91
|
+
const parts = [];
|
|
78
92
|
for (const { reader } of ordered){
|
|
79
93
|
const summary = reader.weeklySummary;
|
|
80
94
|
if (!summary) continue;
|
|
81
95
|
const days = reader.windows.length ? reader.windows.includes(WEEKLY_SUMMARY_DAYS) ? WEEKLY_SUMMARY_DAYS : null : 0;
|
|
82
96
|
if (days === null) continue;
|
|
83
97
|
try {
|
|
84
|
-
var
|
|
98
|
+
var _part_stats, _part_rows;
|
|
85
99
|
const read = await reader.read(_extends({}, base, {
|
|
86
100
|
days,
|
|
87
101
|
uid: null,
|
|
@@ -90,10 +104,8 @@ function weekDays(from) {
|
|
|
90
104
|
if (read.ok === false) continue;
|
|
91
105
|
const table = normalizePluginFigureTable(read.table);
|
|
92
106
|
if (!table) continue;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
headline: (_ref = summary.headline == null ? void 0 : summary.headline.call(summary, table)) != null ? _ref : null
|
|
96
|
-
});
|
|
107
|
+
const part = summary.summarize(table);
|
|
108
|
+
if (part && (((_part_stats = part.stats) == null ? void 0 : _part_stats.length) || ((_part_rows = part.rows) == null ? void 0 : _part_rows.length))) parts.push(part);
|
|
97
109
|
} catch (error) {
|
|
98
110
|
console.error('[weekly-summary] reader failed', {
|
|
99
111
|
reader: reader.id,
|
|
@@ -102,10 +114,45 @@ function weekDays(from) {
|
|
|
102
114
|
});
|
|
103
115
|
}
|
|
104
116
|
}
|
|
105
|
-
return
|
|
117
|
+
return parts;
|
|
118
|
+
}
|
|
119
|
+
/** The emails one person is owed this week, by marker key, in the order they are sent. */ function emailsFor(member, manager, sites, workspace) {
|
|
120
|
+
const active = sites.filter(({ site })=>weeklySummaryHasNews(site));
|
|
121
|
+
if (!manager) {
|
|
122
|
+
return active.filter(({ hostId })=>hostRoleFor(member, hostId) === 'admin').map(({ hostId })=>({
|
|
123
|
+
key: hostId,
|
|
124
|
+
hostId,
|
|
125
|
+
withWorkspace: false
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
// One active site: its email carries the workspace's figures, and an
|
|
129
|
+
// overview would only say it again.
|
|
130
|
+
if (active.length === 1) {
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
key: active[0].hostId,
|
|
134
|
+
hostId: active[0].hostId,
|
|
135
|
+
withWorkspace: true
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
...active.length || workspace.length ? [
|
|
141
|
+
{
|
|
142
|
+
key: WEEKLY_SUMMARY_OVERVIEW,
|
|
143
|
+
hostId: null,
|
|
144
|
+
withWorkspace: false
|
|
145
|
+
}
|
|
146
|
+
] : [],
|
|
147
|
+
...active.map(({ hostId })=>({
|
|
148
|
+
key: hostId,
|
|
149
|
+
hostId,
|
|
150
|
+
withWorkspace: false
|
|
151
|
+
}))
|
|
152
|
+
];
|
|
106
153
|
}
|
|
107
154
|
async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
108
|
-
var _orgDoc_data, _get;
|
|
155
|
+
var _orgDoc_data, _get, _sites_find;
|
|
109
156
|
const { firestore } = deps;
|
|
110
157
|
const orgId = orgDoc.id;
|
|
111
158
|
const org = (_orgDoc_data = orgDoc.data()) != null ? _orgDoc_data : {};
|
|
@@ -116,19 +163,25 @@ async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
|
116
163
|
};
|
|
117
164
|
const markerRef = orgDoc.ref.collection(WEEKLY_SUMMARY_MARKERS).doc(period.week);
|
|
118
165
|
const sentAlready = (_get = (await markerRef.get()).get('sent')) != null ? _get : {};
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
166
|
+
// Who could be told: owners and admins, and each site's own admins. Not
|
|
167
|
+
// suspended, an account that may be mailed, the switch kept on.
|
|
168
|
+
const members = (await deps.orgMembers(orgId)).filter((member)=>member.orgSuspended !== true);
|
|
169
|
+
const isManager = (member)=>member.role === 'owner' || member.role === 'admin';
|
|
170
|
+
const candidates = members.filter((member)=>{
|
|
171
|
+
var _member_hostAccess;
|
|
172
|
+
return isManager(member) || Object.values((_member_hostAccess = member.hostAccess) != null ? _member_hostAccess : {}).includes('admin');
|
|
125
173
|
});
|
|
126
|
-
const
|
|
174
|
+
const reachable = new Set(await deps.reachable(candidates.map((member)=>member.$id)));
|
|
175
|
+
const reached = candidates.filter((member)=>reachable.has(member.$id)).sort((a, b)=>a.$id.localeCompare(b.$id));
|
|
176
|
+
if (!reached.length) return _extends({}, report, {
|
|
177
|
+
skipped: candidates.length ? 'nobody-reachable' : 'no-managers'
|
|
178
|
+
});
|
|
179
|
+
const userDocs = await firestore.getAll(...reached.map((member)=>firestore.collection('users').doc(member.$id)));
|
|
127
180
|
const users = new Map(userDocs.map((doc)=>[
|
|
128
181
|
doc.id,
|
|
129
182
|
doc
|
|
130
183
|
]));
|
|
131
|
-
const recipients =
|
|
184
|
+
const recipients = reached.filter((member)=>{
|
|
132
185
|
var _users_get;
|
|
133
186
|
return digestEnabled((_users_get = users.get(member.$id)) == null ? void 0 : _users_get.get(DIGEST_PREFS_FIELD), WEEKLY_SUMMARY_DIGEST_KEY);
|
|
134
187
|
});
|
|
@@ -149,7 +202,7 @@ async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
|
149
202
|
const orgSlug = str(org['slug']);
|
|
150
203
|
const sites = [];
|
|
151
204
|
for (const host of chosen){
|
|
152
|
-
var _host_data;
|
|
205
|
+
var _host_data, _liveCustomDomain;
|
|
153
206
|
const data = (_host_data = host.data()) != null ? _host_data : {};
|
|
154
207
|
const readers = (await deps.readers({
|
|
155
208
|
orgId,
|
|
@@ -157,19 +210,28 @@ async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
|
157
210
|
org,
|
|
158
211
|
host: data
|
|
159
212
|
})).filter(({ reader })=>pluginFigureSummarySection(reader) === 'site');
|
|
160
|
-
const
|
|
213
|
+
const parts = await readParts(readers, {
|
|
161
214
|
orgId,
|
|
162
215
|
hostId: host.id,
|
|
163
216
|
now: period.now
|
|
164
217
|
});
|
|
165
218
|
const subdomain = str(data['subdomain']);
|
|
219
|
+
const domain = (_liveCustomDomain = liveCustomDomain(data)) != null ? _liveCustomDomain : subdomain ? `${subdomain}.${TENANT_APEX}` : '';
|
|
166
220
|
sites.push({
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
221
|
+
hostId: host.id,
|
|
222
|
+
subdomain,
|
|
223
|
+
site: {
|
|
224
|
+
name: siteName(data, host.id),
|
|
225
|
+
address: domain ? {
|
|
226
|
+
label: domain,
|
|
227
|
+
url: `https://${domain}`
|
|
228
|
+
} : null,
|
|
229
|
+
url: deps.origin && orgSlug && subdomain ? `${deps.origin}${buildRoute(Route.HOST_DASHBOARD, {
|
|
230
|
+
orgSlug,
|
|
231
|
+
host: subdomain
|
|
232
|
+
})}` : null,
|
|
233
|
+
parts
|
|
234
|
+
}
|
|
173
235
|
});
|
|
174
236
|
}
|
|
175
237
|
const orgReaders = await deps.readers({
|
|
@@ -178,98 +240,108 @@ async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
|
178
240
|
org,
|
|
179
241
|
host: null
|
|
180
242
|
});
|
|
181
|
-
const workspace = await
|
|
243
|
+
const workspace = await readParts(orgReaders.filter(({ reader })=>pluginFigureSummarySection(reader) === 'workspace'), {
|
|
182
244
|
orgId,
|
|
183
245
|
hostId: null,
|
|
184
246
|
now: period.now
|
|
185
247
|
});
|
|
248
|
+
report.sites = sites.filter(({ site })=>weeklySummaryHasNews(site)).length;
|
|
249
|
+
if (!report.sites && !workspace.length) return _extends({}, report, {
|
|
250
|
+
skipped: 'nothing-to-report'
|
|
251
|
+
});
|
|
186
252
|
const branding = resolveBrandingProfile(org);
|
|
187
253
|
const workspaceName = str(org['name']) || orgSlug || 'your workspace';
|
|
188
254
|
const settingsUrl = `${deps.origin}${buildRoute(Route.MANAGE_NOTIFICATION_SETTINGS)}`;
|
|
255
|
+
const colors = deps.colors(org);
|
|
189
256
|
// AI weekly insights, pointed to only where the plan includes them and they are released.
|
|
190
257
|
const offersInsights = checkEntitlement(org, 'aiGenerative') && deps.flagOn('release_ai_generative', orgId, org);
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const supportLine = brandSupportLine(branding);
|
|
197
|
-
const subject = composeWeeklySummarySubject(workspaceName, sites);
|
|
198
|
-
const input = {
|
|
258
|
+
const analyticsUrl = (subdomain)=>offersInsights && deps.origin && orgSlug && subdomain ? `${deps.origin}${buildRoute(Route.HOST_ANALYTICS, {
|
|
259
|
+
orgSlug,
|
|
260
|
+
host: subdomain
|
|
261
|
+
})}` : null;
|
|
262
|
+
const common = {
|
|
199
263
|
workspaceName,
|
|
200
264
|
productName: branding.productName,
|
|
201
265
|
periodLabel: period.label,
|
|
202
|
-
|
|
203
|
-
workspace,
|
|
266
|
+
colors,
|
|
204
267
|
settingsUrl
|
|
205
268
|
};
|
|
206
|
-
report.sites = sites.length;
|
|
207
269
|
const lookups = {
|
|
208
270
|
left: WEEKLY_SUMMARY_MAX_UID_LOOKUPS
|
|
209
271
|
};
|
|
210
|
-
|
|
211
|
-
aiInsightsUrl: null
|
|
212
|
-
}))) {
|
|
213
|
-
return _extends({}, report, {
|
|
214
|
-
skipped: 'nothing-to-report'
|
|
215
|
-
});
|
|
216
|
-
}
|
|
272
|
+
const busiest = (_sites_find = sites.find(({ site })=>weeklySummaryHasNews(site))) != null ? _sites_find : sites[0];
|
|
217
273
|
for (const member of recipients){
|
|
218
|
-
var _composeWeeklySummaryEmailText, _composeWeeklySummaryEmailText1;
|
|
219
274
|
var _users_get;
|
|
220
|
-
|
|
221
|
-
|
|
275
|
+
const owed = emailsFor(member, isManager(member), sites, workspace).filter(({ key })=>{
|
|
276
|
+
var _sentAlready_member_$id;
|
|
277
|
+
return !((_sentAlready_member_$id = sentAlready[member.$id]) == null ? void 0 : _sentAlready_member_$id[key]);
|
|
222
278
|
});
|
|
279
|
+
if (!owed.length) continue;
|
|
223
280
|
const subscribed = insightDigestSubscribed((_users_get = users.get(member.$id)) == null ? void 0 : _users_get.get(INSIGHT_DIGESTS_FIELD), orgId);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
report.sent += 1;
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const to = await deps.address(member, lookups);
|
|
234
|
-
if (!to) continue;
|
|
235
|
-
const text = [
|
|
236
|
-
(_composeWeeklySummaryEmailText1 = composeWeeklySummaryEmailText(_extends({}, input, {
|
|
237
|
-
aiInsightsUrl
|
|
238
|
-
}))) != null ? _composeWeeklySummaryEmailText1 : body,
|
|
239
|
-
supportLine
|
|
240
|
-
].filter(Boolean).join('\n\n');
|
|
241
|
-
const result = await deps.send({
|
|
242
|
-
to,
|
|
243
|
-
subject,
|
|
244
|
-
body,
|
|
245
|
-
text,
|
|
246
|
-
org,
|
|
247
|
-
merge: {
|
|
248
|
-
'summary.subject': subject,
|
|
249
|
-
'org.name': workspaceName,
|
|
250
|
-
'summary.period': period.label,
|
|
251
|
-
'summary.body': body,
|
|
252
|
-
settingsUrl
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
if (result.sent) {
|
|
256
|
-
report.sent += 1;
|
|
257
|
-
await markerRef.set({
|
|
258
|
-
week: period.week,
|
|
259
|
-
sent: {
|
|
260
|
-
[member.$id]: true
|
|
261
|
-
},
|
|
262
|
-
updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
263
|
-
}, {
|
|
264
|
-
merge: true
|
|
281
|
+
const to = deps.dryRun ? '' : await deps.address(member, lookups);
|
|
282
|
+
if (!deps.dryRun && !to) continue;
|
|
283
|
+
for (const { key, hostId, withWorkspace } of owed){
|
|
284
|
+
var _ref, _ref1;
|
|
285
|
+
if (overBudget()) return _extends({}, report, {
|
|
286
|
+
deferred: 'time'
|
|
265
287
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
288
|
+
const entry = hostId ? sites.find((site)=>site.hostId === hostId) : null;
|
|
289
|
+
const aiInsightsUrl = subscribed ? null : analyticsUrl((_ref = (_ref1 = entry == null ? void 0 : entry.subdomain) != null ? _ref1 : busiest == null ? void 0 : busiest.subdomain) != null ? _ref : '');
|
|
290
|
+
const email = entry ? composeWeeklySummarySiteEmail(_extends({}, common, {
|
|
291
|
+
aiInsightsUrl,
|
|
292
|
+
site: entry.site
|
|
293
|
+
}, withWorkspace ? {
|
|
294
|
+
workspace
|
|
295
|
+
} : {})) : composeWeeklySummaryOverviewEmail(_extends({}, common, {
|
|
296
|
+
aiInsightsUrl,
|
|
297
|
+
sites: sites.map(({ site })=>site),
|
|
298
|
+
workspace,
|
|
299
|
+
url: deps.origin && orgSlug ? `${deps.origin}${buildRoute(Route.ORG_HOME, {
|
|
300
|
+
orgSlug
|
|
301
|
+
})}` : ''
|
|
302
|
+
}));
|
|
303
|
+
if (!email) continue;
|
|
304
|
+
if (deps.dryRun) {
|
|
305
|
+
report.sent += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const result = await deps.send({
|
|
309
|
+
to: to,
|
|
310
|
+
template: entry ? WEEKLY_SUMMARY_SITE_TEMPLATE : WEEKLY_SUMMARY_TEMPLATE,
|
|
311
|
+
email,
|
|
312
|
+
org,
|
|
313
|
+
merge: {
|
|
314
|
+
'summary.subject': email.subject,
|
|
315
|
+
'summary.title': email.title,
|
|
316
|
+
'summary.period': email.period,
|
|
317
|
+
'summary.html': email.html,
|
|
318
|
+
'summary.buttonLabel': email.buttonLabel,
|
|
319
|
+
'summary.url': email.url,
|
|
320
|
+
'org.name': workspaceName,
|
|
321
|
+
settingsUrl
|
|
322
|
+
}
|
|
269
323
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
324
|
+
if (result.sent) {
|
|
325
|
+
report.sent += 1;
|
|
326
|
+
await markerRef.set({
|
|
327
|
+
week: period.week,
|
|
328
|
+
sent: {
|
|
329
|
+
[member.$id]: {
|
|
330
|
+
[key]: true
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
334
|
+
}, {
|
|
335
|
+
merge: true
|
|
336
|
+
});
|
|
337
|
+
} else if (result.rateLimited) {
|
|
338
|
+
return _extends({}, report, {
|
|
339
|
+
deferred: 'rate'
|
|
340
|
+
});
|
|
341
|
+
} else {
|
|
342
|
+
var _report_failed;
|
|
343
|
+
report.failed = ((_report_failed = report.failed) != null ? _report_failed : 0) + 1;
|
|
344
|
+
}
|
|
273
345
|
}
|
|
274
346
|
}
|
|
275
347
|
return report;
|
|
@@ -340,11 +412,11 @@ async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
|
340
412
|
}
|
|
341
413
|
return report;
|
|
342
414
|
}
|
|
343
|
-
/** The sweep's reach into the real platform: Firestore, the flags, the roster, the mail. */ export async function weeklySummaryDeps(options) {
|
|
415
|
+
/** The sweep's reach into the real platform: Firestore, the flags, the roster, the mail. */ /** The sweep's reach into the real platform: Firestore, the flags, the roster, the mail. */ export async function weeklySummaryDeps(options) {
|
|
344
416
|
const firestore = firebaseAdmin.app().firestore();
|
|
345
417
|
const flags = await getServerReleaseFlagValues();
|
|
346
|
-
//
|
|
347
|
-
|
|
418
|
+
// Each designed email is read once per call and rendered per recipient.
|
|
419
|
+
const loaded = new Map();
|
|
348
420
|
return {
|
|
349
421
|
firestore,
|
|
350
422
|
runAt: options.runAt,
|
|
@@ -356,6 +428,20 @@ async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
|
356
428
|
readers: (context)=>eligibleFigureReaders(_extends({}, context, {
|
|
357
429
|
accept: (reader)=>Boolean(reader.weeklySummary)
|
|
358
430
|
})),
|
|
431
|
+
colors: (org)=>{
|
|
432
|
+
const { palette } = systemEmailTheme(systemEmailBrand(org).options);
|
|
433
|
+
const colors = {};
|
|
434
|
+
for (const key of Object.keys(COLOR_TOKENS)){
|
|
435
|
+
var _ref, _resolveEmailColor;
|
|
436
|
+
// A brand palette that cannot resolve a token falls back to the
|
|
437
|
+
// platform's, which resolves every one of them.
|
|
438
|
+
colors[key] = (_ref = (_resolveEmailColor = resolveEmailColor(COLOR_TOKENS[key], palette)) != null ? _resolveEmailColor : resolveEmailColor(COLOR_TOKENS[key], PLATFORM_EMAIL_PALETTE)) != null ? _ref : 'inherit';
|
|
439
|
+
}
|
|
440
|
+
// Links take the color the renderer gives every other link in the
|
|
441
|
+
// email, not the raw primary, which is too light to read as text.
|
|
442
|
+
colors.link = emailAccent(palette).link;
|
|
443
|
+
return colors;
|
|
444
|
+
},
|
|
359
445
|
address: async (member, lookups)=>{
|
|
360
446
|
let address = str(member.email).toLowerCase();
|
|
361
447
|
if (!address.includes('@') && lookups.left > 0) {
|
|
@@ -370,22 +456,25 @@ async function summarizeOrg(deps, orgDoc, period, overBudget) {
|
|
|
370
456
|
], firestore);
|
|
371
457
|
return kept != null ? kept : null;
|
|
372
458
|
},
|
|
373
|
-
send: async (email)=>{
|
|
459
|
+
send: async ({ to, template, email, org, merge })=>{
|
|
374
460
|
var _result_reason;
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const brand = systemEmailBrand(
|
|
378
|
-
const rendered =
|
|
461
|
+
if (!loaded.has(template)) loaded.set(template, loadSystemEmail(template).catch(()=>null));
|
|
462
|
+
const design = await loaded.get(template);
|
|
463
|
+
const brand = systemEmailBrand(org);
|
|
464
|
+
const rendered = design ? renderLoadedSystemEmail(design, _extends({}, brand.merge, merge), brand.options) : null;
|
|
465
|
+
const content = systemEmailContent(rendered, {
|
|
466
|
+
subject: email.subject,
|
|
467
|
+
text: email.text
|
|
468
|
+
});
|
|
379
469
|
const result = await sendEmail(_extends({
|
|
380
470
|
to: [
|
|
381
|
-
|
|
471
|
+
to
|
|
382
472
|
]
|
|
383
|
-
},
|
|
384
|
-
|
|
385
|
-
text: email.text
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
context: 'weekly-summary',
|
|
473
|
+
}, content, {
|
|
474
|
+
// The figures' plain text, rather than the drawn block with its tags stripped.
|
|
475
|
+
text: email.text,
|
|
476
|
+
fromName: resolveBrandingProfile(org).fromName,
|
|
477
|
+
context: template,
|
|
389
478
|
priority: 'bulk'
|
|
390
479
|
}));
|
|
391
480
|
if (result.sent) {
|