diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f3e9dae --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Mission Control Environment Variables +# Copy to .env.local for local development. + +# Core Supabase config (required) +NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-public-key-here +SUPABASE_SERVICE_ROLE_KEY=your-service-role-secret-key-here + +# Gantt-board API integration (required when consuming gantt-board API) +# Docker example: http://gantt-board:3000/api +GANTT_API_BASE_URL=http://localhost:3000/api +GANTT_API_BEARER_TOKEN=replace_with_same_value_as_gantt_machine_token + +# Optional link targets for UI +NEXT_PUBLIC_GANTT_BOARD_URL=http://localhost:3000 +NEXT_PUBLIC_MISSION_CONTROL_URL=http://localhost:3001 +NEXT_PUBLIC_BLOG_BACKUP_URL=http://localhost:3002 diff --git a/.gitignore b/.gitignore index b831ca9..0948a60 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example +!.env.local.example # vercel .vercel diff --git a/README.md b/README.md index 5a68555..187c17a 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ mission-control/ NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= +GANTT_API_BASE_URL= # ex: http://gantt-board:3000/api (Docker network) # Optional NEXT_PUBLIC_GOOGLE_CLIENT_ID= # For Calendar integration @@ -150,6 +151,9 @@ NEXT_PUBLIC_SUPABASE_SITE_URL= NEXT_PUBLIC_GOOGLE_URL= NEXT_PUBLIC_GOOGLE_CALENDAR_URL= NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL= +NEXT_PUBLIC_GANTT_API_BASE_URL= # Client-side override if needed +GANTT_API_BEARER_TOKEN= # Optional server-to-server auth header +GANTT_API_COOKIE= # Optional server-to-server cookie header ``` Website URLs are centralized in `lib/config/sites.ts` and can be overridden via the optional variables above. @@ -170,6 +174,30 @@ npm run build npm start ``` +## CLI Reuse (API Passthrough) + +Mission Control reuses the existing task/project CLI from the sibling `gantt-board` project instead of duplicating command logic. + +- `scripts/task.sh` delegates to `../gantt-board/scripts/task.sh` +- `scripts/project.sh` delegates to `../gantt-board/scripts/project.sh` +- `scripts/update-task-status.js` delegates to `gantt-board` `task.sh update` + +Environment variables: + +- `GANTT_BOARD_DIR` (optional): override path to the `gantt-board` repo root. +- `API_URL` (optional): forwarded to reused CLI so all business logic stays in API endpoints. + +Mission Control web data for tasks/projects/sprints is also read from `gantt-board` API via `GANTT_API_BASE_URL`, so both CLI and web use the same backend contract. + +For separate Docker images, set `GANTT_API_BASE_URL` to the Docker service DNS name (example: `http://gantt-board:3000/api`) on the same Docker network. +If `gantt-board` API auth is required, provide either `GANTT_API_COOKIE` (session cookie) or `GANTT_API_BEARER_TOKEN` as server-to-server auth context. + +Run passthrough contract test: + +```bash +npm run test:cli-contract +``` + ## Deployment The app deploys automatically to Vercel on push to main: diff --git a/app/podcast/page.tsx b/app/podcast/page.tsx new file mode 100644 index 0000000..21172c6 --- /dev/null +++ b/app/podcast/page.tsx @@ -0,0 +1,42 @@ +export default function PodcastPage() { + return ( +
+
+

OpenClaw Daily Digest Podcast

+

+ Daily tech news and insights for developers, delivered as a podcast. +

+ +
+

Subscribe

+
+ + 🎧 +
+

RSS Feed

+

Copy URL to any podcast app

+
+
+
+
+ +
+

About

+
    +
  • • Daily episodes (~5 minutes)
  • +
  • • iOS, AI, coding tools, and entrepreneurship news
  • +
  • • Generated using AI text-to-speech
  • +
  • • Available every morning at 7 AM CST
  • +
+
+ +
+

New episodes are generated automatically from the daily digest.

