@@ -8,6 +8,7 @@ interface SecurityFinding {
title : string ;
description : string ;
recommendation : string ;
taskId? : string ;
}
interface SecurityAudit {
@@ -89,6 +90,23 @@ async function _deleteAudit(id: string): Promise<void> {
}
export { _deleteAudit as deleteAudit } ;
async function createTask ( data : {
title : string ;
description : string ;
priority : string ;
source : string ;
tags : string [ ] ;
} ) : Promise < { id : string } > {
const res = await fetch ( "/api/tasks" , {
method : "POST" ,
credentials : "include" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( data ) ,
} ) ;
if ( ! res . ok ) throw new Error ( "Failed to create task" ) ;
return res . json ( ) ;
}
// ─── Helpers ───
function scoreColor ( score : number ) : string {
@@ -170,6 +188,60 @@ function timeAgo(dateStr: string): string {
return date . toLocaleDateString ( ) ;
}
function letterGrade ( score : number ) : string {
if ( score >= 90 ) return "A" ;
if ( score >= 80 ) return "B" ;
if ( score >= 70 ) return "C" ;
if ( score >= 60 ) return "D" ;
return "F" ;
}
const PROJECT_NAMES = [
"Hammer Dashboard" ,
"Network App" ,
"Todo App" ,
"nKode" ,
"Infrastructure" ,
] ;
// ─── Toast Component ───
function Toast ( {
message ,
type ,
onClose ,
} : {
message : string ;
type : "success" | "error" ;
onClose : ( ) = > void ;
} ) {
useEffect ( ( ) = > {
const timer = setTimeout ( onClose , 4000 ) ;
return ( ) = > clearTimeout ( timer ) ;
} , [ onClose ] ) ;
return (
< div className = "fixed bottom-6 right-6 z-50 animate-in slide-in-from-bottom-4" >
< div
className = { ` flex items-center gap-3 px-5 py-3 rounded-xl shadow-lg border ${
type === "success"
? "bg-green-50 dark:bg-green-900/40 border-green-200 dark:border-green-700 text-green-800 dark:text-green-300"
: "bg-red-50 dark:bg-red-900/40 border-red-200 dark:border-red-700 text-red-800 dark:text-red-300"
} ` }
>
< span className = "text-lg" > { type === "success" ? "✅" : "❌" } < / span >
< span className = "text-sm font-medium" > { message } < / span >
< button
onClick = { onClose }
className = "ml-2 opacity-60 hover:opacity-100 transition"
>
×
< / button >
< / div >
< / div >
) ;
}
// ─── Score Ring Component ───
function ScoreRing ( {
@@ -216,6 +288,73 @@ function ScoreRing({
) ;
}
// ─── Create Fix Task Button ───
function CreateFixTaskButton ( {
finding ,
projectName ,
onTaskCreated ,
} : {
finding : SecurityFinding ;
projectName : string ;
onTaskCreated : ( findingId : string , taskId : string ) = > void ;
} ) {
const [ creating , setCreating ] = useState ( false ) ;
if ( finding . status === "strong" ) return null ;
if ( finding . taskId ) {
return (
< span className = "inline-flex items-center gap-1 text-[10px] font-medium text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20 px-2 py-1 rounded-full" >
✓ Task Created
< / span >
) ;
}
const handleCreate = async ( e : React.MouseEvent ) = > {
e . stopPropagation ( ) ;
setCreating ( true ) ;
try {
const priority = finding . status === "critical" ? "critical" : "high" ;
const task = await createTask ( {
title : ` Security Fix: ${ projectName } — ${ finding . title } ` ,
description : ` **Finding:** ${ finding . description } \ n \ n**Recommendation:** ${ finding . recommendation } \ n \ n**Category:** Security Audit \ n**Severity:** ${ finding . status === "critical" ? "Critical" : "Needs Improvement" } ` ,
priority ,
source : "hammer" ,
tags : [ "security" ] ,
} ) ;
onTaskCreated ( finding . id , task . id ) ;
} catch ( err ) {
console . error ( "Failed to create task:" , err ) ;
} finally {
setCreating ( false ) ;
}
} ;
return (
< button
onClick = { handleCreate }
disabled = { creating }
className = { ` inline-flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-full transition ${
finding . status === "critical"
? "text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/30"
: "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 hover:bg-amber-100 dark:hover:bg-amber-900/30"
} disabled:opacity-50 ` }
>
{ creating ? (
< >
< svg className = "w-3 h-3 animate-spin" viewBox = "0 0 24 24" fill = "none" >
< circle cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "3" className = "opacity-25" / >
< path d = "M4 12a8 8 0 018-8" stroke = "currentColor" strokeWidth = "3" strokeLinecap = "round" / >
< / svg >
Creating . . .
< / >
) : (
< > 🔨 Create Fix Task < / >
) }
< / button >
) ;
}
// ─── Finding Editor Modal ───
function FindingEditorModal ( {
@@ -305,7 +444,6 @@ function FindingEditorModal({
< / div >
< div className = "flex-1 overflow-y-auto px-6 py-4 space-y-4" >
{ /* Score */ }
< div >
< label className = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" >
Score ( 0 – 100 )
@@ -320,7 +458,6 @@ function FindingEditorModal({
/ >
< / div >
{ /* Findings */ }
< div className = "space-y-3" >
{ findings . map ( ( finding , i ) = > (
< div
@@ -445,14 +582,6 @@ function AddAuditModal({
"Compliance" ,
] ;
const projects = [
"Hammer Dashboard" ,
"Network App" ,
"Todo App" ,
"nKode" ,
"Infrastructure" ,
] ;
const handleCreate = async ( ) = > {
if ( ! projectName ) return ;
setSaving ( true ) ;
@@ -490,7 +619,7 @@ function AddAuditModal({
className = "w-full border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-amber-300"
>
< option value = "" > Select project . . . < / option >
{ projects . map ( ( p ) = > (
{ PROJECT_NAMES . map ( ( p ) = > (
< option key = { p } value = { p } >
{ p }
< / option >
@@ -547,15 +676,62 @@ function AddAuditModal({
) ;
}
// ─── Project C ard ───
// ─── Project Tab B ar ───
function ProjectTabBar ( {
projects ,
selected ,
onSelect ,
} : {
projects : string [ ] ;
selected : string | null ;
onSelect : ( project : string | null ) = > void ;
} ) {
return (
< div className = "flex items-center gap-1 overflow-x-auto pb-1 mb-6 border-b border-gray-200 dark:border-gray-700" >
< button
onClick = { ( ) = > onSelect ( null ) }
className = { ` px-4 py-2.5 text-sm font-medium rounded-t-lg transition whitespace-nowrap ${
selected === null
? "text-amber-600 dark:text-amber-400 border-b-2 border-amber-500 bg-amber-50/50 dark:bg-amber-900/10"
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50"
} ` }
>
📊 Overview
< / button >
{ projects . map ( ( project ) = > (
< button
key = { project }
onClick = { ( ) = > onSelect ( project ) }
className = { ` px-4 py-2.5 text-sm font-medium rounded-t-lg transition whitespace-nowrap ${
selected === project
? "text-amber-600 dark:text-amber-400 border-b-2 border-amber-500 bg-amber-50/50 dark:bg-amber-900/10"
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50"
} ` }
>
{ project }
< / button >
) ) }
< / div >
) ;
}
// ─── Project Score Card ───
function ProjectScoreCard ( {
summary ,
audits ,
onClick ,
} : {
summary : ProjectSummary ;
audits : SecurityAudit [ ] ;
onClick : ( ) = > void ;
} ) {
const projectAudits = audits . filter ( ( a ) = > a . projectName === summary . projectName ) ;
const allFindings = projectAudits . flatMap ( ( a ) = > a . findings || [ ] ) ;
const criticalCount = allFindings . filter ( ( f ) = > f . status === "critical" ) . length ;
const warningCount = allFindings . filter ( ( f ) = > f . status === "needs_improvement" ) . length ;
return (
< button
onClick = { onClick }
@@ -564,13 +740,35 @@ function ProjectScoreCard({
< div className = "flex items-center gap-4" >
< ScoreRing score = { summary . averageScore } size = { 64 } / >
< div className = "flex-1 min-w-0" >
< h3 className = "text-base font-semibold text-gray-900 dark:text-white group-hover:text-amber-600 dark:group-hover:text-amber-400 transition truncate " >
{ summary . projectName }
< / h3 >
< div className = "flex items-center gap-2 " >
< h3 className = "text-base font-semibold text-gray-900 dark:text-white group-hover:text-amber-600 dark:group-hover:text-amber-400 transition truncate" >
{ summary . projectName }
< / h3 >
< span className = { ` text-xs font-bold px-1.5 py-0.5 rounded ${ scoreColor ( summary . averageScore ) } ${
summary . averageScore >= 80
? "bg-green-100 dark:bg-green-900/30"
: summary . averageScore >= 50
? "bg-yellow-100 dark:bg-yellow-900/30"
: "bg-red-100 dark:bg-red-900/30"
} ` } >
{ letterGrade ( summary . averageScore ) }
< / span >
< / div >
< p className = "text-xs text-gray-500 dark:text-gray-400 mt-1" >
{ summary . categoriesAudited } categories audited
< / p >
< p className = "text-xs text-gray-400 dark:text-gray-500 mt-0.5 " >
< div className = "flex gap-3 mt-1.5 text-xs text-gray-400 dark:text-gray-500" >
{ criticalCount > 0 && (
< span className = "text-red-500" > ❌ { criticalCount } critical < / span >
) }
{ warningCount > 0 && (
< span className = "text-yellow-500" > ⚠ ️ { warningCount } warnings < / span >
) }
{ criticalCount === 0 && warningCount === 0 && (
< span className = "text-green-500" > ✅ All clear < / span >
) }
< / div >
< p className = "text-[10px] text-gray-400 dark:text-gray-500 mt-1" >
Last audited : { timeAgo ( summary . lastAudited ) }
< / p >
< / div >
@@ -584,9 +782,11 @@ function ProjectScoreCard({
function CategoryCard ( {
audit ,
onEdit ,
onTaskCreated ,
} : {
audit : SecurityAudit ;
onEdit : ( ) = > void ;
onTaskCreated : ( auditId : string , findingId : string , taskId : string ) = > void ;
} ) {
const [ expanded , setExpanded ] = useState ( false ) ;
const findings = audit . findings || [ ] ;
@@ -654,7 +854,7 @@ function CategoryCard({
{ statusIcon ( finding . status ) }
< / span >
< div className = "flex-1 min-w-0" >
< div className = "flex items-center gap-2 mb-1" >
< div className = "flex items-center gap-2 mb-1 flex-wrap " >
< span className = "text-sm font-medium text-gray-900 dark:text-white" >
{ finding . title }
< / span >
@@ -663,6 +863,13 @@ function CategoryCard({
>
{ statusLabel ( finding . status ) }
< / span >
< CreateFixTaskButton
finding = { finding }
projectName = { audit . projectName }
onTaskCreated = { ( findingId , taskId ) = >
onTaskCreated ( audit . id , findingId , taskId )
}
/ >
< / div >
< p className = "text-xs text-gray-600 dark:text-gray-400 leading-relaxed" >
{ finding . description }
@@ -707,15 +914,14 @@ function CategoryCard({
function ProjectDetail ( {
projectName ,
audits ,
onBack ,
onRefresh ,
onTaskCreated ,
} : {
projectName : string ;
audits : SecurityAudit [ ] ;
onBack : ( ) = > void ;
onRefresh : ( ) = > void ;
onTaskCreated : ( auditId : string , findingId : string , taskId : string ) = > void ;
} ) {
const [ editingAudit , setEditingAudit ] = useState < SecurityAudit | null > ( null ) ;
const [ refreshKey , setRefreshKey ] = useState ( 0 ) ;
const projectAudits = audits . filter ( ( a ) = > a . projectName === projectName ) ;
const avgScore = projectAudits . length
@@ -740,72 +946,75 @@ function ProjectDetail({
( a . findings ? . filter ( ( f ) = > f . status === "needs_improvement" ) . length || 0 ) ,
0
) ;
const strongFindings = projectAudits . reduce (
( sum , a ) = >
sum + ( a . findings ? . filter ( ( f ) = > f . status === "strong" ) . length || 0 ) ,
0
) ;
return (
< div className = "max-w-4xl mx-auto" >
< div className = "flex items-center gap-3 mb-6" >
< button
onClick = { onBack }
className = "p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400"
>
< svg
className = "w-5 h-5"
fill = "none"
stroke = "currentColor"
viewBox = "0 0 24 24"
>
< path
strokeLinecap = "round"
strokeLinejoin = "round "
strokeWidth = { 2 }
d = "M15 19l-7-7 7-7"
/ >
< / svg >
< / button >
< div className = "flex-1 " >
< h1 className = "text-2xl font-bold text-gray-900 dark:text-white" >
{ projectName }
< / h1 >
< p className = "text-sm text-gray-500 dark:text-gray-400 " >
Security Audit Details
< / p >
< div className = "max-w-4xl mx-auto" key = { refreshKey } >
{ /* Header with score summary */ }
< div className = "bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6" >
< div className = "flex items-center gap-6" >
< ScoreRing score = { avgScore } size = { 96 } / >
< div className = "flex-1" >
< div className = "flex items-center gap-3 mb-1" >
< h2 className = "text-xl font-bold text-gray-900 dark:text-white" >
{ projectName }
< / h2 >
< span className = { ` text-sm font-bold px-2 py-0.5 rounded ${ scoreColor ( avgScore ) } ${
avgScore >= 80
? "bg-green-100 dark:bg-green-900/30"
: avgScore >= 50
? "bg-yellow-100 dark:bg-yellow-900/30 "
: "bg-red-100 dark:bg-red-900/30"
} ` } >
Grade : { letterGrade ( avgScore ) }
< / span >
< / div >
< p className = "text-sm text-gray-500 dark:text-gray-400 mb-3 " >
{ projectAudits . length } categories • { totalFindings } total findings
< / p >
< div className = "flex gap-4 text-sm" >
< div className = "flex items-center gap-1.5 " >
< span className = "w-2.5 h-2.5 rounded-full bg-green-500" / >
< span className = "text-gray-600 dark:text-gray-400" >
{ strongFindings } strong
< / span >
< / div >
< div className = "flex items-center gap-1.5" >
< span className = "w-2.5 h-2.5 rounded-full bg-yellow-500" / >
< span className = "text-gray-600 dark:text-gray-400" >
{ warningFindings } warnings
< / span >
< / div >
< div className = "flex items-center gap-1.5" >
< span className = "w-2.5 h-2.5 rounded-full bg-red-500" / >
< span className = "text-gray-600 dark:text-gray-400" >
{ criticalFindings } critical
< / span >
< / div >
< / div >
< / div >
< / div >
< ScoreRing score = { avgScore } size = { 56 } / >
< / div >
{ /* Stats bar */ }
< div className = "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6 " >
< div className = "bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-3 text-center" >
< div className = { ` text-xl font-bold ${ scoreColor ( avgScore ) } ` } >
{ avgScore }
< / div >
< div className = "text-[10px] text-gray-500 uppercase tracking-wide" >
Avg Score
< / div >
< / div >
< div className = "bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-3 text-center" >
< div className = "text-xl font-bold text-gray-9 00 dark:text-whi te" >
{ totalFindings }
< / div >
< div className = "text-[10px] text-gray-500 uppercase tracking-wide" >
Findings
< / div >
< / div >
< div className = "bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-3 text-center" >
< div className = "text-xl font-bold text-red-500" >
{ criticalFindings }
< / div >
< div className = "text-[10px] text-gray-500 uppercase tracking-wide" >
Critical
< / div >
< / div >
< div className = "bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-3 text-center" >
< div className = "text-xl font-bold text-yellow-500" >
{ warningFindings }
< / div >
< div className = "text-[10px] text-gray-500 uppercase tracking-wide" >
Warnings
< / div >
{ /* Score bar per category */ }
< div className = "mt-5 grid grid-cols-2 sm:grid-cols-4 gap-2 " >
{ projectAudits . map ( ( audit ) = > (
< div
key = { audit . id }
className = "bg-gray-50 dark:bg-gray-800/50 rounded-lg p-2.5 text-center"
>
< div className = "text-lg mb-0.5" > { categoryIcon ( audit . category ) } < / div >
< div className = { ` text-sm font-bold ${ scoreColor ( audit . score ) } ` } >
{ audit . score }
< / div >
< div className = "text-[10px] text-gray-5 00 dark:text-gray-400 trunca te" >
{ audit . category }
< / div >
< / div >
) ) }
< / div >
< / div >
@@ -816,6 +1025,7 @@ function ProjectDetail({
key = { audit . id }
audit = { audit }
onEdit = { ( ) = > setEditingAudit ( audit ) }
onTaskCreated = { onTaskCreated }
/ >
) ) }
< / div >
@@ -833,7 +1043,7 @@ function ProjectDetail({
onClose = { ( ) = > setEditingAudit ( null ) }
onSaved = { ( ) = > {
setEditingAudit ( null ) ;
on Refresh( ) ;
set RefreshKey ( ( k ) = > k + 1 ) ;
} }
/ >
) }
@@ -870,9 +1080,20 @@ function PostureSummary({
< div className = "flex items-center gap-6" >
< ScoreRing score = { overallScore } size = { 96 } / >
< div className = "flex-1" >
< h2 className = "text-lg font-bold text-gray-900 dark:text-white mb-1" >
Overall Security Posture
< / h2 >
< div className = "flex items-center gap-3 mb-1" >
< h2 className = "text-lg font-bold text-gray-900 dark:text-white" >
Overall Security Posture
< / h2 >
< span className = { ` text-sm font-bold px-2 py-0.5 rounded ${ scoreColor ( overallScore ) } ${
overallScore >= 80
? "bg-green-100 dark:bg-green-900/30"
: overallScore >= 50
? "bg-yellow-100 dark:bg-yellow-900/30"
: "bg-red-100 dark:bg-red-900/30"
} ` } >
{ letterGrade ( overallScore ) }
< / span >
< / div >
< p className = "text-sm text-gray-500 dark:text-gray-400 mb-3" >
{ summary . length } projects • { audits . length } category audits • { " " }
{ allFindings . length } findings
@@ -911,6 +1132,10 @@ export function SecurityPage() {
const [ loading , setLoading ] = useState ( true ) ;
const [ selectedProject , setSelectedProject ] = useState < string | null > ( null ) ;
const [ showAddModal , setShowAddModal ] = useState ( false ) ;
const [ toast , setToast ] = useState < {
message : string ;
type : "success" | "error" ;
} | null > ( null ) ;
const loadAll = useCallback ( async ( ) = > {
try {
@@ -931,6 +1156,50 @@ export function SecurityPage() {
loadAll ( ) ;
} , [ loadAll ] ) ;
// Handle task creation: update finding with taskId locally and persist
const handleTaskCreated = useCallback (
async ( auditId : string , findingId : string , taskId : string ) = > {
// Update local state
setAudits ( ( prev ) = >
prev . map ( ( audit ) = > {
if ( audit . id !== auditId ) return audit ;
return {
. . . audit ,
findings : audit.findings.map ( ( f ) = >
f . id === findingId ? { . . . f , taskId } : f
) ,
} ;
} )
) ;
// Persist to backend
const audit = audits . find ( ( a ) = > a . id === auditId ) ;
if ( audit ) {
const updatedFindings = audit . findings . map ( ( f ) = >
f . id === findingId ? { . . . f , taskId } : f
) ;
try {
await updateAudit ( auditId , { findings : updatedFindings } ) ;
} catch ( e ) {
console . error ( "Failed to persist taskId:" , e ) ;
}
}
setToast ( { message : "Fix task created in Hammer Queue!" , type : "success" } ) ;
} ,
[ audits ]
) ;
// Get unique project names from data
const projectNames = [ . . . new Set ( audits . map ( ( a ) = > a . projectName ) ) ] . sort (
( a , b ) = > {
const order = PROJECT_NAMES ;
const ai = order . indexOf ( a ) ;
const bi = order . indexOf ( b ) ;
return ( ai === - 1 ? 999 : ai ) - ( bi === - 1 ? 999 : bi ) ;
}
) ;
if ( loading ) {
return (
< div className = "p-4 sm:p-6" >
@@ -941,26 +1210,10 @@ export function SecurityPage() {
) ;
}
if ( selectedProject ) {
return (
< div className = "p-4 sm:p-6" >
< ProjectDetail
projectName = { selectedProject }
audits = { audits }
onBack = { ( ) = > {
setSelectedProject ( null ) ;
loadAll ( ) ;
} }
onRefresh = { loadAll }
/ >
< / div >
) ;
}
return (
< div className = "p-4 sm:p-6" >
{ /* Header */ }
< div className = "flex items-center justify-between mb-6 " >
< div className = "flex items-center justify-between mb-4 " >
< div >
< h1 className = "text-xl sm:text-2xl font-bold text-gray-900 dark:text-white" >
🛡 ️ Security Audit
@@ -977,36 +1230,56 @@ export function SecurityPage() {
< / button >
< / div >
{ /* Overall posture */ }
< PostureSummary audits = { audits } summary = { summary } / >
{ /* Project Tabs */ }
< ProjectTabBar
projects = { projectNames }
selected = { selectedProject }
onSelect = { setSelectedProject }
/ >
{ /* Project score cards */ }
{ summary . length > 0 ? (
< div className = "grid gap-4 sm:grid-cols-2 lg:grid-cols-3" >
{ summary . map ( ( s ) = > (
< ProjectScoreCard
key = { s . projectName }
summary = { s }
onClick = { ( ) = > setSelectedProject ( s . projectName ) }
/ >
) ) }
< / div >
{ /* Content */ }
{ selectedProject === null ? (
< >
{ /* Overview */ }
< PostureSummary audits = { audits } summary = { summary } / >
{ /* Project score cards */ }
{ summary . length > 0 ? (
< div className = "grid gap-4 sm:grid-cols-2 lg:grid-cols-3" >
{ summary . map ( ( s ) = > (
< ProjectScoreCard
key = { s . projectName }
summary = { s }
audits = { audits }
onClick = { ( ) = > setSelectedProject ( s . projectName ) }
/ >
) ) }
< / div >
) : (
< div className = "text-center py-16" >
< span className = "text-5xl block mb-4" > 🛡 ️ < / span >
< h2 className = "text-lg font-semibold text-gray-600 dark:text-gray-400 mb-2" >
No audit data yet
< / h2 >
< p className = "text-sm text-gray-400 mb-4" >
Add security audit entries to start tracking your security
posture
< / p >
< button
onClick = { ( ) = > setShowAddModal ( true ) }
className = "px-4 py-2 bg-amber-500 text-white rounded-lg text-sm font-medium hover:bg-amber-600 transition"
>
Add First Audit
< / button >
< / div >
) }
< / >
) : (
< div className = "text-center py-16" >
< span className = "text-5xl block mb-4" > 🛡 ️ < / span >
< h2 className = "text-lg font-semibold text-gray-600 dark:text-gray-400 mb-2" >
No audit data yet
< / h2 >
< p className = "text-sm text-gray-400 mb-4" >
Add security audit entries to start tracking your security posture
< / p >
< button
onClick = { ( ) = > setShowAddModal ( true ) }
className = "px-4 py-2 bg-amber-500 text-white rounded-lg text-sm font-medium hover:bg-amber-600 transition"
>
Add First Audit
< / button >
< / div >
< ProjectDetail
projectName = { selectedProject }
audits = { audits }
onTaskCreated = { handleTaskCreated }
/ >
) }
{ showAddModal && (
@@ -1018,6 +1291,14 @@ export function SecurityPage() {
} }
/ >
) }
{ toast && (
< Toast
message = { toast . message }
type = { toast . type }
onClose = { ( ) = > setToast ( null ) }
/ >
) }
< / div >
) ;
}