From e16189153ccf3f12a4524a94bf64f11b515a3185 Mon Sep 17 00:00:00 2001 From: yoshikazu Date: Mon, 12 Oct 2026 21:40:00 +0900 Subject: [PATCH] =?UTF-8?q?n8n=20=E3=81=AE=20work=5Flog=20=E3=81=B8?= =?UTF-8?q?=E4=BD=9C=E6=A5=AD=E5=8C=BA=E9=96=93=E3=82=92=E9=80=81=E3=82=8B?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=81=AE=E5=AE=9F=E8=A3=85=E8=A8=88=E7=94=BB?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 設計書のチケット検索 API を /api/redmine/search に改名 (/api/redmine/issues は起票の POST が使っているため)。 Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-12-n8n-worklog-import.md | 1711 +++++++++++++++++ .../2026-10-12-n8n-worklog-import-design.md | 2 +- 2 files changed, 1712 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-10-12-n8n-worklog-import.md diff --git a/docs/superpowers/plans/2026-10-12-n8n-worklog-import.md b/docs/superpowers/plans/2026-10-12-n8n-worklog-import.md new file mode 100644 index 0000000..68a0a3b --- /dev/null +++ b/docs/superpowers/plans/2026-10-12-n8n-worklog-import.md @@ -0,0 +1,1711 @@ +# chktime → n8n work_log 取り込み Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** chktime の画面で Claude のセッションから作った作業区間を選び、n8n の `work_log` に二重登録なしで送れるようにする。 + +**Architecture:** n8n に受け口のワークフロー `work-tracker--import` (Header Auth 付き Webhook 2 本) を作り、`work_log` / `work_log_deleted` に `source` 列を足す。chktime は区間の計算・重なりの判定・n8n への送信を `chktime.py` の新しい節「n8n 連携」に持ち、画面は新しいファイル `web/n8n.js` に置いて右ペインのタブ「n8n へ送る」から開く。時刻はブラウザの値を信用せず、送信時にサーバーが `source` から区間を計算し直す。 + +**Tech Stack:** Python 3 標準ライブラリのみ (unittest, urllib, http.server)、素の JavaScript、n8n (Webhook / Code / Data table / IF / Respond to Webhook ノード)、n8n-api MCP (`mcp__n8n-api__*`)。 + +**Spec:** `docs/superpowers/specs/2026-10-12-n8n-worklog-import-design.md` + +## Global Constraints + +- chktime は **標準ライブラリのみ**。外部パッケージも CDN も使わない。ブラウザ側もフレームワークなし。 +- 外部通信は **Redmine 連携と n8n への送信だけ**。 +- 合言葉 (`token`) はリポジトリ・ログ・画面・コマンド出力に出さない。`n8n.json` は `.gitignore` に入れる。`/api/n8n/config` は `hasToken` だけ返す。 +- 合言葉は利用者が決めて入力する。実装者 (Claude) は作らず、入力もしない。 +- 会話ログ (`~/.claude/projects/**/*.jsonl`) は読み取り専用。テストは一時フォルダに作った jsonl で行う。 +- 検証用サーバーは `python chktime.py --port 8799 --no-browser`。常用の 8787 と `stop.cmd` には触れない (`stop.cmd` は全 chktime を落とす)。 +- `.bat` / `.cmd` は ASCII のみ。コメント・コミットメッセージ・UI 文言は日本語。 +- 区間: 空き **30 分未満でつなぐ / 30 分以上で切る**、**5 分未満は外す (5 分ちょうどは残す)**。設定 `gapMinutes` (1〜240) / `minMinutes` (0〜240)。 +- `source` = `chktime:<セッションID>:<区間開始の epoch ミリ秒>`。 +- n8n 受け口: 期間 **最大 62 日**、1 回 **最大 200 件**、区間 **24 時間以内**、`task` **500 文字以内 (空不可)**、`memo` **2000 文字以内**、`source` **500 文字以内で `chktime:` 始まり**。 +- 重なり: `[開始, 終了)` が少しでも重なれば重なり。接しているだけは重ならない。同じ `source` は相手から除く。進行中の work_log 行は終了 = 今。 +- n8n の本番データに触れるテストは **2000-01-01 付けのダミー**だけで行い、終わったら Data table を直接 (MCP の `deleteRows`) 消す。work-tracker--delete の Webhook は使わない。 +- 既存のコミットされていない `CLAUDE.md` の変更 (ナレッジ保管庫の節) は利用者のもの。自分の変更だけを `git add -p` でステージする。 +- コミットメッセージの末尾に `Co-Authored-By: Claude Opus 5.5 `。 + +## Review Focus + +- 前のターンの応答中に次の発言を送った (並行) セッション: 区間は切れずにつながるべき → Task 3 `test_parallel_prompt_keeps_block` +- 最後のターンが中断されて応答が無いまま放置されたセッション: 進行中ではなく送れる区間になるべき → Task 3 `test_interrupted_last_turn_is_not_in_progress` +- タスク名が空白だけで送られた: n8n に送らず「タスク名が空です」を返すべき → Task 6 `test_send_rejects_blank_task` +- n8n に届かない (停止・合言葉違い): 区間の一覧は返り、送信済みの印は付かず `n8nError` に理由が入るべき → Task 6 `test_blocks_when_n8n_down` +- 日をまたぐ区間: 開始日の 1 件として、開始日を含む期間だけに出るべき → Task 4 `test_midnight_block_belongs_to_start_date` + +## File Structure + +| ファイル | 役割 | +|---|---| +| `chktime.py` (変更) | 新しい節「n8n 連携」: 定数、`N8nError`、`block_source`、`make_blocks`、`default_task_name`、`collect_blocks`、`mark_blocks`、`N8n`、`N8nHolder`、`n8n_range`、`n8n_blocks`、`n8n_send`。Redmine 節に `Redmine.search`。HTTP 節にルート追加。`main()` に `--n8n-config` | +| `tests/test_n8n.py` (新規) | 上記の unittest (純粋関数・偽の n8n サーバー・一時フォルダの jsonl) | +| `web/n8n.js` (新規) | 「n8n へ送る」タブの描画と操作 | +| `web/index.html` (変更) | タブボタンと `` の次の行に: + +```html + +``` + +- [ ] **Step 2: app.js に分岐を足す** + +`syncTabs` の最後に: +```js + $('#tab-n8n').classList.toggle('sel', state.view === 'n8n'); +``` +`updateCsv` の `if (state.view === 'daily') {` の前に: +```js + if (state.view === 'n8n') { + link.removeAttribute('href'); + return; + } +``` +`setView` の `else if (view === 'gantt') loadGantt();` の次に: +```js + else if (view === 'n8n') loadN8n(); +``` +`selectProject` の `else if (state.view === 'gantt') loadGantt();` の次に: +```js + else if (state.view === 'n8n') loadN8n(); +``` +`refreshFollow` の `else if (state.view === 'gantt') await loadGantt({ silent: true });` の次に (編集中の入力を消さないよう、自動追従では読み直さない): +```js + else if (state.view === 'n8n') { /* 自動追従では読み直さない */ } +``` +resize の `if (state.view === 'turns') return;` を: +```js + if (state.view === 'turns' || state.view === 'n8n') return; +``` +タブの onclick (`$('#tab-gantt').onclick = ...` の次) に: +```js +$('#tab-n8n').onclick = () => setView('n8n'); +``` + +- [ ] **Step 3: web/n8n.js を書く** + +```js +'use strict'; +// 「n8n へ送る」タブ: Claude のセッションから作った作業区間を選び、n8n の work_log に送る。 +// app.js の後に読み込む ($ / state / api / post / esc / fmtTime / shortPath を使う)。 + +const N8N_NAMES_KEY = 'chktime.n8n.names'; // セッション ID -> {task, memo} (ブラウザに覚える) + +const n8 = { + cfg: null, // /api/n8n/config (合言葉は含まれない) + from: '', + to: '', + data: null, // /api/n8n/blocks + checked: new Set(), // 送る区間の source + edits: new Map(), // source -> {task, memo} + sameSession: true, // 同じセッションの他の区間にも入れる + settings: false, + busy: false, + result: null, // 直前の送信結果 + pick: null, // チケットを選んでいる区間の source + issues: [], + pickMsg: '', +}; + +function n8LoadNames() { + try { + return JSON.parse(localStorage.getItem(N8N_NAMES_KEY) || '{}') || {}; + } catch (e) { + return {}; + } +} + +function n8SaveName(sessionId, value) { + try { + const all = n8LoadNames(); + all[sessionId] = value; + localStorage.setItem(N8N_NAMES_KEY, JSON.stringify(all)); + } catch (e) { + /* 保存できなくても動く */ + } +} + +const n8CanSend = (b) => !b.sent && !b.inProgress; +const n8DefaultOn = (b) => n8CanSend(b) && !b.deleted && !(b.overlaps || []).length; + +function n8Query() { + const q = new URLSearchParams(); + if (state.project) q.set('project', state.project); + if (n8.from) q.set('from', n8.from); + if (n8.to) q.set('to', n8.to); + return q.toString(); +} + +async function loadN8n({ silent = false } = {}) { + $('#detail-title').textContent = 'n8n へ送る'; + if (!silent) $('#detail-body').innerHTML = '

