ccc-notifier 0.3.0 → 0.5.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.
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ paths
4
+ } from "./chunk-5PH7PPD6.js";
5
+
6
+ // src/data-lock.ts
7
+ import { randomUUID } from "crypto";
8
+ import { hostname } from "os";
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ renameSync,
14
+ rmSync,
15
+ writeFileSync
16
+ } from "fs";
17
+ import { join } from "path";
18
+ var DATA_LOCK_LEASE_MS = 3e4;
19
+ var HEARTBEAT_MS = 5e3;
20
+ function ownerFile(dir) {
21
+ return join(dir, "owner.json");
22
+ }
23
+ function readOwner(dir) {
24
+ try {
25
+ const v = JSON.parse(readFileSync(ownerFile(dir), "utf8"));
26
+ if (typeof v.token !== "string" || typeof v.pid !== "number" || typeof v.hostname !== "string" || typeof v.acquiredAt !== "string" || typeof v.heartbeatAt !== "string") return null;
27
+ return v;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ function writeOwner(dir, owner) {
33
+ const tmp = join(dir, `owner.${owner.token}.${randomUUID()}.tmp`);
34
+ try {
35
+ writeFileSync(tmp, `${JSON.stringify(owner)}
36
+ `, "utf8");
37
+ renameSync(tmp, ownerFile(dir));
38
+ } finally {
39
+ rmSync(tmp, { force: true });
40
+ }
41
+ }
42
+ function quarantineOwned(dir, token, label) {
43
+ const before = readOwner(dir);
44
+ if (before?.token !== token) return false;
45
+ const quarantine = `${dir}.${label}-${token}-${randomUUID()}`;
46
+ try {
47
+ renameSync(dir, quarantine);
48
+ } catch {
49
+ return false;
50
+ }
51
+ const moved = readOwner(quarantine);
52
+ if (moved?.token !== token) {
53
+ return false;
54
+ }
55
+ rmSync(quarantine, { recursive: true, force: true });
56
+ return true;
57
+ }
58
+ function claimDir(fixed, label, now, metadataWriter = writeOwner) {
59
+ const token = randomUUID();
60
+ const staging = `${fixed}.${label}-${token}`;
61
+ const iso = now.toISOString();
62
+ const owner = { token, pid: process.pid, hostname: hostname(), acquiredAt: iso, heartbeatAt: iso };
63
+ try {
64
+ mkdirSync(staging);
65
+ metadataWriter(staging, owner);
66
+ renameSync(staging, fixed);
67
+ return { token, owner };
68
+ } catch {
69
+ rmSync(staging, { recursive: true, force: true });
70
+ return null;
71
+ }
72
+ }
73
+ function processDefinitelyDead(pid) {
74
+ if (!Number.isInteger(pid) || pid <= 0) return false;
75
+ try {
76
+ process.kill(pid, 0);
77
+ return false;
78
+ } catch (err) {
79
+ return err.code === "ESRCH";
80
+ }
81
+ }
82
+ function reclaimerGuardBlocks(now, leaseMs) {
83
+ const owner = readOwner(paths().dataReclaimDir);
84
+ if (owner === null) return true;
85
+ if (owner.hostname !== hostname()) return true;
86
+ const heartbeat = Date.parse(owner.heartbeatAt);
87
+ if (!Number.isFinite(heartbeat) || now.getTime() - heartbeat <= leaseMs) return true;
88
+ return !processDefinitelyDead(owner.pid);
89
+ }
90
+ function tryReclaim(now, leaseMs) {
91
+ const p = paths();
92
+ const guard = claimDir(p.dataReclaimDir, "claim", now);
93
+ if (guard === null) return false;
94
+ try {
95
+ const first = readOwner(p.dataLockDir);
96
+ if (first === null || first.hostname !== hostname()) return false;
97
+ const heartbeatMs = Date.parse(first.heartbeatAt);
98
+ if (!Number.isFinite(heartbeatMs) || now.getTime() - heartbeatMs <= leaseMs) return false;
99
+ if (!processDefinitelyDead(first.pid)) return false;
100
+ const second = readOwner(p.dataLockDir);
101
+ if (second === null || second.token !== first.token || second.heartbeatAt !== first.heartbeatAt) return false;
102
+ const orphan = `${p.dataLockDir}.orphan-${first.token}-${randomUUID()}`;
103
+ try {
104
+ renameSync(p.dataLockDir, orphan);
105
+ } catch {
106
+ return false;
107
+ }
108
+ const moved = readOwner(orphan);
109
+ if (moved?.token !== first.token || moved.heartbeatAt !== first.heartbeatAt) return false;
110
+ rmSync(orphan, { recursive: true, force: true });
111
+ return true;
112
+ } finally {
113
+ quarantineOwned(p.dataReclaimDir, guard.token, "released");
114
+ }
115
+ }
116
+ function acquireDataLock(opts = {}) {
117
+ const now = opts.now ?? /* @__PURE__ */ new Date();
118
+ const leaseMs = opts.leaseMs ?? DATA_LOCK_LEASE_MS;
119
+ const p = paths();
120
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
121
+ let claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
122
+ if (claim === null) {
123
+ if (!tryReclaim(now, leaseMs)) return null;
124
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
125
+ claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
126
+ if (claim === null) return null;
127
+ }
128
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) {
129
+ quarantineOwned(p.dataLockDir, claim.token, "yielded");
130
+ return null;
131
+ }
132
+ let released = false;
133
+ const heartbeat = () => {
134
+ if (released) return false;
135
+ const current = readOwner(p.dataLockDir);
136
+ if (current?.token !== claim.token) return false;
137
+ writeOwner(p.dataLockDir, { ...current, heartbeatAt: (/* @__PURE__ */ new Date()).toISOString() });
138
+ return readOwner(p.dataLockDir)?.token === claim.token;
139
+ };
140
+ const timer = setInterval(() => {
141
+ try {
142
+ heartbeat();
143
+ } catch {
144
+ }
145
+ }, HEARTBEAT_MS);
146
+ timer.unref();
147
+ return {
148
+ token: claim.token,
149
+ heartbeat,
150
+ release() {
151
+ if (released) return;
152
+ released = true;
153
+ clearInterval(timer);
154
+ quarantineOwned(p.dataLockDir, claim.token, "released");
155
+ }
156
+ };
157
+ }
158
+ async function waitForDataLock(timeoutMs, pollMs = 25) {
159
+ const envTimeout = Number.parseInt(process.env.CCCN_LOCK_TIMEOUT_MS ?? "", 10);
160
+ timeoutMs = timeoutMs ?? (Number.isFinite(envTimeout) && envTimeout >= 0 ? envTimeout : 5e3);
161
+ const deadline = Date.now() + timeoutMs;
162
+ do {
163
+ const lock = acquireDataLock();
164
+ if (lock !== null) return lock;
165
+ if (Date.now() >= deadline) return null;
166
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
167
+ } while (true);
168
+ }
169
+
170
+ export {
171
+ waitForDataLock
172
+ };
@@ -8,59 +8,107 @@ import {
8
8
  formatUSD,
9
9
  modelDisplayName
10
10
  } from "./chunk-J5QAYTFE.js";
11
+ import {
12
+ makeFullDashboardState,
13
+ writeFullDashboardStateAtomic
14
+ } from "./chunk-ENGUOLTD.js";
15
+ import {
16
+ waitForDataLock
17
+ } from "./chunk-O34L3NSI.js";
11
18
  import {
12
19
  paths,
13
20
  readConfig,
14
21
  readTurns
15
- } from "./chunk-ECADO26T.js";
22
+ } from "./chunk-5PH7PPD6.js";
16
23
 
