refactor: code quality cleanup — remove any types, add logger, rename Kine to Tabata

- Phase 0: Rename all Kine references to Tabata (types, files, imports, i18n, analytics events)
- Phase 1: Add test coverage for tabataProgramStore, workoutProgramStore, and color utils (47 tests)
- Phase 2: Remove all `any` types from production code with proper typed replacements
- Phase 3: Replace ~60 raw console.* calls with __DEV__-gated logger utility
- Phase 4: Verify .DS_Store housekeeping (already clean)

0 TypeScript errors, 583/583 tests passing.
This commit is contained in:
Millian Lamiaux
2026-04-17 18:56:24 +02:00
parent e0e02c4550
commit 791f432334
176 changed files with 16508 additions and 2305 deletions

171
admin-web/lib/ai.ts Normal file
View File

@@ -0,0 +1,171 @@
import { GoogleGenAI } from '@google/genai'
import { supabase } from './supabase'
import type { Database } from './supabase'
type WorkoutRow = Database['public']['Tables']['workouts']['Row']
function getAI(): GoogleGenAI {
const apiKey = process.env['GEMINI_API_KEY']
if (!apiKey) {
throw new Error('GEMINI_API_KEY environment variable is not set')
}
return new GoogleGenAI({ apiKey })
}
export interface GeneratedImage {
buffer: Buffer
mimeType: string
}
export interface VideoGenerationResult {
workoutId: string
trainerId: string
videoUrl: string
videoPath: string
}
export async function generateTrainerAvatar(
prompt: string,
_trainerName: string
): Promise<GeneratedImage[]> {
const config = {
imageConfig: {
aspectRatio: '1:1',
imageSize: '1K',
personGeneration: 'ALLOW_ADULT',
},
responseModalities: ['IMAGE', 'TEXT'],
}
const response = await getAI().models.generateContentStream({
model: 'gemini-3.1-flash-image-preview',
config,
contents: [{ role: 'user', parts: [{ text: prompt }] }],
})
const images: GeneratedImage[] = []
for await (const chunk of response) {
const parts = chunk.candidates?.[0]?.content?.parts
if (!parts) continue
for (const part of parts) {
if (part.inlineData) {
images.push({
buffer: Buffer.from(part.inlineData.data || '', 'base64'),
mimeType: part.inlineData.mimeType || 'image/png',
})
}
}
}
return images
}
export async function saveAvatarToStorage(
trainerId: string,
imageBuffer: Buffer,
filename: string
): Promise<string> {
const path = `${trainerId}/${filename}`
const { error } = await supabase.storage
.from('trainer-avatars')
.upload(path, imageBuffer, {
contentType: 'image/png',
upsert: true,
})
if (error) throw new Error(`Failed to upload avatar: ${error.message}`)
const { data: { publicUrl } } = supabase.storage
.from('trainer-avatars')
.getPublicUrl(path)
return publicUrl
}
export async function generateWorkoutVideo(
workoutId: string,
trainerId: string,
_trainerAvatarUrl: string,
workoutTitle: string,
exercises: { name: string; duration: number }[]
): Promise<VideoGenerationResult> {
const exerciseList = exercises.map(e => e.name).join(', ')
const prompt = `${workoutTitle}. Exercises: ${exerciseList}. The video must loop seamlessly - start and end in the exact same standing position with arms at sides. Person should appear energetic and properly demonstrate each exercise. 16:9 aspect ratio.`
const operation = await getAI().models.generateVideos({
model: 'veo-3.1-fast-generate-preview',
source: { prompt },
config: {
numberOfVideos: 1,
aspectRatio: '16:9',
resolution: '1080p',
personGeneration: 'dont_allow',
durationSeconds: 8,
},
})
let currentOperation = operation
while (!currentOperation.done) {
await new Promise(resolve => setTimeout(resolve, 10000))
currentOperation = await getAI().operations.getVideosOperation({ operation: currentOperation })
}
const videoUri = currentOperation.response?.generatedVideos?.[0]?.video?.uri
if (!videoUri) throw new Error('No video generated')
const response = await fetch(`${videoUri}&key=${process.env['GEMINI_API_KEY']}`)
const arrayBuffer = await response.arrayBuffer()
const videoBuffer = Buffer.from(arrayBuffer)
const videoPath = `${trainerId}/${workoutId}.mp4`
const { error } = await supabase.storage
.from('trainers-videos')
.upload(videoPath, videoBuffer, {
contentType: 'video/mp4',
upsert: true,
})
if (error) throw new Error(`Failed to upload video: ${error.message}`)
const { data: { publicUrl } } = supabase.storage
.from('trainers-videos')
.getPublicUrl(videoPath)
return { workoutId, trainerId, videoUrl: publicUrl, videoPath }
}
export async function isWorkoutInProgram(_workoutId: string): Promise<boolean> {
// Legacy program_workouts table removed — workout programs now use program_tabatas
return false
}
export async function isVideoGeneratedForWorkout(_workoutId: string): Promise<boolean> {
// Legacy program_workouts table removed — video status tracked in workouts table
return false
}
export async function getWorkoutVideoStatus(workoutId: string): Promise<{
hasVideo: boolean
videoUrl: string | null
inProgram: boolean
videoGenerated: boolean
}> {
const workoutResult = await supabase
.from('workouts')
.select('video_url')
.eq('id', workoutId)
.single()
const workoutVideoUrl = (workoutResult.data as WorkoutRow | null)?.video_url ?? null
return {
hasVideo: !!workoutVideoUrl,
videoUrl: workoutVideoUrl,
inProgram: false,
videoGenerated: false,
}
}