+ {/* Header */}
+
+
+
Kanban Board
+
+ {/* Epic Filter */}
+
+
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+ {/* Board Columns */}
+
+ {COLUMNS.map((column) => (
+
+ {/* Column Header */}
+
+
+
{column.title}
+
+ {filteredTickets[column.status]?.length || 0}
+
+
+
+
+ {/* Ticket List */}
+
+ {filteredTickets[column.status]?.map((ticket) => (
+
+ ))}
+
+
+ ))}
+
+
+ {/* Dialogs */}
+ {showCreateTicket && (
+
setShowCreateTicket(false)}
+ onCreated={handleTicketCreated}
+ epics={epics}
+ projectId={projectId}
+ backendUrl={backendUrl}
+ />
+ )}
+
+ {showCreateEpic && (
+ setShowCreateEpic(false)}
+ onCreated={handleEpicCreated}
+ projectId={projectId}
+ backendUrl={backendUrl}
+ />
+ )}
+
+ )
+}
diff --git a/ushadow/launcher/src/components/TicketCard.tsx b/ushadow/launcher/src/components/TicketCard.tsx
new file mode 100644
index 00000000..752b0fc6
--- /dev/null
+++ b/ushadow/launcher/src/components/TicketCard.tsx
@@ -0,0 +1,149 @@
+import { useState } from 'react'
+import { Tag, Folder, GitBranch, Terminal, Clock, AlertCircle } from 'lucide-react'
+import type { Ticket, Epic } from './KanbanBoard'
+
+interface TicketCardProps {
+ ticket: Ticket
+ epics: Epic[]
+ onUpdate: () => void
+ backendUrl: string
+}
+
+const PRIORITY_COLORS = {
+ low: 'bg-gray-600',
+ medium: 'bg-blue-600',
+ high: 'bg-orange-600',
+ urgent: 'bg-red-600',
+}
+
+const PRIORITY_LABELS = {
+ low: 'Low',
+ medium: 'Medium',
+ high: 'High',
+ urgent: 'Urgent',
+}
+
+export function TicketCard({ ticket, epics, onUpdate, backendUrl }: TicketCardProps) {
+ const [isDragging, setIsDragging] = useState(false)
+
+ // Find epic for this ticket
+ const epic = ticket.epic_id ? epics.find(e => e.id === ticket.epic_id) : null
+
+ // Determine color: ticket color > epic color > generated color
+ const getCardColor = (): string => {
+ if (ticket.color) return ticket.color
+ if (epic?.color) return epic.color
+ // Generate color from ticket ID hash
+ const hash = ticket.id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
+ const hue = hash % 360
+ return `hsl(${hue}, 70%, 60%)`
+ }
+
+ const cardColor = getCardColor()
+
+ // Convert hex/hsl to rgb for border
+ const getBorderStyle = () => {
+ return {
+ borderLeft: `4px solid ${cardColor}`,
+ }
+ }
+
+ const handleDragStart = (e: React.DragEvent) => {
+ setIsDragging(true)
+ e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.setData('text/plain', ticket.id)
+ }
+
+ const handleDragEnd = () => {
+ setIsDragging(false)
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ {ticket.title}
+
+
+ {PRIORITY_LABELS[ticket.priority]}
+
+
+
+ {/* Description */}
+ {ticket.description && (
+
+ {ticket.description}
+
+ )}
+
+ {/* Epic Badge */}
+ {epic && (
+
+
+ {epic.title}
+
+ )}
+
+ {/* Tags */}
+ {ticket.tags.length > 0 && (
+
+ {ticket.tags.map((tag, idx) => (
+
+
+ {tag}
+
+ ))}
+
+ )}
+
+ {/* Footer Metadata */}
+
+ {/* Branch */}
+ {ticket.branch_name && (
+
+
+ {ticket.branch_name}
+
+ )}
+
+ {/* Tmux Window */}
+ {ticket.tmux_window_name && (
+
+
+ Active
+
+ )}
+
+ {/* ID */}
+
+ #{ticket.id.slice(-6)}
+
+
+
+ )
+}
diff --git a/ushadow/launcher/src/store/appStore.ts b/ushadow/launcher/src/store/appStore.ts
index f6d256fb..db17d9b3 100644
--- a/ushadow/launcher/src/store/appStore.ts
+++ b/ushadow/launcher/src/store/appStore.ts
@@ -1,7 +1,7 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
-export type AppMode = 'install' | 'infra' | 'environments'
+export type AppMode = 'install' | 'infra' | 'environments' | 'kanban'
export interface SpoofedPrerequisites {
git_installed: boolean | null // null = use real value