@aglyn/aglyn 1.0.0-beta.261 → 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.
@@ -0,0 +1,351 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { pluginFigureCellText, pluginFigureIsoWeek, pluginFigureWindows } from "../plugin-manager/plugin-figures.js";
18
+ /**
19
+ * THE WEEKLY SUMMARY (AGL-3867): the free Monday emails that say how a
20
+ * workspace did the week before — one for each site that had something to
21
+ * report, and an overview of the workspace that adds the sites up.
22
+ *
23
+ * They spend nothing. Every figure comes from a `plugin-figures` reader that
24
+ * declares a `weeklySummary`, and that reader's owner says what its table
25
+ * means as tiles and ranked rows, so each plugin's counting rules decide what
26
+ * a figure is and nothing here names one. AI weekly insights (AGL-2915) are
27
+ * the paid version that explains the figures, and stay opt-in because each
28
+ * one spends the workspace's AI credits.
29
+ *
30
+ * This module is the period, the wording helpers the readers use, and both
31
+ * emails' words and layout; the reads and the sends are
32
+ * `@aglyn/tenant-data-admin/server/weekly-summary`. The layout is drawn as
33
+ * email-safe tables with inline styles, in colors the caller resolves from
34
+ * the email theme.
35
+ */ /** The key a person's switch is stored under in `users/{uid}.digestPrefs`; on until turned off. */ export const WEEKLY_SUMMARY_DIGEST_KEY = 'weeklySummary';
36
+ /** The window every figure is read over. */ export const WEEKLY_SUMMARY_DAYS = 7;
37
+ /** Rows a ranked list names. */ export const WEEKLY_SUMMARY_TOP_ROWS = 5;
38
+ /** Counts a subject line or the overview's tiles lead with, at most. */ export const WEEKLY_SUMMARY_HEADLINE_COUNTS = 3;
39
+ /** Tiles in one row of an email. */ const TILES_PER_ROW = 3;
40
+ /**
41
+ * The week a run reports on: the seven whole UTC days before the day it runs,
42
+ * so a Monday run covers Monday to Sunday and never a part-day of today.
43
+ * `now` is what each reader is handed; `week` keys the run's marker.
44
+ */ export function weeklySummaryPeriod(runAt) {
45
+ const startOfToday = Date.UTC(runAt.getUTCFullYear(), runAt.getUTCMonth(), runAt.getUTCDate());
46
+ const now = new Date(startOfToday - 1);
47
+ const { current } = pluginFigureWindows(now, WEEKLY_SUMMARY_DAYS);
48
+ return {
49
+ now,
50
+ week: pluginFigureIsoWeek(now),
51
+ from: current.from,
52
+ to: current.to,
53
+ label: `${weeklySummaryDay(current.from)} – ${weeklySummaryDay(current.to)}`
54
+ };
55
+ }
56
+ /** `2026-10-05` as "Oct 5". */ export function weeklySummaryDay(day) {
57
+ const at = new Date(`${day}T00:00:00Z`);
58
+ return at.toLocaleDateString('en-US', {
59
+ month: 'short',
60
+ day: 'numeric',
61
+ timeZone: 'UTC'
62
+ });
63
+ }
64
+ // ── Helpers the readers word their parts with ──────────────────────────────
65
+ /** A cell as a number, `0` for anything that is not one. */ export function figureCount(value) {
66
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
67
+ }
68
+ /** The row whose `key` cell is `label`: "Visitors" in a table of figures. */ export function figureRow(table, key, label) {
69
+ return table.rows.find((row)=>row[key] === label);
70
+ }
71
+ /** Whether a table carries a column, so a reader that changed shape is noticed rather than misread. */ export function figureHasColumn(table, key) {
72
+ return table.columns.some((column)=>column.key === key);
73
+ }
74
+ /** A cell as a tile, with its change when the table has one. */ export function figureStat(table, row, key, label, options = {}) {
75
+ var _row_key;
76
+ const change = options.change ? row[options.change] : undefined;
77
+ return _extends({
78
+ label,
79
+ value: pluginFigureCellText(table, key, (_row_key = row[key]) != null ? _row_key : 0)
80
+ }, typeof change === 'number' ? {
81
+ change
82
+ } : {}, options.total ? {
83
+ total: _extends({
84
+ count: figureCount(row[key])
85
+ }, options.total)
86
+ } : {});
87
+ }
88
+ /** The named rows with the most of `key`, as ranked rows; `[]` when none has any. */ export function figureRanked(table, nameKey, key, rows = table.rows) {
89
+ return rows.filter((row)=>typeof row[nameKey] === 'string' && figureCount(row[key]) > 0).slice(0, WEEKLY_SUMMARY_TOP_ROWS).map((row)=>{
90
+ var _row_key;
91
+ return {
92
+ label: String(row[nameKey]),
93
+ value: pluginFigureCellText(table, key, (_row_key = row[key]) != null ? _row_key : 0)
94
+ };
95
+ });
96
+ }
97
+ /** "1 order", "18 orders". */ export function figurePlural(count, one, many) {
98
+ return `${count.toLocaleString('en-US')} ${count === 1 ? one : many}`;
99
+ }
100
+ /** A site with anything to say. */ export function weeklySummaryHasNews(site) {
101
+ return site.parts.length > 0;
102
+ }
103
+ /** The counts the readers offered for adding up, summed by noun, in the order they were first given. */ export function weeklySummaryTotals(sites) {
104
+ const sums = new Map();
105
+ for (const site of sites){
106
+ for (const part of site.parts){
107
+ var _part_stats;
108
+ for (const stat of (_part_stats = part.stats) != null ? _part_stats : []){
109
+ if (!stat.total || stat.total.count <= 0) continue;
110
+ const sum = sums.get(stat.total.one);
111
+ sums.set(stat.total.one, sum ? _extends({}, sum, {
112
+ count: sum.count + stat.total.count
113
+ }) : _extends({}, stat.total));
114
+ }
115
+ }
116
+ }
117
+ return [
118
+ ...sums.values()
119
+ ];
120
+ }
121
+ function headline(sites) {
122
+ return weeklySummaryTotals(sites).slice(0, WEEKLY_SUMMARY_HEADLINE_COUNTS).map(({ count, one, many })=>figurePlural(count, one, many)).join(', ');
123
+ }
124
+ /** "+12.5%", "−27.9%", "no change"; `null` with nothing to compare with. */ export function weeklySummaryChange(change) {
125
+ if (typeof change !== 'number' || !Number.isFinite(change)) return null;
126
+ if (change === 0) return {
127
+ text: 'no change',
128
+ sign: 0
129
+ };
130
+ const amount = Math.abs(change).toLocaleString('en-US', {
131
+ maximumFractionDigits: 1
132
+ });
133
+ return change > 0 ? {
134
+ text: `+${amount}%`,
135
+ sign: 1
136
+ } : {
137
+ text: `−${amount}%`,
138
+ sign: -1
139
+ };
140
+ }
141
+ const esc = (value)=>value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
142
+ const FONT = 'Helvetica, Arial, sans-serif';
143
+ const capitalized = (value)=>value.charAt(0).toUpperCase() + value.slice(1);
144
+ /**
145
+ * A label that reads like an address — `www.google.com` — kept from being
146
+ * turned into a link by the inbox, which would draw it in the inbox's own
147
+ * blue beside the theme's links. A zero-width space after each dot breaks
148
+ * the detection and nothing a reader sees.
149
+ */ const unlinked = (value)=>esc(value).replace(/\./g, '.\u200b');
150
+ function changeHtml(change, colors) {
151
+ const said = weeklySummaryChange(change);
152
+ if (!said) return '';
153
+ const color = said.sign > 0 ? colors.up : said.sign < 0 ? colors.down : colors.muted;
154
+ const arrow = said.sign > 0 ? '▲ ' : said.sign < 0 ? '▼ ' : '';
155
+ return `<div style="font-family:${FONT};font-size:13px;line-height:1.4;font-weight:600;color:${color};">${arrow}${esc(said.text)}</div>`;
156
+ }
157
+ /**
158
+ * How many tiles share each row: one row of up to three, two rows of two for
159
+ * four, then rows of three — so a row is never left with an empty slot
160
+ * beside two tiles.
161
+ */ export function weeklySummaryTileRows(count) {
162
+ if (count <= 0) return [];
163
+ if (count <= TILES_PER_ROW) return [
164
+ count
165
+ ];
166
+ if (count === 4) return [
167
+ 2,
168
+ 2
169
+ ];
170
+ const rows = [];
171
+ for(let left = count; left > 0; left -= TILES_PER_ROW)rows.push(Math.min(TILES_PER_ROW, left));
172
+ return rows;
173
+ }
174
+ /** Tiles, each a value, its label and its change, filling the width of their row. */ function tilesHtml(stats, colors) {
175
+ const rows = [];
176
+ let at = 0;
177
+ for (const size of weeklySummaryTileRows(stats.length)){
178
+ const cells = stats.slice(at, at + size);
179
+ at += size;
180
+ // A tile alone takes half the row, not all of it, so it reads as a tile.
181
+ const columns = Math.max(size, 2);
182
+ const width = `${Math.floor(100 / columns)}%`;
183
+ const tds = cells.map((stat, index)=>{
184
+ // The gutter is split between neighbours, so the row's outer tiles sit
185
+ // flush with the column's edges.
186
+ const left = index === 0 ? 0 : 4;
187
+ const right = index === cells.length - 1 ? 0 : 4;
188
+ return `<td style="width:${width};padding:0 ${right}px 8px ${left}px;vertical-align:top;">` + `<div style="background-color:${colors.surface};border:1px solid ${colors.border};border-radius:8px;padding:12px 14px;">` + `<div style="font-family:${FONT};font-size:24px;line-height:1.2;font-weight:700;color:${colors.text};">${esc(stat.value)}</div>` + `<div style="font-family:${FONT};font-size:13px;line-height:1.4;color:${colors.muted};">${esc(stat.label)}</div>` + changeHtml(stat.change, colors) + `</div></td>`;
189
+ });
190
+ while(tds.length < columns)tds.push(`<td style="width:${width};padding:0 0 8px 4px;"></td>`);
191
+ rows.push(`<tr>${tds.join('')}</tr>`);
192
+ }
193
+ return `<table role="presentation" style="width:100%;border-collapse:collapse;table-layout:fixed;">${rows.join('')}</table>`;
194
+ }
195
+ /** A ranked list: the label left, its figure right, a hairline between. */ function rowsHtml(rows, colors) {
196
+ const trs = rows.map((row, index)=>`<tr>` + `<td style="font-family:${FONT};font-size:14px;line-height:1.4;color:${colors.text};padding:8px 12px 8px 0;` + `${index < rows.length - 1 ? `border-bottom:1px solid ${colors.border};` : ''}word-break:break-word;">${unlinked(row.label)}</td>` + `<td style="font-family:${FONT};font-size:14px;line-height:1.4;font-weight:600;color:${colors.text};padding:8px 0;text-align:right;white-space:nowrap;` + `${index < rows.length - 1 ? `border-bottom:1px solid ${colors.border};` : ''}">${esc(row.value)}</td>` + `</tr>`);
197
+ return `<table role="presentation" style="width:100%;border-collapse:collapse;">${trs.join('')}</table>`;
198
+ }
199
+ function sectionTitleHtml(title, colors) {
200
+ return `<div style="font-family:${FONT};font-size:12px;line-height:1.4;font-weight:700;letter-spacing:0.06em;` + `text-transform:uppercase;color:${colors.muted};padding:0 0 8px;">${esc(title)}</div>`;
201
+ }
202
+ function partHtml(part, colors) {
203
+ var _part_stats, _part_rows;
204
+ return `<div style="padding:16px 0 8px;">` + sectionTitleHtml(part.title, colors) + (part.note ? `<div style="font-family:${FONT};font-size:13px;line-height:1.5;color:${colors.muted};padding:0 0 8px;">${esc(part.note)}</div>` : '') + (((_part_stats = part.stats) == null ? void 0 : _part_stats.length) ? tilesHtml(part.stats, colors) : '') + (((_part_rows = part.rows) == null ? void 0 : _part_rows.length) ? rowsHtml(part.rows, colors) : '') + `</div>`;
205
+ }
206
+ /** "aglyn.com · Aglyn LLC": which site, linked, and whose. */ function addressHtml(site, workspaceName, colors) {
207
+ const address = site.address ? `<a href="${esc(site.address.url)}" style="color:${colors.link};text-decoration:underline;font-weight:600;">${esc(site.address.label)}</a>` : `<span style="font-weight:600;">${esc(site.name)}</span>`;
208
+ return `<div style="font-family:${FONT};font-size:14px;line-height:1.5;color:${colors.muted};padding:0 0 4px;">` + `${address} · ${esc(workspaceName)}</div>`;
209
+ }
210
+ function insightsHtml(url, colors) {
211
+ return `<div style="margin:16px 0 0;background-color:${colors.surface};border-left:3px solid ${colors.accent};border-radius:4px;padding:14px 16px;">` + `<div style="font-family:${FONT};font-size:14px;line-height:1.5;font-weight:700;color:${colors.text};">Want to know why the numbers moved?</div>` + `<div style="font-family:${FONT};font-size:14px;line-height:1.5;color:${colors.text};">` + `AI weekly insights explain each site's week in a few plain sentences. ` + `<a href="${esc(url)}" style="color:${colors.link};text-decoration:underline;">Turn them on from Ask about your numbers</a>.` + `</div></div>`;
212
+ }
213
+ function partText(part) {
214
+ var _part_stats, _part_rows;
215
+ const lines = [
216
+ part.title.toUpperCase()
217
+ ];
218
+ if (part.note) lines.push(part.note);
219
+ for (const stat of (_part_stats = part.stats) != null ? _part_stats : []){
220
+ const said = weeklySummaryChange(stat.change);
221
+ lines.push(`${stat.label}: ${stat.value}${said ? ` (${said.text})` : ''}`);
222
+ }
223
+ for (const row of (_part_rows = part.rows) != null ? _part_rows : [])lines.push(`- ${row.label}: ${row.value}`);
224
+ return lines;
225
+ }
226
+ function closing(input) {
227
+ return [
228
+ '',
229
+ `You get this summary because you manage ${input.workspaceName}. Turn it off in Notifications: ${input.settingsUrl}`
230
+ ];
231
+ }
232
+ /**
233
+ * One site's email, or `null` for a site with nothing to report. `workspace`
234
+ * carries the workspace's own parts when this site's email stands in for the
235
+ * overview, in a workspace with one active site.
236
+ */ export function composeWeeklySummarySiteEmail(input) {
237
+ var _input_workspace, _input_site_url;
238
+ const workspace = (_input_workspace = input.workspace) != null ? _input_workspace : [];
239
+ if (!weeklySummaryHasNews(input.site) && !workspace.length) return null;
240
+ const lead = headline([
241
+ input.site
242
+ ]);
243
+ const subject = lead ? `${input.site.name} last week: ${lead}` : `${input.site.name} last week`;
244
+ const html = addressHtml(input.site, input.workspaceName, input.colors) + input.site.parts.map((part)=>partHtml(part, input.colors)).join('') + (workspace.length ? `<div style="border-top:1px solid ${input.colors.border};margin:16px 0 0;"></div>` + workspace.map((part)=>partHtml(_extends({}, part, {
245
+ title: `${input.workspaceName} · ${part.title}`
246
+ }), input.colors)).join('') : '') + (input.aiInsightsUrl ? insightsHtml(input.aiInsightsUrl, input.colors) : '');
247
+ const text = [
248
+ `${input.site.name} last week, ${input.periodLabel}. A change in brackets is against the week before.`,
249
+ `Site: ${input.site.address ? `${input.site.address.label} (${input.site.address.url})` : input.site.name} · ${input.workspaceName}`,
250
+ ...input.site.parts.flatMap((part)=>[
251
+ '',
252
+ ...partText(part)
253
+ ]),
254
+ ...workspace.flatMap((part)=>[
255
+ '',
256
+ ...partText(_extends({}, part, {
257
+ title: `${input.workspaceName} · ${part.title}`
258
+ }))
259
+ ]),
260
+ ...input.site.url ? [
261
+ '',
262
+ `Open ${input.site.name} in ${input.productName}: ${input.site.url}`
263
+ ] : [],
264
+ ...input.aiInsightsUrl ? [
265
+ '',
266
+ `Want to know why the numbers moved? Turn on AI weekly insights from Ask about your numbers: ${input.aiInsightsUrl}`
267
+ ] : [],
268
+ ...closing(input)
269
+ ].join('\n');
270
+ return {
271
+ subject,
272
+ title: `${input.site.name} last week`,
273
+ period: `${input.periodLabel}, compared with the week before`,
274
+ html,
275
+ text,
276
+ buttonLabel: `Open ${input.site.name}`,
277
+ url: (_input_site_url = input.site.url) != null ? _input_site_url : ''
278
+ };
279
+ }
280
+ /**
281
+ * The workspace overview: the week's totals across its sites, a line for each
282
+ * site, and the workspace's own figures, or `null` when nothing anywhere has
283
+ * anything to report.
284
+ */ export function composeWeeklySummaryOverviewEmail(input) {
285
+ const active = input.sites.filter(weeklySummaryHasNews);
286
+ if (!active.length && !input.workspace.length) return null;
287
+ const totals = weeklySummaryTotals(active);
288
+ const lead = totals.slice(0, WEEKLY_SUMMARY_HEADLINE_COUNTS).map(({ count, one, many })=>figurePlural(count, one, many)).join(', ');
289
+ const subject = lead ? `Your week at ${input.workspaceName}: ${lead}` : `Your week at ${input.workspaceName}`;
290
+ const { colors } = input;
291
+ const totalsPart = totals.length ? {
292
+ title: active.length === 1 ? 'At a glance' : `Across ${active.length} sites`,
293
+ stats: totals.slice(0, TILES_PER_ROW * 2).map(({ count, one, many })=>({
294
+ label: capitalized(count === 1 ? one : many),
295
+ value: count.toLocaleString('en-US')
296
+ }))
297
+ } : null;
298
+ const siteLine = (site)=>site.parts.flatMap((part)=>{
299
+ var _part_stats;
300
+ return (_part_stats = part.stats) != null ? _part_stats : [];
301
+ }).filter((stat)=>stat.total && stat.total.count > 0).slice(0, WEEKLY_SUMMARY_HEADLINE_COUNTS).map((stat)=>{
302
+ const total = stat.total;
303
+ const said = weeklySummaryChange(stat.change);
304
+ return `${figurePlural(total.count, total.one, total.many)}${said ? ` (${said.text})` : ''}`;
305
+ }).join(' · ');
306
+ const quietCount = input.sites.length - active.length;
307
+ const quiet = quietCount ? `${figurePlural(quietCount, 'other site', 'other sites')} had nothing to report.` : '';
308
+ const sitesHtml = active.length ? `<div style="padding:16px 0 8px;">` + sectionTitleHtml('Your sites', colors) + `<table role="presentation" style="width:100%;border-collapse:collapse;">` + active.map((site, index)=>{
309
+ const border = index < active.length - 1 ? `border-bottom:1px solid ${colors.border};` : '';
310
+ const name = site.url ? `<a href="${esc(site.url)}" style="color:${colors.link};text-decoration:none;font-weight:700;">${esc(site.name)}</a>` : `<span style="font-weight:700;">${esc(site.name)}</span>`;
311
+ const line = siteLine(site);
312
+ const address = site.address ? ` <a href="${esc(site.address.url)}" style="color:${colors.muted};text-decoration:underline;font-size:13px;font-weight:400;">${esc(site.address.label)}</a>` : '';
313
+ return `<tr><td style="font-family:${FONT};font-size:15px;line-height:1.4;color:${colors.text};padding:10px 0;${border}">` + name + address + (line ? `<div style="font-size:13px;line-height:1.5;color:${colors.muted};">${esc(line)}</div>` : '') + `</td></tr>`;
314
+ }).join('') + `</table>` + (quiet ? `<div style="font-family:${FONT};font-size:13px;line-height:1.5;color:${colors.muted};padding:4px 0 0;">${esc(quiet)}</div>` : '') + `</div>` : '';
315
+ const html = (totalsPart ? partHtml(totalsPart, colors) : '') + sitesHtml + input.workspace.map((part)=>partHtml(part, colors)).join('') + (active.length > 1 ? `<div style="font-family:${FONT};font-size:13px;line-height:1.5;color:${colors.muted};padding:8px 0 0;">Each site's own summary arrives beside this one.</div>` : '') + (input.aiInsightsUrl ? insightsHtml(input.aiInsightsUrl, colors) : '');
316
+ const text = [
317
+ `Here is how ${input.workspaceName} did last week, ${input.periodLabel}. A change in brackets is against the week before.`,
318
+ ...totalsPart ? [
319
+ '',
320
+ ...partText(totalsPart)
321
+ ] : [],
322
+ ...active.length ? [
323
+ '',
324
+ 'YOUR SITES',
325
+ ...active.map((site)=>`- ${site.name}${site.address ? ` (${site.address.label})` : ''}: ${siteLine(site) || 'see its summary'}` + (site.url ? ` · ${site.url}` : '')),
326
+ ...quiet ? [
327
+ quiet
328
+ ] : []
329
+ ] : [],
330
+ ...input.workspace.flatMap((part)=>[
331
+ '',
332
+ ...partText(part)
333
+ ]),
334
+ ...input.aiInsightsUrl ? [
335
+ '',
336
+ `Want to know why the numbers moved? Turn on AI weekly insights from Ask about your numbers: ${input.aiInsightsUrl}`
337
+ ] : [],
338
+ ...closing(input)
339
+ ].join('\n');
340
+ return {
341
+ subject,
342
+ title: `${input.workspaceName} last week`,
343
+ period: `${input.periodLabel}, compared with the week before`,
344
+ html,
345
+ text,
346
+ buttonLabel: `Open ${input.workspaceName}`,
347
+ url: input.url
348
+ };
349
+ }
350
+
351
+ //# sourceMappingURL=weekly-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/weekly-summary.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 {\n pluginFigureCellText,\n pluginFigureIsoWeek,\n pluginFigureWindows,\n type PluginFigureCell,\n type PluginFigureRow,\n type PluginFigureSummaryPart,\n type PluginFigureSummaryStat,\n type PluginFigureTable,\n} from '../plugin-manager/plugin-figures'\n\n/**\n * THE WEEKLY SUMMARY (AGL-3867): the free Monday emails that say how a\n * workspace did the week before — one for each site that had something to\n * report, and an overview of the workspace that adds the sites up.\n *\n * They spend nothing. Every figure comes from a `plugin-figures` reader that\n * declares a `weeklySummary`, and that reader's owner says what its table\n * means as tiles and ranked rows, so each plugin's counting rules decide what\n * a figure is and nothing here names one. AI weekly insights (AGL-2915) are\n * the paid version that explains the figures, and stay opt-in because each\n * one spends the workspace's AI credits.\n *\n * This module is the period, the wording helpers the readers use, and both\n * emails' words and layout; the reads and the sends are\n * `@aglyn/tenant-data-admin/server/weekly-summary`. The layout is drawn as\n * email-safe tables with inline styles, in colors the caller resolves from\n * the email theme.\n */\n\n/** The key a person's switch is stored under in `users/{uid}.digestPrefs`; on until turned off. */\nexport const WEEKLY_SUMMARY_DIGEST_KEY = 'weeklySummary'\n\n/** The window every figure is read over. */\nexport const WEEKLY_SUMMARY_DAYS = 7\n\n/** Rows a ranked list names. */\nexport const WEEKLY_SUMMARY_TOP_ROWS = 5\n\n/** Counts a subject line or the overview's tiles lead with, at most. */\nexport const WEEKLY_SUMMARY_HEADLINE_COUNTS = 3\n\n/** Tiles in one row of an email. */\nconst TILES_PER_ROW = 3\n\n/**\n * The week a run reports on: the seven whole UTC days before the day it runs,\n * so a Monday run covers Monday to Sunday and never a part-day of today.\n * `now` is what each reader is handed; `week` keys the run's marker.\n */\nexport function weeklySummaryPeriod(runAt: Date): {\n now: Date\n week: string\n from: string\n to: string\n label: string\n} {\n const startOfToday = Date.UTC(runAt.getUTCFullYear(), runAt.getUTCMonth(), runAt.getUTCDate())\n const now = new Date(startOfToday - 1)\n const { current } = pluginFigureWindows(now, WEEKLY_SUMMARY_DAYS)\n return {\n now,\n week: pluginFigureIsoWeek(now),\n from: current.from,\n to: current.to,\n label: `${weeklySummaryDay(current.from)} – ${weeklySummaryDay(current.to)}`,\n }\n}\n\n/** `2026-10-05` as \"Oct 5\". */\nexport function weeklySummaryDay(day: string): string {\n const at = new Date(`${day}T00:00:00Z`)\n return at.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' })\n}\n\n// ── Helpers the readers word their parts with ──────────────────────────────\n\n/** A cell as a number, `0` for anything that is not one. */\nexport function figureCount(value: PluginFigureCell | undefined): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\n/** The row whose `key` cell is `label`: \"Visitors\" in a table of figures. */\nexport function figureRow(table: PluginFigureTable, key: string, label: string): PluginFigureRow | undefined {\n return table.rows.find((row) => row[key] === label)\n}\n\n/** Whether a table carries a column, so a reader that changed shape is noticed rather than misread. */\nexport function figureHasColumn(table: PluginFigureTable, key: string): boolean {\n return table.columns.some((column) => column.key === key)\n}\n\n/** A cell as a tile, with its change when the table has one. */\nexport function figureStat(\n table: PluginFigureTable,\n row: PluginFigureRow,\n key: string,\n label: string,\n options: { change?: string; total?: { one: string; many: string } } = {},\n): PluginFigureSummaryStat {\n const change = options.change ? row[options.change] : undefined\n return {\n label,\n value: pluginFigureCellText(table, key, row[key] ?? 0),\n ...(typeof change === 'number' ? { change } : {}),\n ...(options.total ? { total: { count: figureCount(row[key]), ...options.total } } : {}),\n }\n}\n\n/** The named rows with the most of `key`, as ranked rows; `[]` when none has any. */\nexport function figureRanked(\n table: PluginFigureTable,\n nameKey: string,\n key: string,\n rows: readonly PluginFigureRow[] = table.rows,\n): Array<{ label: string; value: string }> {\n return rows\n .filter((row) => typeof row[nameKey] === 'string' && figureCount(row[key]) > 0)\n .slice(0, WEEKLY_SUMMARY_TOP_ROWS)\n .map((row) => ({ label: String(row[nameKey]), value: pluginFigureCellText(table, key, row[key] ?? 0) }))\n}\n\n/** \"1 order\", \"18 orders\". */\nexport function figurePlural(count: number, one: string, many: string): string {\n return `${count.toLocaleString('en-US')} ${count === 1 ? one : many}`\n}\n\n// ── The emails ─────────────────────────────────────────────────────────────\n\n/** One site's figures for the week. */\nexport interface WeeklySummarySite {\n name: string\n /** The site's dashboard in the console, absolute; `null` with no origin to build it from. */\n url: string | null\n /**\n * Where the site itself is, so a reader knows which site this is: its\n * custom domain when it serves on one, else its platform address. `null`\n * for a site with no address yet.\n */\n address?: { label: string; url: string } | null\n parts: readonly PluginFigureSummaryPart[]\n}\n\n/** The colors an email is drawn in, resolved from its theme by the caller. */\nexport interface WeeklySummaryColors {\n text: string\n muted: string\n border: string\n /** A tile's ground. */\n surface: string\n /** The accent rule beside a callout. */\n accent: string\n /** Links: the email theme's own link color, the one every other link in the email wears. */\n link: string\n /** A rise and a fall. */\n up: string\n down: string\n}\n\n/** A site with anything to say. */\nexport function weeklySummaryHasNews(site: Pick<WeeklySummarySite, 'parts'>): boolean {\n return site.parts.length > 0\n}\n\n/** The counts the readers offered for adding up, summed by noun, in the order they were first given. */\nexport function weeklySummaryTotals(\n sites: ReadonlyArray<Pick<WeeklySummarySite, 'parts'>>,\n): Array<{ count: number; one: string; many: string }> {\n const sums = new Map<string, { count: number; one: string; many: string }>()\n for (const site of sites) {\n for (const part of site.parts) {\n for (const stat of part.stats ?? []) {\n if (!stat.total || stat.total.count <= 0) continue\n const sum = sums.get(stat.total.one)\n sums.set(stat.total.one, sum ? { ...sum, count: sum.count + stat.total.count } : { ...stat.total })\n }\n }\n }\n return [...sums.values()]\n}\n\nfunction headline(sites: ReadonlyArray<Pick<WeeklySummarySite, 'parts'>>): string {\n return weeklySummaryTotals(sites)\n .slice(0, WEEKLY_SUMMARY_HEADLINE_COUNTS)\n .map(({ count, one, many }) => figurePlural(count, one, many))\n .join(', ')\n}\n\n/** \"+12.5%\", \"−27.9%\", \"no change\"; `null` with nothing to compare with. */\nexport function weeklySummaryChange(change: number | null | undefined): { text: string; sign: 1 | -1 | 0 } | null {\n if (typeof change !== 'number' || !Number.isFinite(change)) return null\n if (change === 0) return { text: 'no change', sign: 0 }\n const amount = Math.abs(change).toLocaleString('en-US', { maximumFractionDigits: 1 })\n return change > 0 ? { text: `+${amount}%`, sign: 1 } : { text: `−${amount}%`, sign: -1 }\n}\n\nconst esc = (value: string): string =>\n value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n\nconst FONT = 'Helvetica, Arial, sans-serif'\n\nconst capitalized = (value: string): string => value.charAt(0).toUpperCase() + value.slice(1)\n\n/**\n * A label that reads like an address — `www.google.com` — kept from being\n * turned into a link by the inbox, which would draw it in the inbox's own\n * blue beside the theme's links. A zero-width space after each dot breaks\n * the detection and nothing a reader sees.\n */\nconst unlinked = (value: string): string => esc(value).replace(/\\./g, '.\\u200b')\n\nfunction changeHtml(change: number | null | undefined, colors: WeeklySummaryColors): string {\n const said = weeklySummaryChange(change)\n if (!said) return ''\n const color = said.sign > 0 ? colors.up : said.sign < 0 ? colors.down : colors.muted\n const arrow = said.sign > 0 ? '▲ ' : said.sign < 0 ? '▼ ' : ''\n return `<div style=\"font-family:${FONT};font-size:13px;line-height:1.4;font-weight:600;color:${color};\">${arrow}${esc(said.text)}</div>`\n}\n\n/**\n * How many tiles share each row: one row of up to three, two rows of two for\n * four, then rows of three — so a row is never left with an empty slot\n * beside two tiles.\n */\nexport function weeklySummaryTileRows(count: number): number[] {\n if (count <= 0) return []\n if (count <= TILES_PER_ROW) return [count]\n if (count === 4) return [2, 2]\n const rows: number[] = []\n for (let left = count; left > 0; left -= TILES_PER_ROW) rows.push(Math.min(TILES_PER_ROW, left))\n return rows\n}\n\n/** Tiles, each a value, its label and its change, filling the width of their row. */\nfunction tilesHtml(stats: readonly PluginFigureSummaryStat[], colors: WeeklySummaryColors): string {\n const rows: string[] = []\n let at = 0\n for (const size of weeklySummaryTileRows(stats.length)) {\n const cells = stats.slice(at, at + size)\n at += size\n // A tile alone takes half the row, not all of it, so it reads as a tile.\n const columns = Math.max(size, 2)\n const width = `${Math.floor(100 / columns)}%`\n const tds = cells.map((stat, index) => {\n // The gutter is split between neighbours, so the row's outer tiles sit\n // flush with the column's edges.\n const left = index === 0 ? 0 : 4\n const right = index === cells.length - 1 ? 0 : 4\n return (\n `<td style=\"width:${width};padding:0 ${right}px 8px ${left}px;vertical-align:top;\">` +\n `<div style=\"background-color:${colors.surface};border:1px solid ${colors.border};border-radius:8px;padding:12px 14px;\">` +\n `<div style=\"font-family:${FONT};font-size:24px;line-height:1.2;font-weight:700;color:${colors.text};\">${esc(stat.value)}</div>` +\n `<div style=\"font-family:${FONT};font-size:13px;line-height:1.4;color:${colors.muted};\">${esc(stat.label)}</div>` +\n changeHtml(stat.change, colors) +\n `</div></td>`\n )\n })\n while (tds.length < columns) tds.push(`<td style=\"width:${width};padding:0 0 8px 4px;\"></td>`)\n rows.push(`<tr>${tds.join('')}</tr>`)\n }\n return `<table role=\"presentation\" style=\"width:100%;border-collapse:collapse;table-layout:fixed;\">${rows.join('')}</table>`\n}\n\n/** A ranked list: the label left, its figure right, a hairline between. */\nfunction rowsHtml(rows: ReadonlyArray<{ label: string; value: string }>, colors: WeeklySummaryColors): string {\n const trs = rows.map(\n (row, index) =>\n `<tr>` +\n `<td style=\"font-family:${FONT};font-size:14px;line-height:1.4;color:${colors.text};padding:8px 12px 8px 0;` +\n `${index < rows.length - 1 ? `border-bottom:1px solid ${colors.border};` : ''}word-break:break-word;\">${unlinked(row.label)}</td>` +\n `<td style=\"font-family:${FONT};font-size:14px;line-height:1.4;font-weight:600;color:${colors.text};padding:8px 0;text-align:right;white-space:nowrap;` +\n `${index < rows.length - 1 ? `border-bottom:1px solid ${colors.border};` : ''}\">${esc(row.value)}</td>` +\n `</tr>`,\n )\n return `<table role=\"presentation\" style=\"width:100%;border-collapse:collapse;\">${trs.join('')}</table>`\n}\n\nfunction sectionTitleHtml(title: string, colors: WeeklySummaryColors): string {\n return (\n `<div style=\"font-family:${FONT};font-size:12px;line-height:1.4;font-weight:700;letter-spacing:0.06em;` +\n `text-transform:uppercase;color:${colors.muted};padding:0 0 8px;\">${esc(title)}</div>`\n )\n}\n\nfunction partHtml(part: PluginFigureSummaryPart, colors: WeeklySummaryColors): string {\n return (\n `<div style=\"padding:16px 0 8px;\">` +\n sectionTitleHtml(part.title, colors) +\n (part.note\n ? `<div style=\"font-family:${FONT};font-size:13px;line-height:1.5;color:${colors.muted};padding:0 0 8px;\">${esc(part.note)}</div>`\n : '') +\n (part.stats?.length ? tilesHtml(part.stats, colors) : '') +\n (part.rows?.length ? rowsHtml(part.rows, colors) : '') +\n `</div>`\n )\n}\n\n/** \"aglyn.com · Aglyn LLC\": which site, linked, and whose. */\nfunction addressHtml(site: WeeklySummarySite, workspaceName: string, colors: WeeklySummaryColors): string {\n const address = site.address\n ? `<a href=\"${esc(site.address.url)}\" style=\"color:${colors.link};text-decoration:underline;font-weight:600;\">${esc(site.address.label)}</a>`\n : `<span style=\"font-weight:600;\">${esc(site.name)}</span>`\n return (\n `<div style=\"font-family:${FONT};font-size:14px;line-height:1.5;color:${colors.muted};padding:0 0 4px;\">` +\n `${address} · ${esc(workspaceName)}</div>`\n )\n}\n\nfunction insightsHtml(url: string, colors: WeeklySummaryColors): string {\n return (\n `<div style=\"margin:16px 0 0;background-color:${colors.surface};border-left:3px solid ${colors.accent};border-radius:4px;padding:14px 16px;\">` +\n `<div style=\"font-family:${FONT};font-size:14px;line-height:1.5;font-weight:700;color:${colors.text};\">Want to know why the numbers moved?</div>` +\n `<div style=\"font-family:${FONT};font-size:14px;line-height:1.5;color:${colors.text};\">` +\n `AI weekly insights explain each site's week in a few plain sentences. ` +\n `<a href=\"${esc(url)}\" style=\"color:${colors.link};text-decoration:underline;\">Turn them on from Ask about your numbers</a>.` +\n `</div></div>`\n )\n}\n\nfunction partText(part: PluginFigureSummaryPart): string[] {\n const lines = [part.title.toUpperCase()]\n if (part.note) lines.push(part.note)\n for (const stat of part.stats ?? []) {\n const said = weeklySummaryChange(stat.change)\n lines.push(`${stat.label}: ${stat.value}${said ? ` (${said.text})` : ''}`)\n }\n for (const row of part.rows ?? []) lines.push(`- ${row.label}: ${row.value}`)\n return lines\n}\n\n/** What a send fills a weekly summary email's tokens with. */\nexport interface WeeklySummaryEmail {\n subject: string\n /** The heading: \"Aglyn last week\". */\n title: string\n /** \"Oct 5 – Oct 11, compared with the week before\". */\n period: string\n /** The figures, drawn. */\n html: string\n /** The whole email as plain text, closing line included. */\n text: string\n buttonLabel: string\n /** Where the button goes; `''` when there is no console origin to link to. */\n url: string\n}\n\ninterface CommonInput {\n workspaceName: string\n productName: string\n periodLabel: string\n colors: WeeklySummaryColors\n /** Where AI weekly insights are turned on, when the plan includes them and this person has not; else `null`. */\n aiInsightsUrl: string | null\n settingsUrl: string\n}\n\nfunction closing(input: CommonInput): string[] {\n return [\n '',\n `You get this summary because you manage ${input.workspaceName}. Turn it off in Notifications: ${input.settingsUrl}`,\n ]\n}\n\n/**\n * One site's email, or `null` for a site with nothing to report. `workspace`\n * carries the workspace's own parts when this site's email stands in for the\n * overview, in a workspace with one active site.\n */\nexport function composeWeeklySummarySiteEmail(\n input: CommonInput & { site: WeeklySummarySite; workspace?: readonly PluginFigureSummaryPart[] },\n): WeeklySummaryEmail | null {\n const workspace = input.workspace ?? []\n if (!weeklySummaryHasNews(input.site) && !workspace.length) return null\n const lead = headline([input.site])\n const subject = lead ? `${input.site.name} last week: ${lead}` : `${input.site.name} last week`\n const html =\n addressHtml(input.site, input.workspaceName, input.colors) +\n input.site.parts.map((part) => partHtml(part, input.colors)).join('') +\n (workspace.length\n ? `<div style=\"border-top:1px solid ${input.colors.border};margin:16px 0 0;\"></div>` +\n workspace.map((part) => partHtml({ ...part, title: `${input.workspaceName} · ${part.title}` }, input.colors)).join('')\n : '') +\n (input.aiInsightsUrl ? insightsHtml(input.aiInsightsUrl, input.colors) : '')\n const text = [\n `${input.site.name} last week, ${input.periodLabel}. A change in brackets is against the week before.`,\n `Site: ${input.site.address ? `${input.site.address.label} (${input.site.address.url})` : input.site.name} · ${input.workspaceName}`,\n ...input.site.parts.flatMap((part) => ['', ...partText(part)]),\n ...workspace.flatMap((part) => ['', ...partText({ ...part, title: `${input.workspaceName} · ${part.title}` })]),\n ...(input.site.url ? ['', `Open ${input.site.name} in ${input.productName}: ${input.site.url}`] : []),\n ...(input.aiInsightsUrl\n ? ['', `Want to know why the numbers moved? Turn on AI weekly insights from Ask about your numbers: ${input.aiInsightsUrl}`]\n : []),\n ...closing(input),\n ].join('\\n')\n return {\n subject,\n title: `${input.site.name} last week`,\n period: `${input.periodLabel}, compared with the week before`,\n html,\n text,\n buttonLabel: `Open ${input.site.name}`,\n url: input.site.url ?? '',\n }\n}\n\n/**\n * The workspace overview: the week's totals across its sites, a line for each\n * site, and the workspace's own figures, or `null` when nothing anywhere has\n * anything to report.\n */\nexport function composeWeeklySummaryOverviewEmail(\n input: CommonInput & {\n sites: readonly WeeklySummarySite[]\n workspace: readonly PluginFigureSummaryPart[]\n /** The workspace's home in the console, absolute; `''` with no origin. */\n url: string\n },\n): WeeklySummaryEmail | null {\n const active = input.sites.filter(weeklySummaryHasNews)\n if (!active.length && !input.workspace.length) return null\n const totals = weeklySummaryTotals(active)\n const lead = totals\n .slice(0, WEEKLY_SUMMARY_HEADLINE_COUNTS)\n .map(({ count, one, many }) => figurePlural(count, one, many))\n .join(', ')\n const subject = lead ? `Your week at ${input.workspaceName}: ${lead}` : `Your week at ${input.workspaceName}`\n const { colors } = input\n const totalsPart: PluginFigureSummaryPart | null = totals.length\n ? {\n title: active.length === 1 ? 'At a glance' : `Across ${active.length} sites`,\n stats: totals.slice(0, TILES_PER_ROW * 2).map(({ count, one, many }) => ({\n label: capitalized(count === 1 ? one : many),\n value: count.toLocaleString('en-US'),\n })),\n }\n : null\n const siteLine = (site: WeeklySummarySite): string =>\n site.parts\n .flatMap((part) => part.stats ?? [])\n .filter((stat) => stat.total && stat.total.count > 0)\n .slice(0, WEEKLY_SUMMARY_HEADLINE_COUNTS)\n .map((stat) => {\n const total = stat.total as NonNullable<PluginFigureSummaryStat['total']>\n const said = weeklySummaryChange(stat.change)\n return `${figurePlural(total.count, total.one, total.many)}${said ? ` (${said.text})` : ''}`\n })\n .join(' · ')\n const quietCount = input.sites.length - active.length\n const quiet = quietCount\n ? `${figurePlural(quietCount, 'other site', 'other sites')} had nothing to report.`\n : ''\n const sitesHtml = active.length\n ? `<div style=\"padding:16px 0 8px;\">` +\n sectionTitleHtml('Your sites', colors) +\n `<table role=\"presentation\" style=\"width:100%;border-collapse:collapse;\">` +\n active\n .map((site, index) => {\n const border = index < active.length - 1 ? `border-bottom:1px solid ${colors.border};` : ''\n const name = site.url\n ? `<a href=\"${esc(site.url)}\" style=\"color:${colors.link};text-decoration:none;font-weight:700;\">${esc(site.name)}</a>`\n : `<span style=\"font-weight:700;\">${esc(site.name)}</span>`\n const line = siteLine(site)\n const address = site.address\n ? ` <a href=\"${esc(site.address.url)}\" style=\"color:${colors.muted};text-decoration:underline;font-size:13px;font-weight:400;\">${esc(site.address.label)}</a>`\n : ''\n return (\n `<tr><td style=\"font-family:${FONT};font-size:15px;line-height:1.4;color:${colors.text};padding:10px 0;${border}\">` +\n name +\n address +\n (line ? `<div style=\"font-size:13px;line-height:1.5;color:${colors.muted};\">${esc(line)}</div>` : '') +\n `</td></tr>`\n )\n })\n .join('') +\n `</table>` +\n (quiet\n ? `<div style=\"font-family:${FONT};font-size:13px;line-height:1.5;color:${colors.muted};padding:4px 0 0;\">${esc(quiet)}</div>`\n : '') +\n `</div>`\n : ''\n const html =\n (totalsPart ? partHtml(totalsPart, colors) : '') +\n sitesHtml +\n input.workspace.map((part) => partHtml(part, colors)).join('') +\n (active.length > 1\n ? `<div style=\"font-family:${FONT};font-size:13px;line-height:1.5;color:${colors.muted};padding:8px 0 0;\">Each site's own summary arrives beside this one.</div>`\n : '') +\n (input.aiInsightsUrl ? insightsHtml(input.aiInsightsUrl, colors) : '')\n const text = [\n `Here is how ${input.workspaceName} did last week, ${input.periodLabel}. A change in brackets is against the week before.`,\n ...(totalsPart ? ['', ...partText(totalsPart)] : []),\n ...(active.length\n ? [\n '',\n 'YOUR SITES',\n ...active.map(\n (site) =>\n `- ${site.name}${site.address ? ` (${site.address.label})` : ''}: ${siteLine(site) || 'see its summary'}` +\n (site.url ? ` · ${site.url}` : ''),\n ),\n ...(quiet ? [quiet] : []),\n ]\n : []),\n ...input.workspace.flatMap((part) => ['', ...partText(part)]),\n ...(input.aiInsightsUrl\n ? ['', `Want to know why the numbers moved? Turn on AI weekly insights from Ask about your numbers: ${input.aiInsightsUrl}`]\n : []),\n ...closing(input),\n ].join('\\n')\n return {\n subject,\n title: `${input.workspaceName} last week`,\n period: `${input.periodLabel}, compared with the week before`,\n html,\n text,\n buttonLabel: `Open ${input.workspaceName}`,\n url: input.url,\n }\n}\n"],"names":["pluginFigureCellText","pluginFigureIsoWeek","pluginFigureWindows","WEEKLY_SUMMARY_DIGEST_KEY","WEEKLY_SUMMARY_DAYS","WEEKLY_SUMMARY_TOP_ROWS","WEEKLY_SUMMARY_HEADLINE_COUNTS","TILES_PER_ROW","weeklySummaryPeriod","runAt","startOfToday","Date","UTC","getUTCFullYear","getUTCMonth","getUTCDate","now","current","week","from","to","label","weeklySummaryDay","day","at","toLocaleDateString","month","timeZone","figureCount","value","Number","isFinite","figureRow","table","key","rows","find","row","figureHasColumn","columns","some","column","figureStat","options","change","undefined","total","count","figureRanked","nameKey","filter","slice","map","String","figurePlural","one","many","toLocaleString","weeklySummaryHasNews","site","parts","length","weeklySummaryTotals","sites","sums","Map","part","stat","stats","sum","get","set","values","headline","join","weeklySummaryChange","text","sign","amount","Math","abs","maximumFractionDigits","esc","replace","FONT","capitalized","charAt","toUpperCase","unlinked","changeHtml","colors","said","color","up","down","muted","arrow","weeklySummaryTileRows","left","push","min","tilesHtml","size","cells","max","width","floor","tds","index","right","surface","border","rowsHtml","trs","sectionTitleHtml","title","partHtml","note","addressHtml","workspaceName","address","url","link","name","insightsHtml","accent","partText","lines","closing","input","settingsUrl","composeWeeklySummarySiteEmail","workspace","lead","subject","html","aiInsightsUrl","periodLabel","flatMap","productName","period","buttonLabel","composeWeeklySummaryOverviewEmail","active","totals","totalsPart","siteLine","quietCount","quiet","sitesHtml","line"],"mappings":"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"}
@@ -165,6 +165,60 @@ export interface PluginFigureReader {
165
165
  /** The parameters the reader takes beyond its window; absent for none. */
166
166
  params?: readonly PluginFigureParam[];
167
167
  read(request: PluginFigureRequest): Promise<PluginFigureRead>;
168
+ /**
169
+ * How the reader's table reads in the weekly summary email (AGL-3867), for
170
+ * a reader that belongs in it; absent, the summary leaves it out. The owner
171
+ * words its own figures, because only the owner knows which of its columns
172
+ * say what happened: the summary reads it over 7 days with no parameters
173
+ * and lists what `lines` answers.
174
+ */
175
+ weeklySummary?: PluginFigureWeeklySummary;
176
+ }
177
+ /** A headline figure, drawn as a tile: "577 Visitors ▼ 27.9%". */
178
+ export interface PluginFigureSummaryStat {
179
+ /** What it counts, as a tile names it: "Visitors". */
180
+ label: string;
181
+ /** The figure as a person reads it: "577", "$1,240.00". */
182
+ value: string;
183
+ /** The percent change on the week before; `null` or absent when there is nothing to compare with. */
184
+ change?: number | null;
185
+ /**
186
+ * A count the workspace overview adds up across sites and the subject line
187
+ * may lead with, and its noun: `{ count: 577, one: 'visitor', many:
188
+ * 'visitors' }`. Stats with the same `one` are summed.
189
+ */
190
+ total?: {
191
+ count: number;
192
+ one: string;
193
+ many: string;
194
+ };
195
+ }
196
+ /** One reader's part of a weekly summary: a titled block of tiles, ranked rows, or both. */
197
+ export interface PluginFigureSummaryPart {
198
+ /** The block's heading: "Traffic", "Top pages". */
199
+ title: string;
200
+ stats?: readonly PluginFigureSummaryStat[];
201
+ /** A ranked list, most first: a page and its views, a form and its submissions. */
202
+ rows?: ReadonlyArray<{
203
+ label: string;
204
+ value: string;
205
+ }>;
206
+ /** One plain sentence the figures need. */
207
+ note?: string;
208
+ }
209
+ /** A reader's part in the weekly summary. */
210
+ export interface PluginFigureWeeklySummary {
211
+ /**
212
+ * Which email lists it: `site`, each site's own; `workspace`, the
213
+ * workspace overview. A workspace reader may say `site` to be read
214
+ * narrowed to each site. Absent, a site reader is `site` and a workspace
215
+ * reader is `workspace`.
216
+ */
217
+ per?: 'site' | 'workspace';
218
+ /** Where its block sits: lower first. */
219
+ order: number;
220
+ /** What the table says this week, or `null` when it has nothing to report. */
221
+ summarize(table: PluginFigureTable): PluginFigureSummaryPart | null;
168
222
  }