区間を計算中…

'; + try { + n8.cfg = await api('/api/n8n/config'); + if (!n8.cfg.url || !n8.cfg.hasToken) n8.settings = true; + n8.data = await api('/api/n8n/blocks?' + n8Query()); + n8.from = n8.data.from; + n8.to = n8.data.to; + const names = n8LoadNames(); + n8.checked = new Set(n8.data.blocks.filter(n8DefaultOn).map((b) => b.source)); + for (const b of n8.data.blocks) { + if (n8.edits.has(b.source)) continue; + const saved = names[b.sessionId] || {}; + n8.edits.set(b.source, { task: saved.task || b.taskName, memo: saved.memo || '' }); + } + renderN8n(); + } catch (e) { + $('#detail-body').innerHTML = `

${esc(e.message)}

`; + } +} + +function n8Status(b) { + const marks = []; + if (b.sent) marks.push('送信済み'); + if (b.deleted) marks.push('削除済み'); + if (b.inProgress) marks.push('進行中'); + if ((b.overlaps || []).length) { + const tip = b.overlaps.map((o) => `${o.kind === 'row' ? 'work_log' : '区間'}: ${fmtTime(o.start)}–${fmtTime(o.end)} ${o.task}`).join('\n'); + marks.push(`重なり ${b.overlaps.length}`); + } + return marks.join(' '); +} + +function n8Settings() { + const c = n8.cfg || {}; + return `
+

