@iann29/rastro 0.9.0 → 0.10.0

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 (257) hide show
  1. package/README.md +9 -1
  2. package/agent/integration.md +23 -1
  3. package/agent/manifest.json +4 -4
  4. package/dist/client/federation.d.ts +114 -9
  5. package/dist/client/federation.d.ts.map +1 -1
  6. package/dist/client/federation.js +54 -2
  7. package/dist/client/federation.js.map +1 -1
  8. package/dist/client/index.d.ts +637 -8
  9. package/dist/client/index.d.ts.map +1 -1
  10. package/dist/client/index.js +385 -9
  11. package/dist/client/index.js.map +1 -1
  12. package/dist/component/_generated/api.d.ts +14 -0
  13. package/dist/component/_generated/api.d.ts.map +1 -1
  14. package/dist/component/_generated/api.js.map +1 -1
  15. package/dist/component/_generated/component.d.ts +277 -1
  16. package/dist/component/_generated/component.d.ts.map +1 -1
  17. package/dist/component/_generated/server.d.ts +4 -0
  18. package/dist/component/_generated/server.d.ts.map +1 -1
  19. package/dist/component/_generated/server.js.map +1 -1
  20. package/dist/component/constants.d.ts +5 -0
  21. package/dist/component/constants.d.ts.map +1 -1
  22. package/dist/component/constants.js +10 -0
  23. package/dist/component/constants.js.map +1 -1
  24. package/dist/component/convex.config.d.ts +4 -0
  25. package/dist/component/convex.config.js +9 -0
  26. package/dist/component/convex.config.js.map +1 -1
  27. package/dist/component/errors.d.ts +1 -1
  28. package/dist/component/errors.d.ts.map +1 -1
  29. package/dist/component/errors.js.map +1 -1
  30. package/dist/component/leads.d.ts +69 -3
  31. package/dist/component/leads.d.ts.map +1 -1
  32. package/dist/component/leads.js +419 -16
  33. package/dist/component/leads.js.map +1 -1
  34. package/dist/component/meta.d.ts +352 -0
  35. package/dist/component/meta.d.ts.map +1 -0
  36. package/dist/component/meta.js +1239 -0
  37. package/dist/component/meta.js.map +1 -0
  38. package/dist/component/metaCapi.d.ts +162 -0
  39. package/dist/component/metaCapi.d.ts.map +1 -0
  40. package/dist/component/metaCapi.js +754 -0
  41. package/dist/component/metaCapi.js.map +1 -0
  42. package/dist/component/metaFake.d.ts +5 -0
  43. package/dist/component/metaFake.d.ts.map +1 -0
  44. package/dist/component/metaFake.js +225 -0
  45. package/dist/component/metaFake.js.map +1 -0
  46. package/dist/component/metaGraph.d.ts +165 -0
  47. package/dist/component/metaGraph.d.ts.map +1 -0
  48. package/dist/component/metaGraph.js +507 -0
  49. package/dist/component/metaGraph.js.map +1 -0
  50. package/dist/component/metaSync.d.ts +186 -0
  51. package/dist/component/metaSync.d.ts.map +1 -0
  52. package/dist/component/metaSync.js +961 -0
  53. package/dist/component/metaSync.js.map +1 -0
  54. package/dist/component/pipeline.d.ts +98 -0
  55. package/dist/component/pipeline.d.ts.map +1 -0
  56. package/dist/component/pipeline.js +324 -0
  57. package/dist/component/pipeline.js.map +1 -0
  58. package/dist/component/schema.d.ts +404 -2
  59. package/dist/component/schema.d.ts.map +1 -1
  60. package/dist/component/schema.js +168 -2
  61. package/dist/component/schema.js.map +1 -1
  62. package/dist/component/secrets.d.ts +15 -0
  63. package/dist/component/secrets.d.ts.map +1 -0
  64. package/dist/component/secrets.js +67 -0
  65. package/dist/component/secrets.js.map +1 -0
  66. package/dist/component/validators.d.ts +1052 -15
  67. package/dist/component/validators.d.ts.map +1 -1
  68. package/dist/component/validators.js +307 -3
  69. package/dist/component/validators.js.map +1 -1
  70. package/dist/tracker/generated.d.ts +1 -1
  71. package/dist/tracker/generated.d.ts.map +1 -1
  72. package/dist/tracker/generated.js +1 -1
  73. package/dist/tracker/generated.js.map +1 -1
  74. package/dist/ui/campaigns/CampaignsView.d.ts +48 -0
  75. package/dist/ui/campaigns/CampaignsView.d.ts.map +1 -0
  76. package/dist/ui/campaigns/CampaignsView.js +47 -0
  77. package/dist/ui/campaigns/CampaignsView.js.map +1 -0
  78. package/dist/ui/campaigns/Conversions.d.ts +16 -0
  79. package/dist/ui/campaigns/Conversions.d.ts.map +1 -0
  80. package/dist/ui/campaigns/Conversions.js +40 -0
  81. package/dist/ui/campaigns/Conversions.js.map +1 -0
  82. package/dist/ui/campaigns/Drawer.d.ts +24 -0
  83. package/dist/ui/campaigns/Drawer.d.ts.map +1 -0
  84. package/dist/ui/campaigns/Drawer.js +120 -0
  85. package/dist/ui/campaigns/Drawer.js.map +1 -0
  86. package/dist/ui/campaigns/Icons.d.ts +35 -0
  87. package/dist/ui/campaigns/Icons.d.ts.map +1 -0
  88. package/dist/ui/campaigns/Icons.js +41 -0
  89. package/dist/ui/campaigns/Icons.js.map +1 -0
  90. package/dist/ui/campaigns/Summary.d.ts +21 -0
  91. package/dist/ui/campaigns/Summary.d.ts.map +1 -0
  92. package/dist/ui/campaigns/Summary.js +56 -0
  93. package/dist/ui/campaigns/Summary.js.map +1 -0
  94. package/dist/ui/campaigns/Thumbnail.d.ts +11 -0
  95. package/dist/ui/campaigns/Thumbnail.d.ts.map +1 -0
  96. package/dist/ui/campaigns/Thumbnail.js +23 -0
  97. package/dist/ui/campaigns/Thumbnail.js.map +1 -0
  98. package/dist/ui/campaigns/Tree.d.ts +12 -0
  99. package/dist/ui/campaigns/Tree.d.ts.map +1 -0
  100. package/dist/ui/campaigns/Tree.js +105 -0
  101. package/dist/ui/campaigns/Tree.js.map +1 -0
  102. package/dist/ui/campaigns/glyphs.d.ts +20 -0
  103. package/dist/ui/campaigns/glyphs.d.ts.map +1 -0
  104. package/dist/ui/campaigns/glyphs.js +19 -0
  105. package/dist/ui/campaigns/glyphs.js.map +1 -0
  106. package/dist/ui/campaigns/index.d.ts +4 -0
  107. package/dist/ui/campaigns/index.d.ts.map +1 -0
  108. package/dist/ui/campaigns/index.js +3 -0
  109. package/dist/ui/campaigns/index.js.map +1 -0
  110. package/dist/ui/campaigns/labels.d.ts +53 -0
  111. package/dist/ui/campaigns/labels.d.ts.map +1 -0
  112. package/dist/ui/campaigns/labels.js +169 -0
  113. package/dist/ui/campaigns/labels.js.map +1 -0
  114. package/dist/ui/campaigns/model.d.ts +99 -0
  115. package/dist/ui/campaigns/model.d.ts.map +1 -0
  116. package/dist/ui/campaigns/model.js +230 -0
  117. package/dist/ui/campaigns/model.js.map +1 -0
  118. package/dist/ui/campaigns/modes.d.ts +18 -0
  119. package/dist/ui/campaigns/modes.d.ts.map +1 -0
  120. package/dist/ui/campaigns/modes.js +7 -0
  121. package/dist/ui/campaigns/modes.js.map +1 -0
  122. package/dist/ui/index.d.ts +21 -0
  123. package/dist/ui/index.d.ts.map +1 -0
  124. package/dist/ui/index.js +19 -0
  125. package/dist/ui/index.js.map +1 -0
  126. package/dist/ui/integrations/AccountPicker.d.ts +26 -0
  127. package/dist/ui/integrations/AccountPicker.d.ts.map +1 -0
  128. package/dist/ui/integrations/AccountPicker.js +97 -0
  129. package/dist/ui/integrations/AccountPicker.js.map +1 -0
  130. package/dist/ui/integrations/ClientCards.d.ts +17 -0
  131. package/dist/ui/integrations/ClientCards.d.ts.map +1 -0
  132. package/dist/ui/integrations/ClientCards.js +38 -0
  133. package/dist/ui/integrations/ClientCards.js.map +1 -0
  134. package/dist/ui/integrations/ClientsTable.d.ts +36 -0
  135. package/dist/ui/integrations/ClientsTable.d.ts.map +1 -0
  136. package/dist/ui/integrations/ClientsTable.js +127 -0
  137. package/dist/ui/integrations/ClientsTable.js.map +1 -0
  138. package/dist/ui/integrations/Connect.d.ts +29 -0
  139. package/dist/ui/integrations/Connect.d.ts.map +1 -0
  140. package/dist/ui/integrations/Connect.js +49 -0
  141. package/dist/ui/integrations/Connect.js.map +1 -0
  142. package/dist/ui/integrations/Connection.d.ts +37 -0
  143. package/dist/ui/integrations/Connection.d.ts.map +1 -0
  144. package/dist/ui/integrations/Connection.js +94 -0
  145. package/dist/ui/integrations/Connection.js.map +1 -0
  146. package/dist/ui/integrations/Dialogs.d.ts +18 -0
  147. package/dist/ui/integrations/Dialogs.d.ts.map +1 -0
  148. package/dist/ui/integrations/Dialogs.js +25 -0
  149. package/dist/ui/integrations/Dialogs.js.map +1 -0
  150. package/dist/ui/integrations/EventsFeed.d.ts +15 -0
  151. package/dist/ui/integrations/EventsFeed.d.ts.map +1 -0
  152. package/dist/ui/integrations/EventsFeed.js +57 -0
  153. package/dist/ui/integrations/EventsFeed.js.map +1 -0
  154. package/dist/ui/integrations/EventsSheet.d.ts +28 -0
  155. package/dist/ui/integrations/EventsSheet.d.ts.map +1 -0
  156. package/dist/ui/integrations/EventsSheet.js +121 -0
  157. package/dist/ui/integrations/EventsSheet.js.map +1 -0
  158. package/dist/ui/integrations/Icons.d.ts +36 -0
  159. package/dist/ui/integrations/Icons.d.ts.map +1 -0
  160. package/dist/ui/integrations/Icons.js +38 -0
  161. package/dist/ui/integrations/Icons.js.map +1 -0
  162. package/dist/ui/integrations/IntegrationsView.d.ts +14 -0
  163. package/dist/ui/integrations/IntegrationsView.d.ts.map +1 -0
  164. package/dist/ui/integrations/IntegrationsView.js +113 -0
  165. package/dist/ui/integrations/IntegrationsView.js.map +1 -0
  166. package/dist/ui/integrations/Overlay.d.ts +18 -0
  167. package/dist/ui/integrations/Overlay.d.ts.map +1 -0
  168. package/dist/ui/integrations/Overlay.js +36 -0
  169. package/dist/ui/integrations/Overlay.js.map +1 -0
  170. package/dist/ui/integrations/RowMenu.d.ts +15 -0
  171. package/dist/ui/integrations/RowMenu.d.ts.map +1 -0
  172. package/dist/ui/integrations/RowMenu.js +47 -0
  173. package/dist/ui/integrations/RowMenu.js.map +1 -0
  174. package/dist/ui/integrations/UnlinkDialog.d.ts +18 -0
  175. package/dist/ui/integrations/UnlinkDialog.d.ts.map +1 -0
  176. package/dist/ui/integrations/UnlinkDialog.js +29 -0
  177. package/dist/ui/integrations/UnlinkDialog.js.map +1 -0
  178. package/dist/ui/integrations/format.d.ts +28 -0
  179. package/dist/ui/integrations/format.d.ts.map +1 -0
  180. package/dist/ui/integrations/format.js +101 -0
  181. package/dist/ui/integrations/format.js.map +1 -0
  182. package/dist/ui/integrations/index.d.ts +5 -0
  183. package/dist/ui/integrations/index.d.ts.map +1 -0
  184. package/dist/ui/integrations/index.js +3 -0
  185. package/dist/ui/integrations/index.js.map +1 -0
  186. package/dist/ui/integrations/model.d.ts +84 -0
  187. package/dist/ui/integrations/model.d.ts.map +1 -0
  188. package/dist/ui/integrations/model.js +131 -0
  189. package/dist/ui/integrations/model.js.map +1 -0
  190. package/dist/ui/integrations/overlay.d.ts +10 -0
  191. package/dist/ui/integrations/overlay.d.ts.map +1 -0
  192. package/dist/ui/integrations/overlay.js +77 -0
  193. package/dist/ui/integrations/overlay.js.map +1 -0
  194. package/dist/ui/integrations/parts.d.ts +36 -0
  195. package/dist/ui/integrations/parts.d.ts.map +1 -0
  196. package/dist/ui/integrations/parts.js +85 -0
  197. package/dist/ui/integrations/parts.js.map +1 -0
  198. package/dist/ui/integrations/types.d.ts +96 -0
  199. package/dist/ui/integrations/types.d.ts.map +1 -0
  200. package/dist/ui/integrations/types.js +2 -0
  201. package/dist/ui/integrations/types.js.map +1 -0
  202. package/dist/ui/metrics.d.ts +106 -0
  203. package/dist/ui/metrics.d.ts.map +1 -0
  204. package/dist/ui/metrics.js +166 -0
  205. package/dist/ui/metrics.js.map +1 -0
  206. package/dist/ui/portfolio/PortfolioView.d.ts +26 -0
  207. package/dist/ui/portfolio/PortfolioView.d.ts.map +1 -0
  208. package/dist/ui/portfolio/PortfolioView.js +216 -0
  209. package/dist/ui/portfolio/PortfolioView.js.map +1 -0
  210. package/dist/ui/portfolio/SpendChart.d.ts +10 -0
  211. package/dist/ui/portfolio/SpendChart.d.ts.map +1 -0
  212. package/dist/ui/portfolio/SpendChart.js +196 -0
  213. package/dist/ui/portfolio/SpendChart.js.map +1 -0
  214. package/dist/ui/portfolio/alerts.d.ts +74 -0
  215. package/dist/ui/portfolio/alerts.d.ts.map +1 -0
  216. package/dist/ui/portfolio/alerts.js +291 -0
  217. package/dist/ui/portfolio/alerts.js.map +1 -0
  218. package/dist/ui/portfolio/figures.d.ts +79 -0
  219. package/dist/ui/portfolio/figures.d.ts.map +1 -0
  220. package/dist/ui/portfolio/figures.js +103 -0
  221. package/dist/ui/portfolio/figures.js.map +1 -0
  222. package/dist/ui/portfolio/format.d.ts +8 -0
  223. package/dist/ui/portfolio/format.d.ts.map +1 -0
  224. package/dist/ui/portfolio/format.js +28 -0
  225. package/dist/ui/portfolio/format.js.map +1 -0
  226. package/dist/ui/sample.d.ts +474 -0
  227. package/dist/ui/sample.d.ts.map +1 -0
  228. package/dist/ui/sample.js +553 -0
  229. package/dist/ui/sample.js.map +1 -0
  230. package/dist/ui/seed.d.ts +188 -0
  231. package/dist/ui/seed.d.ts.map +1 -0
  232. package/dist/ui/seed.js +563 -0
  233. package/dist/ui/seed.js.map +1 -0
  234. package/dist/ui/ui.css +3356 -0
  235. package/docs/federation.md +46 -1
  236. package/docs/meta-ads.md +746 -0
  237. package/docs/upgrading.md +95 -0
  238. package/llms.txt +3 -0
  239. package/package.json +15 -3
  240. package/src/component/_generated/api.ts +14 -0
  241. package/src/component/_generated/component.ts +323 -1
  242. package/src/component/_generated/server.ts +4 -0
  243. package/src/component/constants.ts +10 -0
  244. package/src/component/convex.config.ts +9 -0
  245. package/src/component/errors.ts +5 -1
  246. package/src/component/leads.ts +517 -17
  247. package/src/component/meta.ts +1461 -0
  248. package/src/component/metaCapi.ts +921 -0
  249. package/src/component/metaFake.ts +277 -0
  250. package/src/component/metaGraph.ts +722 -0
  251. package/src/component/metaSync.ts +1164 -0
  252. package/src/component/pipeline.ts +392 -0
  253. package/src/component/schema.ts +197 -1
  254. package/src/component/secrets.ts +98 -0
  255. package/src/component/validators.ts +376 -3
  256. package/src/tracker/generated.ts +1 -1
  257. package/src/ui/seed.ts +755 -0