17
24
  // src/dashboard.ts
18
25
  import { spawn, spawnSync } from "child_process";
26
+ import { randomUUID } from "crypto";
19
27
  import { createRequire } from "module";
20
- import { mkdirSync, writeFileSync } from "fs";
21
- import { basename, dirname, join } from "path";
28
+ import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
29
+ import { basename, dirname } from "path";
22
30
  var PROMPT_MAX = 1e4;
23
31
  var PROMPT_TRUNC_MARK = "\u2026(\u4EE5\u4E0B\u7565)";
32
+ function writeAtomic(file, content) {
33
+ mkdirSync(dirname(file), { recursive: true });
34
+ const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
35
+ try {
36
+ writeFileSync(tmp, content, "utf8");
37
+ renameSync(tmp, file);
38
+ } finally {
39
+ rmSync(tmp, { force: true });
40
+ }
41
+ }
42
+ function ensurePeerPlaceholder(variant) {
43
+ if (variant === "custom") return;
44
+ const p = paths();
45
+ const peer = variant === "recent" ? p.fullDashboardFile : p.recentDashboardFile;
46
+ if (existsSync(peer)) return;
47
+ const peerIsFull = variant === "recent";
48
+ const back = peerIsFull ? "report.html" : "report-all.html";
49
+ const command = peerIsFull ? "ccc-notifier dashboard --all" : "ccc-notifier dashboard";
50
+ const label = peerIsFull ? "\u5168\u5C65\u6B74\u7248" : "\u76F4\u8FD1\u7248";
51
+ const html = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="cccn-placeholder" content="true"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${label}\u306F\u672A\u751F\u6210\u3067\u3059</title></head><body><main><h1>${label}\u306F\u307E\u3060\u751F\u6210\u3055\u308C\u3066\u3044\u307E\u305B\u3093</h1><p><code>${command}</code> \u3092\u5B9F\u884C\u3059\u308B\u3068\u751F\u6210\u3067\u304D\u307E\u3059\u3002</p><p><a href="${back}">\u751F\u6210\u6E08\u307F\u306E\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3078\u623B\u308B</a></p></main></body></html>`;
52
+ writeAtomic(peer, html);
53
+ }
24
54
  function parseDays(value) {
25
- if (value === void 0) return null;
26
- const parsed = Number.parseInt(value, 10);
27
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
55
+ if (value === void 0 || !/^\d+$/.test(value)) throw new Error("--days \u306B\u306F\u6B63\u306E\u6574\u6570\u304C\u5FC5\u8981\u3067\u3059");
56
+ const parsed = Number(value);
57
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error("--days \u306B\u306F\u6B63\u306E\u6574\u6570\u304C\u5FC5\u8981\u3067\u3059");
58
+ return parsed;
28
59
  }
29
- function parseRefresh(value, fallback) {
30
- if (value === void 0) return fallback;
31
- const parsed = Number.parseInt(value, 10);
32
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
60
+ function parseRefresh(value) {
61
+ if (value === void 0 || value.startsWith("--") || !/^\d+$/.test(value)) {
62
+ throw new Error("--refresh \u306B\u306F0\u4EE5\u4E0A\u306E\u6574\u6570\u304C\u5FC5\u8981\u3067\u3059");
63
+ }
64
+ const parsed = Number(value);
65
+ if (!Number.isSafeInteger(parsed) || parsed < 0) throw new Error("--refresh \u306B\u306F0\u4EE5\u4E0A\u306E\u6574\u6570\u304C\u5FC5\u8981\u3067\u3059");
66
+ return parsed;
33
67
  }
34
68
  function parseArgs(argv) {
35
- let days = null;
69
+ let daysValue = null;
70
+ let daysSpecified = false;
71
+ let all = false;
36
72
  let open = true;
37
73
  let out = null;
38
- const cfgReloadSec = readConfig().dashboard.autoReloadSec;
74
+ const cfg = readConfig().dashboard;
75
+ const cfgReloadSec = cfg.autoReloadSec;
39
76
  let autoReloadSec = cfgReloadSec;
40
77
  for (let i = 0; i < argv.length; i++) {
41
78
  const arg = argv[i];
42
79
  if (arg === "--no-open") {
43
80
  open = false;
81
+ } else if (arg === "--all") {
82
+ all = true;
44
83
  } else if (arg === "--days") {
45
- days = parseDays(argv[i + 1]);
84
+ daysSpecified = true;
85
+ daysValue = parseDays(argv[i + 1]);
46
86
  i++;
47
87
  } else if (arg.startsWith("--days=")) {
48
- days = parseDays(arg.slice("--days=".length));
88
+ daysSpecified = true;
89
+ daysValue = parseDays(arg.slice("--days=".length));
49
90
  } else if (arg === "--out") {
50
91
  out = argv[i + 1] ?? null;
92
+ if (out === null || out.startsWith("--")) throw new Error("--out \u306B\u306F\u51FA\u529B\u30D1\u30B9\u304C\u5FC5\u8981\u3067\u3059");
51
93
  i++;
52
94
  } else if (arg.startsWith("--out=")) {
53
95
  out = arg.slice("--out=".length) || null;
96
+ if (out === null) throw new Error("--out \u306B\u306F\u51FA\u529B\u30D1\u30B9\u304C\u5FC5\u8981\u3067\u3059");
54
97
  } else if (arg === "--no-refresh") {
55
98
  autoReloadSec = 0;
56
99
  } else if (arg === "--refresh") {
57
- autoReloadSec = parseRefresh(argv[i + 1], cfgReloadSec);
100
+ autoReloadSec = parseRefresh(argv[i + 1]);
58
101
  i++;
59
102
  } else if (arg.startsWith("--refresh=")) {
60
- autoReloadSec = parseRefresh(arg.slice("--refresh=".length), cfgReloadSec);
103
+ autoReloadSec = parseRefresh(arg.slice("--refresh=".length));
104
+ } else {
105
+ throw new Error(`\u4E0D\u660E\u306A\u5F15\u6570\u3067\u3059 / unknown argument: ${arg}`);
61
106
  }
62
107
  }
63
- return { days, open, out, autoReloadSec };
108
+ if (all && daysSpecified) throw new Error("--all \u3068 --days \u306F\u540C\u6642\u306B\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093");
109
+ const scope = all || out !== null && !daysSpecified ? "all" : "recent";
110
+ const days = scope === "all" ? null : daysValue ?? cfg.days;
111
+ return { scope, days, open, out, autoReloadSec };
64
112
  }
65
113
  function readVersion() {
66
114
  try {
@@ -122,6 +170,13 @@ function truncatePrompt(raw) {
122
170
  if (raw.length <= PROMPT_MAX) return { text: raw, truncated: false };
123
171
  return { text: raw.slice(0, PROMPT_MAX) + PROMPT_TRUNC_MARK, truncated: true };
124
172
  }
173
+ function filterTurnsByDays(turns, days) {
174
+ const cutoff = Date.now() - days * 864e5;
175
+ return turns.filter((rec) => {
176
+ const ts = Date.parse(rec.ts);
177
+ return Number.isFinite(ts) && ts >= cutoff;
178
+ });
179
+ }
125
180
  function computeSlotMap(turns) {
126
181
  const agg = /* @__PURE__ */ new Map();
127
182
  for (const rec of turns) {
@@ -159,6 +214,7 @@ function buildTurnEmbed(rec, map) {
159
214
  const primary = rec.models[0] ?? "unknown";
160
215
  const extra = rec.models.length > 1 ? ` +${rec.models.length - 1}` : "";
161
216
  const saMark = rec.subagents ? " +SA" : "";
217
+ const codexActivityMark = rec.source === "codex" && rec.subagentActivity ? " +SA(\u672A\u96C6\u8A08)" : "";
162
218
  let sa = null;
163
219
  if (rec.subagents) {
164
220
  const saModels = Object.keys(rec.subagents.costByModel).map((m) => modelDisplayName(m));
@@ -176,7 +232,7 @@ function buildTurnEmbed(rec, map) {
176
232
  p: basename(rec.project) || rec.project || "(unknown)",
177
233
  pf: rec.project ?? "",
178
234
  br: rec.gitBranch,
179
- md: modelDisplayName(primary) + extra + saMark,
235
+ md: modelDisplayName(primary) + extra + saMark + codexActivityMark,
180
236
  mr: rec.models,
181
237
  ti: formatTokens(turnInputTokens(rec)),
182
238
  to: formatTokens(turnOutputTokens(rec)),
@@ -188,6 +244,14 @@ function buildTurnEmbed(rec, map) {
188
244
  sa
189
245
  };
190
246
  if (rec.source === "codex") out.sc = "codex";
247
+ if (rec.source === "codex" && rec.subagentActivity) {
248
+ out.ca = {
249
+ started: rec.subagentActivity.started,
250
+ stopped: rec.subagentActivity.stopped,
251
+ agentTypes: [...rec.subagentActivity.agentTypes],
252
+ usageStatus: rec.subagentActivity.usageStatus
253
+ };
254
+ }
191
255
  return out;
192
256
  }
193
257
  function computeKpis(turns) {
@@ -411,7 +475,7 @@ var APP_JS = `<script>
411
475
  return p[1] + '/' + p[2] + (gran === 'week' ? '\u9031' : '');
412
476
  }
