codex-work-receipt 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## 0.7.1 - 2026-07-19
3
+ ## 0.7.2
4
+
5
+ - 修复同一 Codex 会话存在多份 append-only 日志修订时生成重复 canonical factId、导致小程序云端入库拒绝的问题。
6
+ - cwr2 manifest 输出前新增 factId 唯一性防线,避免无效二维码进入扫码链路。
7
+ - “最近 N 小时”在交互选择、终端输出和 HTML 中明确标记为私人滚动摘要,并提示其不参与 AI 供销社统计。
8
+
9
+ ## 0.7.1
4
10
 
5
11
  - 修正 HTML 长图导出:PNG 只保留完整小票和微信小程序码,不再包含数据二维码或分片数据码。
6
12
 
7
- ## 0.7.0 - 2026-07-19
13
+ ## 0.7.0
8
14
 
9
15
  - 新增 Codex 原生桌宠“票仔 · AI 小票工”,提供夜班绿票风格的核心状态与左右移动动画,并支持通过 `--install-pet`、`--uninstall-pet` 和 `--install-companion` 安装管理。
10
16
  - AI 打工小票 Skill 新增“票仔,开今天的票”等自然语言入口。
package/README.en.md CHANGED
@@ -54,6 +54,8 @@ Summarize the last 3 hours:
54
54
  npx codex-work-receipt@latest --hours 3 --lang en
55
55
  ```
56
56
 
57
+ “Last N hours” is a rolling summary for private history only. It does not participate in AI Work Cooperative deduplicated accounting. Use today, this week, the last seven days, or a specific session when you want accountable canonical facts.
58
+
57
59
  HTML and structured data are written to `./codex-work-receipt-output/` by default. The generated page can save a high-resolution PNG containing only the full receipt and WeChat mini-program code; data QR codes are excluded. Non-interactive options such as `--latest` and `--today` remain available; see the [CLI guide](docs/cli.en.md).
58
60
 
59
61
  ## Ask Codex directly
@@ -99,9 +101,9 @@ The desktop page generates one or more privacy-safe data QR codes. The companion
99
101
  ## Sponsor
100
102
 
101
103
  <p align="center">
102
- <a href="https://modelflare.dev/"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
103
- <a href="https://modelflare.dev/"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/">modelflare.dev</a><br>
104
- <sub>Helping independent developers access leading AI model APIs reliably and at a very low cost, so they can quickly build their own products with advanced model capabilities.</sub>
104
+ <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
105
+ <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL">modelflare.dev</a><br>
106
+ <sub>Affordable and reliable access to leading AI model APIs, helping independent developers validate ideas faster and turn them into real products.</sub>
105
107
  </p>
106
108
 
107
109
  ## License
package/README.md CHANGED
@@ -54,6 +54,8 @@ npx codex-work-receipt@latest --today
54
54
  npx codex-work-receipt@latest --hours 3
55
55
  ```
56
56
 
57
+ “最近 N 小时”是滚动摘要,只用于查看和保存私人历史,不参与 AI 供销社的去重统计。需要进入供销社时,请生成“今日 / 本周 / 近 7 日 / 指定会话”小票。
58
+
57
59
  网页和结构数据默认保存在 `./codex-work-receipt-output/`。生成的网页支持一键保存只包含完整小票和微信小程序码的高清长图,数据二维码不会进入图片。也可以继续使用 `--latest`、`--today` 等非交互参数,详见 [CLI 使用文档](docs/cli.md)。
58
60
 
59
61
  ## 直接跟 Codex 说