接続設定 ${esc(c.configPath || '')}

+ + + + + + +
`; +} + +function n8ResultHtml() { + const r = n8.result; + if (!r) return ''; + const errs = (r.errors || []).map((e) => `
  • ${esc(e.source)}: ${esc(e.error)}
  • `).join(''); + const skips = (r.skipped || []).map((s) => `
  • ${esc(s.source)}: ${esc(s.reason)}
  • `).join(''); + return `
    + 追加 ${(r.inserted || []).length} 件 / スキップ ${(r.skipped || []).length} 件 / エラー ${(r.errors || []).length} 件 + ${skips ? `
      ${skips}
    ` : ''}${errs ? `
      ${errs}
    ` : ''} +
    `; +} + +function n8PickHtml() { + if (!n8.pick) return ''; + const rows = n8.issues + .map((i) => `
  • [${esc(i.project)}] ${esc(i.subject)}
  • `) + .join(''); + return `
    +

    Redmine のチケットを選ぶ

    + + ${esc(n8.pickMsg)} +
      ${rows}
    +
    `; +} + +function renderN8n() { + const d = n8.data; + if (!d) return; + const blocks = d.blocks; + const on = blocks.filter((b) => n8.checked.has(b.source) && n8CanSend(b)); + const rows = blocks + .map((b) => { + const e = n8.edits.get(b.source) || { task: b.taskName, memo: '' }; + return ` + + ${esc(b.date)} + ${fmtTime(b.start)}–${fmtTime(b.end)} + ${b.minutes} 分 + ${esc(shortPath(b.projectPath))} + + + ${n8Status(b)} + `; + }) + .join(''); + $('#detail-body').innerHTML = `
    +
    + + + + + + +
    + ${d.n8nError ? `

    n8n に接続できません: ${esc(d.n8nError)} (送信済みが分からないので送れません)

    ` : ''} + ${n8.settings ? n8Settings() : ''} + ${n8ResultHtml()} + ${n8PickHtml()} +

    区間: 空き ${d.gapMinutes} 分以上で区切り、${d.minMinutes} 分未満は除外。${blocks.length} 件

    + + ${rows || ''}
    送る日付時刻時間プロジェクトタスク名 / メモチケット状態
    この期間の区間はありません
    +
    `; + n8Wire(); +} + +function n8Block(source) { + return n8.data.blocks.find((b) => b.source === source); +} + +/** 区間の編集値を変え、必要なら同じセッションの他の区間にも入れて、ブラウザに覚える。 */ +function n8SetEdit(source, patch) { + const b = n8Block(source); + const targets = n8.sameSession ? n8.data.blocks.filter((x) => x.sessionId === b.sessionId && !x.sent) : [b]; + for (const t of targets) n8.edits.set(t.source, { ...(n8.edits.get(t.source) || {}), ...patch }); + n8SaveName(b.sessionId, n8.edits.get(source)); +} + +function n8Wire() { + $('#n8-reload').onclick = () => { + n8.from = $('#n8-from').value; + n8.to = $('#n8-to').value; + loadN8n(); + }; + $('#n8-same').onchange = (e) => { + n8.sameSession = e.target.checked; + }; + $('#n8-toggle').onclick = () => { + n8.settings = !n8.settings; + renderN8n(); + }; + $('#n8-send').onclick = n8Send; + if (n8.settings) $('#n8-save').onclick = n8SaveSettings; + if (n8.pick) { + $('#n8-find').onclick = n8Find; + $('#n8-q').onkeydown = (e) => { + if (e.key === 'Enter') n8Find(); + }; + $('#n8-pick-close').onclick = () => { + n8.pick = null; + renderN8n(); + }; + document.querySelectorAll('.n8-issue').forEach((btn) => { + btn.onclick = () => { + const issue = n8.issues.find((i) => String(i.id) === btn.dataset.id); + n8SetEdit(n8.pick, { task: `[${issue.project}] #${issue.id} ${issue.subject}` }); + n8.pick = null; + renderN8n(); + }; + }); + } + document.querySelectorAll('#n8n-table tbody tr[data-src]').forEach((tr) => { + const source = tr.dataset.src; + tr.querySelector('.n8-on').onchange = (e) => { + if (e.target.checked) n8.checked.add(source); + else n8.checked.delete(source); + renderN8n(); + }; + tr.querySelector('.n8-task').onchange = (e) => { + n8SetEdit(source, { task: e.target.value }); + renderN8n(); + }; + tr.querySelector('.n8-memo').onchange = (e) => { + n8SetEdit(source, { memo: e.target.value }); + }; + tr.querySelector('.n8-choose').onclick = () => { + n8.pick = source; + n8.issues = []; + n8.pickMsg = ''; + renderN8n(); + }; + }); +} + +async function n8Find() { + n8.pickMsg = '検索中…'; + const q = $('#n8-q').value; + renderN8n(); + try { + n8.issues = (await api('/api/redmine/search?q=' + encodeURIComponent(q))).issues; + n8.pickMsg = n8.issues.length ? '' : '見つかりません'; + } catch (e) { + n8.pickMsg = e.message; + } + renderN8n(); + $('#n8-q').value = q; +} + +async function n8SaveSettings() { + const patch = { + url: $('#n8-url').value, + verifySsl: $('#n8-verify').checked, + gapMinutes: Number($('#n8-gap').value), + minMinutes: Number($('#n8-min').value), + }; + const token = $('#n8-token').value; + if (token) patch.token = token; + try { + n8.cfg = await post('/api/n8n/config', patch); + n8.settings = false; + loadN8n(); + } catch (e) { + alert('保存できません: ' + e.message); + } +} + +async function n8Send() { + const items = n8.data.blocks + .filter((b) => n8.checked.has(b.source) && n8CanSend(b)) + .map((b) => ({ project: b.project, source: b.source, ...(n8.edits.get(b.source) || { task: b.taskName, memo: '' }) })); + if (!items.length) return; + n8.busy = true; + renderN8n(); + try { + n8.result = await post('/api/n8n/send', { items }); + } catch (e) { + n8.result = { ok: false, inserted: [], skipped: [], errors: [{ source: '', error: e.message }] }; + } + n8.busy = false; + await loadN8n({ silent: true }); +} +``` + +- [ ] **Step 4: スタイルを足す** (`web/style.css` の末尾) + +```css +/* n8n へ送る */ +.n8 { padding: 8px 12px; } +.n8-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; } +.n8-bar .spacer { flex: 1; } +.n8 .go { border-color: var(--work); color: var(--work); } +.n8 .go:disabled { border-color: var(--line); color: var(--dim); cursor: default; } +.n8 .note { color: var(--dim); font-size: 11px; } +.n8 .err-inline { color: #f7768e; font-size: 11px; } +.n8-settings, .n8-pick, .n8-result { border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; margin: 8px 0; } +.n8-settings label { display: inline-flex; gap: 4px; align-items: center; margin-right: 12px; } +.n8-result.ok { border-color: var(--work); } +.n8-result.ng { border-color: #f7768e; } +.n8-pick ul { list-style: none; padding: 0; margin: 6px 0 0; max-height: 200px; overflow: auto; } +#n8n-table th { text-align: left; font-size: 11px; color: var(--dim); padding: 4px 6px; border-bottom: 1px solid var(--line); } +#n8n-table td { padding: 4px 6px; border-bottom: 1px solid var(--line); vertical-align: middle; } +#n8n-table .time, #n8n-table .num { white-space: nowrap; font-variant-numeric: tabular-nums; } +#n8n-table .num { text-align: right; } +#n8n-table .n8-task { width: 22em; } +#n8n-table .n8-memo { width: 10em; } +#n8n-table tr.n8-sent td { color: var(--dim); } +.n8-mark { display: inline-block; padding: 0 6px; border-radius: 9px; font-size: 10.5px; border: 1px solid var(--line); } +.n8-mark.sent { color: var(--work); border-color: var(--work); } +.n8-mark.warn { color: var(--warn); border-color: var(--warn); } +``` + +- [ ] **Step 5: 検証用サーバーで画面を確かめる** + +1. `n8n.json` (利用者が Task 2 で作ったもの) を使い、`python chktime.py --port 8799 --no-browser` をバックグラウンドで起動。 +2. 組み込みブラウザで `http://127.0.0.1:8799/` を開き、「n8n へ送る」タブを押す。 +3. 確かめること (`read_page` / `javascript_tool` で): + - 直近 7 日の区間の表が出て、件数の表示と行数が合う + - 送信済み・進行中の行のチェックが無効、重なりのある行は最初チェックが外れている + - タスク名を変えると、「同じセッションの区間にも入れる」が on のとき同じセッションの他の行も変わる。ページを読み直しても残る + - 「接続設定」で合言葉の欄が空で、placeholder が「設定済み…」になっている (値は出ない) + - 「選ぶ」→ 件名で検索 → 結果をクリックすると、タスク名が `[プロジェクト] #番号 題名` になる + - 期間を逆にして「読み直す」と、エラーの文言が出る + - **「送る」は押さない** (実データを送るのは Task 8) +4. 8799 番のプロセスだけを止める。 + +- [ ] **Step 6: コミット** + +```bash +cd /c/debug/chktime && git add web/n8n.js web/index.html web/app.js web/style.css && git commit -m "n8n 連携: 作業区間を選んで送る「n8n へ送る」タブを追加 + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 8: 実際の区間を 1 件送る確認と文書 + +**Files:** +- Modify: `README.md`、`CLAUDE.md` (自分の変更だけステージする) +- 保管庫: `C:\debug\obsidian\10_プロジェクト\chktime\ナレッジ\`、`C:\debug\obsidian\00_共通ナレッジ\運用・手順\n8n(n8n.yfox.jp)のMCP接続.md` + +**Interfaces:** +- Consumes: Task 1〜7 のすべて。 + +- [ ] **Step 1: 送る区間を利用者に選んでもらう (ここで止まって待つ)** + +8799 の画面で一覧を見せ、本当に `work_log` に入れてよい区間を 1 件選んでもらう。タスク名も利用者に決めてもらう。 + +- [ ] **Step 2: 送って確かめる** + +1. 選ばれた 1 件だけチェックして「送る」→ 結果が `追加 1 件`。 +2. もう一度「読み直す」→ その行が「送信済み」になり、チェックできない。 +3. `curl -sk https://n8n.yfox.jp/webhook/home | grep -o '<タスク名の一部>'` でダッシュボードに出ること。 +4. 日報: `curl -sk -X POST -H "Content-Type: application/json" -d '{"date":"<その区間の日付>","tasks":[]}' https://n8n.yfox.jp/webhook/work-report-final | grep -o '<タスク名の一部>'` +5. 同じ区間をもう一度送れないこと (チェックが無効) を画面で確かめる。 + +- [ ] **Step 3: README.md に使い方を足す** + +「Redmine へ起票」の節の後に、次の節を足す: + +```markdown +## n8n へ送る + +右ペインの **n8n へ送る** タブで、Claude のセッションから作った「作業区間」を +n8n の work-tracker (`work_log`) に送れます。手で作業開始・終了を記録しなくても済むようにするための機能です。 + +- **作業区間**: 同じセッションの中で、応答が終わってから次の発言までの空きが 30 分未満ならつなぎ、 + 30 分以上で区切ります。5 分未満の区間は出しません (どちらも接続設定で変更可)。 + まだ応答中・ログ更新から 30 分以内の最後の区間は「進行中」で、送れません。 +- **タスク名**: 初期値は `[作業フォルダ名] セッションのタイトル`。「選ぶ」で Redmine のチケットを検索して選ぶと + `[Redmine のプロジェクト名] #番号 題名` になり、work-tracker の Redmine 工数同期とガントの実績に載ります。 + 決めた名前はセッションごとにブラウザが覚え、同じセッションの他の区間にも入ります (切り替え可)。 +- **重なり**: すでに work_log にある記録や、一覧の他の区間と時間が重なる区間には印が付き、最初は送らない状態です。 +- **二重登録の防止**: 区間ごとの `source` (`chktime:<セッションID>:<開始時刻>`) を work_log に保存し、 + n8n 側で同じ `source` は追加しません。送信済みの区間は選べません。 + ただし 2 つの画面から同時に送ると二重に入ることがあります (確認と追加が一度の処理ではないため)。 +- **設定**: `n8n.json` (git 管理外。`--n8n-config` で場所を変更可)。 + `{"url": "https://n8n.yfox.jp", "token": "<合言葉>", "verifySsl": false, "gapMinutes": 30, "minMinutes": 5}` + 合言葉は n8n の Header Auth 認証情報 (`X-WT-Token`) と同じ値。画面とログには出しません。 +- 送り先は n8n のワークフロー `work-tracker--import` (`/webhook/wt-import-rows` と `/webhook/wt-import`)。 +``` + +起動オプションの表に `| --n8n-config | n8n.json | n8n 連携の設定ファイル |` を足し、冒頭の「外部への通信は…」の文を「外部への通信は Redmine へ起票と n8n へ送るを使ったときだけです」に変える。 + +- [ ] **Step 4: CLAUDE.md を直す (自分の変更だけステージ)** + +- 「外部通信は Redmine 連携だけ」の行を次に変える: + `- **外部通信は Redmine 連携と n8n への送信だけ**。それ以外はローカルの jsonl を読むだけで、どこにも接続しない。` +- 構成の表に `web/n8n.js 「n8n へ送る」タブ` と `n8n.json n8n 連携の設定(自動生成・git 管理外)` と `tests/ unittest (python -m unittest discover -s tests -v)` を足す。 +- 「n8n 連携」の表を API の節に足す: + +```markdown +n8n 連携(work_log への取り込み): + +| ルート | 内容 | +| --- | --- | +| `GET/POST /api/n8n/config` | 接続設定の取得(合言葉は返さない)/ 保存 | +| `GET /api/n8n/blocks?from=&to=&project=` | 作業区間の一覧。n8n の送信済み・削除済み・重なりを付ける | +| `POST /api/n8n/send` | 選んだ区間を送る。時刻はサーバーがログから計算し直す | +| `GET /api/redmine/search?q=` | チケット選択用の検索(未完了のみ) | +``` + +ステージは `git add -p CLAUDE.md` で、今回足した行の hunk だけ `y` にする (利用者の「ナレッジ保管庫」の節は `n`)。対話できない環境では、`git diff CLAUDE.md > <スクラッチパッド>/claude.diff` から自分の hunk だけのパッチを作って `git apply --cached` する。 + +- [ ] **Step 5: テストを全部流してコミット** + +Run: `cd /c/debug/chktime && python -m unittest discover -s tests -v` +Expected: 28 件すべて PASS。 + +```bash +cd /c/debug/chktime && git add README.md && git commit -m "n8n へ送る機能の使い方と通信の決まりを文書に追加 + +Co-Authored-By: Claude Opus 5.5 " +``` +(`CLAUDE.md` は Step 4 でステージ済みの分だけが一緒に入る。`git show --stat HEAD` で確認) + +- [ ] **Step 6: 保管庫に記録する** + +- `10_プロジェクト\chktime\ナレッジ\n8nへ作業区間を送る仕組み.md` (ナレッジのテンプレート): 区間の作り方、`source` の形、n8n の受け口 2 本と合言葉 (Header Auth `X-WT-Token`)、同時送信の注意、テストの流し方。 +- chktime の `_index.md` のナレッジに 1 行追加。chktime の `CLAUDE.md` の「このプロジェクトの記録」表にも 1 行 (利用者の未コミットの節なので、追記は利用者に伝えるだけにして、ファイルの編集はしない)。 +- `00_共通ナレッジ\運用・手順\n8n(n8n.yfox.jp)のMCP接続.md` の注意点に: データテーブルの列追加は公開 API ではできず、n8n-mcp の `addColumn` に `N8N_MCP_ACCESS_TOKEN` が要ること (Task 1 で実際にどうなったかを書く)。 + +- [ ] **Step 7: 仕上げ** + +`superpowers:finishing-a-development-branch` に従って、ブランチの扱い (Gitea への push と PR) を利用者と決める。 diff --git a/docs/superpowers/specs/2026-10-12-n8n-worklog-import-design.md b/docs/superpowers/specs/2026-10-12-n8n-worklog-import-design.md index ef7efd8..63da14e 100644 --- a/docs/superpowers/specs/2026-10-12-n8n-worklog-import-design.md +++ b/docs/superpowers/specs/2026-10-12-n8n-worklog-import-design.md @@ -100,7 +100,7 @@ Name `X-WT-Token`) を作り、chktime の設定画面にも同じ値を入れ | `GET/POST /api/n8n/config` | 設定の取得 (合言葉は返さない) / 保存 | | `GET /api/n8n/blocks?from=&to=&project=` | 期間の区間を計算し、n8n の `wt-import-rows` の結果で「送信済み / 削除済み / 重なり」を付けて返す。n8n に届かなくても区間は返し、`n8nError` を付ける | | `POST /api/n8n/send` | `{items:[{source, task, memo}]}` を受け、時刻はサーバーが `source` から区間を計算し直して n8n の `wt-import` に送る。結果をそのまま返す | -| `GET /api/redmine/issues?project_id=&q=` | 既存の Redmine 連携で未完了チケットを検索 (チケット選択用) | +| `GET /api/redmine/search?q=` | 既存の Redmine 連携で未完了チケットを検索 (チケット選択用)。`/api/redmine/issues` は起票 (POST) が使っているので別の名前にする | 重なりの判定: 区間 [開始, 終了) と、`work_log` の行 (進行中は終了 = 今) または一覧の他の区間が 1 秒でも重なれば「重なりあり」。同じ `source` の行 (送信済みの自分) は除く。