AeThex-OS/server/gameforge-routes.ts
MrPiglr b3c308b2c8 Add functional marketplace modules, bottom nav bar, root terminal, arcade games
- ModuleManager: Central tracking for installed marketplace modules
- DataAnalyzerWidget: Real-time CPU/RAM/Battery/Storage widget (unlocked by Data Analyzer module)
- BottomNavBar: Navigation bar for Projects/Chat/Marketplace/Settings
- RootShell: Real root command execution utility
- TerminalActivity: Full root shell with neofetch, sysinfo, real Linux commands
- Terminal Pro module: Adds aliases (ll, la, h), command history
- ArcadeActivity + SnakeGame: Pixel Arcade module unlocks retro games
- fade_in/fade_out animations for smooth transitions
2026-02-18 22:03:50 -07:00

926 lines
25 KiB
TypeScript

/**
* GameForge Routes
* API endpoints for game development project management
* Ported from aethex-forge
*/
import { Router, Request, Response } from "express";
import { supabase } from "./supabase.js";
import { requireAuth } from "./auth.js";
const router = Router();
// Helper to get user ID from session
function getUserId(req: Request): string | null {
return (req.session as any)?.userId || null;
}
// ==================== PROJECTS ROUTES ====================
/**
* GET /api/gameforge/projects - List all projects
* GET /api/gameforge/projects?id=xxx - Get single project
*/
router.get("/projects", requireAuth, async (req: Request, res: Response) => {
try {
const { id, status, platform, limit = 50, offset = 0 } = req.query;
if (id) {
// Get single project with full details
const { data, error } = await supabase
.from("gameforge_projects")
.select(`
*,
gameforge_team_members(*)
`)
.eq("id", id)
.single();
if (error) throw error;
if (!data) return res.status(404).json({ error: "Project not found" });
return res.json(data);
}
// List all projects with filters
let query = supabase
.from("gameforge_projects")
.select(`
id,
name,
description,
status,
platform,
genre,
target_release_date,
actual_release_date,
team_size,
budget,
current_spend,
lead_id,
created_at
`, { count: "exact" });
if (status) query = query.eq("status", status as string);
if (platform) query = query.eq("platform", platform as string);
const { data, error, count } = await query
.order("created_at", { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1);
if (error) throw error;
res.json({
projects: data || [],
total: count || 0,
limit: Number(limit),
offset: Number(offset)
});
} catch (err: any) {
console.error("[GameForge Projects] List error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* POST /api/gameforge/projects - Create a new project
*/
router.post("/projects", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const {
name,
description,
platform,
genre,
target_release_date,
budget,
repository_url,
documentation_url
} = req.body;
if (!name || !platform) {
return res.status(400).json({ error: "name and platform are required" });
}
const { data, error } = await supabase
.from("gameforge_projects")
.insert({
name,
description,
status: "planning",
lead_id: userId,
platform,
genre: genre || [],
target_release_date,
budget,
repository_url,
documentation_url
})
.select()
.single();
if (error) throw error;
res.status(201).json(data);
} catch (err: any) {
console.error("[GameForge Projects] Create error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* PUT /api/gameforge/projects/:id - Update a project
*/
router.put("/projects/:id", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { id } = req.params;
const {
name,
description,
status,
platform,
genre,
target_release_date,
actual_release_date,
budget,
current_spend,
repository_url,
documentation_url
} = req.body;
// Verify user is project lead
const { data: project, error: checkError } = await supabase
.from("gameforge_projects")
.select("lead_id")
.eq("id", id)
.single();
if (checkError || !project) {
return res.status(404).json({ error: "Project not found" });
}
if (project.lead_id !== userId) {
return res.status(403).json({ error: "Only project lead can update" });
}
const updateData: any = {};
if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (status !== undefined) updateData.status = status;
if (platform !== undefined) updateData.platform = platform;
if (genre !== undefined) updateData.genre = genre;
if (target_release_date !== undefined) updateData.target_release_date = target_release_date;
if (actual_release_date !== undefined) updateData.actual_release_date = actual_release_date;
if (budget !== undefined) updateData.budget = budget;
if (current_spend !== undefined) updateData.current_spend = current_spend;
if (repository_url !== undefined) updateData.repository_url = repository_url;
if (documentation_url !== undefined) updateData.documentation_url = documentation_url;
const { data, error } = await supabase
.from("gameforge_projects")
.update(updateData)
.eq("id", id)
.select()
.single();
if (error) throw error;
res.json(data);
} catch (err: any) {
console.error("[GameForge Projects] Update error:", err);
res.status(500).json({ error: err.message });
}
});
// ==================== TEAM MEMBERS ROUTES ====================
/**
* GET /api/gameforge/team - List team members
*/
router.get("/team", requireAuth, async (req: Request, res: Response) => {
try {
const { user_id, project_id, role, limit = 50, offset = 0 } = req.query;
let query = supabase
.from("gameforge_team_members")
.select("*", { count: "exact" });
if (user_id) {
const { data, error } = await supabase
.from("gameforge_team_members")
.select("*")
.eq("user_id", user_id)
.single();
if (error && error.code !== "PGRST116") throw error;
return res.json({ member: data });
}
if (project_id) query = query.contains("project_ids", [project_id]);
if (role) query = query.eq("role", role as string);
query = query.eq("is_active", true);
const { data, error, count } = await query
.order("joined_date", { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1);
if (error) throw error;
res.json({
members: data || [],
total: count || 0,
limit: Number(limit),
offset: Number(offset)
});
} catch (err: any) {
console.error("[GameForge Team] List error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* POST /api/gameforge/team - Add team member
*/
router.post("/team", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const {
user_id,
role,
position,
contract_type,
hourly_rate,
skills,
bio,
project_ids
} = req.body;
if (!user_id || !role) {
return res.status(400).json({ error: "user_id and role are required" });
}
const { data, error } = await supabase
.from("gameforge_team_members")
.insert({
user_id,
role,
position,
contract_type: contract_type || "contractor",
hourly_rate,
skills: skills || [],
bio,
project_ids: project_ids || [],
is_active: true
})
.select()
.single();
if (error) throw error;
res.status(201).json(data);
} catch (err: any) {
console.error("[GameForge Team] Create error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* PUT /api/gameforge/team/:id - Update team member
*/
router.put("/team/:id", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { id } = req.params;
const {
role,
position,
contract_type,
hourly_rate,
skills,
bio,
project_ids,
is_active
} = req.body;
const updateData: any = {};
if (role !== undefined) updateData.role = role;
if (position !== undefined) updateData.position = position;
if (contract_type !== undefined) updateData.contract_type = contract_type;
if (hourly_rate !== undefined) updateData.hourly_rate = hourly_rate;
if (skills !== undefined) updateData.skills = skills;
if (bio !== undefined) updateData.bio = bio;
if (project_ids !== undefined) updateData.project_ids = project_ids;
if (is_active !== undefined) {
updateData.is_active = is_active;
if (is_active === false) updateData.left_date = new Date().toISOString();
}
const { data, error } = await supabase
.from("gameforge_team_members")
.update(updateData)
.eq("id", id)
.select()
.single();
if (error) throw error;
res.json(data);
} catch (err: any) {
console.error("[GameForge Team] Update error:", err);
res.status(500).json({ error: err.message });
}
});
// ==================== BUILDS ROUTES ====================
/**
* GET /api/gameforge/builds - List builds for a project
*/
router.get("/builds", requireAuth, async (req: Request, res: Response) => {
try {
const { id, project_id, build_type, limit = 50, offset = 0 } = req.query;
if (id) {
const { data, error } = await supabase
.from("gameforge_builds")
.select(`
*,
gameforge_projects(id, name, platform)
`)
.eq("id", id)
.single();
if (error) throw error;
if (!data) return res.status(404).json({ error: "Build not found" });
return res.json(data);
}
if (!project_id) {
return res.status(400).json({ error: "project_id is required" });
}
let query = supabase
.from("gameforge_builds")
.select(`
*,
gameforge_projects(id, name)
`, { count: "exact" })
.eq("project_id", project_id);
if (build_type) query = query.eq("build_type", build_type as string);
const { data, error, count } = await query
.order("release_date", { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1);
if (error) throw error;
res.json({
builds: data || [],
total: count || 0,
limit: Number(limit),
offset: Number(offset)
});
} catch (err: any) {
console.error("[GameForge Builds] List error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* POST /api/gameforge/builds - Create a new build
*/
router.post("/builds", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const {
project_id,
version,
build_type,
download_url,
changelog,
file_size,
target_platforms
} = req.body;
if (!project_id || !version || !build_type) {
return res.status(400).json({ error: "project_id, version, and build_type are required" });
}
// Verify user is project lead
const { data: project } = await supabase
.from("gameforge_projects")
.select("lead_id")
.eq("id", project_id)
.single();
if (project?.lead_id !== userId) {
return res.status(403).json({ error: "Only project lead can create builds" });
}
const { data, error } = await supabase
.from("gameforge_builds")
.insert({
project_id,
version,
build_type,
download_url,
changelog,
file_size,
target_platforms: target_platforms || [],
created_by: userId
})
.select()
.single();
if (error) throw error;
res.status(201).json(data);
} catch (err: any) {
console.error("[GameForge Builds] Create error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* PUT /api/gameforge/builds/:id - Update a build
*/
router.put("/builds/:id", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { id } = req.params;
const { version, build_type, download_url, changelog, file_size } = req.body;
// Verify user is project lead
const { data: build } = await supabase
.from("gameforge_builds")
.select("project_id, gameforge_projects(lead_id)")
.eq("id", id)
.single();
if ((build?.gameforge_projects as any)?.lead_id !== userId) {
return res.status(403).json({ error: "Only project lead can update builds" });
}
const updateData: any = {};
if (version !== undefined) updateData.version = version;
if (build_type !== undefined) updateData.build_type = build_type;
if (download_url !== undefined) updateData.download_url = download_url;
if (changelog !== undefined) updateData.changelog = changelog;
if (file_size !== undefined) updateData.file_size = file_size;
const { data, error } = await supabase
.from("gameforge_builds")
.update(updateData)
.eq("id", id)
.select()
.single();
if (error) throw error;
res.json(data);
} catch (err: any) {
console.error("[GameForge Builds] Update error:", err);
res.status(500).json({ error: err.message });
}
});
// ==================== SPRINTS ROUTES ====================
/**
* GET /api/gameforge/sprints - List sprints for a project
*/
router.get("/sprints", requireAuth, async (req: Request, res: Response) => {
try {
const { project_id, phase, status, limit = 50, offset = 0 } = req.query;
if (!project_id) {
return res.status(400).json({ error: "project_id is required" });
}
let query = supabase
.from("gameforge_sprints")
.select("*", { count: "exact" })
.eq("project_id", project_id);
if (phase) query = query.eq("phase", phase as string);
if (status) query = query.eq("status", status as string);
const { data, error, count } = await query
.order("sprint_number", { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1);
if (error) throw error;
res.json({
sprints: data || [],
total: count || 0,
limit: Number(limit),
offset: Number(offset)
});
} catch (err: any) {
console.error("[GameForge Sprints] List error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* POST /api/gameforge/sprints - Create a new sprint
*/
router.post("/sprints", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const {
project_id,
sprint_number,
title,
description,
goal,
start_date,
end_date,
planned_velocity
} = req.body;
if (!project_id || !sprint_number || !title) {
return res.status(400).json({ error: "project_id, sprint_number, and title are required" });
}
// Verify user is project lead
const { data: project } = await supabase
.from("gameforge_projects")
.select("lead_id")
.eq("id", project_id)
.single();
if (project?.lead_id !== userId) {
return res.status(403).json({ error: "Only project lead can create sprints" });
}
const { data, error } = await supabase
.from("gameforge_sprints")
.insert({
project_id,
sprint_number,
title,
description,
goal,
start_date,
end_date,
planned_velocity,
created_by: userId
})
.select()
.single();
if (error) throw error;
res.status(201).json(data);
} catch (err: any) {
console.error("[GameForge Sprints] Create error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* PUT /api/gameforge/sprints/:id - Update a sprint
*/
router.put("/sprints/:id", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { id } = req.params;
const {
title,
description,
phase,
status,
goal,
start_date,
end_date,
planned_velocity,
actual_velocity
} = req.body;
// Verify user is project lead
const { data: sprint } = await supabase
.from("gameforge_sprints")
.select("project_id, gameforge_projects(lead_id)")
.eq("id", id)
.single();
if ((sprint?.gameforge_projects as any)?.lead_id !== userId) {
return res.status(403).json({ error: "Only project lead can update sprints" });
}
const updateData: any = {};
if (title !== undefined) updateData.title = title;
if (description !== undefined) updateData.description = description;
if (phase !== undefined) updateData.phase = phase;
if (status !== undefined) updateData.status = status;
if (goal !== undefined) updateData.goal = goal;
if (start_date !== undefined) updateData.start_date = start_date;
if (end_date !== undefined) updateData.end_date = end_date;
if (planned_velocity !== undefined) updateData.planned_velocity = planned_velocity;
if (actual_velocity !== undefined) updateData.actual_velocity = actual_velocity;
const { data, error } = await supabase
.from("gameforge_sprints")
.update(updateData)
.eq("id", id)
.select()
.single();
if (error) throw error;
res.json(data);
} catch (err: any) {
console.error("[GameForge Sprints] Update error:", err);
res.status(500).json({ error: err.message });
}
});
// ==================== TASKS ROUTES ====================
/**
* GET /api/gameforge/tasks - List tasks
*/
router.get("/tasks", requireAuth, async (req: Request, res: Response) => {
try {
const { sprint_id, project_id, status, assigned_to, limit = 100, offset = 0 } = req.query;
let query = supabase
.from("gameforge_tasks")
.select("*", { count: "exact" });
if (sprint_id) query = query.eq("sprint_id", sprint_id);
if (project_id) query = query.eq("project_id", project_id);
if (status) query = query.eq("status", status as string);
if (assigned_to) query = query.eq("assigned_to", assigned_to);
const { data, error, count } = await query
.order("created_at", { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1);
if (error) throw error;
res.json({
tasks: data || [],
total: count || 0,
limit: Number(limit),
offset: Number(offset)
});
} catch (err: any) {
console.error("[GameForge Tasks] List error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* POST /api/gameforge/tasks - Create a task
*/
router.post("/tasks", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const {
sprint_id,
project_id,
title,
description,
priority,
estimated_hours,
assigned_to,
due_date
} = req.body;
if (!project_id || !title) {
return res.status(400).json({ error: "project_id and title are required" });
}
const { data, error } = await supabase
.from("gameforge_tasks")
.insert({
sprint_id: sprint_id || null,
project_id,
title,
description,
priority: priority || "medium",
estimated_hours,
assigned_to: assigned_to || null,
created_by: userId,
due_date: due_date || null,
status: "todo"
})
.select()
.single();
if (error) throw error;
res.status(201).json(data);
} catch (err: any) {
console.error("[GameForge Tasks] Create error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* PUT /api/gameforge/tasks/:id - Update a task
*/
router.put("/tasks/:id", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { id } = req.params;
const {
title,
description,
status,
priority,
estimated_hours,
actual_hours,
assigned_to,
due_date
} = req.body;
// Verify user can edit (task creator, assignee, or project lead)
const { data: task, error: taskError } = await supabase
.from("gameforge_tasks")
.select("project_id, assigned_to, created_by")
.eq("id", id)
.single();
if (taskError || !task) {
return res.status(404).json({ error: "Task not found" });
}
const { data: project } = await supabase
.from("gameforge_projects")
.select("lead_id")
.eq("id", task.project_id)
.single();
if (
task.assigned_to !== userId &&
task.created_by !== userId &&
project?.lead_id !== userId
) {
return res.status(403).json({ error: "No permission to edit task" });
}
const updateData: any = {};
if (title !== undefined) updateData.title = title;
if (description !== undefined) updateData.description = description;
if (status !== undefined) {
updateData.status = status;
if (status === "done") updateData.completed_at = new Date().toISOString();
else updateData.completed_at = null;
}
if (priority !== undefined) updateData.priority = priority;
if (estimated_hours !== undefined) updateData.estimated_hours = estimated_hours;
if (actual_hours !== undefined) updateData.actual_hours = actual_hours;
if (assigned_to !== undefined) updateData.assigned_to = assigned_to;
if (due_date !== undefined) updateData.due_date = due_date;
const { data, error } = await supabase
.from("gameforge_tasks")
.update(updateData)
.eq("id", id)
.select()
.single();
if (error) throw error;
res.json(data);
} catch (err: any) {
console.error("[GameForge Tasks] Update error:", err);
res.status(500).json({ error: err.message });
}
});
// ==================== METRICS ROUTES ====================
/**
* GET /api/gameforge/metrics - Get project metrics
*/
router.get("/metrics", requireAuth, async (req: Request, res: Response) => {
try {
const { project_id, metric_type, limit = 50, offset = 0 } = req.query;
if (!project_id) {
return res.status(400).json({ error: "project_id is required" });
}
let query = supabase
.from("gameforge_metrics")
.select("*", { count: "exact" })
.eq("project_id", project_id);
if (metric_type) query = query.eq("metric_type", metric_type as string);
const { data, error, count } = await query
.order("metric_date", { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1);
if (error) throw error;
res.json({
metrics: data || [],
total: count || 0,
limit: Number(limit),
offset: Number(offset)
});
} catch (err: any) {
console.error("[GameForge Metrics] List error:", err);
res.status(500).json({ error: err.message });
}
});
/**
* POST /api/gameforge/metrics - Record project metrics
*/
router.post("/metrics", requireAuth, async (req: Request, res: Response) => {
try {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const {
project_id,
metric_type,
metric_date,
velocity,
hours_logged,
team_size_avg,
bugs_found,
bugs_fixed,
build_count,
days_from_planned_to_release,
on_schedule,
budget_allocated,
budget_spent
} = req.body;
if (!project_id || !metric_type) {
return res.status(400).json({ error: "project_id and metric_type are required" });
}
// Verify user is project lead
const { data: project } = await supabase
.from("gameforge_projects")
.select("lead_id")
.eq("id", project_id)
.single();
if (project?.lead_id !== userId) {
return res.status(403).json({ error: "Only project lead can record metrics" });
}
const { data, error } = await supabase
.from("gameforge_metrics")
.insert({
project_id,
metric_type,
metric_date: metric_date || new Date().toISOString(),
velocity,
hours_logged,
team_size_avg,
bugs_found,
bugs_fixed,
build_count,
days_from_planned_to_release,
on_schedule,
budget_allocated,
budget_spent
})
.select()
.single();
if (error) throw error;
res.status(201).json(data);
} catch (err: any) {
console.error("[GameForge Metrics] Create error:", err);
res.status(500).json({ error: err.message });
}
});
export default router;