Add phase persistence to Supabase

- Add phase_metadata column to projects table
- Update database types with phase_metadata field
- Implement useProjectPhase hook with persistence
- Add setPhaseStatus and addActivity functions
- Real-time subscription for phase updates

🤖 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-14 12:33:00 +01:00
parent 38c081527b
commit 54e05173e0
2 changed files with 190 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import { createClient } from '@/lib/supabase/client'
import type { Phase } from '@/components/ui/progress-timeline'
import type { Activity } from '@/components/ui/activity-feed'
export interface ProjectPhaseState {
projectId: string
currentPhase: Phase
completedPhases: Phase[]
phaseStatus: 'not_started' | 'in_progress' | 'completed' | 'blocked' | 'failed'
activities: Activity[]
metrics: {
totalDuration: number
phaseDurations: Record<Phase, number>
iterationCount: number
}
}
export function useProjectPhase(projectId: string) {
const [state, setState] = useState<ProjectPhaseState | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const fetchProjectState = useCallback(async () => {
const supabase = createClient()
try {
const { data: project, error: projectError } = await supabase
.from('projects')
.select('*')
.eq('id', projectId)
.single()
if (projectError) throw projectError
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const metadata = (project as any)?.phase_metadata as ProjectPhaseState | undefined
setState({
projectId,
currentPhase: metadata?.currentPhase || 'think',
completedPhases: metadata?.completedPhases || [],
phaseStatus: metadata?.phaseStatus || 'not_started',
activities: metadata?.activities || [],
metrics: metadata?.metrics || {
totalDuration: 0,
phaseDurations: {} as Record<Phase, number>,
iterationCount: 0
}
})
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch project'))
} finally {
setLoading(false)
}
}, [projectId])
useEffect(() => {
fetchProjectState()
const supabase = createClient()
const channel = supabase
.channel(`project:${projectId}`)
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'projects',
filter: `id=eq.${projectId}`
}, (payload) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const metadata = (payload.new as any)?.phase_metadata as ProjectPhaseState | undefined
if (metadata) {
setState(prev => prev ? { ...prev, ...metadata, projectId } : null)
}
})
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [projectId, fetchProjectState])
const transitionPhase = useCallback(async (newPhase: Phase) => {
if (!state) return
const supabase = createClient()
const newActivity: Activity = {
id: crypto.randomUUID(),
type: 'phase_change',
message: `Transitioned to ${newPhase} phase`,
timestamp: new Date()
}
const updatedState: ProjectPhaseState = {
...state,
currentPhase: newPhase,
completedPhases: state.currentPhase !== newPhase
? [...state.completedPhases, state.currentPhase]
: state.completedPhases,
phaseStatus: 'in_progress',
activities: [newActivity, ...state.activities].slice(0, 50)
}
// Optimistic update
setState(updatedState)
// Persist to database
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: updateError } = await (supabase as any)
.from('projects')
.update({ phase_metadata: updatedState })
.eq('id', projectId)
if (updateError) {
console.error('Failed to persist phase:', updateError)
// Revert on error
setState(state)
}
}, [state, projectId])
const setPhaseStatus = useCallback(async (status: ProjectPhaseState['phaseStatus']) => {
if (!state) return
const supabase = createClient()
const updatedState: ProjectPhaseState = {
...state,
phaseStatus: status
}
setState(updatedState)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: updateError } = await (supabase as any)
.from('projects')
.update({ phase_metadata: updatedState })
.eq('id', projectId)
if (updateError) {
console.error('Failed to update phase status:', updateError)
setState(state)
}
}, [state, projectId])
const addActivity = useCallback(async (activity: Omit<Activity, 'id' | 'timestamp'>) => {
if (!state) return
const supabase = createClient()
const newActivity: Activity = {
...activity,
id: crypto.randomUUID(),
timestamp: new Date()
}
const updatedState: ProjectPhaseState = {
...state,
activities: [newActivity, ...state.activities].slice(0, 50)
}
setState(updatedState)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: updateError } = await (supabase as any)
.from('projects')
.update({ phase_metadata: updatedState })
.eq('id', projectId)
if (updateError) {
console.error('Failed to add activity:', updateError)
setState(state)
}
}, [state, projectId])
return {
state,
loading,
error,
transitionPhase,
setPhaseStatus,
addActivity,
refresh: fetchProjectState
}
}

View File

@@ -98,6 +98,7 @@ export interface Database {
tech_stack: string[]
platform: string | null
status: 'active' | 'archived' | 'paused'
phase_metadata: Json | null
created_at: string
updated_at: string
}
@@ -111,6 +112,7 @@ export interface Database {
tech_stack?: string[]
platform?: string | null
status?: 'active' | 'archived' | 'paused'
phase_metadata?: Json | null
created_at?: string
updated_at?: string
}
@@ -124,6 +126,7 @@ export interface Database {
tech_stack?: string[]
platform?: string | null
status?: 'active' | 'archived' | 'paused'
phase_metadata?: Json | null
created_at?: string
updated_at?: string
}