Claude の作業区間を n8n の work_log へ送る「n8n へ送る」タブを追加 #2
+10
-1
@@ -144,6 +144,7 @@ async function selectProject(id) {
|
|||||||
$('#session-table tbody').innerHTML = '<tr><td class="loading">解析中…</td></tr>';
|
$('#session-table tbody').innerHTML = '<tr><td class="loading">解析中…</td></tr>';
|
||||||
if (state.view === 'daily') loadDaily();
|
if (state.view === 'daily') loadDaily();
|
||||||
else if (state.view === 'gantt') loadGantt();
|
else if (state.view === 'gantt') loadGantt();
|
||||||
|
else if (state.view === 'n8n') loadN8n();
|
||||||
await reloadSessions();
|
await reloadSessions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +318,7 @@ function syncTabs() {
|
|||||||
$('#tab-turns').classList.toggle('sel', state.view === 'turns');
|
$('#tab-turns').classList.toggle('sel', state.view === 'turns');
|
||||||
$('#tab-daily').classList.toggle('sel', state.view === 'daily');
|
$('#tab-daily').classList.toggle('sel', state.view === 'daily');
|
||||||
$('#tab-gantt').classList.toggle('sel', state.view === 'gantt');
|
$('#tab-gantt').classList.toggle('sel', state.view === 'gantt');
|
||||||
|
$('#tab-n8n').classList.toggle('sel', state.view === 'n8n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ganttQuery(extra) {
|
function ganttQuery(extra) {
|
||||||
@@ -343,6 +345,10 @@ function dailyQuery(extra) {
|
|||||||
|
|
||||||
function updateCsv() {
|
function updateCsv() {
|
||||||
const link = $('#csv-turns');
|
const link = $('#csv-turns');
|
||||||
|
if (state.view === 'n8n') {
|
||||||
|
link.removeAttribute('href');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state.view === 'daily') {
|
if (state.view === 'daily') {
|
||||||
link.href = '/api/daily?' + dailyQuery({ format: 'csv' });
|
link.href = '/api/daily?' + dailyQuery({ format: 'csv' });
|
||||||
link.title = '日別集計を CSV 出力';
|
link.title = '日別集計を CSV 出力';
|
||||||
@@ -363,6 +369,7 @@ function setView(view) {
|
|||||||
updateCsv();
|
updateCsv();
|
||||||
if (view === 'daily') loadDaily();
|
if (view === 'daily') loadDaily();
|
||||||
else if (view === 'gantt') loadGantt();
|
else if (view === 'gantt') loadGantt();
|
||||||
|
else if (view === 'n8n') loadN8n();
|
||||||
else renderDetail();
|
else renderDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,6 +1028,7 @@ async function refreshFollow() {
|
|||||||
await reloadSessions();
|
await reloadSessions();
|
||||||
if (state.view === 'daily') await loadDaily({ silent: true });
|
if (state.view === 'daily') await loadDaily({ silent: true });
|
||||||
else if (state.view === 'gantt') await loadGantt({ silent: true });
|
else if (state.view === 'gantt') await loadGantt({ silent: true });
|
||||||
|
else if (state.view === 'n8n') { /* 編集中の入力を消さないよう、自動追従では読み直さない */ }
|
||||||
else await reloadDetail();
|
else await reloadDetail();
|
||||||
list.scrollTop = listTop;
|
list.scrollTop = listTop;
|
||||||
// やりとりを末尾まで見ていたら、追記された行に追従する (tail -f と同じ感覚)
|
// やりとりを末尾まで見ていたら、追記された行に追従する (tail -f と同じ感覚)
|
||||||
@@ -1472,6 +1480,7 @@ $('#relative').onchange = (e) => {
|
|||||||
$('#tab-turns').onclick = () => setView('turns');
|
$('#tab-turns').onclick = () => setView('turns');
|
||||||
$('#tab-daily').onclick = () => setView('daily');
|
$('#tab-daily').onclick = () => setView('daily');
|
||||||
$('#tab-gantt').onclick = () => setView('gantt');
|
$('#tab-gantt').onclick = () => setView('gantt');
|
||||||
|
$('#tab-n8n').onclick = () => setView('n8n');
|
||||||
$('#redmine').onclick = openRedmine;
|
$('#redmine').onclick = openRedmine;
|
||||||
$('#rm-close').onclick = closeRedmine;
|
$('#rm-close').onclick = closeRedmine;
|
||||||
$('#rm-run').onclick = runRedmine;
|
$('#rm-run').onclick = runRedmine;
|
||||||
@@ -1490,7 +1499,7 @@ $('#reload').onclick = async () => {
|
|||||||
|
|
||||||
let resizeTimer = null;
|
let resizeTimer = null;
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (state.view === 'turns') return;
|
if (state.view === 'turns' || state.view === 'n8n') return;
|
||||||
clearTimeout(resizeTimer);
|
clearTimeout(resizeTimer);
|
||||||
resizeTimer = setTimeout(() => {
|
resizeTimer = setTimeout(() => {
|
||||||
if (state.view === 'daily' && state.daily) renderDaily();
|
if (state.view === 'daily' && state.daily) renderDaily();
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<button class="tab sel" id="tab-turns">やりとり</button>
|
<button class="tab sel" id="tab-turns">やりとり</button>
|
||||||
<button class="tab" id="tab-daily">日別集計</button>
|
<button class="tab" id="tab-daily">日別集計</button>
|
||||||
<button class="tab" id="tab-gantt">ガント</button>
|
<button class="tab" id="tab-gantt">ガント</button>
|
||||||
|
<button class="tab" id="tab-n8n" title="Claude の作業区間を n8n の work_log に送る">n8n へ送る</button>
|
||||||
</span>
|
</span>
|
||||||
<a id="csv-turns" class="btn" href="#" download>CSV</a>
|
<a id="csv-turns" class="btn" href="#" download>CSV</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,5 +68,6 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
<script src="n8n.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+277
@@ -0,0 +1,277 @@
|
|||||||
|
'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 = '<p class="loading">区間を計算中…</p>';
|
||||||
|
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 = `<p class="err">${esc(e.message)}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function n8Status(b) {
|
||||||
|
const marks = [];
|
||||||
|
if (b.sent) marks.push('<span class="n8-mark sent">送信済み</span>');
|
||||||
|
if (b.deleted) marks.push('<span class="n8-mark warn" title="一度送ってから work_log で削除された区間">削除済み</span>');
|
||||||
|
if (b.inProgress) marks.push('<span class="n8-mark warn">進行中</span>');
|
||||||
|
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(`<span class="n8-mark warn" title="${esc(tip)}">重なり ${b.overlaps.length}</span>`);
|
||||||
|
}
|
||||||
|
return marks.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function n8Settings() {
|
||||||
|
const c = n8.cfg || {};
|
||||||
|
return `<div class="n8-settings">
|
||||||
|
<h4>接続設定 <span class="note">${esc(c.configPath || '')}</span></h4>
|
||||||
|
<label>URL <input id="n8-url" value="${esc(c.url || '')}" placeholder="https://n8n.yfox.jp"></label>
|
||||||
|
<label>合言葉 <input id="n8-token" type="password" placeholder="${c.hasToken ? '設定済み (変えるときだけ入力)' : '未設定'}"></label>
|
||||||
|
<label class="chk"><input type="checkbox" id="n8-verify" ${c.verifySsl ? 'checked' : ''}> 証明書を検証する</label>
|
||||||
|
<label>区切る空き (分) <input id="n8-gap" type="number" min="1" max="240" value="${c.gapMinutes ?? 30}"></label>
|
||||||
|
<label>最短 (分) <input id="n8-min" type="number" min="0" max="240" value="${c.minMinutes ?? 5}"></label>
|
||||||
|
<button id="n8-save">保存</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function n8ResultHtml() {
|
||||||
|
const r = n8.result;
|
||||||
|
if (!r) return '';
|
||||||
|
const errs = (r.errors || []).map((e) => `<li>${esc(e.source)}: ${esc(e.error)}</li>`).join('');
|
||||||
|
const skips = (r.skipped || []).map((s) => `<li>${esc(s.source)}: ${esc(s.reason)}</li>`).join('');
|
||||||
|
return `<div class="n8-result ${r.ok ? 'ok' : 'ng'}">
|
||||||
|
追加 ${(r.inserted || []).length} 件 / スキップ ${(r.skipped || []).length} 件 / エラー ${(r.errors || []).length} 件
|
||||||
|
${skips ? `<ul>${skips}</ul>` : ''}${errs ? `<ul class="err-inline">${errs}</ul>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function n8PickHtml() {
|
||||||
|
if (!n8.pick) return '';
|
||||||
|
const rows = n8.issues
|
||||||
|
.map((i) => `<li><button class="n8-issue" data-id="${i.id}">#${i.id}</button> <span class="note">[${esc(i.project)}]</span> ${esc(i.subject)}</li>`)
|
||||||
|
.join('');
|
||||||
|
return `<div class="n8-pick">
|
||||||
|
<h4>Redmine のチケットを選ぶ</h4>
|
||||||
|
<input id="n8-q" placeholder="件名の一部、または番号"> <button id="n8-find">検索</button> <button id="n8-pick-close">閉じる</button>
|
||||||
|
<span class="note">${esc(n8.pickMsg)}</span>
|
||||||
|
<ul>${rows}</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `<tr data-src="${esc(b.source)}" class="${b.sent ? 'n8-sent' : ''}">
|
||||||
|
<td><input type="checkbox" class="n8-on" ${n8.checked.has(b.source) && n8CanSend(b) ? 'checked' : ''} ${n8CanSend(b) ? '' : 'disabled'}></td>
|
||||||
|
<td class="time">${esc(b.date)}</td>
|
||||||
|
<td class="time">${fmtTime(b.start)}–${fmtTime(b.end)}</td>
|
||||||
|
<td class="num">${b.minutes} 分</td>
|
||||||
|
<td class="n8-pj" title="${esc(b.projectPath)}">${esc(shortPath(b.projectPath))}</td>
|
||||||
|
<td><input class="n8-task" value="${esc(e.task)}"> <input class="n8-memo" value="${esc(e.memo)}" placeholder="メモ"></td>
|
||||||
|
<td><button class="n8-choose">選ぶ</button></td>
|
||||||
|
<td>${n8Status(b)}</td>
|
||||||
|
</tr>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
$('#detail-body').innerHTML = `<div class="n8">
|
||||||
|
<div class="n8-bar">
|
||||||
|
<label>期間 <input type="date" id="n8-from" value="${esc(n8.from)}"> – <input type="date" id="n8-to" value="${esc(n8.to)}"></label>
|
||||||
|
<button id="n8-reload">読み直す</button>
|
||||||
|
<label class="chk"><input type="checkbox" id="n8-same" ${n8.sameSession ? 'checked' : ''}> 同じセッションの区間にも入れる</label>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button id="n8-toggle">${n8.settings ? '設定を閉じる' : '接続設定'}</button>
|
||||||
|
<button id="n8-send" class="go" ${d.n8nError || n8.busy || !on.length ? 'disabled' : ''}>選んだ ${on.length} 件を送る</button>
|
||||||
|
</div>
|
||||||
|
${d.n8nError ? `<p class="err-inline">n8n に接続できません: ${esc(d.n8nError)} (送信済みが分からないので送れません)</p>` : ''}
|
||||||
|
${n8.settings ? n8Settings() : ''}
|
||||||
|
${n8ResultHtml()}
|
||||||
|
${n8PickHtml()}
|
||||||
|
<p class="note">区間: 空き ${d.gapMinutes} 分以上で区切り、${d.minMinutes} 分未満は除外。${blocks.length} 件</p>
|
||||||
|
<table id="n8n-table"><thead><tr><th>送る</th><th>日付</th><th>時刻</th><th>時間</th><th>プロジェクト</th><th>タスク名 / メモ</th><th>チケット</th><th>状態</th></tr></thead>
|
||||||
|
<tbody>${rows || '<tr><td colspan="8" class="empty">この期間の区間はありません</td></tr>'}</tbody></table>
|
||||||
|
</div>`;
|
||||||
|
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 });
|
||||||
|
}
|
||||||
@@ -395,3 +395,27 @@ table { width: 100%; border-collapse: collapse; }
|
|||||||
.gantt svg rect.t-work { fill: var(--work); }
|
.gantt svg rect.t-work { fill: var(--work); }
|
||||||
.gantt svg rect.t-none { fill: var(--idle); }
|
.gantt svg rect.t-none { fill: var(--idle); }
|
||||||
#turns tr.hi td { background: rgba(122, 162, 247, .18); }
|
#turns tr.hi td { background: rgba(122, 162, 247, .18); }
|
||||||
|
|
||||||
|
/* 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); }
|
||||||
|
|||||||
Reference in New Issue
Block a user