413
477
  function periodText(key, gran){
414
- if(key === null) return '\u901A\u7B97 / All time';
478
+ if(key === null) return data.allPeriodLabel || '\u901A\u7B97 / All time';
415
479
  if(gran === 'month') return key + ' (\u6708)';
416
480
  if(gran === 'week') return key + ' \u306E\u9031';
417
481
  return key;
@@ -436,6 +500,12 @@ var APP_JS = `<script>
436
500
  var SEL = (storedSel === null) ? defaultSel(GRAN) : (storedSel === '__all__' ? null : storedSel);
437
501
  function setGran(g){ GRAN = g; ssSet('cccn-gran', g); }
438
502
  function setSel(v){ SEL = v; ssSet('cccn-sel', v === null ? '__all__' : v); }
503
+ // \u81EA\u52D5\u518D\u751F\u6210\u3067\u57CB\u3081\u8FBC\u307F\u7BC4\u56F2\u304C\u7E2E\u3093\u3060\u5F8C\u3082\u3001\u53E4\u3044\u9078\u629E\u30AD\u30FC\u3067\u7A7A\u8868\u793A\u306B\u3057\u306A\u3044\u3002
504
+ if(SEL !== null){
505
+ var initialBuckets = buildBuckets(GRAN), initialSelPresent = false;
506
+ for(var ib=0;ib<initialBuckets.length;ib++){ if(initialBuckets[ib].key === SEL){ initialSelPresent = true; break; } }
507
+ if(!initialSelPresent) setSel(null);
508
+ }
439
509
  // \u30BD\u30FC\u30B9\u5207\u66FF\u3002\u30D5\u30A3\u30EB\u30BF\u5F8C\u306B\u9078\u629E\u4E2D\u30D0\u30B1\u30C3\u30C8\u304C\u6D88\u3048\u305F\u3089\u901A\u7B97\u3078\u5012\u3059(\u7A7A\u8868\u793A\u306E\u8AA4\u89E3\u9632\u6B62\u30FBgran \u30C8\u30B0\u30EB\u3068\u540C\u3058\u6D41\u5100)\u3002
440
510
  function setSrc(s){
441
511
  SRC = s; ssSet('cccn-src', s);
@@ -715,10 +785,12 @@ var APP_JS = `<script>
715
785
  projEl.appendChild(table);
716
786
  }
