From 87e9d80b450162cf22f3ee0d35c6925a98afdb7d Mon Sep 17 00:00:00 2001 From: Logan Date: Thu, 2 Apr 2026 13:10:27 -0700 Subject: [PATCH] Refactor auth dashboard work item rendering (#1228) --- website/public/auth/index.html | 545 ++++++++++++++++++++------------- 1 file changed, 325 insertions(+), 220 deletions(-) diff --git a/website/public/auth/index.html b/website/public/auth/index.html index bdf1c44e..1fdcfe45 100644 --- a/website/public/auth/index.html +++ b/website/public/auth/index.html @@ -320,36 +320,49 @@ border-color: #ef4444; } - /* Task list styles */ - .task-item { + /* Shared work item styles */ + .work-item, + .task-item, + .routine-item { display: flex; flex-direction: column; padding: 1rem; background: var(--surface, #1a1a1a); border: 1px solid var(--border, #333); + } + .task-item { border-radius: 8px; margin-bottom: 0.5rem; gap: 0.5rem; } + .routine-item { + border-radius: 10px; + gap: 0.75rem; + } + .work-item-header, .task-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; } + .work-item-title, .task-name { font-weight: 600; flex: 1; min-width: 0; word-break: break-word; } + .work-item-badge, .task-badge { - padding: 0.25rem 0.5rem; - border-radius: 4px; font-size: 0.75rem; font-weight: 600; flex-shrink: 0; } + .task-badge { + padding: 0.25rem 0.5rem; + border-radius: 4px; + } .task-badge.success { background: rgba(34, 197, 94, 0.2); color: #22c55e; @@ -366,6 +379,7 @@ background: rgba(156, 163, 175, 0.2); color: #9ca3af; } + .work-item-meta-flow, .task-details { display: flex; flex-wrap: wrap; @@ -373,26 +387,31 @@ font-size: 0.85rem; color: var(--text-muted, #888); } + .work-item-meta-flow-item, .task-detail { display: flex; align-items: center; gap: 0.25rem; } + .work-item-meta-label, .task-detail-label { opacity: 0.7; } + .work-section-header, .tasks-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } + .work-section-controls, .tasks-controls { display: flex; align-items: center; gap: 0.5rem; } + .work-section-status, .tasks-status { font-size: 0.85rem; color: var(--text-muted, #888); @@ -447,43 +466,41 @@ flex-direction: column; gap: 0.75rem; } - .routine-item { - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 1rem; - background: var(--surface, #1a1a1a); - border: 1px solid var(--border, #333); - border-radius: 10px; - } + .work-item-meta-grid, .routine-meta-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.6rem 1rem; } + .work-item-meta-block, .routine-meta-item { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; } + .work-item-meta-grid .work-item-meta-label, .routine-meta-label { font-size: 0.75rem; color: var(--text-muted, #888); text-transform: uppercase; letter-spacing: 0.04em; } + .work-item-meta-value, .routine-meta-value { font-size: 0.9rem; color: var(--text, #fff); word-break: break-word; } + .routine-status-badge, + .work-item-badge { + flex-shrink: 0; + } .routine-status-badge { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; - flex-shrink: 0; } .routine-status-badge.active { background: rgba(34, 197, 94, 0.16); @@ -505,11 +522,13 @@ background: rgba(239, 68, 68, 0.18); color: #f87171; } + .work-item-actions, .routine-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; } + .work-item-action-btn, .routine-action-btn { border: 1px solid var(--border, #333); background: transparent; @@ -521,22 +540,27 @@ cursor: pointer; transition: all 0.2s ease; } + .work-item-action-btn:hover:not(:disabled), .routine-action-btn:hover:not(:disabled) { border-color: color-mix(in srgb, var(--accent, #38bdf8) 45%, var(--border, #333)); color: color-mix(in srgb, var(--accent, #38bdf8) 82%, var(--text, #fff)); } + .work-item-action-btn:disabled, .routine-action-btn:disabled { opacity: 0.55; cursor: wait; } + .work-item-action-btn.danger:hover:not(:disabled), .routine-action-btn.danger:hover:not(:disabled) { border-color: rgba(239, 68, 68, 0.7); color: #f87171; } + .work-item-inline-error, .routine-inline-error { font-size: 0.85rem; color: #ef4444; } + .work-item-empty-state, .routine-empty-state { padding: 1.5rem; text-align: center; @@ -1932,8 +1956,8 @@

Setup checklist

@@ -5834,7 +5858,11 @@

${escapeHtml(title)}

if (!res.ok) { if (res.status === 404) { cachedTasks = []; - taskList.innerHTML = '
  • No tasks yet. Send Oliver one request from a connected app and it will show up here.
  • '; + taskList.innerHTML = renderWorkListState({ + tagName: 'li', + className: 'task-item work-item', + message: 'No tasks yet. Send Oliver one request from a connected app and it will show up here.' + }); loadWorkspaceSummaryFromStorage(); renderNextSteps(cachedIdentifiers); return; @@ -5880,7 +5908,11 @@

    ${escapeHtml(title)}

    } if (allTasks.length === 0) { - taskList.innerHTML = '
  • No tasks yet. Send Oliver one request from a connected app and it will show up here.
  • '; + taskList.innerHTML = renderWorkListState({ + tagName: 'li', + className: 'task-item work-item', + message: 'No tasks yet. Send Oliver one request from a connected app and it will show up here.' + }); } else { taskList.innerHTML = allTasks.map(task => renderTask(task)).join(''); } @@ -5892,7 +5924,12 @@

    ${escapeHtml(title)}

    const displayMessage = normalizeApiFetchError(err); console.error('Failed to load tasks:', err); cachedTasks = []; - taskList.innerHTML = `
  • Error loading tasks: ${escapeHtml(displayMessage)}
  • `; + taskList.innerHTML = renderWorkListState({ + tagName: 'li', + className: 'task-item work-item', + message: `Error loading tasks: ${displayMessage}`, + tone: 'danger' + }); loadWorkspaceSummaryFromStorage(); renderNextSteps(cachedIdentifiers); } finally { @@ -5902,6 +5939,21 @@

    ${escapeHtml(title)}

    } } + const WORK_CHANNEL_LABELS = { + 'email': 'Email', + 'slack': 'Slack', + 'discord': 'Discord', + 'sms': 'SMS', + 'telegram': 'Telegram', + 'whatsapp': 'WhatsApp', + 'google_docs': 'Google Docs', + 'google_sheets': 'Google Sheets', + 'google_slides': 'Google Slides', + 'bluebubbles': 'iMessage', + 'wechat': 'WeChat', + 'lark': 'Lark' + }; + // Build concise task titles that describe what each item is doing function humanizeTaskToken(value) { if (!value || typeof value !== 'string') return ''; @@ -5914,92 +5966,178 @@

    ${escapeHtml(title)}

    .join(' '); } + function formatWorkChannelLabel(channel) { + const normalized = String(channel || '').trim().toLowerCase(); + return WORK_CHANNEL_LABELS[normalized] || humanizeTaskToken(normalized) || 'Unknown'; + } + + function formatLocalDateTime(dateStr, timeOptions = { hour: '2-digit', minute: '2-digit' }) { + if (!dateStr) return 'N/A'; + const date = new Date(dateStr); + if (Number.isNaN(date.getTime())) return 'N/A'; + const timeLabel = timeOptions + ? date.toLocaleTimeString([], timeOptions) + : date.toLocaleTimeString(); + return `${date.toLocaleDateString()} ${timeLabel}`; + } + + function renderWorkListState({ tagName = 'div', className = 'work-item-empty-state', message, tone = 'muted' }) { + const color = tone === 'danger' ? '#ef4444' : 'var(--text-muted, #888)'; + return `<${tagName} class="${className}" style="text-align: center; color: ${color};">${escapeHtml(message)}`; + } + + function renderWorkMetaFlowItem(item) { + const toneStyle = item.tone === 'danger' ? ' style="color: #ef4444;"' : ''; + const value = item.value == null || item.value === '' ? 'N/A' : String(item.value); + return ` + + ${escapeHtml(item.label)}: + ${escapeHtml(value)} + + `; + } + + function renderWorkMetaFlow(items, trailingHtml = '') { + return ` +
    + ${items.map((item) => renderWorkMetaFlowItem(item)).join('')} + ${trailingHtml} +
    + `; + } + + function renderWorkMetaGrid(items) { + return ` +
    + ${items.map((item) => { + const value = item.value == null || item.value === '' ? 'N/A' : String(item.value); + return ` +
    + ${escapeHtml(item.label)} + ${escapeHtml(value)} +
    + `; + }).join('')} +
    + `; + } + + function renderWorkActionButton(action) { + const disabledAttr = action.disabled ? 'disabled' : ''; + const dangerClass = action.tone === 'danger' ? ' danger' : ''; + return ` + + `; + } + + function renderWorkActions(actions) { + if (!Array.isArray(actions) || actions.length === 0) { + return ''; + } + return ` +
    + ${actions.map((action) => renderWorkActionButton(action)).join('')} +
    + `; + } + + function renderWorkItemCard(item) { + const metaHtml = item.metaLayout === 'grid' + ? renderWorkMetaGrid(item.metaItems || []) + : renderWorkMetaFlow(item.metaItems || [], item.metaSuffixHtml || ''); + const actionsHtml = renderWorkActions(item.actions); + const inlineErrorHtml = item.inlineError + ? `
    ${escapeHtml(item.inlineError)}
    ` + : ''; + + return ` + <${item.tagName} class="${item.className}"> +
    + ${escapeHtml(item.title)} + ${escapeHtml(item.badgeLabel)} +
    + ${metaHtml} + ${actionsHtml} + ${inlineErrorHtml} + + `; + } + function formatTaskTitle(task) { const kind = (task.kind || '').toLowerCase(); const channel = (task.channel || task.source || '').toLowerCase(); const requestSummary = (task.request_summary || '').trim(); - const channelMap = { - 'email': 'Email', - 'slack': 'Slack', - 'discord': 'Discord', - 'sms': 'SMS', - 'telegram': 'Telegram', - 'whatsapp': 'WhatsApp', - 'google_docs': 'Google Docs', - 'google_sheets': 'Google Sheets', - 'google_slides': 'Google Slides', - 'bluebubbles': 'iMessage', - 'wechat': 'WeChat' - }; - if (requestSummary) return requestSummary; if (kind === 'send_email') return 'Send Reply'; if (kind === 'noop') return 'No-op'; if (kind === 'run_task') { - const channelLabel = channelMap[channel] || humanizeTaskToken(channel); + const channelLabel = formatWorkChannelLabel(channel); return channelLabel ? `Handle ${channelLabel}` : 'Handle Task'; } return humanizeTaskToken(kind) || 'Task'; } - // Render a single task item - function renderTask(task) { - const status = task.execution_status || 'running'; - const statusLabel = status.charAt(0).toUpperCase() + status.slice(1); - - // Format dates - const formatDate = (dateStr) => { - if (!dateStr) return 'N/A'; - const date = new Date(dateStr); - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - }; - - // Determine schedule display - let scheduleDisplay = task.schedule_type || 'One-time'; - if (task.schedule_type === 'once' && task.run_at) { - scheduleDisplay = `Once at ${formatDate(task.run_at)}`; - } else if (task.schedule_type === 'cron' && task.next_run) { - scheduleDisplay = `Next: ${formatDate(task.next_run)}`; + function buildTaskScheduleDisplay(task) { + const scheduleType = String(task.schedule_type || '').toLowerCase(); + if ((scheduleType === 'once' || scheduleType === 'one_shot') && task.run_at) { + return `Once at ${formatLocalDateTime(task.run_at)}`; + } + if (scheduleType === 'cron' && task.next_run) { + return `Next: ${formatLocalDateTime(task.next_run)}`; } + return task.schedule_type || 'One-time'; + } - // Prefer response time when available, fallback to request creation time - const updatedAt = task.last_run || task.execution_started_at || task.created_at; - const updatedDisplay = formatDate(updatedAt); + function buildTaskModalRows(task) { + return [ + { label: 'Task ID', value: task.id || 'N/A' }, + { label: 'Channel', value: formatWorkChannelLabel(task.channel || task.source) }, + { label: 'Status', value: task.execution_status || 'running' }, + { label: 'Enabled', value: task.enabled ? 'Yes' : 'No' }, + { label: 'Schedule Type', value: task.schedule_type || 'N/A' }, + { label: 'Created At', value: formatLocalDateTime(task.created_at, null) }, + { label: 'Next Run', value: formatLocalDateTime(task.next_run, null) }, + { label: 'Last Run', value: formatLocalDateTime(task.last_run, null) }, + ]; + } - // Encode task data for modal + function mapTaskToWorkItem(task) { + const status = String(task.execution_status || 'running').toLowerCase(); + const statusLabel = status.charAt(0).toUpperCase() + status.slice(1); + const updatedAt = task.last_run || task.execution_started_at || task.created_at; const taskData = encodeURIComponent(JSON.stringify(task)); + const metaItems = [ + { label: 'Channel', value: formatWorkChannelLabel(task.channel || task.source) }, + { label: 'Schedule', value: buildTaskScheduleDisplay(task) }, + { label: 'Updated', value: formatLocalDateTime(updatedAt) } + ]; - return ` -
  • -
    - ${escapeHtml(formatTaskTitle(task))} - ${statusLabel} -
    -
    - - Channel: - ${escapeHtml(task.channel || task.source || 'Unknown')} - - - Schedule: - ${scheduleDisplay} - - - Updated: - ${updatedDisplay} - - ${task.error_message ? ` - - Error: - ${escapeHtml(parseErrorMessage(task.error_message))} - - ` : ''} - -
    -
  • - `; + if (task.error_message) { + metaItems.push({ + label: 'Error', + value: parseErrorMessage(task.error_message), + tone: 'danger' + }); + } + + return { + tagName: 'li', + className: 'task-item work-item', + title: formatTaskTitle(task), + badgeLabel: statusLabel, + badgeClassName: `task-badge work-item-badge ${status}`, + metaLayout: 'flow', + metaItems, + metaSuffixHtml: `` + }; + } + + function renderTask(task) { + return renderWorkItemCard(mapTaskToWorkItem(task)); } // Escape HTML to prevent XSS @@ -6060,25 +6198,6 @@

    ${escapeHtml(title)}

    renderRoutineList(); } - function formatRoutineChannelLabel(channel) { - const channelMap = { - 'email': 'Email', - 'slack': 'Slack', - 'discord': 'Discord', - 'sms': 'SMS', - 'telegram': 'Telegram', - 'whatsapp': 'WhatsApp', - 'google_docs': 'Google Docs', - 'google_sheets': 'Google Sheets', - 'google_slides': 'Google Slides', - 'bluebubbles': 'iMessage', - 'wechat': 'WeChat', - 'lark': 'Lark' - }; - const normalized = String(channel || '').trim().toLowerCase(); - return channelMap[normalized] || humanizeTaskToken(normalized) || 'Unknown'; - } - function formatUtcDateTime(dateStr) { if (!dateStr) return 'N/A'; const date = new Date(dateStr); @@ -6139,6 +6258,69 @@

    ${escapeHtml(title)}

    return { label: 'Paused', className: 'paused' }; } + function buildRoutineModalRows(routine) { + return [ + { label: 'Routine / Task ID', value: routine.id || 'N/A' }, + { label: 'Title', value: routine.name || 'Scheduled Oliver work' }, + { label: 'Channel', value: formatWorkChannelLabel(routine.channel) }, + { label: 'Enabled', value: routine.enabled ? 'Yes' : 'No' }, + { label: 'Schedule Type', value: formatRoutineCadence(routine) }, + { label: 'Next Run', value: formatRoutineNextRun(routine) }, + { label: 'Last Run', value: formatUtcDateTime(routine.last_run) }, + { label: 'Created At', value: formatUtcDateTime(routine.created_at) }, + { label: 'Latest Error', value: routine.error_message ? parseErrorMessage(routine.error_message) : 'None' } + ]; + } + + function mapRoutineToWorkItem(routine) { + const badge = routineStatusBadge(routine); + const routineData = encodeURIComponent(JSON.stringify(routine)); + const isBusy = routineActionBusyIds.has(routine.id); + const errorMessage = routineActionErrors[routine.id]; + const title = (routine.name || '').trim() || 'Scheduled Oliver work'; + const busyLabel = isBusy ? 'Working...' : ''; + + return { + tagName: 'article', + className: 'routine-item work-item', + title, + badgeLabel: badge.label, + badgeClassName: `routine-status-badge work-item-badge ${badge.className}`, + metaLayout: 'grid', + metaItems: [ + { label: 'Source', value: formatWorkChannelLabel(routine.channel) }, + { label: 'Cadence', value: formatRoutineCadence(routine) }, + { label: 'Next Run', value: formatRoutineNextRun(routine) }, + { label: 'Last Result', value: formatRoutineLastResult(routine) } + ], + actions: [ + routine.enabled + ? { + label: busyLabel || 'Pause', + onClick: `handleRoutineAction('${routine.id}', 'pause')`, + disabled: isBusy + } + : { + label: busyLabel || 'Resume', + onClick: `handleRoutineAction('${routine.id}', 'resume')`, + disabled: isBusy + }, + { + label: busyLabel || 'Delete', + onClick: `handleRoutineAction('${routine.id}', 'delete')`, + disabled: isBusy, + tone: 'danger' + }, + { + label: 'View details', + onClick: `showRoutineModal('${routineData}')`, + disabled: false + } + ], + inlineError: errorMessage || '' + }; + } + function renderRoutineList() { const routineList = document.getElementById('routine-list'); if (!routineList) return; @@ -6150,14 +6332,20 @@

    ${escapeHtml(title)}

    const current = activeRoutineTab === 'history' ? history : active; if (active.length === 0 && history.length === 0) { - routineList.innerHTML = '
    No routines yet. When Oliver schedules repeat work, you can manage it here.
    '; + routineList.innerHTML = renderWorkListState({ + className: 'routine-empty-state work-item-empty-state', + message: 'No routines yet. When Oliver schedules repeat work, you can manage it here.' + }); return; } if (current.length === 0) { - routineList.innerHTML = activeRoutineTab === 'history' - ? '
    No routine history yet.
    ' - : '
    No active routines right now.
    '; + routineList.innerHTML = renderWorkListState({ + className: 'routine-empty-state work-item-empty-state', + message: activeRoutineTab === 'history' + ? 'No routine history yet.' + : 'No active routines right now.' + }); return; } @@ -6165,57 +6353,7 @@

    ${escapeHtml(title)}

    } function renderRoutine(routine) { - const badge = routineStatusBadge(routine); - const routineData = encodeURIComponent(JSON.stringify(routine)); - const isBusy = routineActionBusyIds.has(routine.id); - const errorMessage = routineActionErrors[routine.id]; - const title = (routine.name || '').trim() || 'Scheduled Oliver work'; - const busyLabel = isBusy ? 'Working...' : ''; - - return ` -
    -
    - ${escapeHtml(title)} - ${badge.label} -
    -
    -
    - Source - ${escapeHtml(formatRoutineChannelLabel(routine.channel))} -
    -
    - Cadence - ${escapeHtml(formatRoutineCadence(routine))} -
    -
    - Next Run - ${escapeHtml(formatRoutineNextRun(routine))} -
    -
    - Last Result - ${escapeHtml(formatRoutineLastResult(routine))} -
    -
    -
    - ${routine.enabled ? ` - - ` : ` - - `} - - -
    - ${errorMessage ? `
    ${escapeHtml(errorMessage)}
    ` : ''} -
    - `; + return renderWorkItemCard(mapRoutineToWorkItem(routine)); } async function loadRoutines(accessToken) { @@ -6256,7 +6394,11 @@

    ${escapeHtml(title)}

    console.error('Failed to load routines:', err); setRoutineTabState(); if (!hadExistingData) { - routineList.innerHTML = `
    Error loading routines: ${escapeHtml(displayMessage)}
    `; + routineList.innerHTML = renderWorkListState({ + className: 'routine-empty-state work-item-empty-state', + message: `Error loading routines: ${displayMessage}`, + tone: 'danger' + }); } routinesStatus.textContent = `Error: ${displayMessage}`; } @@ -6306,73 +6448,36 @@

    ${escapeHtml(title)}

    } } - function showRoutineModal(encodedRoutine) { - const routine = JSON.parse(decodeURIComponent(encodedRoutine)); - const modalTitle = document.getElementById('task-modal-title'); - const modalContent = document.getElementById('task-modal-content'); - - if (modalTitle) { - modalTitle.textContent = 'Routine Details'; - } - - const rows = [ - { label: 'Routine / Task ID', value: routine.id || 'N/A' }, - { label: 'Title', value: routine.name || 'Scheduled Oliver work' }, - { label: 'Channel', value: formatRoutineChannelLabel(routine.channel) }, - { label: 'Enabled', value: routine.enabled ? 'Yes' : 'No' }, - { label: 'Schedule Type', value: formatRoutineCadence(routine) }, - { label: 'Next Run', value: formatRoutineNextRun(routine) }, - { label: 'Last Run', value: formatUtcDateTime(routine.last_run) }, - { label: 'Created At', value: formatUtcDateTime(routine.created_at) }, - { label: 'Latest Error', value: routine.error_message ? parseErrorMessage(routine.error_message) : 'None' } - ]; - - modalContent.innerHTML = rows.map((row) => ` + function renderWorkItemModalRows(rows) { + return rows.map((row) => `
    ${row.label} ${escapeHtml(String(row.value))}
    `).join(''); - - document.getElementById('task-modal-overlay').classList.add('active'); } - // Show task details modal - function showTaskModal(encodedTask) { - const task = JSON.parse(decodeURIComponent(encodedTask)); + function showWorkItemModal(title, rows) { const modalTitle = document.getElementById('task-modal-title'); const modalContent = document.getElementById('task-modal-content'); if (modalTitle) { - modalTitle.textContent = 'Task Details'; + modalTitle.textContent = title; } - const formatModalDate = (dateStr) => { - if (!dateStr) return 'N/A'; - const date = new Date(dateStr); - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); - }; - - // Build modal content - const rows = [ - { label: 'Task ID', value: task.id || 'N/A' }, - { label: 'Channel', value: task.channel || 'N/A' }, - { label: 'Status', value: task.execution_status || 'running' }, - { label: 'Enabled', value: task.enabled ? 'Yes' : 'No' }, - { label: 'Schedule Type', value: task.schedule_type || 'N/A' }, - { label: 'Created At', value: formatModalDate(task.created_at) }, - { label: 'Next Run', value: formatModalDate(task.next_run) }, - { label: 'Last Run', value: formatModalDate(task.last_run) }, - ]; + modalContent.innerHTML = renderWorkItemModalRows(rows); + document.getElementById('task-modal-overlay').classList.add('active'); + } - modalContent.innerHTML = rows.map(row => ` -
    - ${row.label} - ${escapeHtml(String(row.value))} -
    - `).join(''); + function showRoutineModal(encodedRoutine) { + const routine = JSON.parse(decodeURIComponent(encodedRoutine)); + showWorkItemModal('Routine Details', buildRoutineModalRows(routine)); + } - document.getElementById('task-modal-overlay').classList.add('active'); + // Show task details modal + function showTaskModal(encodedTask) { + const task = JSON.parse(decodeURIComponent(encodedTask)); + showWorkItemModal('Task Details', buildTaskModalRows(task)); } // Close task details modal