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 +6 -4
- package/dist/{budget-KKTFYAXH.js → budget-EHWPEYXY.js} +1 -1
- package/dist/{chunk-ECADO26T.js → chunk-26CISNOE.js} +10 -2
- package/dist/{chunk-QX5KIRSU.js → chunk-2VPBBIDW.js} +167 -53
- package/dist/{chunk-7KVOQ4UZ.js → chunk-5LHZOZZO.js} +1 -1
- package/dist/{chunk-DSV75EF7.js → chunk-CKVK3UCA.js} +1 -1
- package/dist/chunk-J6Y3RMMC.js +172 -0
- package/dist/{chunk-4RZ6OGTD.js → chunk-TUZVISLD.js} +1 -1
- package/dist/chunk-ZHIZZ6V5.js +92 -0
- package/dist/cli.js +13 -13
- package/dist/{dashboard-TH3NOVT3.js → dashboard-GYKABTTN.js} +4 -2
- package/dist/history-6OUJLXJT.js +162 -0
- package/dist/{mute-EPLYH67P.js → mute-GBDNN5PB.js} +2 -2
- package/dist/{setup-PTO5N46B.js → setup-W3CSTHJC.js} +2 -2
- package/dist/{sweep-25EZAOGC.js → sweep-IBSV4LRD.js} +134 -102
- package/dist/track-QT2U3E2R.js +235 -0
- package/package.json +1 -1
- package/dist/history-DLFYQJFM.js +0 -114
- package/dist/track-PXFSGMGL.js +0 -184
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 ダッシュボードを生成してブラウザで開く(
|
|
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` コマンドで、サマリー(今日 / 今週 / 今月 / 通算)、コスト推移(日/週/月で切替・横スクロールで過去まで)、モデル別/プロジェクト別内訳、検索・行展開できるターン履歴を
|
|
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
|
-
|
|
149
|
+
グラフの棒をクリックするとその期間に内訳・履歴が連動し、全履歴版では「通算」、期間限定版では「対象期間合計」で埋め込まれた全期間に戻せます。直近版は応答完了ごと、全履歴版は1日1回(または手動 `dashboard --all`)更新されます。Codex CLI のレコードがあれば **Claude / Codex** を絞り込むソースフィルタも表示されます(詳細は [Codex CLI 対応](docs/codex.md))。検索・行クリックでプロンプト全文を確認できます:
|
|
148
150
|
|
|
149
151
|

|
|
150
152
|
|
|
@@ -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
|
-
|
|
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-
|
|
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
|
|
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)
|
|
26
|
-
const parsed = Number
|
|
27
|
-
|
|
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
|
|
30
|
-
if (value === void 0)
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
69
|
+
let daysValue = null;
|
|
70
|
+
let daysSpecified = false;
|
|
71
|
+
let all = false;
|
|
36
72
|
let open = true;
|
|
37
73
|
let out = null;
|
|
38
|
-
const
|
|
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
|
-
|
|
84
|
+
daysSpecified = true;
|
|
85
|
+
daysValue = parseDays(argv[i + 1]);
|
|
46
86
|
i++;
|
|
47
87
|
} else if (arg.startsWith("--days=")) {
|
|
48
|
-
|
|
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]
|
|
100
|
+
autoReloadSec = parseRefresh(argv[i + 1]);
|
|
58
101
|
i++;
|
|
59
102
|
} else if (arg.startsWith("--refresh=")) {
|
|
60
|
-
autoReloadSec = parseRefresh(arg.slice("--refresh=".length)
|
|
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
|
-
|
|
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\
|
|
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 =
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
// \
|
|
754
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
1032
|
-
const
|
|
1033
|
-
|
|
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
|
|
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(
|
|
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\
|
|
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
|
|
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
|
|
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
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
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()) {
|