717
787
 
718
- // ---- \u6708\u4E88\u7B97(\u9078\u629E\u4E2D\u306E\u6708\u306B\u9023\u52D5)----
788
+ // ---- \u6708\u4E88\u7B97(\u5168\u5C65\u6B74\u7248\u306F\u9078\u629E\u6708\u306B\u9023\u52D5\u3001\u671F\u9593\u9650\u5B9A\u7248\u306F\u5F53\u6708\u306B\u56FA\u5B9A)----
719
789
  var budgetEl = document.getElementById('cccn-budget');
720
790
  var BUDGET = +data.budget || 0;
721
791
  var BUDGET_RATE = +data.budgetRate || 0;
792
+ var BUDGET_FIXED = data.budgetFixed === true;
793
+ var BUDGET_MONTH = data.budgetMonth || null;
722
794
  function monthKeyOf(t){ var d = new Date(t); if(isNaN(d.getTime())) return null; return d.getFullYear() + '-' + pad(d.getMonth()+1); }
723
795
  // \u4E88\u7B97\u306F\u300C\u6708\u300D\u5358\u4F4D\u3002\u9078\u629E\u4E2D\u30D0\u30B1\u30C3\u30C8\u304C\u5C5E\u3059\u308B\u66A6\u6708\u3092\u5BFE\u8C61\u306B\u3059\u308B(\u901A\u7B97\u306E\u3068\u304D\u306F\u4ECA\u6708)\u3002
