Skip to content

Commit 7620ff7

Browse files
catlog22claude
andcommitted
feat(issue-manager): Add queue history modal for viewing and switching queues
- Add GET /api/queue/history endpoint to fetch all queues from index - Add GET /api/queue/:id endpoint to fetch specific queue details - Add POST /api/queue/switch endpoint to switch active queue - Add History button in queue toolbar - Add queue history modal with list view and detail view - Add switch functionality to change active queue - Add CSS styles for queue history components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent d705a3e commit 7620ff7

File tree

3 files changed

+571
-0
lines changed

3 files changed

+571
-0
lines changed

ccw/src/core/routes/issue-routes.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,82 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
236236
return true;
237237
}
238238

239+
// GET /api/queue/history - Get queue history (all queues from index)
240+
if (pathname === '/api/queue/history' && req.method === 'GET') {
241+
const queuesDir = join(issuesDir, 'queues');
242+
const indexPath = join(queuesDir, 'index.json');
243+
244+
if (!existsSync(indexPath)) {
245+
res.writeHead(200, { 'Content-Type': 'application/json' });
246+
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
247+
return true;
248+
}
249+
250+
try {
251+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
252+
res.writeHead(200, { 'Content-Type': 'application/json' });
253+
res.end(JSON.stringify(index));
254+
} catch {
255+
res.writeHead(200, { 'Content-Type': 'application/json' });
256+
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
257+
}
258+
return true;
259+
}
260+
261+
// GET /api/queue/:id - Get specific queue by ID
262+
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
263+
if (queueDetailMatch && req.method === 'GET' && queueDetailMatch[1] !== 'history' && queueDetailMatch[1] !== 'reorder') {
264+
const queueId = queueDetailMatch[1];
265+
const queuesDir = join(issuesDir, 'queues');
266+
const queueFilePath = join(queuesDir, `${queueId}.json`);
267+
268+
if (!existsSync(queueFilePath)) {
269+
res.writeHead(404, { 'Content-Type': 'application/json' });
270+
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
271+
return true;
272+
}
273+
274+
try {
275+
const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
276+
res.writeHead(200, { 'Content-Type': 'application/json' });
277+
res.end(JSON.stringify(groupQueueByExecutionGroup(queue)));
278+
} catch {
279+
res.writeHead(500, { 'Content-Type': 'application/json' });
280+
res.end(JSON.stringify({ error: 'Failed to read queue' }));
281+
}
282+
return true;
283+
}
284+
285+
// POST /api/queue/switch - Switch active queue
286+
if (pathname === '/api/queue/switch' && req.method === 'POST') {
287+
handlePostRequest(req, res, async (body: any) => {
288+
const { queueId } = body;
289+
if (!queueId) return { error: 'queueId required' };
290+
291+
const queuesDir = join(issuesDir, 'queues');
292+
const indexPath = join(queuesDir, 'index.json');
293+
const queueFilePath = join(queuesDir, `${queueId}.json`);
294+
295+
if (!existsSync(queueFilePath)) {
296+
return { error: `Queue ${queueId} not found` };
297+
}
298+
299+
try {
300+
const index = existsSync(indexPath)
301+
? JSON.parse(readFileSync(indexPath, 'utf8'))
302+
: { active_queue_id: null, queues: [] };
303+
304+
index.active_queue_id = queueId;
305+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
306+
307+
return { success: true, active_queue_id: queueId };
308+
} catch (err) {
309+
return { error: 'Failed to switch queue' };
310+
}
311+
});
312+
return true;
313+
}
314+
239315
// POST /api/queue/reorder - Reorder queue items
240316
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
241317
handlePostRequest(req, res, async (body: any) => {

ccw/src/templates/dashboard-css/32-issue-manager.css

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,3 +2542,260 @@
25422542
min-width: 80px;
25432543
}
25442544
}
2545+
2546+
/* ==========================================
2547+
QUEUE HISTORY MODAL
2548+
========================================== */
2549+
2550+
.queue-history-list {
2551+
display: flex;
2552+
flex-direction: column;
2553+
gap: 0.75rem;
2554+
}
2555+
2556+
.queue-history-item {
2557+
padding: 1rem;
2558+
background: hsl(var(--muted) / 0.3);
2559+
border: 1px solid hsl(var(--border));
2560+
border-radius: 0.5rem;
2561+
cursor: pointer;
2562+
transition: all 0.15s ease;
2563+
}
2564+
2565+
.queue-history-item:hover {
2566+
background: hsl(var(--muted) / 0.5);
2567+
border-color: hsl(var(--primary) / 0.3);
2568+
}
2569+
2570+
.queue-history-item.active {
2571+
border-color: hsl(var(--primary));
2572+
background: hsl(var(--primary) / 0.1);
2573+
}
2574+
2575+
.queue-history-header {
2576+
display: flex;
2577+
align-items: center;
2578+
gap: 0.75rem;
2579+
margin-bottom: 0.5rem;
2580+
}
2581+
2582+
.queue-history-id {
2583+
font-size: 0.875rem;
2584+
font-weight: 600;
2585+
color: hsl(var(--foreground));
2586+
}
2587+
2588+
.queue-active-badge {
2589+
padding: 0.125rem 0.5rem;
2590+
font-size: 0.625rem;
2591+
font-weight: 600;
2592+
text-transform: uppercase;
2593+
background: hsl(var(--primary));
2594+
color: hsl(var(--primary-foreground));
2595+
border-radius: 9999px;
2596+
}
2597+
2598+
.queue-history-status {
2599+
padding: 0.125rem 0.5rem;
2600+
font-size: 0.75rem;
2601+
border-radius: 0.25rem;
2602+
background: hsl(var(--muted));
2603+
color: hsl(var(--muted-foreground));
2604+
}
2605+
2606+
.queue-history-status.active {
2607+
background: hsl(142 76% 36% / 0.2);
2608+
color: hsl(142 76% 36%);
2609+
}
2610+
2611+
.queue-history-status.completed {
2612+
background: hsl(142 76% 36% / 0.2);
2613+
color: hsl(142 76% 36%);
2614+
}
2615+
2616+
.queue-history-status.archived {
2617+
background: hsl(var(--muted));
2618+
color: hsl(var(--muted-foreground));
2619+
}
2620+
2621+
.queue-history-meta {
2622+
display: flex;
2623+
flex-wrap: wrap;
2624+
gap: 1rem;
2625+
margin-bottom: 0.75rem;
2626+
}
2627+
2628+
.queue-history-actions {
2629+
display: flex;
2630+
gap: 0.5rem;
2631+
justify-content: flex-end;
2632+
}
2633+
2634+
/* Queue Detail View */
2635+
.queue-detail-view {
2636+
padding: 0.5rem 0;
2637+
}
2638+
2639+
.queue-detail-header {
2640+
display: flex;
2641+
align-items: center;
2642+
padding-bottom: 1rem;
2643+
border-bottom: 1px solid hsl(var(--border));
2644+
}
2645+
2646+
.queue-detail-stats {
2647+
display: grid;
2648+
grid-template-columns: repeat(4, 1fr);
2649+
gap: 0.75rem;
2650+
}
2651+
2652+
.queue-detail-stats .stat-item {
2653+
text-align: center;
2654+
padding: 0.75rem;
2655+
background: hsl(var(--muted) / 0.3);
2656+
border-radius: 0.5rem;
2657+
}
2658+
2659+
.queue-detail-stats .stat-value {
2660+
display: block;
2661+
font-size: 1.5rem;
2662+
font-weight: 700;
2663+
color: hsl(var(--foreground));
2664+
}
2665+
2666+
.queue-detail-stats .stat-label {
2667+
font-size: 0.75rem;
2668+
color: hsl(var(--muted-foreground));
2669+
}
2670+
2671+
.queue-detail-stats .stat-item.completed .stat-value {
2672+
color: hsl(142 76% 36%);
2673+
}
2674+
2675+
.queue-detail-stats .stat-item.pending .stat-value {
2676+
color: hsl(48 96% 53%);
2677+
}
2678+
2679+
.queue-detail-stats .stat-item.failed .stat-value {
2680+
color: hsl(0 84% 60%);
2681+
}
2682+
2683+
.queue-detail-groups {
2684+
display: flex;
2685+
flex-direction: column;
2686+
gap: 1rem;
2687+
}
2688+
2689+
.queue-group-section {
2690+
background: hsl(var(--muted) / 0.2);
2691+
border: 1px solid hsl(var(--border));
2692+
border-radius: 0.5rem;
2693+
overflow: hidden;
2694+
}
2695+
2696+
.queue-group-header {
2697+
display: flex;
2698+
align-items: center;
2699+
gap: 0.5rem;
2700+
padding: 0.75rem 1rem;
2701+
background: hsl(var(--muted) / 0.3);
2702+
border-bottom: 1px solid hsl(var(--border));
2703+
font-weight: 500;
2704+
}
2705+
2706+
.queue-group-items {
2707+
padding: 0.5rem;
2708+
}
2709+
2710+
.queue-detail-item {
2711+
display: flex;
2712+
align-items: center;
2713+
gap: 1rem;
2714+
padding: 0.5rem 0.75rem;
2715+
border-radius: 0.25rem;
2716+
}
2717+
2718+
.queue-detail-item:hover {
2719+
background: hsl(var(--muted) / 0.3);
2720+
}
2721+
2722+
.queue-detail-item .item-id {
2723+
min-width: 120px;
2724+
color: hsl(var(--muted-foreground));
2725+
}
2726+
2727+
.queue-detail-item .item-issue {
2728+
min-width: 80px;
2729+
color: hsl(var(--primary));
2730+
}
2731+
2732+
.queue-detail-item .item-status {
2733+
margin-left: auto;
2734+
padding: 0.125rem 0.5rem;
2735+
font-size: 0.75rem;
2736+
border-radius: 0.25rem;
2737+
background: hsl(var(--muted));
2738+
color: hsl(var(--muted-foreground));
2739+
}
2740+
2741+
.queue-detail-item .item-status.completed {
2742+
background: hsl(142 76% 36% / 0.2);
2743+
color: hsl(142 76% 36%);
2744+
}
2745+
2746+
.queue-detail-item .item-status.pending {
2747+
background: hsl(48 96% 53% / 0.2);
2748+
color: hsl(48 96% 53%);
2749+
}
2750+
2751+
.queue-detail-item .item-status.executing {
2752+
background: hsl(217 91% 60% / 0.2);
2753+
color: hsl(217 91% 60%);
2754+
}
2755+
2756+
.queue-detail-item .item-status.failed {
2757+
background: hsl(0 84% 60% / 0.2);
2758+
color: hsl(0 84% 60%);
2759+
}
2760+
2761+
/* Small Buttons */
2762+
.btn-sm {
2763+
display: inline-flex;
2764+
align-items: center;
2765+
gap: 0.25rem;
2766+
padding: 0.25rem 0.5rem;
2767+
font-size: 0.75rem;
2768+
border-radius: 0.25rem;
2769+
border: none;
2770+
cursor: pointer;
2771+
transition: all 0.15s ease;
2772+
}
2773+
2774+
.btn-sm.btn-primary {
2775+
background: hsl(var(--primary));
2776+
color: hsl(var(--primary-foreground));
2777+
}
2778+
2779+
.btn-sm.btn-primary:hover {
2780+
opacity: 0.9;
2781+
}
2782+
2783+
.btn-sm.btn-secondary {
2784+
background: hsl(var(--muted));
2785+
color: hsl(var(--foreground));
2786+
}
2787+
2788+
.btn-sm.btn-secondary:hover {
2789+
background: hsl(var(--muted) / 0.8);
2790+
}
2791+
2792+
@media (max-width: 640px) {
2793+
.queue-detail-stats {
2794+
grid-template-columns: repeat(2, 1fr);
2795+
}
2796+
2797+
.queue-history-meta {
2798+
flex-direction: column;
2799+
gap: 0.25rem;
2800+
}
2801+
}

0 commit comments

Comments
 (0)