Add cascade delete for projects with Gitea repository cleanup

- Add deleteGiteaRepo server action to remove repos via Gitea API
- Add deleteProject function with full cascade:
  - Deletes Gitea repository if linked
  - Removes all agent_runs, messages, backlog items
  - Removes project phases, activities, recommendations
  - Finally removes the project itself
- Add ProjectSettingsMenu with delete confirmation dialog
- Add use-toast hook for notifications using sonner
- Add shadcn alert-dialog component
- Restore brand button variant after shadcn update

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-15 14:37:43 +01:00
parent 0ea463f1fa
commit 8e1ec041de
10 changed files with 748 additions and 16 deletions

View File

@@ -1,4 +1,5 @@
import { createClient } from '@/lib/supabase/client'
import { deleteGiteaRepo } from '@/app/(dashboard)/projects/new/actions'
import type {
DesignPhase,
PhaseStatus,
@@ -317,3 +318,113 @@ export async function getProjectActivities(projectId: string, limit = 20) {
created_at: string
}>
}
// Delete project with cascade (includes Gitea repo deletion)
export async function deleteProject(projectId: string): Promise<{ success: boolean; error?: string }> {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { success: false, error: 'Not authenticated' }
}
try {
// First, get the project to check ownership and get gitea_repo
const { data: projectData, error: fetchError } = await supabase
.from('projects')
.select('id, user_id, gitea_repo, name')
.eq('id', projectId)
.single()
if (fetchError || !projectData) {
return { success: false, error: 'Project not found' }
}
const project = projectData as unknown as {
id: string
user_id: string
gitea_repo: string | null
name: string
}
// Verify ownership
if (project.user_id !== user.id) {
return { success: false, error: 'Not authorized to delete this project' }
}
// Extract repo name from gitea_repo URL if it exists
// Format: https://gitea.mylder.io/admin/repo-name
if (project.gitea_repo) {
const repoMatch = project.gitea_repo.match(/\/([^/]+)$/)
const repoName = repoMatch ? repoMatch[1] : null
if (repoName) {
const giteaResult = await deleteGiteaRepo(repoName)
if (!giteaResult.success) {
console.error('Failed to delete Gitea repo:', giteaResult.error)
// Continue with database deletion even if Gitea fails
}
}
}
// Delete related data in order (due to foreign key constraints)
// Delete agent_runs
await supabase
.from('agent_runs')
.delete()
.eq('project_id', projectId)
// Delete messages
await supabase
.from('messages')
.delete()
.eq('project_id', projectId)
// Delete backlog items
await supabase
.from('backlog_items')
.delete()
.eq('project_id', projectId)
// Delete project activities
await supabase
.from('project_activities')
.delete()
.eq('project_id', projectId)
// Delete AI recommendations
await supabase
.from('ai_recommendations')
.delete()
.eq('project_id', projectId)
// Delete project phases
await supabase
.from('project_phases')
.delete()
.eq('project_id', projectId)
// Delete health snapshots
await supabase
.from('project_health_snapshots')
.delete()
.eq('project_id', projectId)
// Finally, delete the project itself
const { error: deleteError } = await supabase
.from('projects')
.delete()
.eq('id', projectId)
if (deleteError) {
return { success: false, error: deleteError.message }
}
return { success: true }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete project'
}
}
}