ccc-notifier 0.3.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.
package/README.md CHANGED
@@ -112,7 +112,7 @@ CI などから非対話で `init` したい場合は次のフラグが使えま
112
112
  | `init` | Stop hook を対話形式でセットアップ(前述のフラグで非対話実行も可) |
113
113
  | `doctor` | hook登録・設定・単価表・為替・通知・直近セッション合計を診断 |
114
114
  | `report [--days N] [--json]` | 蓄積した履歴を集計してターミナルに表示(`--days` の既定は30、不正な値も30扱い)。`--json` で機械可読な出力 |
115
- | `dashboard [--days N] [--no-open] [--out <path>] [--refresh <sec>\|--no-refresh]` | 履歴を可視化した HTML ダッシュボードを生成してブラウザで開く(既定で全履歴を埋め込み、日/週/月・通算をブラウザ側で切替。詳細は [ダッシュボード](docs/dashboard.md)) |
115
+ | `dashboard [--all\|--days N] [--no-open] [--out <path>] [--refresh <sec>\|--no-refresh]` | 履歴を可視化した HTML ダッシュボードを生成してブラウザで開く(引数なしは設定期間の直近版 `report.html`、`--all` は全履歴版 `report-all.html`。詳細は [ダッシュボード](docs/dashboard.md)) |
116
116
  | `sweep [--dry-run] [--days N] [--include-active]` | 過去の未計上分(hook 導入前や後から完了したサブエージェント分)を一括で履歴に取り込む。ローカル走査のみで **Claude API を呼ばず料金ゼロ**・二重計上なし(詳細は [過去分の取り込み](docs/sweep.md)) |
117
117
  | `history <clear\|redact> [--days N] [--yes]` | 履歴(`history.jsonl`)を削除。`clear` はレコードごと、`redact` はプロンプト全文だけ消去。`--days N` で「N 日より前」だけ対象(詳細は [履歴の削除](docs/dashboard.md#履歴の削除--deleting-history)) |
118
118
  | `budget [<USD>]` | 月予算(USD)の表示/設定。金額省略で現在の予算と当月の使用率を表示、`budget 400` で設定、`budget 0` で解除(詳細は [月予算](docs/monthly-budget.md)) |
@@ -138,13 +138,15 @@ CI などから非対話で `init` したい場合は次のフラグが使えま
138
138
 
139
139
  ## ダッシュボード / Dashboard
140
140
 
141
- `dashboard` コマンドで、サマリー(今日 / 今週 / 今月 / 通算)、コスト推移(日/週/月で切替・横スクロールで過去まで)、モデル別/プロジェクト別内訳、検索・行展開できるターン履歴を1枚の HTML に書き出してブラウザで開きます。生成物は **CSS/JS/SVG をすべてインライン化した完全自己完結・オフライン動作・外部通信ゼロ**のファイルで、ライト/ダーク両対応です。
141
+ `dashboard` コマンドで、サマリー(今日 / 今週 / 今月 / 通算)、コスト推移(日/週/月で切替・横スクロールで過去まで)、モデル別/プロジェクト別内訳、検索・行展開できるターン履歴を HTML に書き出してブラウザで開きます。自動生成物は、毎ターン更新する直近版 `~/.ccc-notifier/report.html`(既定30日)と、ローカル日の最初の正常なターンだけ更新する全履歴版 `~/.ccc-notifier/report-all.html` に分かれます。片方が未生成でも生成方法を示すplaceholderを置くため、ページ内リンクは切れません。履歴・カーソル・生成snapshotは `cache/data.lock/` で直列化されます。履歴の読み込みと解析は正確な当月予算を保つため全履歴が対象です。生成物は **CSS/JS/SVG をすべてインライン化した完全自己完結・オフライン動作・外部通信ゼロ**のファイルで、ライト/ダーク両対応です。
142
142
 
143
143
  ```bash
144
- npx ccc-notifier dashboard # 生成してブラウザで開く
144
+ npx ccc-notifier dashboard # 設定期間(既定30日)の直近版 report.html を生成して開く
145
+ npx ccc-notifier dashboard --all # 全履歴版 report-all.html を生成して開く
146
+ npx ccc-notifier dashboard --days 7 # 直近7日の report.html を生成して開く
145
147
  ```
146
148
 
147
- グラフの棒をクリックするとその期間に内訳・履歴が連動し、「通算」で全期間に戻せます。Claude Code / Codex CLI の応答完了ごとに自動再生成され、開きっぱなしのタブは約30秒ごとに最新化されます。Codex CLI のレコードがあれば **Claude / Codex** を絞り込むソースフィルタも表示されます(詳細は [Codex CLI 対応](docs/codex.md))。検索・行クリックでプロンプト全文を確認できます:
149
+ グラフの棒をクリックするとその期間に内訳・履歴が連動し、全履歴版では「通算」、期間限定版では「対象期間合計」で埋め込まれた全期間に戻せます。直近版は応答完了ごと、全履歴版は1日1回(または手動 `dashboard --all`)更新されます。Codex CLI のレコードがあれば **Claude / Codex** を絞り込むソースフィルタも表示されます(詳細は [Codex CLI 対応](docs/codex.md))。検索・行クリックでプロンプト全文を確認できます:
148
150
 
149
151
  ![ターン履歴(検索と全文展開)](docs/images/history-expand.png)
150
152
 
@@ -7,7 +7,7 @@ import {
7
7
  currentMonthTotals,
8
8
  paths,
9
9
  readConfig
10
- } from "./chunk-ECADO26T.js";
10
+ } from "./chunk-26CISNOE.js";
11
11
 
12
12
  // src/budget.ts
13
13
  import { writeFileSync } from "fs";
@@ -40,7 +40,12 @@ function paths() {
40
40
  cacheDir,
41
41
  errorLog: join(home, "error.log"),
42
42
  lastNotifyFile: join(home, "last-notify.json"),
43
- muteFile: join(home, "muted.json")
43
+ muteFile: join(home, "muted.json"),
44
+ recentDashboardFile: join(home, "report.html"),
45
+ fullDashboardFile: join(home, "report-all.html"),
46
+ dashboardFullStateFile: join(cacheDir, "dashboard-full-state.json"),
47
+ dataLockDir: join(cacheDir, "data.lock"),
48
+ dataReclaimDir: join(cacheDir, "data.lock.reclaim")
44
49
  };
45
50
  }
