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 +8 -2
- package/README.en.md +5 -3
- package/README.md +5 -3
- package/docs/cli.en.md +1 -1
- package/docs/cli.md +1 -1
- package/docs/codex-pet.md +2 -0
- package/docs/codex-skill.md +2 -0
- package/docs/data-schema.en.md +3 -1
- package/docs/data-schema.md +3 -1
- package/package.json +1 -1
- package/skills/ai-work-receipt/SKILL.md +2 -0
- package/src/cli.mjs +7 -1
- package/src/core/fact-buckets.mjs +5 -0
- package/src/core/presentation.mjs +6 -0
- package/src/core/selector.mjs +3 -1
- package/src/parsers/codex.mjs +35 -4
- package/src/renderers/html.mjs +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.7.
|
|
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
|
|
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>
|
|
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
|
|
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
|
|
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
package/docs/codex-skill.md
CHANGED
|
@@ -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。
|
package/docs/data-schema.en.md
CHANGED
|
@@ -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
|
|
package/docs/data-schema.md
CHANGED
|
@@ -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
|
@@ -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 {
|
package/src/core/selector.mjs
CHANGED
|
@@ -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
|
|
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. 选择一个具体会话");
|
package/src/parsers/codex.mjs
CHANGED
|
@@ -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
|
|
package/src/renderers/html.mjs
CHANGED
|
@@ -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>
|