169
223
  export declare const PLUGIN_FIGURES: import("./plugin-services").PluginServiceContract<PluginFigureReader>;
170
224
  /** The most rows one table carries; the rest are counted in `omitted`. */
@@ -249,3 +303,13 @@ export declare function pluginFigureWindows(now: Date, days: number): {
249
303
  * reads as no change figure at all rather than as a rise from zero.
250
304
  */
251
305
  export declare function pluginFigureChange(current: number, previous: number | null): number | null;
306
+ /**
307
+ * How a cell reads to a person, by its column's kind; `null` is "—". Every
308
+ * consumer that shows a table to a person words its cells here, so an insight
309
+ * answer and a weekly summary print the same figure the same way.
310
+ */
311
+ export declare function pluginFigureCellText(table: Pick<PluginFigureTable, 'columns'>, key: string, value: PluginFigureCell | undefined): string;
312
+ /** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */
313
+ export declare function pluginFigureIsoWeek(at: Date): string;
314
+ /** The section of the weekly summary a reader is listed in, or `null` for a reader that is not in it. */
315
+ export declare function pluginFigureSummarySection(reader: PluginFigureReader): 'site' | 'workspace' | null;
@@ -248,5 +248,56 @@ function cellFor(column, value) {
248
248
  if (!Number.isFinite(current)) return null;
249
249
  return Math.round((current - previous) / previous * 1000) / 10;
250
250
  }
251
+ /**
252
+ * How a cell reads to a person, by its column's kind; `null` is "—". Every
253
+ * consumer that shows a table to a person words its cells here, so an insight
254
+ * answer and a weekly summary print the same figure the same way.
255
+ */ export function pluginFigureCellText(table, key, value) {
256
+ if (value === null || value === undefined || value === '') return '—';
257
+ const column = table.columns.find((entry)=>entry.key === key);
258
+ if (typeof value === 'string' || !column) return String(value);
259
+ switch(column.kind){
260
+ case 'money':
261
+ var _column_currency;
262
+ return new Intl.NumberFormat('en-US', {
263
+ style: 'currency',
264
+ currency: (_column_currency = column.currency) != null ? _column_currency : 'USD'
265
+ }).format(value);
266
+ case 'percent':
267
+ return `${value.toLocaleString('en-US', {
268
+ maximumFractionDigits: 1
269
+ })}%`;
270
+ case 'change':
271
+ return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', {
272
+ maximumFractionDigits: 1
273
+ })}%`;
274
+ case 'duration':
275
+ {
276
+ const seconds = Math.round(value);
277
+ if (seconds < 60) return `${seconds}s`;
278
+ const minutes = Math.floor(seconds / 60);
279
+ if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`;
280
+ return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`;
281
+ }
282
+ default:
283
+ return value.toLocaleString('en-US', {
284
+ maximumFractionDigits: 2
285
+ });
286
+ }
287
+ }
288
+ /** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */ export function pluginFigureIsoWeek(at) {
289
+ const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()));
290
+ // Thursday decides the week's year.
291
+ const weekday = date.getUTCDay() || 7;
292
+ date.setUTCDate(date.getUTCDate() + 4 - weekday);
293
+ const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1);
294
+ const week = Math.ceil(((date.getTime() - yearStart) / 86400000 + 1) / 7);
295
+ return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
296
+ }
297
+ /** The section of the weekly summary a reader is listed in, or `null` for a reader that is not in it. */ export function pluginFigureSummarySection(reader) {
298
+ var _reader_weeklySummary_per;
299
+ if (!reader.weeklySummary) return null;
300
+ return (_reader_weeklySummary_per = reader.weeklySummary.per) != null ? _reader_weeklySummary_per : reader.scope === 'site' ? 'site' : 'workspace';
301
+ }
251
302
 
252
303
  //# sourceMappingURL=plugin-figures.js.map