46
51
  function isPlainObject(v) {
@@ -88,7 +93,10 @@ function mergeConfig(partial) {
88
93
  result.dashboard.autoReloadSec = partial.dashboard.autoReloadSec;
89
94
  }
90
95
  if ("days" in partial.dashboard) {
91
- result.dashboard.days = partial.dashboard.days;
96
+ const days = partial.dashboard.days;
97
+ if (typeof days === "number" && Number.isFinite(days) && Number.isInteger(days) && days > 0) {
98
+ result.dashboard.days = days;
99
+ }
92
100
  }
93
101
  }
94
102
  return result;
@@ -2,6 +2,13 @@
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,
@@ -12,55 +19,96 @@ import {
12
19
  paths,
13
20
  readConfig,
14
21
  readTurns
15
- } from "./chunk-ECADO26T.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) {
@@ -411,7 +466,7 @@ var APP_JS = `<script>
411
466
  return p[1] + '/' + p[2] + (gran === 'week' ? '\u9031' : '');
412
467
  }
413
468
  function periodText(key, gran){
414
- if(key === null) return '\u901A\u7B97 / All time';
469
+ if(key === null) return data.allPeriodLabel || '\u901A\u7B97 / All time';
415
470
  if(gran === 'month') return key + ' (\u6708)';
416
471
  if(gran === 'week') return key + ' \u306E\u9031';
417
472
  return key;
@@ -436,6 +491,12 @@ var APP_JS = `<script>
436
491
  var SEL = (storedSel === null) ? defaultSel(GRAN) : (storedSel === '__all__' ? null : storedSel);
437
492
  function setGran(g){ GRAN = g; ssSet('cccn-gran', g); }
438
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
+ }
439
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
440
501
  function setSrc(s){
441
502
  SRC = s; ssSet('cccn-src', s);
@@ -715,10 +776,12 @@ var APP_JS = `<script>
715
776
  projEl.appendChild(table);
716
777
  }
717
778
 
718
- // ---- \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)----
719
780
  var budgetEl = document.getElementById('cccn-budget');
720
781
  var BUDGET = +data.budget || 0;
721
782
  var BUDGET_RATE = +data.budgetRate || 0;
783
+ var BUDGET_FIXED = data.budgetFixed === true;
784
+ var BUDGET_MONTH = data.budgetMonth || null;
722
785
  function monthKeyOf(t){ var d = new Date(t); if(isNaN(d.getTime())) return null; return d.getFullYear() + '-' + pad(d.getMonth()+1); }
723
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
724
787
  function targetMonthKey(){
@@ -728,13 +791,16 @@ var APP_JS = `<script>
728
791
  }
729
792
  function renderBudget(){
730
793
  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; } }
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
+ }
738
804
  }
739
805
  var pct = BUDGET > 0 ? (usd / BUDGET) * 100 : 0;
740
806
  var width = Math.max(0, Math.min(100, pct));