@@ -0,0 +1,291 @@
1
+ import { change, formatMoney, formatReturn, RETURN_BANDS, returnBand, rowMetrics, } from "../metrics.js";
2
+ import { portfolioFigures, spendState, } from "./figures.js";
3
+ import { compactMoney } from "./format.js";
4
+ /** The thresholds the rules use, exported so hosts and tests can read them. */
5
+ export const PORTFOLIO_RULES = Object.freeze({
6
+ /** Days in each window of the week-over-week comparison. */
7
+ weekDays: 7,
8
+ /**
9
+ * Days left out at the end of the series: a lead is counted on the day it
10
+ * arrived, so the newest week has not finished selling yet.
11
+ */
12
+ maturingDays: 7,
13
+ /** Sales each week needs before its cost per sale means anything. */
14
+ minWeeklySales: 3,
15
+ /** A cost per sale this much higher than the week before raises an alert. */
16
+ costPerSaleRise: 0.15,
17
+ /** "Few sales": at most this many in the period… */
18
+ fewSales: 5,
19
+ /** …with an average sale this many times the portfolio's. */
20
+ highTicketFactor: 3,
21
+ /** A top ad needs this many ads beside it to stand out… */
22
+ minAdsForTopAd: 3,
23
+ /** …and this many times its fair share (1 ÷ ads) of the client's sales. */
24
+ topAdShareFactor: 2,
25
+ /** A campaign that makes at least this share of the sales "pulls" them. */
26
+ leadingCampaignShare: 0.5,
27
+ });
28
+ const TONE_ORDER = {
29
+ bad: 0,
30
+ watch: 1,
31
+ good: 2,
32
+ };
33
+ /** A broken Meta sync outranks everything: every other number goes stale. */
34
+ const rank = (alert) => alert.kind === "metaSync" ? -1 : TONE_ORDER[alert.tone];
35
+ const sales = (count) => (count === 1 ? "1 venda" : `${count} vendas`);
36
+ /**
37
+ * The last week that finished selling against the week before it: with a
38
+ * series of N days, days N−14…N−8 against N−21…N−15.
39
+ */
40
+ function matureWeeks(daily) {
41
+ const { weekDays: days, maturingDays: skip } = PORTFOLIO_RULES;
42
+ const sorted = [...daily].sort((a, b) => a.dayStart - b.dayStart);
43
+ const end = sorted.length - skip;
44
+ if (end - days * 2 < 0)
45
+ return null;
46
+ const sum = (rows) => rows.reduce((acc, row) => ({
47
+ won: acc.won + row.won,
48
+ spendCents: acc.spendCents === null || row.spendCents === null
49
+ ? null
50
+ : acc.spendCents + row.spendCents,
51
+ }), { won: 0, spendCents: 0 });
52
+ return {
53
+ current: sum(sorted.slice(end - days, end)),
54
+ previous: sum(sorted.slice(end - days * 2, end - days)),
55
+ /** The UTC day the compared week starts, for "na semana de 14/09". */
56
+ weekStart: sorted[end - days].dayStart,
57
+ };
58
+ }
59
+ /** "14/09": a UTC day as the screens write it. */
60
+ const dayMonth = (dayStart) => new Date(dayStart).toLocaleDateString("pt-BR", {
61
+ day: "2-digit",
62
+ month: "2-digit",
63
+ timeZone: "UTC",
64
+ });
65
+ const costPerSale = (week) => rowMetrics({ ...week, leads: 0, qualified: 0, revenueCents: 0 })
66
+ .costPerSaleCents;
67
+ function costPerSaleAlert(client, daily, portfolio) {
68
+ if (!daily || client.spendCents === null)
69
+ return null;
70
+ const weeks = matureWeeks(daily);
71
+ if (!weeks)
72
+ return null;
73
+ const { current, previous, weekStart } = weeks;
74
+ if (current.won < PORTFOLIO_RULES.minWeeklySales ||
75
+ previous.won < PORTFOLIO_RULES.minWeeklySales) {
76
+ return null;
77
+ }
78
+ const now = costPerSale(current);
79
+ const before = costPerSale(previous);
80
+ const rise = change(now, before);
81
+ if (rise.relative === null || rise.relative < PORTFOLIO_RULES.costPerSaleRise)
82
+ return null;
83
+ const money = (cents) => formatMoney(cents, client.spendCurrency);
84
+ const cheapLeads = client.spendCurrency === portfolio.spendCurrency &&
85
+ client.metrics.costPerLeadCents !== null &&
86
+ portfolio.metrics.costPerLeadCents !== null &&
87
+ client.metrics.costPerLeadCents <= portfolio.metrics.costPerLeadCents;
88
+ return {
89
+ id: `costPerSaleUp:${client.site.siteId}`,
90
+ kind: "costPerSaleUp",
91
+ tone: "bad",
92
+ siteId: client.site.siteId,
93
+ siteName: client.site.name,
94
+ headline: `custo por venda subiu ${rise.label?.replace("+", "")} na semana de ${dayMonth(weekStart)}`,
95
+ detail: `Foi de ${money(before)} para ${money(now)}. ${cheapLeads
96
+ ? `Os leads seguem baratos (${money(client.metrics.costPerLeadCents)}), mas estão comprando menos.`
97
+ : "Vale ver quais anúncios gastaram mais sem vender."}`,
98
+ action: `Ver campanhas da ${client.site.name}`,
99
+ };
100
+ }
101
+ /** Few sales with a big ticket: one sale more or less swings the month. */
102
+ function isFewSalesHighTicket(client, portfolio) {
103
+ return (client.won > 0 &&
104
+ client.won <= PORTFOLIO_RULES.fewSales &&
105
+ client.ticketCents !== null &&
106
+ portfolio.ticketCents !== null &&
107
+ client.ticketCents >=
108
+ portfolio.ticketCents * PORTFOLIO_RULES.highTicketFactor);
109
+ }
110
+ function fewSalesAlert(client, portfolio) {
111
+ if (!isFewSalesHighTicket(client, portfolio))
112
+ return null;
113
+ const qualified = client.qualified === 1
114
+ ? "o 1 qualificado"
115
+ : `os ${client.qualified} qualificados`;
116
+ return {
117
+ id: `fewSalesHighTicket:${client.site.siteId}`,
118
+ kind: "fewSalesHighTicket",
119
+ tone: "watch",
120
+ siteId: client.site.siteId,
121
+ siteName: client.site.name,
122
+ headline: `${sales(client.won)}, mas ticket médio de ${compactMoney(client.ticketCents, portfolio.currency)}`,
123
+ detail: `Uma venda a mais ou a menos muda o mês inteiro. Vale olhar ${qualified}.`,
124
+ action: `Abrir campanhas da ${client.site.name}`,
125
+ };
126
+ }
127
+ /**
128
+ * The ad that sells far more than its share, among several, strictly ahead of
129
+ * the runner-up, at a strong return.
130
+ */
131
+ function topAd(client) {
132
+ const [best, second] = client.ads;
133
+ if (!best ||
134
+ (second && second.won >= best.won) ||
135
+ client.ads.length < PORTFOLIO_RULES.minAdsForTopAd ||
136
+ client.won === 0 ||
137
+ best.won / client.won < PORTFOLIO_RULES.topAdShareFactor / client.ads.length) {
138
+ return null;
139
+ }
140
+ const returnOnSpend = metricsInRevenueCurrency(client, {
141
+ leads: 0,
142
+ qualified: 0,
143
+ won: best.won,
144
+ revenueCents: best.revenueCents,
145
+ spendCents: best.spendCents,
146
+ });
147
+ if (returnBand(returnOnSpend) !== "strong")
148
+ return null;
149
+ return { ad: best, returnOnSpend };
150
+ }
151
+ /** An ad's return, or null when its account bills in another currency. */
152
+ function metricsInRevenueCurrency(client, row) {
153
+ return client.metrics.returnOnSpend === null && client.spendCents !== null
154
+ ? null
155
+ : rowMetrics(row).returnOnSpend;
156
+ }
157
+ function topAdAlert(client) {
158
+ const top = topAd(client);
159
+ if (!top)
160
+ return null;
161
+ return {
162
+ id: `topAd:${client.site.siteId}`,
163
+ kind: "topAd",
164
+ tone: "good",
165
+ siteId: client.site.siteId,
166
+ siteName: client.site.name,
167
+ headline: `criativo “${top.ad.name}” é o que mais vende`,
168
+ detail: `${top.ad.won} das ${client.won} vendas, com retorno de ${formatReturn(top.returnOnSpend)}. Dá pra pôr mais verba nele.`,
169
+ action: "Abrir o anúncio",
170
+ adId: top.ad.adId,
171
+ };
172
+ }
173
+ /** An ad account Meta stopped syncing: the client's numbers go stale. */
174
+ function metaSyncAlert(client) {
175
+ const broken = client.accounts.find((account) => account.syncState === "error" || account.lastError);
176
+ if (!broken)
177
+ return null;
178
+ return {
179
+ id: `metaSync:${client.site.siteId}`,
180
+ kind: "metaSync",
181
+ tone: "bad",
182
+ siteId: client.site.siteId,
183
+ siteName: client.site.name,
184
+ headline: `a Meta parou de sincronizar a conta “${broken.name}”`,
185
+ detail: `${broken.lastError ? `A Meta respondeu: ${broken.lastError}. ` : ""}O gasto deste cliente pode estar desatualizado até a conta ser conectada de novo.`,
186
+ action: `Ver campanhas da ${client.site.name}`,
187
+ };
188
+ }
189
+ /**
190
+ * The "Precisa de atenção" list: a broken Meta sync, then problems, what to
191
+ * watch and what to grow; within a tone, in the order of `sites`. Pass
192
+ * `limit` to cut it; the view shows the first few and says how many are left.
193
+ */
194
+ export function portfolioAlerts({ sites, report, dailyBySite, status, limit = Infinity, }) {
195
+ const portfolio = portfolioFigures({ sites, report, status });
196
+ const alerts = [];
197
+ for (const client of portfolio.clients) {
198
+ for (const alert of [
199
+ metaSyncAlert(client),
200
+ costPerSaleAlert(client, dailyBySite?.[client.site.siteId], portfolio),
201
+ fewSalesAlert(client, portfolio),
202
+ topAdAlert(client),
203
+ ]) {
204
+ if (alert)
205
+ alerts.push(alert);
206
+ }
207
+ }
208
+ return alerts
209
+ .map((alert, index) => ({ alert, index }))
210
+ .sort((a, b) => rank(a.alert) - rank(b.alert) || a.index - b.index)
211
+ .slice(0, limit)
212
+ .map(({ alert }) => alert);
213
+ }
214
+ /** The label a campaign goes by: "Coleção Verão · Conversas" → "Coleção Verão". */
215
+ const campaignLabel = (name) => name.split(" · ")[0]?.trim() || name;
216
+ /**
217
+ * One line for a client's card, the first rule that holds: no Meta, no sales,
218
+ * the portfolio's best return, few sales with a big ticket, cheap leads that
219
+ * buy little, a weak return, one campaign carrying the sales, or the band.
220
+ */
221
+ export function clientInsight(client, portfolio) {
222
+ if (client.spendCents === null) {
223
+ // A linked account without spend yet is syncing, not missing.
224
+ return spendState(client, portfolio) === "syncing"
225
+ ? { kind: "syncing", tone: "neutral", text: "Sincronizando com a Meta" }
226
+ : { kind: "noSpend", tone: "neutral", text: "Meta não conectada" };
227
+ }
228
+ if (client.won === 0) {
229
+ return {
230
+ kind: "noSales",
231
+ tone: "watch",
232
+ text: "Ainda sem vendas de anúncio",
233
+ };
234
+ }
235
+ const returnOnSpend = client.metrics.returnOnSpend;
236
+ const withReturn = portfolio.clients.filter((other) => other.metrics.returnOnSpend !== null);
237
+ if (returnOnSpend !== null &&
238
+ withReturn.length >= 2 &&
239
+ withReturn.every((other) => other === client || other.metrics.returnOnSpend < returnOnSpend)) {
240
+ return {
241
+ kind: "bestReturn",
242
+ tone: "good",
243
+ text: "Melhor retorno da carteira",
244
+ };
245
+ }
246
+ if (isFewSalesHighTicket(client, portfolio)) {
247
+ return {
248
+ kind: "fewSalesHighTicket",
249
+ tone: "watch",
250
+ text: "Poucas vendas, ticket alto",
251
+ };
252
+ }
253
+ const band = returnBand(returnOnSpend);
254
+ if (band === "weak") {
255
+ const cheap = client.spendCurrency === portfolio.spendCurrency &&
256
+ client.metrics.costPerLeadCents !== null &&
257
+ portfolio.metrics.costPerLeadCents !== null &&
258
+ client.metrics.costPerLeadCents < portfolio.metrics.costPerLeadCents &&
259
+ client.ticketCents !== null &&
260
+ portfolio.ticketCents !== null &&
261
+ client.ticketCents < portfolio.ticketCents;
262
+ return cheap
263
+ ? { kind: "cheapLeads", tone: "bad", text: "Lead barato, ticket baixo" }
264
+ : {
265
+ kind: "weakReturn",
266
+ tone: "bad",
267
+ text: `Retorno abaixo de ${RETURN_BANDS.ok}x`,
268
+ };
269
+ }
270
+ const [leading] = client.campaigns;
271
+ if (leading &&
272
+ client.campaigns.length >= 2 &&
273
+ leading.won / client.won >= PORTFOLIO_RULES.leadingCampaignShare) {
274
+ return {
275
+ kind: "leadingCampaign",
276
+ tone: "good",
277
+ text: `${campaignLabel(leading.name)} puxando as vendas`,
278
+ };
279
+ }
280
+ if (band === "strong") {
281
+ return { kind: "band", tone: "good", text: "Retorno forte" };
282
+ }
283
+ if (band === "ok")
284
+ return { kind: "band", tone: "watch", text: "Retorno ok" };
285
+ return {
286
+ kind: "sales",
287
+ tone: "neutral",
288
+ text: `${sales(client.won)} no período`,
289
+ };
290
+ }
291
+ //# sourceMappingURL=alerts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alerts.js","sourceRoot":"","sources":["../../../src/ui/portfolio/alerts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The agency view's numbers, derived once from a multi-site report: one row
3
+ * per client from `bySite[].paid` (what the ads bought) and its spend, the
4
+ * portfolio's paid funnel over the clients whose spend is known (what the
5
+ * totals, costs, return and averages use), and each client's ads and
6
+ * campaigns found through the ad rows' `siteId`.
7
+ */
8
+ import type { LeadsReport, MetaStatus } from "../../client/index.js";
9
+ import { type PaidFunnel, type RowMetrics } from "../metrics.js";
10
+ export interface PortfolioSite {
11
+ siteId: string;
12
+ name: string;
13
+ segment?: string | null;
14
+ }
15
+ export interface ClientAd {
16
+ adId: string;
17
+ name: string;
18
+ campaignName: string | null;
19
+ won: number;
20
+ revenueCents: number;
21
+ spendCents: number | null;
22
+ }
23
+ export interface ClientCampaign {
24
+ name: string;
25
+ won: number;
26
+ }
27
+ export interface ClientFigures {
28
+ site: PortfolioSite;
29
+ leads: number;
30
+ qualified: number;
31
+ won: number;
32
+ revenueCents: number;
33
+ /** Null when no linked ad account covers the client. */
34
+ spendCents: number | null;
35
+ /** The currency the client's ad account bills in. */
36
+ spendCurrency: string;
37
+ /** The account bills in another currency than the client sells in. */
38
+ currencyMismatch: boolean;
39
+ /**
40
+ * Cost per lead/sale in `spendCurrency`; the return is null when the
41
+ * account bills in another currency than the revenue.
42
+ */
43
+ metrics: RowMetrics;
44
+ /** Average sale value (revenue ÷ sales), in cents. */
45
+ ticketCents: number | null;
46
+ ads: ClientAd[];
47
+ campaigns: ClientCampaign[];
48
+ /** The client's linked ad accounts, from `meta.status`. */
49
+ accounts: MetaStatus[number]["adAccounts"];
50
+ }
51
+ export interface PortfolioFigures {
52
+ clients: ClientFigures[];
53
+ paid: PaidFunnel;
54
+ /** Costs and return of `paid`; null where currencies make them meaningless. */
55
+ metrics: RowMetrics;
56
+ /** Average sale over every client's paid sales, in `currency`. */
57
+ ticketCents: number | null;
58
+ /** Revenue currency (the sites'). */
59
+ currency: string;
60
+ /** The one currency the accounts bill in, or null when they differ. */
61
+ spendCurrency: string | null;
62
+ /** Some account bills in another currency than the revenue. */
63
+ currencyMismatch: boolean;
64
+ /** The ad tree has no spend (too many rows); the per-client spend holds. */
65
+ spendIncomplete: boolean;
66
+ }
67
+ export declare function portfolioFigures({ sites, report, status, }: {
68
+ sites: readonly PortfolioSite[];
69
+ report: LeadsReport;
70
+ status?: MetaStatus | undefined;
71
+ }): PortfolioFigures;
72
+ /**
73
+ * Why a client's spend is or is not in the totals: counted, still on the
74
+ * first Meta sync (a linked account, no spend yet), not connected, or billed
75
+ * in another currency than the one the totals sum.
76
+ */
77
+ export type SpendState = "counted" | "syncing" | "notConnected" | "otherCurrency";
78
+ export declare function spendState(client: ClientFigures, portfolio: Pick<PortfolioFigures, "paid">): SpendState;
79
+ //# sourceMappingURL=figures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figures.d.ts","sourceRoot":"","sources":["../../../src/ui/portfolio/figures.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,UAAU,EAEhB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAKD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,OAAO,EAAE,UAAU,CAAC;IACpB,sDAAsD;IACtD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,QAAQ,EAAE,CAAC;IAChB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,2DAA2D;IAC3D,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,IAAI,EAAE,UAAU,CAAC;IACjB,+EAA+E;IAC/E,OAAO,EAAE,UAAU,CAAC;IACpB,kEAAkE;IAClE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,+DAA+D;IAC/D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,4EAA4E;IAC5E,eAAe,EAAE,OAAO,CAAC;CAC1B;AA0CD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,MAAM,EACN,MAAM,GACP,EAAE;IACD,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IAChC,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC,GAAG,gBAAgB,CAkEnB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GACpB,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,eAAe,CAAC;AAE3D,wBAAgB,UAAU,CACxB,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,GACxC,UAAU,CAOZ"}
@@ -0,0 +1,103 @@
1
+ import { paidFunnel, rowMetrics, } from "../metrics.js";
2
+ const perSale = (revenueCents, won) => won > 0 ? Math.round(revenueCents / won) : null;
3
+ /** Spend-based figures; the return is dropped when currencies differ. */
4
+ function metricsOf(row, sameCurrency) {
5
+ const metrics = rowMetrics(row);
6
+ return sameCurrency ? metrics : { ...metrics, returnOnSpend: null };
7
+ }
8
+ function adsBySite(campaigns) {
9
+ const ads = new Map();
10
+ const byCampaign = new Map();
11
+ for (const campaign of campaigns) {
12
+ for (const adset of campaign.adsets) {
13
+ for (const ad of adset.ads) {
14
+ if (!ad.siteId)
15
+ continue;
16
+ const list = ads.get(ad.siteId) ?? [];
17
+ list.push({
18
+ adId: ad.adId,
19
+ name: ad.adName ?? ad.title ?? ad.adId,
20
+ campaignName: campaign.campaignName,
21
+ won: ad.won,
22
+ revenueCents: ad.revenueCents,
23
+ spendCents: ad.spendCents ?? null,
24
+ });
25
+ ads.set(ad.siteId, list);
26
+ if (campaign.campaignName) {
27
+ const sold = byCampaign.get(ad.siteId) ?? new Map();
28
+ sold.set(campaign.campaignName, (sold.get(campaign.campaignName) ?? 0) + ad.won);
29
+ byCampaign.set(ad.siteId, sold);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ return { ads, byCampaign };
35
+ }
36
+ export function portfolioFigures({ sites, report, status, }) {
37
+ const currency = report.currency ?? "BRL";
38
+ const accountsBySite = new Map((status ?? []).map((site) => [site.siteId, site.adAccounts]));
39
+ // Read as `SitePaidRow`: hosts from 0.10 say each site's spend currency.
40
+ const rows = new Map((report.bySite ?? []).map((row) => [
41
+ row.siteId,
42
+ row,
43
+ ]));
44
+ const { ads, byCampaign } = adsBySite(report.campaigns);
45
+ const clients = [];
46
+ const siteRows = [];
47
+ for (const site of sites) {
48
+ const row = rows.get(site.siteId);
49
+ if (!row)
50
+ continue;
51
+ const spendCents = row.spendCents ?? null;
52
+ const spendCurrency = row.spendCurrency ?? report.spendCurrency ?? currency;
53
+ const currencyMismatch = row.currencyMismatch ?? spendCurrency !== currency;
54
+ siteRows.push({ ...row, spendCurrency, currencyMismatch });
55
+ clients.push({
56
+ site,
57
+ ...row.paid,
58
+ spendCents,
59
+ spendCurrency,
60
+ currencyMismatch,
61
+ metrics: metricsOf({ ...row.paid, spendCents }, !currencyMismatch),
62
+ ticketCents: perSale(row.paid.revenueCents, row.paid.won),
63
+ ads: (ads.get(site.siteId) ?? []).sort((a, b) => b.won - a.won),
64
+ campaigns: [...(byCampaign.get(site.siteId) ?? new Map())]
65
+ .map(([name, won]) => ({ name, won }))
66
+ .sort((a, b) => b.won - a.won),
67
+ accounts: accountsBySite.get(site.siteId) ?? [],
68
+ });
69
+ }
70
+ const paid = paidFunnel({ ...report, bySite: siteRows });
71
+ const spent = clients.filter((client) => client.spendCents !== null);
72
+ // Spend in several currencies does not add up: no total, no cost, no return.
73
+ const mixed = new Set(spent.map((client) => client.spendCurrency)).size > 1 &&
74
+ paid.row.spendCents === null;
75
+ const spendCurrency = mixed
76
+ ? null
77
+ : (paid.spendCurrency ?? report.spendCurrency ?? currency);
78
+ const allPaid = clients.reduce((sum, client) => ({
79
+ won: sum.won + client.won,
80
+ revenueCents: sum.revenueCents + client.revenueCents,
81
+ }), { won: 0, revenueCents: 0 });
82
+ return {
83
+ clients,
84
+ paid,
85
+ metrics: metricsOf(paid.row, !mixed && !paid.currencyMismatch),
86
+ ticketCents: perSale(allPaid.revenueCents, allPaid.won),
87
+ currency,
88
+ spendCurrency,
89
+ currencyMismatch: mixed ||
90
+ paid.currencyMismatch ||
91
+ spent.some((client) => client.currencyMismatch),
92
+ spendIncomplete: report.spendIncomplete === true,
93
+ };
94
+ }
95
+ export function spendState(client, portfolio) {
96
+ if (client.spendCents === null) {
97
+ return client.accounts.length > 0 ? "syncing" : "notConnected";
98
+ }
99
+ return portfolio.paid.sitesWithoutSpend.includes(client.site.siteId)
100
+ ? "otherCurrency"
101
+ : "counted";
102
+ }
103
+ //# sourceMappingURL=figures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figures.js","sourceRoot":"","sources":["../../../src/ui/portfolio/figures.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /** Formatting only the agency view needs, on top of `../metrics.ts`. */
2
+ /** "R$ 13 mil", "R$ 2,5 mil": round money for tickets and chart axes. */
3
+ export declare function compactMoney(cents: number, currency?: string, locale?: string): string;
4
+ /** "Clínica Sorriso" → "CS", "Loja X" → "LX". */
5
+ export declare function initials(name: string): string;
6
+ /** A stable 0–5 hue slot per client, so its avatar keeps its color. */
7
+ export declare function hueSlot(key: string): number;
8
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/ui/portfolio/format.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE,yEAAyE;AACzE,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,SAAQ,EAChB,MAAM,SAAU,UAUjB;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,UAMpC;AAED,uEAAuE;AACvE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,UAIlC"}
@@ -0,0 +1,28 @@
1
+ /** Formatting only the agency view needs, on top of `../metrics.ts`. */
2
+ /** "R$ 13 mil", "R$ 2,5 mil": round money for tickets and chart axes. */
3
+ export function compactMoney(cents, currency = "BRL", locale = "pt-BR") {
4
+ return new Intl.NumberFormat(locale, {
5
+ style: "currency",
6
+ currency,
7
+ notation: "compact",
8
+ maximumFractionDigits: 1,
9
+ })
10
+ .format(cents / 100)
11
+ .replace(/[\u00a0\u202f]/g, " ");
12
+ }
13
+ /** "Clínica Sorriso" → "CS", "Loja X" → "LX". */
14
+ export function initials(name) {
15
+ const words = name.trim().split(/\s+/).filter(Boolean);
16
+ const letters = (words.length > 1 ? [words[0], words[1]] : [name.trim()])
17
+ .map((word) => (words.length > 1 ? word[0] : word.slice(0, 2)))
18
+ .join("");
19
+ return letters.toLocaleUpperCase("pt-BR");
20
+ }
21
+ /** A stable 0–5 hue slot per client, so its avatar keeps its color. */
22
+ export function hueSlot(key) {
23
+ let hash = 0;
24
+ for (const char of key)
25
+ hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
26
+ return hash % 6;
27
+ }
28
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../../src/ui/portfolio/format.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAC1B,KAAa,EACb,QAAQ,GAAG,KAAK,EAChB,MAAM,GAAG,OAAO;IAEhB,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE;QACnC,KAAK,EAAE,UAAU;QACjB,QAAQ;QACR,QAAQ,EAAE,SAAS;QACnB,qBAAqB,EAAE,CAAC;KACzB,CAAC;SACC,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC;SACnB,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;AACrC,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;SACtE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;SAChE,IAAI,CAAC,EAAE,CAAC,CAAC;IACZ,OAAO,OAAO,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,OAAO,CAAC,GAAW;IACjC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,GAAG;QAAE,IAAI,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtE,OAAO,IAAI,GAAG,CAAC,CAAC;AAClB,CAAC"}