ccc-notifier 0.2.0 → 0.4.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.
@@ -2,65 +2,113 @@
2
2
  import {
3
3
  isWSL
4
4
  } from "./chunk-DGXUSPS4.js";
5
+ import {
6
+ makeFullDashboardState,
7
+ writeFullDashboardStateAtomic
8
+ } from "./chunk-ZHIZZ6V5.js";
9
+ import {
10
+ waitForDataLock
11
+ } from "./chunk-J6Y3RMMC.js";
5
12
  import {
6
13
  formatJPY,
7
14
  formatTokens,
8
15
  formatUSD,
9
16
  modelDisplayName
10
- } from "./chunk-TBFKGFZX.js";
17
+ } from "./chunk-J5QAYTFE.js";
11
18
  import {
12
19
  paths,
13
20
  readConfig,
14
21
  readTurns
15
- } from "./chunk-IIYMGLV4.js";
22
+ } from "./chunk-26CISNOE.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) {
@@ -170,7 +225,7 @@ function buildTurnEmbed(rec, map) {
170
225
  };
171
226
  }
172
227
  const ms = Date.parse(rec.ts);
173
- return {
228
+ const out = {
174
229
  t: Number.isFinite(ms) ? ms : 0,
175
230
  ts: fmtLocalDateTime(rec.ts),
176
231
  p: basename(rec.project) || rec.project || "(unknown)",
@@ -187,6 +242,8 @@ function buildTurnEmbed(rec, map) {
187
242
  tr: truncated,
188
243
  sa
189
244
  };
245
+ if (rec.source === "codex") out.sc = "codex";
246
+ return out;
190
247
  }
191
248
  function computeKpis(turns) {
192
249
  const now = /* @__PURE__ */ new Date();
@@ -331,6 +388,7 @@ section.card > .note{color:var(--muted); font-size:12px; margin:0 0 14px;}
331
388
  .turns .c-time,.turns .c-model{white-space:nowrap;}
332
389
  .turns .c-proj{max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
333
390
  .turns .c-prompt{color:var(--ink2);}
391
+ .src-badge{display:inline-block; margin-left:6px; padding:1px 6px; border-radius:6px; font-size:11px; font-weight:600; line-height:1.4; background:var(--plane); color:var(--ink2); border:1px solid var(--border); vertical-align:middle;}
334
392
  .turn-row{cursor:pointer;}
335
393
  .turn-row:hover{background:color-mix(in srgb, var(--ink) 4%, transparent);}
336
394
  .turn-row:focus-visible{outline:2px solid var(--s1); outline-offset:-2px;}
@@ -408,7 +466,7 @@ var APP_JS = `<script>
408
466
  return p[1] + '/' + p[2] + (gran === 'week' ? '\u9031' : '');
409
467
  }
410
468
  function periodText(key, gran){
411
- if(key === null) return '\u901A\u7B97 / All time';
469
+ if(key === null) return data.allPeriodLabel || '\u901A\u7B97 / All time';
412
470
  if(gran === 'month') return key + ' (\u6708)';
413
471
  if(gran === 'week') return key + ' \u306E\u9031';
414
472
  return key;
@@ -418,17 +476,43 @@ var APP_JS = `<script>
418
476
  function ssGet(k){ try { return sessionStorage.getItem(k); } catch(e){ return null; } }
419
477
  function ssSet(k, v){ try { sessionStorage.setItem(k, v); } catch(e){} }
420
478
  var GRAN = ssGet('cccn-gran'); if(['day','week','month'].indexOf(GRAN) < 0) GRAN = 'day';
479
+ // \u30BD\u30FC\u30B9(Claude/Codex)\u30D5\u30A3\u30EB\u30BF\u3002\u30C1\u30C3\u30D7\u304C\u63CF\u753B\u3055\u308C\u3066\u3044\u308B(=Codex \u30EC\u30B3\u30FC\u30C9\u304C\u5B58\u5728\u3059\u308B)\u3068\u304D\u306E\u307F\u6709\u52B9\u3002
480
+ // \u65E2\u5B9A 'all'\u3002sessionStorage \u306B\u4FDD\u5B58\u3057\u81EA\u52D5\u30EA\u30ED\u30FC\u30C9(meta refresh)\u3092\u8DE8\u3044\u3067\u7DAD\u6301\u3059\u308B(cccn-gran \u3068\u540C\u3058\u4ED5\u7D44\u307F)\u3002
481
+ var HAS_CODEX = document.querySelectorAll('[data-src]').length > 0;
482
+ var SRC = ssGet('cccn-src'); if(['all','claude','codex'].indexOf(SRC) < 0) SRC = 'all';
483
+ // \u8FF0\u8A9E: claude \u2192 sc \u306A\u3057 / codex \u2192 sc==='codex' / all \u2192 \u5168\u4EF6\u3002Codex \u7121\u3057(\u30C1\u30C3\u30D7\u7121\u3057)\u306A\u3089\u5E38\u306B\u5168\u4EF6\u3002
484
+ function activeTurns(){
485
+ if(!HAS_CODEX || SRC === 'all') return turns;
486
+ if(SRC === 'codex') return turns.filter(function(tn){ return tn.sc === 'codex'; });
487
+ return turns.filter(function(tn){ return !tn.sc; });
488
+ }
421
489
  // \u65E2\u5B9A\u306F\u300C\u8D77\u52D5\u3057\u305F\u65E5\u300D\u3092\u521D\u671F\u9078\u629E\u3059\u308B(\u672A\u4FDD\u5B58\u6642)\u3002\u660E\u793A\u7684\u306A\u901A\u7B97\u306F '__all__'\u3001\u305D\u306E\u4ED6\u306F\u4FDD\u5B58\u30AD\u30FC\u3002
422
490
  var storedSel = ssGet('cccn-sel');
423
491
  var SEL = (storedSel === null) ? defaultSel(GRAN) : (storedSel === '__all__' ? null : storedSel);
424
492
  function setGran(g){ GRAN = g; ssSet('cccn-gran', g); }
425
493
  function setSel(v){ SEL = v; ssSet('cccn-sel', v === null ? '__all__' : v); }
494
+ // \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
495
+ if(SEL !== null){
496
+ var initialBuckets = buildBuckets(GRAN), initialSelPresent = false;
497
+ for(var ib=0;ib<initialBuckets.length;ib++){ if(initialBuckets[ib].key === SEL){ initialSelPresent = true; break; } }
498
+ if(!initialSelPresent) setSel(null);
499
+ }
500
+ // \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
501
+ function setSrc(s){
502
+ SRC = s; ssSet('cccn-src', s);
503
+ if(SEL !== null){
504
+ var present = false, bk = buildBuckets(GRAN);
505
+ for(var i=0;i<bk.length;i++){ if(bk[i].key === SEL){ present = true; break; } }
506
+ if(!present) setSel(null);
507
+ }
508
+ }
426
509
  var lastRenderedGran = null; // \u76F4\u524D\u306B\u63CF\u753B\u3057\u305F\u7C92\u5EA6\u3002\u5909\u308F\u3063\u305F\u3068\u304D(=\u30D0\u30B1\u30C3\u30C8\u6570\u304C\u5909\u308F\u308B\u3068\u304D)\u3060\u3051\u53F3\u7AEF\u3078\u5BC4\u305B\u308B\u3002
427
510
 
428
511
  function buildBuckets(gran){
512
+ var src = activeTurns();
429
513
  var map = {}; var keys = [];
430
- for(var i=0;i<turns.length;i++){
431
- var tn = turns[i];
514
+ for(var i=0;i<src.length;i++){
515
+ var tn = src[i];
432
516
  var k = bucketKey(tn.t, gran);
433
517
  if(k === null) continue;
434
518
  var b = map[k];
@@ -452,8 +536,9 @@ var APP_JS = `<script>
452
536
  }
453
537
 
454
538
  function turnsInSelection(){
455
- if(SEL === null) return turns;
456
- return turns.filter(function(tn){ return bucketKey(tn.t, GRAN) === SEL; });
539
+ var src = activeTurns();
540
+ if(SEL === null) return src;
541
+ return src.filter(function(tn){ return bucketKey(tn.t, GRAN) === SEL; });
457
542
  }
458
543
 
459
544
  // ---- SVG \u30D8\u30EB\u30D1\u30FC ----
@@ -691,10 +776,12 @@ var APP_JS = `<script>
691
776
  projEl.appendChild(table);
692
777
  }
693
778
 
694
- // ---- \u6708\u4E88\u7B97(\u9078\u629E\u4E2D\u306E\u6708\u306B\u9023\u52D5)----
779
+ // ---- \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)----
695
780
  var budgetEl = document.getElementById('cccn-budget');
696
781
  var BUDGET = +data.budget || 0;
697
782
  var BUDGET_RATE = +data.budgetRate || 0;
783
+ var BUDGET_FIXED = data.budgetFixed === true;
784
+ var BUDGET_MONTH = data.budgetMonth || null;
698
785
  function monthKeyOf(t){ var d = new Date(t); if(isNaN(d.getTime())) return null; return d.getFullYear() + '-' + pad(d.getMonth()+1); }
699
786
  // \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
700
787
  function targetMonthKey(){
@@ -704,13 +791,16 @@ var APP_JS = `<script>
704
791
  }
705
792
  function renderBudget(){
706
793
  if(!budgetEl || !(BUDGET > 0)) return;
707
- var mk = targetMonthKey();
708
- var usd = 0, jpy = 0;
709
- for(var i=0;i<turns.length;i++){
710
- var tn = turns[i];
711
- if(monthKeyOf(tn.t) !== mk) continue;
712
- var bs = tn.bs || {}, fx = tn.fx || 0;
713
- for(var s in bs){ if(bs.hasOwnProperty(s)){ usd += bs[s]; jpy += bs[s]*fx; } }
794
+ var mk = BUDGET_FIXED ? monthKeyOf(Date.now()) : targetMonthKey();
795
+ var usd = BUDGET_FIXED && BUDGET_MONTH ? (+BUDGET_MONTH.usd || 0) : 0;
796
+ var jpy = BUDGET_FIXED && BUDGET_MONTH ? (+BUDGET_MONTH.jpy || 0) : 0;
797
+ if(!BUDGET_FIXED){
798
+ for(var i=0;i<turns.length;i++){
799
+ var tn = turns[i];
800
+ if(monthKeyOf(tn.t) !== mk) continue;
801
+ var bs = tn.bs || {}, fx = tn.fx || 0;
802
+ for(var s in bs){ if(bs.hasOwnProperty(s)){ usd += bs[s]; jpy += bs[s]*fx; } }
803
+ }
714
804
  }
715
805
  var pct = BUDGET > 0 ? (usd / BUDGET) * 100 : 0;
716
806
  var width = Math.max(0, Math.min(100, pct));
@@ -726,6 +816,15 @@ var APP_JS = `<script>
726
816
  sub.textContent = ' ' + mk + (mk === curMonth ? '(\u4ECA\u6708)' : '');
727
817
  h.appendChild(sub);
728
818
  budgetEl.appendChild(h);
819
+ // \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
820
+ // \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
821
+ if(BUDGET_FIXED || HAS_CODEX){
822
+ var srcNote = document.createElement('p'); srcNote.className = 'note';
823
+ srcNote.textContent = BUDGET_FIXED
824
+ ? '\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'
825
+ : '\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources';
826
+ budgetEl.appendChild(srcNote);
827
+ }
729
828
 
730
829
  var bar = document.createElement('div'); bar.className = 'budget-bar';
731
830
  var fill = document.createElement('div'); fill.className = 'budget-fill lvl-' + level;
@@ -746,6 +845,55 @@ var APP_JS = `<script>
746
845
  budgetEl.appendChild(foot);
747
846
  }
748
847
 
848
+ // ---- KPI(\u4ECA\u65E5/\u4ECA\u9031/\u4ECA\u6708/\u901A\u7B97)+ \u30D2\u30FC\u30ED\u30FC(\u901A\u7B97\u30D0\u30CA\u30FC)----
849
+ // \u30BD\u30FC\u30B9\u30D5\u30A3\u30EB\u30BF\u9023\u52D5\u3002Codex \u7121\u3057\u306A\u3089\u30B5\u30FC\u30D0\u63CF\u753B\u306E\u307E\u307E(\u65E2\u5B58\u6319\u52D5\u30921px\u3082\u5909\u3048\u306A\u3044\u305F\u3081\u4F55\u3082\u3057\u306A\u3044)\u3002
850
+ // \u30AB\u30FC\u30C9\u306F .kpi \u5185\u306E .stat \u3092\u9806\u306B today / week / month / all \u3068\u3057\u3066\u66F4\u65B0\u3059\u308B(\u30B5\u30FC\u30D0\u63CF\u753B\u9806\u306B\u4E00\u81F4)\u3002
851
+ // \u30D2\u30FC\u30ED\u30FC\u306F\u30D5\u30A3\u30EB\u30BF\u5F8C\u306E\u5168\u671F\u9593\u5408\u8A08\u306B\u5DEE\u3057\u66FF\u3048\u3001SA \u5185\u8A33\u884C\u306F SA \u5408\u8A08\u30BC\u30ED\u306A\u3089\u884C\u3054\u3068\u96A0\u3059
852
+ // (Codex \u30EC\u30B3\u30FC\u30C9\u306F SA \u3092\u6301\u305F\u306A\u3044\u305F\u3081 Codex \u30D5\u30A3\u30EB\u30BF\u6642\u306F\u5FC5\u7136\u7684\u306B\u975E\u8868\u793A)\u3002\u884C\u306E\u6587\u8A00\u306F\u30B5\u30FC\u30D0\u63CF\u753B\u306E
853
+ // \u63A5\u982D\u8F9E($ \u306E\u624B\u524D\u307E\u3067)\u3092\u518D\u5229\u7528\u3057\u3066\u6570\u5024\u90E8\u5206\u3060\u3051\u7D44\u307F\u76F4\u3059(APP_JS \u306B\u8868\u793A\u6587\u8A00\u3092\u91CD\u8907\u3055\u305B\u306A\u3044)\u3002
854
+ var kpiCards = document.querySelectorAll('.kpi .stat');
855
+ var heroValueEl = document.querySelector('.hero .hero-value');
856
+ var heroMetaEls = document.querySelectorAll('.hero .hero-meta');
857
+ var heroSaEl = heroMetaEls.length > 1 ? heroMetaEls[1] : null; // SA \u884C(SA \u3042\u308A\u306E\u3068\u304D\u3060\u3051\u30B5\u30FC\u30D0\u304C\u63CF\u753B)
858
+ var heroSaPrefix = heroSaEl ? ((heroSaEl.textContent || '').split('$')[0]) : '';
859
+ function setKpiCard(card, tot){
860
+ if(!card) return;
861
+ var v = card.querySelector('.stat-value');
862
+ var m = card.querySelector('.stat-meta');
863
+ if(v) v.textContent = formatUSD(tot.usd);
864
+ if(m) m.textContent = formatJPY(tot.jpy) + ' \xB7 ' + tot.turns + ' \u30BF\u30FC\u30F3';
865
+ }
866
+ function renderKpis(){
867
+ if(!HAS_CODEX) return;
868
+ var src = activeTurns();
869
+ var now = new Date();
870
+ var y = now.getFullYear(), mo = now.getMonth(), d = now.getDate();
871
+ var weekCutoff = Date.now() - 7*86400000;
872
+ var today = {usd:0,jpy:0,turns:0}, week = {usd:0,jpy:0,turns:0}, month = {usd:0,jpy:0,turns:0}, all = {usd:0,jpy:0,turns:0};
873
+ var saUsd = 0, saJpy = 0;
874
+ for(var i=0;i<src.length;i++){
875
+ var tn = src[i], bs = tn.bs || {}, fx = tn.fx || 0, u = 0;
876
+ for(var s in bs){ if(bs.hasOwnProperty(s)) u += bs[s]; }
877
+ var j = u * fx;
878
+ if(tn.sa){ var sau = u - (tn.um || 0); saUsd += sau; saJpy += sau * fx; } // SA \u5206 = \u7DCF\u984D \u2212 \u30E1\u30A4\u30F3
879
+ all.usd += u; all.jpy += j; all.turns++;
880
+ var dt = new Date(tn.t);
881
+ if(isNaN(dt.getTime())) continue;
882
+ if(dt.getFullYear()===y && dt.getMonth()===mo && dt.getDate()===d){ today.usd+=u; today.jpy+=j; today.turns++; }
883
+ if(tn.t >= weekCutoff){ week.usd+=u; week.jpy+=j; week.turns++; }
884
+ if(dt.getFullYear()===y && dt.getMonth()===mo){ month.usd+=u; month.jpy+=j; month.turns++; }
885
+ }
886
+ if(kpiCards.length >= 4){
887
+ setKpiCard(kpiCards[0], today); setKpiCard(kpiCards[1], week); setKpiCard(kpiCards[2], month); setKpiCard(kpiCards[3], all);
888
+ }
889
+ if(heroValueEl) heroValueEl.textContent = formatUSD(all.usd);
890
+ if(heroMetaEls.length > 0) heroMetaEls[0].textContent = formatJPY(all.jpy) + ' \xB7 ' + all.turns + ' \u30BF\u30FC\u30F3';
891
+ if(heroSaEl){
892
+ heroSaEl.hidden = !(saUsd > 0);
893
+ if(saUsd > 0) heroSaEl.textContent = heroSaPrefix + formatUSD(saUsd) + '(' + formatJPY(saJpy) + ')';
894
+ }
895
+ }
896
+
749
897
  // ---- \u30BF\u30FC\u30F3\u5C65\u6B74 ----
750
898
  // \u5168\u671F\u9593\u3060\u3068\u6570\u5343\u884C\u306B\u306A\u308A\u30DA\u30FC\u30B8\u304C\u6975\u7AEF\u306B\u9577\u304F\u30FB\u91CD\u304F\u306A\u308B\u305F\u3081\u3001DOM \u63CF\u753B\u306F\u6700\u5927 HIST_CAP \u4EF6\u306B\u6291\u3048\u308B\u3002
751
899
  // \u691C\u7D22\u306F\u9078\u629E\u4E2D\u306E\u5168\u30BF\u30FC\u30F3\u3092\u5BFE\u8C61\u306B\u3057\u3001\u4E00\u81F4\u4EF6\u6570\u3092\u4EF6\u6570\u8868\u793A\u306B\u51FA\u3059(\u5148\u982D HIST_CAP \u4EF6\u3060\u3051\u63CF\u753B)\u3002
@@ -765,7 +913,14 @@ var APP_JS = `<script>
765
913
  function cell(text, cls){ var td = document.createElement('td'); if(cls) td.className = cls; td.textContent = text; tr.appendChild(td); return td; }
766
914
  cell(t.ts || '', 'c-time');
767
915
  cell(t.p || '', 'c-proj');
768
- cell(t.md || '', 'c-model');
916
+ var mcell = cell(t.md || '', 'c-model');
917
+ // Codex \u7531\u6765\u306F\u30E2\u30C7\u30EB\u540D\u306E\u96A3\u306B\u5C0F\u3055\u306A Codex \u30D0\u30C3\u30B8\u3002\u52D5\u7684\u5024\u306F\u5165\u308C\u305A\u9759\u7684\u30C6\u30AD\u30B9\u30C8\u3092 textContent \u3067\u633F\u5165\u3002
918
+ if(t.sc === 'codex'){
919
+ mcell.appendChild(document.createTextNode(' '));
920
+ var badge = document.createElement('span'); badge.className = 'src-badge';
921
+ badge.textContent = 'Codex';
922
+ mcell.appendChild(badge);
923
+ }
769
924
  cell(t.ti || '', 'c-num');
770
925
  cell(t.to || '', 'c-num');
771
926
  cell(formatUSD(t.um || 0), 'c-num');
@@ -832,6 +987,8 @@ var APP_JS = `<script>
832
987
  for(var i=0;i<gbtns.length;i++){ gbtns[i].classList.toggle('active', gbtns[i].getAttribute('data-gran') === GRAN); }
833
988
  var allBtn = document.getElementById('cccn-all');
834
989
  if(allBtn) allBtn.classList.toggle('active', SEL === null);
990
+ var sbtns = document.querySelectorAll('[data-src]');
991
+ for(var k=0;k<sbtns.length;k++){ sbtns[k].classList.toggle('active', sbtns[k].getAttribute('data-src') === SRC); }
835
992
  }
836
993
 
837
994
  // ---- \u5168\u4F53\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0 ----
@@ -839,6 +996,7 @@ var APP_JS = `<script>
839
996
  var sub = turnsInSelection();
840
997
  renderChart();
841
998
  renderBudget();
999
+ renderKpis();
842
1000
  renderByModel(sub);
843
1001
  renderByProject(sub);
844
1002
  renderHistory(sub);
@@ -853,6 +1011,10 @@ var APP_JS = `<script>
853
1011
  }
854
1012
  var allBtn = document.getElementById('cccn-all');
855
1013
  if(allBtn) allBtn.addEventListener('click', function(){ setSel(null); render(); });
1014
+ var srcBtns = document.querySelectorAll('[data-src]');
1015
+ for(var sbi=0;sbi<srcBtns.length;sbi++){
1016
+ (function(btn){ btn.addEventListener('click', function(){ setSrc(btn.getAttribute('data-src')); render(); }); })(srcBtns[sbi]);
1017
+ }
856
1018
  if(search){
857
1019
  var saved = ssGet('cccn-search');
858
1020
  if(saved !== null) search.value = saved;
@@ -879,28 +1041,36 @@ function renderLegend(slots) {
879
1041
  const items = slots.map((m) => `<span class="legend-item"><span class="swatch k${m.slot}"></span>${esc(m.name)}</span>`).join("");
880
1042
  return `<div class="legend">${items}</div>`;
881
1043
  }
882
- function budgetCard(budgetUSD, month, fallbackRate) {
1044
+ function budgetCard(budgetUSD, month, fallbackRate, hasCodex, fixedCurrentMonth) {
883
1045
  if (!(budgetUSD > 0)) return "";
884
1046
  const pct = month.usd / budgetUSD * 100;
885
1047
  const width = Math.max(0, Math.min(100, pct));
886
1048
  const level = pct >= 100 ? "over" : pct >= 70 ? "warn" : "ok";
887
1049
  const budgetJpy = budgetUSD * fallbackRate;
888
- return `<section class="card" id="cccn-budget"><h2>\u6708\u4E88\u7B97 / Monthly budget<span class="stat-sub"> \u4ECA\u6708(\u66A6\u6708)</span></h2><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>`;
1050
+ 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>`;
889
1051
  }
890
1052
  function escapeJsonForScript(json) {
891
1053
  return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
892
1054
  }
893
- function renderDashboard(turns, opts) {
1055
+ function renderDashboard(turns, opts, fullHistory) {
894
1056
  const version = readVersion();
895
- const generatedAt = fmtLocalDateTime((/* @__PURE__ */ new Date()).toISOString());
1057
+ const generatedAtIso = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1058
+ const generatedAt = fmtLocalDateTime(generatedAtIso);
896
1059
  const period = opts.days === null ? "\u5168\u671F\u9593" : `\u76F4\u8FD1 ${opts.days} \u65E5\u9593`;
897
- const map = computeSlotMap(turns);
1060
+ const limited = opts.days !== null;
1061
+ const variant = opts.variant ?? "custom";
1062
+ const totalLabel = limited ? "\u5BFE\u8C61\u671F\u9593\u5408\u8A08" : "\u901A\u7B97";
1063
+ const totalLabelEn = limited ? "Embedded period total" : "Total";
1064
+ const totalSub = limited ? "\u57CB\u3081\u8FBC\u307F\u5BFE\u8C61\u671F\u9593" : "\u5168\u671F\u9593";
1065
+ const map = fullHistory?.slotMap ?? computeSlotMap(turns);
898
1066
  const kpi = computeKpis(turns);
899
1067
  const cfg = readConfig();
900
1068
  const budgetUSD = cfg.monthlyBudgetUSD;
1069
+ const budgetMonth = limited ? fullHistory?.currentMonth ?? kpi.month : kpi.month;
901
1070
  const turnsEmbed = turns.map((rec) => buildTurnEmbed(rec, map));
902
1071
  const anyTruncated = turnsEmbed.some((t) => t.tr);
903
1072
  const anySub = turns.some((r) => r.subagents);
1073
+ const anyCodex = turns.some((r) => r.source === "codex");
904
1074
  let subUsd = 0;
905
1075
  let subJpy = 0;
906
1076
  for (const r of turns) {
@@ -924,24 +1094,32 @@ function renderDashboard(turns, opts) {
924
1094
  slots: map.slots,
925
1095
  turns: turnsEmbed,
926
1096
  budget: budgetUSD,
927
- budgetRate: cfg.fx.fallbackRate
1097
+ budgetRate: cfg.fx.fallbackRate,
1098
+ budgetMonth,
1099
+ budgetFixed: limited,
1100
+ allPeriodLabel: `${totalLabel} / ${totalLabelEn}`,
1101
+ variant
928
1102
  };
929
1103
  const dataJson = escapeJsonForScript(JSON.stringify(embed));
930
1104
  const reloadSec = Number.isFinite(opts.autoReloadSec) && opts.autoReloadSec > 0 ? Math.floor(opts.autoReloadSec) : 0;
931
1105
  const refreshMeta = reloadSec > 0 ? `<meta http-equiv="refresh" content="${reloadSec}">` : "";
932
- const autoUpdateFoot = reloadSec > 0 ? `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u81EA\u52D5\u66F4\u65B0(\u6700\u65B0\u5316\u306F Claude Code \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642)</div>` : "";
1106
+ const updateTrigger = anyCodex ? "Claude Code / Codex \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642" : "Claude Code \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642";
1107
+ 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>` : "";
1108
+ 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>` : "";
933
1109
  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">`;
934
1110
  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>`;
1111
+ const budgetSection = budgetCard(budgetUSD, budgetMonth, cfg.fx.fallbackRate, anyCodex, limited);
935
1112
  if (turns.length === 0) {
936
- 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>`;
937
- 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>`;
938
- return head + header2 + empty + foot;
1113
+ const outsideEmbeddedPeriod = limited && (fullHistory?.hasTurns ?? false);
1114
+ 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>`;
1115
+ 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>`;
1116
+ return head + header2 + budgetSection + empty + foot;
939
1117
  }
940
- 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>`;
941
- 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>`;
942
- const budgetSection = budgetCard(budgetUSD, kpi.month, cfg.fx.fallbackRate);
943
- 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><span id="cccn-sel-label" class="sel-label"></span></div>` + renderLegend(map.slots) + `<div class="chart-scroll"><div id="cccn-chart"></div></div></section>`;
944
- 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>`;
1118
+ 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>`;
1119
+ 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>`;
1120
+ 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>` : "";
1121
+ 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>`;
1122
+ 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>`;
945
1123
  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";
946
1124
  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>`;
947
1125
  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>`;
@@ -983,27 +1161,59 @@ function openInBrowser(path) {
983
1161
  }
984
1162
  function writeDashboardHtml(opts) {
985
1163
  const days = opts.days ?? null;
986
- const turns = readTurns(days ?? void 0);
1164
+ const variant = opts.variant ?? (basename(opts.outPath) === "report.html" ? "recent" : basename(opts.outPath) === "report-all.html" ? "full" : "custom");
1165
+ ensurePeerPlaceholder(variant);
1166
+ const allTurns = opts.allTurns ?? readTurns();
1167
+ const turns = days === null ? allTurns : filterTurnsByDays(allTurns, days);
1168
+ const fullHistory = days === null ? void 0 : {
1169
+ currentMonth: computeKpis(allTurns).month,
1170
+ slotMap: computeSlotMap(allTurns),
1171
+ hasTurns: allTurns.length > 0
1172
+ };
987
1173
  const html = renderDashboard(turns, {
1174
+ scope: days === null ? "all" : "recent",
988
1175
  days,
989
1176
  open: false,
990
1177
  out: opts.outPath,
991
- autoReloadSec: opts.autoReloadSec
992
- });
993
- mkdirSync(dirname(opts.outPath), { recursive: true });
994
- writeFileSync(opts.outPath, html, "utf8");
1178
+ autoReloadSec: opts.autoReloadSec,
1179
+ variant,
1180
+ generatedAt: opts.generatedAt,
1181
+ peerAvailable: variant !== "custom"
1182
+ }, fullHistory);
1183
+ writeAtomic(opts.outPath, html);
995
1184
  }
996
1185
  async function runDashboard(argv) {
997
- const opts = parseArgs(argv);
998
- let outPath;
1186
+ let opts;
1187
+ try {
1188
+ opts = parseArgs(argv);
1189
+ } catch (err) {
1190
+ console.error(`dashboard \u306E\u5F15\u6570\u304C\u4E0D\u6B63\u3067\u3059 / invalid dashboard arguments: ${err instanceof Error ? err.message : String(err)}`);
1191
+ return 1;
1192
+ }
1193
+ const canonical = opts.out === null;
1194
+ const outPath = opts.out ?? (opts.scope === "all" ? paths().fullDashboardFile : paths().recentDashboardFile);
1195
+ let lock = null;
999
1196
  try {
1000
- outPath = opts.out ?? join(paths().home, "report.html");
1001
- writeDashboardHtml({ days: opts.days, outPath, autoReloadSec: opts.autoReloadSec });
1197
+ lock = await waitForDataLock();
1198
+ if (lock === null) throw new Error("data lock is busy; retry later");
1199
+ const generatedAt = /* @__PURE__ */ new Date();
1200
+ writeDashboardHtml({
1201
+ days: opts.days,
1202
+ outPath,
1203
+ autoReloadSec: opts.autoReloadSec,
1204
+ variant: canonical ? opts.scope === "all" ? "full" : "recent" : "custom",
1205
+ generatedAt: generatedAt.toISOString()
1206
+ });
1207
+ if (canonical && opts.scope === "all") {
1208
+ writeFullDashboardStateAtomic(makeFullDashboardState(generatedAt));
1209
+ }
1002
1210
  } catch (err) {
1003
1211
  console.error(
1004
1212
  `dashboard \u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F / failed to generate dashboard: ${err instanceof Error ? err.message : String(err)}`
1005
1213
  );
1006
1214
  return 1;
1215
+ } finally {
1216
+ lock?.release();
1007
1217
  }
1008
1218
  console.log(`\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3092\u751F\u6210\u3057\u307E\u3057\u305F / dashboard written: ${outPath}`);
1009
1219
  if (process.platform === "linux" && isWSL()) {