mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-29 13:10:17 +00:00
Stage 319: PR #1886 — Kanban lifecycle controls by @franksong2702
This commit is contained in:
+16
-8
@@ -490,6 +490,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'Include archived',
|
||||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
@@ -515,7 +516,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'Add comment',
|
||||
kanban_only_mine: 'Only mine',
|
||||
kanban_bulk_action: 'Bulk action',
|
||||
kanban_nudge_dispatcher: 'Nudge dispatcher',
|
||||
kanban_nudge_dispatcher: 'Preview dispatcher',
|
||||
kanban_stats: 'Stats',
|
||||
kanban_worker_log: 'Worker log',
|
||||
kanban_block: 'Block',
|
||||
@@ -1509,6 +1510,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'アーカイブを含める',
|
||||
kanban_no_matching_tasks: '一致するタスクがありません',
|
||||
kanban_no_data: 'カンバンデータがありません',
|
||||
kanban_work_queue_hint: 'これは Hermes Agent のワークキューです。タスクを作成またはトリアージし、担当者を割り当て、Ready に移動すると、ディスパッチャーがそれをクレームします。',
|
||||
kanban_unavailable: 'カンバンを利用できません',
|
||||
kanban_read_only: '読み取り専用',
|
||||
kanban_empty: '空',
|
||||
@@ -1534,7 +1536,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'コメント追加',
|
||||
kanban_only_mine: '自分のみ',
|
||||
kanban_bulk_action: '一括操作',
|
||||
kanban_nudge_dispatcher: 'ディスパッチャーに催促',
|
||||
kanban_nudge_dispatcher: 'ディスパッチャープレビュー',
|
||||
kanban_stats: '統計',
|
||||
kanban_worker_log: 'ワーカーログ',
|
||||
kanban_block: 'ブロック',
|
||||
@@ -2367,6 +2369,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'Include archived',
|
||||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
@@ -2392,7 +2395,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'Add comment',
|
||||
kanban_only_mine: 'Only mine',
|
||||
kanban_bulk_action: 'Bulk action',
|
||||
kanban_nudge_dispatcher: 'Nudge dispatcher',
|
||||
kanban_nudge_dispatcher: 'Preview dispatcher',
|
||||
kanban_stats: 'Stats',
|
||||
kanban_worker_log: 'Worker log',
|
||||
kanban_block: 'Block',
|
||||
@@ -3320,6 +3323,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'Include archived',
|
||||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
@@ -3345,7 +3349,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'Add comment',
|
||||
kanban_only_mine: 'Only mine',
|
||||
kanban_bulk_action: 'Bulk action',
|
||||
kanban_nudge_dispatcher: 'Nudge dispatcher',
|
||||
kanban_nudge_dispatcher: 'Preview dispatcher',
|
||||
kanban_stats: 'Stats',
|
||||
kanban_worker_log: 'Worker log',
|
||||
kanban_block: 'Block',
|
||||
@@ -4261,6 +4265,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'Include archived',
|
||||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
@@ -4286,7 +4291,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'Add comment',
|
||||
kanban_only_mine: 'Only mine',
|
||||
kanban_bulk_action: 'Bulk action',
|
||||
kanban_nudge_dispatcher: 'Nudge dispatcher',
|
||||
kanban_nudge_dispatcher: 'Preview dispatcher',
|
||||
kanban_stats: 'Stats',
|
||||
kanban_worker_log: 'Worker log',
|
||||
kanban_block: 'Block',
|
||||
@@ -5223,6 +5228,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'Include archived',
|
||||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
@@ -5248,7 +5254,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'Add comment',
|
||||
kanban_only_mine: 'Only mine',
|
||||
kanban_bulk_action: 'Bulk action',
|
||||
kanban_nudge_dispatcher: 'Nudge dispatcher',
|
||||
kanban_nudge_dispatcher: 'Preview dispatcher',
|
||||
kanban_stats: 'Stats',
|
||||
kanban_worker_log: 'Worker log',
|
||||
kanban_block: 'Block',
|
||||
@@ -7216,6 +7222,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'Include archived',
|
||||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
@@ -7241,7 +7248,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'Add comment',
|
||||
kanban_only_mine: 'Only mine',
|
||||
kanban_bulk_action: 'Bulk action',
|
||||
kanban_nudge_dispatcher: 'Nudge dispatcher',
|
||||
kanban_nudge_dispatcher: 'Preview dispatcher',
|
||||
kanban_stats: 'Stats',
|
||||
kanban_worker_log: 'Worker log',
|
||||
kanban_block: 'Block',
|
||||
@@ -8133,6 +8140,7 @@ const LOCALES = {
|
||||
kanban_include_archived: 'Include archived',
|
||||
kanban_no_matching_tasks: 'No matching tasks',
|
||||
kanban_no_data: 'No Kanban data',
|
||||
kanban_work_queue_hint: 'This is the Hermes Agent work queue. Create or triage a task, assign it, move it to Ready, then let the dispatcher claim it.',
|
||||
kanban_unavailable: 'Kanban unavailable',
|
||||
kanban_read_only: 'Read-only view',
|
||||
kanban_empty: 'Empty',
|
||||
@@ -8158,7 +8166,7 @@ const LOCALES = {
|
||||
kanban_add_comment: 'Add comment',
|
||||
kanban_only_mine: 'Only mine',
|
||||
kanban_bulk_action: 'Bulk action',
|
||||
kanban_nudge_dispatcher: 'Nudge dispatcher',
|
||||
kanban_nudge_dispatcher: 'Preview dispatcher',
|
||||
kanban_stats: 'Stats',
|
||||
kanban_worker_log: 'Worker log',
|
||||
kanban_block: 'Block',
|
||||
|
||||
+3
-3
@@ -158,9 +158,9 @@
|
||||
<label class="kanban-check"><input id="kanbanOnlyMine" type="checkbox" onchange="loadKanban(true)"> <span data-i18n="kanban_only_mine">Only mine</span></label>
|
||||
<div id="kanbanStats" class="kanban-stats" aria-live="polite"></div>
|
||||
<div id="kanbanBulkBar" class="kanban-bulk-bar">
|
||||
<select id="kanbanBulkStatus" aria-label="Bulk status"><option value="">Status</option><option value="ready">Ready</option><option value="running">Running</option><option value="blocked">Blocked</option><option value="done">Done</option><option value="archived">Archived</option></select>
|
||||
<select id="kanbanBulkStatus" aria-label="Bulk status"><option value="">Status</option><option value="ready">Ready</option><option value="blocked">Blocked</option><option value="done">Done</option><option value="archived">Archived</option></select>
|
||||
<button class="btn secondary" onclick="bulkUpdateKanban()" data-i18n="kanban_bulk_action">Bulk action</button>
|
||||
<button class="btn secondary" onclick="nudgeKanbanDispatcher()" data-i18n="kanban_nudge_dispatcher">Nudge dispatcher</button>
|
||||
<button class="btn secondary" onclick="nudgeKanbanDispatcher()" data-i18n="kanban_nudge_dispatcher">Preview dispatcher</button>
|
||||
</div>
|
||||
<div class="kanban-new-task-row">
|
||||
<input id="kanbanNewTaskTitle" placeholder="New task" data-i18n-placeholder="kanban_new_task" onkeydown="if(event.key==='Enter')createKanbanTask()">
|
||||
@@ -709,7 +709,7 @@
|
||||
</div>
|
||||
<div class="main-view-actions">
|
||||
<button class="panel-head-btn has-tooltip has-tooltip--bottom" id="btnKanbanCreateBoard" onclick="openKanbanCreateBoard()" data-tooltip="New board" data-i18n-title="kanban_new_board" aria-label="New board"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><line x1="17.5" y1="14" x2="17.5" y2="21"/><line x1="14" y1="17.5" x2="21" y2="17.5"/></svg></button>
|
||||
<button class="panel-head-btn has-tooltip has-tooltip--bottom" onclick="nudgeKanbanDispatcher()" data-tooltip="Nudge dispatcher" data-i18n-title="kanban_nudge_dispatcher" aria-label="Nudge dispatcher">▶</button>
|
||||
<button class="panel-head-btn has-tooltip has-tooltip--bottom" onclick="nudgeKanbanDispatcher()" data-tooltip="Preview dispatcher" data-i18n-title="kanban_nudge_dispatcher" aria-label="Preview dispatcher">▶</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-task-preview" id="kanbanTaskPreview" style="display:none"></div>
|
||||
|
||||
+11
-3
@@ -1096,10 +1096,9 @@ function _kanbanCardStalenessClass(task){
|
||||
function _kanbanCardQuickActions(task){
|
||||
const id = esc(task.id || '');
|
||||
const status = task.status || '';
|
||||
const start = status !== 'running' && status !== 'done' && status !== 'archived' ? `<button type="button" class="kanban-card-action" onclick="quickKanbanCardAction(event,'${id}','running')">${esc(t('kanban_card_start'))}</button>` : '';
|
||||
const complete = status !== 'done' && status !== 'archived' ? `<button type="button" class="kanban-card-action" onclick="quickKanbanCardAction(event,'${id}','done')">${esc(t('kanban_card_complete'))}</button>` : '';
|
||||
const archive = status !== 'archived' ? `<button type="button" class="kanban-card-action danger" onclick="quickKanbanCardAction(event,'${id}','archived')">${esc(t('kanban_card_archive'))}</button>` : '';
|
||||
return `<div class="kanban-card-actions" onclick="event.stopPropagation()">${start}${complete}${archive}</div>`;
|
||||
return `<div class="kanban-card-actions" onclick="event.stopPropagation()">${complete}${archive}</div>`;
|
||||
}
|
||||
|
||||
async function quickKanbanCardAction(event, taskId, status){
|
||||
@@ -1168,17 +1167,25 @@ function _kanbanRenderProfileLanes(columns){
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
function _kanbanEmptyBoardHtml(){
|
||||
return `<div class="main-view-empty"><div class="main-view-empty-title">${esc(t('kanban_no_data'))}</div><div class="main-view-empty-sub">${esc(t('kanban_work_queue_hint'))}</div></div>`;
|
||||
}
|
||||
|
||||
function _kanbanRenderBoard(){
|
||||
const board = $('kanbanBoard');
|
||||
if (!board) return;
|
||||
if (!_kanbanBoard || !_kanbanBoard.columns) {
|
||||
board.innerHTML = `<div class="main-view-empty"><div class="main-view-empty-title">${esc(t('kanban_no_data'))}</div></div>`;
|
||||
board.innerHTML = _kanbanEmptyBoardHtml();
|
||||
return;
|
||||
}
|
||||
const columns = _kanbanVisibleTasks();
|
||||
const total = columns.reduce((n, col) => n + (col.tasks || []).length, 0);
|
||||
if ($('kanbanSummary')) $('kanbanSummary').textContent = String(t('kanban_visible_tasks')).replace('{0}', total);
|
||||
_kanbanRenderSidebar(columns);
|
||||
if (total === 0) {
|
||||
board.innerHTML = _kanbanEmptyBoardHtml();
|
||||
return;
|
||||
}
|
||||
board.innerHTML = _kanbanLanesByProfile ? _kanbanRenderProfileLanes(columns) : columns.map(_kanbanRenderColumn).join('');
|
||||
}
|
||||
|
||||
@@ -1219,6 +1226,7 @@ async function hardRefreshWebUIClient(){
|
||||
function _kanbanLooksLikeStaleClientError(err){
|
||||
const msg = String((err && err.message) || err || '').toLowerCase();
|
||||
return !!(err && err.status === 404 && (
|
||||
msg === 'not found' ||
|
||||
msg.includes('unknown kanban endpoint') ||
|
||||
msg.includes('stale cached bundle')
|
||||
));
|
||||
|
||||
@@ -164,6 +164,7 @@ def test_kanban_dashboard_parity_i18n_keys_exist():
|
||||
"kanban_only_mine",
|
||||
"kanban_bulk_action",
|
||||
"kanban_nudge_dispatcher",
|
||||
"kanban_work_queue_hint",
|
||||
"kanban_stats",
|
||||
"kanban_worker_log",
|
||||
"kanban_block",
|
||||
@@ -205,6 +206,16 @@ def test_kanban_ui_parity_polish_adds_card_metadata_quick_actions_and_swimlanes(
|
||||
assert "javascript:" not in PANELS.lower()
|
||||
|
||||
|
||||
def test_kanban_lifecycle_controls_do_not_offer_manual_running_start():
|
||||
assert "quickKanbanCardAction(event,'${id}','running')" not in PANELS
|
||||
assert "kanban_card_start" not in PANELS
|
||||
assert '<option value="running">Running</option>' not in INDEX
|
||||
assert "Cannot set status to 'running' directly" not in PANELS
|
||||
assert "kanban_work_queue_hint" in PANELS
|
||||
assert "Preview dispatcher" in INDEX
|
||||
assert "Nudge dispatcher" not in INDEX
|
||||
|
||||
|
||||
def test_kanban_ui_parity_polish_css_and_i18n_exist():
|
||||
for selector in (
|
||||
".kanban-profile-lanes",
|
||||
@@ -219,7 +230,7 @@ def test_kanban_ui_parity_polish_css_and_i18n_exist():
|
||||
):
|
||||
assert selector in STYLE
|
||||
locale_blocks = re.findall(r"\n\s*([a-z]{2}(?:-[A-Z]{2})?): \{(.*?)\n\s*\},", I18N, flags=re.S)
|
||||
required_keys = ["kanban_lanes_by_profile", "kanban_card_start", "kanban_card_complete", "kanban_card_archive", "kanban_unassigned"]
|
||||
required_keys = ["kanban_lanes_by_profile", "kanban_card_complete", "kanban_card_archive", "kanban_unassigned", "kanban_work_queue_hint"]
|
||||
missing = [
|
||||
f"{locale}:{key}"
|
||||
for locale, body in locale_blocks
|
||||
|
||||
Reference in New Issue
Block a user