@aglyn/plugins-ai 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.
Files changed (90) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
  3. package/src/lib/components/ai-brief-dialog.component.js +8 -132
  4. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  5. package/src/lib/components/ai-job-follow.component.js +2 -35
  6. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  7. package/src/lib/components/ai-job-plan.component.d.ts +2 -8
  8. package/src/lib/components/ai-job-plan.component.js +13 -74
  9. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  10. package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
  11. package/src/lib/components/ai-template-collection-field.component.js +13 -10
  12. package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
  13. package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
  14. package/src/lib/components/ai-upsell-dialog.component.js +9 -66
  15. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  16. package/src/lib/components/use-ai-job-run.d.ts +3 -8
  17. package/src/lib/components/use-ai-job-run.js +4 -15
  18. package/src/lib/components/use-ai-job-run.js.map +1 -1
  19. package/src/lib/insights/ai-figure-readers.js +15 -5
  20. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  21. package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
  22. package/src/lib/insights/ai-figure-summaries.js +96 -0
  23. package/src/lib/insights/ai-figure-summaries.js.map +1 -0
  24. package/src/lib/insights/ai-insight-digest.d.ts +2 -0
  25. package/src/lib/insights/ai-insight-digest.js +7 -4
  26. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  27. package/src/lib/insights/ai-insight-readers.d.ts +2 -1
  28. package/src/lib/insights/ai-insight-readers.js +11 -26
  29. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  30. package/src/lib/jobs/ai-job-insight-step.d.ts +1 -1
  31. package/src/lib/jobs/ai-job-insight-step.js +5 -3
  32. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-page-language.d.ts +2 -0
  34. package/src/lib/jobs/ai-job-page-language.js +23 -2
  35. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  36. package/src/lib/jobs/ai-job-plan-step.d.ts +14 -1
  37. package/src/lib/jobs/ai-job-plan-step.js +44 -9
  38. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  39. package/src/lib/jobs/ai-jobs-notify.d.ts +8 -1
  40. package/src/lib/jobs/ai-jobs-notify.js +44 -2
  41. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  42. package/src/lib/jobs/ai-layout-stock-photos.js +11 -1
  43. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  44. package/src/lib/layout-language/ai-layout-compiler.js +18 -7
  45. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  46. package/src/lib/layout-language/ai-layout-promised-copy.d.ts +54 -0
  47. package/src/lib/layout-language/ai-layout-promised-copy.js +104 -0
  48. package/src/lib/layout-language/ai-layout-promised-copy.js.map +1 -0
  49. package/src/lib/model/ai-brief-copy.d.ts +85 -0
  50. package/src/lib/model/ai-brief-copy.js +152 -0
  51. package/src/lib/model/ai-brief-copy.js.map +1 -0
  52. package/src/lib/model/ai-create-view.d.ts +200 -0
  53. package/src/lib/model/ai-create-view.js +263 -0
  54. package/src/lib/model/ai-create-view.js.map +1 -0
  55. package/src/lib/model/ai-insight.d.ts +10 -1
  56. package/src/lib/model/ai-insight.js +16 -39
  57. package/src/lib/model/ai-insight.js.map +1 -1
  58. package/src/lib/model/ai-job-activity.d.ts +24 -0
  59. package/src/lib/model/ai-job-activity.js +52 -1
  60. package/src/lib/model/ai-job-activity.js.map +1 -1
  61. package/src/lib/model/ai-job-notice.d.ts +7 -1
  62. package/src/lib/model/ai-job-notice.js +10 -9
  63. package/src/lib/model/ai-job-notice.js.map +1 -1
  64. package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
  65. package/src/lib/model/ai-job-plan-lines.js +70 -0
  66. package/src/lib/model/ai-job-plan-lines.js.map +1 -0
  67. package/src/lib/model/ai-plan-said-sections.d.ts +81 -0
  68. package/src/lib/model/ai-plan-said-sections.js +168 -0
  69. package/src/lib/model/ai-plan-said-sections.js.map +1 -0
  70. package/src/lib/model/ai-site-job.d.ts +8 -2
  71. package/src/lib/model/ai-site-job.js +7 -1
  72. package/src/lib/model/ai-site-job.js.map +1 -1
  73. package/src/lib/model/ai-template-collections.d.ts +28 -0
  74. package/src/lib/model/ai-template-collections.js +28 -0
  75. package/src/lib/model/ai-template-collections.js.map +1 -0
  76. package/src/lib/model/ai-upsell-copy.d.ts +41 -0
  77. package/src/lib/model/ai-upsell-copy.js +85 -0
  78. package/src/lib/model/ai-upsell-copy.js.map +1 -0
  79. package/src/lib/runtime/ai-doctrine-validators.d.ts +15 -1
  80. package/src/lib/runtime/ai-doctrine-validators.js +96 -3
  81. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  82. package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
  83. package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
  84. package/src/lib/runtime/ai-palette.generated.js +11 -3
  85. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  86. package/src/lib/server/ai-create-route.d.ts +32 -0
  87. package/src/lib/server/ai-create-route.js +295 -0
  88. package/src/lib/server/ai-create-route.js.map +1 -0
  89. package/src/lib/server.js +14 -0
  90. package/src/lib/server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/use-ai-job-run.ts"],"sourcesContent":["'use client'\n\n/**\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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobKind,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport { followAiJobEvents } from './ai-job-events'\nimport { publishAiJob } from './ai-jobs-store'\n\n/**\n * Starting one AI job from a console control, and following it until it\n * settles (AGL-2919): the jobs route's create door, then its events door, the\n * way the SEO card follows its jobs. A control that starts a job the beat\n * runs shows its progress here and its result when it arrives; closing the\n * control stops following, and the job carries on in AI jobs.\n */\n\n/** What a control asks the create door for. */\nexport interface AiJobRequest {\n orgId: string\n /** `null` for a job about the workspace rather than one site. */\n hostId: string | null\n kind: AiJobKind\n brief: string\n inputs: Record<string, string>\n}\n\n/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */\nexport function aiJobSettled(job: AiJobSummary | null): boolean {\n if (!job) return false\n return (\n AI_JOB_TERMINAL_STATUSES.includes(job.status) ||\n job.status === 'needs_input' ||\n job.status === 'needs_review'\n )\n}\n\n/**\n * Why a settled job has no result to show, in words a person can act on, or\n * `null` for a job that finished.\n */\nexport function aiJobProblem(job: AiJobSummary | null, failed: string): string | null {\n if (!job || !aiJobSettled(job) || job.status === 'done') return null\n if (job.status === 'canceled') return 'The job was canceled.'\n if (job.status === 'needs_review') return job.review?.message || job.error || failed\n return job.error || failed\n}\n\nexport interface AiJobRun {\n /** The job as it last arrived; `null` before one is started. */\n job: AiJobSummary | null\n /** The create door is being asked. */\n starting: boolean\n /** The job exists and has not settled. */\n running: boolean\n /** Why the create door refused, or could not be reached. */\n notice: string | null\n /** Starts the job, `true` once it exists, and follows it from there; `false` when the door refused. */\n start: (request: AiJobRequest) => Promise<boolean>\n /** Stops following and forgets the job, for a control asked again. */\n reset: () => void\n}\n\nexport function useAiJobRun(user: MaybeTokenSource, failed: string): AiJobRun {\n const userRef = useRef(user)\n userRef.current = user\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [starting, setStarting] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n /** Follows a job through the events route until it settles or the control goes. */\n const watch = useCallback(async (jobId: string, orgId: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n await followAiJobEvents(() => userRef.current, orgId, jobId, controller.signal, setJob)\n }, [])\n\n const start = useCallback(\n async (request: AiJobRequest) => {\n watchRef.current?.abort()\n setStarting(true)\n setNotice(null)\n setJob(null)\n let next: AiJobSummary | undefined\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? failed))\n return false\n }\n next = payload?.job as AiJobSummary | undefined\n if (!next) {\n setNotice(failed)\n return false\n }\n setJob(next)\n // The indicator counts it from the moment it exists (AGL-3593).\n publishAiJob(next)\n } catch {\n setNotice(failed)\n return false\n } finally {\n setStarting(false)\n }\n if (!aiJobSettled(next)) void watch(next.id, request.orgId)\n return true\n },\n [failed, watch],\n )\n\n const reset = useCallback(() => {\n watchRef.current?.abort()\n setJob(null)\n setNotice(null)\n }, [])\n\n return { job, starting, running: Boolean(job && !aiJobSettled(job)), notice, start, reset }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","useCallback","useEffect","useRef","useState","AI_JOB_TERMINAL_STATUSES","followAiJobEvents","publishAiJob","aiJobSettled","job","includes","status","aiJobProblem","failed","review","message","error","useAiJobRun","user","userRef","current","setJob","starting","setStarting","notice","setNotice","watchRef","abort","watch","jobId","orgId","controller","AbortController","signal","start","request","next","response","method","headers","body","JSON","stringify","payload","json","catch","ok","locked","String","id","reset","running","Boolean"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/use-ai-job-run.ts"],"sourcesContent":["'use client'\n\n/**\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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { AiJobKind, AiJobSummary } from '../model/ai-jobs.types'\nimport { aiJobProblem, aiJobSettled } from '../model/ai-job-activity'\nimport { followAiJobEvents } from './ai-job-events'\nimport { publishAiJob } from './ai-jobs-store'\n\n/**\n * Starting one AI job from a console control, and following it until it\n * settles (AGL-2919): the jobs route's create door, then its events door, the\n * way the SEO card follows its jobs. A control that starts a job the beat\n * runs shows its progress here and its result when it arrives; closing the\n * control stops following, and the job carries on in AI jobs.\n */\n\n/** What a control asks the create door for. */\nexport interface AiJobRequest {\n orgId: string\n /** `null` for a job about the workspace rather than one site. */\n hostId: string | null\n kind: AiJobKind\n brief: string\n inputs: Record<string, string>\n}\n\n// Whether a job settled, and why it has nothing to show, are the model's\n// (AGL-3854), which the native apps' follow view reads too.\nexport { aiJobProblem, aiJobSettled }\n\nexport interface AiJobRun {\n /** The job as it last arrived; `null` before one is started. */\n job: AiJobSummary | null\n /** The create door is being asked. */\n starting: boolean\n /** The job exists and has not settled. */\n running: boolean\n /** Why the create door refused, or could not be reached. */\n notice: string | null\n /** Starts the job, `true` once it exists, and follows it from there; `false` when the door refused. */\n start: (request: AiJobRequest) => Promise<boolean>\n /** Stops following and forgets the job, for a control asked again. */\n reset: () => void\n}\n\nexport function useAiJobRun(user: MaybeTokenSource, failed: string): AiJobRun {\n const userRef = useRef(user)\n userRef.current = user\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [starting, setStarting] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n /** Follows a job through the events route until it settles or the control goes. */\n const watch = useCallback(async (jobId: string, orgId: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n await followAiJobEvents(() => userRef.current, orgId, jobId, controller.signal, setJob)\n }, [])\n\n const start = useCallback(\n async (request: AiJobRequest) => {\n watchRef.current?.abort()\n setStarting(true)\n setNotice(null)\n setJob(null)\n let next: AiJobSummary | undefined\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? failed))\n return false\n }\n next = payload?.job as AiJobSummary | undefined\n if (!next) {\n setNotice(failed)\n return false\n }\n setJob(next)\n // The indicator counts it from the moment it exists (AGL-3593).\n publishAiJob(next)\n } catch {\n setNotice(failed)\n return false\n } finally {\n setStarting(false)\n }\n if (!aiJobSettled(next)) void watch(next.id, request.orgId)\n return true\n },\n [failed, watch],\n )\n\n const reset = useCallback(() => {\n watchRef.current?.abort()\n setJob(null)\n setNotice(null)\n }, [])\n\n return { job, starting, running: Boolean(job && !aiJobSettled(job)), notice, start, reset }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","useCallback","useEffect","useRef","useState","aiJobProblem","aiJobSettled","followAiJobEvents","publishAiJob","useAiJobRun","user","failed","userRef","current","job","setJob","starting","setStarting","notice","setNotice","watchRef","abort","watch","jobId","orgId","controller","AbortController","signal","start","request","next","response","method","headers","body","JSON","stringify","payload","json","catch","ok","locked","status","String","error","id","reset","running","Boolean"],"mappings":"AAAA;AAEA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,oBAAoB,QAAQ,eAAc;AACxE,SACEC,eAAe,QAEV,2CAA0C;AACjD,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAEhE,SAASC,YAAY,EAAEC,YAAY,QAAQ,8BAA0B;AACrE,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,YAAY,QAAQ,qBAAiB;AAoB9C,yEAAyE;AACzE,4DAA4D;AAC5D,SAASH,YAAY,EAAEC,YAAY,GAAE;AAiBrC,OAAO,SAASG,YAAYC,IAAsB,EAAEC,MAAc;IAChE,MAAMC,UAAUT,OAAOO;IACvBE,QAAQC,OAAO,GAAGH;IAClB,MAAM,CAACI,KAAKC,OAAO,GAAGX,SAA8B;IACpD,MAAM,CAACY,UAAUC,YAAY,GAAGb,SAAS;IACzC,MAAM,CAACc,QAAQC,UAAU,GAAGf,SAAwB;IACpD,MAAMgB,WAAWjB,OAA+B;IAEhDD,UAAU,IAAM;gBAAMkB;oBAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;WAAI,EAAE;IAEnD,iFAAiF,GACjF,MAAMC,QAAQrB,YAAY,OAAOsB,OAAeC;YAC9CJ;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvB,MAAMI,aAAa,IAAIC;QACvBN,SAASP,OAAO,GAAGY;QACnB,MAAMlB,kBAAkB,IAAMK,QAAQC,OAAO,EAAEW,OAAOD,OAAOE,WAAWE,MAAM,EAAEZ;IAClF,GAAG,EAAE;IAEL,MAAMa,QAAQ3B,YACZ,OAAO4B;YACLT;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvBJ,YAAY;QACZE,UAAU;QACVJ,OAAO;QACP,IAAIe;QACJ,IAAI;YACF,MAAMC,WAAW,MAAM/B,gBAAgBY,QAAQC,OAAO,EAAE,gBAAgB;gBACtEmB,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,MAAMC,KAAKC,SAAS,CAACP;YACvB;YACA,MAAMQ,UAAU,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAM;YAClD,IAAI,CAACR,SAASS,EAAE,EAAE;;gBAChB,MAAMC,SAAS1C,qBAAqBgC,SAASW,MAAM,EAAEL;gBACrDlB,UAAUsB,SAAS3C,oBAAoB2C,UAAUE,eAAON,2BAAAA,QAASO,KAAK,mBAAIjC;gBAC1E,OAAO;YACT;YACAmB,OAAOO,2BAAAA,QAASvB,GAAG;YACnB,IAAI,CAACgB,MAAM;gBACTX,UAAUR;gBACV,OAAO;YACT;YACAI,OAAOe;YACP,gEAAgE;YAChEtB,aAAasB;QACf,EAAE,eAAM;YACNX,UAAUR;YACV,OAAO;QACT,SAAU;YACRM,YAAY;QACd;QACA,IAAI,CAACX,aAAawB,OAAO,KAAKR,MAAMQ,KAAKe,EAAE,EAAEhB,QAAQL,KAAK;QAC1D,OAAO;IACT,GACA;QAACb;QAAQW;KAAM;IAGjB,MAAMwB,QAAQ7C,YAAY;YACxBmB;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvBN,OAAO;QACPI,UAAU;IACZ,GAAG,EAAE;IAEL,OAAO;QAAEL;QAAKE;QAAU+B,SAASC,QAAQlC,OAAO,CAACR,aAAaQ;QAAOI;QAAQU;QAAOkB;IAAM;AAC5F"}
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  */ import { formPeriodKey, formStatsTotals } from "@aglyn/aglyn/app-utils/forms";
18
18
  import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureChange, pluginFigureDay, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
