@aglyn/aglyn 1.0.0-beta.262 → 1.0.0-beta.264
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 +11 -11
- package/src/lib/app-utils/docs-help-sections.generated.js +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +3 -3
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/native-app-web-view.d.ts +63 -0
- package/src/lib/app-utils/native-app-web-view.js +83 -0
- package/src/lib/app-utils/native-app-web-view.js.map +1 -0
- package/src/lib/app-utils/notifications.js +1 -1
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/weekly-summary.d.ts +112 -39
- package/src/lib/app-utils/weekly-summary.js +275 -61
- package/src/lib/app-utils/weekly-summary.js.map +1 -1
- package/src/lib/plugin-manager/plugin-figures.d.ts +38 -16
- package/src/lib/plugin-manager/plugin-figures.js.map +1 -1
|
@@ -1 +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 PluginFigureHeadline,\n type PluginFigureRow,\n type PluginFigureTable,\n} from '../plugin-manager/plugin-figures'\n\n/**\n * THE WEEKLY SUMMARY (AGL-3867): the free Monday email that says how each of\n * a workspace's sites did last week, and how the workspace's own figures\n * moved.\n *\n * It spends nothing. Every figure comes from a `plugin-figures` reader that\n * declares a `weeklySummary`, and that reader's owner words its own lines, so\n * each plugin's counting rules decide what a figure is and nothing here names\n * one. AI weekly insights (AGL-2915) are the paid version that explains the\n * figures, and stay opt-in because each one spends the workspace's AI credits.\n *\n * This module is the period, the shared wording helpers the readers use, and\n * the email's words; the reads and the sends are\n * `@aglyn/tenant-data-admin/server/weekly-summary`.\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 line names: the top three. */\nexport const WEEKLY_SUMMARY_TOP_ROWS = 3\n\n/** Counts the subject line leads with, at most. */\nexport const WEEKLY_SUMMARY_HEADLINE_COUNTS = 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/** 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/** \"1,204 (+12%)\": a cell and, when there is an earlier week to compare with, its change. */\nexport function figureWithChange(\n table: PluginFigureTable,\n row: PluginFigureRow,\n key: string,\n changeKey: string,\n): string {\n const value = pluginFigureCellText(table, key, row[key] ?? 0)\n const change = row[changeKey]\n if (typeof change !== 'number') return value\n return `${value} (${change === 0 ? 'no change' : pluginFigureCellText(table, changeKey, change)})`\n}\n\n/** The named rows with the most of `key`, as \"name 12 unit, name 4\"; `null` when none has any. */\nexport function figureRanked(\n table: PluginFigureTable,\n nameKey: string,\n key: string,\n unit = '',\n rows: readonly PluginFigureRow[] = table.rows,\n): string | null {\n const top = rows\n .filter((row) => typeof row[nameKey] === 'string' && figureCount(row[key]) > 0)\n .slice(0, WEEKLY_SUMMARY_TOP_ROWS)\n if (!top.length) return null\n return top\n .map((row, index) => {\n const amount = pluginFigureCellText(table, key, row[key] ?? 0)\n return `${row[nameKey]} ${amount}${index === 0 && unit ? ` ${unit}` : ''}`\n })\n .join(', ')\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/** What one reader said this week: its lines and the count it adds to the subject. */\nexport interface WeeklySummaryEntry {\n lines: string[]\n headline: PluginFigureHeadline | null\n}\n\n/** One site's part of the email. */\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 entries: readonly WeeklySummaryEntry[]\n}\n\n/** The subject's counts, summed across sites by their noun, in the order the readers first gave them. */\nexport function weeklySummaryHeadline(sites: readonly WeeklySummarySite[]): string {\n const sums = new Map<string, PluginFigureHeadline>()\n for (const site of sites) {\n for (const { headline } of site.entries) {\n if (!headline || headline.count <= 0) continue\n const sum = sums.get(headline.one)\n sums.set(headline.one, sum ? { ...sum, count: sum.count + headline.count } : { ...headline })\n }\n }\n return [...sums.values()]\n .slice(0, WEEKLY_SUMMARY_HEADLINE_COUNTS)\n .map(({ count, one, many }) => figurePlural(count, one, many))\n .join(', ')\n}\n\nexport function composeWeeklySummarySubject(workspaceName: string, sites: readonly WeeklySummarySite[]): string {\n const headline = weeklySummaryHeadline(sites)\n return headline ? `Your week at ${workspaceName}: ${headline}` : `Your week at ${workspaceName}`\n}\n\nexport interface WeeklySummaryEmailInput {\n workspaceName: string\n productName: string\n periodLabel: string\n sites: readonly WeeklySummarySite[]\n /** The workspace's own figures, such as its deals. */\n workspace: readonly WeeklySummaryEntry[]\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 /** Drops the closing \"why\" line, which the designed email's footer carries. */\n omitClosing?: boolean\n}\n\n/**\n * The email's words, or `null` when there is nothing to report at all — a\n * week with no figures is not worth an email, and sending one anyway is how a\n * summary trains people to ignore it. A site with nothing to say is left out.\n */\nexport function composeWeeklySummaryEmailText(input: WeeklySummaryEmailInput): string | null {\n const sections = input.sites\n .map((site) => ({ site, lines: site.entries.flatMap((entry) => entry.lines) }))\n .filter(({ lines }) => lines.length > 0)\n const workspaceLines = input.workspace.flatMap((entry) => entry.lines)\n if (!sections.length && !workspaceLines.length) return null\n const lines: string[] = [\n `Here is how ${input.workspaceName} did last week, ${input.periodLabel}. ` +\n 'A change in brackets is against the week before.',\n ]\n for (const { site, lines: siteLines } of sections) {\n lines.push('', site.name, ...siteLines.map((line) => `- ${line}`))\n if (site.url) lines.push(`Open this site in ${input.productName}: ${site.url}`)\n }\n if (workspaceLines.length) {\n lines.push('', `Across ${input.workspaceName}`, ...workspaceLines.map((line) => `- ${line}`))\n }\n if (input.aiInsightsUrl) {\n lines.push(\n '',\n `Want to know why the numbers moved? AI weekly insights explain each site's figures every Monday. ` +\n `Turn them on from Ask about your numbers on a site's Analytics page: ${input.aiInsightsUrl}`,\n )\n }\n if (!input.omitClosing) {\n lines.push(\n '',\n `You get this summary because you manage ${input.workspaceName}. Turn it off in Notifications: ${input.settingsUrl}`,\n )\n }\n return lines.join('\\n')\n}\n"],"names":["pluginFigureCellText","pluginFigureIsoWeek","pluginFigureWindows","WEEKLY_SUMMARY_DIGEST_KEY","WEEKLY_SUMMARY_DAYS","WEEKLY_SUMMARY_TOP_ROWS","WEEKLY_SUMMARY_HEADLINE_COUNTS","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","figureWithChange","changeKey","change","figureRanked","nameKey","unit","top","filter","slice","length","map","index","amount","join","figurePlural","count","one","many","toLocaleString","weeklySummaryHeadline","sites","sums","Map","site","headline","entries","sum","get","set","values","composeWeeklySummarySubject","workspaceName","composeWeeklySummaryEmailText","input","sections","lines","flatMap","entry","workspaceLines","workspace","periodLabel","siteLines","push","name","line","url","productName","aiInsightsUrl","omitClosing","settingsUrl"],"mappings":"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"}
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"')\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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"}
|
|
@@ -174,29 +174,51 @@ export interface PluginFigureReader {
|
|
|
174
174
|
*/
|
|
175
175
|
weeklySummary?: PluginFigureWeeklySummary;
|
|
176
176
|
}
|
|
177
|
-
/** A
|
|
178
|
-
export interface
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
208
|
}
|
|
185
209
|
/** A reader's part in the weekly summary. */
|
|
186
210
|
export interface PluginFigureWeeklySummary {
|
|
187
211
|
/**
|
|
188
|
-
* Which
|
|
189
|
-
*
|
|
190
|
-
* each site. Absent, a site reader is `site` and a workspace
|
|
191
|
-
* `workspace`.
|
|
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`.
|
|
192
216
|
*/
|
|
193
217
|
per?: 'site' | 'workspace';
|
|
194
|
-
/** Where its
|
|
218
|
+
/** Where its block sits: lower first. */
|
|
195
219
|
order: number;
|
|
196
|
-
/** What the table says
|
|
197
|
-
|
|
198
|
-
/** The count it adds to the subject line, or `null` for none this week. */
|
|
199
|
-
headline?(table: PluginFigureTable): PluginFigureHeadline | null;
|
|
220
|
+
/** What the table says this week, or `null` when it has nothing to report. */
|
|
221
|
+
summarize(table: PluginFigureTable): PluginFigureSummaryPart | null;
|
|
200
222
|
}
|
|
201
223
|
export declare const PLUGIN_FIGURES: import("./plugin-services").PluginServiceContract<PluginFigureReader>;
|
|
202
224
|
/** The most rows one table carries; the rest are counted in `omitted`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-figures.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Figures one plugin publishes for another (AGL-2915).\n *\n * The records behind a figure belong to the plugin that models them: orders\n * to commerce, bookings to bookings, campaigns and experiments to marketing.\n * The owner knows which documents count, which are test data, what a refund\n * does to revenue and which fields could name a person. A different plugin\n * that wants the figure — a weekly report, an export, an assistant answering\n * a question about the numbers — must not read those documents itself,\n * because it would restate the owner's rules and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the owner registers a READER from its server surfaces, and a caller asks\n * for one by id. A reader answers with one compact table:\n *\n * - read-only: a reader writes nothing, schedules nothing and sends nothing;\n * - aggregate: counts, sums, rates and labels, never the records themselves;\n * - about nobody: no row names a person, and no cell carries a name, an email\n * address, a phone number or a street address — a customer is counted,\n * never listed;\n * - bounded: at most `PLUGIN_FIGURE_MAX_ROWS` rows, each labelled with where\n * in the console the figures come from.\n *\n * Built on the typed service registry (`plugin-services`): one contract, every\n * reader registered under its id as the service's key.\n *\n * ## The registry carries no authorization of the CALLER\n *\n * Like `plugin-resource-drafts`, a reader is a pre-authorized operation: it\n * reads the site or the workspace it is handed. A caller establishes, in its\n * own terms and before it asks, who is asking, that the org owns the site, that\n * the person may see that site's figures, and that the owning plugin is\n * switched on for it.\n *\n * ## A table is checked on the way out as well as on the way in\n *\n * `normalizePluginFigureTable` holds whatever a reader returned to the shape\n * above — row and cell bounds, column kinds, and personal details masked in\n * every text cell — so a consumer that shows or forwards a table relies on the\n * contract rather than on every reader having kept it.\n */\n\n/** Whether a reader's figures are about one site or about the whole workspace. */\nexport type PluginFigureScope = 'site' | 'org'\n\n/**\n * How a column's cells read:\n *\n * - `text` — a label: a page path, a form's name, a region;\n * - `count` — a whole number of things;\n * - `number` — any other figure, such as an average per day;\n * - `money` — an amount in major units of the column's `currency`;\n * - `percent` — a share or a rate, from 0 to 100;\n * - `change` — the percent change from the window before; `null` where there\n * is no earlier window to compare with, never 0 or 100;\n * - `duration` — seconds;\n * - `date` — a calendar day, `YYYY-MM-DD`.\n */\nexport type PluginFigureColumnKind =\n | 'text'\n | 'count'\n | 'number'\n | 'money'\n | 'percent'\n | 'change'\n | 'duration'\n | 'date'\n\nexport const PLUGIN_FIGURE_COLUMN_KINDS: readonly PluginFigureColumnKind[] = [\n 'text',\n 'count',\n 'number',\n 'money',\n 'percent',\n 'change',\n 'duration',\n 'date',\n]\n\nexport interface PluginFigureColumn {\n /** The row field the column reads. */\n key: string\n label: string\n kind: PluginFigureColumnKind\n /** The ISO 4217 code of a `money` column. */\n currency?: string\n}\n\nexport type PluginFigureCell = string | number | null\n\nexport type PluginFigureRow = Record<string, PluginFigureCell>\n\nexport interface PluginFigureTable {\n title: string\n /**\n * Where the figures come from: the name of the console page, and its path\n * under the site's console root for a site reader, or under the workspace's\n * for an org reader (`commerce/orders`), so a consumer can link to it.\n */\n source: { label: string; path: string | null }\n /** The days the figures cover, inclusive; `null` for a current count. */\n period: { from: string; to: string; days: number } | null\n columns: PluginFigureColumn[]\n rows: PluginFigureRow[]\n /** Rows the reader left out past its bound. */\n omitted: number\n /** Short plain sentences a reader of the figures needs, such as a window with nothing earlier to compare with. */\n notes: string[]\n}\n\nexport interface PluginFigureRequest {\n orgId: string\n /** The site a `site` reader reads; for an `org` reader, the site the figures are narrowed to, or `null`. */\n hostId: string | null\n /** The window, in days, ending at `now`: one of the reader's `windows`, or 0 for a reader with none. */\n days: number\n now: Date\n /**\n * The member asking. A reader of records a member may see only part of —\n * a dataset shared with some sites — reads what this member may see; with\n * `null`, nobody asked, and it reads only what every member of the site may.\n */\n uid: string | null\n /** The reader's own parameters, by the names its `params` declare; each a string. */\n params: Readonly<Record<string, string>>\n}\n\n/** One parameter a reader takes beyond its window. */\nexport interface PluginFigureParam {\n /** The name a request carries it under: letters, digits and underscores. */\n name: string\n /** What it selects, in one plain sentence. */\n description: string\n required: boolean\n}\n\nexport type PluginFigureRead =\n | { ok: true; table: PluginFigureTable }\n | {\n ok: false\n status: 400 | 403 | 404\n /** Customer-safe: a caller shows it as it stands. */\n error: string\n }\n\nexport interface PluginFigureReader {\n /** Stable and unique across plugins: lowercase words joined by dots (`commerce.orders`). */\n id: string\n /** What a person calls the figures: \"Orders\". */\n label: string\n /**\n * What the reader answers, in one or two plain sentences. The same bytes for\n * every site, so a list of readers can be shown to anyone who may use them.\n */\n description: string\n scope: PluginFigureScope\n /** The windows the reader covers, in days; empty for a reader of current counts. */\n windows: readonly number[]\n /**\n * The plugin whose records the figures are read from, which must run on the\n * site — or on the workspace, for an `org` reader — before anyone is offered\n * the reader. Absent, the plugin that registered it; `null` for figures the\n * platform itself keeps, such as page views.\n */\n plugin?: string | null\n /** The plan feature the figures are sold under; a workspace without it is not offered the reader. */\n feature?: string\n /** The parameters the reader takes beyond its window; absent for none. */\n params?: readonly PluginFigureParam[]\n read(request: PluginFigureRequest): Promise<PluginFigureRead>\n /**\n * How the reader's table reads in the weekly summary email (AGL-3867), for\n * a reader that belongs in it; absent, the summary leaves it out. The owner\n * words its own figures, because only the owner knows which of its columns\n * say what happened: the summary reads it over 7 days with no parameters\n * and lists what `lines` answers.\n */\n weeklySummary?: PluginFigureWeeklySummary\n}\n\n/** A count a weekly summary's subject line may lead with, and how it is named. */\nexport interface PluginFigureHeadline {\n count: number\n /** The noun for one, which also keys the sum across sites: \"visitor\". */\n one: string\n /** The noun for any other count: \"visitors\". */\n many: string\n}\n\n/** A reader's part in the weekly summary. */\nexport interface PluginFigureWeeklySummary {\n /**\n * Which section lists it: `site`, under each site; `workspace`, once for the\n * whole workspace. A workspace reader may say `site` to be read narrowed to\n * each site. Absent, a site reader is `site` and a workspace reader is\n * `workspace`.\n */\n per?: 'site' | 'workspace'\n /** Where its lines sit in a section: lower first. */\n order: number\n /** What the table says, one line each; `[]` when it has nothing to report this week. */\n lines(table: PluginFigureTable): string[]\n /** The count it adds to the subject line, or `null` for none this week. */\n headline?(table: PluginFigureTable): PluginFigureHeadline | null\n}\n\nexport const PLUGIN_FIGURES = definePluginServiceContract<PluginFigureReader>('core.figures', {\n multiple: true,\n})\n\n/** The most rows one table carries; the rest are counted in `omitted`. */\nexport const PLUGIN_FIGURE_MAX_ROWS = 25\n\n/** The most columns one table carries. */\nexport const PLUGIN_FIGURE_MAX_COLUMNS = 8\n\n/** The longest text cell, title or label a table carries, in characters. */\nexport const PLUGIN_FIGURE_MAX_TEXT = 120\n\n/** The most notes one table carries. */\nexport const PLUGIN_FIGURE_MAX_NOTES = 3\n\nconst READER_ID = /^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$/\nconst PARAM_NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst COLUMN_KEY = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst CONSOLE_PATH = /^[a-z0-9][a-z0-9/_-]{0,119}$/\nconst CALENDAR_DAY = /^\\d{4}-\\d{2}-\\d{2}$/\nconst CURRENCY = /^[A-Z]{3}$/\n\n/**\n * Registers a reader. The owner is the loader's marker when a register fn is\n * running, else `options.pluginId`; with neither the registration throws. A\n * reader id another plugin already registered throws naming both, and the\n * incumbent keeps serving; the same plugin registering again replaces its own.\n */\nexport function registerPluginFigureReader(\n reader: PluginFigureReader,\n options?: { pluginId?: string },\n): void {\n const key = reader.id.trim()\n if (!READER_ID.test(key)) {\n throw new Error(`a figure reader id is lowercase words joined by dots; refused \"${reader.id}\"`)\n }\n for (const param of reader.params ?? []) {\n if (!PARAM_NAME.test(param.name)) {\n throw new Error(`figure reader \"${key}\" declares a parameter named \"${param.name}\", which is not a name`)\n }\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_FIGURES).find((entry) => entry.key === key)\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `figure reader \"${key}\" is already registered by \"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_FIGURES, reader, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginFigureReader {\n /** The plugin that owns the records behind the figures. */\n pluginId: string\n reader: PluginFigureReader\n}\n\n/** The reader registered under an id, with its owner, or `null`. */\nexport function pluginFigureReader(id: string): ResolvedPluginFigureReader | null {\n const key = id.trim()\n const entry = resolvePluginServices(PLUGIN_FIGURES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, reader: entry.impl } : null\n}\n\n/** Every registered reader with its owner, ordered by id so a list of them is the same bytes on every call. */\nexport function listPluginFigureReaders(): ResolvedPluginFigureReader[] {\n return resolvePluginServices(PLUGIN_FIGURES)\n .map((entry) => ({ pluginId: entry.pluginId, reader: entry.impl }))\n .sort((a, b) => a.reader.id.localeCompare(b.reader.id))\n}\n\n/** The plugin that must run for a reader to be offered: its own `plugin`, else its owner; `null` for platform figures. */\nexport function pluginFigureReaderPlugin(resolved: ResolvedPluginFigureReader): string | null {\n return resolved.reader.plugin === undefined ? resolved.pluginId : resolved.reader.plugin\n}\n\n/**\n * The parameters a request carries that the reader declares, each trimmed to\n * a short string, or the name of the first required one that is missing.\n */\nexport function pluginFigureParams(\n reader: Pick<PluginFigureReader, 'params'>,\n raw: Readonly<Record<string, unknown>> | null | undefined,\n): { ok: true; params: Record<string, string> } | { ok: false; missing: string } {\n const params: Record<string, string> = {}\n for (const param of reader.params ?? []) {\n const value = raw?.[param.name]\n const text = typeof value === 'string' ? value.trim().slice(0, PLUGIN_FIGURE_MAX_TEXT) : ''\n if (text) params[param.name] = text\n else if (param.required) return { ok: false, missing: param.name }\n }\n return { ok: true, params }\n}\n\n// ── Personal details ────────────────────────────────────────────────────\n\nconst EMAIL = /[\\p{L}\\p{N}._%+-]+@[\\p{L}\\p{N}-]+(?:\\.[\\p{L}\\p{N}-]+)+/gu\n/** A run of digits with the separators a phone number is written with; its digits are counted apart. */\nconst PHONE_LIKE = /(?<![\\p{L}\\p{N}/_])\\+?\\(?\\d[\\d\\s().-]{6,}\\d(?![\\p{L}\\p{N}/_])/gu\n\n/** Nine to fifteen digits: a phone number's length anywhere, and longer than any calendar day or year. */\nfunction looksLikePhone(match: string): boolean {\n const digits = match.replace(/\\D/g, '').length\n return digits >= 9 && digits <= 15\n}\n\n/**\n * The text with every email address and phone number replaced by a marker.\n * A figure's label is never meant to carry either, so a match is masked\n * rather than trusted: a campaign named after a person's address says\n * nothing about the campaign that the marker does not.\n */\nexport function maskPersonalDetails(text: string): string {\n return text\n .replace(EMAIL, '[email]')\n .replace(PHONE_LIKE, (match) => (looksLikePhone(match) ? '[phone]' : match))\n}\n\n/** Whether a text carries an email address or a phone number. */\nexport function hasPersonalDetails(text: string): boolean {\n return maskPersonalDetails(text) !== text\n}\n\n// ── The table, held to the contract ─────────────────────────────────────\n\nfunction cut(value: unknown, max = PLUGIN_FIGURE_MAX_TEXT): string {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\nfunction finite(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\nfunction day(value: unknown): string | null {\n return typeof value === 'string' && CALENDAR_DAY.test(value) && !Number.isNaN(Date.parse(value))\n ? value\n : null\n}\n\nfunction cellFor(column: PluginFigureColumn, value: unknown): PluginFigureCell {\n switch (column.kind) {\n case 'text':\n return typeof value === 'string' ? maskPersonalDetails(cut(value)) : null\n case 'date':\n return day(value)\n case 'count': {\n const number = finite(value)\n return number === null ? null : Math.round(number)\n }\n default:\n return finite(value)\n }\n}\n\n/**\n * A reader's table held to the contract: bounded, typed and masked. Columns\n * with a key, a kind or a currency the contract does not know are dropped, and\n * every cell is read through its column, so a cell of the wrong type is `null`\n * rather than passed on. `null` for a value that is not a table at all.\n */\nexport function normalizePluginFigureTable(value: unknown): PluginFigureTable | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const table = value as Partial<PluginFigureTable>\n const title = cut(table.title)\n if (!title) return null\n\n const seen = new Set<string>()\n const columns: PluginFigureColumn[] = []\n for (const raw of Array.isArray(table.columns) ? table.columns : []) {\n if (columns.length === PLUGIN_FIGURE_MAX_COLUMNS) break\n const column = raw as Partial<PluginFigureColumn> | null\n const key = typeof column?.key === 'string' ? column.key : ''\n const kind = column?.kind as PluginFigureColumnKind\n if (!COLUMN_KEY.test(key) || seen.has(key) || !PLUGIN_FIGURE_COLUMN_KINDS.includes(kind)) continue\n if (kind === 'money' && !(typeof column?.currency === 'string' && CURRENCY.test(column.currency))) {\n continue\n }\n seen.add(key)\n columns.push({\n key,\n label: cut(column?.label, 60) || key,\n kind,\n ...(kind === 'money' ? { currency: column?.currency as string } : {}),\n })\n }\n if (!columns.length) return null\n\n const rawRows = Array.isArray(table.rows) ? table.rows : []\n const rows = rawRows.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((raw) => {\n const source = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}\n return Object.fromEntries(columns.map((column) => [column.key, cellFor(column, source[column.key])]))\n })\n const omitted =\n Math.max(0, rawRows.length - PLUGIN_FIGURE_MAX_ROWS) +\n (typeof table.omitted === 'number' && Number.isFinite(table.omitted) && table.omitted > 0\n ? Math.floor(table.omitted)\n : 0)\n\n const period = table.period\n const from = day(period?.from)\n const to = day(period?.to)\n const days = finite(period?.days)\n const path = typeof table.source?.path === 'string' ? table.source.path.trim() : ''\n\n return {\n title,\n source: {\n label: cut(table.source?.label, 80) || title,\n path: CONSOLE_PATH.test(path) && !path.includes('..') ? path : null,\n },\n period:\n from && to && days !== null && days >= 1 && days <= 366\n ? { from, to, days: Math.round(days) }\n : null,\n columns,\n rows,\n omitted,\n notes: (Array.isArray(table.notes) ? table.notes : [])\n .map((note) => maskPersonalDetails(cut(note, 200)))\n .filter(Boolean)\n .slice(0, PLUGIN_FIGURE_MAX_NOTES),\n }\n}\n\n// ── Windows ─────────────────────────────────────────────────────────────\n\n/** A calendar day in UTC. */\nexport function pluginFigureDay(at: Date): string {\n return at.toISOString().slice(0, 10)\n}\n\n/**\n * The window of `days` ending on the day of `now`, inclusive, and the window\n * of the same length immediately before it: what every `change` column\n * compares, so a figure from one reader compares like a figure from another.\n */\nexport function pluginFigureWindows(\n now: Date,\n days: number,\n): {\n current: { from: string; to: string; startMs: number; endMs: number }\n previous: { from: string; to: string; startMs: number; endMs: number }\n} {\n const span = Math.max(1, Math.floor(days))\n const dayMs = 86_400_000\n const endOfToday = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + dayMs\n const currentStart = endOfToday - span * dayMs\n const previousStart = currentStart - span * dayMs\n return {\n current: {\n from: pluginFigureDay(new Date(currentStart)),\n to: pluginFigureDay(new Date(endOfToday - dayMs)),\n startMs: currentStart,\n endMs: endOfToday,\n },\n previous: {\n from: pluginFigureDay(new Date(previousStart)),\n to: pluginFigureDay(new Date(currentStart - dayMs)),\n startMs: previousStart,\n endMs: currentStart,\n },\n }\n}\n\n/**\n * The percent change from `previous` to `current`, to one decimal place.\n * `null` when there is nothing earlier to compare with, so a first window\n * reads as no change figure at all rather than as a rise from zero.\n */\nexport function pluginFigureChange(current: number, previous: number | null): number | null {\n if (previous === null || !Number.isFinite(previous) || previous <= 0) return null\n if (!Number.isFinite(current)) return null\n return Math.round(((current - previous) / previous) * 1_000) / 10\n}\n\n/**\n * How a cell reads to a person, by its column's kind; `null` is \"—\". Every\n * consumer that shows a table to a person words its cells here, so an insight\n * answer and a weekly summary print the same figure the same way.\n */\nexport function pluginFigureCellText(\n table: Pick<PluginFigureTable, 'columns'>,\n key: string,\n value: PluginFigureCell | undefined,\n): string {\n if (value === null || value === undefined || value === '') return '—'\n const column = table.columns.find((entry) => entry.key === key)\n if (typeof value === 'string' || !column) return String(value)\n switch (column.kind) {\n case 'money':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: column.currency ?? 'USD',\n }).format(value)\n case 'percent':\n return `${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'change':\n return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'duration': {\n const seconds = Math.round(value)\n if (seconds < 60) return `${seconds}s`\n const minutes = Math.floor(seconds / 60)\n if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`\n return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`\n }\n default:\n return value.toLocaleString('en-US', { maximumFractionDigits: 2 })\n }\n}\n\n/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */\nexport function pluginFigureIsoWeek(at: Date): string {\n const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()))\n // Thursday decides the week's year.\n const weekday = date.getUTCDay() || 7\n date.setUTCDate(date.getUTCDate() + 4 - weekday)\n const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1)\n const week = Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7)\n return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`\n}\n\n/** The section of the weekly summary a reader is listed in, or `null` for a reader that is not in it. */\nexport function pluginFigureSummarySection(reader: PluginFigureReader): 'site' | 'workspace' | null {\n if (!reader.weeklySummary) return null\n return reader.weeklySummary.per ?? (reader.scope === 'site' ? 'site' : 'workspace')\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_FIGURE_COLUMN_KINDS","PLUGIN_FIGURES","multiple","PLUGIN_FIGURE_MAX_ROWS","PLUGIN_FIGURE_MAX_COLUMNS","PLUGIN_FIGURE_MAX_TEXT","PLUGIN_FIGURE_MAX_NOTES","READER_ID","PARAM_NAME","COLUMN_KEY","CONSOLE_PATH","CALENDAR_DAY","CURRENCY","registerPluginFigureReader","reader","options","key","id","trim","test","Error","param","params","name","pluginId","incumbent","find","entry","pluginFigureReader","one","impl","listPluginFigureReaders","map","sort","a","b","localeCompare","pluginFigureReaderPlugin","resolved","plugin","undefined","pluginFigureParams","raw","value","text","slice","required","ok","missing","EMAIL","PHONE_LIKE","looksLikePhone","match","digits","replace","length","maskPersonalDetails","hasPersonalDetails","cut","max","finite","Number","isFinite","day","isNaN","Date","parse","cellFor","column","kind","number","Math","round","normalizePluginFigureTable","table","Array","isArray","title","seen","Set","columns","has","includes","currency","add","push","label","rawRows","rows","source","Object","fromEntries","omitted","floor","period","from","to","days","path","notes","note","filter","Boolean","pluginFigureDay","at","toISOString","pluginFigureWindows","now","span","dayMs","endOfToday","UTC","getUTCFullYear","getUTCMonth","getUTCDate","currentStart","previousStart","current","startMs","endMs","previous","pluginFigureChange","pluginFigureCellText","String","Intl","NumberFormat","style","format","toLocaleString","maximumFractionDigits","seconds","minutes","padStart","pluginFigureIsoWeek","date","weekday","getUTCDay","setUTCDate","yearStart","week","ceil","getTime","pluginFigureSummarySection","weeklySummary","per","scope"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-figures.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Figures one plugin publishes for another (AGL-2915).\n *\n * The records behind a figure belong to the plugin that models them: orders\n * to commerce, bookings to bookings, campaigns and experiments to marketing.\n * The owner knows which documents count, which are test data, what a refund\n * does to revenue and which fields could name a person. A different plugin\n * that wants the figure — a weekly report, an export, an assistant answering\n * a question about the numbers — must not read those documents itself,\n * because it would restate the owner's rules and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the owner registers a READER from its server surfaces, and a caller asks\n * for one by id. A reader answers with one compact table:\n *\n * - read-only: a reader writes nothing, schedules nothing and sends nothing;\n * - aggregate: counts, sums, rates and labels, never the records themselves;\n * - about nobody: no row names a person, and no cell carries a name, an email\n * address, a phone number or a street address — a customer is counted,\n * never listed;\n * - bounded: at most `PLUGIN_FIGURE_MAX_ROWS` rows, each labelled with where\n * in the console the figures come from.\n *\n * Built on the typed service registry (`plugin-services`): one contract, every\n * reader registered under its id as the service's key.\n *\n * ## The registry carries no authorization of the CALLER\n *\n * Like `plugin-resource-drafts`, a reader is a pre-authorized operation: it\n * reads the site or the workspace it is handed. A caller establishes, in its\n * own terms and before it asks, who is asking, that the org owns the site, that\n * the person may see that site's figures, and that the owning plugin is\n * switched on for it.\n *\n * ## A table is checked on the way out as well as on the way in\n *\n * `normalizePluginFigureTable` holds whatever a reader returned to the shape\n * above — row and cell bounds, column kinds, and personal details masked in\n * every text cell — so a consumer that shows or forwards a table relies on the\n * contract rather than on every reader having kept it.\n */\n\n/** Whether a reader's figures are about one site or about the whole workspace. */\nexport type PluginFigureScope = 'site' | 'org'\n\n/**\n * How a column's cells read:\n *\n * - `text` — a label: a page path, a form's name, a region;\n * - `count` — a whole number of things;\n * - `number` — any other figure, such as an average per day;\n * - `money` — an amount in major units of the column's `currency`;\n * - `percent` — a share or a rate, from 0 to 100;\n * - `change` — the percent change from the window before; `null` where there\n * is no earlier window to compare with, never 0 or 100;\n * - `duration` — seconds;\n * - `date` — a calendar day, `YYYY-MM-DD`.\n */\nexport type PluginFigureColumnKind =\n | 'text'\n | 'count'\n | 'number'\n | 'money'\n | 'percent'\n | 'change'\n | 'duration'\n | 'date'\n\nexport const PLUGIN_FIGURE_COLUMN_KINDS: readonly PluginFigureColumnKind[] = [\n 'text',\n 'count',\n 'number',\n 'money',\n 'percent',\n 'change',\n 'duration',\n 'date',\n]\n\nexport interface PluginFigureColumn {\n /** The row field the column reads. */\n key: string\n label: string\n kind: PluginFigureColumnKind\n /** The ISO 4217 code of a `money` column. */\n currency?: string\n}\n\nexport type PluginFigureCell = string | number | null\n\nexport type PluginFigureRow = Record<string, PluginFigureCell>\n\nexport interface PluginFigureTable {\n title: string\n /**\n * Where the figures come from: the name of the console page, and its path\n * under the site's console root for a site reader, or under the workspace's\n * for an org reader (`commerce/orders`), so a consumer can link to it.\n */\n source: { label: string; path: string | null }\n /** The days the figures cover, inclusive; `null` for a current count. */\n period: { from: string; to: string; days: number } | null\n columns: PluginFigureColumn[]\n rows: PluginFigureRow[]\n /** Rows the reader left out past its bound. */\n omitted: number\n /** Short plain sentences a reader of the figures needs, such as a window with nothing earlier to compare with. */\n notes: string[]\n}\n\nexport interface PluginFigureRequest {\n orgId: string\n /** The site a `site` reader reads; for an `org` reader, the site the figures are narrowed to, or `null`. */\n hostId: string | null\n /** The window, in days, ending at `now`: one of the reader's `windows`, or 0 for a reader with none. */\n days: number\n now: Date\n /**\n * The member asking. A reader of records a member may see only part of —\n * a dataset shared with some sites — reads what this member may see; with\n * `null`, nobody asked, and it reads only what every member of the site may.\n */\n uid: string | null\n /** The reader's own parameters, by the names its `params` declare; each a string. */\n params: Readonly<Record<string, string>>\n}\n\n/** One parameter a reader takes beyond its window. */\nexport interface PluginFigureParam {\n /** The name a request carries it under: letters, digits and underscores. */\n name: string\n /** What it selects, in one plain sentence. */\n description: string\n required: boolean\n}\n\nexport type PluginFigureRead =\n | { ok: true; table: PluginFigureTable }\n | {\n ok: false\n status: 400 | 403 | 404\n /** Customer-safe: a caller shows it as it stands. */\n error: string\n }\n\nexport interface PluginFigureReader {\n /** Stable and unique across plugins: lowercase words joined by dots (`commerce.orders`). */\n id: string\n /** What a person calls the figures: \"Orders\". */\n label: string\n /**\n * What the reader answers, in one or two plain sentences. The same bytes for\n * every site, so a list of readers can be shown to anyone who may use them.\n */\n description: string\n scope: PluginFigureScope\n /** The windows the reader covers, in days; empty for a reader of current counts. */\n windows: readonly number[]\n /**\n * The plugin whose records the figures are read from, which must run on the\n * site — or on the workspace, for an `org` reader — before anyone is offered\n * the reader. Absent, the plugin that registered it; `null` for figures the\n * platform itself keeps, such as page views.\n */\n plugin?: string | null\n /** The plan feature the figures are sold under; a workspace without it is not offered the reader. */\n feature?: string\n /** The parameters the reader takes beyond its window; absent for none. */\n params?: readonly PluginFigureParam[]\n read(request: PluginFigureRequest): Promise<PluginFigureRead>\n /**\n * How the reader's table reads in the weekly summary email (AGL-3867), for\n * a reader that belongs in it; absent, the summary leaves it out. The owner\n * words its own figures, because only the owner knows which of its columns\n * say what happened: the summary reads it over 7 days with no parameters\n * and lists what `lines` answers.\n */\n weeklySummary?: PluginFigureWeeklySummary\n}\n\n/** A headline figure, drawn as a tile: \"577 Visitors ▼ 27.9%\". */\nexport interface PluginFigureSummaryStat {\n /** What it counts, as a tile names it: \"Visitors\". */\n label: string\n /** The figure as a person reads it: \"577\", \"$1,240.00\". */\n value: string\n /** The percent change on the week before; `null` or absent when there is nothing to compare with. */\n change?: number | null\n /**\n * A count the workspace overview adds up across sites and the subject line\n * may lead with, and its noun: `{ count: 577, one: 'visitor', many:\n * 'visitors' }`. Stats with the same `one` are summed.\n */\n total?: { count: number; one: string; many: string }\n}\n\n/** One reader's part of a weekly summary: a titled block of tiles, ranked rows, or both. */\nexport interface PluginFigureSummaryPart {\n /** The block's heading: \"Traffic\", \"Top pages\". */\n title: string\n stats?: readonly PluginFigureSummaryStat[]\n /** A ranked list, most first: a page and its views, a form and its submissions. */\n rows?: ReadonlyArray<{ label: string; value: string }>\n /** One plain sentence the figures need. */\n note?: string\n}\n\n/** A reader's part in the weekly summary. */\nexport interface PluginFigureWeeklySummary {\n /**\n * Which email lists it: `site`, each site's own; `workspace`, the\n * workspace overview. A workspace reader may say `site` to be read\n * narrowed to each site. Absent, a site reader is `site` and a workspace\n * reader is `workspace`.\n */\n per?: 'site' | 'workspace'\n /** Where its block sits: lower first. */\n order: number\n /** What the table says this week, or `null` when it has nothing to report. */\n summarize(table: PluginFigureTable): PluginFigureSummaryPart | null\n}\n\nexport const PLUGIN_FIGURES = definePluginServiceContract<PluginFigureReader>('core.figures', {\n multiple: true,\n})\n\n/** The most rows one table carries; the rest are counted in `omitted`. */\nexport const PLUGIN_FIGURE_MAX_ROWS = 25\n\n/** The most columns one table carries. */\nexport const PLUGIN_FIGURE_MAX_COLUMNS = 8\n\n/** The longest text cell, title or label a table carries, in characters. */\nexport const PLUGIN_FIGURE_MAX_TEXT = 120\n\n/** The most notes one table carries. */\nexport const PLUGIN_FIGURE_MAX_NOTES = 3\n\nconst READER_ID = /^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$/\nconst PARAM_NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst COLUMN_KEY = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst CONSOLE_PATH = /^[a-z0-9][a-z0-9/_-]{0,119}$/\nconst CALENDAR_DAY = /^\\d{4}-\\d{2}-\\d{2}$/\nconst CURRENCY = /^[A-Z]{3}$/\n\n/**\n * Registers a reader. The owner is the loader's marker when a register fn is\n * running, else `options.pluginId`; with neither the registration throws. A\n * reader id another plugin already registered throws naming both, and the\n * incumbent keeps serving; the same plugin registering again replaces its own.\n */\nexport function registerPluginFigureReader(\n reader: PluginFigureReader,\n options?: { pluginId?: string },\n): void {\n const key = reader.id.trim()\n if (!READER_ID.test(key)) {\n throw new Error(`a figure reader id is lowercase words joined by dots; refused \"${reader.id}\"`)\n }\n for (const param of reader.params ?? []) {\n if (!PARAM_NAME.test(param.name)) {\n throw new Error(`figure reader \"${key}\" declares a parameter named \"${param.name}\", which is not a name`)\n }\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_FIGURES).find((entry) => entry.key === key)\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `figure reader \"${key}\" is already registered by \"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_FIGURES, reader, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginFigureReader {\n /** The plugin that owns the records behind the figures. */\n pluginId: string\n reader: PluginFigureReader\n}\n\n/** The reader registered under an id, with its owner, or `null`. */\nexport function pluginFigureReader(id: string): ResolvedPluginFigureReader | null {\n const key = id.trim()\n const entry = resolvePluginServices(PLUGIN_FIGURES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, reader: entry.impl } : null\n}\n\n/** Every registered reader with its owner, ordered by id so a list of them is the same bytes on every call. */\nexport function listPluginFigureReaders(): ResolvedPluginFigureReader[] {\n return resolvePluginServices(PLUGIN_FIGURES)\n .map((entry) => ({ pluginId: entry.pluginId, reader: entry.impl }))\n .sort((a, b) => a.reader.id.localeCompare(b.reader.id))\n}\n\n/** The plugin that must run for a reader to be offered: its own `plugin`, else its owner; `null` for platform figures. */\nexport function pluginFigureReaderPlugin(resolved: ResolvedPluginFigureReader): string | null {\n return resolved.reader.plugin === undefined ? resolved.pluginId : resolved.reader.plugin\n}\n\n/**\n * The parameters a request carries that the reader declares, each trimmed to\n * a short string, or the name of the first required one that is missing.\n */\nexport function pluginFigureParams(\n reader: Pick<PluginFigureReader, 'params'>,\n raw: Readonly<Record<string, unknown>> | null | undefined,\n): { ok: true; params: Record<string, string> } | { ok: false; missing: string } {\n const params: Record<string, string> = {}\n for (const param of reader.params ?? []) {\n const value = raw?.[param.name]\n const text = typeof value === 'string' ? value.trim().slice(0, PLUGIN_FIGURE_MAX_TEXT) : ''\n if (text) params[param.name] = text\n else if (param.required) return { ok: false, missing: param.name }\n }\n return { ok: true, params }\n}\n\n// ── Personal details ────────────────────────────────────────────────────\n\nconst EMAIL = /[\\p{L}\\p{N}._%+-]+@[\\p{L}\\p{N}-]+(?:\\.[\\p{L}\\p{N}-]+)+/gu\n/** A run of digits with the separators a phone number is written with; its digits are counted apart. */\nconst PHONE_LIKE = /(?<![\\p{L}\\p{N}/_])\\+?\\(?\\d[\\d\\s().-]{6,}\\d(?![\\p{L}\\p{N}/_])/gu\n\n/** Nine to fifteen digits: a phone number's length anywhere, and longer than any calendar day or year. */\nfunction looksLikePhone(match: string): boolean {\n const digits = match.replace(/\\D/g, '').length\n return digits >= 9 && digits <= 15\n}\n\n/**\n * The text with every email address and phone number replaced by a marker.\n * A figure's label is never meant to carry either, so a match is masked\n * rather than trusted: a campaign named after a person's address says\n * nothing about the campaign that the marker does not.\n */\nexport function maskPersonalDetails(text: string): string {\n return text\n .replace(EMAIL, '[email]')\n .replace(PHONE_LIKE, (match) => (looksLikePhone(match) ? '[phone]' : match))\n}\n\n/** Whether a text carries an email address or a phone number. */\nexport function hasPersonalDetails(text: string): boolean {\n return maskPersonalDetails(text) !== text\n}\n\n// ── The table, held to the contract ─────────────────────────────────────\n\nfunction cut(value: unknown, max = PLUGIN_FIGURE_MAX_TEXT): string {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\nfunction finite(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\nfunction day(value: unknown): string | null {\n return typeof value === 'string' && CALENDAR_DAY.test(value) && !Number.isNaN(Date.parse(value))\n ? value\n : null\n}\n\nfunction cellFor(column: PluginFigureColumn, value: unknown): PluginFigureCell {\n switch (column.kind) {\n case 'text':\n return typeof value === 'string' ? maskPersonalDetails(cut(value)) : null\n case 'date':\n return day(value)\n case 'count': {\n const number = finite(value)\n return number === null ? null : Math.round(number)\n }\n default:\n return finite(value)\n }\n}\n\n/**\n * A reader's table held to the contract: bounded, typed and masked. Columns\n * with a key, a kind or a currency the contract does not know are dropped, and\n * every cell is read through its column, so a cell of the wrong type is `null`\n * rather than passed on. `null` for a value that is not a table at all.\n */\nexport function normalizePluginFigureTable(value: unknown): PluginFigureTable | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const table = value as Partial<PluginFigureTable>\n const title = cut(table.title)\n if (!title) return null\n\n const seen = new Set<string>()\n const columns: PluginFigureColumn[] = []\n for (const raw of Array.isArray(table.columns) ? table.columns : []) {\n if (columns.length === PLUGIN_FIGURE_MAX_COLUMNS) break\n const column = raw as Partial<PluginFigureColumn> | null\n const key = typeof column?.key === 'string' ? column.key : ''\n const kind = column?.kind as PluginFigureColumnKind\n if (!COLUMN_KEY.test(key) || seen.has(key) || !PLUGIN_FIGURE_COLUMN_KINDS.includes(kind)) continue\n if (kind === 'money' && !(typeof column?.currency === 'string' && CURRENCY.test(column.currency))) {\n continue\n }\n seen.add(key)\n columns.push({\n key,\n label: cut(column?.label, 60) || key,\n kind,\n ...(kind === 'money' ? { currency: column?.currency as string } : {}),\n })\n }\n if (!columns.length) return null\n\n const rawRows = Array.isArray(table.rows) ? table.rows : []\n const rows = rawRows.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((raw) => {\n const source = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}\n return Object.fromEntries(columns.map((column) => [column.key, cellFor(column, source[column.key])]))\n })\n const omitted =\n Math.max(0, rawRows.length - PLUGIN_FIGURE_MAX_ROWS) +\n (typeof table.omitted === 'number' && Number.isFinite(table.omitted) && table.omitted > 0\n ? Math.floor(table.omitted)\n : 0)\n\n const period = table.period\n const from = day(period?.from)\n const to = day(period?.to)\n const days = finite(period?.days)\n const path = typeof table.source?.path === 'string' ? table.source.path.trim() : ''\n\n return {\n title,\n source: {\n label: cut(table.source?.label, 80) || title,\n path: CONSOLE_PATH.test(path) && !path.includes('..') ? path : null,\n },\n period:\n from && to && days !== null && days >= 1 && days <= 366\n ? { from, to, days: Math.round(days) }\n : null,\n columns,\n rows,\n omitted,\n notes: (Array.isArray(table.notes) ? table.notes : [])\n .map((note) => maskPersonalDetails(cut(note, 200)))\n .filter(Boolean)\n .slice(0, PLUGIN_FIGURE_MAX_NOTES),\n }\n}\n\n// ── Windows ─────────────────────────────────────────────────────────────\n\n/** A calendar day in UTC. */\nexport function pluginFigureDay(at: Date): string {\n return at.toISOString().slice(0, 10)\n}\n\n/**\n * The window of `days` ending on the day of `now`, inclusive, and the window\n * of the same length immediately before it: what every `change` column\n * compares, so a figure from one reader compares like a figure from another.\n */\nexport function pluginFigureWindows(\n now: Date,\n days: number,\n): {\n current: { from: string; to: string; startMs: number; endMs: number }\n previous: { from: string; to: string; startMs: number; endMs: number }\n} {\n const span = Math.max(1, Math.floor(days))\n const dayMs = 86_400_000\n const endOfToday = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + dayMs\n const currentStart = endOfToday - span * dayMs\n const previousStart = currentStart - span * dayMs\n return {\n current: {\n from: pluginFigureDay(new Date(currentStart)),\n to: pluginFigureDay(new Date(endOfToday - dayMs)),\n startMs: currentStart,\n endMs: endOfToday,\n },\n previous: {\n from: pluginFigureDay(new Date(previousStart)),\n to: pluginFigureDay(new Date(currentStart - dayMs)),\n startMs: previousStart,\n endMs: currentStart,\n },\n }\n}\n\n/**\n * The percent change from `previous` to `current`, to one decimal place.\n * `null` when there is nothing earlier to compare with, so a first window\n * reads as no change figure at all rather than as a rise from zero.\n */\nexport function pluginFigureChange(current: number, previous: number | null): number | null {\n if (previous === null || !Number.isFinite(previous) || previous <= 0) return null\n if (!Number.isFinite(current)) return null\n return Math.round(((current - previous) / previous) * 1_000) / 10\n}\n\n/**\n * How a cell reads to a person, by its column's kind; `null` is \"—\". Every\n * consumer that shows a table to a person words its cells here, so an insight\n * answer and a weekly summary print the same figure the same way.\n */\nexport function pluginFigureCellText(\n table: Pick<PluginFigureTable, 'columns'>,\n key: string,\n value: PluginFigureCell | undefined,\n): string {\n if (value === null || value === undefined || value === '') return '—'\n const column = table.columns.find((entry) => entry.key === key)\n if (typeof value === 'string' || !column) return String(value)\n switch (column.kind) {\n case 'money':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: column.currency ?? 'USD',\n }).format(value)\n case 'percent':\n return `${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'change':\n return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'duration': {\n const seconds = Math.round(value)\n if (seconds < 60) return `${seconds}s`\n const minutes = Math.floor(seconds / 60)\n if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`\n return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`\n }\n default:\n return value.toLocaleString('en-US', { maximumFractionDigits: 2 })\n }\n}\n\n/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */\nexport function pluginFigureIsoWeek(at: Date): string {\n const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()))\n // Thursday decides the week's year.\n const weekday = date.getUTCDay() || 7\n date.setUTCDate(date.getUTCDate() + 4 - weekday)\n const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1)\n const week = Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7)\n return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`\n}\n\n/** The section of the weekly summary a reader is listed in, or `null` for a reader that is not in it. */\nexport function pluginFigureSummarySection(reader: PluginFigureReader): 'site' | 'workspace' | null {\n if (!reader.weeklySummary) return null\n return reader.weeklySummary.per ?? (reader.scope === 'site' ? 'site' : 'workspace')\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_FIGURE_COLUMN_KINDS","PLUGIN_FIGURES","multiple","PLUGIN_FIGURE_MAX_ROWS","PLUGIN_FIGURE_MAX_COLUMNS","PLUGIN_FIGURE_MAX_TEXT","PLUGIN_FIGURE_MAX_NOTES","READER_ID","PARAM_NAME","COLUMN_KEY","CONSOLE_PATH","CALENDAR_DAY","CURRENCY","registerPluginFigureReader","reader","options","key","id","trim","test","Error","param","params","name","pluginId","incumbent","find","entry","pluginFigureReader","one","impl","listPluginFigureReaders","map","sort","a","b","localeCompare","pluginFigureReaderPlugin","resolved","plugin","undefined","pluginFigureParams","raw","value","text","slice","required","ok","missing","EMAIL","PHONE_LIKE","looksLikePhone","match","digits","replace","length","maskPersonalDetails","hasPersonalDetails","cut","max","finite","Number","isFinite","day","isNaN","Date","parse","cellFor","column","kind","number","Math","round","normalizePluginFigureTable","table","Array","isArray","title","seen","Set","columns","has","includes","currency","add","push","label","rawRows","rows","source","Object","fromEntries","omitted","floor","period","from","to","days","path","notes","note","filter","Boolean","pluginFigureDay","at","toISOString","pluginFigureWindows","now","span","dayMs","endOfToday","UTC","getUTCFullYear","getUTCMonth","getUTCDate","currentStart","previousStart","current","startMs","endMs","previous","pluginFigureChange","pluginFigureCellText","String","Intl","NumberFormat","style","format","toLocaleString","maximumFractionDigits","seconds","minutes","padStart","pluginFigureIsoWeek","date","weekday","getUTCDay","setUTCDate","yearStart","week","ceil","getTime","pluginFigureSummarySection","weeklySummary","per","scope"],"mappings":"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"}
|