+
+
+
+ ); +} \ No newline at end of file diff --git a/app/podcast/rss.xml/route.ts b/app/podcast/rss.xml/route.ts new file mode 100644 index 0000000..2647143 --- /dev/null +++ b/app/podcast/rss.xml/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server"; +import { readFileSync, existsSync } from "fs"; +import { join } from "path"; + +const PODCAST_DIR = "/Users/mattbruce/.openclaw/workspace/podcast"; +const RSS_FILE = join(PODCAST_DIR, "rss.xml"); + +export async function GET() { + try { + // Check if RSS file exists + if (!existsSync(RSS_FILE)) { + // Return a default RSS feed if none exists yet + const defaultFeed = ` + + + OpenClaw Daily Digest + https://mission-control-rho-pink.vercel.app/podcast + en-us + © 2026 OpenClaw + OpenClaw + Daily tech news and insights for developers + + + no + + Welcome to OpenClaw Daily Digest Podcast + Your daily tech news podcast is coming soon. Check back for the first episode! + ${new Date().toUTCString()} + welcome + + +`; + + return new NextResponse(defaultFeed, { + headers: { + "Content-Type": "application/rss+xml", + "Cache-Control": "public, max-age=3600", + }, + }); + } + + // Read and serve the RSS file + const rssContent = readFileSync(RSS_FILE, "utf-8"); + + return new NextResponse(rssContent, { + headers: { + "Content-Type": "application/rss+xml", + "Cache-Control": "public, max-age=3600", + }, + }); + } catch (error) { + console.error("Error serving RSS feed:", error); + return NextResponse.json( + { error: "Failed to load RSS feed" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/lib/data/gantt-api.ts b/lib/data/gantt-api.ts new file mode 100644 index 0000000..3033a2b --- /dev/null +++ b/lib/data/gantt-api.ts @@ -0,0 +1,50 @@ +const DEFAULT_GANTT_API_BASE_URL = "http://localhost:3000/api"; + +function normalizeBaseUrl(url: string): string { + return url.replace(/\/+$/, ""); +} + +export function getGanttApiBaseUrl(): string { + const configured = + process.env.GANTT_API_BASE_URL || + process.env.NEXT_PUBLIC_GANTT_API_BASE_URL || + DEFAULT_GANTT_API_BASE_URL; + return normalizeBaseUrl(configured); +} + +function buildAuthHeaders(): HeadersInit { + const headers: Record = {}; + + if (process.env.GANTT_API_BEARER_TOKEN) { + headers.Authorization = `Bearer ${process.env.GANTT_API_BEARER_TOKEN}`; + } + + if (process.env.GANTT_API_COOKIE) { + headers.Cookie = process.env.GANTT_API_COOKIE; + } + + return headers; +} + +export async function fetchGanttApi(endpoint: string): Promise { + const baseUrl = getGanttApiBaseUrl(); + const response = await fetch(`${baseUrl}${endpoint}`, { + method: "GET", + cache: "no-store", + headers: { + Accept: "application/json", + ...buildAuthHeaders(), + }, + }); + + const payload = (await response.json().catch(() => null)) as + | { error?: string; message?: string } + | null; + + if (!response.ok) { + const details = payload?.error || payload?.message || response.statusText; + throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): ${details}`); + } + + return payload as T; +} diff --git a/lib/data/mission.ts b/lib/data/mission.ts index 25a8d3c..e6e0352 100644 --- a/lib/data/mission.ts +++ b/lib/data/mission.ts @@ -1,11 +1,6 @@ -import { getServiceSupabase } from "@/lib/supabase/client"; +import { getGanttTaskUrl } from "@/lib/config/sites"; import { Task, fetchAllTasks } from "./tasks"; import { Project, fetchAllProjects } from "./projects"; -import { getGanttTaskUrl } from "@/lib/config/sites"; - -// ============================================================================ -// Types -// ============================================================================ export interface ProgressMetric { id: string; @@ -45,90 +40,37 @@ export interface MissionProgress { nextSteps: NextStep[]; } -// ============================================================================ -// Constants -// ============================================================================ - -// Target goals const TARGET_APPS = 20; -const TARGET_REVENUE = 10000; // $10K MRR target -const TARGET_TRAVEL_FUND = 50000; // $50K travel fund -const TARGET_RETIREMENT_TASKS = 100; // Tasks completed as proxy for progress - -// iOS-related keywords for identifying app projects +const TARGET_REVENUE = 10000; +const TARGET_TRAVEL_FUND = 50000; +const TARGET_RETIREMENT_TASKS = 100; const IOS_KEYWORDS = ["ios", "app", "swift", "mobile", "iphone", "ipad", "xcode"]; -// ============================================================================ -// Helper Functions -// ============================================================================ - -/** - * Check if a project is iOS-related - */ function isIOSProject(project: Project): boolean { const nameLower = project.name.toLowerCase(); const descLower = (project.description || "").toLowerCase(); - return IOS_KEYWORDS.some(keyword => - nameLower.includes(keyword) || descLower.includes(keyword) - ); + return IOS_KEYWORDS.some((keyword) => nameLower.includes(keyword) || descLower.includes(keyword)); } -/** - * Check if a task is iOS-related - */ function isIOSTask(task: Task): boolean { const titleLower = task.title.toLowerCase(); const descLower = (task.description || "").toLowerCase(); - const hasIOSTag = task.tags.some(tag => - ["ios", "app", "swift", "mobile"].includes(tag.toLowerCase()) - ); - return hasIOSTag || IOS_KEYWORDS.some(keyword => - titleLower.includes(keyword) || descLower.includes(keyword) - ); + const hasIOSTag = task.tags.some((tag) => ["ios", "app", "swift", "mobile"].includes(tag.toLowerCase())); + return hasIOSTag || IOS_KEYWORDS.some((keyword) => titleLower.includes(keyword) || descLower.includes(keyword)); } -/** - * Calculate percentage with bounds - */ function calculatePercentage(current: number, target: number): number { if (target === 0) return 0; return Math.min(100, Math.max(0, Math.round((current / target) * 100))); } -/** - * Format currency - */ -function formatCurrency(amount: number): string { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - maximumFractionDigits: 0, - }).format(amount); +function byUpdatedDesc(a: Task, b: Task): number { + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); } -// ============================================================================ -// Progress Metric Functions -// ============================================================================ - -/** - * Get retirement progress based on completed tasks as a proxy - * In future, this could be based on actual financial data - */ export async function getRetirementProgress(): Promise { - const supabase = getServiceSupabase(); - - // Count completed tasks as a proxy for "progress toward freedom" - const { count: completedTasks, error } = await supabase - .from("tasks") - .select("*", { count: "exact", head: true }) - .eq("status", "done"); - - if (error) { - console.error("Error calculating retirement progress:", error); - } - - const current = completedTasks || 0; - const percentage = calculatePercentage(current, TARGET_RETIREMENT_TASKS); + const tasks = await fetchAllTasks(); + const current = tasks.filter((task) => task.status === "done").length; return { id: "retirement", @@ -136,34 +78,18 @@ export async function getRetirementProgress(): Promise { current, target: TARGET_RETIREMENT_TASKS, unit: "tasks", - percentage, + percentage: calculatePercentage(current, TARGET_RETIREMENT_TASKS), icon: "Target", color: "from-emerald-500 to-teal-500", }; } -/** - * Get iOS portfolio progress - * Counts completed iOS projects and tasks - */ export async function getiOSPortfolioProgress(): Promise { - const [projects, tasks] = await Promise.all([ - fetchAllProjects(), - fetchAllTasks(), - ]); - - // Count iOS projects + const [projects, tasks] = await Promise.all([fetchAllProjects(), fetchAllTasks()]); const iosProjects = projects.filter(isIOSProject); - - // Count completed iOS tasks as additional progress - const completedIOSTasks = tasks.filter(t => - isIOSTask(t) && t.status === "done" - ).length; - - // Weight: 1 project = 1 app, 10 completed tasks = 1 app progress + const completedIOSTasks = tasks.filter((task) => isIOSTask(task) && task.status === "done").length; const taskContribution = Math.floor(completedIOSTasks / 10); const current = Math.min(iosProjects.length + taskContribution, TARGET_APPS); - const percentage = calculatePercentage(current, TARGET_APPS); return { id: "ios-portfolio", @@ -171,42 +97,22 @@ export async function getiOSPortfolioProgress(): Promise { current, target: TARGET_APPS, unit: "apps", - percentage, + percentage: calculatePercentage(current, TARGET_APPS), icon: "Smartphone", color: "from-blue-500 to-cyan-500", }; } -/** - * Get side hustle revenue progress - * Placeholder - will be replaced with real revenue tracking - */ export async function getSideHustleRevenue(): Promise { - // Placeholder: For now, calculate based on completed milestones - // In future, this will pull from actual revenue data - const supabase = getServiceSupabase(); - - const { data: doneTasks, error } = await supabase - .from("tasks") - .select("*") - .eq("status", "done"); - - if (error) { - console.error("Error fetching revenue milestones:", error); - } - - // Filter for tasks with revenue or milestone tags - const milestones = (doneTasks || []).filter(task => { - const tags = (task.tags || []) as string[]; - return tags.some(tag => - tag.toLowerCase() === "revenue" || - tag.toLowerCase() === "milestone" - ); - }); - - // Estimate: each revenue milestone = $500 progress (placeholder logic) - const milestoneCount = milestones.length; - const estimatedRevenue = milestoneCount * 500; + const tasks = await fetchAllTasks(); + const doneTasks = tasks.filter((task) => task.status === "done"); + const milestones = doneTasks.filter((task) => + task.tags.some((tag) => { + const normalized = tag.toLowerCase(); + return normalized === "revenue" || normalized === "milestone"; + }) + ); + const estimatedRevenue = milestones.length * 500; return { id: "revenue", @@ -220,30 +126,10 @@ export async function getSideHustleRevenue(): Promise { }; } -/** - * Get travel fund progress - * Placeholder - will be replaced with actual savings tracking - */ export async function getTravelFundProgress(): Promise { - // Placeholder: Calculate based on "travel" tagged completed tasks - const supabase = getServiceSupabase(); - - const { data: doneTasks, error } = await supabase - .from("tasks") - .select("*") - .eq("status", "done"); - - if (error) { - console.error("Error fetching travel progress:", error); - } - - // Filter for tasks with travel tag - const travelTasks = (doneTasks || []).filter(task => { - const tags = (task.tags || []) as string[]; - return tags.some(tag => tag.toLowerCase() === "travel"); - }); - - // Estimate: each travel task = $500 saved (placeholder) + const tasks = await fetchAllTasks(); + const doneTasks = tasks.filter((task) => task.status === "done"); + const travelTasks = doneTasks.filter((task) => task.tags.some((tag) => tag.toLowerCase() === "travel")); const estimatedSavings = travelTasks.length * 500; return { @@ -258,48 +144,23 @@ export async function getTravelFundProgress(): Promise { }; } -// ============================================================================ -// Milestones Functions -// ============================================================================ - -/** - * Get mission milestones from completed tasks tagged with "milestone" or "mission" - */ export async function getMissionMilestones(): Promise { - const supabase = getServiceSupabase(); - - // Fetch all completed tasks and filter for tags in code - // (Supabase JSON filtering can be tricky with different setups) - const { data, error } = await supabase - .from("tasks") - .select("*") - .eq("status", "done"); + const tasks = await fetchAllTasks(); + const milestoneTasks = tasks + .filter((task) => task.status === "done") + .filter((task) => + task.tags.some((tag) => { + const normalized = tag.toLowerCase(); + return normalized === "milestone" || normalized === "mission"; + }) + ) + .sort(byUpdatedDesc); - if (error) { - console.error("Error fetching mission milestones:", error); - return []; - } - - // Filter for tasks with milestone or mission tags - const milestoneTasks = (data || []).filter(task => { - const tags = (task.tags || []) as string[]; - return tags.some(tag => - tag.toLowerCase() === "milestone" || - tag.toLowerCase() === "mission" - ); - }); - - // Sort by completion date (updated_at) descending - const sortedData = milestoneTasks.sort((a, b) => - new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() - ); - - return sortedData.map((task, index) => { - // Determine category based on tags and title + return milestoneTasks.map((task) => { let category: Milestone["category"] = "business"; const titleLower = task.title.toLowerCase(); - const tagsLower = (task.tags || []).map((t: string) => t.toLowerCase()); - + const tagsLower = task.tags.map((tag) => tag.toLowerCase()); + if (tagsLower.includes("ios") || tagsLower.includes("app") || titleLower.includes("app")) { category = "ios"; } else if (tagsLower.includes("travel") || titleLower.includes("travel")) { @@ -310,7 +171,6 @@ export async function getMissionMilestones(): Promise { category = "financial"; } - // Determine icon based on category const iconMap: Record = { ios: "Smartphone", financial: "DollarSign", @@ -323,74 +183,39 @@ export async function getMissionMilestones(): Promise { id: task.id, title: task.title, description: task.description, - completedAt: task.updated_at, + completedAt: task.updatedAt, category, icon: iconMap[category], }; }); } -// ============================================================================ -// Next Steps Functions -// ============================================================================ - -/** - * Get next mission steps - high priority tasks that advance the mission - */ export async function getNextMissionSteps(): Promise { - const supabase = getServiceSupabase(); - - // Fetch high/urgent priority tasks that are not done - const { data: tasks, error: tasksError } = await supabase - .from("tasks") - .select("*") - .in("priority", ["high", "urgent"]) - .neq("status", "done") - .order("due_date", { ascending: true }) - .limit(6); + const [tasks, projects] = await Promise.all([fetchAllTasks(), fetchAllProjects()]); + const projectMap = new Map(projects.map((project) => [project.id, project.name])); - if (tasksError) { - console.error("Error fetching next mission steps:", tasksError); - return []; - } + const prioritized = tasks + .filter((task) => (task.priority === "high" || task.priority === "urgent") && task.status !== "done") + .sort((a, b) => { + const aDue = a.dueDate ? new Date(a.dueDate).getTime() : Number.MAX_SAFE_INTEGER; + const bDue = b.dueDate ? new Date(b.dueDate).getTime() : Number.MAX_SAFE_INTEGER; + if (aDue !== bDue) return aDue - bDue; + return byUpdatedDesc(a, b); + }) + .slice(0, 6); - // Fetch projects for names - const { data: projects, error: projectsError } = await supabase - .from("projects") - .select("id, name"); - - if (projectsError) { - console.error("Error fetching projects:", projectsError); - } - - const projectMap = new Map((projects || []).map(p => [p.id, p.name])); - - return (tasks || []).map(task => ({ + return prioritized.map((task) => ({ id: task.id, title: task.title, priority: task.priority as "high" | "urgent", - projectName: projectMap.get(task.project_id), - dueDate: task.due_date, + projectName: projectMap.get(task.projectId), + dueDate: task.dueDate, ganttBoardUrl: getGanttTaskUrl(task.id), })); } -// ============================================================================ -// Combined Fetch -// ============================================================================ - -/** - * Fetch all mission progress data in parallel - */ export async function fetchMissionProgress(): Promise { - const [ - retirement, - iosPortfolio, - sideHustleRevenue, - travelFund, - milestones, - nextSteps, - ] = await Promise.all([ + const [retirement, iosPortfolio, sideHustleRevenue, travelFund, milestones, nextSteps] = await Promise.all([ getRetirementProgress(), getiOSPortfolioProgress(), getSideHustleRevenue(), diff --git a/lib/data/projects.ts b/lib/data/projects.ts index dc8e43c..fe020db 100644 --- a/lib/data/projects.ts +++ b/lib/data/projects.ts @@ -1,5 +1,5 @@ -import { getServiceSupabase } from "@/lib/supabase/client"; -import { Task } from "./tasks"; +import { fetchGanttApi } from "@/lib/data/gantt-api"; +import { Task, fetchAllTasks } from "./tasks"; export interface Project { id: string; @@ -30,173 +30,136 @@ export interface ProjectStats { recentTasks: Task[]; } -function toNonEmptyString(value: unknown): string | undefined { - return typeof value === "string" && value.trim().length > 0 ? value : undefined; +interface GanttProjectsResponse { + projects?: Array<{ + id: string; + name: string; + description?: string | null; + color?: string | null; + created_at?: string; + createdAt?: string; + }>; } -function mapProjectRow(row: Record): Project { +interface GanttSprintsResponse { + sprints?: Array<{ + id: string; + name: string; + status: Sprint["status"]; + start_date?: string; + end_date?: string; + startDate?: string; + endDate?: string; + project_id?: string; + projectId?: string; + goal?: string | null; + }>; +} + +interface GanttCurrentSprintResponse { + sprint?: { + id: string; + name: string; + status: Sprint["status"]; + startDate: string; + endDate: string; + projectId: string; + goal?: string | null; + } | null; +} + +function mapProject(project: NonNullable[number]): Project { return { - id: String(row.id ?? ""), - name: toNonEmptyString(row.name) ?? "Untitled Project", - description: toNonEmptyString(row.description), - color: toNonEmptyString(row.color) ?? "#3b82f6", - createdAt: toNonEmptyString(row.created_at) ?? new Date().toISOString(), + id: project.id, + name: project.name, + description: project.description ?? undefined, + color: project.color || "#3b82f6", + createdAt: project.createdAt || project.created_at || new Date().toISOString(), + }; +} + +function mapSprint(sprint: NonNullable[number]): Sprint { + return { + id: sprint.id, + name: sprint.name, + status: sprint.status, + startDate: sprint.startDate || sprint.start_date || new Date().toISOString(), + endDate: sprint.endDate || sprint.end_date || new Date().toISOString(), + projectId: sprint.projectId || sprint.project_id || "", + goal: sprint.goal ?? undefined, }; } -/** - * Fetch all projects from Supabase - */ export async function fetchAllProjects(): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("projects") - .select("*") - .order("created_at", { ascending: true }); - - if (error) { - console.error("Error fetching projects:", error); - throw new Error(`Failed to fetch projects: ${error.message}`); + try { + const response = await fetchGanttApi("/projects"); + return Array.isArray(response.projects) ? response.projects.map(mapProject) : []; + } catch (error) { + console.error("Error fetching projects from gantt-board API:", error); + return []; } - - return (data || []).map((row) => mapProjectRow(row as Record)); } -/** - * Count total projects - */ export async function countProjects(): Promise { - const supabase = getServiceSupabase(); - const { count, error } = await supabase - .from("projects") - .select("*", { count: "exact", head: true }); - - if (error) { - console.error("Error counting projects:", error); - return 0; - } - - return count || 0; + const projects = await fetchAllProjects(); + return projects.length; } -// Sprint status type -const SPRINT_STATUSES = ["planning", "active", "completed", "cancelled"] as const; -type SprintStatus = typeof SPRINT_STATUSES[number]; - -function isSprintStatus(value: unknown): value is SprintStatus { - return typeof value === "string" && SPRINT_STATUSES.includes(value as SprintStatus); -} - -function mapSprintRow(row: Record): Sprint { - return { - id: String(row.id ?? ""), - name: toNonEmptyString(row.name) ?? "Untitled Sprint", - status: isSprintStatus(row.status) ? row.status : "planning", - startDate: toNonEmptyString(row.start_date) ?? new Date().toISOString(), - endDate: toNonEmptyString(row.end_date) ?? new Date().toISOString(), - projectId: String(row.project_id ?? ""), - goal: toNonEmptyString(row.goal), - }; -} - -/** - * Fetch all sprints from Supabase - */ export async function fetchAllSprints(): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("sprints") - .select("*") - .order("start_date", { ascending: false }); - - if (error) { - console.error("Error fetching sprints:", error); + try { + const response = await fetchGanttApi("/sprints"); + return Array.isArray(response.sprints) ? response.sprints.map(mapSprint) : []; + } catch (error) { + console.error("Error fetching sprints from gantt-board API:", error); return []; } - - return (data || []).map((row) => mapSprintRow(row as Record)); } -/** - * Fetch sprints for a specific project - */ export async function fetchProjectSprints(projectId: string): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("sprints") - .select("*") - .eq("project_id", projectId) - .order("start_date", { ascending: false }); - - if (error) { - console.error("Error fetching project sprints:", error); - return []; - } - - return (data || []).map((row) => mapSprintRow(row as Record)); + const sprints = await fetchAllSprints(); + return sprints.filter((sprint) => sprint.projectId === projectId); } -/** - * Fetch active sprint - */ export async function fetchActiveSprint(): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("sprints") - .select("*") - .eq("status", "active") - .maybeSingle(); - - if (error) { - console.error("Error fetching active sprint:", error); + try { + const response = await fetchGanttApi("/sprints/current"); + if (!response.sprint) return null; + return { + id: response.sprint.id, + name: response.sprint.name, + status: response.sprint.status, + startDate: response.sprint.startDate, + endDate: response.sprint.endDate, + projectId: response.sprint.projectId, + goal: response.sprint.goal ?? undefined, + }; + } catch (error) { + console.error("Error fetching current sprint from gantt-board API:", error); return null; } - - return data ? mapSprintRow(data as Record) : null; } -/** - * Count sprints by status - */ export async function countSprintsByStatus(): Promise<{ planning: number; active: number; completed: number; total: number }> { - const supabase = getServiceSupabase(); - - const { data, error } = await supabase - .from("sprints") - .select("status"); - - if (error) { - console.error("Error counting sprints:", error); - return { planning: 0, active: 0, completed: 0, total: 0 }; + const sprints = await fetchAllSprints(); + const counts = { planning: 0, active: 0, completed: 0, total: sprints.length }; + for (const sprint of sprints) { + if (sprint.status === "planning") counts.planning++; + else if (sprint.status === "active") counts.active++; + else if (sprint.status === "completed") counts.completed++; } - - const counts = { planning: 0, active: 0, completed: 0, total: 0 }; - - for (const row of data || []) { - counts.total++; - const status = row.status; - if (status === "planning") counts.planning++; - else if (status === "active") counts.active++; - else if (status === "completed") counts.completed++; - } - return counts; } -/** - * Calculate project statistics - */ export function calculateProjectStats(project: Project, tasks: Task[]): ProjectStats { - const projectTasks = tasks.filter(t => t.projectId === project.id); - const completedTasks = projectTasks.filter(t => t.status === "done"); - const inProgressTasks = projectTasks.filter(t => t.status === "in-progress"); - const urgentTasks = projectTasks.filter(t => t.priority === "urgent" && t.status !== "done"); - const highPriorityTasks = projectTasks.filter(t => t.priority === "high" && t.status !== "done"); - + const projectTasks = tasks.filter((task) => task.projectId === project.id); + const completedTasks = projectTasks.filter((task) => task.status === "done"); + const inProgressTasks = projectTasks.filter((task) => task.status === "in-progress"); + const urgentTasks = projectTasks.filter((task) => task.priority === "urgent" && task.status !== "done"); + const highPriorityTasks = projectTasks.filter((task) => task.priority === "high" && task.status !== "done"); + const totalTasks = projectTasks.length; const progress = totalTasks > 0 ? Math.round((completedTasks.length / totalTasks) * 100) : 0; - - // Get recent tasks (last 5 updated) + const recentTasks = [...projectTasks] .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .slice(0, 5); @@ -213,78 +176,29 @@ export function calculateProjectStats(project: Project, tasks: Task[]): ProjectS }; } -/** - * Fetch all projects with their statistics - */ export async function fetchProjectsWithStats(): Promise { - const supabase = getServiceSupabase(); - - // Fetch projects and tasks in parallel - const [projectsResult, tasksResult] = await Promise.all([ - supabase.from("projects").select("*").order("created_at", { ascending: true }), - supabase.from("tasks").select("*"), - ]); - - if (projectsResult.error) { - console.error("Error fetching projects:", projectsResult.error); - throw new Error(`Failed to fetch projects: ${projectsResult.error.message}`); - } - - const projects = (projectsResult.data || []).map((row) => mapProjectRow(row as Record)); - const tasks = (tasksResult.data || []).map((row) => { - const fallbackDate = new Date().toISOString(); - return { - id: String(row.id ?? ""), - title: toNonEmptyString(row.title) ?? "", - description: toNonEmptyString(row.description), - type: (toNonEmptyString(row.type) as Task["type"]) ?? "task", - status: (toNonEmptyString(row.status) as Task["status"]) ?? "todo", - priority: (toNonEmptyString(row.priority) as Task["priority"]) ?? "medium", - projectId: String(row.project_id ?? ""), - sprintId: toNonEmptyString(row.sprint_id), - createdAt: toNonEmptyString(row.created_at) ?? fallbackDate, - updatedAt: toNonEmptyString(row.updated_at) ?? fallbackDate, - createdById: toNonEmptyString(row.created_by_id), - createdByName: toNonEmptyString(row.created_by_name), - assigneeId: toNonEmptyString(row.assignee_id), - assigneeName: toNonEmptyString(row.assignee_name), - dueDate: toNonEmptyString(row.due_date), - comments: Array.isArray(row.comments) ? row.comments : [], - tags: Array.isArray(row.tags) ? row.tags.filter((tag: unknown): tag is string => typeof tag === "string") : [], - attachments: Array.isArray(row.attachments) ? row.attachments : [], - } as Task; - }); - - return projects.map(project => calculateProjectStats(project, tasks)); + const [projects, tasks] = await Promise.all([fetchAllProjects(), fetchAllTasks()]); + return projects.map((project) => calculateProjectStats(project, tasks)); } -/** - * Get project health status based on various metrics - */ export function getProjectHealth(stats: ProjectStats): { status: "healthy" | "warning" | "critical"; label: string; color: string; } { const { progress, urgentTasks, highPriorityTasks, totalTasks } = stats; - - // Critical: Has urgent tasks or no progress with many open tasks + if (urgentTasks > 0 || (totalTasks > 5 && progress === 0)) { return { status: "critical", label: "Needs Attention", color: "text-red-500" }; } - - // Warning: Has high priority tasks or low progress + if (highPriorityTasks > 2 || (totalTasks > 10 && progress < 30)) { return { status: "warning", label: "At Risk", color: "text-yellow-500" }; } - - // Healthy: Good progress, no urgent issues + return { status: "healthy", label: "On Track", color: "text-green-500" }; } -/** - * Calculate days remaining in sprint - */ export function getSprintDaysRemaining(sprint: Sprint): number { const endDate = new Date(sprint.endDate); const today = new Date(); @@ -292,11 +206,11 @@ export function getSprintDaysRemaining(sprint: Sprint): number { return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } -/** - * Format sprint date range - */ export function formatSprintDateRange(sprint: Sprint): string { const start = new Date(sprint.startDate); const end = new Date(sprint.endDate); - return `${start.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${end.toLocaleDateString("en-US", { month: "short", day: "numeric" })}`; + return `${start.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${end.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + })}`; } diff --git a/lib/data/stats.ts b/lib/data/stats.ts index 7b7af5b..5b1edd6 100644 --- a/lib/data/stats.ts +++ b/lib/data/stats.ts @@ -1,5 +1,5 @@ import { fetchAllProjects, countProjects } from "./projects"; -import { countActiveTasks, countTotalTasks } from "./tasks"; +import { countActiveTasks, countTotalTasks, fetchAllTasks } from "./tasks"; export interface DashboardStats { activeTasksCount: number; @@ -32,24 +32,13 @@ function calculateYearProgress(): { progress: number; day: number; totalDays: nu * For now, this uses a simple heuristic based on done tasks vs total tasks */ async function calculateGoalsProgress(): Promise { - const supabase = (await import("@/lib/supabase/client")).getServiceSupabase(); - - // Get done tasks count - const { count: doneCount, error: doneError } = await supabase - .from("tasks") - .select("*", { count: "exact", head: true }) - .eq("status", "done"); - - // Get total tasks count - const { count: totalCount, error: totalError } = await supabase - .from("tasks") - .select("*", { count: "exact", head: true }); - - if (doneError || totalError || !totalCount || totalCount === 0) { + const tasks = await fetchAllTasks(); + if (tasks.length === 0) { return 0; } - return Math.round(((doneCount || 0) / totalCount) * 100); + const doneCount = tasks.filter((task) => task.status === "done").length; + return Math.round((doneCount / tasks.length) * 100); } /** diff --git a/lib/data/tasks.ts b/lib/data/tasks.ts index f28885f..7a68336 100644 --- a/lib/data/tasks.ts +++ b/lib/data/tasks.ts @@ -1,4 +1,4 @@ -import { getServiceSupabase } from "@/lib/supabase/client"; +import { fetchGanttApi } from "@/lib/data/gantt-api"; export interface Task { id: string; @@ -27,121 +27,10 @@ export interface Task { attachments: unknown[]; } -const TASK_STATUSES: Task["status"][] = ["open", "todo", "blocked", "in-progress", "review", "validate", "archived", "canceled", "done"]; - -function isTaskStatus(value: unknown): value is Task["status"] { - return typeof value === "string" && TASK_STATUSES.includes(value as Task["status"]); +interface GanttTasksResponse { + tasks?: Task[]; } -function toNonEmptyString(value: unknown): string | undefined { - return typeof value === "string" && value.trim().length > 0 ? value : undefined; -} - -function mapTaskRow(row: Record): Task { - const fallbackDate = new Date().toISOString(); - return { - id: String(row.id ?? ""), - title: toNonEmptyString(row.title) ?? "", - description: toNonEmptyString(row.description), - type: (toNonEmptyString(row.type) as Task["type"]) ?? "task", - status: isTaskStatus(row.status) ? row.status : "todo", - priority: (toNonEmptyString(row.priority) as Task["priority"]) ?? "medium", - projectId: String(row.project_id ?? ""), - sprintId: toNonEmptyString(row.sprint_id), - createdAt: toNonEmptyString(row.created_at) ?? fallbackDate, - updatedAt: toNonEmptyString(row.updated_at) ?? fallbackDate, - createdById: toNonEmptyString(row.created_by_id), - createdByName: toNonEmptyString(row.created_by_name), - createdByAvatarUrl: toNonEmptyString(row.created_by_avatar_url), - updatedById: toNonEmptyString(row.updated_by_id), - updatedByName: toNonEmptyString(row.updated_by_name), - updatedByAvatarUrl: toNonEmptyString(row.updated_by_avatar_url), - assigneeId: toNonEmptyString(row.assignee_id), - assigneeName: toNonEmptyString(row.assignee_name), - assigneeEmail: toNonEmptyString(row.assignee_email), - assigneeAvatarUrl: toNonEmptyString(row.assignee_avatar_url), - dueDate: toNonEmptyString(row.due_date), - comments: Array.isArray(row.comments) ? row.comments : [], - tags: Array.isArray(row.tags) ? row.tags.filter((tag): tag is string => typeof tag === "string") : [], - attachments: Array.isArray(row.attachments) ? row.attachments : [], - }; -} - -/** - * Fetch all tasks from Supabase - */ -export async function fetchAllTasks(): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("tasks") - .select("*") - .order("created_at", { ascending: true }); - - if (error) { - console.error("Error fetching tasks:", error); - throw new Error(`Failed to fetch tasks: ${error.message}`); - } - - return (data || []).map((row) => mapTaskRow(row as Record)); -} - -/** - * Fetch active tasks (status != 'done') - */ -export async function fetchActiveTasks(): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("tasks") - .select("*") - .neq("status", "done") - .order("created_at", { ascending: true }); - - if (error) { - console.error("Error fetching active tasks:", error); - throw new Error(`Failed to fetch active tasks: ${error.message}`); - } - - return (data || []).map((row) => mapTaskRow(row as Record)); -} - -/** - * Count active tasks - */ -export async function countActiveTasks(): Promise { - const supabase = getServiceSupabase(); - const { count, error } = await supabase - .from("tasks") - .select("*", { count: "exact", head: true }) - .neq("status", "done"); - - if (error) { - console.error("Error counting active tasks:", error); - return 0; - } - - return count || 0; -} - -/** - * Count total tasks - */ -export async function countTotalTasks(): Promise { - const supabase = getServiceSupabase(); - const { count, error } = await supabase - .from("tasks") - .select("*", { count: "exact", head: true }); - - if (error) { - console.error("Error counting total tasks:", error); - return 0; - } - - return count || 0; -} - -/** - * Get task counts by status - */ export interface TaskStatusCounts { open: number; inProgress: number; @@ -150,135 +39,94 @@ export interface TaskStatusCounts { total: number; } -export async function getTaskStatusCounts(): Promise { - const supabase = getServiceSupabase(); - - // Get all tasks and count by status - const { data, error } = await supabase - .from("tasks") - .select("status"); +function isDoneStatus(status: Task["status"]): boolean { + return status === "done"; +} - if (error) { - console.error("Error fetching task status counts:", error); - return { open: 0, inProgress: 0, review: 0, done: 0, total: 0 }; +function isOpenStatus(status: Task["status"]): boolean { + return status === "open" || status === "todo" || status === "blocked"; +} + +function isReviewStatus(status: Task["status"]): boolean { + return status === "review" || status === "validate"; +} + +function byUpdatedDesc(a: Task, b: Task): number { + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); +} + +function toDateOnly(value: string): string { + return value.split("T")[0]; +} + +async function fetchTasksFromApi(): Promise { + try { + const response = await fetchGanttApi("/tasks?scope=all&include=detail"); + return Array.isArray(response.tasks) ? response.tasks : []; + } catch (error) { + console.error("Error fetching tasks from gantt-board API:", error); + return []; } +} - const counts = { open: 0, inProgress: 0, review: 0, done: 0, total: 0 }; - - for (const row of data || []) { - counts.total++; - const status = row.status; - if (status === "open" || status === "todo" || status === "blocked") { - counts.open++; - } else if (status === "in-progress") { - counts.inProgress++; - } else if (status === "review" || status === "validate") { - counts.review++; - } else if (status === "done") { - counts.done++; - } +export async function fetchAllTasks(): Promise { + return fetchTasksFromApi(); +} + +export async function fetchActiveTasks(): Promise { + const tasks = await fetchTasksFromApi(); + return tasks.filter((task) => !isDoneStatus(task.status)); +} + +export async function countActiveTasks(): Promise { + const tasks = await fetchTasksFromApi(); + return tasks.filter((task) => !isDoneStatus(task.status)).length; +} + +export async function countTotalTasks(): Promise { + const tasks = await fetchTasksFromApi(); + return tasks.length; +} + +export async function getTaskStatusCounts(): Promise { + const tasks = await fetchTasksFromApi(); + const counts: TaskStatusCounts = { open: 0, inProgress: 0, review: 0, done: 0, total: tasks.length }; + + for (const task of tasks) { + if (isOpenStatus(task.status)) counts.open++; + else if (task.status === "in-progress") counts.inProgress++; + else if (isReviewStatus(task.status)) counts.review++; + else if (task.status === "done") counts.done++; } return counts; } -/** - * Count high priority tasks (high or urgent priority, not done) - */ export async function countHighPriorityTasks(): Promise { - const supabase = getServiceSupabase(); - const { count, error } = await supabase - .from("tasks") - .select("*", { count: "exact", head: true }) - .in("priority", ["high", "urgent"]) - .neq("status", "done"); - - if (error) { - console.error("Error counting high priority tasks:", error); - return 0; - } - - return count || 0; + const tasks = await fetchTasksFromApi(); + return tasks.filter((task) => (task.priority === "high" || task.priority === "urgent") && !isDoneStatus(task.status)).length; } -/** - * Count overdue tasks (due date in the past, not done) - */ export async function countOverdueTasks(): Promise { - const supabase = getServiceSupabase(); - const today = new Date().toISOString().split("T")[0]; - - const { count, error } = await supabase - .from("tasks") - .select("*", { count: "exact", head: true }) - .lt("due_date", today) - .neq("status", "done") - .not("due_date", "is", null); - - if (error) { - console.error("Error counting overdue tasks:", error); - return 0; - } - - return count || 0; + const tasks = await fetchTasksFromApi(); + const today = toDateOnly(new Date().toISOString()); + return tasks.filter((task) => Boolean(task.dueDate) && !isDoneStatus(task.status) && toDateOnly(task.dueDate as string) < today).length; } -/** - * Fetch recently updated tasks (last 5) - */ export async function fetchRecentlyUpdatedTasks(limit = 5): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("tasks") - .select("*") - .order("updated_at", { ascending: false }) - .limit(limit); - - if (error) { - console.error("Error fetching recently updated tasks:", error); - return []; - } - - return (data || []).map((row) => mapTaskRow(row as Record)); + const tasks = await fetchTasksFromApi(); + return [...tasks].sort(byUpdatedDesc).slice(0, limit); } -/** - * Fetch recently completed tasks (last 5) - */ export async function fetchRecentlyCompletedTasks(limit = 5): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("tasks") - .select("*") - .eq("status", "done") - .order("updated_at", { ascending: false }) - .limit(limit); - - if (error) { - console.error("Error fetching recently completed tasks:", error); - return []; - } - - return (data || []).map((row) => mapTaskRow(row as Record)); + const tasks = await fetchTasksFromApi(); + return tasks.filter((task) => isDoneStatus(task.status)).sort(byUpdatedDesc).slice(0, limit); } -/** - * Fetch high priority open tasks (top 5) - */ export async function fetchHighPriorityOpenTasks(limit = 5): Promise { - const supabase = getServiceSupabase(); - const { data, error } = await supabase - .from("tasks") - .select("*") - .in("priority", ["high", "urgent"]) - .neq("status", "done") - .order("updated_at", { ascending: false }) - .limit(limit); - - if (error) { - console.error("Error fetching high priority open tasks:", error); - return []; - } - - return (data || []).map((row) => mapTaskRow(row as Record)); + const tasks = await fetchTasksFromApi(); + return tasks + .filter((task) => (task.priority === "high" || task.priority === "urgent") && !isDoneStatus(task.status)) + .sort(byUpdatedDesc) + .slice(0, limit); } diff --git a/package.json b/package.json index a91fe65..b0afb40 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev -p 3001", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test:cli-contract": "bash scripts/tests/reuse-gantt-cli-contract.sh" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/scripts/lib/gantt_cli.sh b/scripts/lib/gantt_cli.sh new file mode 100755 index 0000000..7dbf7cc --- /dev/null +++ b/scripts/lib/gantt_cli.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MISSION_CONTROL_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +GANTT_BOARD_DIR="${GANTT_BOARD_DIR:-$MISSION_CONTROL_ROOT/../gantt-board}" + +resolve_cli_script() { + local script_name="$1" + local script_path="$GANTT_BOARD_DIR/scripts/$script_name" + + if [[ ! -x "$script_path" ]]; then + echo "Missing executable: $script_path" >&2 + echo "Set GANTT_BOARD_DIR to your gantt-board project root." >&2 + exit 1 + fi + + echo "$script_path" +} + +run_gantt_cli() { + local script_name="$1" + shift || true + + local script_path + script_path="$(resolve_cli_script "$script_name")" + + "$script_path" "$@" +} diff --git a/scripts/project.sh b/scripts/project.sh new file mode 100755 index 0000000..b0af2eb --- /dev/null +++ b/scripts/project.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./lib/gantt_cli.sh +source "$SCRIPT_DIR/lib/gantt_cli.sh" + +run_gantt_cli "project.sh" "$@" diff --git a/scripts/task.sh b/scripts/task.sh new file mode 100755 index 0000000..8dcc652 --- /dev/null +++ b/scripts/task.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./lib/gantt_cli.sh +source "$SCRIPT_DIR/lib/gantt_cli.sh" + +run_gantt_cli "task.sh" "$@" diff --git a/scripts/tests/reuse-gantt-cli-contract.sh b/scripts/tests/reuse-gantt-cli-contract.sh new file mode 100755 index 0000000..bf73918 --- /dev/null +++ b/scripts/tests/reuse-gantt-cli-contract.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TMP_DIR="$(mktemp -d)" +MOCK_GANTT_DIR="$TMP_DIR/gantt-board" +MOCK_LOG="$TMP_DIR/delegation.log" + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +mkdir -p "$MOCK_GANTT_DIR/scripts" +touch "$MOCK_LOG" + +cat > "$MOCK_GANTT_DIR/scripts/task.sh" <<'MOCK_TASK' +#!/bin/bash +set -euo pipefail +echo "task.sh $*" >> "${MOCK_DELEGATION_LOG:?}" +MOCK_TASK + +cat > "$MOCK_GANTT_DIR/scripts/project.sh" <<'MOCK_PROJECT' +#!/bin/bash +set -euo pipefail +echo "project.sh $*" >> "${MOCK_DELEGATION_LOG:?}" +MOCK_PROJECT + +chmod +x "$MOCK_GANTT_DIR/scripts/task.sh" "$MOCK_GANTT_DIR/scripts/project.sh" + +export MOCK_DELEGATION_LOG="$MOCK_LOG" +export GANTT_BOARD_DIR="$MOCK_GANTT_DIR" + +bash "$ROOT_DIR/scripts/task.sh" list --json +bash "$ROOT_DIR/scripts/project.sh" create --name "Demo" +TASK_ID="65bc8c5a-212a-4532-a027-ea424e62900e" \ +TASK_STATUS="review" \ +node "$ROOT_DIR/scripts/update-task-status.js" + +if ! grep -F "task.sh list --json" "$MOCK_LOG" >/dev/null 2>&1; then + echo "Expected task wrapper to delegate to gantt-board task.sh" >&2 + cat "$MOCK_LOG" >&2 + exit 1 +fi + +if ! grep -F "project.sh create --name Demo" "$MOCK_LOG" >/dev/null 2>&1; then + echo "Expected project wrapper to delegate to gantt-board project.sh" >&2 + cat "$MOCK_LOG" >&2 + exit 1 +fi + +if ! grep -F "task.sh update 65bc8c5a-212a-4532-a027-ea424e62900e --status review" "$MOCK_LOG" >/dev/null 2>&1; then + echo "Expected update-task-status.js to delegate to gantt-board task.sh update" >&2 + cat "$MOCK_LOG" >&2 + exit 1 +fi + +# No direct database credentials/queries in scripts +if rg -n --glob '!scripts/tests/*' "SUPABASE_SERVICE_ROLE_KEY|createClient\\(|rest/v1|from\\('tasks'\\)|from\\(\"tasks\"\\)" "$ROOT_DIR/scripts" >/dev/null 2>&1; then + echo "Direct DB references found under scripts/. CLI must remain API passthrough only." >&2 + rg -n --glob '!scripts/tests/*' "SUPABASE_SERVICE_ROLE_KEY|createClient\\(|rest/v1|from\\('tasks'\\)|from\\(\"tasks\"\\)" "$ROOT_DIR/scripts" >&2 || true + exit 1 +fi + +echo "reuse-gantt-cli-contract: OK" diff --git a/scripts/update-task-status.js b/scripts/update-task-status.js index 74bd306..a1d343d 100644 --- a/scripts/update-task-status.js +++ b/scripts/update-task-status.js @@ -1,28 +1,28 @@ -const { createClient } = require('@supabase/supabase-js'); +#!/usr/bin/env node -const supabaseUrl = 'https://qnatchrjlpehiijwtreh.supabase.co'; -const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE2NDA0MzYsImV4cCI6MjA4NzIxNjQzNn0.47XOMrQBzcQEh71phQflPoO4v79Jk3rft7BC72KHDvA'; +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); -const supabase = createClient(supabaseUrl, supabaseKey); +const taskId = process.env.TASK_ID || process.argv[2]; +const status = process.env.TASK_STATUS || process.argv[3] || "review"; +const ganttBoardDir = + process.env.GANTT_BOARD_DIR || + path.resolve(__dirname, "..", "..", "..", "gantt-board"); +const taskCliPath = path.join(ganttBoardDir, "scripts", "task.sh"); -async function updateTaskStatus() { - const taskId = '65bc8c5a-212a-4532-a027-ea424e62900e'; - - const { data, error } = await supabase - .from('tasks') - .update({ - status: 'review', - updated_at: new Date().toISOString() - }) - .eq('id', taskId) - .select(); - - if (error) { - console.error('Error updating task:', error); - process.exit(1); - } - - console.log('Task updated successfully:', data); +if (!taskId) { + console.error("Usage: TASK_ID= node scripts/update-task-status.js [taskId] [status]"); + process.exit(1); } -updateTaskStatus(); +const result = spawnSync(taskCliPath, ["update", taskId, "--status", status], { + stdio: "inherit", + env: process.env, +}); + +if (result.error) { + console.error("Failed to execute gantt-board task CLI:", result.error.message); + process.exit(1); +} + +process.exit(result.status ?? 1);