ccc-notifier 0.1.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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/budget-5H2GQRXH.js +71 -0
- package/dist/chunk-4JNZ7BHO.js +276 -0
- package/dist/chunk-64N5SGTT.js +1024 -0
- package/dist/chunk-DGXUSPS4.js +21 -0
- package/dist/chunk-IIYMGLV4.js +309 -0
- package/dist/chunk-KUJZYZSG.js +66 -0
- package/dist/chunk-L5MLVTA2.js +393 -0
- package/dist/chunk-OEG3AVU6.js +436 -0
- package/dist/chunk-TB5A7U7G.js +82 -0
- package/dist/chunk-TBFKGFZX.js +87 -0
- package/dist/cli.js +717 -0
- package/dist/dashboard-LKK6NOBN.js +14 -0
- package/dist/history-MWFHO5VR.js +114 -0
- package/dist/mute-2JW3NQXN.js +12 -0
- package/dist/setup-RT62VO37.js +15 -0
- package/dist/sweep-XKKODGMA.js +460 -0
- package/dist/track-PBHTVZ3L.js +168 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isWSL
|
|
4
|
+
} from "./chunk-DGXUSPS4.js";
|
|
5
|
+
import {
|
|
6
|
+
formatJPY,
|
|
7
|
+
formatTokens,
|
|
8
|
+
formatUSD,
|
|
9
|
+
modelDisplayName
|
|
10
|
+
} from "./chunk-TBFKGFZX.js";
|
|
11
|
+
import {
|
|
12
|
+
paths,
|
|
13
|
+
readConfig,
|
|
14
|
+
readTurns
|
|
15
|
+
} from "./chunk-IIYMGLV4.js";
|
|
16
|
+
|
|
17
|
+
// src/dashboard.ts
|
|
18
|
+
import { spawn, spawnSync } from "child_process";
|
|
19
|
+
import { createRequire } from "module";
|
|
20
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
21
|
+
import { basename, dirname, join } from "path";
|
|
22
|
+
var PROMPT_MAX = 1e4;
|
|
23
|
+
var PROMPT_TRUNC_MARK = "\u2026(\u4EE5\u4E0B\u7565)";
|
|
24
|
+
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;
|
|
28
|
+
}
|
|
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;
|
|
33
|
+
}
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
let days = null;
|
|
36
|
+
let open = true;
|
|
37
|
+
let out = null;
|
|
38
|
+
const cfgReloadSec = readConfig().dashboard.autoReloadSec;
|
|
39
|
+
let autoReloadSec = cfgReloadSec;
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const arg = argv[i];
|
|
42
|
+
if (arg === "--no-open") {
|
|
43
|
+
open = false;
|
|
44
|
+
} else if (arg === "--days") {
|
|
45
|
+
days = parseDays(argv[i + 1]);
|
|
46
|
+
i++;
|
|
47
|
+
} else if (arg.startsWith("--days=")) {
|
|
48
|
+
days = parseDays(arg.slice("--days=".length));
|
|
49
|
+
} else if (arg === "--out") {
|
|
50
|
+
out = argv[i + 1] ?? null;
|
|
51
|
+
i++;
|
|
52
|
+
} else if (arg.startsWith("--out=")) {
|
|
53
|
+
out = arg.slice("--out=".length) || null;
|
|
54
|
+
} else if (arg === "--no-refresh") {
|
|
55
|
+
autoReloadSec = 0;
|
|
56
|
+
} else if (arg === "--refresh") {
|
|
57
|
+
autoReloadSec = parseRefresh(argv[i + 1], cfgReloadSec);
|
|
58
|
+
i++;
|
|
59
|
+
} else if (arg.startsWith("--refresh=")) {
|
|
60
|
+
autoReloadSec = parseRefresh(arg.slice("--refresh=".length), cfgReloadSec);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { days, open, out, autoReloadSec };
|
|
64
|
+
}
|
|
65
|
+
function readVersion() {
|
|
66
|
+
try {
|
|
67
|
+
const require2 = createRequire(import.meta.url);
|
|
68
|
+
const pkg = require2("../package.json");
|
|
69
|
+
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
|
|
70
|
+
} catch {
|
|
71
|
+
return "unknown";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function esc(s) {
|
|
75
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
76
|
+
}
|
|
77
|
+
function dateKeyOf(d) {
|
|
78
|
+
const y = d.getFullYear();
|
|
79
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
80
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
81
|
+
return `${y}-${m}-${day}`;
|
|
82
|
+
}
|
|
83
|
+
function fmtLocalDateTime(iso) {
|
|
84
|
+
const d = new Date(iso);
|
|
85
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
86
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
87
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
88
|
+
return `${dateKeyOf(d)} ${hh}:${mm}`;
|
|
89
|
+
}
|
|
90
|
+
function effectiveInputTokens(t) {
|
|
91
|
+
if (!t) return 0;
|
|
92
|
+
return t.input + t.cacheRead + t.cacheWrite5m + t.cacheWrite1h;
|
|
93
|
+
}
|
|
94
|
+
function turnInputTokens(rec) {
|
|
95
|
+
return effectiveInputTokens(rec.tokens) + effectiveInputTokens(rec.sidechainTokens);
|
|
96
|
+
}
|
|
97
|
+
function turnOutputTokens(rec) {
|
|
98
|
+
return rec.tokens.output + (rec.sidechainTokens ? rec.sidechainTokens.output : 0);
|
|
99
|
+
}
|
|
100
|
+
function turnCostByModel(rec) {
|
|
101
|
+
if (rec.costByModel && Object.keys(rec.costByModel).length > 0) return rec.costByModel;
|
|
102
|
+
return { [rec.models[0] ?? "unknown"]: rec.costUSD };
|
|
103
|
+
}
|
|
104
|
+
function turnCostByModelWithSA(rec) {
|
|
105
|
+
const base = { ...turnCostByModel(rec) };
|
|
106
|
+
const sa = rec.subagents?.costByModel;
|
|
107
|
+
if (sa) {
|
|
108
|
+
for (const [model, usd] of Object.entries(sa)) {
|
|
109
|
+
base[model] = (base[model] ?? 0) + usd;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return base;
|
|
113
|
+
}
|
|
114
|
+
function turnTotalUSD(rec) {
|
|
115
|
+
return rec.costUSD + (rec.subagents?.costUSD ?? 0);
|
|
116
|
+
}
|
|
117
|
+
function turnTotalJPY(rec) {
|
|
118
|
+
const sa = rec.subagents?.costUSD ?? 0;
|
|
119
|
+
return rec.costJPY + sa * rec.fxRate;
|
|
120
|
+
}
|
|
121
|
+
function truncatePrompt(raw) {
|
|
122
|
+
if (raw.length <= PROMPT_MAX) return { text: raw, truncated: false };
|
|
123
|
+
return { text: raw.slice(0, PROMPT_MAX) + PROMPT_TRUNC_MARK, truncated: true };
|
|
124
|
+
}
|
|
125
|
+
function computeSlotMap(turns) {
|
|
126
|
+
const agg = /* @__PURE__ */ new Map();
|
|
127
|
+
for (const rec of turns) {
|
|
128
|
+
for (const [model, usd] of Object.entries(turnCostByModelWithSA(rec))) {
|
|
129
|
+
agg.set(model, (agg.get(model) ?? 0) + usd);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const entries = [...agg.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
133
|
+
let named = entries;
|
|
134
|
+
let hasOther = false;
|
|
135
|
+
if (entries.length > 8) {
|
|
136
|
+
named = entries.slice(0, 7);
|
|
137
|
+
hasOther = true;
|
|
138
|
+
}
|
|
139
|
+
const slots = named.map(([key], i) => ({ slot: String(i + 1), name: modelDisplayName(key) }));
|
|
140
|
+
const slotByModel = /* @__PURE__ */ new Map();
|
|
141
|
+
named.forEach(([key], i) => slotByModel.set(key, String(i + 1)));
|
|
142
|
+
if (hasOther) slots.push({ slot: "other", name: "\u305D\u306E\u4ED6" });
|
|
143
|
+
return { slots, slotByModel, hasOther };
|
|
144
|
+
}
|
|
145
|
+
function turnBySlot(rec, map) {
|
|
146
|
+
const bs = {};
|
|
147
|
+
for (const [model, usd] of Object.entries(turnCostByModelWithSA(rec))) {
|
|
148
|
+
const slot = map.slotByModel.get(model) ?? "other";
|
|
149
|
+
bs[slot] = (bs[slot] ?? 0) + usd;
|
|
150
|
+
}
|
|
151
|
+
return bs;
|
|
152
|
+
}
|
|
153
|
+
function emptyTotals() {
|
|
154
|
+
return { usd: 0, jpy: 0, turns: 0 };
|
|
155
|
+
}
|
|
156
|
+
function buildTurnEmbed(rec, map) {
|
|
157
|
+
const raw = String(rec.prompt ?? "");
|
|
158
|
+
const { text, truncated } = truncatePrompt(raw);
|
|
159
|
+
const primary = rec.models[0] ?? "unknown";
|
|
160
|
+
const extra = rec.models.length > 1 ? ` +${rec.models.length - 1}` : "";
|
|
161
|
+
const saMark = rec.subagents ? " +SA" : "";
|
|
162
|
+
let sa = null;
|
|
163
|
+
if (rec.subagents) {
|
|
164
|
+
const saModels = Object.keys(rec.subagents.costByModel).map((m) => modelDisplayName(m));
|
|
165
|
+
sa = {
|
|
166
|
+
usd: formatUSD(rec.subagents.costUSD),
|
|
167
|
+
jpy: formatJPY(rec.subagents.costUSD * rec.fxRate),
|
|
168
|
+
models: saModels.join(", "),
|
|
169
|
+
apiCalls: rec.subagents.apiCalls
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const ms = Date.parse(rec.ts);
|
|
173
|
+
return {
|
|
174
|
+
t: Number.isFinite(ms) ? ms : 0,
|
|
175
|
+
ts: fmtLocalDateTime(rec.ts),
|
|
176
|
+
p: basename(rec.project) || rec.project || "(unknown)",
|
|
177
|
+
pf: rec.project ?? "",
|
|
178
|
+
br: rec.gitBranch,
|
|
179
|
+
md: modelDisplayName(primary) + extra + saMark,
|
|
180
|
+
mr: rec.models,
|
|
181
|
+
ti: formatTokens(turnInputTokens(rec)),
|
|
182
|
+
to: formatTokens(turnOutputTokens(rec)),
|
|
183
|
+
um: rec.costUSD,
|
|
184
|
+
fx: rec.fxRate,
|
|
185
|
+
bs: turnBySlot(rec, map),
|
|
186
|
+
pr: text,
|
|
187
|
+
tr: truncated,
|
|
188
|
+
sa
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function computeKpis(turns) {
|
|
192
|
+
const now = /* @__PURE__ */ new Date();
|
|
193
|
+
const y = now.getFullYear();
|
|
194
|
+
const mo = now.getMonth();
|
|
195
|
+
const d = now.getDate();
|
|
196
|
+
const weekCutoff = now.getTime() - 7 * 864e5;
|
|
197
|
+
const today = emptyTotals();
|
|
198
|
+
const week = emptyTotals();
|
|
199
|
+
const month = emptyTotals();
|
|
200
|
+
const all = emptyTotals();
|
|
201
|
+
for (const rec of turns) {
|
|
202
|
+
const usd = turnTotalUSD(rec);
|
|
203
|
+
const jpy = turnTotalJPY(rec);
|
|
204
|
+
all.usd += usd;
|
|
205
|
+
all.jpy += jpy;
|
|
206
|
+
all.turns += 1;
|
|
207
|
+
const dt = new Date(rec.ts);
|
|
208
|
+
if (Number.isNaN(dt.getTime())) continue;
|
|
209
|
+
if (dt.getFullYear() === y && dt.getMonth() === mo && dt.getDate() === d) {
|
|
210
|
+
today.usd += usd;
|
|
211
|
+
today.jpy += jpy;
|
|
212
|
+
today.turns += 1;
|
|
213
|
+
}
|
|
214
|
+
if (dt.getTime() >= weekCutoff) {
|
|
215
|
+
week.usd += usd;
|
|
216
|
+
week.jpy += jpy;
|
|
217
|
+
week.turns += 1;
|
|
218
|
+
}
|
|
219
|
+
if (dt.getFullYear() === y && dt.getMonth() === mo) {
|
|
220
|
+
month.usd += usd;
|
|
221
|
+
month.jpy += jpy;
|
|
222
|
+
month.turns += 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { today, week, month, all };
|
|
226
|
+
}
|
|
227
|
+
var STYLE = `<style>
|
|
228
|
+
:root{
|
|
229
|
+
color-scheme: light dark;
|
|
230
|
+
--surface:#fcfcfb; --plane:#f9f9f7; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
|
|
231
|
+
--grid:#e1e0d9; --axis:#c3c2b7; --border:rgba(11,11,11,0.10); --good:#006300;
|
|
232
|
+
--s1:#2a78d6; --s2:#1baf7a; --s3:#eda100; --s4:#008300; --s5:#4a3aa7; --s6:#e34948; --s7:#e87ba4; --s8:#eb6834; --sother:#a7a69f;
|
|
233
|
+
--shadow: 0 1px 2px rgba(11,11,11,0.04), 0 1px 3px rgba(11,11,11,0.06);
|
|
234
|
+
}
|
|
235
|
+
@media (prefers-color-scheme: dark){
|
|
236
|
+
:root{
|
|
237
|
+
--surface:#1a1a19; --plane:#0d0d0d; --ink:#ffffff; --ink2:#c3c2b7; --muted:#898781;
|
|
238
|
+
--grid:#2c2c2a; --axis:#383835; --border:rgba(255,255,255,0.10); --good:#0ca30c;
|
|
239
|
+
--s1:#3987e5; --s2:#199e70; --s3:#c98500; --s4:#008300; --s5:#9085e9; --s6:#e66767; --s7:#d55181; --s8:#d95926; --sother:#6d6c66;
|
|
240
|
+
--shadow: 0 1px 2px rgba(0,0,0,0.4), 0 1px 3px rgba(0,0,0,0.5);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
*{box-sizing:border-box;}
|
|
244
|
+
html,body{margin:0;padding:0;}
|
|
245
|
+
body{background:var(--plane); color:var(--ink); font-family: system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; line-height:1.5; -webkit-font-smoothing:antialiased;}
|
|
246
|
+
.wrap{max-width:1120px; margin:0 auto; padding:24px 20px 72px;}
|
|
247
|
+
a{color:inherit;}
|
|
248
|
+
|
|
249
|
+
.head{display:flex; flex-wrap:wrap; gap:20px 32px; align-items:flex-end; justify-content:space-between; margin-bottom:24px;}
|
|
250
|
+
.head h1{font-size:22px; font-weight:650; margin:0 0 6px;}
|
|
251
|
+
.head .sub{color:var(--ink2); font-size:13px; margin:2px 0;}
|
|
252
|
+
.head .muted{color:var(--muted);}
|
|
253
|
+
.hero{text-align:right;}
|
|
254
|
+
.hero-label{color:var(--ink2); font-size:12px; letter-spacing:.02em; text-transform:uppercase;}
|
|
255
|
+
.hero-value{font-size:46px; font-weight:680; line-height:1.05; margin:2px 0;}
|
|
256
|
+
.hero-meta{color:var(--ink2); font-size:13px;}
|
|
257
|
+
|
|
258
|
+
.kpi{display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:28px;}
|
|
259
|
+
.stat{background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:16px 18px; box-shadow:var(--shadow);}
|
|
260
|
+
.stat-label{color:var(--ink2); font-size:13px; font-weight:600; display:flex; flex-wrap:wrap; align-items:baseline; gap:6px;}
|
|
261
|
+
.stat-sub{color:var(--muted); font-size:11px; font-weight:400;}
|
|
262
|
+
.stat-value{font-size:27px; font-weight:660; margin:8px 0 4px;}
|
|
263
|
+
.stat-meta{color:var(--muted); font-size:12px;}
|
|
264
|
+
|
|
265
|
+
section.card{background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:20px 22px; margin-bottom:22px; box-shadow:var(--shadow);}
|
|
266
|
+
section.card > h2{font-size:15px; font-weight:640; margin:0 0 4px;}
|
|
267
|
+
section.card > .note{color:var(--muted); font-size:12px; margin:0 0 14px;}
|
|
268
|
+
|
|
269
|
+
.toolbar-row{display:flex; flex-wrap:wrap; gap:10px 14px; align-items:center; margin:2px 0 12px;}
|
|
270
|
+
.seg-toggle{display:inline-flex; border:1px solid var(--border); border-radius:9px; overflow:hidden;}
|
|
271
|
+
.seg-toggle button{padding:6px 14px; background:var(--surface); color:var(--ink2); border:0; border-left:1px solid var(--border); cursor:pointer; font:inherit; font-size:13px;}
|
|
272
|
+
.seg-toggle button:first-child{border-left:0;}
|
|
273
|
+
.seg-toggle button.active{background:var(--s1); color:#fff;}
|
|
274
|
+
.btn{padding:6px 14px; border:1px solid var(--border); border-radius:9px; background:var(--surface); color:var(--ink2); cursor:pointer; font:inherit; font-size:13px;}
|
|
275
|
+
.btn.active{background:var(--s1); color:#fff; border-color:var(--s1);}
|
|
276
|
+
.sel-label{color:var(--ink2); font-size:13px; margin-left:auto; text-align:right; font-variant-numeric:tabular-nums;}
|
|
277
|
+
.sel-label b{color:var(--ink); font-weight:640;}
|
|
278
|
+
|
|
279
|
+
.legend{display:flex; flex-wrap:wrap; gap:8px 18px; margin:2px 0 14px;}
|
|
280
|
+
.legend-item{display:inline-flex; align-items:center; gap:7px; font-size:13px; color:var(--ink2);}
|
|
281
|
+
.swatch{display:inline-block; width:11px; height:11px; border-radius:3px; flex:0 0 auto;}
|
|
282
|
+
.k1{background:var(--s1);} .k2{background:var(--s2);} .k3{background:var(--s3);} .k4{background:var(--s4);}
|
|
283
|
+
.k5{background:var(--s5);} .k6{background:var(--s6);} .k7{background:var(--s7);} .k8{background:var(--s8);} .kother{background:var(--sother);}
|
|
284
|
+
|
|
285
|
+
.budget-bar{height:12px; background:var(--plane); border:1px solid var(--border); border-radius:7px; overflow:hidden; margin:12px 0;}
|
|
286
|
+
.budget-fill{height:100%; border-radius:6px; min-width:2px;}
|
|
287
|
+
.budget-fill.lvl-ok{background:var(--s2);}
|
|
288
|
+
.budget-fill.lvl-warn{background:var(--s3);}
|
|
289
|
+
.budget-fill.lvl-over{background:var(--s6);}
|
|
290
|
+
.budget-foot{display:flex; justify-content:space-between; align-items:baseline; gap:12px; flex-wrap:wrap; font-size:13px; color:var(--ink2);}
|
|
291
|
+
.budget-foot b{color:var(--ink); font-weight:660; font-variant-numeric:tabular-nums;}
|
|
292
|
+
.budget-pct{font-variant-numeric:tabular-nums; font-weight:640;}
|
|
293
|
+
.budget-pct.lvl-warn{color:var(--s3);}
|
|
294
|
+
.budget-pct.lvl-over{color:var(--s6);}
|
|
295
|
+
|
|
296
|
+
.chart{height:auto; display:block;}
|
|
297
|
+
.chart-scroll{overflow-x:auto;}
|
|
298
|
+
.seg.s1{fill:var(--s1);} .seg.s2{fill:var(--s2);} .seg.s3{fill:var(--s3);} .seg.s4{fill:var(--s4);}
|
|
299
|
+
.seg.s5{fill:var(--s5);} .seg.s6{fill:var(--s6);} .seg.s7{fill:var(--s7);} .seg.s8{fill:var(--s8);} .seg.sother{fill:var(--sother);}
|
|
300
|
+
.seg.dim{opacity:0.28;}
|
|
301
|
+
.grid-line{stroke:var(--grid); stroke-width:1;}
|
|
302
|
+
.axis-line{stroke:var(--axis); stroke-width:1;}
|
|
303
|
+
.tick-label{fill:var(--muted); font-size:11px; font-variant-numeric:tabular-nums;}
|
|
304
|
+
.bar-name{fill:var(--ink); font-size:13px; font-weight:550;}
|
|
305
|
+
.bar-val{fill:var(--ink2); font-size:12px; font-variant-numeric:tabular-nums;}
|
|
306
|
+
.cccn-band{fill:transparent; cursor:pointer;}
|
|
307
|
+
.cccn-band:hover{fill:var(--ink); fill-opacity:0.05;}
|
|
308
|
+
.cccn-band.sel{fill:var(--s1); fill-opacity:0.10;}
|
|
309
|
+
|
|
310
|
+
.grid2{display:grid; grid-template-columns:1.15fr 1fr; gap:22px; align-items:start;}
|
|
311
|
+
|
|
312
|
+
.data-table{width:100%; border-collapse:collapse; font-size:13px;}
|
|
313
|
+
.data-table th{ text-align:left; color:var(--muted); font-weight:600; font-size:12px; padding:6px 10px; border-bottom:1px solid var(--border); }
|
|
314
|
+
.data-table td{padding:7px 10px; border-bottom:1px solid var(--grid);}
|
|
315
|
+
.data-table tr:last-child td{border-bottom:none;}
|
|
316
|
+
.data-table .c-num{text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap;}
|
|
317
|
+
.data-table .c-name{display:flex; align-items:center; gap:8px;}
|
|
318
|
+
.table-twin{margin-top:14px;}
|
|
319
|
+
.table-twin summary{cursor:pointer; color:var(--ink2); font-size:12px; user-select:none;}
|
|
320
|
+
.table-twin[open] summary{margin-bottom:10px;}
|
|
321
|
+
|
|
322
|
+
.toolbar{display:flex; flex-wrap:wrap; gap:10px 16px; align-items:center; margin-bottom:14px;}
|
|
323
|
+
.search{flex:1 1 260px; min-width:200px; background:var(--plane); color:var(--ink); border:1px solid var(--border); border-radius:9px; padding:9px 12px; font-size:13px; font-family:inherit;}
|
|
324
|
+
.search::placeholder{color:var(--muted);}
|
|
325
|
+
.count{color:var(--muted); font-size:12px; white-space:nowrap;}
|
|
326
|
+
|
|
327
|
+
.turns{width:100%; border-collapse:collapse; font-size:13px;}
|
|
328
|
+
.turns th{ text-align:left; color:var(--muted); font-weight:600; font-size:12px; padding:8px 10px; border-bottom:1px solid var(--border); position:sticky; top:0; background:var(--surface); }
|
|
329
|
+
.turns td{padding:8px 10px; border-bottom:1px solid var(--grid); vertical-align:top;}
|
|
330
|
+
.turns .c-num{text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap;}
|
|
331
|
+
.turns .c-time,.turns .c-model{white-space:nowrap;}
|
|
332
|
+
.turns .c-proj{max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
|
|
333
|
+
.turns .c-prompt{color:var(--ink2);}
|
|
334
|
+
.turn-row{cursor:pointer;}
|
|
335
|
+
.turn-row:hover{background:color-mix(in srgb, var(--ink) 4%, transparent);}
|
|
336
|
+
.turn-row:focus-visible{outline:2px solid var(--s1); outline-offset:-2px;}
|
|
337
|
+
.turn-row.open{background:color-mix(in srgb, var(--ink) 5%, transparent);}
|
|
338
|
+
.turn-detail > td{background:var(--plane); color:var(--ink); border-bottom:1px solid var(--grid);}
|
|
339
|
+
.detail-meta{color:var(--muted); font-size:12px; margin-bottom:8px; word-break:break-all;}
|
|
340
|
+
.detail-prompt{white-space:pre-wrap; word-break:break-word; font-size:13px; line-height:1.55;}
|
|
341
|
+
.table-wrap{overflow-x:auto;}
|
|
342
|
+
.empty-hint{color:var(--muted); font-size:13px; padding:20px 0; text-align:center;}
|
|
343
|
+
|
|
344
|
+
.empty{background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:56px 24px; text-align:center; box-shadow:var(--shadow);}
|
|
345
|
+
.empty h2{font-size:20px; margin:0 0 8px;}
|
|
346
|
+
.empty p{color:var(--ink2); margin:4px 0;}
|
|
347
|
+
|
|
348
|
+
.foot{color:var(--muted); font-size:12px; margin-top:32px; text-align:center;}
|
|
349
|
+
|
|
350
|
+
.cccn-tip{position:fixed; z-index:50; pointer-events:none; background:var(--surface); color:var(--ink); border:1px solid var(--border); border-radius:10px; box-shadow:var(--shadow); padding:9px 11px; font-size:12px; max-width:280px;}
|
|
351
|
+
.cccn-tip .tip-title{font-weight:640; margin-bottom:6px; font-variant-numeric:tabular-nums;}
|
|
352
|
+
.cccn-tip .tip-row{display:flex; align-items:center; gap:8px; margin:2px 0;}
|
|
353
|
+
.cccn-tip .tip-key{display:inline-block; width:9px; height:9px; border-radius:2px; flex:0 0 auto;}
|
|
354
|
+
.cccn-tip .tip-name{color:var(--ink2); flex:1 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
|
|
355
|
+
.cccn-tip .tip-val{font-variant-numeric:tabular-nums; font-weight:560;}
|
|
356
|
+
|
|
357
|
+
@media (max-width: 780px){
|
|
358
|
+
.kpi{grid-template-columns:repeat(2,1fr);}
|
|
359
|
+
.grid2{grid-template-columns:1fr;}
|
|
360
|
+
.hero{text-align:left;}
|
|
361
|
+
.head{align-items:flex-start;}
|
|
362
|
+
.sel-label{margin-left:0;}
|
|
363
|
+
}
|
|
364
|
+
</style>`;
|
|
365
|
+
var APP_JS = `<script>
|
|
366
|
+
(function(){
|
|
367
|
+
var el = document.getElementById('cccn-data');
|
|
368
|
+
if(!el){ return; }
|
|
369
|
+
var data;
|
|
370
|
+
try { data = JSON.parse(el.textContent || '{}'); } catch(e){ return; }
|
|
371
|
+
var turns = data.turns || [];
|
|
372
|
+
var slots = data.slots || [];
|
|
373
|
+
var slotOrder = slots.map(function(s){ return s.slot; });
|
|
374
|
+
var slotName = {}; slots.forEach(function(s){ slotName[s.slot] = s.name; });
|
|
375
|
+
|
|
376
|
+
// ---- \u6570\u5024\u6574\u5F62(\u30B5\u30FC\u30D0\u306E format.ts \u3068\u7B49\u4FA1\u306B\u79FB\u690D)----
|
|
377
|
+
function formatUSD(n){
|
|
378
|
+
var v = n || 0;
|
|
379
|
+
if(v >= 1) return '$' + v.toFixed(2);
|
|
380
|
+
if(v >= 0.01) return '$' + v.toFixed(3);
|
|
381
|
+
return '$' + v.toFixed(4);
|
|
382
|
+
}
|
|
383
|
+
function formatJPY(n){
|
|
384
|
+
var v = n || 0;
|
|
385
|
+
if(v >= 1) return '\xA5' + Math.round(v).toLocaleString('en-US');
|
|
386
|
+
return '\xA5' + (Math.round(v * 10) / 10);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---- \u65E5\u4ED8/\u30D0\u30B1\u30C3\u30C8(\u30ED\u30FC\u30AB\u30EBTZ)----
|
|
390
|
+
function pad(n){ return (n < 10 ? '0' : '') + n; }
|
|
391
|
+
function dkey(d){ return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()); }
|
|
392
|
+
function mondayOf(d){
|
|
393
|
+
var x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
394
|
+
var wd = (x.getDay() + 6) % 7; // \u6708=0 .. \u65E5=6
|
|
395
|
+
x.setDate(x.getDate() - wd);
|
|
396
|
+
return x;
|
|
397
|
+
}
|
|
398
|
+
function bucketKey(t, gran){
|
|
399
|
+
var d = new Date(t);
|
|
400
|
+
if(isNaN(d.getTime())) return null;
|
|
401
|
+
if(gran === 'month') return d.getFullYear() + '-' + pad(d.getMonth()+1);
|
|
402
|
+
if(gran === 'week') return dkey(mondayOf(d));
|
|
403
|
+
return dkey(d);
|
|
404
|
+
}
|
|
405
|
+
function bucketLabel(key, gran){
|
|
406
|
+
var p = key.split('-');
|
|
407
|
+
if(gran === 'month') return p[0] + '/' + p[1];
|
|
408
|
+
return p[1] + '/' + p[2] + (gran === 'week' ? '\u9031' : '');
|
|
409
|
+
}
|
|
410
|
+
function periodText(key, gran){
|
|
411
|
+
if(key === null) return '\u901A\u7B97 / All time';
|
|
412
|
+
if(gran === 'month') return key + ' (\u6708)';
|
|
413
|
+
if(gran === 'week') return key + ' \u306E\u9031';
|
|
414
|
+
return key;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---- \u72B6\u614B(sessionStorage \u306B\u4FDD\u5B58\u3057\u3001\u81EA\u52D5\u30EA\u30ED\u30FC\u30C9(meta refresh)\u3092\u8DE8\u3044\u3067\u3082\u7DAD\u6301)----
|
|
418
|
+
function ssGet(k){ try { return sessionStorage.getItem(k); } catch(e){ return null; } }
|
|
419
|
+
function ssSet(k, v){ try { sessionStorage.setItem(k, v); } catch(e){} }
|
|
420
|
+
var GRAN = ssGet('cccn-gran'); if(['day','week','month'].indexOf(GRAN) < 0) GRAN = 'day';
|
|
421
|
+
// \u65E2\u5B9A\u306F\u300C\u8D77\u52D5\u3057\u305F\u65E5\u300D\u3092\u521D\u671F\u9078\u629E\u3059\u308B(\u672A\u4FDD\u5B58\u6642)\u3002\u660E\u793A\u7684\u306A\u901A\u7B97\u306F '__all__'\u3001\u305D\u306E\u4ED6\u306F\u4FDD\u5B58\u30AD\u30FC\u3002
|
|
422
|
+
var storedSel = ssGet('cccn-sel');
|
|
423
|
+
var SEL = (storedSel === null) ? defaultSel(GRAN) : (storedSel === '__all__' ? null : storedSel);
|
|
424
|
+
function setGran(g){ GRAN = g; ssSet('cccn-gran', g); }
|
|
425
|
+
function setSel(v){ SEL = v; ssSet('cccn-sel', v === null ? '__all__' : v); }
|
|
426
|
+
var lastRenderedGran = null; // \u76F4\u524D\u306B\u63CF\u753B\u3057\u305F\u7C92\u5EA6\u3002\u5909\u308F\u3063\u305F\u3068\u304D(=\u30D0\u30B1\u30C3\u30C8\u6570\u304C\u5909\u308F\u308B\u3068\u304D)\u3060\u3051\u53F3\u7AEF\u3078\u5BC4\u305B\u308B\u3002
|
|
427
|
+
|
|
428
|
+
function buildBuckets(gran){
|
|
429
|
+
var map = {}; var keys = [];
|
|
430
|
+
for(var i=0;i<turns.length;i++){
|
|
431
|
+
var tn = turns[i];
|
|
432
|
+
var k = bucketKey(tn.t, gran);
|
|
433
|
+
if(k === null) continue;
|
|
434
|
+
var b = map[k];
|
|
435
|
+
if(!b){ b = map[k] = { key:k, total:0, turns:0, bs:{} }; keys.push(k); }
|
|
436
|
+
b.turns++;
|
|
437
|
+
var bs = tn.bs || {};
|
|
438
|
+
for(var s in bs){ if(bs.hasOwnProperty(s)){ b.bs[s] = (b.bs[s]||0) + bs[s]; b.total += bs[s]; } }
|
|
439
|
+
}
|
|
440
|
+
keys.sort();
|
|
441
|
+
return keys.map(function(k){ return map[k]; });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// \u8D77\u52D5\u6642\u306E\u65E2\u5B9A\u9078\u629E: \u8D77\u52D5\u65E5(\u4ECA\u65E5)\u306B\u30C7\u30FC\u30BF\u304C\u3042\u308C\u3070\u305D\u306E\u65E5\u3001\u7121\u3051\u308C\u3070\u76F4\u8FD1\u306E\u30C7\u30FC\u30BF\u304C\u3042\u308B\u671F\u9593\u3002
|
|
445
|
+
// \u30C7\u30FC\u30BF\u304C\u5168\u304F\u7121\u3051\u308C\u3070 null(\u901A\u7B97)\u3002
|
|
446
|
+
function defaultSel(gran){
|
|
447
|
+
var buckets = buildBuckets(gran);
|
|
448
|
+
if(buckets.length === 0) return null;
|
|
449
|
+
var todayKey = bucketKey(Date.now(), gran);
|
|
450
|
+
for(var i=0;i<buckets.length;i++){ if(buckets[i].key === todayKey) return todayKey; }
|
|
451
|
+
return buckets[buckets.length - 1].key; // \u6700\u65B0\u306E\u30C7\u30FC\u30BF\u304C\u3042\u308B\u671F\u9593(\u30D0\u30B1\u30C3\u30C8\u306F\u6607\u9806\u30BD\u30FC\u30C8\u6E08\u307F)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function turnsInSelection(){
|
|
455
|
+
if(SEL === null) return turns;
|
|
456
|
+
return turns.filter(function(tn){ return bucketKey(tn.t, GRAN) === SEL; });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- SVG \u30D8\u30EB\u30D1\u30FC ----
|
|
460
|
+
var SVGNS = 'http://www.w3.org/2000/svg';
|
|
461
|
+
function svgEl(tag, attrs){
|
|
462
|
+
var e = document.createElementNS(SVGNS, tag);
|
|
463
|
+
if(attrs){ for(var k in attrs){ if(attrs.hasOwnProperty(k)) e.setAttribute(k, attrs[k]); } }
|
|
464
|
+
return e;
|
|
465
|
+
}
|
|
466
|
+
function niceCeil(max){
|
|
467
|
+
if(!(max > 0)) return 1;
|
|
468
|
+
var pow = Math.pow(10, Math.floor(Math.log10(max)));
|
|
469
|
+
var n = max / pow;
|
|
470
|
+
var nice = n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10;
|
|
471
|
+
return nice * pow;
|
|
472
|
+
}
|
|
473
|
+
function f2(n){ return Math.round(n * 100) / 100; }
|
|
474
|
+
function clearNode(node){ while(node.firstChild){ node.removeChild(node.firstChild); } }
|
|
475
|
+
|
|
476
|
+
// ---- \u65E5\u5225/\u9031\u5225/\u6708\u5225 \u30C1\u30E3\u30FC\u30C8 ----
|
|
477
|
+
var chartEl = document.getElementById('cccn-chart');
|
|
478
|
+
var tip = document.getElementById('cccn-tip');
|
|
479
|
+
|
|
480
|
+
function renderChart(){
|
|
481
|
+
if(!chartEl) return;
|
|
482
|
+
var scroller0 = chartEl.parentNode;
|
|
483
|
+
var prevScroll = scroller0 ? scroller0.scrollLeft : 0; // clearNode \u524D\u306B\u73FE\u5728\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u4F4D\u7F6E\u3092\u9000\u907F
|
|
484
|
+
clearNode(chartEl);
|
|
485
|
+
var buckets = buildBuckets(GRAN);
|
|
486
|
+
if(buckets.length === 0){
|
|
487
|
+
var p = document.createElement('p'); p.className = 'empty-hint';
|
|
488
|
+
p.textContent = '\u30C7\u30FC\u30BF\u304C\u3042\u308A\u307E\u305B\u3093';
|
|
489
|
+
chartEl.appendChild(p);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
var H = 320, ml = 58, mr = 16, mt = 18, mb = 40;
|
|
493
|
+
var plotH = H - mt - mb;
|
|
494
|
+
var baseY = mt + plotH;
|
|
495
|
+
var GAP = 2;
|
|
496
|
+
var n = buckets.length;
|
|
497
|
+
// \u5E2F\u5E45\u306F\u30B3\u30F3\u30C6\u30CA\u5E45\u304B\u3089\u6C7A\u3081\u308B: \u30D0\u30B1\u30C3\u30C8\u304C\u5C11\u306A\u3051\u308C\u3070\u6A2A\u5E45\u3044\u3063\u3071\u3044\u306B\u5E83\u3052\u3001\u591A\u3051\u308C\u3070\u6700\u5C0F\u5E45\u3067
|
|
498
|
+
// \u6A2A\u30B9\u30AF\u30ED\u30FC\u30EB\u3055\u305B\u308B(svg \u81EA\u8EAB\u306E px \u5E45\u3067\u63CF\u304F\u305F\u3081 .chart \u306F width:100% \u306B\u3057\u306A\u3044)\u3002
|
|
499
|
+
var scroller = chartEl.parentNode;
|
|
500
|
+
var avail = (scroller && scroller.clientWidth) ? scroller.clientWidth : 900;
|
|
501
|
+
var band = Math.max(28, Math.min(90, (avail - ml - mr) / n));
|
|
502
|
+
var barW = Math.min(26, band * 0.7);
|
|
503
|
+
var plotW = band * n;
|
|
504
|
+
var W = ml + plotW + mr;
|
|
505
|
+
|
|
506
|
+
var maxTotal = 0;
|
|
507
|
+
buckets.forEach(function(b){ if(b.total > maxTotal) maxTotal = b.total; });
|
|
508
|
+
var niceMax = niceCeil(maxTotal);
|
|
509
|
+
var yscale = plotH / niceMax;
|
|
510
|
+
// \u9078\u629E\u4E2D\u30D0\u30B1\u30C3\u30C8\u304C\u73FE\u5728\u306E\u7C92\u5EA6\u306B\u5B58\u5728\u3059\u308B\u3068\u304D\u3060\u3051\u4ED6\u3092\u6DE1\u8272\u5316\u3059\u308B(\u30C7\u30FC\u30BF\u7121\u3057\u306E\u65E5\u3092\u9078\u3076\u3068
|
|
511
|
+
// \u5168\u90E8\u6697\u304F\u306A\u308B\u306E\u3092\u907F\u3051\u308B)\u3002
|
|
512
|
+
var selPresent = false;
|
|
513
|
+
for(var sp=0; sp<buckets.length; sp++){ if(buckets[sp].key === SEL){ selPresent = true; break; } }
|
|
514
|
+
|
|
515
|
+
var svg = svgEl('svg', { 'class':'chart', viewBox:'0 0 ' + W + ' ' + H, role:'img',
|
|
516
|
+
'aria-label':'\u30B3\u30B9\u30C8\u7A4D\u307F\u4E0A\u3052\u68D2\u30B0\u30E9\u30D5', preserveAspectRatio:'xMidYMid meet' });
|
|
517
|
+
svg.setAttribute('width', String(W));
|
|
518
|
+
svg.setAttribute('height', String(H));
|
|
519
|
+
|
|
520
|
+
var ticks = 5;
|
|
521
|
+
for(var t=0;t<=ticks;t++){
|
|
522
|
+
var val = niceMax * t / ticks;
|
|
523
|
+
var yy = f2(baseY - val * yscale);
|
|
524
|
+
svg.appendChild(svgEl('line', { 'class':'grid-line', x1:ml, y1:yy, x2:W-mr, y2:yy }));
|
|
525
|
+
var lbl = svgEl('text', { 'class':'tick-label', x:ml-8, y:yy+4, 'text-anchor':'end' });
|
|
526
|
+
lbl.textContent = formatUSD(val);
|
|
527
|
+
svg.appendChild(lbl);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// \u30E9\u30D9\u30EB\u306F\u5E45\u3067\u9593\u5F15\u304F(\u5E2F\u5E45\u3088\u308A\u30E9\u30D9\u30EB\u304C\u5E83\u3044\u3068\u91CD\u306A\u308B\u305F\u3081)\u3002
|
|
531
|
+
var labelPx = bucketLabel(buckets[0].key, GRAN).length * 7.5 + 8;
|
|
532
|
+
var xStep = Math.max(1, Math.ceil(labelPx / band));
|
|
533
|
+
for(var i=0;i<n;i++){
|
|
534
|
+
var b = buckets[i];
|
|
535
|
+
var cx = ml + band * (i + 0.5);
|
|
536
|
+
var x = cx - barW/2;
|
|
537
|
+
var dim = (SEL !== null && selPresent && b.key !== SEL);
|
|
538
|
+
var cumV = 0;
|
|
539
|
+
for(var j=0;j<slotOrder.length;j++){
|
|
540
|
+
var slot = slotOrder[j];
|
|
541
|
+
var v = b.bs[slot] || 0;
|
|
542
|
+
if(v <= 0) continue;
|
|
543
|
+
var vTop = cumV + v;
|
|
544
|
+
var yTop = baseY - vTop * yscale;
|
|
545
|
+
var yBot = baseY - cumV * yscale;
|
|
546
|
+
if(cumV > 0) yBot -= GAP;
|
|
547
|
+
cumV = vTop;
|
|
548
|
+
var h = yBot - yTop;
|
|
549
|
+
if(h <= 0.4) continue;
|
|
550
|
+
var rect = svgEl('rect', { 'class':'seg s'+slot+(dim?' dim':''), x:f2(x), y:f2(yTop), width:f2(barW), height:f2(h) });
|
|
551
|
+
svg.appendChild(rect);
|
|
552
|
+
}
|
|
553
|
+
if(i % xStep === 0 || i === n-1){
|
|
554
|
+
var xl = svgEl('text', { 'class':'tick-label', x:f2(cx), y:baseY+16, 'text-anchor':'middle' });
|
|
555
|
+
xl.textContent = bucketLabel(b.key, GRAN);
|
|
556
|
+
svg.appendChild(xl);
|
|
557
|
+
}
|
|
558
|
+
var band2 = svgEl('rect', { 'class':'cccn-band'+(b.key===SEL?' sel':''), x:f2(ml+band*i), y:mt, width:f2(band), height:plotH });
|
|
559
|
+
band2.setAttribute('data-i', String(i));
|
|
560
|
+
(function(bucket){
|
|
561
|
+
band2.addEventListener('pointermove', function(e){ showTip(e, bucket); });
|
|
562
|
+
band2.addEventListener('pointerenter', function(e){ showTip(e, bucket); });
|
|
563
|
+
band2.addEventListener('pointerleave', function(){ if(tip) tip.hidden = true; });
|
|
564
|
+
band2.addEventListener('click', function(){
|
|
565
|
+
setSel(SEL === bucket.key ? null : bucket.key);
|
|
566
|
+
if(tip) tip.hidden = true;
|
|
567
|
+
render();
|
|
568
|
+
});
|
|
569
|
+
})(b);
|
|
570
|
+
svg.appendChild(band2);
|
|
571
|
+
}
|
|
572
|
+
svg.appendChild(svgEl('line', { 'class':'axis-line', x1:ml, y1:baseY, x2:W-mr, y2:baseY }));
|
|
573
|
+
chartEl.appendChild(svg);
|
|
574
|
+
// \u30B9\u30AF\u30ED\u30FC\u30EB\u4F4D\u7F6E: \u521D\u56DE\u306F\u30EA\u30ED\u30FC\u30C9\u8DE8\u304E\u3067\u5FA9\u5143(\u7121\u3051\u308C\u3070\u53F3\u7AEF=\u6700\u65B0)\u3001\u7C92\u5EA6\u304C\u5909\u308F\u3063\u305F\u3089\u53F3\u7AEF\u3001
|
|
575
|
+
// \u305D\u308C\u4EE5\u5916(\u9078\u629E\u306E\u5909\u66F4\u306A\u3069\u540C\u4E00\u7C92\u5EA6\u306E\u518D\u63CF\u753B)\u306F\u76F4\u524D\u306E\u30B9\u30AF\u30ED\u30FC\u30EB\u4F4D\u7F6E\u3092\u7DAD\u6301\u3059\u308B\u3002
|
|
576
|
+
var scroller = chartEl.parentNode;
|
|
577
|
+
if(scroller){
|
|
578
|
+
if(lastRenderedGran === null){
|
|
579
|
+
var saved = parseInt(ssGet('cccn-chart-scroll'), 10);
|
|
580
|
+
scroller.scrollLeft = isNaN(saved) ? scroller.scrollWidth : saved;
|
|
581
|
+
} else if(GRAN !== lastRenderedGran){
|
|
582
|
+
scroller.scrollLeft = scroller.scrollWidth;
|
|
583
|
+
} else {
|
|
584
|
+
scroller.scrollLeft = prevScroll;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
lastRenderedGran = GRAN;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function showTip(evt, bucket){
|
|
591
|
+
if(!tip) return;
|
|
592
|
+
clearNode(tip);
|
|
593
|
+
var h = document.createElement('div'); h.className = 'tip-title';
|
|
594
|
+
h.textContent = periodText(bucket.key, GRAN) + ' \u5408\u8A08 ' + formatUSD(bucket.total) + ' \xB7 ' + bucket.turns + ' \u30BF\u30FC\u30F3';
|
|
595
|
+
tip.appendChild(h);
|
|
596
|
+
for(var j=0;j<slotOrder.length;j++){
|
|
597
|
+
var slot = slotOrder[j];
|
|
598
|
+
var v = bucket.bs[slot] || 0;
|
|
599
|
+
if(v <= 0) continue;
|
|
600
|
+
var row = document.createElement('div'); row.className = 'tip-row';
|
|
601
|
+
var key = document.createElement('span'); key.className = 'tip-key k'+slot;
|
|
602
|
+
var name = document.createElement('span'); name.className = 'tip-name'; name.textContent = slotName[slot] || slot;
|
|
603
|
+
var val = document.createElement('span'); val.className = 'tip-val'; val.textContent = formatUSD(v);
|
|
604
|
+
row.appendChild(key); row.appendChild(name); row.appendChild(val);
|
|
605
|
+
tip.appendChild(row);
|
|
606
|
+
}
|
|
607
|
+
tip.hidden = false;
|
|
608
|
+
positionTip(evt);
|
|
609
|
+
}
|
|
610
|
+
function positionTip(evt){
|
|
611
|
+
if(!tip) return;
|
|
612
|
+
var pad = 14; var r = tip.getBoundingClientRect();
|
|
613
|
+
var x = evt.clientX + pad, y = evt.clientY + pad;
|
|
614
|
+
if(x + r.width > window.innerWidth - 8){ x = evt.clientX - r.width - pad; }
|
|
615
|
+
if(y + r.height > window.innerHeight - 8){ y = evt.clientY - r.height - pad; }
|
|
616
|
+
tip.style.left = (x < 8 ? 8 : x) + 'px';
|
|
617
|
+
tip.style.top = (y < 8 ? 8 : y) + 'px';
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ---- \u30E2\u30C7\u30EB\u5225\u5185\u8A33 ----
|
|
621
|
+
var modelEl = document.getElementById('cccn-bymodel');
|
|
622
|
+
function renderByModel(sub){
|
|
623
|
+
if(!modelEl) return;
|
|
624
|
+
clearNode(modelEl);
|
|
625
|
+
var usd = {}, jpy = {}, tcount = {}, total = 0;
|
|
626
|
+
sub.forEach(function(tn){
|
|
627
|
+
var bs = tn.bs || {}, fx = tn.fx || 0;
|
|
628
|
+
for(var s in bs){ if(bs.hasOwnProperty(s) && bs[s] > 0){
|
|
629
|
+
usd[s] = (usd[s]||0) + bs[s]; jpy[s] = (jpy[s]||0) + bs[s]*fx; total += bs[s];
|
|
630
|
+
tcount[s] = (tcount[s]||0) + 1;
|
|
631
|
+
} }
|
|
632
|
+
});
|
|
633
|
+
var table = document.createElement('table'); table.className = 'data-table';
|
|
634
|
+
var thead = document.createElement('thead');
|
|
635
|
+
thead.innerHTML = '<tr><th>\u30E2\u30C7\u30EB / Model</th><th class="c-num">\u30BF\u30FC\u30F3</th><th class="c-num">$</th><th class="c-num">\xA5</th><th class="c-num">\u69CB\u6210\u6BD4</th></tr>';
|
|
636
|
+
table.appendChild(thead);
|
|
637
|
+
var tbody = document.createElement('tbody');
|
|
638
|
+
var any = false;
|
|
639
|
+
for(var j=0;j<slotOrder.length;j++){
|
|
640
|
+
var slot = slotOrder[j];
|
|
641
|
+
if(!(usd[slot] > 0)) continue;
|
|
642
|
+
any = true;
|
|
643
|
+
var share = total > 0 ? (usd[slot]/total*100) : 0;
|
|
644
|
+
var tr = document.createElement('tr');
|
|
645
|
+
var c1 = document.createElement('td'); c1.className = 'c-name';
|
|
646
|
+
var sw = document.createElement('span'); sw.className = 'swatch k'+slot; c1.appendChild(sw);
|
|
647
|
+
c1.appendChild(document.createTextNode(slotName[slot] || slot));
|
|
648
|
+
var c2 = document.createElement('td'); c2.className = 'c-num'; c2.textContent = String(tcount[slot]||0);
|
|
649
|
+
var c3 = document.createElement('td'); c3.className = 'c-num'; c3.textContent = formatUSD(usd[slot]);
|
|
650
|
+
var c4 = document.createElement('td'); c4.className = 'c-num'; c4.textContent = formatJPY(jpy[slot]);
|
|
651
|
+
var c5 = document.createElement('td'); c5.className = 'c-num'; c5.textContent = share.toFixed(1) + '%';
|
|
652
|
+
tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); tr.appendChild(c4); tr.appendChild(c5);
|
|
653
|
+
tbody.appendChild(tr);
|
|
654
|
+
}
|
|
655
|
+
table.appendChild(tbody);
|
|
656
|
+
if(!any){ var p = document.createElement('p'); p.className='empty-hint'; p.textContent='\u3053\u306E\u671F\u9593\u306E\u30C7\u30FC\u30BF\u306F\u3042\u308A\u307E\u305B\u3093'; modelEl.appendChild(p); return; }
|
|
657
|
+
modelEl.appendChild(table);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ---- \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5225 ----
|
|
661
|
+
var projEl = document.getElementById('cccn-byproject');
|
|
662
|
+
function renderByProject(sub){
|
|
663
|
+
if(!projEl) return;
|
|
664
|
+
clearNode(projEl);
|
|
665
|
+
var agg = {};
|
|
666
|
+
sub.forEach(function(tn){
|
|
667
|
+
var bs = tn.bs || {}, fx = tn.fx || 0, total = 0;
|
|
668
|
+
for(var s in bs){ if(bs.hasOwnProperty(s)) total += bs[s]; }
|
|
669
|
+
var name = tn.p || '(unknown)';
|
|
670
|
+
var a = agg[name] || (agg[name] = { usd:0, jpy:0, turns:0 });
|
|
671
|
+
a.usd += total; a.jpy += total*fx; a.turns++;
|
|
672
|
+
});
|
|
673
|
+
var rows = Object.keys(agg).map(function(name){ var a = agg[name]; return { name:name, usd:a.usd, jpy:a.jpy, turns:a.turns }; });
|
|
674
|
+
rows.sort(function(a,b){ return b.usd - a.usd || a.name.localeCompare(b.name); });
|
|
675
|
+
if(rows.length === 0){ var p = document.createElement('p'); p.className='empty-hint'; p.textContent='\u3053\u306E\u671F\u9593\u306E\u30C7\u30FC\u30BF\u306F\u3042\u308A\u307E\u305B\u3093'; projEl.appendChild(p); return; }
|
|
676
|
+
var table = document.createElement('table'); table.className = 'data-table';
|
|
677
|
+
var thead = document.createElement('thead');
|
|
678
|
+
thead.innerHTML = '<tr><th>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8 / Project</th><th class="c-num">\u30BF\u30FC\u30F3</th><th class="c-num">$</th><th class="c-num">\xA5</th></tr>';
|
|
679
|
+
table.appendChild(thead);
|
|
680
|
+
var tbody = document.createElement('tbody');
|
|
681
|
+
rows.forEach(function(r){
|
|
682
|
+
var tr = document.createElement('tr');
|
|
683
|
+
var c1 = document.createElement('td'); c1.className='c-name'; c1.textContent = r.name;
|
|
684
|
+
var c2 = document.createElement('td'); c2.className='c-num'; c2.textContent = String(r.turns);
|
|
685
|
+
var c3 = document.createElement('td'); c3.className='c-num'; c3.textContent = formatUSD(r.usd);
|
|
686
|
+
var c4 = document.createElement('td'); c4.className='c-num'; c4.textContent = formatJPY(r.jpy);
|
|
687
|
+
tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); tr.appendChild(c4);
|
|
688
|
+
tbody.appendChild(tr);
|
|
689
|
+
});
|
|
690
|
+
table.appendChild(tbody);
|
|
691
|
+
projEl.appendChild(table);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ---- \u6708\u4E88\u7B97(\u9078\u629E\u4E2D\u306E\u6708\u306B\u9023\u52D5)----
|
|
695
|
+
var budgetEl = document.getElementById('cccn-budget');
|
|
696
|
+
var BUDGET = +data.budget || 0;
|
|
697
|
+
var BUDGET_RATE = +data.budgetRate || 0;
|
|
698
|
+
function monthKeyOf(t){ var d = new Date(t); if(isNaN(d.getTime())) return null; return d.getFullYear() + '-' + pad(d.getMonth()+1); }
|
|
699
|
+
// \u4E88\u7B97\u306F\u300C\u6708\u300D\u5358\u4F4D\u3002\u9078\u629E\u4E2D\u30D0\u30B1\u30C3\u30C8\u304C\u5C5E\u3059\u308B\u66A6\u6708\u3092\u5BFE\u8C61\u306B\u3059\u308B(\u901A\u7B97\u306E\u3068\u304D\u306F\u4ECA\u6708)\u3002
|
|
700
|
+
function targetMonthKey(){
|
|
701
|
+
if(SEL === null) return monthKeyOf(Date.now());
|
|
702
|
+
if(GRAN === 'month') return SEL; // \u65E2\u306B YYYY-MM
|
|
703
|
+
return SEL.slice(0, 7); // YYYY-MM-DD -> YYYY-MM
|
|
704
|
+
}
|
|
705
|
+
function renderBudget(){
|
|
706
|
+
if(!budgetEl || !(BUDGET > 0)) return;
|
|
707
|
+
var mk = targetMonthKey();
|
|
708
|
+
var usd = 0, jpy = 0;
|
|
709
|
+
for(var i=0;i<turns.length;i++){
|
|
710
|
+
var tn = turns[i];
|
|
711
|
+
if(monthKeyOf(tn.t) !== mk) continue;
|
|
712
|
+
var bs = tn.bs || {}, fx = tn.fx || 0;
|
|
713
|
+
for(var s in bs){ if(bs.hasOwnProperty(s)){ usd += bs[s]; jpy += bs[s]*fx; } }
|
|
714
|
+
}
|
|
715
|
+
var pct = BUDGET > 0 ? (usd / BUDGET) * 100 : 0;
|
|
716
|
+
var width = Math.max(0, Math.min(100, pct));
|
|
717
|
+
var level = pct >= 100 ? 'over' : pct >= 70 ? 'warn' : 'ok';
|
|
718
|
+
var budgetJpy = BUDGET * BUDGET_RATE;
|
|
719
|
+
var curMonth = monthKeyOf(Date.now());
|
|
720
|
+
var monthShort = (mk === curMonth) ? '\u4ECA\u6708' : mk;
|
|
721
|
+
|
|
722
|
+
clearNode(budgetEl);
|
|
723
|
+
var h = document.createElement('h2');
|
|
724
|
+
h.textContent = '\u6708\u4E88\u7B97 / Monthly budget';
|
|
725
|
+
var sub = document.createElement('span'); sub.className = 'stat-sub';
|
|
726
|
+
sub.textContent = ' ' + mk + (mk === curMonth ? '(\u4ECA\u6708)' : '');
|
|
727
|
+
h.appendChild(sub);
|
|
728
|
+
budgetEl.appendChild(h);
|
|
729
|
+
|
|
730
|
+
var bar = document.createElement('div'); bar.className = 'budget-bar';
|
|
731
|
+
var fill = document.createElement('div'); fill.className = 'budget-fill lvl-' + level;
|
|
732
|
+
fill.style.width = width.toFixed(1) + '%';
|
|
733
|
+
bar.appendChild(fill); budgetEl.appendChild(bar);
|
|
734
|
+
|
|
735
|
+
var foot = document.createElement('div'); foot.className = 'budget-foot';
|
|
736
|
+
var left = document.createElement('span');
|
|
737
|
+
left.appendChild(document.createTextNode(monthShort + ' '));
|
|
738
|
+
var b = document.createElement('b'); b.textContent = formatUSD(usd); left.appendChild(b);
|
|
739
|
+
left.appendChild(document.createTextNode(' / ' + formatUSD(BUDGET) + ' '));
|
|
740
|
+
var mut = document.createElement('span'); mut.className = 'muted';
|
|
741
|
+
mut.textContent = '(' + formatJPY(jpy) + ' / ' + formatJPY(budgetJpy) + ')';
|
|
742
|
+
left.appendChild(mut);
|
|
743
|
+
var pctEl = document.createElement('span'); pctEl.className = 'budget-pct lvl-' + level;
|
|
744
|
+
pctEl.textContent = pct.toFixed(1) + '% used';
|
|
745
|
+
foot.appendChild(left); foot.appendChild(pctEl);
|
|
746
|
+
budgetEl.appendChild(foot);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ---- \u30BF\u30FC\u30F3\u5C65\u6B74 ----
|
|
750
|
+
// \u5168\u671F\u9593\u3060\u3068\u6570\u5343\u884C\u306B\u306A\u308A\u30DA\u30FC\u30B8\u304C\u6975\u7AEF\u306B\u9577\u304F\u30FB\u91CD\u304F\u306A\u308B\u305F\u3081\u3001DOM \u63CF\u753B\u306F\u6700\u5927 HIST_CAP \u4EF6\u306B\u6291\u3048\u308B\u3002
|
|
751
|
+
// \u691C\u7D22\u306F\u9078\u629E\u4E2D\u306E\u5168\u30BF\u30FC\u30F3\u3092\u5BFE\u8C61\u306B\u3057\u3001\u4E00\u81F4\u4EF6\u6570\u3092\u4EF6\u6570\u8868\u793A\u306B\u51FA\u3059(\u5148\u982D HIST_CAP \u4EF6\u3060\u3051\u63CF\u753B)\u3002
|
|
752
|
+
var HIST_CAP = 200;
|
|
753
|
+
var tbody = document.getElementById('turn-body');
|
|
754
|
+
var search = document.getElementById('turn-search');
|
|
755
|
+
var count = document.getElementById('turn-count');
|
|
756
|
+
var histData = []; // \u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3(\u65B0\u3057\u3044\u9806)
|
|
757
|
+
|
|
758
|
+
function matchStr(t){
|
|
759
|
+
return ((t.pr||'') + ' ' + (t.p||'') + ' ' + (t.pf||'') + ' ' + (t.md||'') + ' ' + ((t.mr||[]).join(' '))).toLowerCase();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function buildRow(t){
|
|
763
|
+
var tr = document.createElement('tr');
|
|
764
|
+
tr.className = 'turn-row'; tr.tabIndex = 0; tr.title = '\u30AF\u30EA\u30C3\u30AF\u3067\u5168\u6587\u8868\u793A / click to expand';
|
|
765
|
+
function cell(text, cls){ var td = document.createElement('td'); if(cls) td.className = cls; td.textContent = text; tr.appendChild(td); return td; }
|
|
766
|
+
cell(t.ts || '', 'c-time');
|
|
767
|
+
cell(t.p || '', 'c-proj');
|
|
768
|
+
cell(t.md || '', 'c-model');
|
|
769
|
+
cell(t.ti || '', 'c-num');
|
|
770
|
+
cell(t.to || '', 'c-num');
|
|
771
|
+
cell(formatUSD(t.um || 0), 'c-num');
|
|
772
|
+
cell(formatJPY((t.um || 0) * (t.fx || 0)), 'c-num');
|
|
773
|
+
var full = t.pr || '';
|
|
774
|
+
var head = full.length > 80 ? full.slice(0,80) + '\u2026' : full;
|
|
775
|
+
cell(head.length ? head : '(\u30D7\u30ED\u30F3\u30D7\u30C8\u306A\u3057)', 'c-prompt');
|
|
776
|
+
|
|
777
|
+
var dtr = document.createElement('tr'); dtr.className = 'turn-detail'; dtr.hidden = true;
|
|
778
|
+
var dtd = document.createElement('td'); dtd.colSpan = 8;
|
|
779
|
+
var meta = document.createElement('div'); meta.className = 'detail-meta';
|
|
780
|
+
var mp = [];
|
|
781
|
+
if(t.pf) mp.push(t.pf);
|
|
782
|
+
if(t.br) mp.push(t.br);
|
|
783
|
+
if(t.mr && t.mr.length) mp.push(t.mr.join(', '));
|
|
784
|
+
meta.textContent = mp.join(' \xB7 ');
|
|
785
|
+
dtd.appendChild(meta);
|
|
786
|
+
if(t.sa){
|
|
787
|
+
var saLine = document.createElement('div'); saLine.className = 'detail-meta';
|
|
788
|
+
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);
|
|
789
|
+
dtd.appendChild(saLine);
|
|
790
|
+
}
|
|
791
|
+
var pre = document.createElement('div'); pre.className = 'detail-prompt';
|
|
792
|
+
pre.textContent = full.length ? full : '(\u30D7\u30ED\u30F3\u30D7\u30C8\u306A\u3057)';
|
|
793
|
+
dtd.appendChild(pre); dtr.appendChild(dtd);
|
|
794
|
+
function toggle(){ dtr.hidden = !dtr.hidden; if(dtr.hidden) tr.classList.remove('open'); else tr.classList.add('open'); }
|
|
795
|
+
tr.addEventListener('click', toggle);
|
|
796
|
+
tr.addEventListener('keydown', function(e){ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); toggle(); } });
|
|
797
|
+
tbody.appendChild(tr); tbody.appendChild(dtr);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function renderHistory(sub){
|
|
801
|
+
if(!tbody) return;
|
|
802
|
+
histData = sub.slice().sort(function(a,b){ return b.t - a.t; });
|
|
803
|
+
applyFilter();
|
|
804
|
+
}
|
|
805
|
+
function applyFilter(){
|
|
806
|
+
if(!tbody) return;
|
|
807
|
+
clearNode(tbody);
|
|
808
|
+
var q = ((search && search.value) || '').trim().toLowerCase();
|
|
809
|
+
var matched = 0, shown = 0;
|
|
810
|
+
for(var i=0;i<histData.length;i++){
|
|
811
|
+
var t = histData[i];
|
|
812
|
+
if(q && matchStr(t).indexOf(q) < 0) continue;
|
|
813
|
+
matched++;
|
|
814
|
+
if(shown < HIST_CAP){ buildRow(t); shown++; }
|
|
815
|
+
}
|
|
816
|
+
if(count){ count.textContent = (matched > shown) ? (matched + ' \u4EF6\u4E2D ' + shown + ' \u4EF6\u3092\u8868\u793A') : (matched + ' \u4EF6'); }
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---- \u9078\u629E\u30E9\u30D9\u30EB\u30FB\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB ----
|
|
820
|
+
var selLabelEl = document.getElementById('cccn-sel-label');
|
|
821
|
+
function updateSelLabel(sub){
|
|
822
|
+
if(!selLabelEl) return;
|
|
823
|
+
var usd = 0, jpy = 0;
|
|
824
|
+
sub.forEach(function(tn){ var fx = tn.fx||0; var bs = tn.bs||{}; for(var s in bs){ if(bs.hasOwnProperty(s)){ usd += bs[s]; jpy += bs[s]*fx; } } });
|
|
825
|
+
clearNode(selLabelEl);
|
|
826
|
+
var b = document.createElement('b'); b.textContent = periodText(SEL, GRAN);
|
|
827
|
+
selLabelEl.appendChild(b);
|
|
828
|
+
selLabelEl.appendChild(document.createTextNode(' ' + formatUSD(usd) + ' \xB7 ' + formatJPY(jpy) + ' \xB7 ' + sub.length + ' \u30BF\u30FC\u30F3'));
|
|
829
|
+
}
|
|
830
|
+
function updateControls(){
|
|
831
|
+
var gbtns = document.querySelectorAll('[data-gran]');
|
|
832
|
+
for(var i=0;i<gbtns.length;i++){ gbtns[i].classList.toggle('active', gbtns[i].getAttribute('data-gran') === GRAN); }
|
|
833
|
+
var allBtn = document.getElementById('cccn-all');
|
|
834
|
+
if(allBtn) allBtn.classList.toggle('active', SEL === null);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// ---- \u5168\u4F53\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0 ----
|
|
838
|
+
function render(){
|
|
839
|
+
var sub = turnsInSelection();
|
|
840
|
+
renderChart();
|
|
841
|
+
renderBudget();
|
|
842
|
+
renderByModel(sub);
|
|
843
|
+
renderByProject(sub);
|
|
844
|
+
renderHistory(sub);
|
|
845
|
+
updateSelLabel(sub);
|
|
846
|
+
updateControls();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// \u30A4\u30D9\u30F3\u30C8\u914D\u7DDA\u3002
|
|
850
|
+
var gbtns = document.querySelectorAll('[data-gran]');
|
|
851
|
+
for(var i=0;i<gbtns.length;i++){
|
|
852
|
+
(function(btn){ btn.addEventListener('click', function(){ setGran(btn.getAttribute('data-gran')); setSel(null); render(); }); })(gbtns[i]);
|
|
853
|
+
}
|
|
854
|
+
var allBtn = document.getElementById('cccn-all');
|
|
855
|
+
if(allBtn) allBtn.addEventListener('click', function(){ setSel(null); render(); });
|
|
856
|
+
if(search){
|
|
857
|
+
var saved = ssGet('cccn-search');
|
|
858
|
+
if(saved !== null) search.value = saved;
|
|
859
|
+
search.addEventListener('input', function(){ ssSet('cccn-search', search.value); applyFilter(); });
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
render();
|
|
863
|
+
|
|
864
|
+
// \u81EA\u52D5\u30EA\u30ED\u30FC\u30C9\u3092\u8DE8\u3044\u3067\u30B9\u30AF\u30ED\u30FC\u30EB\u4F4D\u7F6E\u3092\u7DAD\u6301\u3059\u308B(\u30ED\u30FC\u30C9\u6642\u306B\u5FA9\u5143 \u2192 \u4EE5\u5F8C\u306F\u4FDD\u5B58\u306E\u307F)\u3002
|
|
865
|
+
var savedScroll = ssGet('cccn-scroll');
|
|
866
|
+
if(savedScroll !== null){ var sy = parseInt(savedScroll, 10); if(!isNaN(sy)){ try { window.scrollTo(0, sy); } catch(e){} } }
|
|
867
|
+
window.addEventListener('scroll', function(){ ssSet('cccn-scroll', String(window.scrollY)); });
|
|
868
|
+
|
|
869
|
+
// \u30C1\u30E3\u30FC\u30C8\u306E\u6A2A\u30B9\u30AF\u30ED\u30FC\u30EB\u4F4D\u7F6E\u3082\u4FDD\u5B58\u3059\u308B(\u30EA\u30ED\u30FC\u30C9\u8DE8\u304E\u306E\u5FA9\u5143\u7528\u3002\u5FA9\u5143\u306F renderChart \u306E\u521D\u56DE\u3067\u884C\u3046)\u3002
|
|
870
|
+
var chartScroller = chartEl ? chartEl.parentNode : null;
|
|
871
|
+
if(chartScroller){ chartScroller.addEventListener('scroll', function(){ ssSet('cccn-chart-scroll', String(chartScroller.scrollLeft)); }); }
|
|
872
|
+
})();
|
|
873
|
+
</script>`;
|
|
874
|
+
function statCard(label, sub, totals) {
|
|
875
|
+
return `<div class="stat"><div class="stat-label">${esc(label)}<span class="stat-sub">${esc(sub)}</span></div><div class="stat-value">${esc(formatUSD(totals.usd))}</div><div class="stat-meta">${esc(formatJPY(totals.jpy))} \xB7 ${totals.turns} \u30BF\u30FC\u30F3</div></div>`;
|
|
876
|
+
}
|
|
877
|
+
function renderLegend(slots) {
|
|
878
|
+
if (slots.length < 2) return "";
|
|
879
|
+
const items = slots.map((m) => `<span class="legend-item"><span class="swatch k${m.slot}"></span>${esc(m.name)}</span>`).join("");
|
|
880
|
+
return `<div class="legend">${items}</div>`;
|
|
881
|
+
}
|
|
882
|
+
function budgetCard(budgetUSD, month, fallbackRate) {
|
|
883
|
+
if (!(budgetUSD > 0)) return "";
|
|
884
|
+
const pct = month.usd / budgetUSD * 100;
|
|
885
|
+
const width = Math.max(0, Math.min(100, pct));
|
|
886
|
+
const level = pct >= 100 ? "over" : pct >= 70 ? "warn" : "ok";
|
|
887
|
+
const budgetJpy = budgetUSD * fallbackRate;
|
|
888
|
+
return `<section class="card" id="cccn-budget"><h2>\u6708\u4E88\u7B97 / Monthly budget<span class="stat-sub"> \u4ECA\u6708(\u66A6\u6708)</span></h2><div class="budget-bar"><div class="budget-fill lvl-${level}" style="width:${width.toFixed(1)}%"></div></div><div class="budget-foot"><span>\u4ECA\u6708 <b>${esc(formatUSD(month.usd))}</b> / ${esc(formatUSD(budgetUSD))} <span class="muted">(${esc(formatJPY(month.jpy))} / ${esc(formatJPY(budgetJpy))})</span></span><span class="budget-pct lvl-${level}">${pct.toFixed(1)}% used</span></div></section>`;
|
|
889
|
+
}
|
|
890
|
+
function escapeJsonForScript(json) {
|
|
891
|
+
return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
892
|
+
}
|
|
893
|
+
function renderDashboard(turns, opts) {
|
|
894
|
+
const version = readVersion();
|
|
895
|
+
const generatedAt = fmtLocalDateTime((/* @__PURE__ */ new Date()).toISOString());
|
|
896
|
+
const period = opts.days === null ? "\u5168\u671F\u9593" : `\u76F4\u8FD1 ${opts.days} \u65E5\u9593`;
|
|
897
|
+
const map = computeSlotMap(turns);
|
|
898
|
+
const kpi = computeKpis(turns);
|
|
899
|
+
const cfg = readConfig();
|
|
900
|
+
const budgetUSD = cfg.monthlyBudgetUSD;
|
|
901
|
+
const turnsEmbed = turns.map((rec) => buildTurnEmbed(rec, map));
|
|
902
|
+
const anyTruncated = turnsEmbed.some((t) => t.tr);
|
|
903
|
+
const anySub = turns.some((r) => r.subagents);
|
|
904
|
+
let subUsd = 0;
|
|
905
|
+
let subJpy = 0;
|
|
906
|
+
for (const r of turns) {
|
|
907
|
+
if (r.subagents) {
|
|
908
|
+
subUsd += r.subagents.costUSD;
|
|
909
|
+
subJpy += r.subagents.costUSD * r.fxRate;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
let minMs = Infinity;
|
|
913
|
+
let maxMs = -Infinity;
|
|
914
|
+
for (const t of turnsEmbed) {
|
|
915
|
+
if (t.t > 0) {
|
|
916
|
+
if (t.t < minMs) minMs = t.t;
|
|
917
|
+
if (t.t > maxMs) maxMs = t.t;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const rangeText = Number.isFinite(minMs) && Number.isFinite(maxMs) ? `${dateKeyOf(new Date(minMs))} \u301C ${dateKeyOf(new Date(maxMs))}` : "\u2014";
|
|
921
|
+
const embed = {
|
|
922
|
+
version,
|
|
923
|
+
generatedAt,
|
|
924
|
+
slots: map.slots,
|
|
925
|
+
turns: turnsEmbed,
|
|
926
|
+
budget: budgetUSD,
|
|
927
|
+
budgetRate: cfg.fx.fallbackRate
|
|
928
|
+
};
|
|
929
|
+
const dataJson = escapeJsonForScript(JSON.stringify(embed));
|
|
930
|
+
const reloadSec = Number.isFinite(opts.autoReloadSec) && opts.autoReloadSec > 0 ? Math.floor(opts.autoReloadSec) : 0;
|
|
931
|
+
const refreshMeta = reloadSec > 0 ? `<meta http-equiv="refresh" content="${reloadSec}">` : "";
|
|
932
|
+
const autoUpdateFoot = reloadSec > 0 ? `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u81EA\u52D5\u66F4\u65B0(\u6700\u65B0\u5316\u306F Claude Code \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642)</div>` : "";
|
|
933
|
+
const head = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark">` + refreshMeta + `<title>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</title>` + STYLE + `</head><body><div class="wrap">`;
|
|
934
|
+
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>`;
|
|
935
|
+
if (turns.length === 0) {
|
|
936
|
+
const header2 = `<div class="head"><div><h1>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</h1><div class="sub">${esc(period)} \xB7 \u751F\u6210 ${esc(generatedAt)}</div></div></div>`;
|
|
937
|
+
const empty = `<div class="empty"><h2>\u307E\u3060\u5C65\u6B74\u304C\u3042\u308A\u307E\u305B\u3093</h2><p>Claude Code \u3067\u30D7\u30ED\u30F3\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u3068\u3001\u3053\u3053\u306B\u30B3\u30B9\u30C8\u5C65\u6B74\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002</p><p class="empty-hint">No history yet \u2014 run a prompt in Claude Code and it will appear here.</p></div>`;
|
|
938
|
+
return head + header2 + empty + foot;
|
|
939
|
+
}
|
|
940
|
+
const header = `<div class="head"><div><h1>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</h1><div class="sub">${esc(period)}<span class="muted"> \xB7 ${esc(rangeText)}</span></div><div class="sub muted">\u751F\u6210 ${esc(generatedAt)} / generated</div></div><div class="hero"><div class="hero-label">\u901A\u7B97 / Total</div><div class="hero-value">${esc(formatUSD(kpi.all.usd))}</div><div class="hero-meta">${esc(formatJPY(kpi.all.jpy))} \xB7 ${kpi.all.turns} \u30BF\u30FC\u30F3</div>` + (anySub ? `<div class="hero-meta">\u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8 ${esc(formatUSD(subUsd))}(${esc(formatJPY(subJpy))})</div>` : "") + `</div></div>`;
|
|
941
|
+
const kpiSection = `<div class="kpi">` + statCard("\u4ECA\u65E5", "Today", kpi.today) + statCard("\u4ECA\u9031", "\u76F4\u8FD17\u65E5", kpi.week) + statCard("\u4ECA\u6708", "\u66A6\u6708", kpi.month) + statCard("\u901A\u7B97", "\u5168\u671F\u9593", kpi.all) + `</div>`;
|
|
942
|
+
const budgetSection = budgetCard(budgetUSD, kpi.month, cfg.fx.fallbackRate);
|
|
943
|
+
const chartSection = `<section class="card"><h2>\u30B3\u30B9\u30C8\u63A8\u79FB / Cost over time</h2><p class="note">\u7C92\u5EA6\u3092 \u65E5 / \u9031 / \u6708 \u3067\u5207\u308A\u66FF\u3048\u3089\u308C\u307E\u3059(\u6A2A\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u904E\u53BB\u307E\u3067)\u3002\u68D2\u3092\u30AF\u30EA\u30C3\u30AF\u3059\u308B\u3068\u305D\u306E\u671F\u9593\u304C\u9078\u629E\u3055\u308C\u3001\u4E0B\u306E\u30E2\u30C7\u30EB\u5225\u30FB\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5225\u30FB\u5C65\u6B74\u304C\u9023\u52D5\u3057\u307E\u3059\u3002\u300C\u901A\u7B97\u300D\u3067\u5168\u671F\u9593\u306B\u623B\u308A\u307E\u3059\u3002\u30E2\u30C7\u30EB\u5225\u306B\u8272\u5206\u3051\u3002</p><div class="toolbar-row"><div class="seg-toggle"><button type="button" data-gran="day">\u65E5</button><button type="button" data-gran="week">\u9031</button><button type="button" data-gran="month">\u6708</button></div><button type="button" id="cccn-all" class="btn">\u901A\u7B97</button><span id="cccn-sel-label" class="sel-label"></span></div>` + renderLegend(map.slots) + `<div class="chart-scroll"><div id="cccn-chart"></div></div></section>`;
|
|
944
|
+
const breakdownSection = `<div class="grid2"><section class="card"><h2>\u30E2\u30C7\u30EB\u5225\u5185\u8A33 / By model</h2><p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593(\u65E2\u5B9A\u306F\u901A\u7B97)\u306E\u30B3\u30B9\u30C8\u964D\u9806\u3002\u8907\u6570\u30E2\u30C7\u30EB\u306E\u30BF\u30FC\u30F3\u306F\u5404\u30E2\u30C7\u30EB\u306B1\u305A\u3064\u8A08\u4E0A\u3002</p><div class="table-wrap" id="cccn-bymodel"></div></section><section class="card"><h2>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u5225 / By project</h2><p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8(\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA basename)\u5358\u4F4D\u30FB\u30B3\u30B9\u30C8\u964D\u9806\u3002</p><div class="table-wrap" id="cccn-byproject"></div></section></div>`;
|
|
945
|
+
const capNote = "\u4EF6\u6570\u304C\u591A\u3044\u5834\u5408\u306F\u65B0\u3057\u3044\u9806\u306B\u5148\u982D200\u4EF6\u306E\u307F\u63CF\u753B\u3057\u307E\u3059(\u691C\u7D22\u3084\u3001\u4E0A\u306E\u30B0\u30E9\u30D5\u3067\u671F\u9593\u3092\u9078\u3076\u3068\u7D5E\u308A\u8FBC\u3081\u307E\u3059)\u3002";
|
|
946
|
+
const truncNote = anyTruncated ? `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u30D7\u30ED\u30F3\u30D7\u30C8\u306F1\u4EF6\u3042\u305F\u308A\u6700\u5927 ${PROMPT_MAX.toLocaleString("en-US")} \u5B57\u307E\u3067(\u8D85\u904E\u5206\u306F\u300C${esc(PROMPT_TRUNC_MARK)}\u300D)\u3002\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u5168\u6587\u5C55\u958B\u3002</p>` : `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092\u5C55\u958B\u3057\u307E\u3059\u3002</p>`;
|
|
947
|
+
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>`;
|
|
948
|
+
return head + header + kpiSection + budgetSection + chartSection + breakdownSection + historySection + foot;
|
|
949
|
+
}
|
|
950
|
+
function toWindowsPath(path) {
|
|
951
|
+
try {
|
|
952
|
+
const r = spawnSync("wslpath", ["-w", path], { encoding: "utf8", timeout: 3e3 });
|
|
953
|
+
if (r.status === 0 && typeof r.stdout === "string") {
|
|
954
|
+
const win = r.stdout.trim();
|
|
955
|
+
return win.length > 0 ? win : null;
|
|
956
|
+
}
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
function browserOpenPlan(platform, wsl, path, winPath) {
|
|
962
|
+
if (platform === "darwin") return { cmd: "open", args: [path] };
|
|
963
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", "", path] };
|
|
964
|
+
if (wsl && winPath) {
|
|
965
|
+
return {
|
|
966
|
+
cmd: "powershell.exe",
|
|
967
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath "${winPath}"`]
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
return { cmd: "xdg-open", args: [path] };
|
|
971
|
+
}
|
|
972
|
+
function openInBrowser(path) {
|
|
973
|
+
try {
|
|
974
|
+
const wsl = process.platform === "linux" && isWSL();
|
|
975
|
+
const winPath = wsl ? toWindowsPath(path) : null;
|
|
976
|
+
const { cmd, args } = browserOpenPlan(process.platform, wsl, path, winPath);
|
|
977
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
978
|
+
child.on("error", () => {
|
|
979
|
+
});
|
|
980
|
+
child.unref();
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function writeDashboardHtml(opts) {
|
|
985
|
+
const days = opts.days ?? null;
|
|
986
|
+
const turns = readTurns(days ?? void 0);
|
|
987
|
+
const html = renderDashboard(turns, {
|
|
988
|
+
days,
|
|
989
|
+
open: false,
|
|
990
|
+
out: opts.outPath,
|
|
991
|
+
autoReloadSec: opts.autoReloadSec
|
|
992
|
+
});
|
|
993
|
+
mkdirSync(dirname(opts.outPath), { recursive: true });
|
|
994
|
+
writeFileSync(opts.outPath, html, "utf8");
|
|
995
|
+
}
|
|
996
|
+
async function runDashboard(argv) {
|
|
997
|
+
const opts = parseArgs(argv);
|
|
998
|
+
let outPath;
|
|
999
|
+
try {
|
|
1000
|
+
outPath = opts.out ?? join(paths().home, "report.html");
|
|
1001
|
+
writeDashboardHtml({ days: opts.days, outPath, autoReloadSec: opts.autoReloadSec });
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
console.error(
|
|
1004
|
+
`dashboard \u306E\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F / failed to generate dashboard: ${err instanceof Error ? err.message : String(err)}`
|
|
1005
|
+
);
|
|
1006
|
+
return 1;
|
|
1007
|
+
}
|
|
1008
|
+
console.log(`\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3092\u751F\u6210\u3057\u307E\u3057\u305F / dashboard written: ${outPath}`);
|
|
1009
|
+
if (process.platform === "linux" && isWSL()) {
|
|
1010
|
+
const winPath = toWindowsPath(outPath);
|
|
1011
|
+
if (winPath) console.log(`Windows \u304B\u3089\u958B\u304F\u5834\u5408 / on Windows: ${winPath}`);
|
|
1012
|
+
}
|
|
1013
|
+
if (opts.open) {
|
|
1014
|
+
openInBrowser(outPath);
|
|
1015
|
+
console.log("\u30D6\u30E9\u30A6\u30B6\u3067\u958B\u3044\u3066\u3044\u307E\u3059\u2026 / opening in your browser\u2026");
|
|
1016
|
+
}
|
|
1017
|
+
return 0;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export {
|
|
1021
|
+
browserOpenPlan,
|
|
1022
|
+
writeDashboardHtml,
|
|
1023
|
+
runDashboard
|
|
1024
|
+
};
|