724
796
  function targetMonthKey(){
@@ -728,13 +800,16 @@ var APP_JS = `<script>
728
800
  }
729
801
  function renderBudget(){
730
802
  if(!budgetEl || !(BUDGET > 0)) return;
731
- var mk = targetMonthKey();
732
- var usd = 0, jpy = 0;
733
- for(var i=0;i<turns.length;i++){
734
- var tn = turns[i];
735
- if(monthKeyOf(tn.t) !== mk) continue;
736
- var bs = tn.bs || {}, fx = tn.fx || 0;
737
- for(var s in bs){ if(bs.hasOwnProperty(s)){ usd += bs[s]; jpy += bs[s]*fx; } }
803
+ var mk = BUDGET_FIXED ? monthKeyOf(Date.now()) : targetMonthKey();
804
+ var usd = BUDGET_FIXED && BUDGET_MONTH ? (+BUDGET_MONTH.usd || 0) : 0;
805
+ var jpy = BUDGET_FIXED && BUDGET_MONTH ? (+BUDGET_MONTH.jpy || 0) : 0;
806
+ if(!BUDGET_FIXED){
807
+ for(var i=0;i<turns.length;i++){
808
+ var tn = turns[i];
809
+ if(monthKeyOf(tn.t) !== mk) continue;
810
+ var bs = tn.bs || {}, fx = tn.fx || 0;
811
+ for(var s in bs){ if(bs.hasOwnProperty(s)){ usd += bs[s]; jpy += bs[s]*fx; } }
812
+ }
738
813
  }
739
814
  var pct = BUDGET > 0 ? (usd / BUDGET) * 100 : 0;
740
815
  var width = Math.max(0, Math.min(100, pct));
@@ -750,10 +825,13 @@ var APP_JS = `<script>
750
825
  sub.textContent = ' ' + mk + (mk === curMonth ? '(\u4ECA\u6708)' : '');
751
826
  h.appendChild(sub);
752
827
  budgetEl.appendChild(h);
753
- // \u4E88\u7B97\u30AB\u30FC\u30C9\u306F\u5E38\u306B\u5168\u30BD\u30FC\u30B9\u5408\u7B97(\u30BD\u30FC\u30B9\u30D5\u30A3\u30EB\u30BF\u975E\u9023\u52D5)\u3002Codex \u304C\u3042\u308B\u6642\u3060\u3051\u305D\u306E\u65E8\u3092\u660E\u8A18\u3059\u308B\u3002
754
- if(HAS_CODEX){
828
+ // \u671F\u9593\u9650\u5B9A\u7248\u306F\u57CB\u3081\u8FBC\u307E\u306A\u3044\u5168\u5C65\u6B74\u304B\u3089\u5F53\u6708\u3092\u96C6\u8A08\u3057\u305F\u56FA\u5B9A\u5024\u3002
829
+ // \u5168\u5C65\u6B74\u7248\u306E\u307F\u5F93\u6765\u3069\u304A\u308A\u9078\u629E\u6708\u3078\u9023\u52D5\u3059\u308B\u3002\u3044\u305A\u308C\u3082\u30BD\u30FC\u30B9\u30D5\u30A3\u30EB\u30BF\u975E\u9023\u52D5\u3002
830
+ if(BUDGET_FIXED || HAS_CODEX){
755
831
  var srcNote = document.createElement('p'); srcNote.className = 'note';
756
- srcNote.textContent = '\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources';
832
+ srcNote.textContent = BUDGET_FIXED
833
+ ? '\u4ECA\u6708\u30FB\u5168\u5C65\u6B74\u304B\u3089\u6B63\u78BA\u306B\u96C6\u8A08(\u5168\u30BD\u30FC\u30B9\u5408\u7B97) / exact current month from full history'
834
+ : '\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources';
757
835
  budgetEl.appendChild(srcNote);
758
836
  }
759
837
 
@@ -874,6 +952,12 @@ var APP_JS = `<script>
874
952
  saLine.textContent = '\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ' + (t.sa.usd||'') + '(' + (t.sa.jpy||'') + ')\xB7 ' + (t.sa.models||'') + ' \xB7 API\u30B3\u30FC\u30EB ' + (t.sa.apiCalls||0);
875
953
  dtd.appendChild(saLine);
876
954
  }
955
+ if(t.ca){
956
+ var caLine = document.createElement('div'); caLine.className = 'detail-meta';
957
+ caLine.textContent = 'Codex\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: \u5229\u7528\u3042\u308A\u30FB\u6599\u91D1\u672A\u96C6\u8A08 \xB7 \u958B\u59CB ' +
958
+ (t.ca.started||0) + ' \xB7 \u7D42\u4E86 ' + (t.ca.stopped||0) + ' \xB7 \u7A2E\u5225 ' + ((t.ca.agentTypes||[]).join(', ')||'unknown');
959
+ dtd.appendChild(caLine);
960
+ }
877
961
  var pre = document.createElement('div'); pre.className = 'detail-prompt';
878
962
  pre.textContent = full.length ? full : '(\u30D7\u30ED\u30F3\u30D7\u30C8\u306A\u3057)';
879
963
  dtd.appendChild(pre); dtr.appendChild(dtd);
@@ -972,25 +1056,32 @@ function renderLegend(slots) {
972
1056
  const items = slots.map((m) => `<span class="legend-item"><span class="swatch k${m.slot}"></span>${esc(m.name)}</span>`).join("");
973
1057
  return `<div class="legend">${items}</div>`;
974
1058
  }
975
- function budgetCard(budgetUSD, month, fallbackRate, hasCodex) {
1059
+ function budgetCard(budgetUSD, month, fallbackRate, hasCodex, fixedCurrentMonth) {
976
1060
  if (!(budgetUSD > 0)) return "";
977
1061
  const pct = month.usd / budgetUSD * 100;
978
1062
  const width = Math.max(0, Math.min(100, pct));
979
1063
  const level = pct >= 100 ? "over" : pct >= 70 ? "warn" : "ok";
980
1064
  const budgetJpy = budgetUSD * fallbackRate;
981
- return `<section class="card" id="cccn-budget"><h2>\u6708\u4E88\u7B97 / Monthly budget<span class="stat-sub"> \u4ECA\u6708(\u66A6\u6708)</span></h2>` + (hasCodex ? `<p class="note">\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources</p>` : "") + `<div class="budget-bar"><div class="budget-fill lvl-${level}" style="width:${width.toFixed(1)}%"></div></div><div class="budget-foot"><span>\u4ECA\u6708 <b>${esc(formatUSD(month.usd))}</b> / ${esc(formatUSD(budgetUSD))} <span class="muted">(${esc(formatJPY(month.jpy))} / ${esc(formatJPY(budgetJpy))})</span></span><span class="budget-pct lvl-${level}">${pct.toFixed(1)}% used</span></div></section>`;
1065
+ return `<section class="card" id="cccn-budget"><h2>\u6708\u4E88\u7B97 / Monthly budget<span class="stat-sub"> \u4ECA\u6708(\u66A6\u6708)</span></h2>` + (fixedCurrentMonth ? `<p class="note">\u4ECA\u6708\u30FB\u5168\u5C65\u6B74\u304B\u3089\u6B63\u78BA\u306B\u96C6\u8A08(\u5168\u30BD\u30FC\u30B9\u5408\u7B97) / exact current month from full history</p>` : hasCodex ? `<p class="note">\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources</p>` : "") + `<div class="budget-bar"><div class="budget-fill lvl-${level}" style="width:${width.toFixed(1)}%"></div></div><div class="budget-foot"><span>\u4ECA\u6708 <b>${esc(formatUSD(month.usd))}</b> / ${esc(formatUSD(budgetUSD))} <span class="muted">(${esc(formatJPY(month.jpy))} / ${esc(formatJPY(budgetJpy))})</span></span><span class="budget-pct lvl-${level}">${pct.toFixed(1)}% used</span></div></section>`;
982
1066
  }
983
1067
  function escapeJsonForScript(json) {
984
1068
  return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
985
1069
  }
986
- function renderDashboard(turns, opts) {
1070
+ function renderDashboard(turns, opts, fullHistory) {
987
1071
  const version = readVersion();
988
- const generatedAt = fmtLocalDateTime((/* @__PURE__ */ new Date()).toISOString());
1072
+ const generatedAtIso = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1073
+ const generatedAt = fmtLocalDateTime(generatedAtIso);
989
1074
  const period = opts.days === null ? "\u5168\u671F\u9593" : `\u76F4\u8FD1 ${opts.days} \u65E5\u9593`;
990
- const map = computeSlotMap(turns);
1075
+ const limited = opts.days !== null;
1076
+ const variant = opts.variant ?? "custom";
1077
+ const totalLabel = limited ? "\u5BFE\u8C61\u671F\u9593\u5408\u8A08" : "\u901A\u7B97";
1078
+ const totalLabelEn = limited ? "Embedded period total" : "Total";
1079
+ const totalSub = limited ? "\u57CB\u3081\u8FBC\u307F\u5BFE\u8C61\u671F\u9593" : "\u5168\u671F\u9593";
1080
+ const map = fullHistory?.slotMap ?? computeSlotMap(turns);
991
1081
  const kpi = computeKpis(turns);
992
1082
  const cfg = readConfig();
993
1083
  const budgetUSD = cfg.monthlyBudgetUSD;
1084
+ const budgetMonth = limited ? fullHistory?.currentMonth ?? kpi.month : kpi.month;
994
1085
  const turnsEmbed = turns.map((rec) => buildTurnEmbed(rec, map));
995
1086
  const anyTruncated = turnsEmbed.some((t) => t.tr);
996
1087
  const anySub = turns.some((r) => r.subagents);
@@ -1018,26 +1109,32 @@ function renderDashboard(turns, opts) {
1018
1109
  slots: map.slots,
1019
1110
  turns: turnsEmbed,
1020
1111
  budget: budgetUSD,
1021
- budgetRate: cfg.fx.fallbackRate
1112
+ budgetRate: cfg.fx.fallbackRate,
1113
+ budgetMonth,
1114
+ budgetFixed: limited,
1115
+ allPeriodLabel: `${totalLabel} / ${totalLabelEn}`,
1116
+ variant
1022
1117
  };
1023
1118
  const dataJson = escapeJsonForScript(JSON.stringify(embed));
1024
1119
  const reloadSec = Number.isFinite(opts.autoReloadSec) && opts.autoReloadSec > 0 ? Math.floor(opts.autoReloadSec) : 0;
1025
1120
  const refreshMeta = reloadSec > 0 ? `<meta http-equiv="refresh" content="${reloadSec}">` : "";
1026
1121
  const updateTrigger = anyCodex ? "Claude Code / Codex \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642" : "Claude Code \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642";
1027
- const autoUpdateFoot = reloadSec > 0 ? `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u81EA\u52D5\u66F4\u65B0(\u6700\u65B0\u5316\u306F ${updateTrigger})</div>` : "";
1122
+ const autoUpdateFoot = reloadSec > 0 ? variant === "full" ? `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u30D5\u30A1\u30A4\u30EB\u3092\u518D\u8AAD\u8FBC(\u5168\u5C65\u6B74\u7248\u306E\u751F\u6210\u306F\u30ED\u30FC\u30AB\u30EB\u65E5\u3054\u3068\u3001\u307E\u305F\u306F\u624B\u52D5 dashboard --all)</div>` : `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u81EA\u52D5\u66F4\u65B0(\u6700\u65B0\u5316\u306F ${updateTrigger})</div>` : "";
1123
+ const variantNav = variant === "recent" ? `<div class="sub"><strong>\u76F4\u8FD1 ${opts.days} \u65E5\u7248 / Recent</strong> \xB7 ${opts.peerAvailable ? `<a href="report-all.html">\u5168\u5C65\u6B74\u7248\u3078 / Full history</a>` : `<span aria-disabled="true">\u5168\u5C65\u6B74\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard --all \u3067\u751F\u6210\uFF09 / Full history not generated</span>`}</div>` : variant === "full" ? `<div class="sub"><strong>\u5168\u5C65\u6B74\u7248 / Full history</strong> \xB7 ${opts.peerAvailable ? `<a href="report.html">\u76F4\u8FD1\u7248\u3078 / Recent</a>` : `<span aria-disabled="true">\u76F4\u8FD1\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard \u3067\u751F\u6210\uFF09 / Recent not generated</span>`}</div><div class="sub muted">\u6700\u7D42\u751F\u6210 ${esc(generatedAt)}\u3002\u30ED\u30FC\u30AB\u30EB\u65E5\u306E\u6700\u521D\u306E\u6B63\u5E38\u306A\u30BF\u30FC\u30F3\u6642\u3001\u307E\u305F\u306F\u624B\u52D5\u306E dashboard --all \u3067\u66F4\u65B0\u3055\u308C\u307E\u3059\u3002</div>` : "";
1028
1124
  const head = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark">` + refreshMeta + `<title>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</title>` + STYLE + `</head><body><div class="wrap">`;
1029
1125
  const foot = autoUpdateFoot + `<div class="foot">ccc-notifier v${esc(version)} \xB7 \u30C7\u30FC\u30BF\u306F\u30ED\u30FC\u30AB\u30EB\u306E\u307F / all data stays local</div></div><div id="cccn-tip" class="cccn-tip" hidden></div><script id="cccn-data" type="application/json">${dataJson}</script>` + APP_JS + `</body></html>`;
1126
+ const budgetSection = budgetCard(budgetUSD, budgetMonth, cfg.fx.fallbackRate, anyCodex, limited);
1030
1127
  if (turns.length === 0) {
1031
- const header2 = `<div class="head"><div><h1>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</h1><div class="sub">${esc(period)} \xB7 \u751F\u6210 ${esc(generatedAt)}</div></div></div>`;
1032
- const empty = `<div class="empty"><h2>\u307E\u3060\u5C65\u6B74\u304C\u3042\u308A\u307E\u305B\u3093</h2><p>Claude Code \u3067\u30D7\u30ED\u30F3\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u3068\u3001\u3053\u3053\u306B\u30B3\u30B9\u30C8\u5C65\u6B74\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002</p><p class="empty-hint">No history yet \u2014 run a prompt in Claude Code and it will appear here.</p></div>`;
1033
- return head + header2 + empty + foot;
1128
+ const outsideEmbeddedPeriod = limited && (fullHistory?.hasTurns ?? false);
1129
+ const header2 = `<div class="head"><div><h1>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</h1>` + variantNav + `<div class="sub">${esc(period)} \xB7 \u751F\u6210 ${esc(generatedAt)}</div></div></div>`;
1130
+ const empty = `<div class="empty">` + (outsideEmbeddedPeriod ? `<h2>\u5BFE\u8C61\u671F\u9593\u306B\u5C65\u6B74\u304C\u3042\u308A\u307E\u305B\u3093</h2><p>\u5C65\u6B74\u306F\u3042\u308A\u307E\u3059\u304C\u3001\u76F4\u8FD1 ${opts.days} \u65E5\u9593\u306B\u306F\u8A18\u9332\u304C\u3042\u308A\u307E\u305B\u3093\u3002</p><p class="empty-hint">No history in the embedded period.</p>` : `<h2>\u307E\u3060\u5C65\u6B74\u304C\u3042\u308A\u307E\u305B\u3093</h2><p>Claude Code \u3067\u30D7\u30ED\u30F3\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u3068\u3001\u3053\u3053\u306B\u30B3\u30B9\u30C8\u5C65\u6B74\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002</p><p class="empty-hint">No history yet \u2014 run a prompt in Claude Code and it will appear here.</p>`) + `</div>`;
1131
+ return head + header2 + budgetSection + empty + foot;
1034
1132
  }
1035
- const header = `<div class="head"><div><h1>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</h1><div class="sub">${esc(period)}<span class="muted"> \xB7 ${esc(rangeText)}</span></div><div class="sub muted">\u751F\u6210 ${esc(generatedAt)} / generated</div></div><div class="hero"><div class="hero-label">\u901A\u7B97 / Total</div><div class="hero-value">${esc(formatUSD(kpi.all.usd))}</div><div class="hero-meta">${esc(formatJPY(kpi.all.jpy))} \xB7 ${kpi.all.turns} \u30BF\u30FC\u30F3</div>` + (anySub ? `<div class="hero-meta">\u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8 ${esc(formatUSD(subUsd))}(${esc(formatJPY(subJpy))})</div>` : "") + `</div></div>`;
1036
- const kpiSection = `<div class="kpi">` + statCard("\u4ECA\u65E5", "Today", kpi.today) + statCard("\u4ECA\u9031", "\u76F4\u8FD17\u65E5", kpi.week) + statCard("\u4ECA\u6708", "\u66A6\u6708", kpi.month) + statCard("\u901A\u7B97", "\u5168\u671F\u9593", kpi.all) + `</div>`;
1037
- const budgetSection = budgetCard(budgetUSD, kpi.month, cfg.fx.fallbackRate, anyCodex);
1133
+ const header = `<div class="head"><div><h1>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</h1>` + variantNav + `<div class="sub">${esc(period)}<span class="muted"> \xB7 ${esc(rangeText)}</span></div><div class="sub muted">\u751F\u6210 ${esc(generatedAt)} / generated</div></div><div class="hero"><div class="hero-label">${esc(totalLabel)} / ${esc(totalLabelEn)}</div><div class="hero-value">${esc(formatUSD(kpi.all.usd))}</div><div class="hero-meta">${esc(formatJPY(kpi.all.jpy))} \xB7 ${kpi.all.turns} \u30BF\u30FC\u30F3</div>` + (anySub ? `<div class="hero-meta">\u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8 ${esc(formatUSD(subUsd))}(${esc(formatJPY(subJpy))})</div>` : "") + `</div></div>`;
1134
+ const kpiSection = `<div class="kpi">` + statCard("\u4ECA\u65E5", "Today", kpi.today) + statCard("\u4ECA\u9031", "\u76F4\u8FD17\u65E5", kpi.week) + statCard("\u4ECA\u6708", "\u66A6\u6708", kpi.month) + statCard(totalLabel, totalSub, kpi.all) + `</div>`;
1038
1135
  const srcToggle = anyCodex ? `<div class="seg-toggle" id="cccn-src-toggle"><button type="button" data-src="all">\u5168\u4F53</button><button type="button" data-src="claude">Claude</button><button type="button" data-src="codex">Codex</button></div>` : "";
1039
- const chartSection = `<section class="card"><h2>\u30B3\u30B9\u30C8\u63A8\u79FB / Cost over time</h2><p class="note">\u7C92\u5EA6\u3092 \u65E5 / \u9031 / \u6708 \u3067\u5207\u308A\u66FF\u3048\u3089\u308C\u307E\u3059(\u6A2A\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u904E\u53BB\u307E\u3067)\u3002\u68D2\u3092\u30AF\u30EA\u30C3\u30AF\u3059\u308B\u3068\u305D\u306E\u671F\u9593\u304C\u9078\u629E\u3055\u308C\u3001\u4E0B\u306E\u30E2\u30C7\u30EB\u5225\u30FB\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5225\u30FB\u5C65\u6B74\u304C\u9023\u52D5\u3057\u307E\u3059\u3002\u300C\u901A\u7B97\u300D\u3067\u5168\u671F\u9593\u306B\u623B\u308A\u307E\u3059\u3002\u30E2\u30C7\u30EB\u5225\u306B\u8272\u5206\u3051\u3002</p><div class="toolbar-row"><div class="seg-toggle"><button type="button" data-gran="day">\u65E5</button><button type="button" data-gran="week">\u9031</button><button type="button" data-gran="month">\u6708</button></div><button type="button" id="cccn-all" class="btn">\u901A\u7B97</button>` + srcToggle + `<span id="cccn-sel-label" class="sel-label"></span></div>` + renderLegend(map.slots) + `<div class="chart-scroll"><div id="cccn-chart"></div></div></section>`;
1040
- const breakdownSection = `<div class="grid2"><section class="card"><h2>\u30E2\u30C7\u30EB\u5225\u5185\u8A33 / By model</h2><p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593(\u65E2\u5B9A\u306F\u901A\u7B97)\u306E\u30B3\u30B9\u30C8\u964D\u9806\u3002\u8907\u6570\u30E2\u30C7\u30EB\u306E\u30BF\u30FC\u30F3\u306F\u5404\u30E2\u30C7\u30EB\u306B1\u305A\u3064\u8A08\u4E0A\u3002</p><div class="table-wrap" id="cccn-bymodel"></div></section><section class="card"><h2>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5225 / By project</h2><p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8(\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA basename)\u5358\u4F4D\u30FB\u30B3\u30B9\u30C8\u964D\u9806\u3002</p><div class="table-wrap" id="cccn-byproject"></div></section></div>`;
1136
+ const chartSection = `<section class="card"><h2>\u30B3\u30B9\u30C8\u63A8\u79FB / Cost over time</h2><p class="note">\u7C92\u5EA6\u3092 \u65E5 / \u9031 / \u6708 \u3067\u5207\u308A\u66FF\u3048\u3089\u308C\u307E\u3059(\u6A2A\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u904E\u53BB\u307E\u3067)\u3002\u68D2\u3092\u30AF\u30EA\u30C3\u30AF\u3059\u308B\u3068\u305D\u306E\u671F\u9593\u304C\u9078\u629E\u3055\u308C\u3001\u4E0B\u306E\u30E2\u30C7\u30EB\u5225\u30FB\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5225\u30FB\u5C65\u6B74\u304C\u9023\u52D5\u3057\u307E\u3059\u3002\u300C${esc(totalLabel)}\u300D\u3067${esc(totalSub)}\u306B\u623B\u308A\u307E\u3059\u3002\u30E2\u30C7\u30EB\u5225\u306B\u8272\u5206\u3051\u3002</p><div class="toolbar-row"><div class="seg-toggle"><button type="button" data-gran="day">\u65E5</button><button type="button" data-gran="week">\u9031</button><button type="button" data-gran="month">\u6708</button></div><button type="button" id="cccn-all" class="btn">${esc(totalLabel)}</button>` + srcToggle + `<span id="cccn-sel-label" class="sel-label"></span></div>` + renderLegend(map.slots) + `<div class="chart-scroll"><div id="cccn-chart"></div></div></section>`;
1137
+ const breakdownSection = `<div class="grid2"><section class="card"><h2>\u30E2\u30C7\u30EB\u5225\u5185\u8A33 / By model</h2><p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593(\u65E2\u5B9A\u306F${esc(totalLabel)})\u306E\u30B3\u30B9\u30C8\u964D\u9806\u3002\u8907\u6570\u30E2\u30C7\u30EB\u306E\u30BF\u30FC\u30F3\u306F\u5404\u30E2\u30C7\u30EB\u306B1\u305A\u3064\u8A08\u4E0A\u3002</p><div class="table-wrap" id="cccn-bymodel"></div></section><section class="card"><h2>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5225 / By project</h2><p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8(\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA basename)\u5358\u4F4D\u30FB\u30B3\u30B9\u30C8\u964D\u9806\u3002</p><div class="table-wrap" id="cccn-byproject"></div></section></div>`;
1041
1138
  const capNote = "\u4EF6\u6570\u304C\u591A\u3044\u5834\u5408\u306F\u65B0\u3057\u3044\u9806\u306B\u5148\u982D200\u4EF6\u306E\u307F\u63CF\u753B\u3057\u307E\u3059(\u691C\u7D22\u3084\u3001\u4E0A\u306E\u30B0\u30E9\u30D5\u3067\u671F\u9593\u3092\u9078\u3076\u3068\u7D5E\u308A\u8FBC\u3081\u307E\u3059)\u3002";
1042
1139
  const truncNote = anyTruncated ? `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u30D7\u30ED\u30F3\u30D7\u30C8\u306F1\u4EF6\u3042\u305F\u308A\u6700\u5927 ${PROMPT_MAX.toLocaleString("en-US")} \u5B57\u307E\u3067(\u8D85\u904E\u5206\u306F\u300C${esc(PROMPT_TRUNC_MARK)}\u300D)\u3002\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u5168\u6587\u5C55\u958B\u3002</p>` : `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092\u5C55\u958B\u3057\u307E\u3059\u3002</p>`;
1043
1140
  const historySection = `<section class="card"><h2>\u30BF\u30FC\u30F3\u5C65\u6B74 / History</h2>` + truncNote + `<div class="toolbar"><input id="turn-search" class="search" type="search" placeholder="\u30D7\u30ED\u30F3\u30D7\u30C8\u30FB\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30FB\u30E2\u30C7\u30EB\u3067\u691C\u7D22 / search\u2026" autocomplete="off"><span class="count"><span id="turn-count">0</span></span></div><div class="table-wrap"><table class="turns"><thead><tr><th>\u6642\u523B</th><th>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8</th><th>\u30E2\u30C7\u30EB</th><th class="c-num">in</th><th class="c-num">out</th><th class="c-num">$</th><th class="c-num">\xA5</th><th>\u30D7\u30ED\u30F3\u30D7\u30C8</th></tr></thead><tbody id="turn-body"></tbody></table></div></section>`;
@@ -1079,27 +1176,59 @@ function openInBrowser(path) {
1079
1176
  }
1080
1177
  function writeDashboardHtml(opts) {
1081
1178
  const days = opts.days ?? null;
1082
- const turns = readTurns(days ?? void 0);
1179
+ const variant = opts.variant ?? (basename(opts.outPath) === "report.html" ? "recent" : basename(opts.outPath) === "report-all.html" ? "full" : "custom");
1180
+ ensurePeerPlaceholder(variant);
1181
+ const allTurns = opts.allTurns ?? readTurns();
1182
+ const turns = days === null ? allTurns : filterTurnsByDays(allTurns, days);
1183
+ const fullHistory = days === null ? void 0 : {
1184
+ currentMonth: computeKpis(allTurns).month,
1185
+ slotMap: computeSlotMap(allTurns),
1186
+ hasTurns: allTurns.length > 0
1187
+ };
1083
1188
  const html = renderDashboard(turns, {
1189
+ scope: days === null ? "all" : "recent",
1084
1190
  days,
1085
1191
  open: false,
1086
1192
  out: opts.outPath,
1087
- autoReloadSec: opts.autoReloadSec
1088
- });
1089
- mkdirSync(dirname(opts.outPath), { recursive: true });
1090
- writeFileSync(opts.outPath, html, "utf8");
1193
+ autoReloadSec: opts.autoReloadSec,
1194
+ variant,
1195
+ generatedAt: opts.generatedAt,
1196
+ peerAvailable: variant !== "custom"
1197
+ }, fullHistory);
1198
+ writeAtomic(opts.outPath, html);
1091
1199
  }
