diff --git a/NodeSrv/apps/app-portal/src/n8nTree.js b/NodeSrv/apps/app-portal/src/n8nTree.js
index 0a1ac65e..3c8597d0 100644
--- a/NodeSrv/apps/app-portal/src/n8nTree.js
+++ b/NodeSrv/apps/app-portal/src/n8nTree.js
@@ -22,6 +22,9 @@ function buildTagTree(workflows) {
}
const result = [...groups.values()];
+ for (const group of result) {
+ group.children.sort((a, b) => a.name.localeCompare(b.name));
+ }
result.sort((a, b) => {
if (a.id === UNTAGGED_ID) return 1;
if (b.id === UNTAGGED_ID) return -1;
diff --git a/NodeSrv/apps/app-portal/src/n8nView.js b/NodeSrv/apps/app-portal/src/n8nView.js
index b6169825..98987483 100644
--- a/NodeSrv/apps/app-portal/src/n8nView.js
+++ b/NodeSrv/apps/app-portal/src/n8nView.js
@@ -37,6 +37,25 @@ const EXECUTION_BADGE = {
unknown: 'bg-secondary',
};
+const EXECUTION_STATUS_LABEL = {
+ success: '成功',
+ error: 'エラー',
+ crashed: 'クラッシュ',
+ running: '実行中',
+ waiting: '待機中',
+ canceled: 'キャンセル',
+ new: '新規',
+ unknown: '不明',
+};
+
+function formatJst(isoString) {
+ if (!isoString) return '';
+ const utc = new Date(isoString);
+ const jst = new Date(utc.getTime() + 9 * 60 * 60 * 1000);
+ const pad = (n) => String(n).padStart(2, '0');
+ return `${jst.getUTCFullYear()}-${pad(jst.getUTCMonth() + 1)}-${pad(jst.getUTCDate())} ${pad(jst.getUTCHours())}:${pad(jst.getUTCMinutes())}:${pad(jst.getUTCSeconds())}`;
+}
+
function renderExecutionList(executions) {
if (!executions || executions.length === 0) {
return '
実行履歴なし
';
@@ -44,14 +63,15 @@ function renderExecutionList(executions) {
const rows = executions
.map((e) => {
const badgeClass = EXECUTION_BADGE[e.status] || 'bg-secondary';
+ const statusLabel = EXECUTION_STATUS_LABEL[e.status] || e.status;
return `
- | ${escapeHtml(e.startedAt || '')} |
- ${escapeHtml(e.status)} |
+ ${escapeHtml(formatJst(e.startedAt))} |
+ ${escapeHtml(statusLabel)} |
`;
})
.join('\n');
return `
- | 開始日時 | 状態 |
+ | 開始日時(JST) | 状態 |
${rows}
`;
}
diff --git a/NodeSrv/apps/app-portal/test/n8nTree.test.js b/NodeSrv/apps/app-portal/test/n8nTree.test.js
index 256d4219..81cac4e8 100644
--- a/NodeSrv/apps/app-portal/test/n8nTree.test.js
+++ b/NodeSrv/apps/app-portal/test/n8nTree.test.js
@@ -43,6 +43,18 @@ test('buildTagTree puts a multi-tagged workflow into every one of its tag groups
assert.strictEqual(tree[1].children.length, 1);
});
+test('buildTagTree sorts workflows within a group alphabetically by name', () => {
+ const workflows = [
+ { id: 'w2', name: 'Zワークフロー', active: true, tags: [{ name: 'タグA' }] },
+ { id: 'w1', name: 'Aワークフロー', active: true, tags: [{ name: 'タグA' }] },
+ ];
+ const tree = buildTagTree(workflows);
+ assert.deepStrictEqual(
+ tree[0].children.map((w) => w.name),
+ ['Aワークフロー', 'Zワークフロー']
+ );
+});
+
test('buildTagTree sorts tag groups alphabetically and puts "未分類" last', () => {
const workflows = [
{ id: 'w1', name: 'A', active: true, tags: [{ name: 'Zタグ' }] },
diff --git a/NodeSrv/apps/app-portal/test/n8nView.test.js b/NodeSrv/apps/app-portal/test/n8nView.test.js
index 38eedb08..9193c7ad 100644
--- a/NodeSrv/apps/app-portal/test/n8nView.test.js
+++ b/NodeSrv/apps/app-portal/test/n8nView.test.js
@@ -49,6 +49,20 @@ test('renderExecutionList renders a row per execution with a status badge', () =
assert.ok(html.includes('bg-danger'));
});
+test('renderExecutionList shows the started time converted to JST, not raw UTC', () => {
+ const html = renderExecutionList([{ id: 'e1', status: 'success', startedAt: '2026-09-19T00:00:00.000Z' }]);
+ assert.ok(html.includes('2026-09-19 09:00:00'));
+ assert.ok(!html.includes('2026-09-19T00:00:00.000Z'));
+});
+
+test('renderExecutionList shows a Japanese label for each status', () => {
+ const statuses = ['success', 'error', 'running', 'waiting', 'canceled'];
+ for (const status of statuses) {
+ const html = renderExecutionList([{ id: 'e1', status, startedAt: '2026-09-19T00:00:00.000Z' }]);
+ assert.ok(!html.includes(`>${status}<`), `expected status "${status}" to be translated`);
+ }
+});
+
test('renderExecutionList shows a message when there are no executions', () => {
const html = renderExecutionList([]);
assert.ok(html.includes('実行履歴なし'));