feat: ship agent progress visibility
CI - Build & Test / Backend (.NET) (push) Failing after 31s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 19s
CI - Build & Test / Security Check (push) Successful in 4s

This commit is contained in:
2026-06-20 20:22:54 +02:00
parent 3dd745586b
commit adae7ba26d
9 changed files with 202 additions and 45 deletions
+59 -18
View File
@@ -13,7 +13,7 @@
* - Waiting section for Iris overview
*/
import { computed, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { Plus, X, CalendarDays, Clock3, ExternalLink, Link2, ListChecks, Save, AlertTriangle, Eye, Bot, ShieldBan } from '@lucide/vue'
import { Plus, X, CalendarDays, Clock3, ExternalLink, Link2, ListChecks, Save, AlertTriangle, Eye, Bot, ShieldBan, MessageSquareText } from '@lucide/vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useTaskStore } from '../stores/tasks'
@@ -238,6 +238,22 @@ function hoursSince(dateStr: string): number {
return Math.round((now - then) / 3600000)
}
function relativeTime(date?: string | null): string {
if (!date) return 'keine Updates'
const diffMs = Date.now() - new Date(date).getTime()
const mins = Math.max(0, Math.round(diffMs / 60000))
if (mins < 1) return 'gerade eben'
if (mins < 60) return `vor ${mins} min`
const hours = Math.round(mins / 60)
if (hours < 24) return `vor ${hours} h`
const days = Math.round(hours / 24)
return `vor ${days} d`
}
function activityHint(task: BoardTask): string {
return task.lastActivityMessage?.trim() || (task.expectedFrom ? `Wartet auf ${expectedFromLabel(task.expectedFrom)}` : 'Noch kein relevanter Progress-Status')
}
/* ── Task Navigation ───────────────────────────── */
function navigateToTask(taskId: string) {
router.push('/tasks/' + taskId)
@@ -401,9 +417,12 @@ onUnmounted(() => {
Warte auf Iris <span class="section-count">{{ waitingForIrisCount }}</span>
</div>
<div v-if="taskStore.waitingForIrisTasks.length === 0" class="iris-empty">Keine Tasks</div>
<div v-for="t in taskStore.waitingForIrisTasks" :key="t.id" class="iris-task-row">
<span class="iris-task-title">{{ t.title }}</span>
<span class="iris-task-meta">{{ stateLabel(t.state) }}</span>
<div v-for="t in taskStore.waitingForIrisTasks" :key="t.id" class="iris-task-row progress-row">
<div>
<span class="iris-task-title">{{ t.title }}</span>
<div class="iris-task-progress">{{ activityHint(t) }}</div>
</div>
<span class="iris-task-meta">{{ relativeTime(t.lastActivityAt ?? t.updatedAt) }}</span>
</div>
</section>
@@ -413,9 +432,12 @@ onUnmounted(() => {
Warte auf Bao <span class="section-count">{{ waitingForBaoCount }}</span>
</div>
<div v-if="taskStore.waitingForBaoTasks.length === 0" class="iris-empty">Keine Tasks</div>
<div v-for="t in taskStore.waitingForBaoTasks" :key="t.id" class="iris-task-row">
<span class="iris-task-title">{{ t.title }}</span>
<span class="iris-task-meta">{{ stateLabel(t.state) }}</span>
<div v-for="t in taskStore.waitingForBaoTasks" :key="t.id" class="iris-task-row progress-row">
<div>
<span class="iris-task-title">{{ t.title }}</span>
<div class="iris-task-progress">{{ activityHint(t) }}</div>
</div>
<span class="iris-task-meta">{{ relativeTime(t.lastActivityAt ?? t.updatedAt) }}</span>
</div>
</section>
@@ -425,8 +447,11 @@ onUnmounted(() => {
Warte auf andere <span class="section-count">{{ taskStore.waitingForOthersTasks.length }}</span>
</div>
<div v-if="taskStore.waitingForOthersTasks.length === 0" class="iris-empty">Keine Tasks</div>
<div v-for="t in taskStore.waitingForOthersTasks" :key="t.id" class="iris-task-row">
<span class="iris-task-title">{{ t.title }}</span>
<div v-for="t in taskStore.waitingForOthersTasks" :key="t.id" class="iris-task-row progress-row">
<div>
<span class="iris-task-title">{{ t.title }}</span>
<div class="iris-task-progress">{{ activityHint(t) }}</div>
</div>
<span class="iris-task-meta">{{ expectedFromLabel(t.expectedFrom) }}</span>
</div>
</section>
@@ -437,8 +462,11 @@ onUnmounted(() => {
Stale Tasks <span class="section-count stale-count">{{ staleCount }}</span>
</div>
<div v-if="taskStore.staleTasksList.length === 0" class="iris-empty">Keine stale Tasks</div>
<div v-for="t in taskStore.staleTasksList" :key="t.id" class="iris-task-row stale-row">
<span class="iris-task-title">{{ t.title }}</span>
<div v-for="t in taskStore.staleTasksList" :key="t.id" class="iris-task-row stale-row progress-row">
<div>
<span class="iris-task-title">{{ t.title }}</span>
<div class="iris-task-progress">{{ activityHint(t) }}</div>
</div>
<span class="iris-task-meta stale-meta">{{ hoursSince(t.updatedAt) }}h offen</span>
</div>
</section>
@@ -493,7 +521,8 @@ onUnmounted(() => {
</div>
<div class="card-title">{{ task.title }}</div>
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
<div class="card-meta">{{ new Date(task.createdAt).toLocaleDateString('de-DE') }}</div>
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
</button>
<div v-if="!taskStore.board.offen.length" class="empty-col">Keine Aufgaben</div>
</div>
@@ -541,7 +570,8 @@ onUnmounted(() => {
</div>
<div class="card-title">{{ task.title }}</div>
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
<div class="card-meta">{{ new Date(task.createdAt).toLocaleDateString('de-DE') }}</div>
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
</button>
<div v-if="!taskStore.board.inProgress.length" class="empty-col">Keine Aufgaben</div>
</div>
@@ -589,7 +619,8 @@ onUnmounted(() => {
</div>
<div class="card-title">{{ task.title }}</div>
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
<div class="card-meta">{{ new Date(task.createdAt).toLocaleDateString('de-DE') }}</div>
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
</button>
<div v-if="!taskStore.board.delegated.length" class="empty-col">Keine delegierten Aufgaben</div>
</div>
@@ -637,7 +668,8 @@ onUnmounted(() => {
</div>
<div class="card-title">{{ task.title }}</div>
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
<div class="card-meta">{{ new Date(task.createdAt).toLocaleDateString('de-DE') }}</div>
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
</button>
<div v-if="!taskStore.board.review.length" class="empty-col">Keine Aufgaben</div>
</div>
@@ -680,7 +712,8 @@ onUnmounted(() => {
</div>
<div class="card-title">{{ task.title }}</div>
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
<div class="card-meta">{{ new Date(task.createdAt).toLocaleDateString('de-DE') }}</div>
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
</button>
<div v-if="!taskStore.board.done.length" class="empty-col">Keine Aufgaben</div>
</div>
@@ -728,7 +761,7 @@ onUnmounted(() => {
</div>
<div class="card-title">{{ task.title }}</div>
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
<div class="card-meta">{{ new Date(task.createdAt).toLocaleDateString('de-DE') }}</div>
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
</button>
<div v-if="!taskStore.board.blocked.length" class="empty-col">Keine Blockierer</div>
</div>
@@ -818,6 +851,10 @@ onUnmounted(() => {
<span v-if="selectedTask.expectedFrom" class="meta-expected"> Erwartet: {{ selectedTask.expectedFrom }}</span>
<span><Clock3 :size="13" /> Aktualisiert {{ formatDate(selectedTask.updatedAt, true) }}</span>
<span><CalendarDays :size="13" /> Erstellt {{ formatDate(selectedTask.createdAt) }}</span>
<span v-if="selectedTask.isAgentTask"><MessageSquareText :size="13" /> Letzter Status {{ relativeTime(selectedTask.lastActivityAt ?? selectedTask.updatedAt) }}</span>
</div>
<div v-if="selectedTask.isAgentTask" class="detail-progress-banner">
<strong>Letzter Fortschritt:</strong> {{ activityHint(selectedTask) }}
</div>
</div>
@@ -1003,7 +1040,9 @@ onUnmounted(() => {
.iris-empty { font-size: 11px; color: var(--tx-3); font-style: italic; padding: 8px; text-align: center; }
.iris-task-row { padding: 6px 8px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.iris-task-row:last-child { border-bottom: none; }
.iris-task-title { font-size: 11.5px; font-weight: 500; color: var(--tx); }
.progress-row { align-items: flex-start; }
.iris-task-title { font-size: 11.5px; font-weight: 500; color: var(--tx); display: block; }
.iris-task-progress { margin-top: 4px; font-size: 10px; color: var(--tx-3); line-height: 1.35; }
.iris-task-meta { font-size: 10px; color: var(--tx-3); white-space: nowrap; }
.stale-row { background: rgba(244,63,94,.05); border-radius: 4px; }
.stale-meta { color: #fda4af; font-weight: 600; }
@@ -1041,6 +1080,7 @@ select:disabled { opacity: .45; cursor: not-allowed; }
.assignee-bao { background: rgba(59, 130, 246, .12); color: #60a5fa; }
.card-title { font-size: 12.5px; font-weight: 600; color: var(--tx); line-height: 1.4; word-break: break-word; font-family: 'Manrope', sans-serif; }
.card-preview { margin-top: 6px; font-size: 11px; line-height: 1.45; color: var(--tx-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.card-progress-hint { margin-top: 7px; font-size: 10.5px; color: var(--tx-2); line-height: 1.4; padding: 6px 8px; border-radius: 8px; background: rgba(124,108,255,.07); border: 1px solid rgba(124,108,255,.12); }
.card-meta { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--tx-3); margin-top: 5px; font-variant-numeric: tabular-nums; }
.empty-col, .detail-empty { display: flex; align-items: center; justify-content: center; padding: 24px 12px; font-size: 11px; color: var(--tx-3); font-style: italic; font-family: 'Manrope', sans-serif; }
@@ -1092,6 +1132,7 @@ select:disabled { opacity: .45; cursor: not-allowed; }
.detail-meta-row span { display: inline-flex; align-items: center; gap: 5px; }
.meta-agent-tag { color: #c084fc; }
.meta-expected { color: #a78bfa; }
.detail-progress-banner { padding: 10px 12px; border-radius: 12px; background: rgba(124,108,255,.08); border: 1px solid rgba(124,108,255,.14); color: var(--tx-2); font-size: 12px; }
.detail-section { background: rgba(255,255,255,.02); border: 1px solid var(--line); border-radius: 16px; padding: 16px; }
.detail-section-header, .sidebar-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; color: var(--tx-2); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
.detail-textarea { width: 100%; min-height: 180px; border-radius: 12px; border: 1px solid var(--line); background: rgba(10,9,24,.55); color: var(--tx); padding: 14px; font-size: 14px; line-height: 1.6; outline: none; box-sizing: border-box; }