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