@@ -99,9 +101,9 @@ Codex 会选择统计范围和主题、执行命令并打开小票。详见 [Cod
99
101
  ## 赞助商
100
102
 
101
103
  <p align="center">
102
- <a href="https://modelflare.dev/"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
103
- <a href="https://modelflare.dev/"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/">modelflare.dev</a><br>
104
- <sub>让个人开发者也可以以很低的成本、稳定地接入前沿 AI 模型的 API,借助模型能力快速构建自己的产品。</sub>
104
+ <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><img src="docs/images/sponsors/modelflare-logo.png" alt="ModelFlare Logo" width="56"></a><br>
105
+ <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL"><strong>ModelFlare</strong></a> · <a href="https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL">modelflare.dev</a><br>
106
+ <sub>为个人开发者提供价格友好、稳定便捷的前沿 AI 模型 API 接入,帮助你更快完成原型验证,把想法变成真正可用的产品。</sub>
105
107
  </p>
106
108
 
107
109
  ## License
package/docs/cli.en.md CHANGED
@@ -39,7 +39,7 @@ Summarize the last 3 hours:
39
39
  npx codex-work-receipt@latest --hours 3 --lang en
40
40
  ```
41
41
 
42
- `--hours` accepts an integer from 1 to 168 and filters exact timestamps, including windows that cross midnight. Rolling-hour receipts use the compatible cwr1 summary protocol and do not create cwr2 session-day canonical facts.
42
+ `--hours` accepts an integer from 1 to 168 and filters exact timestamps, including windows that cross midnight. Rolling-hour receipts use the compatible cwr1 summary protocol, stay in private history, and do not participate in AI Work Cooperative accounting. Use today, this week, the last seven days, or a specific session for cwr2 session-day canonical facts.
43
43
 
44
44
  Last 7 calendar days, including today:
45
45
 
package/docs/cli.md CHANGED
@@ -39,7 +39,7 @@ npx codex-work-receipt@latest --today
39
39
  npx codex-work-receipt@latest --hours 3
40
40
  ```
41
41
 
42
- `--hours` 支持 1~168 的整数,并按精确时间戳筛选,可以跨越午夜。滚动小时小票使用兼容的 cwr1 摘要协议,不会写入 cwr2 的“会话 × 自然日”规范事实。
42
+ `--hours` 支持 1~168 的整数,并按精确时间戳筛选,可以跨越午夜。滚动小时小票使用兼容的 cwr1 摘要协议,不会写入 cwr2 的“会话 × 自然日”规范事实,只保存到私人历史,不参与 AI 供销社统计。需要进入供销社时,请生成“今日 / 本周 / 近 7 日 / 指定会话”小票。
43
43
 
44
44
  最近 7 个自然日(含今天):
45
45
 
package/docs/codex-pet.md CHANGED
@@ -30,6 +30,8 @@ npx codex-work-receipt@latest --install-companion
30
30
 
31
31
  “最近几个小时”默认表示最近 3 小时。
32
32
 
33
+ 最近 N 小时小票是私人滚动摘要,不进入 AI 供销社;需要参与供销社统计时,请让票仔生成今日、本周、近七日或指定会话小票。
34
+
33
35
  ## 只安装或卸载桌宠
34
36
 
35
37
  只安装票仔:
@@ -38,6 +38,8 @@ Skill 会安装到:
38
38
 
39
39
  Codex 会根据表达选择单次会话、最近 1~168 小时、今天、近七日或本周,以及语言和主题,然后执行命令并打开生成的网页。“最近几个小时”默认统计最近 3 小时。
40
40
 
41
+ “最近 N 小时”生成的是只用于私人历史的滚动摘要,不进入 AI 供销社;如果用户明确希望参与供销社统计,应改用今日、本周、近七日或指定会话范围。
42
+
41
43
  如果当前 Codex 任务仍在运行,而你希望立刻查看小票,建议新开一个本地 Codex 会话再说“开今天的票”;在当前运行会话中发送的消息可能根据 Follow-up behavior 被用于 Steer 或 Queue。
42
44
 
43
45
  也可以通过 `$ai-work-receipt` 显式调用这个 Skill。
@@ -32,7 +32,9 @@ cwr2p.<transferId>.<partIndex>.<partCount>.<totalChecksum>.<partChecksum>.<chunk
32
32
 
33
33
  The mini program validates the prefix and checksum before decompressing and parsing the payload. Future schema versions use the compact `v` field for compatibility.
34
34
 
35
- Compact field `o` explicitly carries `latest`, `session`, `last-hours`, `today`, `last-7-days`, or `this-week`. `d[3]` and `d[4]` carry date boundaries. Today, last-seven-days, and this-week receipts continue to use cwr2 canonical facts. `last-hours` is a rolling window, so it uses the compatible cwr1 summary payload to avoid colliding with session-day fact identities. The updated mini program can still import older QR codes.
35
+ Compact field `o` explicitly carries `latest`, `session`, `last-hours`, `today`, `last-7-days`, or `this-week`. `d[3]` and `d[4]` carry date boundaries. Today, last-seven-days, and this-week receipts continue to use cwr2 canonical facts. `last-hours` is a rolling window, so it uses the compatible cwr1 summary payload to avoid colliding with session-day fact identities; it stays in private history and does not participate in AI Work Cooperative accounting. The updated mini program can still import older QR codes.
36
+
37
+ When the same Codex session has multiple append-only log revisions, the generator keeps the more complete revision. Fact IDs in a cwr2 manifest must be unique; an identity collision stops QR generation instead of emitting an invalid payload.
36
38
 
37
39
  `presentation.compensation` contains playful AI work points, not real API cost. For compatibility with the current Chinese mini program, QR display copy remains Chinese while compact fields `l` and `r` carry the desktop locale and language-neutral role ID. English HTML and local JSON remain fully localized.
38
40
 
@@ -32,7 +32,9 @@ cwr2p.<transferId>.<partIndex>.<partCount>.<totalChecksum>.<partChecksum>.<chunk
32
32
 
33
33
  小程序应检查前缀和校验值,再解压并解析数据。未来结构升级通过 `v` 字段兼容。
34
34
 
35
- 精简字段 `o` 显式携带 `latest`、`session`、`last-hours`、`today`、`last-7-days` 或 `this-week`;`d[3]` 和 `d[4]` 携带日期边界。今日、近 7 日和本周继续使用 cwr2 canonical facts。`last-hours` 是滚动时间窗,为避免与“会话 × 自然日”事实发生身份冲突,使用兼容的 cwr1 摘要载荷。旧二维码仍可由新版小程序导入。
35
+ 精简字段 `o` 显式携带 `latest`、`session`、`last-hours`、`today`、`last-7-days` 或 `this-week`;`d[3]` 和 `d[4]` 携带日期边界。今日、近 7 日和本周继续使用 cwr2 canonical facts。`last-hours` 是滚动时间窗,为避免与“会话 × 自然日”事实发生身份冲突,使用兼容的 cwr1 摘要载荷,只进入私人历史、不参与 AI 供销社统计。旧二维码仍可由新版小程序导入。
36
+
37
+ 同一 Codex 会话出现多份 append-only 日志修订时,生成器只采用更完整的修订;cwr2 manifest 中的 factId 必须唯一,检测到身份冲突时不会输出可扫码二维码。
36
38
 
37
39
  `presentation.compensation` 是娱乐化 AI 工分,不代表真实 API 费用。为兼容当前中文小程序,二维码展示文案继续使用中文,并通过精简字段 `l` 和 `r` 携带桌面语言及工种语义 ID;英文 HTML 和本地 JSON 不受影响。
38
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-work-receipt",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "description": "Generate a privacy-first Codex work receipt from local session metadata.",
6
6
  "author": "a-bai-2026",
@@ -13,6 +13,7 @@ Use the published `codex-work-receipt` CLI as the only statistics and rendering
13
13
  - Use `--hours <N>` for “最近 N 小时”“过去 N 小时” or “近 N 小时”. Accept integers from 1 to 168.
14
14
  - Use `--hours 3` for an unspecified “最近几个小时”“过去几个小时” request.
15
15
  - Use `--hours 12` for “最近半天”.
16
+ - Treat `--hours` receipts as rolling summaries for private history only. They do not participate in AI Work Cooperative accounting; if the user asks for an accountable cooperative receipt, use today, this week, the last seven days, or a specific session instead.
16
17
  - Use `--today` for “今天”“今日”“今天全部工作” or a daily summary.
17
18
  - Use `--range last-7-days` for “最近七天”“近 7 日” or a rolling seven-calendar-day summary.
18
19
  - Use `--range this-week` for “本周”“这周” or a Monday-to-now summary.
@@ -39,6 +40,7 @@ Replace only the mode and optional flags according to the request.
39
40
 
40
41
  3. Let the CLI open the generated HTML unless the user requested `--no-open`.
41
42
  4. On success, report the HTML path and tell the user to scan the official mini-program code first, then the adjacent data QR code.
43
+ 5. When `--hours` was used, explicitly state that the receipt is a private rolling summary and will not enter AI Work Cooperative statistics.
42
44
 
43
45
  ## Privacy and failures
44
46
 
package/src/cli.mjs CHANGED
@@ -9,7 +9,7 @@ import QRCode from "qrcode";
9
9
  import { parseArgs, printHelp } from "./core/args.mjs";
10
10
  import { buildCanonicalFacts } from "./core/fact-buckets.mjs";
11
11
  import { collectMetrics } from "./core/metrics.mjs";
12
- import { getScopeLabel } from "./core/presentation.mjs";
12
+ import { getRollingSummaryNotice, getScopeLabel } from "./core/presentation.mjs";
13
13
  import { encodeReceiptPayloads } from "./core/qr-payload.mjs";
14
14
  import { outputSlugForRange, resolveRange } from "./core/range.mjs";
15
15
  import { buildReceiptRecord, persistReceiptRecord } from "./core/receipt-record.mjs";
@@ -145,6 +145,9 @@ async function main() {
145
145
  console.log(record.manifest
146
146
  ? `Data QR: ${qrPayloads.length} code(s) · ${record.manifest.fact_count} canonical fact(s) · schema v${record.schema_version}`
147
147
  : `Data QR: ${qrPayloads.length} code(s) · rolling summary · schema v${record.schema_version}`);
148
+ if (record.source.scope === "last-hours") {
149
+ console.log(`Note: ${getRollingSummaryNotice(options.locale, record.source.hours)}`);
150
+ }
148
151
  if (!miniProgramCodeDataUrl) console.log("Mini-program code: not configured; using the explicit placeholder");
149
152
  } else {
150
153
  console.log(`已生成网页:${outputFile}`);
@@ -155,6 +158,9 @@ async function main() {
155
158
  console.log(record.manifest
156
159
  ? `数据二维码:${qrPayloads.length} 个 · ${record.manifest.fact_count} 条规范事实 · schema v${record.schema_version}`
157
160
  : `数据二维码:${qrPayloads.length} 个 · 滚动摘要 · schema v${record.schema_version}`);
161
+ if (record.source.scope === "last-hours") {
162
+ console.log(`提示:${getRollingSummaryNotice(options.locale, record.source.hours)}`);
163
+ }
158
164
  if (!miniProgramCodeDataUrl) console.log("小程序码:尚未配置,页面使用明确占位符");
159
165
  }
160
166
  if (options.open) openFile(outputFile);
@@ -198,6 +198,11 @@ export function buildCanonicalFacts(sessions, range, options = {}) {
198
198
  left.local_date.localeCompare(right.local_date) || left.fact_id.localeCompare(right.fact_id)
199
199
  ));
200
200
 
201
+ const uniqueFactIds = new Set(facts.map((fact) => fact.fact_id));
202
+ if (uniqueFactIds.size !== facts.length) {
203
+ throw new Error("检测到重复的规范事实,请更新生成器后重新生成小票");
204
+ }
205
+
201
206
  return {
202
207
  facts,
203
208
  coverage: coverageForRange(range, sessions.scanMode || options.scanMode || "best_effort", observedAt),
@@ -147,6 +147,7 @@ const RECEIPT_COPY = {
147
147
  placeholderValue: "待接入",
148
148
  placeholderAria: "小程序码待接入",
149
149
  transferNote: "数据码只包含时间、轮次、Token和工具调用等统计,不包含Prompt、回复正文、代码、项目路径或文件名。",
150
+ rollingSummaryNotice: "最近 {hours} 小时属于滚动摘要,只保存到私人历史,不参与 AI 供销社统计。需要统计时请生成“今日 / 本周 / 近 7 日 / 指定会话”小票。",
150
151
  privacy: "结构数据同时保存在本机,未来可以批量导入小程序历史记录。",
151
152
  },
152
153
  en: {
@@ -221,6 +222,7 @@ const RECEIPT_COPY = {
221
222
  placeholderValue: "Pending",
222
223
  placeholderAria: "Mini-program code pending",
223
224
  transferNote: "The data code contains only statistics such as time, turns, Tokens, and tool calls. It does not contain prompts, responses, code, project paths, or file names.",
225
+ rollingSummaryNotice: "The last {hours} hours is a rolling summary for private history only. It does not participate in AI Work Cooperative accounting. Use today, this week, the last seven days, or a specific session for accountable facts.",
224
226
  privacy: "Structured data is also stored locally for future batch import into your mini-program history.",
225
227
  },
226
228
  };
@@ -273,6 +275,10 @@ export function getScopeLabel(scope, locale = DEFAULT_LOCALE, hours = null) {
273
275
  return String(template).replaceAll("{hours}", String(hours || 3));
274
276
  }
275
277
 
278
+ export function getRollingSummaryNotice(locale = DEFAULT_LOCALE, hours = null) {
279
+ return String(getReceiptCopy(locale).rollingSummaryNotice).replaceAll("{hours}", String(hours || 3));
280
+ }
281
+
276
282
  export function buildCompensation(scope, amount, locale = DEFAULT_LOCALE) {
277
283
  const copy = COMPENSATION_COPY[SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE];
278
284
  return {
@@ -37,7 +37,9 @@ export async function promptForRange({ locale, timezone, loadRecentSessions }) {
37
37
  try {
38
38
  console.log(isEnglish ? "\nChoose a receipt range:\n" : "\n请选择小票统计范围:\n");
39
39
  console.log(isEnglish ? "1. All activity today (recommended)" : "1. 今天全部活动(推荐)");
40
- console.log(isEnglish ? "2. Last 3 hours" : "2. 最近 3 小时");
40
+ console.log(isEnglish
41
+ ? "2. Last 3 hours (private rolling summary; not counted by the cooperative)"
42
+ : "2. 最近 3 小时(私人滚动摘要,不计入供销社)");
41
43
  console.log(isEnglish ? "3. Last 7 calendar days" : "3. 最近 7 个自然日");
42
44
  console.log(isEnglish ? "4. This week (Monday to now)" : "4. 本周(周一至今)");
43
45
  console.log(isEnglish ? "5. Choose a specific session" : "5. 选择一个具体会话");
@@ -69,6 +69,38 @@ function calendarCandidates(files, startDate) {
69
69
  return files.filter((file) => file.modifiedAt >= approximateStart);
70
70
  }
71
71
 
72
+ function revisionDominates(candidate, current) {
73
+ const candidateRevision = candidate.sourceRevision || {};
74
+ const currentRevision = current.sourceRevision || {};
75
+ const candidateRows = Number(candidateRevision.row_count || 0);
76
+ const currentRows = Number(currentRevision.row_count || 0);
77
+ const candidateBytes = Number(candidateRevision.byte_length || 0);
78
+ const currentBytes = Number(currentRevision.byte_length || 0);
79
+ return candidateRows >= currentRows && candidateBytes >= currentBytes
80
+ && (candidateRows > currentRows || candidateBytes > currentBytes);
81
+ }
82
+
83
+ function preferSession(candidate, current) {
84
+ if (revisionDominates(candidate, current)) return true;
85
+ if (revisionDominates(current, candidate)) return false;
86
+
87
+ const candidateModifiedAt = Number(candidate.modifiedAt || candidate.endAt || 0);
88
+ const currentModifiedAt = Number(current.modifiedAt || current.endAt || 0);
89
+ if (candidateModifiedAt !== currentModifiedAt) return candidateModifiedAt > currentModifiedAt;
90
+
91
+ return String(candidate.filePath || "").localeCompare(String(current.filePath || "")) > 0;
92
+ }
93
+
94
+ export function deduplicateCodexSessions(sessions) {
95
+ const selected = new Map();
96
+ for (const session of sessions) {
97
+ const key = `${session.identityQuality || "metadata"}:${session.sessionId}`;
98
+ const current = selected.get(key);
99
+ if (!current || preferSession(session, current)) selected.set(key, session);
100
+ }
101
+ return [...selected.values()];
102
+ }
103
+
72
104
  export function loadCodexSessions(range) {
73
105
  const files = codexSessionFiles();
74
106
  let candidates = files;
@@ -85,8 +117,7 @@ export function loadCodexSessions(range) {
85
117
  scanMode = fullScan ? "full" : "best_effort";
86
118
  }
87
119
 
88
- const sessions = candidates
89
- .map(sessionFromFile)
120
+ const sessions = deduplicateCodexSessions(candidates.map(sessionFromFile))
90
121
  .sort((left, right) => right.endAt - left.endAt);
91
122
 
92
123
  if (range.scope === "latest") {
@@ -106,10 +137,10 @@ export function loadCodexSessions(range) {
106
137
  }
107
138
 
108
139
  export function listRecentCodexSessions(limit = 10) {
109
- const sessions = codexSessionFiles()
140
+ const sessions = deduplicateCodexSessions(codexSessionFiles()
110
141
  .slice(0, Math.max(40, limit * 3))
111
142
  .map(sessionFromFile)
112
- .filter((session) => session.rows.length)
143
+ .filter((session) => session.rows.length))
113
144
  .sort((left, right) => right.endAt - left.endAt)
114
145
  .slice(0, limit);
115
146
 
@@ -7,6 +7,7 @@ import {
7
7
  buildCompensation,
8
8
  DEFAULT_LOCALE,
9
9
  getReceiptCopy,
10
+ getRollingSummaryNotice,
10
11
  getScopeLabel,
11
12
  } from "../core/presentation.mjs";
12
13
 
@@ -71,6 +72,9 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
71
72
  const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope);
72
73
  const spansMultipleDates = rangeStartDate !== rangeEndDate;
73
74
  const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours);
75
+ const rollingSummaryNotice = record.source.scope === "last-hours"
76
+ ? getRollingSummaryNotice(locale, record.source.hours)
77
+ : null;
74
78
  const dateRange = spansMultipleDates
75
79
  ? `${formatDateKey(rangeStartDate, locale)}—${formatDateKey(rangeEndDate, locale)}`
76
80
  : formatDateKey(rangeEndDate, locale);
@@ -493,6 +497,13 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
493
497
  line-height: 1.65;
494
498
  text-align: center;
495
499
  }
500
+ .transfer-note--warning {
501
+ padding: 12px 14px;
502
+ border: 1px solid color-mix(in srgb, var(--ink) 28%, transparent);
503
+ background: color-mix(in srgb, var(--accent) 76%, var(--paper));
504
+ color: var(--ink);
505
+ font-weight: 700;
506
+ }
496
507
  .privacy {
497
508
  margin: 24px auto 0;
498
509
  max-width: 440px;
@@ -569,6 +580,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
569
580
  <p>${escapeHtml(copy.transferDescription)}</p>
570
581
  </header>
571
582
  ${transferVisual}
583
+ ${rollingSummaryNotice ? `<p class="transfer-note transfer-note--warning">${escapeHtml(rollingSummaryNotice)}</p>` : ""}
572
584
  <p class="transfer-note">${escapeHtml(copy.transferNote)}</p>
573
585
  </section>
574
586
  </div>