@@ -750,10 +816,13 @@ var APP_JS = `<script>
750
816
  sub.textContent = ' ' + mk + (mk === curMonth ? '(\u4ECA\u6708)' : '');
751
817
  h.appendChild(sub);
752
818
  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){
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){
755
822
  var srcNote = document.createElement('p'); srcNote.className = 'note';
756
- srcNote.textContent = '\u5168\u30BD\u30FC\u30B9\u5408\u7B97 / all sources';
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';
757
826
  budgetEl.appendChild(srcNote);
758
827
  }
759
828
 
@@ -972,25 +1041,32 @@ function renderLegend(slots) {
972
1041
  const items = slots.map((m) => `<span class="legend-item"><span class="swatch k${m.slot}"></span>${esc(m.name)}</span>`).join("");
973
1042
  return `<div class="legend">${items}</div>`;
974
1043
  }
975
- function budgetCard(budgetUSD, month, fallbackRate, hasCodex) {
1044
+ function budgetCard(budgetUSD, month, fallbackRate, hasCodex, fixedCurrentMonth) {
976
1045
  if (!(budgetUSD > 0)) return "";
977
1046
  const pct = month.usd / budgetUSD * 100;
978
1047
  const width = Math.max(0, Math.min(100, pct));
979
1048
  const level = pct >= 100 ? "over" : pct >= 70 ? "warn" : "ok";
980
1049
  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>`;
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>`;
982
1051
  }
983
1052
  function escapeJsonForScript(json) {
984
1053
  return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
985
1054
  }
986
- function renderDashboard(turns, opts) {
1055
+ function renderDashboard(turns, opts, fullHistory) {
987
1056
  const version = readVersion();
988
- const generatedAt = fmtLocalDateTime((/* @__PURE__ */ new Date()).toISOString());
1057
+ const generatedAtIso = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1058
+ const generatedAt = fmtLocalDateTime(generatedAtIso);
989
1059
  const period = opts.days === null ? "\u5168\u671F\u9593" : `\u76F4\u8FD1 ${opts.days} \u65E5\u9593`;
990
- 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);
991
1066
  const kpi = computeKpis(turns);
992
1067
  const cfg = readConfig();
993
1068
  const budgetUSD = cfg.monthlyBudgetUSD;
1069
+ const budgetMonth = limited ? fullHistory?.currentMonth ?? kpi.month : kpi.month;
994
1070
  const turnsEmbed = turns.map((rec) => buildTurnEmbed(rec, map));
995
1071
  const anyTruncated = turnsEmbed.some((t) => t.tr);
996
1072
  const anySub = turns.some((r) => r.subagents);
@@ -1018,26 +1094,32 @@ function renderDashboard(turns, opts) {
1018
1094
  slots: map.slots,
1019
1095
  turns: turnsEmbed,
1020
1096
  budget: budgetUSD,
1021
- budgetRate: cfg.fx.fallbackRate
1097
+ budgetRate: cfg.fx.fallbackRate,
1098
+ budgetMonth,
1099
+ budgetFixed: limited,
1100
+ allPeriodLabel: `${totalLabel} / ${totalLabelEn}`,
1101
+ variant
1022
1102
  };
1023
1103
  const dataJson = escapeJsonForScript(JSON.stringify(embed));
1024
1104
  const reloadSec = Number.isFinite(opts.autoReloadSec) && opts.autoReloadSec > 0 ? Math.floor(opts.autoReloadSec) : 0;
1025
1105
  const refreshMeta = reloadSec > 0 ? `<meta http-equiv="refresh" content="${reloadSec}">` : "";
1026
1106
  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>` : "";
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>` : "";
1028
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">`;
1029
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);
1030
1112
  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;
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;
1034
1117
  }
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);
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>`;
1038
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>` : "";
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>`;
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>`;
1041
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";
1042
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>`;
1043
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>`;
@@ -1079,27 +1161,59 @@ function openInBrowser(path) {
1079
1161
  }
1080
1162
  function writeDashboardHtml(opts) {
1081
1163
  const days = opts.days ?? null;
1082
- 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
+ };
1083
1173
  const html = renderDashboard(turns, {
1174
+ scope: days === null ? "all" : "recent",
1084
1175
  days,
1085
1176
  open: false,
1086
1177
  out: opts.outPath,
1087
- autoReloadSec: opts.autoReloadSec
1088
- });
1089
- mkdirSync(dirname(opts.outPath), { recursive: true });
1090
- 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);
1091
1184
  }
1092
1185
  async function runDashboard(argv) {
1093
- const opts = parseArgs(argv);
1094
- 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;
1095
1196
  try {
1096
- outPath = opts.out ?? join(paths().home, "report.html");
1097
- 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
+ }
1098
1210
  } catch (err) {
1099
1211
  console.error(
1100
1212
  `dashboard \u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F / failed to generate dashboard: ${err instanceof Error ? err.message : String(err)}`
1101
1213
  );
1102
1214
  return 1;
1215
+ } finally {
1216
+ lock?.release();
1103
1217
  }
1104
1218
  console.log(`\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3092\u751F\u6210\u3057\u307E\u3057\u305F / dashboard written: ${outPath}`);
1105
1219
  if (process.platform === "linux" && isWSL()) {
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  paths,
12
12
  readConfig
13
- } from "./chunk-ECADO26T.js";
13
+ } from "./chunk-26CISNOE.js";
14
14
 
15
15
  // src/setup.ts
16
16
  import {
@@ -6,7 +6,7 @@ import {
6
6
  loadCursor,
7
7
  logError,
8
8
  sanitizeCursor
9
- } from "./chunk-ECADO26T.js";
9
+ } from "./chunk-26CISNOE.js";
10
10
 
11
11
  // src/codex/transcript.ts
12
12
  import { readFile } from "fs/promises";