elmeg-demo/frontend/app/bugs/known-issues/page.tsx
fullsizemalt 14a509ddb5
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
feat: Add bug tracker MVP (decoupled, feature-flagged)
- Backend: Ticket and TicketComment models (no FK to User)
- API: /tickets/* endpoints for submit, view, comment, upvote
- Admin: /tickets/admin/* for triage queue
- Frontend: /bugs pages (submit, my-tickets, known-issues, detail)
- Feature flag: ENABLE_BUG_TRACKER env var (default: true)

To disable: Set ENABLE_BUG_TRACKER=false
To remove: Delete models_tickets.py, routers/tickets.py, frontend/app/bugs/
2025-12-23 13:18:00 -08:00

143 lines
5.8 KiB
TypeScript

"use client"
/**
* Known Issues Page - Public bugs/feature requests
*/
import { useEffect, useState } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { getApiUrl } from "@/lib/api-config"
import Link from "next/link"
import { ArrowLeft, ThumbsUp, Bug, Lightbulb, Loader2 } from "lucide-react"
interface Ticket {
id: number
ticket_number: string
type: string
status: string
priority: string
title: string
description: string
upvotes: number
created_at: string
}
const TYPE_ICONS: Record<string, typeof Bug> = {
bug: Bug,
feature: Lightbulb,
}
export default function KnownIssuesPage() {
const [tickets, setTickets] = useState<Ticket[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchKnownIssues()
}, [])
const fetchKnownIssues = async () => {
try {
const res = await fetch(`${getApiUrl()}/tickets/known-issues`)
if (res.ok) {
setTickets(await res.json())
}
} catch (e) {
console.error("Failed to fetch known issues", e)
} finally {
setLoading(false)
}
}
const handleUpvote = async (ticketNumber: string, e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
try {
await fetch(`${getApiUrl()}/tickets/${ticketNumber}/upvote`, {
method: "POST",
})
// Optimistic update
setTickets(tickets.map(t =>
t.ticket_number === ticketNumber
? { ...t, upvotes: t.upvotes + 1 }
: t
))
} catch (e) {
console.error("Failed to upvote", e)
}
}
return (
<div className="container max-w-4xl py-8">
<div className="flex items-center gap-4 mb-8">
<Link href="/bugs">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold">Known Issues</h1>
<p className="text-muted-foreground">Active bugs and feature requests</p>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : tickets.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">No known issues at this time.</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{tickets.map((ticket) => {
const Icon = TYPE_ICONS[ticket.type] || Bug
return (
<Link key={ticket.id} href={`/bugs/ticket/${ticket.ticket_number}`}>
<Card className="hover:border-primary transition-colors cursor-pointer">
<CardContent className="py-4">
<div className="flex items-start gap-4">
<div className="p-2 rounded-lg bg-muted">
<Icon className="h-5 w-5 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-mono text-sm text-muted-foreground">
{ticket.ticket_number}
</span>
<Badge variant={ticket.status === "in_progress" ? "secondary" : "default"}>
{ticket.status === "in_progress" ? "In Progress" : "Open"}
</Badge>
</div>
<h3 className="font-semibold">{ticket.title}</h3>
{ticket.description && (
<p className="text-sm text-muted-foreground line-clamp-2 mt-1">
{ticket.description}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
className="gap-2 shrink-0"
onClick={(e) => handleUpvote(ticket.ticket_number, e)}
>
<ThumbsUp className="h-4 w-4" />
{ticket.upvotes}
</Button>
</div>
</CardContent>
</Card>
</Link>
)
})}
</div>
)}
</div>
)
}