1092
1200
  async function runDashboard(argv) {
1093
- const opts = parseArgs(argv);
1094
- let outPath;
1201
+ let opts;
1095
1202
  try {
1096
- outPath = opts.out ?? join(paths().home, "report.html");
1097
- writeDashboardHtml({ days: opts.days, outPath, autoReloadSec: opts.autoReloadSec });
1203
+ opts = parseArgs(argv);
1204
+ } catch (err) {
1205
+ console.error(`dashboard \u306E\u5F15\u6570\u304C\u4E0D\u6B63\u3067\u3059 / invalid dashboard arguments: ${err instanceof Error ? err.message : String(err)}`);
1206
+ return 1;
1207
+ }
1208
+ const canonical = opts.out === null;
1209
+ const outPath = opts.out ?? (opts.scope === "all" ? paths().fullDashboardFile : paths().recentDashboardFile);
1210
+ let lock = null;
1211
+ try {
1212
+ lock = await waitForDataLock();
1213
+ if (lock === null) throw new Error("data lock is busy; retry later");
1214
+ const generatedAt = /* @__PURE__ */ new Date();
1215
+ writeDashboardHtml({
1216
+ days: opts.days,
1217
+ outPath,
1218
+ autoReloadSec: opts.autoReloadSec,
1219
+ variant: canonical ? opts.scope === "all" ? "full" : "recent" : "custom",
1220
+ generatedAt: generatedAt.toISOString()
1221
+ });
1222
+ if (canonical && opts.scope === "all") {
1223
+ writeFullDashboardStateAtomic(makeFullDashboardState(generatedAt));
1224
+ }
1098
1225
  } catch (err) {
1099
1226
  console.error(
1100
1227
  `dashboard \u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F / failed to generate dashboard: ${err instanceof Error ? err.message : String(err)}`
1101
1228
  );
1102
1229
  return 1;
1230
+ } finally {
1231
+ lock?.release();
1103
1232
  }
1104
1233
  console.log(`\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3092\u751F\u6210\u3057\u307E\u3057\u305F / dashboard written: ${outPath}`);
1105
1234
  if (process.platform === "linux" && isWSL()) {