feat: webhook to Clawdbot when task activated + session auth for all mutations

This commit is contained in:
2026-01-28 23:36:40 +00:00
parent b7df98bd94
commit 9a99b612ba
4 changed files with 373 additions and 55 deletions

View File

@@ -19,19 +19,15 @@ const sourceColors: Record<string, string> = {
const statusActions: Record<TaskStatus, { label: string; next: TaskStatus }[]> = {
active: [
{ label: "⏸ Pause", next: "queued" },
{ label: "🚫 Block", next: "blocked" },
{ label: "✅ Complete", next: "completed" },
{ label: "❌ Cancel", next: "cancelled" },
],
queued: [
{ label: "▶ Activate", next: "active" },
{ label: "🚫 Block", next: "blocked" },
{ label: "❌ Cancel", next: "cancelled" },
],
blocked: [
{ label: "▶ Activate", next: "active" },
{ label: "📋 Queue", next: "queued" },
{ label: "❌ Cancel", next: "cancelled" },
],
completed: [{ label: "🔄 Requeue", next: "queued" }],
cancelled: [{ label: "🔄 Requeue", next: "queued" }],
@@ -56,6 +52,7 @@ interface TaskCardProps {
isFirst?: boolean;
isLast?: boolean;
isActive?: boolean;
onClick?: () => void;
}
export function TaskCard({
@@ -66,77 +63,64 @@ export function TaskCard({
isFirst,
isLast,
isActive,
onClick,
}: TaskCardProps) {
const actions = statusActions[task.status] || [];
const noteCount = task.progressNotes?.length || 0;
return (
<div
className={`rounded-lg border p-4 transition-all ${
onClick={onClick}
className={`rounded-xl border p-4 transition-all cursor-pointer group ${
isActive
? "border-amber-400 bg-amber-50 shadow-lg shadow-amber-100"
: "border-gray-200 bg-white shadow-sm hover:shadow-md"
? "border-amber-300 bg-gradient-to-r from-amber-50 to-orange-50 shadow-lg shadow-amber-100/50 hover:shadow-xl hover:shadow-amber-200/50"
: "border-gray-200 bg-white shadow-sm hover:shadow-md hover:border-gray-300"
}`}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap mb-1">
<div className="flex items-center gap-2 flex-wrap mb-1.5">
{isActive && (
<span className="inline-flex items-center text-xs font-bold text-amber-700 bg-amber-200 px-2 py-0.5 rounded-full animate-pulse">
ACTIVE
<span className="relative flex h-2.5 w-2.5 mr-0.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-amber-500"></span>
</span>
)}
<h3 className="font-semibold text-gray-900 truncate">{task.title}</h3>
<h3 className={`font-semibold truncate ${isActive ? "text-amber-900" : "text-gray-900"}`}>
{task.title}
</h3>
</div>
<div className="flex items-center gap-2 mb-2 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${priorityColors[task.priority]}`}>
{task.priority}
</span>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${sourceColors[task.source]}`}>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${sourceColors[task.source] || sourceColors.other}`}>
{task.source}
</span>
<span className="text-xs text-gray-400">
created {timeAgo(task.createdAt)}
{timeAgo(task.createdAt)}
</span>
{task.updatedAt !== task.createdAt && (
<span className="text-xs text-gray-400">
· updated {timeAgo(task.updatedAt)}
{noteCount > 0 && (
<span className="text-xs text-gray-400 flex items-center gap-0.5">
💬 {noteCount}
</span>
)}
</div>
{task.description && (
<p className="text-sm text-gray-600 mb-2 line-clamp-2">{task.description}</p>
)}
{task.progressNotes && task.progressNotes.length > 0 && (
<details className="mt-2">
<summary className="text-xs text-gray-500 cursor-pointer hover:text-gray-700">
{task.progressNotes.length} progress note{task.progressNotes.length !== 1 ? "s" : ""}
</summary>
<div className="mt-1 space-y-1 pl-2 border-l-2 border-gray-200">
{task.progressNotes
.slice()
.reverse()
.map((note, i) => (
<div key={i} className="text-xs text-gray-600">
<span className="text-gray-400">{timeAgo(note.timestamp)}</span>{" "}
{note.note}
</div>
))}
</div>
</details>
<p className="text-sm text-gray-500 line-clamp-1">{task.description}</p>
)}
</div>
<div className="flex flex-col gap-1 shrink-0">
<div className="flex items-center gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
{/* Reorder buttons for queued tasks */}
{task.status === "queued" && (
<div className="flex gap-1 mb-1">
<div className="flex gap-1 mr-1">
<button
onClick={onMoveUp}
disabled={isFirst}
className="text-xs px-1.5 py-0.5 rounded bg-gray-100 hover:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed"
className="text-xs px-1.5 py-0.5 rounded-md bg-gray-100 hover:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition"
title="Move up"
>
@@ -144,7 +128,7 @@ export function TaskCard({
<button
onClick={onMoveDown}
disabled={isLast}
className="text-xs px-1.5 py-0.5 rounded bg-gray-100 hover:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed"
className="text-xs px-1.5 py-0.5 rounded-md bg-gray-100 hover:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition"
title="Move down"
>
@@ -152,16 +136,23 @@ export function TaskCard({
</div>
)}
{/* Status actions */}
{actions.map((action) => (
{/* Quick status actions - show fewer on card, full set in detail */}
{actions.slice(0, 2).map((action) => (
<button
key={action.next}
onClick={() => onStatusChange(task.id, action.next)}
className="text-xs px-2 py-1 rounded border border-gray-200 hover:bg-gray-50 whitespace-nowrap text-left"
className="text-xs px-2.5 py-1.5 rounded-lg border border-gray-200 hover:bg-gray-50 whitespace-nowrap text-gray-600 hover:text-gray-800 transition font-medium"
>
{action.label}
</button>
))}
{/* Expand indicator */}
<div className="ml-1 text-gray-300 group-hover:text-gray-500 transition">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>