19
19
  import { AI_PLUGIN_ID } from "../constants.js";
20
+ import { FORMS_WEEKLY_SUMMARY, SOURCES_WEEKLY_SUMMARY, TOP_PAGES_WEEKLY_SUMMARY, TRAFFIC_WEEKLY_SUMMARY } from "./ai-figure-summaries.js";
20
21
  /** The traffic windows: the Traffic card's own ranges. */ export const AI_TRAFFIC_WINDOWS = [
21
22
  7,
22
23
  14,
@@ -129,7 +130,8 @@ export function trafficFigureReaders(firestore) {
129
130
  id: 'traffic.summary',
130
131
  label: 'Traffic',
131
132
  description: 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',
132
- windows: AI_TRAFFIC_WINDOWS
133
+ windows: AI_TRAFFIC_WINDOWS,
134
+ weeklySummary: TRAFFIC_WEEKLY_SUMMARY
133
135
  }, async (db, request)=>{
134
136
  const ids = dayIds(request.now, request.days);
135
137
  const [current, previous] = await Promise.all([
@@ -205,7 +207,8 @@ export function trafficFigureReaders(firestore) {
205
207
  id: 'traffic.pages',
206
208
  label: 'Top pages',
207
209
  description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,
208
- windows: AI_TRAFFIC_WINDOWS
210
+ windows: AI_TRAFFIC_WINDOWS,
211
+ weeklySummary: TOP_PAGES_WEEKLY_SUMMARY
209
212
  }, async (db, request)=>{
210
213
  const ids = dayIds(request.now, request.days);
211
214
  const [current, previous] = await Promise.all([
@@ -262,11 +265,17 @@ export function trafficFigureReaders(firestore) {
262
265
  id: 'traffic.sources',
263
266
  label: 'Traffic sources',
264
267
  description: 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',
265
- windows: AI_TRAFFIC_WINDOWS
268
+ windows: AI_TRAFFIC_WINDOWS,
269
+ weeklySummary: SOURCES_WEEKLY_SUMMARY
266
270
  }, async (db, request)=>{
267
271
  const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current);
268
272
  const all = days.reduce((sum, day)=>sum + day.total, 0);
269
- const referrers = ranked(sumMaps(days, (day)=>day.referrers));
273
+ // The collector stores a referring host with its dots as `_`, because a
274
+ // Firestore field path cannot hold one; a host is read back as written.
275
+ const referrers = ranked(sumMaps(days, (day)=>day.referrers)).map(([host, views])=>[
276
+ host.replace(/_/g, '.'),
277
+ views
278
+ ]);
270
279
  const sources = ranked(sumMaps(days, (day)=>{
271
280
  var _day_utm_source;
272
281
  return (_day_utm_source = day.utm['source']) != null ? _day_utm_source : {};
@@ -375,7 +384,8 @@ export function formsFigureReader(firestore) {
375
384
  id: 'forms.performance',
376
385
  label: 'Forms',
377
386
  description: 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',
378
- windows: AI_TRAFFIC_WINDOWS
387
+ windows: AI_TRAFFIC_WINDOWS,
388
+ weeklySummary: FORMS_WEEKLY_SUMMARY
379
389
  }, async (db, request)=>{
380
390
  const { current } = pluginFigureWindows(request.now, request.days);
381
391
  const from = formPeriodKey(current.startMs);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-readers.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 { formPeriodKey, formStatsTotals, type FormStats } from '@aglyn/aglyn/app-utils/forms'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureChange,\n pluginFigureDay,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRead,\n type PluginFigureRequest,\n type PluginFigureRow,\n type PluginFigureTable,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { AI_PLUGIN_ID } from '../constants'\n\n/**\n * The figure readers this plugin registers for records the PLATFORM keeps\n * (AGL-2915): a site's page-view counters and its forms' counters. Every\n * other reader an insight reads is registered by the plugin that owns its\n * records — commerce, bookings, marketing, the workspace's datasets — on the\n * `plugin-figures` seam, and nothing here reads their documents.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a page path, a form's name\n * or a referring site — never a visitor or a submission.\n *\n * The readers use the arithmetic their console cards use: the Traffic card's\n * windows and change (`pluginFigureWindows`, `pluginFigureChange`, which are\n * that card's split and delta), and the forms plugin's month counters through\n * `formStatsTotals`, the one function every forms surface windows by.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type AiFigureFirestore = () => Firestore\n\n/** The traffic windows: the Traffic card's own ranges. */\nexport const AI_TRAFFIC_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** A day-by-day table fits a table's row bound only over a short window. */\nexport const AI_TRAFFIC_DAILY_WINDOWS: readonly number[] = [7, 14]\n\n/** Pages a pages table lists; the rest are counted in `omitted`. */\nexport const AI_TRAFFIC_TOP_PAGES = 10\n\n/** Referring sites, and campaign sources and campaigns, a sources table lists. */\nexport const AI_TRAFFIC_TOP_REFERRERS = 8\nexport const AI_TRAFFIC_TOP_CAMPAIGNS = 5\n\n/** Forms a forms table reads. */\nexport const AI_FORMS_READ_LIMIT = 100\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\n/** A window's days as the table's period, oldest to newest. */\nfunction periodOf(request: PluginFigureRequest): PluginFigureTable['period'] {\n const { current } = pluginFigureWindows(request.now, request.days)\n return { from: current.from, to: current.to, days: request.days }\n}\n\n/** Every day id of the window and the one before it, newest first. */\nfunction dayIds(now: Date, days: number): { current: string[]; previous: string[] } {\n const { current, previous } = pluginFigureWindows(now, days)\n const walk = (startMs: number, endMs: number): string[] => {\n const ids: string[] = []\n for (let at = endMs - 86_400_000; at >= startMs; at -= 86_400_000) ids.push(pluginFigureDay(new Date(at)))\n return ids\n }\n return { current: walk(current.startMs, current.endMs), previous: walk(previous.startMs, previous.endMs) }\n}\n\n// ── Traffic ───────────────────────────────────────────────────────────────\n\ninterface TrafficDay {\n total: number\n visitors: number\n paths: Record<string, number>\n referrers: Record<string, number>\n utm: Record<string, Record<string, number>>\n}\n\nconst counts = (value: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n if (!value || typeof value !== 'object') return out\n for (const [key, count] of Object.entries(value as Record<string, unknown>)) {\n const n = num(count)\n if (n > 0) out[key] = n\n }\n return out\n}\n\nasync function readTrafficDays(\n firestore: Firestore,\n hostId: string,\n ids: readonly string[],\n): Promise<TrafficDay[]> {\n if (!ids.length) return []\n const collection = firestore.collection('hosts').doc(hostId).collection('analytics')\n const snapshots = await firestore.getAll(...ids.map((id) => collection.doc(id)))\n return snapshots.map((snapshot) => {\n const data = (snapshot.exists ? snapshot.data() : null) ?? {}\n const utm: Record<string, Record<string, number>> = {}\n for (const [param, values] of Object.entries((data['utm'] ?? {}) as Record<string, unknown>)) {\n utm[param] = counts(values)\n }\n return {\n total: num(data['total']),\n visitors: num(data['visitors']),\n paths: counts(data['paths']),\n referrers: counts(data['referrers']),\n utm,\n }\n })\n}\n\nfunction sumMaps(days: readonly TrafficDay[], pick: (day: TrafficDay) => Record<string, number>): Map<string, number> {\n const totals = new Map<string, number>()\n for (const day of days) {\n for (const [key, count] of Object.entries(pick(day))) totals.set(key, (totals.get(key) ?? 0) + count)\n }\n return totals\n}\n\nconst ranked = (totals: Map<string, number>): Array<[string, number]> =>\n [...totals.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n\nfunction siteReader(\n base: Omit<PluginFigureReader, 'read' | 'scope' | 'plugin'>,\n read: (firestore: Firestore, request: PluginFigureRequest & { hostId: string }) => Promise<PluginFigureRead>,\n firestore: AiFigureFirestore,\n plugin: string | null,\n): PluginFigureReader {\n return {\n ...base,\n scope: 'site',\n plugin,\n read: async (request) => {\n if (!request.hostId) return refused(400, 'Open a site to read its figures')\n if (!base.windows.includes(request.days)) return refused(400, 'That window is not one this reader covers')\n return read(firestore(), { ...request, hostId: request.hostId })\n },\n }\n}\n\nexport function trafficFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n const source = { label: 'Analytics', path: 'analytics' }\n const visitorsNote =\n 'Visitors are counted once per browser tab per day, so one person on two days counts twice.'\n return [\n siteReader(\n {\n id: 'traffic.summary',\n label: 'Traffic',\n description:\n 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const total = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.total, 0)\n const visitors = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.visitors, 0)\n const views = total(current)\n const viewsBefore = total(previous)\n const perDay = Math.round(views / request.days)\n const perDayBefore = Math.round(viewsBefore / request.days)\n const table: PluginFigureTable = {\n title: 'Traffic',\n source,\n period: periodOf(request),\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'Page views', current: views, previous: viewsBefore, change: pluginFigureChange(views, viewsBefore) },\n {\n figure: 'Visitors',\n current: visitors(current),\n previous: visitors(previous),\n change: pluginFigureChange(visitors(current), visitors(previous)),\n },\n {\n figure: 'Page views a day',\n current: perDay,\n previous: perDayBefore,\n change: pluginFigureChange(perDay, perDayBefore),\n },\n ],\n omitted: 0,\n notes: [\n visitorsNote,\n ...(viewsBefore ? [] : ['The window before this one recorded no page views, so there is no change to compare.']),\n ],\n }\n return { ok: true, table }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.pages',\n label: 'Top pages',\n description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const now = ranked(sumMaps(current, (day) => day.paths))\n const before = sumMaps(previous, (day) => day.paths)\n const all = now.reduce((sum, [, count]) => sum + count, 0)\n const rows: PluginFigureRow[] = now.slice(0, AI_TRAFFIC_TOP_PAGES).map(([path, views]) => ({\n page: path,\n views,\n share: share(views, all),\n change: pluginFigureChange(views, before.get(path) ?? null),\n }))\n return {\n ok: true,\n table: {\n title: 'Top pages',\n source,\n period: periodOf(request),\n columns: [\n { key: 'page', label: 'Page', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of views', kind: 'percent' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows,\n omitted: Math.max(0, now.length - rows.length),\n notes: [],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.sources',\n label: 'Traffic sources',\n description:\n 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current)\n const all = days.reduce((sum, day) => sum + day.total, 0)\n const referrers = ranked(sumMaps(days, (day) => day.referrers))\n const sources = ranked(sumMaps(days, (day) => day.utm['source'] ?? {}))\n const campaigns = ranked(sumMaps(days, (day) => day.utm['campaign'] ?? {}))\n const rowsOf = (entries: Array<[string, number]>, limit: number, type: string) =>\n entries.slice(0, limit).map(([name, views]) => ({ source: name, type, views, share: share(views, all) }))\n const rows = [\n ...rowsOf(referrers, AI_TRAFFIC_TOP_REFERRERS, 'Referring site'),\n ...rowsOf(sources, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign source'),\n ...rowsOf(campaigns, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign'),\n ]\n return {\n ok: true,\n table: {\n title: 'Traffic sources',\n source,\n period: periodOf(request),\n columns: [\n { key: 'source', label: 'Source', kind: 'text' },\n { key: 'type', label: 'Kind', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of all views', kind: 'percent' },\n ],\n rows,\n omitted:\n Math.max(0, referrers.length - AI_TRAFFIC_TOP_REFERRERS) +\n Math.max(0, sources.length - AI_TRAFFIC_TOP_CAMPAIGNS) +\n Math.max(0, campaigns.length - AI_TRAFFIC_TOP_CAMPAIGNS),\n notes: ['A visit with no referring site, such as a typed address, is in no row.'],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.daily',\n label: 'Traffic by day',\n description: 'Page views and visitors on each day of the window, oldest first, for spotting a day that stood out.',\n windows: AI_TRAFFIC_DAILY_WINDOWS,\n },\n async (db, request) => {\n const ids = [...dayIds(request.now, request.days).current].reverse()\n const days = await readTrafficDays(db, request.hostId, ids)\n return {\n ok: true,\n table: {\n title: 'Traffic by day',\n source,\n period: periodOf(request),\n columns: [\n { key: 'day', label: 'Day', kind: 'date' },\n { key: 'views', label: 'Page views', kind: 'count' },\n { key: 'visitors', label: 'Visitors', kind: 'count' },\n ],\n rows: days.map((day, index) => ({ day: ids[index], views: day.total, visitors: day.visitors })),\n omitted: 0,\n notes: [visitorsNote],\n },\n }\n },\n firestore,\n null,\n ),\n ]\n}\n\n// ── Forms ─────────────────────────────────────────────────────────────────\n\nexport function formsFigureReader(firestore: AiFigureFirestore): PluginFigureReader {\n return siteReader(\n {\n id: 'forms.performance',\n label: 'Forms',\n description:\n 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const { current } = pluginFigureWindows(request.now, request.days)\n const from = formPeriodKey(current.startMs)\n const to = formPeriodKey(current.endMs - 1)\n const snapshot = await db\n .collection('hosts')\n .doc(request.hostId)\n .collection('forms')\n .limit(AI_FORMS_READ_LIMIT)\n .get()\n const forms = snapshot.docs\n .map((doc) => ({ doc, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => !data['deletedAt'])\n .map(({ doc, data }) => {\n const totals = formStatsTotals(data['stats'] as FormStats | undefined, { from, to })\n // Every submission to a form is counted on it, so a form with none\n // recorded in the window had none: 0, not a blank the answer has to\n // hedge over (AGL-3834). Leads are counted only on a form that\n // routes them, so one that does reads 0 the same way, and a form\n // that never has keeps a blank, which the table's note explains.\n const submissions = totals.submissions ?? 0\n const routesLeads = (data['routing'] as { lead?: unknown } | undefined)?.lead === true\n return {\n form: str(data['displayName']) || doc.id,\n views: totals.views,\n submissions,\n completion: totals.views ? share(submissions, totals.views) : null,\n leads: totals.leads ?? (routesLeads ? 0 : null),\n }\n })\n .sort((a, b) => b.submissions - a.submissions || a.form.localeCompare(b.form))\n const monthStart = from ? `${from}-01` : current.from\n return {\n ok: true,\n table: {\n title: 'Forms',\n source: { label: 'Forms', path: 'forms' },\n period: {\n from: monthStart,\n to: current.to,\n days: Math.round((Date.parse(current.to) - Date.parse(monthStart)) / 86_400_000) + 1,\n },\n columns: [\n { key: 'form', label: 'Form', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'submissions', label: 'Submissions', kind: 'count' },\n { key: 'completion', label: 'Views that became a submission', kind: 'percent' },\n { key: 'leads', label: 'Leads', kind: 'count' },\n ],\n rows: forms.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, forms.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n 'Form figures are kept by calendar month, so they cover whole months from the first day of the month the window starts in.',\n 'Views are counted in the visitor’s browser and submissions on the server, so a rate can pass 100%.',\n 'A blank Leads figure means the form does not add its submissions to Leads.',\n ],\n },\n }\n },\n firestore,\n 'forms',\n )\n}\n\n/** Every reader this plugin registers, for the platform's own records. */\nexport function aiFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n return [...trafficFigureReaders(firestore), formsFigureReader(firestore)]\n}\n\n/**\n * Registers them from the console surface, the one surface that runs insight\n * jobs. By a call, never by an import (AGL-3025).\n */\nexport function registerAiFigureReaders(firestore: AiFigureFirestore): void {\n for (const reader of aiFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: AI_PLUGIN_ID })\n }\n}\n"],"names":["formPeriodKey","formStatsTotals","PLUGIN_FIGURE_MAX_ROWS","pluginFigureChange","pluginFigureDay","pluginFigureWindows","registerPluginFigureReader","AI_PLUGIN_ID","AI_TRAFFIC_WINDOWS","AI_TRAFFIC_DAILY_WINDOWS","AI_TRAFFIC_TOP_PAGES","AI_TRAFFIC_TOP_REFERRERS","AI_TRAFFIC_TOP_CAMPAIGNS","AI_FORMS_READ_LIMIT","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","periodOf","request","current","now","days","from","to","dayIds","previous","walk","startMs","endMs","ids","at","push","Date","counts","out","key","count","Object","entries","n","readTrafficDays","firestore","hostId","length","collection","doc","snapshots","getAll","map","id","snapshot","data","exists","utm","param","values","total","visitors","paths","referrers","sumMaps","pick","totals","Map","day","set","get","ranked","sort","a","b","localeCompare","siteReader","base","read","plugin","scope","windows","includes","trafficFigureReaders","source","label","path","visitorsNote","description","db","Promise","all","reduce","sum","views","viewsBefore","perDay","perDayBefore","table","title","period","columns","kind","rows","figure","change","omitted","notes","before","slice","page","max","sources","campaigns","rowsOf","limit","type","name","reverse","index","formsFigureReader","forms","docs","filter","submissions","routesLeads","lead","form","completion","leads","monthStart","parse","aiFigureReaders","registerAiFigureReaders","reader","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-readers.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 { formPeriodKey, formStatsTotals, type FormStats } from '@aglyn/aglyn/app-utils/forms'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureChange,\n pluginFigureDay,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRead,\n type PluginFigureRequest,\n type PluginFigureRow,\n type PluginFigureTable,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { AI_PLUGIN_ID } from '../constants'\nimport {\n FORMS_WEEKLY_SUMMARY,\n SOURCES_WEEKLY_SUMMARY,\n TOP_PAGES_WEEKLY_SUMMARY,\n TRAFFIC_WEEKLY_SUMMARY,\n} from './ai-figure-summaries'\n\n/**\n * The figure readers this plugin registers for records the PLATFORM keeps\n * (AGL-2915): a site's page-view counters and its forms' counters. Every\n * other reader an insight reads is registered by the plugin that owns its\n * records — commerce, bookings, marketing, the workspace's datasets — on the\n * `plugin-figures` seam, and nothing here reads their documents.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a page path, a form's name\n * or a referring site — never a visitor or a submission.\n *\n * The readers use the arithmetic their console cards use: the Traffic card's\n * windows and change (`pluginFigureWindows`, `pluginFigureChange`, which are\n * that card's split and delta), and the forms plugin's month counters through\n * `formStatsTotals`, the one function every forms surface windows by.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type AiFigureFirestore = () => Firestore\n\n/** The traffic windows: the Traffic card's own ranges. */\nexport const AI_TRAFFIC_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** A day-by-day table fits a table's row bound only over a short window. */\nexport const AI_TRAFFIC_DAILY_WINDOWS: readonly number[] = [7, 14]\n\n/** Pages a pages table lists; the rest are counted in `omitted`. */\nexport const AI_TRAFFIC_TOP_PAGES = 10\n\n/** Referring sites, and campaign sources and campaigns, a sources table lists. */\nexport const AI_TRAFFIC_TOP_REFERRERS = 8\nexport const AI_TRAFFIC_TOP_CAMPAIGNS = 5\n\n/** Forms a forms table reads. */\nexport const AI_FORMS_READ_LIMIT = 100\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\n/** A window's days as the table's period, oldest to newest. */\nfunction periodOf(request: PluginFigureRequest): PluginFigureTable['period'] {\n const { current } = pluginFigureWindows(request.now, request.days)\n return { from: current.from, to: current.to, days: request.days }\n}\n\n/** Every day id of the window and the one before it, newest first. */\nfunction dayIds(now: Date, days: number): { current: string[]; previous: string[] } {\n const { current, previous } = pluginFigureWindows(now, days)\n const walk = (startMs: number, endMs: number): string[] => {\n const ids: string[] = []\n for (let at = endMs - 86_400_000; at >= startMs; at -= 86_400_000) ids.push(pluginFigureDay(new Date(at)))\n return ids\n }\n return { current: walk(current.startMs, current.endMs), previous: walk(previous.startMs, previous.endMs) }\n}\n\n// ── Traffic ───────────────────────────────────────────────────────────────\n\ninterface TrafficDay {\n total: number\n visitors: number\n paths: Record<string, number>\n referrers: Record<string, number>\n utm: Record<string, Record<string, number>>\n}\n\nconst counts = (value: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n if (!value || typeof value !== 'object') return out\n for (const [key, count] of Object.entries(value as Record<string, unknown>)) {\n const n = num(count)\n if (n > 0) out[key] = n\n }\n return out\n}\n\nasync function readTrafficDays(\n firestore: Firestore,\n hostId: string,\n ids: readonly string[],\n): Promise<TrafficDay[]> {\n if (!ids.length) return []\n const collection = firestore.collection('hosts').doc(hostId).collection('analytics')\n const snapshots = await firestore.getAll(...ids.map((id) => collection.doc(id)))\n return snapshots.map((snapshot) => {\n const data = (snapshot.exists ? snapshot.data() : null) ?? {}\n const utm: Record<string, Record<string, number>> = {}\n for (const [param, values] of Object.entries((data['utm'] ?? {}) as Record<string, unknown>)) {\n utm[param] = counts(values)\n }\n return {\n total: num(data['total']),\n visitors: num(data['visitors']),\n paths: counts(data['paths']),\n referrers: counts(data['referrers']),\n utm,\n }\n })\n}\n\nfunction sumMaps(days: readonly TrafficDay[], pick: (day: TrafficDay) => Record<string, number>): Map<string, number> {\n const totals = new Map<string, number>()\n for (const day of days) {\n for (const [key, count] of Object.entries(pick(day))) totals.set(key, (totals.get(key) ?? 0) + count)\n }\n return totals\n}\n\nconst ranked = (totals: Map<string, number>): Array<[string, number]> =>\n [...totals.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n\nfunction siteReader(\n base: Omit<PluginFigureReader, 'read' | 'scope' | 'plugin'>,\n read: (firestore: Firestore, request: PluginFigureRequest & { hostId: string }) => Promise<PluginFigureRead>,\n firestore: AiFigureFirestore,\n plugin: string | null,\n): PluginFigureReader {\n return {\n ...base,\n scope: 'site',\n plugin,\n read: async (request) => {\n if (!request.hostId) return refused(400, 'Open a site to read its figures')\n if (!base.windows.includes(request.days)) return refused(400, 'That window is not one this reader covers')\n return read(firestore(), { ...request, hostId: request.hostId })\n },\n }\n}\n\nexport function trafficFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n const source = { label: 'Analytics', path: 'analytics' }\n const visitorsNote =\n 'Visitors are counted once per browser tab per day, so one person on two days counts twice.'\n return [\n siteReader(\n {\n id: 'traffic.summary',\n label: 'Traffic',\n description:\n 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: TRAFFIC_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const total = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.total, 0)\n const visitors = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.visitors, 0)\n const views = total(current)\n const viewsBefore = total(previous)\n const perDay = Math.round(views / request.days)\n const perDayBefore = Math.round(viewsBefore / request.days)\n const table: PluginFigureTable = {\n title: 'Traffic',\n source,\n period: periodOf(request),\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'Page views', current: views, previous: viewsBefore, change: pluginFigureChange(views, viewsBefore) },\n {\n figure: 'Visitors',\n current: visitors(current),\n previous: visitors(previous),\n change: pluginFigureChange(visitors(current), visitors(previous)),\n },\n {\n figure: 'Page views a day',\n current: perDay,\n previous: perDayBefore,\n change: pluginFigureChange(perDay, perDayBefore),\n },\n ],\n omitted: 0,\n notes: [\n visitorsNote,\n ...(viewsBefore ? [] : ['The window before this one recorded no page views, so there is no change to compare.']),\n ],\n }\n return { ok: true, table }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.pages',\n label: 'Top pages',\n description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: TOP_PAGES_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const now = ranked(sumMaps(current, (day) => day.paths))\n const before = sumMaps(previous, (day) => day.paths)\n const all = now.reduce((sum, [, count]) => sum + count, 0)\n const rows: PluginFigureRow[] = now.slice(0, AI_TRAFFIC_TOP_PAGES).map(([path, views]) => ({\n page: path,\n views,\n share: share(views, all),\n change: pluginFigureChange(views, before.get(path) ?? null),\n }))\n return {\n ok: true,\n table: {\n title: 'Top pages',\n source,\n period: periodOf(request),\n columns: [\n { key: 'page', label: 'Page', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of views', kind: 'percent' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows,\n omitted: Math.max(0, now.length - rows.length),\n notes: [],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.sources',\n label: 'Traffic sources',\n description:\n 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: SOURCES_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current)\n const all = days.reduce((sum, day) => sum + day.total, 0)\n // The collector stores a referring host with its dots as `_`, because a\n // Firestore field path cannot hold one; a host is read back as written.\n const referrers = ranked(sumMaps(days, (day) => day.referrers)).map(\n ([host, views]): [string, number] => [host.replace(/_/g, '.'), views],\n )\n const sources = ranked(sumMaps(days, (day) => day.utm['source'] ?? {}))\n const campaigns = ranked(sumMaps(days, (day) => day.utm['campaign'] ?? {}))\n const rowsOf = (entries: Array<[string, number]>, limit: number, type: string) =>\n entries.slice(0, limit).map(([name, views]) => ({ source: name, type, views, share: share(views, all) }))\n const rows = [\n ...rowsOf(referrers, AI_TRAFFIC_TOP_REFERRERS, 'Referring site'),\n ...rowsOf(sources, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign source'),\n ...rowsOf(campaigns, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign'),\n ]\n return {\n ok: true,\n table: {\n title: 'Traffic sources',\n source,\n period: periodOf(request),\n columns: [\n { key: 'source', label: 'Source', kind: 'text' },\n { key: 'type', label: 'Kind', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of all views', kind: 'percent' },\n ],\n rows,\n omitted:\n Math.max(0, referrers.length - AI_TRAFFIC_TOP_REFERRERS) +\n Math.max(0, sources.length - AI_TRAFFIC_TOP_CAMPAIGNS) +\n Math.max(0, campaigns.length - AI_TRAFFIC_TOP_CAMPAIGNS),\n notes: ['A visit with no referring site, such as a typed address, is in no row.'],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.daily',\n label: 'Traffic by day',\n description: 'Page views and visitors on each day of the window, oldest first, for spotting a day that stood out.',\n windows: AI_TRAFFIC_DAILY_WINDOWS,\n },\n async (db, request) => {\n const ids = [...dayIds(request.now, request.days).current].reverse()\n const days = await readTrafficDays(db, request.hostId, ids)\n return {\n ok: true,\n table: {\n title: 'Traffic by day',\n source,\n period: periodOf(request),\n columns: [\n { key: 'day', label: 'Day', kind: 'date' },\n { key: 'views', label: 'Page views', kind: 'count' },\n { key: 'visitors', label: 'Visitors', kind: 'count' },\n ],\n rows: days.map((day, index) => ({ day: ids[index], views: day.total, visitors: day.visitors })),\n omitted: 0,\n notes: [visitorsNote],\n },\n }\n },\n firestore,\n null,\n ),\n ]\n}\n\n// ── Forms ─────────────────────────────────────────────────────────────────\n\nexport function formsFigureReader(firestore: AiFigureFirestore): PluginFigureReader {\n return siteReader(\n {\n id: 'forms.performance',\n label: 'Forms',\n description:\n 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: FORMS_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const { current } = pluginFigureWindows(request.now, request.days)\n const from = formPeriodKey(current.startMs)\n const to = formPeriodKey(current.endMs - 1)\n const snapshot = await db\n .collection('hosts')\n .doc(request.hostId)\n .collection('forms')\n .limit(AI_FORMS_READ_LIMIT)\n .get()\n const forms = snapshot.docs\n .map((doc) => ({ doc, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => !data['deletedAt'])\n .map(({ doc, data }) => {\n const totals = formStatsTotals(data['stats'] as FormStats | undefined, { from, to })\n // Every submission to a form is counted on it, so a form with none\n // recorded in the window had none: 0, not a blank the answer has to\n // hedge over (AGL-3834). Leads are counted only on a form that\n // routes them, so one that does reads 0 the same way, and a form\n // that never has keeps a blank, which the table's note explains.\n const submissions = totals.submissions ?? 0\n const routesLeads = (data['routing'] as { lead?: unknown } | undefined)?.lead === true\n return {\n form: str(data['displayName']) || doc.id,\n views: totals.views,\n submissions,\n completion: totals.views ? share(submissions, totals.views) : null,\n leads: totals.leads ?? (routesLeads ? 0 : null),\n }\n })\n .sort((a, b) => b.submissions - a.submissions || a.form.localeCompare(b.form))\n const monthStart = from ? `${from}-01` : current.from\n return {\n ok: true,\n table: {\n title: 'Forms',\n source: { label: 'Forms', path: 'forms' },\n period: {\n from: monthStart,\n to: current.to,\n days: Math.round((Date.parse(current.to) - Date.parse(monthStart)) / 86_400_000) + 1,\n },\n columns: [\n { key: 'form', label: 'Form', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'submissions', label: 'Submissions', kind: 'count' },\n { key: 'completion', label: 'Views that became a submission', kind: 'percent' },\n { key: 'leads', label: 'Leads', kind: 'count' },\n ],\n rows: forms.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, forms.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n 'Form figures are kept by calendar month, so they cover whole months from the first day of the month the window starts in.',\n 'Views are counted in the visitor’s browser and submissions on the server, so a rate can pass 100%.',\n 'A blank Leads figure means the form does not add its submissions to Leads.',\n ],\n },\n }\n },\n firestore,\n 'forms',\n )\n}\n\n/** Every reader this plugin registers, for the platform's own records. */\nexport function aiFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n return [...trafficFigureReaders(firestore), formsFigureReader(firestore)]\n}\n\n/**\n * Registers them from the console surface, the one surface that runs insight\n * jobs. By a call, never by an import (AGL-3025).\n */\nexport function registerAiFigureReaders(firestore: AiFigureFirestore): void {\n for (const reader of aiFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: AI_PLUGIN_ID })\n }\n}\n"],"names":["formPeriodKey","formStatsTotals","PLUGIN_FIGURE_MAX_ROWS","pluginFigureChange","pluginFigureDay","pluginFigureWindows","registerPluginFigureReader","AI_PLUGIN_ID","FORMS_WEEKLY_SUMMARY","SOURCES_WEEKLY_SUMMARY","TOP_PAGES_WEEKLY_SUMMARY","TRAFFIC_WEEKLY_SUMMARY","AI_TRAFFIC_WINDOWS","AI_TRAFFIC_DAILY_WINDOWS","AI_TRAFFIC_TOP_PAGES","AI_TRAFFIC_TOP_REFERRERS","AI_TRAFFIC_TOP_CAMPAIGNS","AI_FORMS_READ_LIMIT","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","periodOf","request","current","now","days","from","to","dayIds","previous","walk","startMs","endMs","ids","at","push","Date","counts","out","key","count","Object","entries","n","readTrafficDays","firestore","hostId","length","collection","doc","snapshots","getAll","map","id","snapshot","data","exists","utm","param","values","total","visitors","paths","referrers","sumMaps","pick","totals","Map","day","set","get","ranked","sort","a","b","localeCompare","siteReader","base","read","plugin","scope","windows","includes","trafficFigureReaders","source","label","path","visitorsNote","description","weeklySummary","db","Promise","all","reduce","sum","views","viewsBefore","perDay","perDayBefore","table","title","period","columns","kind","rows","figure","change","omitted","notes","before","slice","page","max","host","replace","sources","campaigns","rowsOf","limit","type","name","reverse","index","formsFigureReader","forms","docs","filter","submissions","routesLeads","lead","form","completion","leads","monthStart","parse","aiFigureReaders","registerAiFigureReaders","reader","pluginId"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginFigureWeeklySummary } from '@aglyn/aglyn/plugin-manager/plugin-figures';
18
+ /**
19
+ * How the traffic and forms readers read in the weekly summary (AGL-3867).
20
+ * Each answers `null` for a week it has nothing to say about, and for a table
21
+ * that is not the shape it was written for, so a changed reader drops out of
22
+ * the email rather than misreporting.
23
+ */
24
+ export declare const TRAFFIC_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
25
+ export declare const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
26
+ export declare const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
27
+ export declare const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { figureCount, figureHasColumn, figureRanked, figureRow, figureStat, weeklySummaryDay } from "@aglyn/aglyn/app-utils/weekly-summary";
18
+ /**
19
+ * How the traffic and forms readers read in the weekly summary (AGL-3867).
20
+ * Each answers `null` for a week it has nothing to say about, and for a table
21
+ * that is not the shape it was written for, so a changed reader drops out of
22
+ * the email rather than misreporting.
23
+ */ export const TRAFFIC_WEEKLY_SUMMARY = {
24
+ order: 10,
25
+ summarize: (table)=>{
26
+ const visitors = figureRow(table, 'figure', 'Visitors');
27
+ const views = figureRow(table, 'figure', 'Page views');
28
+ if (!figureCount(visitors == null ? void 0 : visitors['current']) && !figureCount(views == null ? void 0 : views['current'])) return null;
29
+ return {
30
+ title: 'Traffic',
31
+ stats: [
32
+ ...visitors ? [
33
+ figureStat(table, visitors, 'current', 'Visitors', {
34
+ change: 'change',
35
+ total: {
36
+ one: 'visitor',
37
+ many: 'visitors'
38
+ }
39
+ })
40
+ ] : [],
41
+ ...views ? [
42
+ figureStat(table, views, 'current', 'Page views', {
43
+ change: 'change'
44
+ })
45
+ ] : []
46
+ ]
47
+ };
48
+ }
49
+ };
50
+ export const TOP_PAGES_WEEKLY_SUMMARY = {
51
+ order: 20,
52
+ summarize: (table)=>{
53
+ const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views') : [];
54
+ return rows.length ? {
55
+ title: 'Top pages',
56
+ rows: rows.map((row)=>_extends({}, row, {
57
+ value: `${row.value} views`
58
+ }))
59
+ } : null;
60
+ }
61
+ };
62
+ export const SOURCES_WEEKLY_SUMMARY = {
63
+ order: 30,
64
+ summarize: (table)=>{
65
+ const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views') : [];
66
+ return rows.length ? {
67
+ title: 'Where visits came from',
68
+ rows: rows.map((row)=>_extends({}, row, {
69
+ value: `${row.value} views`
70
+ }))
71
+ } : null;
72
+ }
73
+ };
74
+ export const FORMS_WEEKLY_SUMMARY = {
75
+ order: 40,
76
+ summarize: (table)=>{
77
+ if (!figureHasColumn(table, 'submissions')) return null;
78
+ const total = table.rows.reduce((sum, row)=>sum + figureCount(row['submissions']), 0);
79
+ if (!total) return null;
80
+ // Form figures are kept by calendar month, so the block says so rather
81
+ // than calling a month-to-date count "last week".
82
+ const since = table.period ? weeklySummaryDay(table.period.from) : null;
83
+ return {
84
+ title: 'Forms',
85
+ stats: [
86
+ {
87
+ label: since ? `Submissions since ${since}` : 'Submissions',
88
+ value: total.toLocaleString('en-US')
89
+ }
90
+ ],
91
+ rows: figureRanked(table, 'form', 'submissions')
92
+ };
93
+ }
94
+ };
95
+
96
+ //# sourceMappingURL=ai-figure-summaries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-summaries.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginFigureWeeklySummary } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n figureCount,\n figureHasColumn,\n figureRanked,\n figureRow,\n figureStat,\n weeklySummaryDay,\n} from '@aglyn/aglyn/app-utils/weekly-summary'\n\n/**\n * How the traffic and forms readers read in the weekly summary (AGL-3867).\n * Each answers `null` for a week it has nothing to say about, and for a table\n * that is not the shape it was written for, so a changed reader drops out of\n * the email rather than misreporting.\n */\n\nexport const TRAFFIC_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 10,\n summarize: (table) => {\n const visitors = figureRow(table, 'figure', 'Visitors')\n const views = figureRow(table, 'figure', 'Page views')\n if (!figureCount(visitors?.['current']) && !figureCount(views?.['current'])) return null\n return {\n title: 'Traffic',\n stats: [\n ...(visitors\n ? [figureStat(table, visitors, 'current', 'Visitors', { change: 'change', total: { one: 'visitor', many: 'visitors' } })]\n : []),\n ...(views ? [figureStat(table, views, 'current', 'Page views', { change: 'change' })] : []),\n ],\n }\n },\n}\n\nexport const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 20,\n summarize: (table) => {\n const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views') : []\n return rows.length ? { title: 'Top pages', rows: rows.map((row) => ({ ...row, value: `${row.value} views` })) } : null\n },\n}\n\nexport const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 30,\n summarize: (table) => {\n const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views') : []\n return rows.length ? { title: 'Where visits came from', rows: rows.map((row) => ({ ...row, value: `${row.value} views` })) } : null\n },\n}\n\nexport const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 40,\n summarize: (table) => {\n if (!figureHasColumn(table, 'submissions')) return null\n const total = table.rows.reduce((sum, row) => sum + figureCount(row['submissions']), 0)\n if (!total) return null\n // Form figures are kept by calendar month, so the block says so rather\n // than calling a month-to-date count \"last week\".\n const since = table.period ? weeklySummaryDay(table.period.from) : null\n return {\n title: 'Forms',\n stats: [\n {\n label: since ? `Submissions since ${since}` : 'Submissions',\n value: total.toLocaleString('en-US'),\n },\n ],\n rows: figureRanked(table, 'form', 'submissions'),\n }\n },\n}\n"],"names":["figureCount","figureHasColumn","figureRanked","figureRow","figureStat","weeklySummaryDay","TRAFFIC_WEEKLY_SUMMARY","order","summarize","table","visitors","views","title","stats","change","total","one","many","TOP_PAGES_WEEKLY_SUMMARY","rows","length","map","row","value","SOURCES_WEEKLY_SUMMARY","FORMS_WEEKLY_SUMMARY","reduce","sum","since","period","from","label","toLocaleString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,WAAW,EACXC,eAAe,EACfC,YAAY,EACZC,SAAS,EACTC,UAAU,EACVC,gBAAgB,QACX,wCAAuC;AAE9C;;;;;CAKC,GAED,OAAO,MAAMC,yBAAoD;IAC/DC,OAAO;IACPC,WAAW,CAACC;QACV,MAAMC,WAAWP,UAAUM,OAAO,UAAU;QAC5C,MAAME,QAAQR,UAAUM,OAAO,UAAU;QACzC,IAAI,CAACT,YAAYU,4BAAAA,QAAU,CAAC,UAAU,KAAK,CAACV,YAAYW,yBAAAA,KAAO,CAAC,UAAU,GAAG,OAAO;QACpF,OAAO;YACLC,OAAO;YACPC,OAAO;mBACDH,WACA;oBAACN,WAAWK,OAAOC,UAAU,WAAW,YAAY;wBAAEI,QAAQ;wBAAUC,OAAO;4BAAEC,KAAK;4BAAWC,MAAM;wBAAW;oBAAE;iBAAG,GACvH,EAAE;mBACFN,QAAQ;oBAACP,WAAWK,OAAOE,OAAO,WAAW,cAAc;wBAAEG,QAAQ;oBAAS;iBAAG,GAAG,EAAE;aAC3F;QACH;IACF;AACF,EAAC;AAED,OAAO,MAAMI,2BAAsD;IACjEX,OAAO;IACPC,WAAW,CAACC;QACV,MAAMU,OAAOlB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,QAAQ,WAAW,EAAE;QACxF,OAAOU,KAAKC,MAAM,GAAG;YAAER,OAAO;YAAaO,MAAMA,KAAKE,GAAG,CAAC,CAACC,MAAS,aAAKA;oBAAKC,OAAO,GAAGD,IAAIC,KAAK,CAAC,MAAM,CAAC;;QAAK,IAAI;IACpH;AACF,EAAC;AAED,OAAO,MAAMC,yBAAoD;IAC/DjB,OAAO;IACPC,WAAW,CAACC;QACV,MAAMU,OAAOlB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,UAAU,WAAW,EAAE;QAC1F,OAAOU,KAAKC,MAAM,GAAG;YAAER,OAAO;YAA0BO,MAAMA,KAAKE,GAAG,CAAC,CAACC,MAAS,aAAKA;oBAAKC,OAAO,GAAGD,IAAIC,KAAK,CAAC,MAAM,CAAC;;QAAK,IAAI;IACjI;AACF,EAAC;AAED,OAAO,MAAME,uBAAkD;IAC7DlB,OAAO;IACPC,WAAW,CAACC;QACV,IAAI,CAACR,gBAAgBQ,OAAO,gBAAgB,OAAO;QACnD,MAAMM,QAAQN,MAAMU,IAAI,CAACO,MAAM,CAAC,CAACC,KAAKL,MAAQK,MAAM3B,YAAYsB,GAAG,CAAC,cAAc,GAAG;QACrF,IAAI,CAACP,OAAO,OAAO;QACnB,uEAAuE;QACvE,kDAAkD;QAClD,MAAMa,QAAQnB,MAAMoB,MAAM,GAAGxB,iBAAiBI,MAAMoB,MAAM,CAACC,IAAI,IAAI;QACnE,OAAO;YACLlB,OAAO;YACPC,OAAO;gBACL;oBACEkB,OAAOH,QAAQ,CAAC,kBAAkB,EAAEA,OAAO,GAAG;oBAC9CL,OAAOR,MAAMiB,cAAc,CAAC;gBAC9B;aACD;YACDb,MAAMjB,aAAaO,OAAO,QAAQ;QACpC;IACF;AACF,EAAC"}
@@ -164,6 +164,8 @@ export declare function aiInsightDigestEmailText(input: {
164
164
  workspaceName?: string;
165
165
  /** The AI credits the digest's job drew; 0 or absent says nothing. */
166
166
  creditsSpent?: number;
167
+ /** The week the figures cover, "Oct 5 – Oct 11"; absent says "this week". */
168
+ periodLabel?: string;
167
169
  }): string;
168
170
  /**
169
171
  * One call of the sweep: delivery for this week and the last on the first
@@ -18,7 +18,8 @@ import { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed, notificationMuted } fro
18
18
  import { hostRoleFor } from "@aglyn/aglyn/app-utils/organizations";
19
19
  import { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
20
  import { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides } from "@aglyn/aglyn/app-utils/release-flags";
21
- import { AI_INSIGHTS_COLLECTION, aiInsightIsoWeek } from "../model/ai-insight.js";
21
+ import { weeklySummaryPeriod } from "@aglyn/aglyn/app-utils/weekly-summary";
22
+ import { aiInsightDigestPeriod, AI_INSIGHTS_COLLECTION, aiInsightIsoWeek } from "../model/ai-insight.js";
22
23
  import { isAiOnForSite } from "../model/ai-site-switch.js";
23
24
  import { cancelAiJob, createAiJob, getAiJob } from "../jobs/ai-jobs.js";
24
25
  export const AI_INSIGHTS_DIGEST_CRON_ID = 'ai-insights-digest';
@@ -56,7 +57,7 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
56
57
  const drew = credits ? ` Writing it used ${credits.toLocaleString('en-US')} of ` + `${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.` : '';
57
58
  const forWhom = workspace || 'this workspace';
58
59
  return [
59
- `Here is what ${input.siteName}'s figures showed this week.${drew}`,
60
+ input.periodLabel ? `Here is what ${input.siteName}'s figures showed last week, ${input.periodLabel}.${drew}` : `Here is what ${input.siteName}'s figures showed this week.${drew}`,
60
61
  '',
61
62
  ...input.insights.map((insight, index)=>`${index + 1}. ${insight.text}`),
62
63
  '',
@@ -124,7 +125,8 @@ async function readUsers(firestore, uids) {
124
125
  brief: `Weekly insights for ${site.name}`,
125
126
  inputs: {
126
127
  surface: 'digest',
127
- week
128
+ week,
129
+ through: weeklySummaryPeriod(now).to
128
130
  },
129
131
  createdBy: site.creator
130
132
  }, now);
@@ -240,7 +242,8 @@ async function readUsers(firestore, uids) {
240
242
  analyticsUrl,
241
243
  settingsUrl: origin ? `${origin}/manage/notifications` : null,
242
244
  workspaceName: str(org['name']),
243
- creditsSpent: job.creditsSpent
245
+ creditsSpent: job.creditsSpent,
246
+ periodLabel: aiInsightDigestPeriod(job.inputs, new Date(Number(marker.createdAtMs) || now.getTime())).label
244
247
  }),
245
248
  fromName: branding.fromName,
246
249
  siteName: site.name,