claude-spotter 1.5.1 → 1.5.3
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/CHANGELOG.md +19 -0
- package/README.md +3 -2
- package/docs/11_dashboard-operations.md +4 -2
- package/ops/dashboard/launchd/dev.kitepon.spotter-dashboard-device.plist +4 -0
- package/ops/dashboard/windows/spotter-dashboard-device.ps1 +1 -1
- package/ops/dashboard/windows/spotter-dashboard-tunnel.ps1 +1 -1
- package/package.json +1 -1
- package/src/core/evaluation-store.mjs +2 -2
- package/src/dashboard/render.mjs +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.3 — 2026-08-04
|
|
4
|
+
|
|
5
|
+
- **dashboardの難解な集計略号を廃止。** 概要cardとproject/tool別内訳の
|
|
6
|
+
`S/P/I/C/A/M`を、対象ターン、ツール提案あり、提案ツール数、利用判定済み、
|
|
7
|
+
実際に使用、判定不能の日本語表示へ置き換える。集計schemaとCLIの内部キーは変更しない。
|
|
8
|
+
- **率の意味を式ではなく言葉で表示。** 提案率は「ツール提案あり ÷ 対象ターン」、
|
|
9
|
+
採用率は「実際に使用 ÷ 利用判定済み」と併記する。
|
|
10
|
+
|
|
11
|
+
## 1.5.2 — 2026-08-04
|
|
12
|
+
|
|
13
|
+
- **Mac LaunchAgentでNodeを解決。** `spotter`の絶対pathだけでなく、env shebangが使う
|
|
14
|
+
Homebrew Nodeを含むPATHをplistへ明示し、launchdの最小PATHでもdevice serverを起動できるようにした。
|
|
15
|
+
- **Windows nativeとWSL2のloopback portを分離。** WSL2 localhost relayがWindows側の53940も
|
|
16
|
+
占有する実環境に合わせ、Windows native deviceを53944へ移し、main-serverの53943 tunnelも
|
|
17
|
+
53944へ接続する。Windowsを選んだ時にWSL2の評価DBが表示される衝突を解消した。
|
|
18
|
+
- **評価SQLiteの初回同時open待ちを実測に合わせた。** 高負荷のNode 22.13でschema作成の競合が
|
|
19
|
+
既定1秒を超えたため、SQLite自身のbounded `busy_timeout`を5秒へ変更した。retry、lockfile、
|
|
20
|
+
background処理は追加せず、lockがない通常処理は待たない。
|
|
21
|
+
|
|
3
22
|
## 1.5.1 — 2026-08-04
|
|
4
23
|
|
|
5
24
|
- **main-server hubのsystemd PATHを明示設定可能にした。** user managerがnvmのnpm binを
|
package/README.md
CHANGED
|
@@ -256,8 +256,9 @@ spotter uninstall # remove hooks from this project (leaves ~/.spotter int
|
|
|
256
256
|
|
|
257
257
|
The dashboard is local-first. Every terminal reads its own `~/.spotter/evaluation.db`; the hub
|
|
258
258
|
keeps only a static device-to-upstream map and does not copy evaluation data into a cloud database.
|
|
259
|
-
The device view shows
|
|
260
|
-
non-adopted cases, and the two
|
|
259
|
+
The device view shows Japanese labels for every evaluation metric, proposal and adoption rates
|
|
260
|
+
with their numerator and denominator, project/tool breakdowns, non-adopted cases, and the two
|
|
261
|
+
separately captured context sources. The hub checks health only
|
|
261
262
|
when the device list is requested, so an offline terminal is isolated without a background monitor
|
|
262
263
|
or retry queue.
|
|
263
264
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## 固定構成
|
|
4
4
|
|
|
5
|
-
各端末のdevice server
|
|
5
|
+
各端末のdevice serverはloopbackだけで待ち受ける。main-server、Mac、FOX WSL2は
|
|
6
|
+
`127.0.0.1:53940`、FOX Windows nativeはWSL2 localhost relayとの衝突を避けて
|
|
7
|
+
`127.0.0.1:53944`を使う。main-serverのhubは
|
|
6
8
|
Docker Caddyから到達できる`172.18.0.1:53940`で待ち受ける。評価DBは各端末の
|
|
7
9
|
`~/.spotter/evaluation.db`をその場で読み、端末外へ複製しない。
|
|
8
10
|
|
|
@@ -88,7 +90,7 @@ main-serverのSSH host名またはIPを明示して次を1回実行する。
|
|
|
88
90
|
登録される2 taskはログオン時にdevice serverとreverse tunnelを起動する。確認:
|
|
89
91
|
|
|
90
92
|
```powershell
|
|
91
|
-
Invoke-RestMethod http://127.0.0.1:
|
|
93
|
+
Invoke-RestMethod http://127.0.0.1:53944/_spotter/health
|
|
92
94
|
Get-ScheduledTask -TaskName 'Spotter dashboard *' | Get-ScheduledTaskInfo
|
|
93
95
|
```
|
|
94
96
|
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
<string>--host</string><string>127.0.0.1</string>
|
|
13
13
|
<string>--port</string><string>53940</string>
|
|
14
14
|
</array>
|
|
15
|
+
<key>EnvironmentVariables</key>
|
|
16
|
+
<dict>
|
|
17
|
+
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
18
|
+
</dict>
|
|
15
19
|
<key>RunAtLoad</key><true/>
|
|
16
20
|
<key>KeepAlive</key><true/>
|
|
17
21
|
<key>ThrottleInterval</key><integer>10</integer>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
$ErrorActionPreference = 'Stop'
|
|
2
2
|
$spotter = Join-Path $env:APPDATA 'npm\spotter.cmd'
|
|
3
|
-
& $spotter dashboard device --id fox-windows --name 'FOX Windows native' --host 127.0.0.1 --port
|
|
3
|
+
& $spotter dashboard device --id fox-windows --name 'FOX Windows native' --host 127.0.0.1 --port 53944
|
|
4
4
|
exit $LASTEXITCODE
|
|
@@ -3,5 +3,5 @@ $ErrorActionPreference = 'Stop'
|
|
|
3
3
|
& "$env:WINDIR\System32\OpenSSH\ssh.exe" -N `
|
|
4
4
|
-o BatchMode=yes -o ExitOnForwardFailure=yes `
|
|
5
5
|
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o TCPKeepAlive=yes `
|
|
6
|
-
-R '127.0.0.1:53943:127.0.0.1:
|
|
6
|
+
-R '127.0.0.1:53943:127.0.0.1:53944' $MainServer
|
|
7
7
|
exit $LASTEXITCODE
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
5
|
|
|
6
6
|
export const EVALUATION_STORE_SCHEMA = 'spotter.evaluation_store.v1';
|
|
7
|
-
export const DEFAULT_EVALUATION_BUSY_TIMEOUT_MS =
|
|
7
|
+
export const DEFAULT_EVALUATION_BUSY_TIMEOUT_MS = 5_000;
|
|
8
8
|
// daemonの無通信寿命と同じ30分を超えたopen観測は、保存行を変更せず、
|
|
9
9
|
// report上だけproposal itemをoutcome_missingとして扱う。
|
|
10
10
|
export const DEFAULT_OPEN_TURN_STALE_MS = 30 * 60 * 1_000;
|
|
@@ -29,7 +29,7 @@ export function createEvaluationStore({
|
|
|
29
29
|
mkdirSync(dirname(databasePath), { recursive: true, mode: 0o700 });
|
|
30
30
|
const database = new DatabaseSync(databasePath);
|
|
31
31
|
// cold startでは別projectのprocessも同じ未作成DBを同時に開き得る。
|
|
32
|
-
// WAL化とschema作成が最初のwrite lockを競う前に、既定
|
|
32
|
+
// WAL化とschema作成が最初のwrite lockを競う前に、既定5秒のbounded waitを有効にする。
|
|
33
33
|
database.exec(`PRAGMA busy_timeout=${busyTimeoutMs};`);
|
|
34
34
|
database.exec('PRAGMA foreign_keys=ON;');
|
|
35
35
|
initialize(database);
|
package/src/dashboard/render.mjs
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
// opens EvaluationStore or asks Throughline for context.
|
|
4
4
|
|
|
5
5
|
const METRIC_KEYS = ['S', 'P', 'I', 'C', 'A', 'M'];
|
|
6
|
+
const METRIC_LABELS = {
|
|
7
|
+
S: '対象ターン',
|
|
8
|
+
P: 'ツール提案あり',
|
|
9
|
+
I: '提案ツール数',
|
|
10
|
+
C: '利用判定済み',
|
|
11
|
+
A: '実際に使用',
|
|
12
|
+
M: '判定不能',
|
|
13
|
+
};
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
* Render a complete dashboard document.
|
|
@@ -50,7 +58,7 @@ export function renderDashboard({
|
|
|
50
58
|
<header class="page-header">
|
|
51
59
|
<p class="eyebrow">Spotter</p>
|
|
52
60
|
<h1>${escapeHtml(title)}</h1>
|
|
53
|
-
${selectedDevice ? `<p class="subtitle">端末: ${escapeHtml(selectedDevice.name ?? selectedDevice.id)}
|
|
61
|
+
${selectedDevice ? `<p class="subtitle">端末: ${escapeHtml(selectedDevice.name ?? selectedDevice.id)} · <a class="device-picker-link" href="/">端末を選び直す</a></p>` : '<p class="subtitle">端末を選択してください。</p>'}
|
|
54
62
|
</header>
|
|
55
63
|
${renderDevices(devices, selectedId)}
|
|
56
64
|
${selectedDevice ? renderDeviceContent({ overview: dashboardModel, cases: renderedCases, caseDetail, filters, action }) : ''}
|
|
@@ -96,7 +104,7 @@ function renderDeviceContent({ overview, cases, caseDetail, filters, action }) {
|
|
|
96
104
|
return `${renderFilters(filters, action)}
|
|
97
105
|
<section aria-labelledby="overview-heading">
|
|
98
106
|
<div class="section-heading"><h2 id="overview-heading">概要</h2>${renderRates(totals)}</div>
|
|
99
|
-
<div class="metrics" aria-label="評価メトリクス">${METRIC_KEYS.map((key) => `<article class="metric"><span>${key}</span><strong>${escapeHtml(metric(totals, key))}</strong></article>`).join('')}</div>
|
|
107
|
+
<div class="metrics" aria-label="評価メトリクス">${METRIC_KEYS.map((key) => `<article class="metric"><span>${METRIC_LABELS[key]}</span><strong>${escapeHtml(metric(totals, key))}</strong></article>`).join('')}</div>
|
|
100
108
|
</section>
|
|
101
109
|
${renderBreakdown('project-breakdown', 'project別内訳', projects)}
|
|
102
110
|
${renderBreakdown('tool-breakdown', 'tool別内訳', tools)}
|
|
@@ -119,13 +127,13 @@ function renderFilters(filters, action) {
|
|
|
119
127
|
function renderRates(summary) {
|
|
120
128
|
const proposal = rate(metricNumber(summary, 'P'), metricNumber(summary, 'S'));
|
|
121
129
|
const adoption = rate(metricNumber(summary, 'A'), metricNumber(summary, 'C'));
|
|
122
|
-
return `<dl class="rates"><div><dt
|
|
130
|
+
return `<dl class="rates"><div><dt>提案率<small>ツール提案あり ÷ 対象ターン</small></dt><dd>${escapeHtml(proposal)}</dd></div><div><dt>採用率<small>実際に使用 ÷ 利用判定済み</small></dt><dd>${escapeHtml(adoption)}</dd></div></dl>`;
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
function renderBreakdown(id, title, rows) {
|
|
126
134
|
return `<section aria-labelledby="${id}">
|
|
127
135
|
<h2 id="${id}">${title}</h2>
|
|
128
|
-
${rows.length === 0 ? '<p class="empty">該当するデータはありません。</p>' : `<div class="table-wrap"><table><thead><tr><th scope="col">項目</th>${METRIC_KEYS.map((key) => `<th scope="col">${key}</th>`).join('')}<th scope="col">提案率</th><th scope="col">採用率</th></tr></thead><tbody>${rows.map(([label, summary]) => `<tr><th scope="row">${escapeHtml(label)}</th>${METRIC_KEYS.map((key) => `<td>${escapeHtml(metric(summary, key))}</td>`).join('')}<td>${escapeHtml(rate(metricNumber(summary, 'P'), metricNumber(summary, 'S')))}</td><td>${escapeHtml(rate(metricNumber(summary, 'A'), metricNumber(summary, 'C')))}</td></tr>`).join('')}</tbody></table></div>`}
|
|
136
|
+
${rows.length === 0 ? '<p class="empty">該当するデータはありません。</p>' : `<div class="table-wrap"><table><thead><tr><th scope="col">項目</th>${METRIC_KEYS.map((key) => `<th scope="col">${METRIC_LABELS[key]}</th>`).join('')}<th scope="col">提案率</th><th scope="col">採用率</th></tr></thead><tbody>${rows.map(([label, summary]) => `<tr><th scope="row">${escapeHtml(label)}</th>${METRIC_KEYS.map((key) => `<td>${escapeHtml(metric(summary, key))}</td>`).join('')}<td>${escapeHtml(rate(metricNumber(summary, 'P'), metricNumber(summary, 'S')))}</td><td>${escapeHtml(rate(metricNumber(summary, 'A'), metricNumber(summary, 'C')))}</td></tr>`).join('')}</tbody></table></div>`}
|
|
129
137
|
</section>`;
|
|
130
138
|
}
|
|
131
139
|
|
|
@@ -212,12 +220,13 @@ body { margin: 0; }
|
|
|
212
220
|
.eyebrow { color: #3565a0; font-weight: 700; letter-spacing: .08em; margin: 0; text-transform: uppercase; }
|
|
213
221
|
h1 { margin: .25rem 0; font-size: clamp(1.75rem, 4vw, 2.5rem); } h2 { margin-top: 2rem; } h3 { margin-bottom: .5rem; }
|
|
214
222
|
.subtitle, .empty { color: #52606d; }
|
|
223
|
+
.device-picker-link { color: inherit; }
|
|
215
224
|
.device-nav ul { display: flex; flex-wrap: wrap; gap: .5rem; list-style: none; margin: .5rem 0; padding: 0; }
|
|
216
225
|
.device { align-items: center; border: 1px solid #ccd4dd; border-radius: 999px; color: inherit; display: inline-flex; gap: .45rem; padding: .45rem .7rem; text-decoration: none; }
|
|
217
226
|
.device[aria-current="page"] { border-color: #1c63b8; box-shadow: 0 0 0 2px #b8d6fa; }.device small { color: #52606d; }.status-dot { background: #a03333; border-radius: 50%; height: .55rem; width: .55rem; }.online .status-dot { background: #16803c; }
|
|
218
227
|
.filters { align-items: end; display: flex; flex-wrap: wrap; gap: .75rem; margin: 1.5rem 0; }.filters label { display: grid; font-size: .85rem; gap: .25rem; }.filters input, button { border: 1px solid #aeb8c4; border-radius: .35rem; font: inherit; padding: .45rem; }button { background: #1c63b8; color: white; cursor: pointer; }
|
|
219
|
-
.section-heading { align-items: baseline; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; }.rates { display: flex; gap: 1.25rem; margin: 0; }.rates div { display: flex; gap: .
|
|
220
|
-
.metrics { display: grid; gap: .75rem; grid-template-columns: repeat(6, minmax(
|
|
228
|
+
.section-heading { align-items: baseline; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; }.rates { display: flex; gap: 1.25rem; margin: 0; }.rates div { display: flex; gap: .45rem; }.rates dt { color: #52606d; }.rates dt small { display: block; font-size: .72rem; }.rates dd { font-weight: 700; margin: 0; }
|
|
229
|
+
.metrics { display: grid; gap: .75rem; grid-template-columns: repeat(6, minmax(110px, 1fr)); }.metric { background: #fff; border: 1px solid #d7dce2; border-radius: .5rem; padding: .8rem; }.metric span { color: #52606d; display: block; }.metric strong { font-size: 1.5rem; }
|
|
221
230
|
.table-wrap { overflow-x: auto; }table { border-collapse: collapse; min-width: 680px; width: 100%; }th, td { border-bottom: 1px solid #d7dce2; padding: .6rem; text-align: left; vertical-align: top; }thead { background: #eaf0f6; }tbody tr:nth-child(even) { background: #fbfcfd; }
|
|
222
231
|
.case-detail { border-top: 2px solid #1c63b8; margin-top: 2.5rem; }.metadata { display: flex; flex-wrap: wrap; gap: 1rem; }.metadata div { min-width: 12rem; }.metadata dt { color: #52606d; }.metadata dd { margin: .2rem 0; overflow-wrap: anywhere; }pre { background: #1e2935; color: #e6edf3; margin: 0; overflow-x: auto; padding: 1rem; white-space: pre-wrap; word-break: break-word; }
|
|
223
232
|
@media (max-width: 700px) { .dashboard { padding-top: 1rem; }.metrics { grid-template-columns: repeat(3, 1fr); }.filters { align-items: stretch; flex-direction: column; }.filters input, button { width: 100%; }.rates { flex-direction: column; gap: .25rem; } }
|