diff --git a/src/app/api/daily-summary/route.ts b/src/app/api/daily-summary/route.ts index 1cb684a9..e9a7ca12 100644 --- a/src/app/api/daily-summary/route.ts +++ b/src/app/api/daily-summary/route.ts @@ -18,7 +18,24 @@ import { OutputValidationError, MAX_SUMMARY_OUTPUT_LENGTH, } from '@/lib/daily-summary-generator'; -import { SUMMARY_ALLOWED_TOOLS } from '@/config/review-config'; +import type { DailyReport } from '@/lib/db/daily-report-db'; +import { SUMMARY_ALLOWED_TOOLS, MAX_USER_INSTRUCTION_LENGTH } from '@/config/review-config'; + +// ============================================================================= +// Helpers +// ============================================================================= + +/** Serialize a DailyReport to a plain JSON-safe object (Date -> ISO string) */ +function serializeReport(report: DailyReport) { + return { + date: report.date, + content: report.content, + generatedByTool: report.generatedByTool, + model: report.model, + createdAt: report.createdAt.toISOString(), + updatedAt: report.updatedAt.toISOString(), + }; +} // ============================================================================= // Validation @@ -81,14 +98,7 @@ export async function GET(request: NextRequest) { const messages = getMessagesByDateRange(db, { after: dayStart, before: dayEnd }); return NextResponse.json({ - report: report ? { - date: report.date, - content: report.content, - generatedByTool: report.generatedByTool, - model: report.model, - createdAt: report.createdAt.toISOString(), - updatedAt: report.updatedAt.toISOString(), - } : null, + report: report ? serializeReport(report) : null, messageCount: messages.length, }); } catch (error) { @@ -107,7 +117,13 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { const body = await request.json(); - const { date, tool, model } = body; + + // Body shape validation + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); + } + + const { date, tool, model, userInstruction } = body; // Validate date if (!date || typeof date !== 'string') { @@ -128,19 +144,30 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid model parameter' }, { status: 400 }); } + // Validate userInstruction (optional, Issue #612) + if (userInstruction !== undefined && userInstruction !== null) { + if (typeof userInstruction !== 'string') { + return NextResponse.json({ error: 'Invalid userInstruction parameter' }, { status: 400 }); + } + if (userInstruction.length > MAX_USER_INSTRUCTION_LENGTH) { + return NextResponse.json( + { error: `userInstruction exceeds maximum length (${MAX_USER_INSTRUCTION_LENGTH})` }, + { status: 400 } + ); + } + } + const db = getDbInstance(); - const report = await generateDailySummary(db, { date, tool, model }); + const report = await generateDailySummary(db, { + date, + tool, + model, + userInstruction: userInstruction || undefined, + }); return NextResponse.json({ - report: { - date: report.date, - content: report.content, - generatedByTool: report.generatedByTool, - model: report.model, - createdAt: report.createdAt.toISOString(), - updatedAt: report.updatedAt.toISOString(), - }, + report: serializeReport(report), generated: true, }); } catch (error) { @@ -212,14 +239,7 @@ export async function PUT(request: NextRequest) { const updated = getDailyReport(db, date)!; return NextResponse.json({ - report: { - date: updated.date, - content: updated.content, - generatedByTool: updated.generatedByTool, - model: updated.model, - createdAt: updated.createdAt.toISOString(), - updatedAt: updated.updatedAt.toISOString(), - }, + report: serializeReport(updated), }); } catch (error) { console.error('PUT /api/daily-summary error:', error); diff --git a/src/app/sessions/page.tsx b/src/app/sessions/page.tsx index bf01a30c..14532014 100644 --- a/src/app/sessions/page.tsx +++ b/src/app/sessions/page.tsx @@ -79,18 +79,29 @@ function CliDot({ status, label }: { status: BranchStatus; label: string }) { return ; } +/** Status display labels */ +const STATUS_LABELS: Record = { + ready: 'Ready', + in_progress: 'In Progress', + in_review: 'In Review', + done: 'Done', +}; + /** Format status display label */ function formatStatus(status: string | null | undefined): string { if (!status) return ''; - switch (status) { - case 'ready': return 'Ready'; - case 'in_progress': return 'In Progress'; - case 'in_review': return 'In Review'; - case 'done': return 'Done'; - default: return status; - } + return STATUS_LABELS[status] ?? status; } +/** Status badge CSS classes keyed by status value */ +const STATUS_BADGE_CLASSES: Record = { + done: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400', + in_review: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400', + in_progress: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-700 dark:text-cyan-400', +}; + +const DEFAULT_BADGE_CLASS = 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300'; + // ============================================================================ // Component // ============================================================================ @@ -98,8 +109,8 @@ function formatStatus(status: string | null | undefined): string { export default function SessionsPage() { const { worktrees, isLoading, error } = useWorktreesCache(); const [filterText, setFilterText] = useState(''); - const [sortKey, setSortKey] = useState('lastSent'); - const [sortDirection, setSortDirection] = useState('desc'); + const [sortKey, setSortKey] = useState('repositoryName'); + const [sortDirection, setSortDirection] = useState('asc'); const filteredAndSorted = useMemo(() => { let result = worktrees; @@ -131,12 +142,18 @@ export default function SessionsPage() { } case 'repositoryName': { comparison = a.repositoryName.toLowerCase().localeCompare(b.repositoryName.toLowerCase()); + if (comparison === 0) { + comparison = a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + } break; } case 'status': { const priorityA = WORKTREE_STATUS_PRIORITY[a.status ?? ''] ?? DEFAULT_STATUS_PRIORITY; const priorityB = WORKTREE_STATUS_PRIORITY[b.status ?? ''] ?? DEFAULT_STATUS_PRIORITY; comparison = priorityA - priorityB; + if (comparison === 0) { + comparison = a.repositoryName.toLowerCase().localeCompare(b.repositoryName.toLowerCase()); + } break; } default: @@ -265,13 +282,7 @@ export default function SessionsPage() { {wt.status && (
{formatStatus(wt.status)} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index cdfe79e5..14fb8c37 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -24,7 +24,7 @@ const NAV_ITEMS: Array<{ label: string; href: string; isActive: (pathname: strin { label: 'Home', href: '/', isActive: (p) => p === '/' }, { label: 'Sessions', href: '/sessions', isActive: (p) => p.startsWith('/sessions') }, { label: 'Repos', href: '/repositories', isActive: (p) => p.startsWith('/repositories') }, - { label: 'Review', href: '/review', isActive: (p) => p.startsWith('/review') }, + { label: 'Review/Report', href: '/review', isActive: (p) => p.startsWith('/review') }, { label: 'More', href: '/more', isActive: (p) => p.startsWith('/more') }, ]; diff --git a/src/components/mobile/GlobalMobileNav.tsx b/src/components/mobile/GlobalMobileNav.tsx index ab6fd8f3..00dbe6de 100644 --- a/src/components/mobile/GlobalMobileNav.tsx +++ b/src/components/mobile/GlobalMobileNav.tsx @@ -57,7 +57,7 @@ const MoreIcon = () => ( const MOBILE_NAV_TABS: MobileNavTab[] = [ { label: 'Home', href: '/', isActive: (p) => p === '/', icon: }, { label: 'Sessions', href: '/sessions', isActive: (p) => p.startsWith('/sessions'), icon: }, - { label: 'Review', href: '/review', isActive: (p) => p.startsWith('/review'), icon: }, + { label: 'Review/Report', href: '/review', isActive: (p) => p.startsWith('/review'), icon: }, { label: 'More', href: '/more', isActive: (p) => p.startsWith('/more'), icon: }, ]; diff --git a/src/components/review/ReportTab.tsx b/src/components/review/ReportTab.tsx index 9882b36e..4b9e8b58 100644 --- a/src/components/review/ReportTab.tsx +++ b/src/components/review/ReportTab.tsx @@ -9,7 +9,7 @@ import { useState, useEffect, useCallback } from 'react'; import ReportDatePicker from './ReportDatePicker'; -import { SUMMARY_ALLOWED_TOOLS } from '@/config/review-config'; +import { SUMMARY_ALLOWED_TOOLS, MAX_USER_INSTRUCTION_LENGTH } from '@/config/review-config'; /** Format Date to YYYY-MM-DD */ function formatToday(): string { @@ -41,6 +41,7 @@ export default function ReportTab() { const [isGenerating, setIsGenerating] = useState(false); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); + const [userInstruction, setUserInstruction] = useState(''); // Fetch report for selected date const fetchReport = useCallback(async (date: string) => { @@ -90,6 +91,9 @@ export default function ReportTab() { if (selectedTool === 'copilot' && modelInput.trim()) { body.model = modelInput.trim(); } + if (userInstruction.trim()) { + body.userInstruction = userInstruction.trim(); + } const res = await fetch('/api/daily-summary', { method: 'POST', @@ -181,6 +185,23 @@ export default function ReportTab() { )}
+ + + {/* User instruction textarea (Issue #612) */} +
+